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.
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?
Reveal 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 makeseval_freq: how many training steps lie between evaluationseval_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.
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
optimizer.zero_grad()resets the loss gradients left over from the previous batch iteration - 2
calc_loss_batchcomputes the loss for the current input and target batch - 3
loss.backward()calculates the loss gradients - 4
optimizer.step()updates the model weights using those gradients
- 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_gradclears gradients left over from the previous batch.calc_loss_batchproduces 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.
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.
optimizer.zero_grad()Gradients hold values left over from the previous batch
loss.backward()
optimizer.step()Gradients reflect the current batch, and the weights have been updated from them
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.
input_batch.shape[0] is the number of rows, that is, examples, in the current batch.global_stepcounts 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.
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.
| Aspect | Training step (inside the batch loop) | evaluate_model |
|---|---|---|
| Mode set | model.train() at the start of each epoch | model.eval() |
| Gradient tracking | Gradients are computed by loss.backward() | Runs inside torch.no_grad(), so no gradients are computed |
| Weight changes | optimizer.step() updates the weights | No weights are updated |
| Data used | Each training batch from the training loader | Loss estimated on both datasets |
| When it runs | Every batch | Only when global_step is a multiple of eval_freq |
| Bookkeeping | examples_seen and global_step are updated | Results 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 likeExpecting an evaluation once per epoch when eval_freq is 5
Why it failsThe source defines
eval_freqas a count of training steps between evaluations, andglobal_stepcounts batches across all epochs.Do this insteadCount 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 likeCalling optimizer.zero_grad() after loss.backward()Why it failsThe 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 insteadKeep 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 failsexamples_seenadds the number of rows in each batch, whileglobal_stepincreases by one per batch. Onlyglobal_stepcontrols evaluation.Do this insteadUse 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 failsevaluate_modelruns insidetorch.no_grad()and does not update any weights.Do this insteadOnly optimizer.step() in the batch loop changes the weights.
Common mistake Forgetting that the model returns to training mode
Why it failsIf
model.train()were not called after evaluation, later batches would run in evaluation mode.Do this insteadRely on the final model.train() call inside evaluate_model and on the call at the start of each epoch.
Tracing a Short Training Run
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_stepincreases by one after every batch and does not reset between epochs.- Evaluation fires when
global_stepis a multiple ofeval_freq, so check the values 3 and 6 within the range covered by the 8 batches.
Key Takeaways
train_classifier_simplenests 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_seenand incrementsglobal_step. - Evaluation fires whenever
global_stepis a multiple ofeval_freq, soeval_freqcounts training steps across all epochs rather than epochs. evaluate_modelswitches to eval mode, runs insidetorch.no_grad(), estimates loss without updating weights, and restores training mode withmodel.train().