The training loop — write it yourself
The loss works. Now — the whole loop that you saw in words in step 2. This time you write it without holes: only the skeleton and the contract.
Pseudocode
train(x, y, steps, lr, seed):
set the random seed
W ← small random weights [784, 10], marked "track the gradient"
b ← zeros [10], marked "track the gradient"
history ← an empty list
repeat steps + 1 times:
probs ← forward(x, W, b)
loss ← cross_entropy(probs, y)
save (loss, accuracy) to history
if this is the last time: stop # here we only measure the result
loss.backward() # fills W.grad and b.grad
W ← W − lr · W.grad # the update, NOT tracked
b ← b − lr · b.grad
W.grad ← 0, b.grad ← 0 # clean up
return W, b, history
Notice that the loop runs steps + 1 times: the first measurement is before any update, and the last one after the final update. That is why history has steps + 1 entries.
What is new here
Three things you have to write yourself in Python, and each has its name in PyTorch:
- "Mark it to track the gradient" —
requires_grad_()on a tensor (orrequires_grad=Trueon creation, as in lesson 2). Without itbackward()has nothing to fill. - "The update is not tracked" — the update is not part of the computation we learn the gradient of. Write it inside a
with torch.no_grad():block. Without it PyTorch will try to track the update operation too, and that is not allowed on a "leaf" tensor. - "Clean up" —
W.grad.zero_(). Again lesson 2's fifth breakage: everybackward()adds.
Write the update "in place": W -= lr * W.grad, not W = W - lr * W.grad. The difference is in the next step, but the test will catch it.
Your job
Create train_scratch.py and test_train_scratch.py (right panel).
train_scratch.py has one function, train, with a docstring and raise NotImplementedError. The whole contract is in the docstring; read it, not just the signature. Above — the imports from neuron.py.
test_train_scratch.py runs the training loop for 100 steps on 10,000 pictures and checks:
- the history has 101 entries;
- the starting loss ≈
ln 10, accuracy still low; - every loss is a real number (not
nan); - the loss falls and ends below
1.0; - after one step the accuracy has already risen by at least 5 points;
- after 100 steps accuracy is above 70 %;
- the returned weights have the right shape, and the bias has moved.
The first run (before you write anything) says only one thing:
$ python -m unittest
...................E
ERROR: setUpClass (test_train_scratch.TestTrain)
NotImplementedError
----------------------------------------------------------------------
Ran 19 tests in 0.089s
FAILED (errors=1)
19 dots are your neuron.py tests, passing. Now write train and run again. All the tests together should take a few seconds.
When it does not pass
All of these failures are real; some of them you will see again in the next step.
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.— you wroteW -= ...outside awith torch.no_grad():block.AttributeError: 'NoneType' object has no attribute 'zero_'—W = W - lr * W.gradinstead ofW -= lr * W.grad. The newWis already a different, non-leaf tensor, and its.gradisNone.TypeError: unsupported operand type(s) for *: 'float' and 'NoneType'— you never calledloss.backward().W.gradis stillNone.- three
FAILs at once:loss is nan at step 61,accuracy after 100 steps is 9.4%,loss 2.3207 -> nan— you forgotzero_(). The loop looks like it works — and falls over only after a few dozen steps. That is what step 6 is about.