Course outline

PyTorch / Tensor, gradient, and module foundations / Lesson 6

A controlled training and evaluation step

Make the order and state of one complete PyTorch update visible before building a larger training loop.

About 5 min readContent reviewed
On this page

Lesson goals

  • Order reset, forward, loss, backward, and optimizer update correctly.
  • Compare a pre-update loss with a separately computed post-update value.
  • Set module mode and gradient recording intentionally during evaluation.

Begin with a calculation whose answer is known

The fixture fits a one-input linear module to two supplied pairs using mean squared error. Both weight and bias are explicitly filled with zero before training, so random construction does not determine the expected result. The input and target shapes both remain (2,1), avoiding unintended pairwise broadcasting. The model, inputs, and target use CPU float64, and SGD has a fixed learning rate with no momentum or decay. These choices define a deliberately narrow update contract. They are not recommended hyperparameters or a claim that two points establish useful external prediction.

The update has several distinct operations

Enter the intended module training mode, clear gradient buffers at the chosen boundary, compute predictions and the scalar loss, call backward, and then call optimizer.step. Differentiation calculates derivatives; the optimizer consumes them to change parameters. zero_grad is not an optimizer update, and step does not automatically run another forward pass to refresh the loss. The illustrative program preserves the pre-update scalar loss, then separately recomputes the objective after the update. Make this ordering explicit so a log named after does not accidentally display the old loss object.

python Illustrative CPU PyTorch 2.14 API program; torch unavailable, expected output derived
import torch
from torch import nn
x = torch.tensor([[1.0],[2.0]],dtype=torch.float64,device="cpu")
y = torch.tensor([[2.0],[4.0]],dtype=torch.float64,device="cpu")
model = nn.Linear(1,1,dtype=torch.float64,device="cpu")
with torch.no_grad():
    model.weight.fill_(0.0)
    model.bias.fill_(0.0)
optimizer = torch.optim.SGD(model.parameters(),lr=0.1)
model.train()
optimizer.zero_grad(set_to_none=True)
loss = nn.MSELoss()(model(x),y)
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
    after = nn.MSELoss()(model(x),y)
print(f"before={loss.item():.6f}; after={after.item():.6f}")
print(f"weight={model.weight.item():.6f}; bias={model.bias.item():.6f}")
Expected output
before=10.000000; after=1.060000
weight=1.000000; bias=0.600000

Explicitly initialized parameters make one ordinary SGD update analytically checkable; framework forward, backward, optimizer, and evaluation were not executed.

Evaluation needs both mode and recording choices

model.eval changes mode-specific module behavior, while torch.no_grad avoids building a backward graph for the shown evaluation calculation. This linear module has no dropout or running normalization statistics, so changing its module mode does not alter its algebra. More complex modules can differ between modes even at identical parameters. Decide whether an evaluation should include regularization terms or stochastic transforms before comparing it with a training curve. Do not allow validation outcomes to trigger parameter updates through an accidentally reused training routine. A validation loop should have its own clear state and data boundary.

Derive the first update independently

The standard-library companion calculates residuals minus two and minus four, mean loss ten, weight derivative minus ten, and bias derivative minus six. A learning rate of 0.1 therefore gives weight one and bias 0.6. Predictions become 1.6 and 2.6, with squared errors 0.16 and 1.96 and mean 1.06. This is an exact mathematical account with rounded display under the fixture, not evidence that SGD converges for arbitrary objectives. Match the mean reduction and derivative factor two before comparing a manual reference with a framework result.

python Executed deterministic standard-library companion
x, y = [1.0,2.0], [2.0,4.0]
w, b, rate = 0.0, 0.0, 0.1
residuals = [w*a+b-target for a,target in zip(x,y)]
before = sum(r*r for r in residuals)/len(x)
gw = 2*sum(r*a for r,a in zip(residuals,x))/len(x)
gb = 2*sum(residuals)/len(x)
w -= rate*gw
b -= rate*gb
after = sum((w*a+b-target)**2 for a,target in zip(x,y))/len(x)
print(f"dw={gw:.1f}; db={gb:.1f}")
print(f"before={before:.6f}; after={after:.6f}")
Expected output
dw=-10.0; db=-6.0
before=10.000000; after=1.060000

The executed derivative and update calculation supplies an independent expected fixture; it does not verify any torch optimizer implementation or general training convergence.

Expand only after the one-step boundary is understood

Check named parameter changes, gradient presence, finite values, target alignment, and the expected output on the fixed pair before adding shuffling, workers, schedules, accumulation, or a larger architecture. Preserve observation ids and an honest held-out protocol when moving beyond a training-correctness fixture. Fitting these two targets is not a validation score or evidence against leakage. The PyTorch program remains illustrative because torch is not installed. Only the scalar companion was executed; no model training, evaluation mode, optimizer call, numerical kernel, device timing, or framework checkpoint was tested during review.

Common errors and fixes

The displayed post-update loss is unchanged unexpectedly

Why it happens
The code printed the old forward loss instead of recomputing it.
How to fix it
Run a fresh forward under the intended evaluation boundary after step.
Prevent it next time
Label pre-update and post-update measurements explicitly.

A training loop updates without the expected current gradients

Why it happens
Reset, backward, and step were ordered incorrectly or a path was missing.
How to fix it
Trace one complete update and inspect named gradient buffers.
Prevent it next time
Keep an independently derived one-step fixture before adding loop features.

Validation silently changes model parameters

Why it happens
Evaluation reused a routine that still called backward or optimizer.step.
How to fix it
Separate evaluation state and disable unintended updates.
Prevent it next time
Compare relevant parameters and buffers before and after validation.

Check your understanding

1. How are the weight and bias updates obtained from the two initial residuals?

Show worked solution

Mean squared error has derivative 2*r/B for each prediction. With B=2, the weight derivative is (-2)*1+(-4)*2=-10 and the bias derivative is -2-4=-6. Subtracting 0.1 times those values from zero gives one and 0.6. Then recompute predictions before measuring the new loss.

2. Would reducing loss from ten to 1.06 prove that the trained module generalizes?

Show worked solution

No. The same two examples supplied both the update and the displayed loss. The result checks one implementation calculation. Generalization requires an appropriate separate evaluation, legitimate feature availability, a baseline, and evidence under the intended population and selection protocol.

Key takeaways

  • Reset, differentiation, mutation, and measurement are separate boundaries.
  • A manual first step can expose shape and reduction mistakes.
  • A successful training fixture is not external evaluation evidence.

Official references