Training the Model
Last updated on 2026-07-24 | Edit this page
Overview
Questions
- What do the loss function and optimizer actually do during training?
- What happens, step by step, inside the training loop for one batch of data?
- What is an epoch, and why do we repeat the training loop for several of them?
- Why is training accuracy not enough to trust on its own?
Objectives
- Explain the roles of
CrossEntropyLossandAdamWin training MiniParT. - Walk through the six steps that happen for every batch during training.
- Define “epoch” and explain why the model trains over several of them.
- Explain why training accuracy alone cannot tell you whether the model generalizes.
Run this first
PYTHON
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
epochs = 10
for epoch in range(epochs):
model.train()
total_loss = 0
correct = 0
total = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
_, predicted = outputs.max(1)
total += batch_y.size(0)
correct += predicted.eq(batch_y).sum().item()
train_acc = 100. * correct / total
print(f"Epoch {epoch+1}/{epochs} | Loss: {total_loss/len(train_loader):.4f} | Train Acc: {train_acc:.2f}%")
OUTPUT
Epoch 1/10 | Loss: 0.6823 | Train Acc: 65.98%
Epoch 2/10 | Loss: 0.6512 | Train Acc: 67.42%
Epoch 3/10 | Loss: 0.6301 | Train Acc: 68.55%
Epoch 4/10 | Loss: 0.6147 | Train Acc: 69.31%
Epoch 5/10 | Loss: 0.6029 | Train Acc: 69.98%
Epoch 6/10 | Loss: 0.5934 | Train Acc: 70.52%
Epoch 7/10 | Loss: 0.5856 | Train Acc: 70.94%
Epoch 8/10 | Loss: 0.5790 | Train Acc: 71.28%
Epoch 9/10 | Loss: 0.5734 | Train Acc: 71.58%
Epoch 10/10 | Loss: 0.5687 | Train Acc: 71.84%
These exact numbers are illustrative - your own run will vary slightly - but they land in the same range as the roughly 0.68 → 0.56 loss and roughly 66% → 72% training accuracy trend used throughout this lesson, including in Evaluating the Model.
Run the block above first, then read on to see what each part does.
The model from Building MiniParT starts out knowing nothing - its weights are randomly initialized. Training is the repeated process of showing it examples, checking how wrong its guesses are, and nudging its weights to be a little less wrong next time - which is exactly what the code above just did. The rest of this episode walks back through it piece by piece.
Setup
PYTHON
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
-
device- trains on a GPU (cuda) if available, since GPUs do this math much faster than a CPU; otherwise falls back to CPU. Either works for a model this small, just at different speeds. -
criterion(the loss function) -CrossEntropyLossmeasures “how wrong was the guess,” comparing the model’s 3 class scores against the true label. Low if confidently correct, high if confidently wrong - think of it as an automatic grader. -
optimizer-AdamWadjusts the model’s weights based on the loss, like an editor deciding how to nudge every tunable number after seeing the grade.-
lr=1e-3(learning rate) - how big a nudge to make each step. Too big and the model overshoots; too small and it learns painfully slowly.0.001is a common starting point. -
weight_decay=1e-4- mild extra pressure discouraging weights from growing unnecessarily large, another safeguard against over-memorizing.
-
The training loop
This is the core loop that updates the model’s weights, once per batch, every epoch:
PYTHON
epochs = 10
for epoch in range(epochs):
model.train()
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
One epoch is one full pass through every batch in
the training set; we do 10 (epochs = 10). A single pass
isn’t enough - the weights only move a small amount with each nudge, so
the model needs to see the training data multiple times, nudging its
weights a little further each time, before it settles into a genuinely
useful set of weights. As it trains, watch the printed loss: it should
generally trend downward epoch over epoch - that’s the sign the model is
actually learning something, rather than stuck or diverging.
For every batch of 256 jet pairs:
-
model.train()- tells the model we’re training, switching on dropout (see Building MiniParT). -
optimizer.zero_grad()- clears leftover gradient information from the previous batch, so nudges don’t incorrectly pile on top of each other. -
outputs = model(batch_x)- the forward pass: run this batch through the model and get 3 class scores for each example. -
loss = criterion(outputs, batch_y)- compares those scores to the true labels and produces one number: how wrong, on average. -
loss.backward()- backpropagation: PyTorch works backward through every layer and calculates how much each weight contributed to the error, so we know which direction to nudge each one. -
optimizer.step()- applies those nudges, updating every weight a small amount in the direction that should reduce the loss.
Repeat for every batch, for 10 epochs, and the model gradually gets better at telling Hbb, Hcc, and QCD jet pairs apart.
CrossEntropyLoss treats every example equally by
default. As the previous episode noted, this dataset ends up with
slightly more QCD examples than Hbb or Hcc after selection, so the model
sees somewhat more QCD during training. The imbalance here is small
enough not to need special handling, but it’s worth checking in any
classification problem with unequal class sizes - left unchecked in a
more imbalanced dataset, a model can learn to just favor the larger
class.
Watching it learn
PYTHON
train_acc = 100. * correct / total
print(f"Epoch {epoch+1}/{epochs} | Loss: {total_loss/len(train_loader):.4f} | Train Acc: {train_acc:.2f}%")
After each epoch, we print the average loss and training accuracy (the percentage of training examples the model got right during that epoch). You should generally see loss go down and accuracy go up epoch over epoch - the model visibly learning.
This is training accuracy, measured on data the model has already seen - useful to sanity-check that learning is happening, but not a fair report card. For that, we need data the model has never seen, which the next episode covers.
Question
Q: Suppose optimizer.zero_grad() was accidentally left
out of the training loop. Would the code raise an error? What would you
expect to happen to training over the 10 epochs instead?
A: No error would be raised. Without
optimizer.zero_grad(), the gradient information from
loss.backward() would accumulate on top of the previous
batch’s instead of starting fresh each time. The weight updates in
optimizer.step() would then be based on this incorrectly
piled-up information, so training would likely become unstable: loss
might behave erratically instead of steadily decreasing, and accuracy
might fail to improve smoothly across epochs.
Quick recap
-
CrossEntropyLossgrades how wrong the model’s guesses are;AdamWdecides how to adjust the model’s weights in response. - Each batch: clear old gradients → forward pass → compute loss → backpropagate → update weights.
- One full pass through all batches is an epoch; we repeat for several epochs so the model keeps improving.
- Training accuracy is a useful sanity check, but not a fair test - see Evaluating the Model for that.
-
CrossEntropyLossgrades how wrong the model’s guesses are;AdamWdecides how to adjust the model’s weights in response. - Each batch follows the same six steps: clear old gradients, forward pass, compute loss, backpropagate, update weights.
- One full pass through all batches is an epoch; we repeat for several epochs so the model keeps improving.
- Training accuracy is a useful sanity check, but not a fair test, since it is measured on data the model has already seen.
