Matrix shapes, broadcasting, and the five-step loop built from scratch
Do this before Course 4. Goal: the five-line PyTorch training loop stops being an incantation, because you've built each piece in plain Python + NumPy first.
Most "my code is broken" moments in Courses 4–6 are one of these three. Learn to read them now:
import numpy as np
A = np.array([[1., 2., 3.],
[4., 5., 6.]]) # shape (2, 3)
B = np.array([[10., 20.],
[30., 40.],
[50., 60.]]) # shape (3, 2)
print((A @ B).shape) # (2, 2) — works: inner numbers match (2,3)@(3,2)
# print((B @ B).shape) # ERROR — (3,2)@(3,2): 2 ≠ 3
Matrix multiply rule: (m, n) @ (n, p) → (m, p). The two inner numbers must match; the outer two are the answer's shape. When PyTorch says mat1 and mat2 shapes cannot be multiplied (2x3 and 2x3) — it's telling you the inner numbers, 3 and 2, disagree. Not mysterious. Arithmetic.
x = np.array([[1.], [2.], [3.]]) # shape (3, 1) — a column
y = np.array([10., 20., 30.]) # shape (3,) — a flat vector
print((x + y).shape) # (3, 3) !!!
NumPy stretched the column against the row and made a 3×3 grid. Sometimes that's exactly what you want. Sometimes — as the Course 4 notes warn about MSELoss with a (N,) target vs (N,1) prediction — it silently computes the wrong thing with no error. That warning is this exact mechanism.
The defence is one habit: print .shape before you trust any operation. (3,) and (3,1) are different animals; .view(-1, 1) / .reshape(-1, 1) turns the flat one into a column ( -1 means "you figure out this dimension").
(4, 2) @ (2, 7) — legal? Result shape?(5,) + (5,1) dangerous rather than illegal?Course 2 taught the theory: forward → loss → backward → update. Here it is with no framework at all — fitting y = w·x to data, in the Code Lab, right now:
import numpy as np
# data that secretly follows y = 3x
x = np.array([1., 2., 3., 4.])
y = np.array([3., 6., 9., 12.])
w = 0.0 # our guess, starting wrong
lr = 0.05 # learning rate: how big each correction step is
for epoch in range(20):
y_hat = w * x # 1. FORWARD: predict with current w
loss = ((y_hat - y) ** 2).mean() # 2. LOSS: how wrong, on average (MSE)
grad = (2 * (y_hat - y) * x).mean() # 3. BACKWARD: slope of loss wrt w
w = w - lr * grad # 4. STEP: nudge w downhill
print(f"epoch {epoch:2d} w={w:.3f} loss={loss:.3f}")
Run it. Watch w walk from 0.0 to ~3.0 while loss collapses. That's all training is — these four lines, scaled up to millions of parameters.
New syntax decoded:
range(20) — the numbers 0..19; "do this 20 times".** 2 — squared. .mean() — average of everything in the array.grad line is calculus done for you (derivative of MSE) — in PyTorch, loss.backward() computes this automatically for every parameter. That's the entire reason PyTorch exists.f"...{w:.3f}" — the f-string from Primer 2, formatting to 3 decimals.The certificate's five-step PyTorch loop, side by side with what you just built:
for x, y in loader: # batches, not the whole set (mini-batch GD)
optimizer.zero_grad() # clear old gradients — they ACCUMULATE otherwise
y_hat = model(x) # 1. FORWARD (your w * x)
loss = criterion(y_hat, y) # 2. LOSS (your MSE line)
loss.backward() # 3. BACKWARD (your grad line, automated)
optimizer.step() # 4. STEP (your w = w - lr*grad)
One genuinely new thing: zero_grad(). PyTorch adds new gradients onto old ones (useful for advanced tricks, a trap for everyone else), so you wipe the slate each batch. Your NumPy version recomputed grad fresh each loop — that's why it didn't need the wipe. The Course 4 notes call forgetting this "the classic PyTorch bug"; now you know what it actually means.
loss.backward()?lr = 1.2 and rerun. What happens to loss, and which page of the notes predicted it?zero_grad() when the NumPy one didn't?mean() averaged everything. Real code averages along a direction:
X = np.array([[1., 2., 3.],
[4., 5., 6.]]) # (2, 3): 2 samples, 3 features
print(X.mean()) # 3.5 — everything
print(X.mean(axis=0)) # [2.5 3.5 4.5] — down the columns: per-FEATURE mean
print(X.mean(axis=1)) # [2. 5.] — across the rows: per-SAMPLE mean
Memory hook: axis=n is the axis that disappears. axis=0 collapses the rows away, leaving one value per column. Feature scaling in Course 1 (StandardScaler) is exactly (X - X.mean(axis=0)) / X.std(axis=0) — you can now read that.
scores.shape == (32, 10) — 32 images, 10 class scores each. Which axis do you soft-max over, and why?
(m,n) @ (n,p) → (m,p); inner numbers must match; the error message tells you them(N,) meets (N,1); print .shape first, .reshape(-1,1) to fixloss.backward() = "do my derivative line automatically, for every parameter"zero_grad() exists because PyTorch accumulates gradients; wipe per batchaxis=n is the dimension that disappears; axis=0 = per-feature, axis=1 = per-sample