The previous lesson traced one error backwards by hand. That is valuable because it tells you what a gradient means. But manually writing derivatives for every parameter stops being practical as soon as the model has more than a few calculations.
PyTorch is a Python library for numerical computation and deep learning. A tensorTensorA container for numbers arranged with one or more dimensions. A single number is a zero-dimensional tensor, a list is often one-dimensional, and a table is often two-dimensional. In PyTorch, tensors can hold data and, when needed, record operations for gradient calculation. stores its numbers; the library can record eligible calculations for automatic differentiation and provides model and optimiser building blocks. It does not choose a valid target, protect a data split, or decide that a model is safe to use.
Lesson details
- Module
- Deep learning
- Lesson
- 3 · PyTorch: an honest training loop
- Difficulty
- Beginner
- Learning time
- 65 minutes
Before you start
- Forward passes, loss, gradients, and one gradient-descent update
- Basic Python functions, loops, lists, and virtual environments
Skills you will gain
- Describe what tensors, modules, losses, gradients, and optimisers each do in PyTorch
- Trace the five essential calls in a training step in the correct order
- Build a small nn.Module and train it on a deliberately tiny regression task
- Separate training mode from evaluation mode and explain why gradients are disabled for evaluation
- Save the model state and record enough context to make a training result inspectable
Why this matters
A training framework makes the repeated arithmetic manageable. It also makes it easy to hide mistakes behind five short method calls. A readable training loop gives every call one job: make a prediction, measure the error, clear old gradients, calculate new gradients, then update parameters.
Plain-English mental model
Step 1 — Tensors are shaped containers for numbers
A tensor is a container whose shape says how numbers are arranged. A score for one example might have shape (1,). A batch of four examples, each with three input features, has shape (4, 3): four rows and three values per row.
PyTorch can run the same tensor code on a CPU or a supported accelerator. Start on the CPU, make shapes and types clear, and move only after measurement gives you a reason. Inputs for a neural network are usually floating-point tensors. Classification targets often have a different type, depending on the loss; always read the loss function’s contract rather than guessing.
import torch
features = torch.tensor(
[[0.0], [1.0], [2.0]],
dtype=torch.float32,
)
targets = torch.tensor([[1.0], [3.0], [5.0]], dtype=torch.float32)
print(features.shape)
print(features.dtype)
print(targets.shape)
The expected output is:
torch.Size([3, 1])
torch.float32
torch.Size([3, 1])
The outer dimension is the example or batch dimension. Keeping it—even for one example—helps a model accept one example and many examples consistently.
Step 2 — Keep the five training jobs separate
Here is the essential order. It is short enough to memorise, but each line has a distinct responsibility.
prediction = model(features) # 1. forward pass
loss = loss_fn(prediction, targets) # 2. numeric training signal
optimizer.zero_grad() # 3. remove old accumulated gradients
loss.backward() # 4. calculate fresh gradients
optimizer.step() # 5. update parameters
PyTorch adds gradients to a parameter’s .grad field by default. That is useful when you intentionally combine several smaller computations before an update. In the ordinary one-batch pattern, forgetting zero_grad() means a later update mixes in an earlier batch’s gradient. Clear before backward(), so the gradient you inspect belongs to the current batch.
Worked example — learn a line with an explicit model
This intentionally tiny problem has the rule target = 2 × input + 1. It is not a realistic product task. It lets us inspect the mechanics without pretending a short example proves generalisation.
nn.Linear(1, 1) is a model with one input, one output, a weight, and a bias. We set both parameters to zero so this first update is repeatable and understandable. The mean-squared-error loss averages the squared prediction errors.
import torch
from torch import nn
features = torch.tensor([[0.0], [1.0]])
targets = torch.tensor([[1.0], [3.0]]) # target = 2 × input + 1
model = nn.Linear(1, 1)
with torch.no_grad():
model.weight.fill_(0.0)
model.bias.fill_(0.0)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
prediction = model(features)
loss = loss_fn(prediction, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('loss before:', round(loss.item(), 3))
print('weight after:', round(model.weight.item(), 3))
print('bias after:', round(model.bias.item(), 3))Important lines
with torch.no_grad()- This setup changes parameters intentionally but should not become part of a gradient graph. It is not the usual way to initialise a model; it keeps this worked example deterministic.
optimizer = torch.optim.SGD(...)- The optimiser receives the parameters it may update and a learning rate. It does nothing until step() is called.
optimizer.zero_grad()- This clears the gradients stored from a prior backward call. In the normal pattern it comes before calculating the current batch gradient.
loss.backward()- Autograd follows the operations that produced loss and fills each trainable parameter’s gradient.
optimizer.step()- The optimiser reads those gradients and changes the model parameters according to its update rule.
The result does not jump to weight 2 and bias 1 after one batch. One small update moves in a helpful direction. Training repeats on data that represents the intended task, while validation checks whether progress also appears on protected examples.
Step 3 — A DataLoader provides batches, not validity
A DataLoaderDataLoaderA PyTorch object that iterates over a dataset and yields batches. It can batch examples, choose an order, and optionally load data using worker processes. It does not make a split valid or a dataset representative; those are separate design decisions. turns a dataset into an iterator over batches. It can group examples, shuffle a training order, and use worker processes when reading data becomes a measured bottleneck. It does not create a train/validation/test split for you, prevent leakage, make labels correct, or make shuffling appropriate for time-ordered data.
from torch.utils.data import DataLoader, TensorDataset
dataset = TensorDataset(features, targets)
train_loader = DataLoader(dataset, batch_size=2, shuffle=True)
for batch_features, batch_targets in train_loader:
print(batch_features.shape, batch_targets.shape)
For this two-example dataset, the output shapes are always torch.Size([2, 1]) and torch.Size([2, 1]); only the row order may differ because shuffle=True changes it. Set shuffle=True only for a training split when the examples may safely be reordered. Do not casually shuffle a time sequence, grouped records, or a validation set merely because the default training pattern did.
An epochEpochOne pass through the training examples as defined by a data loader. An epoch is an accounting unit, not proof that a model has learned enough or will work on new data. means one pass through the examples yielded by a loader. It is a convenient counter, not a target by itself. Record both the training loss and a protected validation measure at each epoch so you can see divergence, overfitting, or a plateau rather than relying on a final number alone.
Step 4 — Train and evaluate deliberately differently
The training step needs gradients. Evaluation does not. During evaluation, use model.eval() and torch.no_grad().
model.eval() switches certain layers—such as dropout and batch normalisation—to their evaluation behaviour. It does not disable gradients. torch.no_grad() stops PyTorch recording gradient-tracking operations for the enclosed block. It does not switch model behaviour. In real code, use both for ordinary validation or inference.
for epoch in range(20):
model.train()
for batch_features, batch_targets in train_loader:
prediction = model(batch_features)
loss = loss_fn(prediction, batch_targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
validation_prediction = model(validation_features)
validation_loss = loss_fn(validation_prediction, validation_targets)
print(epoch, validation_loss.item())Keep validation examples separate before you create the loader. A clean-looking loop cannot repair a contaminated split. For a real project, make preprocessing an explicit, versioned part of the training and evaluation path; fit data-dependent transformations on training data only, then apply the frozen transformation elsewhere.
Try it — see why accumulated gradients matter
This browser exercise uses plain Python to make the accumulation rule visible. The arithmetic matches a simplified one-parameter update; PyTorch performs the same bookkeeping for tensors. Run it, then change clear_before_second_batch to False and compare the final weight.
With clear_before_second_batch = True, the output is:
after batch 1: 0.4
gradient used for batch 2: -2.0
final weight: 0.6000000000000001
If you set it to False, the second update uses -6.0 instead: the old -4.0 has been added to the new -2.0. Gradient accumulation can be intentional, but it must be an explicit design choice with the loss scaling checked carefully.
Step 5 — Save model state, context, and evidence
A model stateModel stateThe values a model needs to reproduce its learned behaviour, including its parameters and persistent buffers. A PyTorch module exposes this information through state_dict; a usable training checkpoint commonly also records optimiser state and run metadata. is the learned parameter and persistent-buffer values needed for a module to reproduce its behaviour. In PyTorch, model.state_dict() provides that mapping. If you may resume training, store optimizer.state_dict() too; optimisers such as Adam keep internal history that affects future updates.
checkpoint = {
'model_state': model.state_dict(),
'optimizer_state': optimizer.state_dict(),
'epoch': epoch,
'validation_loss': validation_loss.item(),
'data_version': 'support-labels-2026-07-15',
}
torch.save(checkpoint, 'checkpoint.pt')
The filename and one loss value are not enough evidence. Record the code revision, package versions, model configuration, data and split version, preprocessing version, random seeds where applicable, metrics and error slices, and the decision that determined whether this run may proceed. Never place secrets or unnecessary personal data into a checkpoint or its logs.
Real-world AI engineering example — a classifier that can be investigated
Common mistakes
Calling backward() an optimiser update.
Fix — backward() calculates and stores gradients. step() is the call that changes parameters.
Forgetting zero_grad() because loss fell on a toy run.
Fix — Use the explicit clear → backward → step pattern unless you are intentionally accumulating gradients and have checked scaling.
Using model.eval() as if it disabled gradients.
Fix — Use model.eval() for evaluation behaviour and torch.no_grad() to stop gradient tracking. They solve different problems.
Assuming DataLoader shuffle creates a sound train/validation split.
Fix — Create and audit the split first. A loader only iterates over the data you give it.
Saving only a .pt file with no context.
Fix — Record data, split, preprocessing, code, configuration, metrics, and optimiser state when resuming matters.
Treating a falling training loss as deployment evidence.
Fix — Compare protected validation and test evidence, relevant error slices, calibration, latency, cost, and the actual decision policy.
Knowledge check
Knowledge check
Practice activity
Practice · 25–35 minutes
Build one transparent training step
Goal: Run a deterministic PyTorch update, then explain the role and order of every line.
- Install PyTorch for your platform in a project virtual environment using the current official installation instructions.
- Run the worked example with its zeroed weight and bias.
- Print the prediction, loss, weight gradient, and bias gradient immediately after backward() but before step().
- Confirm that the first gradients are -3.0 for the weight and -4.0 for the bias.
- Call step(), then calculate the new loss without calling backward().
- Write one sentence distinguishing model.eval() from torch.no_grad().
Expected result: Before the step, the loss is 5.0, the gradients are -3.0 and -4.0, and the first update gives weight 0.3 and bias 0.4. You should be able to say that eval changes layer behaviour while no_grad stops graph recording.
Optional challenge: Wrap the two tensors in TensorDataset and DataLoader, then make the update loop work for one batch without changing the learning result.
Reveal solution
Solution: for predictions [0, 0] and targets [1, 3], mean squared error
is (1² + 3²) / 2 = 5. The loss gradient for the weight is -3 and for the bias
is -4. With learning rate 0.1, SGD takes weight = 0 - 0.1×(-3) = 0.3 and
bias = 0 - 0.1×(-4) = 0.4. Evaluation should use both model.eval() and
torch.no_grad(): the first selects evaluation behaviour for relevant modules;
the second avoids creating a gradient graph.
Key takeaways
- Tensors carry numeric data and their shapes make the batch and feature dimensions explicit.
- A training batch follows a stable order: forward pass, loss, clear gradients, backward, optimiser step.
- backward() calculates gradients and step() updates parameters; they are not interchangeable.
- DataLoader makes batching and iteration convenient but cannot validate labels, splits, ordering, or leakage.
- Evaluation normally needs both model.eval() and torch.no_grad().
Flashcards
Flashcards
1 of 7Revision and interview questions
- Draw the PyTorch training loop for one batch. State what exists or changes after forward, loss, zero_grad, backward, and step.
- Explain with a two-batch example why PyTorch gradient accumulation can be useful but dangerous when accidental.
- Compare model.train(), model.eval(), torch.no_grad(), and optimizer.zero_grad(). What distinct concern does each address?
- Design the minimum data, configuration, and model-state record needed to investigate a surprising validation result.
One-page sketchnote summary
Print me · one-page revision sheet
PyTorch: an honest training loop
Main idea
PyTorch turns the repeated mechanics of learning into a small, inspectable loop. The framework handles tensor arithmetic and automatic differentiation; engineers remain responsible for the data, objective, evidence, and decision policy around it.
Core terms
- Tensor — shaped numeric container
- Module — model calculation + parameters
- Batch — small group of examples
- Epoch — one loader pass
- Optimiser — applies gradient-based updates
- Model state — learned values to save
One picture
Code pattern
predict → loss
clear → backward → step
eval: eval() + no_grad()Watch out
- Do not confuse backward with step
- Do not forget gradient clearing
- Do not use eval as a substitute for no_grad
- Do not confuse batching with a valid split
Remember
- Keep five training jobs separate
- Inspect shapes at boundaries
- Evaluate deliberately
- Save context, not just weights
- Lower training loss is not deployment evidence
Three quick questions
- What does backward calculate?
- Why clear gradients?
- What does the loader not know?
Lesson checklist
0 of 7 completeResources
- docsPyTorch — Learn the BasicsOfficial guided path through tensors, data loading, model building, autograd, optimisation, and saving state.
- docsPyTorch — Autograd mechanicsOfficial explanation of the graph that PyTorch records during gradient-enabled forward calculations.
- docsPyTorch — Data loadingOfficial reference for Dataset and DataLoader behaviour, batching, ordering, and worker processes.