Fine-Tuning a Pre-Trained LLM for Spam Classification
train_classifier_simple nests a batch loop inside an epoch loop, and each batch resets gradients, computes loss, backpropagates, and updates weights.
Reading the Function as a Program
This article works from train_classifier_simple, a function used to fine-tune a model to classify spam. Its job is to train a model on a training dataset and to evaluate it on a validation dataset as training proceeds. Instead of memorizing the function's description, treat it as a program you can trace. Ask two questions as you read: which inputs drive the loops, and which outputs does the function hand back to its caller?
What do you think happens?
The function has two loops, and one of them contains a conditional check. What is the counter global_step compared against, and where is it incremented?
Reveal answer
global_step is compared against eval_freq using the remainder operator, and it is incremented inside the batch loop after each weight update.
The check if global_step % eval_freq == 0 decides whether evaluation runs. The counter is incremented by one after every batch's update, so it keeps counting across all epochs. Keep these two facts in mind as you trace the function below.
Inputs and Outputs of the Function
The function takes the model, the two data loaders (one for training and one for validation), an optimizer, the device to run on, and four control values: num_epochs, eval_freq, and eval_iter. The first group supplies the things to be trained and the data to train on. The control values set how long training runs and how often it pauses to measure performance.
The function returns four lists and a counter. The lists record training loss, validation loss, training accuracy, and validation accuracy at each evaluation point. The counter, examples_seen, records how many training examples the model has processed in total. Together these outputs let you follow training progress over time, because each list entry corresponds to one evaluation point during the run.
The Epoch Loop and the Batch Loop
The function contains two loops, one inside the other. The outer loop, for epoch in range(num_epochs), runs once per epoch. At the start of each epoch the function calls model.train(), which puts the model in training mode. The inner loop, for input_batch, target_batch in train_loader, runs once per batch of training data. Each batch passes through the same sequence of steps, and only after the inner loop finishes does the outer loop move on to the next epoch.
The loops nest in a fixed way: the outer loop controls how many full passes over the data occur, and the inner loop controls how many batches make up each pass. The batch body is the only place where weights change during training. Counting the work is therefore simple multiplication: the total number of batch passes equals num_epochs times the number of batches in the training loader.
Tracing One Batch Step by Step
Each batch runs four training steps in a fixed order. First, optimizer.zero_grad() resets the loss gradients left over from the previous batch iteration. Second, calc_loss_batch computes the loss for the current input and target batch. Third, loss.backward() calculates the loss gradients. Fourth, optimizer.step() updates the model weights using those gradients. The reset sits at the top of the batch body because it clears the gradients from the previous batch before new ones are calculated.
Read the diagram as the path a single batch follows. Every batch goes through the four weight-update steps. Only some batches reach the evaluation step, and the diagram shows that choice as a decision rather than a fixed step. The evaluation branch returns to the batch check, so training continues after an evaluation rather than restarting.
After the weight update, the function performs two bookkeeping actions. It adds the number of rows in the current input batch to examples_seen, using input_batch.shape[0], and it increments global_step by one. Because examples_seen counts examples rather than tokens, the source notes this as a change from the earlier pretraining code. The counter global_step is what later decides whether an evaluation happens.
Counting Steps with global_step and eval_freq
The check if global_step % eval_freq == 0 controls how often the model is evaluated. The % operator gives the remainder after division, so the condition is true whenever global_step is an exact multiple of eval_freq. The value eval_freq is therefore a count of training steps between evaluations, not a count of epochs. Since global_step starts at -1 and increases with every batch across all epochs, the counter keeps running through the whole training run and does not reset at each epoch.
How many evaluations happen in a short run
Suppose num_epochs is 2, the training loader has 4 batches per epoch, and eval_freq is 3. How many times does the batch body run, and how many evaluations fire? Assume the check runs after the counter is incremented, as described for the batch body.
- 1Count batch passes
The batch body runs once per batch per epoch. With 2 epochs of 4 batches, that is 2 times 4, which equals 8 batch passes.
- 2Track global_step
global_stepstarts at -1. After the first batch it becomes 0, after the second 1, and so on. After the eighth batch it becomes 7. The check is evaluated at the counter values 0 through 7. - 3Find multiples of eval_freq
The values that are exact multiples of 3 in that range are 0, 3, and 6. Each of these makes the check true.
- 4Count evaluations
Three evaluations fire: after batch 1, batch 4, and batch 7. The counter continues across the epoch boundary, so the epoch change does not reset it.
The batch body runs 8 times and evaluation fires 3 times, at global_step values 0, 3, and 6.
What Happens During an Evaluation
When the check passes, the function calls evaluate_model to get a training loss and a validation loss, appends both to their lists, and prints them with the current epoch and step. It then calculates accuracy on both loaders and records those values as well. The accuracy calculation uses num_batches=eval_iter, so only a limited number of batches from each loader are used for this measurement.
The evaluate_model function is the other half of the picture. The source states it is identical to the one used for pretraining. It switches the model into evaluation mode with model.eval(), and it runs its calculations inside a torch.no_grad() block so that no gradients are computed. It estimates loss on both datasets and does not update any weights. This is the key difference from a training step: a training step computes gradients and changes weights, while evaluate_model only measures.
| Aspect | Training step in the batch loop | evaluate_model |
|---|---|---|
| Mode set | model.train() is called at the start of each epoch | model.eval() is called at the start of evaluation |
| Gradients | optimizer.zero_grad() resets them, and loss.backward() computes them | Computed inside torch.no_grad(), so none are calculated |
| Weights | optimizer.step() updates them | Not updated |
| Data used | Training batches from the training loader | Training and validation data, with eval_iter limiting batches for accuracy |
| Counters | examples_seen and global_step increase | Used to record losses and accuracies at this point |
| Mode restored | Not applicable | model.train() is called at the end |
A training step and an evaluation step inside the same training run
Mistakes Beginners Make When Reading This Loop
Common mistake Thinking eval_freq counts epochs
Why it failsThe source says
eval_freqis a count of training steps between evaluations.global_stepincreases on every batch across all epochs.Do this insteadRead eval_freq as a number of batches. Evaluation fires when the batch-level counter reaches a multiple of it.
Common mistake Assuming global_step resets at each epoch
Why it failsglobal_stepstarts at -1 and is incremented for every batch, so it keeps counting through the whole run.Do this insteadTrack one counter for the whole run. The epoch loop does not reset it.
Common mistake Assuming evaluation changes the weights
Why it failsevaluate_modelruns insidetorch.no_grad(), computes no gradients, and does not calloptimizer.step().Do this insteadTreat evaluation as measurement only. Weight changes happen only in the four training steps of the batch body.
Common mistake Forgetting that the model must return to training mode
Why it failsIf
evaluate_modelleft the model in evaluation mode, the following batches would train in the wrong mode.Do this insteadRemember that evaluate_model ends with model.train(), and that the epoch loop also sets training mode at the start of each epoch.
Common mistake Confusing the reset and the update, and assuming gradients accumulate across batches without being cleared
Tracing a Run on Paper
Suppose train_classifier_simple runs with num_epochs set to 3, 5 batches in each epoch, and eval_freq set to 4. Trace the run: how many times does the batch body execute, at which global_step values does evaluation fire, and does the first evaluation happen before or after the first weight update?
Hints
- Multiply the number of epochs by the number of batches per epoch to get the number of batch passes.
global_stepstarts at -1, and each batch's update is followed by an increment, so the first batch givesglobal_step0.- Look for the values in the counter range that are exact multiples of 4.
Explain in two or three sentences why evaluate_model must be called with gradient computation turned off, and what would go wrong if it updated weights during a measurement.
Hints
- Recall what
evaluate_modelis described as doing: estimating loss on both datasets without updating weights. - Think about whether a measurement step should change the thing it is measuring.
Key Takeaways
train_classifier_simplenests a batch loop inside an epoch loop. The outer loop runs once per epoch and callsmodel.train(), and the inner loop runs once per batch of training data.- Each batch resets gradients with
optimizer.zero_grad(), computes loss withcalc_loss_batch, backpropagates withloss.backward(), and updates weights withoptimizer.step(). Then it adds the batch rows toexamples_seenand incrementsglobal_step. - Evaluation runs when
global_stepis a multiple ofeval_freq. Becauseglobal_stepcounts batches across all epochs,eval_freqis a number of training steps between evaluations. evaluate_modelcallsmodel.eval(), runs insidetorch.no_grad()so that no gradients are computed, measures loss without updating weights, and then callsmodel.train()to restore training mode.