Theory

Watch: a batch, a model, one update

You write nothing here. Below is a real Python console session: we look at what the new parts do and check, number by number, that it is the same thing you did by hand in lesson 4.

Same data: the first 10 000 FashionMNIST images (load_fashion("train", n=10_000) from fashion.py), with torch.manual_seed(42) first.

1. Batches

>>> loader = DataLoader(TensorDataset(x, y), batch_size=64, shuffle=True)
>>> len(loader)
157
>>> xb, yb = next(iter(loader))
>>> xb.shape, yb.shape
(torch.Size([64, 784]), torch.Size([64]))
>>> yb[:8]
tensor([9, 4, 9, 8, 5, 4, 2, 7])
>>> [len(b[0]) for b in loader][-2:]
[64, 16]

TensorDataset(x, y) pairs each image with its label and DataLoader deals them out in batches. shuffle=True means a different order every epoch.

2. The model as one object

>>> model = nn.Linear(784, 10)
>>> [p.shape for p in model.parameters()]
[torch.Size([10, 784]), torch.Size([10])]
>>> sum(p.numel() for p in model.parameters())
7850

3. One update — two routes, the same result

>>> loss_fn = nn.CrossEntropyLoss()
>>> loss = loss_fn(model(xb), yb)
>>> loss.item()
2.358222723007202
>>> loss.backward()
>>> w_before = model.weight.detach().clone()
>>> grad = model.weight.grad.clone()
>>> by_hand = w_before - 0.1 * grad               # lesson 4's formula
>>> torch.optim.SGD(model.parameters(), lr=0.1).step()

The story of one weight ([0, 300]):

before           -0.012392
gradient         -0.033848
after (by hand)  -0.009007     -0.012392 - 0.1 * (-0.033848)
after (optimizer)-0.009007
>>> torch.allclose(by_hand, model.weight.detach())
True
>>> (by_hand - model.weight.detach()).abs().max().item()
3.725290298461914e-09
>>> loss_fn(model(xb), yb).item()      # same batch, after one update
2.0123372077941895

What to notice

1. A batch is the same kind of matrix as in lesson 4 — just 64 rows. xb is [64, 784]. Your whole formula works unchanged. 10 000 images = 156 full batches of 64 plus one leftover of 16 (156·64 + 16 = 10000), so one epoch = 157 updates, not one.

2. nn.Linear holds the same 7 850 numbers, stored transposed. In lesson 4 W was [784, 10]; nn.Linear(784, 10) stores weight as [10, 784] and computes x @ weight.T + bias itself. Same count: 784·10 + 10. model.parameters() lists all of them — which is why the optimizer no longer needs you to tell it what to update.

3. optimizer.step() is not magic. It is exactly W -= lr * W.grad: the difference between "by hand" and "optimizer" is 3.7·10⁻⁹, which is just floating-point rounding. And the loss on that same batch dropped from 2.358 to 2.012 after one update.

The numbers here are from my run. If you repeat exactly the same sequence you get the same ones; otherwise different batches and different starting weights give different numbers — but you will still see the three observations.