Evaluating a Classifier During Training
train_classifier_simple nests a batch loop inside an epoch loop, and each batch resets gradients, computes loss, backpropagates, and updates weights.
Reading a Training Function as a Program
train_classifier_simple is a function used to fine-tune a model to classify spam. Its job is to train a model on a training dataset and, as training proceeds, to evaluate it on a validation dataset. Before reading its internals, it helps to look at it the way a programmer would: which inputs drive the loops, and which outputs does the function hand back to its caller?
What do you think happens?
Suppose the training data has 4 batches and the function runs for 3 epochs. How many times does the batch body run in total?
Reveal answer
12
The outer loop runs 3 times. Each time, the inner loop runs once per batch, so 3 epochs times 4 batches gives 12 passes through the batch body. Evaluation adds extra work on some passes, but it does not change how many times the batch body runs.
Inputs and Outputs of the Function
The function takes the model, the two data loaders, an optimizer, the device to run on, and four control values: num_epochs, eval_freq, and eval_iter. The two data loaders are the training loader and the validation loader. The optimizer is the object that performs the weight updates.
num_epochs: how many full passes over the training data the function makeseval_freq: how many training steps pass between evaluationseval_iter: how many batches from each loader are used when measuring accuracy during an evaluation- Returns: four lists recording training loss, validation loss, training accuracy, and validation accuracy at each evaluation point, plus a counter named
examples_seen
The returned lists are what make the function useful after it finishes. Because the losses and accuracies are recorded at each evaluation point, you can follow how training progressed over time. examples_seen records how many training examples the model has processed in total, which gives a second way to place each measurement on a timeline.
The Nested Loop Structure
The function has two loops, one inside the other. The outer loop, written as 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, written as for input_batch, target_batch in train_loader, runs once per batch of training data. Only after the inner loop finishes does the outer loop move on to the next epoch.
The diagram below shows the progression for a small run with two epochs and three batches per epoch. Each batch passes through the same body of steps. The inner loop returns control to the outer loop only when every batch in the current epoch has been processed.
The Four Steps Inside Each Batch
Each batch runs four steps in a fixed order. First, the optimizer resets the loss gradients left over from the previous batch iteration. Second, the function computes the loss for the current input and target batch. Third, the loss is backpropagated, which calculates the loss gradients. Fourth, the optimizer uses those gradients to update the model weights. The reset sits at the top of the batch body because the gradients must be cleared before this batch's gradients are calculated.
optimizer.zero_grad()clears gradients left from the previous batchcalc_loss_batchcomputes the loss for the current batchloss.backward()calculates the loss gradientsoptimizer.step()updates the model weights using those gradients
The sequence below shows which part of the program acts at each point in one batch iteration. Read each arrow as a message from one part to another, in the order it happens.
Bookkeeping: examples_seen and global_step
After the weight update, the function does two bookkeeping actions. It adds the number of rows in the current input batch to examples_seen, using input_batch.shape[0]. It also increments global_step by one. The source notes that examples_seen counts examples rather than tokens, which is the change from the earlier pretraining code.
The counter global_step is what later decides whether an evaluation happens. It starts at -1 and increases with every batch across all epochs. It is not reset at the start of each epoch, so it keeps counting through the whole run.
When Evaluation Fires
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. This means eval_freq is a count of training steps between evaluations, not a count of epochs.
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, using num_batches=eval_iter, so only a limited number of batches from each loader are used for this measurement.
Finding the evaluation points for eval_freq of 5
Suppose eval_freq is 5 and the run has 12 training batches in total. At which global_step values does evaluation fire? Assume the check runs after the counter has been incremented for each batch, which is the order the source describes.
- 1Track the counter
global_stepstarts at -1. The first batch increments it to 0, the second to 1, and so on, so the 12th batch leaves it at 11. - 2Apply the check
Evaluation fires when
global_step% 5 == 0, meaning the remainder after dividing by 5 is zero. - 3List the multiples
Within the values 0 through 11, the multiples of 5 are 0 and 5 and 10.
Evaluation fires at global_step 0, 5, and 10. The first evaluation happens on the very first batch, because 0 is a multiple of 5. The number of evaluations depends only on the total step count and eval_freq, not on how many batches are in each epoch.
What evaluate_model Does Differently
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 the training and validation datasets, and it does not update any weights. The training data is used only for measurement here; the weight updates that come from backpropagation happen only in the batch loop.
| Aspect | Training step in the batch loop | evaluate_model call |
|---|---|---|
| Gradients | optimizer.zero_grad() clears them, and loss.backward() computes them | Computed inside torch.no_grad(), so none are computed |
| Loss | Computed for the current training batch | Estimated on both training and validation loaders |
| Weights | Updated by optimizer.step() | Never updated |
| Model mode | Training mode, set by model.train() at the start of each epoch | Evaluation mode via model.eval(), then restored with model.train() |
| Counter | examples_seen and global_step increase | No change to the step counter in this function |
Differences between a training step and an evaluate_model call
Restoring Training Mode After Evaluation
The function ends by setting the model back to training mode with model.train(). This matters because train_classifier_simple calls evaluate_model partway through an epoch, not only at epoch boundaries. Without that final call, the next batches would run with the model still in evaluation mode. This is also why the training function calls model.train() at the start of each epoch.
Mistakes Beginners Make With Evaluation
Common mistake Treating eval_freq as a number of epochs
Why it failsThe check compares
global_step, which counts batches across the whole run, againsteval_freq.eval_freqis a count of training steps between evaluations.Do this insteadRead eval_freq as a number of batches. Evaluation fires whenever the running counter is an exact multiple of it.
Common mistake Assuming global_step resets at the start of each epoch
Why it failsglobal_stepincreases with every batch across all epochs and is not reset when a new epoch begins.Do this insteadUse examples_seen and global_step to place a measurement on the run's timeline, not the epoch number alone.
Common mistake Expecting evaluate_model to update weights
Why it failsevaluate_modelruns insidetorch.no_grad()and does not call the optimizer, so it measures loss without changing the model.Do this insteadOnly the batch loop, through optimizer.step(), changes the weights.
Common mistake Forgetting to switch back to training mode
Why it failsIf the model remains in evaluation mode after a mid-epoch evaluation, the following batches run without training behavior.
Do this insteadKeep the model.train() call at the end of evaluate_model, and keep the call at the start of each epoch.
Common mistake Removing zero_grad() from the batch body, which lets gradients from one batch accumulate into the next
Tracing a Short Run by Hand
A run has 2 epochs with 3 batches each, and eval_freq is 2. Before reading the hints, determine how many evaluations fire and at which global_step values. Assume global_step starts at -1 and is incremented once per batch before the check.
Hints
- Count the total batches first: 2 epochs times 3 batches gives 6 batches.
- After the six increments,
global_steptakes the values 0 through 5. Which of those are multiples of 2? - Answer: evaluation fires at
global_step0, 2, and 4, so three evaluations occur, and the second and third occur in the second epoch.
Key Takeaways
train_classifier_simplenests a batch loop inside an epoch loop. Each epoch callsmodel.train()once at its start, and the inner loop processes every batch in that epoch.- Each batch resets gradients with
optimizer.zero_grad(), computes the loss, backpropagates withloss.backward(), updates weights withoptimizer.step(), and then updatesexamples_seenandglobal_step. global_stepcounts batches across the whole run, and evaluation fires whenever it is an exact multiple ofeval_freq.eval_freqcounts training steps, not epochs.evaluate_modelswitches to eval mode, runs insidetorch.no_grad(), estimates loss on both datasets without updating weights, and then restores training mode withmodel.train().