Concepts / Training Loops in PyTorch

Training Loops in PyTorch

train_classifier_simple nests a batch loop inside an epoch loop, and each batch resets gradients, computes loss, backpropagates, and updates weights.

  • Programming

A Spam Classifier Training Run

This article studies a single function, train_classifier_simple, which is used to fine-tune a model to classify spam. Its job is to train the model on a training dataset and, as training proceeds, to evaluate it on a validation dataset. Reading the function as a program is the best way in: the function is driven by a few inputs, and it hands a set of records back to its caller. Once you can see the shape of the loops, the details of each step fall into place.

What do you think happens?

The function has two loops, and one of them contains a conditional check on global_step. Which loop holds that check: the outer epoch loop or the inner batch loop?

  • The outer epoch loop, once per epoch

  • The inner batch loop, once per batch of training data

Reveal answer
Answer:

The inner batch loop, once per batch of training data.

The source describes the evaluation step as a branch in the path a single batch follows. Only some batches reach it, and the decision is made after the weight update for that batch, so the check belongs to the batch loop.

Inputs and Outputs of the Function

Before tracing the loops, look at what the function receives and what it returns. 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 control values set how long training runs and how often the model is checked. The function returns four lists and a counter.

  • num_epochs: how many full passes over the training data the function makes
  • eval_freq: how many training steps lie between evaluations
  • eval_iter: how many batches from each loader are used when accuracy is measured at an evaluation point
  • Four lists: training loss, validation loss, training accuracy, and validation accuracy, each recorded at every evaluation point
  • examples_seen: a counter of how many training examples the model has processed in total

The returned lists let you follow training progress over time. Because examples_seen is returned alongside them, each recorded loss or accuracy can be matched to how much training data the model had consumed at that point.

The Epoch and Batch Loops

The function uses two nested loops. 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. Each batch passes through the same sequence of steps. Only after the inner loop has finished every batch does the outer loop advance to the next epoch.

The diagram below shows the order of these loops. Trace it with a concrete picture in mind: if the training loader contains a fixed number of batches, the inner body runs that many times in every epoch, and the whole run executes the inner body once for each batch in each of the num_epochs passes.

Epoch 1model.train() calledBatch 1four-step batch bodyBatch 2four-step batch bodyLast batchfour-step batch bodyEpoch 2model.train() calledBatch 1 againfour-step batch bodyenter batch loopnext batchnext batchinner loop finishesenter batch loop
In what order do the epoch and batch loops run, and how many times does the inner batch body execute across the whole training run?

The Four Steps Inside Each Batch

Each batch runs four steps in a fixed order. The order matters because each step consumes what the previous one produced. The steps are listed below, followed by a skeleton that shows where each one sits inside the batch body.

  1. 1
    optimizer.zero_grad() resets the loss gradients left over from the previous batch iteration
  2. 2
    calc_loss_batch computes the loss for the current input and target batch
  3. 3
    loss.backward() calculates the loss gradients
  4. 4
    optimizer.step() updates the model weights using those gradients
python
Notes on the code
  • The outer loop runs once per epoch and switches the model to training mode.
  • The inner loop runs once per batch of training data.
  • zero_grad clears gradients left over from the previous batch.
  • calc_loss_batch produces the loss for this batch.
  • backward computes gradients from that loss.
  • step applies the update to the weights.

The flowchart below follows a single batch from the top of the batch body to the point where the bookkeeping begins. Notice that the loss is the output of one step and the input to the next, so the sequence is a chain rather than a set of independent actions.

Input and targetbatchoptimizer.zero_grad()clears previous gradientscalc_loss_batchproduces lossloss.backward()computes gradientsoptimizer.step()updates weightsBookkeepingexamples_seen, global_stepbatch enterscleared gradientslossgradientsupdated weights
What happens in sequence inside one batch iteration, and which step consumes the output of which?

Why Gradients Are Reset Before Each Backward Pass

The reset sits at the top of the batch body for a specific reason. The source describes optimizer.zero_grad() as clearing the gradients left over from the previous batch iteration. Gradients from one batch must not be mixed into the calculation for the next batch, so the reset happens before loss.backward() produces new gradients for the current batch.

Before
optimizer.zero_grad()

Gradients hold values left over from the previous batch

After
loss.backward()
optimizer.step()

Gradients reflect the current batch, and the weights have been updated from them

Gradientsprevious batch valuesGradientscleared, then current batchWeightsbefore this batchWeightsupdated by optimizer.step()
What does the gradient buffer look like before and after zero_grad, backward, and optimizer.step, and why must the reset happen before each backward pass?

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

python
Notes on the code
  • input_batch.shape[0] is the number of rows, that is, examples, in the current batch.
  • global_step counts every batch processed so far, across all epochs.

The two counters measure different things. examples_seen tracks how much data the model has consumed, while global_step tracks how many batches have been processed. Only global_step is used to decide whether an evaluation happens.

How eval_freq Decides When to Evaluate

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 makes eval_freq a count of training steps between evaluations, not a count of epochs.

The counter global_step starts at -1 and increases with every batch across all epochs. It is never reset at the start of an epoch, so the count keeps running through the whole training run. An evaluation therefore can fire in the middle of an epoch, and the next batches continue normally afterward.

global_step 4no evaluationglobal_step 5evaluation runsglobal_step 6no evaluationglobal_step 10evaluation runsglobal_step 15evaluation runsnext batchnext batchbatches continuebatches continue
Across the batches of several epochs, at which global_step values does the evaluation fire, and how does that relate to eval_freq?

With eval_freq set to 5, the check passes at global_step values that are multiples of 5, such as 5, 10, and 15, and the evaluation runs at those points. Between them, the batches only train. Changing eval_freq changes the spacing of these points without changing the training itself.

When the check passes, the function calls evaluate_model to obtain 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. The accuracy calculation uses num_batches=eval_iter, so only a limited number of batches from each loader are used for this measurement.

What evaluate_model Does Differently

The evaluate_model function is the other half of the picture. The source states that 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. The comparison below sets it against a training step.

AspectTraining step (inside the batch loop)evaluate_model
Mode setmodel.train() at the start of each epochmodel.eval()
Gradient trackingGradients are computed by loss.backward()Runs inside torch.no_grad(), so no gradients are computed
Weight changesoptimizer.step() updates the weightsNo weights are updated
Data usedEach training batch from the training loaderLoss estimated on both datasets
When it runsEvery batchOnly when global_step is a multiple of eval_freq
Bookkeepingexamples_seen and global_step are updatedResults are appended to the lists returned to the caller

How a training step and evaluate_model differ

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. Without that final call, the next batches would run with the model still in evaluation mode. This is also the reason the training function calls model.train() at the start of each epoch: the mode is set again explicitly rather than assumed.

Mistakes Beginners Make When Tracing the Loop

  • Common mistake

    Treating eval_freq as a number of epochs

    What it looks like

    Expecting an evaluation once per epoch when eval_freq is 5

    Why it fails

    The source defines eval_freq as a count of training steps between evaluations, and global_step counts batches across all epochs.

    Do this instead

    Count batches, not epochs: the check fires whenever global_step is a multiple of eval_freq.

  • Common mistake

    Assuming the reset can happen anywhere in the batch

    What it looks like
    Calling optimizer.zero_grad() after loss.backward()
    Why it fails

    The source places the reset at the top of the batch body so it clears the gradients left over from the previous batch before the current backward pass.

    Do this instead

    Keep optimizer.zero_grad() as the first step of each batch, before the loss is computed.

  • Common mistake

    Confusing examples_seen with global_step

    Why it fails

    examples_seen adds the number of rows in each batch, while global_step increases by one per batch. Only global_step controls evaluation.

    Do this instead

    Use global_step for the evaluation check and examples_seen for how much data has been processed.

  • Common mistake

    Believing evaluate_model updates the model

    Why it fails

    evaluate_model runs inside torch.no_grad() and does not update any weights.

    Do this instead

    Only optimizer.step() in the batch loop changes the weights.

  • Common mistake

    Forgetting that the model returns to training mode

    Why it fails

    If model.train() were not called after evaluation, later batches would run in evaluation mode.

    Do this instead

    Rely on the final model.train() call inside evaluate_model and on the call at the start of each epoch.

Tracing a Short Training Run

MEDIUM

Suppose num_epochs is 2, eval_freq is 3, and the training loader contains 4 batches per epoch. Using the nested loop and global_step rules, list the global_step values at which evaluation fires across the whole run, and count how many times the four-step batch body executes in total.

Hints
  • The batch body runs once per batch in each epoch, so 2 epochs of 4 batches gives 8 executions in total.
  • global_step increases by one after every batch and does not reset between epochs.
  • Evaluation fires when global_step is a multiple of eval_freq, so check the values 3 and 6 within the range covered by the 8 batches.

Key Takeaways

  • train_classifier_simple nests a batch loop inside an epoch loop: the outer loop sets training mode once per epoch, and the inner loop runs the four-step batch body for every batch.
  • Each batch resets gradients, computes the loss, backpropagates, and updates the weights in that fixed order, then adds the batch size to examples_seen and increments global_step.
  • Evaluation fires whenever global_step is a multiple of eval_freq, so eval_freq counts training steps across all epochs rather than epochs.
  • evaluate_model switches to eval mode, runs inside torch.no_grad(), estimates loss without updating weights, and restores training mode with model.train().

Related concepts

More concepts starting with T