Mark the variable in the subscript of \(\nabla\)—or the denominator of \(\partial b/\partial a\) for scalars.
Here w and v are the trainable leaves. Their .grad fields will hold the weight derivatives after backward.
The forward pass builds values and a backward graph
print("Before computation, w.grad_fn:", w.grad_fn)z = w * xprint("After computation, z.grad_fn:", type(z.grad_fn).__name__)h = torch.relu(z)s = v * hr = s - yloss =0.5* r.square()print(z.item(), h.item(), s.item(), loss.item())
Output:
Before computation, w.grad_fn: NoneAfter computation, z.grad_fn: MulBackward02.0 2.0 2.0 0.5
Forward values:\(w,x\to z\to h\); then \(v,h\to s\to r\to\ell\).
Backward graph:w is a leaf with no producing operation. Multiplying it creates z with a .grad_fn linking to that operation.
requires_grad=True enables tracking; the forward computation creates the graph.
Backward rules need selected forward values
For our scalar example:
Operation
A value its backward rule needs
Here
\(z=wx\)
\(x\) to obtain \(\partial\ell/\partial w\)
\(2\)
\(h=\operatorname{ReLU}(z)\)
Which inputs were positive
Active
\(s=vh\)
\(h\) for \(v\)’s derivative; \(v\) for \(h\)’s
\(2\), \(1\)
\(\ell=\tfrac12 r^2\)
Residual \(r\)
\(1\)
Saved forward values are not gradients. They let backward evaluate the local derivative rules.
We can observe what autograd saves for backward
saved = []def pack(t): saved.append(t.detach().clone()) # snapshot for inspectionreturn t.detach() # value kept for backwarddef unpack(t):return twith torch.autograd.graph.saved_tensors_hooks(pack, unpack): loss =0.5* (v * torch.relu(w * x) - y).square()print([t.item() for t in saved])
Output:
[2.0, 2.0, 2.0, 1.0, 1.0]
These values supply the multiplication, ReLU, and squared-loss backward rules.
zero_grad() clears the previous accumulated gradients.
It resets gradients, not parameter values. Clear before each independent batch’s backward pass.
step() uses the gradients to change the weights
print("Before: w, v =", round(w.item(), 2), round(v.item(), 2))print("Before: loss =", round(loss.item(), 4))optimizer.step()with torch.no_grad(): new_loss = objective()print("After: w, v =", round(w.item(), 2), round(v.item(), 2))print("After: loss =", round(new_loss.item(), 4))
Output:
Before: w, v = 1.0 1.0Before: loss = 0.5After: w, v = 0.8 0.8After: loss = 0.0392
Here \(w,v: 1\to 1-0.1(2)=0.8\), and loss decreases from \(0.5\) to \(0.0392\).
no_grad() skips recording this evaluation; the next training forward pass records again.
The same operations train the product-image MLP
Implementation · From two scalar weights to 50,890 parameters
Data batch → model → scalar loss → backward → optimizer step.
Now assemble the complete loop, including validation.
A dataset provides examples; a loader assembles batches
from torch.utils.data import DataLoader, TensorDataset# X_train: float32 [12000, 1, 28, 28], pixels in [0, 1]# y_train: int64 [12000], class indices 0,...,9train_dataset = TensorDataset(X_train, y_train)train_loader = DataLoader( train_dataset, batch_size=64, shuffle=True)val_loader = DataLoader( TensorDataset(X_val, y_val), batch_size=256)xb, yb =next(iter(train_loader))print(xb.shape, yb.shape)
Output:
torch.Size([64, 1, 28, 28]) torch.Size([64])
Dataset:train_dataset[i] returns one image–label pair; len(train_dataset) is the number of examples.
DataLoader: iterates over batches of those pairs, handling batch size and shuffling. Here next(iter(train_loader)) returns 64 examples.
nn.Module collects the model’s trainable parameters
from torch import nntorch.manual_seed(570)model = nn.Sequential( nn.Flatten(), nn.Linear(784, 64), nn.ReLU(), nn.Linear(64, 10))print(sum(p.numel() for p in model.parameters()))
Use .item() for a scalar loss; store detached scores for later plots or evaluation metrics. The original loss still supports backward.
Storing loss or logits directly keeps references to their graphs. Logging should keep the numbers, not their training history.
Initialization is part of the training recipe
PyTorch provides useful defaults: standard layers initialize their parameters when constructed; nn.Linear scales its initial values with the input width.
Match the method you reproduce: follow a paper’s specified initialization, including its scale, distribution, and bias settings. Defaults may differ.
Initialization can materially change training and the final model. Record the strategy and random seed; use multiple seeds when comparing results.
torch.nn.init provides strategies such as Xavier and Kaiming initialization. Choose for the architecture and activation; do not replace a paper’s recipe blindly.
Training curves help diagnose the complete loop
Both losses remain high
Training falls, validation rises
Loss oscillates sharply
Check code, fitting, and representation
Revisit capacity, regularization, or stopping
Inspect initialization, scaling, and step size
Running training loss is cheap and useful on large datasets, but mixes changing weights. For a clearer divergence diagnosis, evaluate a fixed model on a representative training subset and validation data using the same loss.
Illustrative curves, not experiment results. Keep test data out of these choices.
PyTorch connects a model, its gradients, and repeated updates
Stage
What we control
Define
Dataset, batches, model, initialization, scalar loss
Differentiate
Forward builds the graph; backward adds parameter gradients
Update
Clear old gradients, compute new ones, then call optimizer.step()
Evaluate
Use held-out data and log values without retaining graphs
The training loop stays useful when the architecture changes. Next, replace unrestricted pixel connections with local, shared filters.