Concepts / Fine-Tuning a Pre-Trained LLM for Spam Classification

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.

  • Programming

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
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.

Epoch 1Batch 1batch body runsBatch 2batch body runsEpoch 2Batch 3batch body runsBatch 1batch body runsBatch 2batch body runsBatch 3batch body runsfirst batchnext batchnext batchnext epochfirst batchnext batchnext batch
How the epoch loop and batch loop nest. With 2 epochs and 3 batches per epoch, the batch body runs 6 times in total, and the epoch loop only advances after all batches in an epoch finish.

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.

Next batchoptimizer.zero_grad()calc_loss_batchloss.backward()optimizer.step()examples_seen +=batch rowsglobal_step += 1global_step %eval_freq == 0evaluate_modelNext batch or nextepochyesnoback to batch check
What happens in order on each batch. The weight-update steps run for every batch, while the evaluation step is reached only when the step counter passes the eval_freq check.

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.

Batch 1global_step 0, evaluateBatch 2global_step 1Batch 3global_step 2Batch 4global_step 3Batch 5global_step 4Batch 6global_step 5, evaluateBatch 7global_step 6
Given eval_freq of 5 and a global_step counter that starts at -1 and increases by one per batch, the evaluation check fires on the batches where the counter is a multiple of 5. Here the counter after each batch update is shown for the first seven batches.
Worked example

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.

  1. 1
    Count 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.

  2. 2
    Track global_step

    global_step starts 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.

  3. 3
    Find 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.

  4. 4
    Count 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.

Result

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.

AspectTraining step in the batch loopevaluate_model
Mode setmodel.train() is called at the start of each epochmodel.eval() is called at the start of evaluation
Gradientsoptimizer.zero_grad() resets them, and loss.backward() computes themComputed inside torch.no_grad(), so none are calculated
Weightsoptimizer.step() updates themNot updated
Data usedTraining batches from the training loaderTraining and validation data, with eval_iter limiting batches for accuracy
Countersexamples_seen and global_step increaseUsed to record losses and accuracies at this point
Mode restoredNot applicablemodel.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 fails

    The source says eval_freq is a count of training steps between evaluations. global_step increases on every batch across all epochs.

    Do this instead

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

    global_step starts at -1 and is incremented for every batch, so it keeps counting through the whole run.

    Do this instead

    Track one counter for the whole run. The epoch loop does not reset it.

  • Common mistake

    Assuming evaluation changes the weights

    Why it fails

    evaluate_model runs inside torch.no_grad(), computes no gradients, and does not call optimizer.step().

    Do this instead

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

    If evaluate_model left the model in evaluation mode, the following batches would train in the wrong mode.

    Do this instead

    Remember 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

MEDIUM

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_step starts at -1, and each batch's update is followed by an increment, so the first batch gives global_step 0.
  • Look for the values in the counter range that are exact multiples of 4.
EASY

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_model is 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_simple nests a batch loop inside an epoch loop. The outer loop runs once per epoch and calls model.train(), and the inner loop runs once per batch of training data.
  • Each batch resets gradients with optimizer.zero_grad(), computes loss with calc_loss_batch, backpropagates with loss.backward(), and updates weights with optimizer.step(). Then it adds the batch rows to examples_seen and increments global_step.
  • Evaluation runs when global_step is a multiple of eval_freq. Because global_step counts batches across all epochs, eval_freq is a number of training steps between evaluations.
  • evaluate_model calls model.eval(), runs inside torch.no_grad() so that no gradients are computed, measures loss without updating weights, and then calls model.train() to restore training mode.

Related concepts

More concepts starting with F