Training Deep Networks
Tensors, broadcasting, and autograd
The array machinery frameworks are built on
What every deep-learning framework quietly does for you: n-dimensional arrays, the broadcasting rules that stretch shapes to fit, and an engine that hands you every gradient for free.
Open any PyTorch or NumPy program and you will find just three ideas doing all the work. Data lives in tensors - n-dimensional arrays. Operations between tensors of different shapes are made to line up by broadcasting. And every operation is quietly recorded so an autograd engine can replay it backward and compute gradients. Two of these you already half-know; the third - broadcasting - is where the majority of real bugs actually live. Let's make all three concrete.
The idea in one line
Tensors: one array type for everything
A tensor is the generalisation of the containers you already met: a scalar is a 0-D tensor, a vector a 1-D tensor, a matrix a 2-D tensor (Lesson 13), and beyond that we just keep adding axes. Its shape is the tuple of sizes along each axis, and its rank is how many axes it has. A batch of RGB images at is one tensor of shape - rank 4 - and the whole point of a framework is to run one operation across that entire block at once, on a GPU, instead of looping.
Axes are read left-to-right but the rules that follow always align from the right, so it pays to think of the last axis as the innermost, fastest-moving one. Get a shape wrong by a single axis and nothing crashes politely - you get silently wrong numbers. That is exactly the failure broadcasting both enables and hides.
Broadcasting: how shapes stretch to fit
You constantly want to combine tensors of different shapes - add a length- bias vector to a whole batch of activations, multiply a matrix by a per-row scale. Broadcasting is the deterministic rule that decides whether that is legal and what the result shape is. It is identical across NumPy, PyTorch, and JAX.
The broadcasting rule
Line the two shapes up from the right-most axis. For each aligned pair of axis lengths, they are compatible when:
they are equal, or one of them is .
A size- axis is stretched - conceptually copied, never actually materialised in memory - to match its partner, and the result takes the larger size on every axis. If two aligned axes disagree and neither is , the operation is illegal: a shape error. The canonical case is a column meeting a row:
Rows: vs - the stretches to . Cols: vs - the stretches to . Every output cell - an outer sum built from just stored numbers, not .
Now play with it. Resize the two tensors below and watch the rule decide: green means an axis lined up, an amber highlight marks a size- axis being stretched to fill the result, and red is the shape error you will meet in real code.
Broadcast succeeds. axis 0 (rows): 3 vs 1 - the 1 stretches to 3 ✓ axis 1 (cols): 1 vs 4 - the 1 stretches to 4 ✓ result shape (3, 4)
The bug that broadcasting hides
print(x.shape) before you print anything else.Autograd: the graph that differentiates itself
Every tensor operation is a node in a computational graph (Lesson 22): the forward pass fills each node's value; the framework simultaneously records how that node was produced. To get gradients it walks the graph backward in reverse order, multiplying in each node's local derivative - reverse-mode automatic differentiation. Below is a single neuron, , unrolled into its graph. Step forward to fill the values, then keep stepping to watch the gradient flow back to every input at once.
Forward pass. The input x = 2 is known. Now fill each node, left to right.
Why this is the whole game
The same engine, in a dozen lines
What makes autograd feel like magic is almost embarrassingly small. Each operation returns a node that remembers how to push gradient back to its inputs. Build the graph forward, seed the tail with , then call those closures in reverse - this is the beating heart of Karpathy's micrograd.
# each op returns a node that remembers its local derivative
def mul(a, b):
out = node(a.data * b.data)
def _backward(): # local: d(a*b)/da = b, d(a*b)/db = a
a.grad += b.data * out.grad
b.grad += a.data * out.grad
out._backward = _backward
return out
# ... build the graph forward, collecting nodes in order, then:
loss.grad = 1.0 # seed the tail: dL/dL
for n in reversed(graph): # reverse topological order
n._backward() # each node hands gradient to its inputsCheck yourself
Broadcasting shapes (3, 1) and (1, 4) yields a result of shape:
Why does a framework record a computational graph on the forward pass?
Recall: state the broadcasting rule for aligning two tensor shapes.
Try to state it, then check.
Lock it in
- Three ideas run every framework: tensors (n-dimensional arrays with a shape), broadcasting (line shapes up and stretch), and autograd (a tape that replays the computation backward).
- Broadcasting rule: align shapes from the right-most axis; two axes are compatible if equal or one is . The size- axis stretches (never copied) and the result takes the larger size on every axis.
- Because a size- axis silently stretches, a wrong shape can still run and produce wrong-shaped numbers with no error - print
x.shapefirst when a loss quietly refuses to drop. - Autograd records the forward computational graph and walks it backward (reverse-mode autodiff), multiplying in each node's local derivative, so every input gets its gradient in one backward sweep.
- Scale the graph to a trillion nodes and the algorithm is unchanged: one forward sweep, one backward sweep, and you never hand-derive a gradient again.
Primary source
micrograd video builds this exact backward-closure pattern from scratch. For tensors, shapes, and the broadcasting rules as your framework enforces them, work through the official PyTorch tensor tutorial.Ask your teacher