§6.5.3 Recursively applying the chain rule
Writing down the gradient with the chain rule is easy; evaluating it
efficiently is the whole game. The trap is repeated subexpressions.
Take the book’s fig 6.9 — a chain — and expand the chain
rule fully: . Written this
way, is recomputed again and again; for a general graph the number
of such repeats grows exponentially with depth. Back-propagation
instead stores each intermediate gradient in a grad_table and reuses
it — a table-filling dynamic-programming strategy that makes the cost
proportional to the number of edges.
Watch it happen: a forward pass fills each node’s value, then the backward pass fills the gradients top-to-bottom, each step one Jacobian-gradient product:
phase: ready · step 0/7
Each backward step is one Jacobian-gradient product: the local derivative f′ times the gradient already stored downstream. Storing them (not recomputing) is what makes backprop O(edges) instead of exponential.
The two halves as algorithms — forward (algorithm 6.1) computes and stores every node; backward (algorithm 6.2) fills the gradient table in reverse:
- 1FORWARD: for i = 1…nᵢ: (set the inputs)ⓘ
- 2for i = nᵢ+1…n: ⓘ
- 3BACKWARD: grad_table[] ⓘ
- 4for j = n−1 down to 1:ⓘ
- 5grad_table[] grad_table[] ⓘ
- 6return grad_table restricted to the inputsⓘ
§6.5.4 Back-propagation in a fully-connected MLP
For an actual network, the forward and backward passes take a particularly clean form. Forward (algorithm 6.3): march up the layers. Backward (algorithm 6.4): the gradient flows down, and at each layer you read off the weight/bias gradients on the way:
- 1ⓘ
- 2for k = 1…l:ⓘ
- 3ⓘ
- 4ⓘ
- 5ⓘ
- 1ⓘ
- 2for k = l, l−1, …, 1:ⓘ
- 3ⓘ
- 4ⓘ
- 5ⓘ
The rhythm of the backward pass
Each layer does three things in order: push the gradient through the nonlinearity (), harvest the weight/bias gradients (outer product with the stored activation), and propagate the gradient down one layer (). Its cost mirrors the forward pass — multiply by each going up, by each going down: compute; memory to hold the stored pre-activations.
§6.5.5 Symbol-to-symbol differentiation
Two ways to implement all this:
Term by term
| take the graph + numeric inputs, return numeric gradients (Torch, Caffe) | numbers out | |
| add NEW NODES to the graph that symbolically describe the derivatives (Theano, TensorFlow, fig 6.10) — a generic engine evaluates them later for any numeric input | graph out | |
| because the derivatives are themselves a graph, you can run back-propagation AGAIN on them — differentiating the derivatives to get Hessians etc. (§6.5.10) | the payoff |
The symbol-to-symbol view subsumes symbol-to-number (it does the same
computations, just without exposing the derivative graph). The next
section formalizes the general algorithm — every operation supplying its
own local bprop rule — and its cost:
general back-propagation.
Check yourself — how back-propagation works
1.For z = f(f(f(w))), the naive chain-rule expansion recomputes f(w) many times. How does back-propagation avoid the exponential blowup?
2.In BackpropLab with w = 1.5, step through to the end. What is ∂z/∂w, and does it match the closed form?
3.In the MLP backward pass (algorithm 6.4), what three things does each layer do, in order?
4.Why does the forward pass STORE the pre-activation a⁽ᵏ⁾?
5.What does the symbol-to-symbol approach (Theano/TensorFlow) enable that symbol-to-number (Torch/Caffe) does not?