Concepts / Evaluating a Classifier During Training

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.

  • Programming

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
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 makes
  • eval_freq: how many training steps pass between evaluations
  • eval_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.

Epoch 1 startsmodel.train() calledBatch 1four steps runBatch 2four steps runBatch 3four steps runEpoch 2 startsmodel.train() calledBatch 4four steps runBatch 5four steps runBatch 6four steps runinner loopnext batchnext batchepoch endsinner loopnext batchnext batch
For each epoch, how many times does the batch loop run, and what happens on each pass before control returns to the outer loop?

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.

python
Notes on the code
  • optimizer.zero_grad() clears gradients left from the previous batch
  • calc_loss_batch computes the loss for the current batch
  • loss.backward() calculates the loss gradients
  • optimizer.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.

Batch loopOptimizergradients and weightsLoss computationcalc_loss_batchModelweights1. zero_grad clears old gradients2. compute loss for this batch3. backward computes gradients4. step updates weights5. examples_seen and global_step increase
What is the exact order of operations inside one batch iteration, and what does each operation read or change?

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.

Worked example

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.

  1. 1
    Track the counter

    global_step starts at -1. The first batch increments it to 0, the second to 1, and so on, so the 12th batch leaves it at 11.

  2. 2
    Apply the check

    Evaluation fires when global_step % 5 == 0, meaning the remainder after dividing by 5 is zero.

  3. 3
    List the multiples

    Within the values 0 through 11, the multiples of 5 are 0 and 5 and 10.

Result

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.

global_step 0evaluateglobal_step 1train onlyglobal_step 2train onlyglobal_step 3train onlyglobal_step 4train onlyglobal_step 5evaluateglobal_step 10evaluatenext batchnext batchnext batchnext batchnext batchfive batches later
At which global_step values does evaluation fire, and how does the counter relate to eval_freq across batches?

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.

AspectTraining step in the batch loopevaluate_model call
Gradientsoptimizer.zero_grad() clears them, and loss.backward() computes themComputed inside torch.no_grad(), so none are computed
LossComputed for the current training batchEstimated on both training and validation loaders
WeightsUpdated by optimizer.step()Never updated
Model modeTraining mode, set by model.train() at the start of each epochEvaluation mode via model.eval(), then restored with model.train()
Counterexamples_seen and global_step increaseNo 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 fails

    The check compares global_step, which counts batches across the whole run, against eval_freq. eval_freq is a count of training steps between evaluations.

    Do this instead

    Read 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 fails

    global_step increases with every batch across all epochs and is not reset when a new epoch begins.

    Do this instead

    Use 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 fails

    evaluate_model runs inside torch.no_grad() and does not call the optimizer, so it measures loss without changing the model.

    Do this instead

    Only the batch loop, through optimizer.step(), changes the weights.

  • Common mistake

    Forgetting to switch back to training mode

    Why it fails

    If the model remains in evaluation mode after a mid-epoch evaluation, the following batches run without training behavior.

    Do this instead

    Keep 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

MEDIUM

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_step takes the values 0 through 5. Which of those are multiples of 2?
  • Answer: evaluation fires at global_step 0, 2, and 4, so three evaluations occur, and the second and third occur in the second epoch.

Key Takeaways

  • train_classifier_simple nests a batch loop inside an epoch loop. Each epoch calls model.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 with loss.backward(), updates weights with optimizer.step(), and then updates examples_seen and global_step.
  • global_step counts batches across the whole run, and evaluation fires whenever it is an exact multiple of eval_freq. eval_freq counts training steps, not epochs.
  • evaluate_model switches to eval mode, runs inside torch.no_grad(), estimates loss on both datasets without updating weights, and then restores training mode with model.train().

Related concepts

More concepts starting with E