§6.5.3–6.5.5How Back-Propagation Works · MLP Forward & Backward · Symbol-to-Symbol

Part II DL pp. 207–214 · ~4 min read

§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 z=f(f(f(w)))z = f(f(f(w))) — and expand the chain rule fully: dzdw=f(f(f(w)))f(f(w))f(w)\frac{dz}{dw} = f'(f(f(w)))\, f'(f(w))\, f'(w). Written this way, f(w)f(w) 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:

Fig 6.9 (interactive) — back-propagation on z = f(f(f(w))), f(v) = v²: forward values, then the grad_table filling backward
fffw= 1.50xyz

phase: ready · step 0/7

grad_table
∂z/∂w = ?
∂z/∂x = ?
∂z/∂y = ?
∂z/∂z = ?

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:

Algorithms 6.1 + 6.2 — forward computation, then back-propagation (click lines)
  1. 1FORWARD: for i = 1…nᵢ: u(i)xiu^{(i)} \leftarrow x_i (set the inputs)
  2. 2for i = nᵢ+1…n: u(i)f(i)(A(i))u^{(i)} \leftarrow f^{(i)}(\mathbb{A}^{(i)})
  3. 3BACKWARD: grad_table[u(n)u^{(n)}] 1\leftarrow 1
  4. 4for j = n−1 down to 1:
  5. 5grad_table[u(j)u^{(j)}] i:jPa(u(i))\leftarrow \sum_{i:\, j \in Pa(u^{(i)})} grad_table[u(i)u^{(i)}] u(i)u(j)\frac{\partial u^{(i)}}{\partial u^{(j)}}
  6. 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 g\boldsymbol{g} flows down, and at each layer you read off the weight/bias gradients on the way:

Algorithm 6.3 — forward pass of a depth-l MLP
  1. 1h(0)=x\boldsymbol{h}^{(0)} = \boldsymbol{x}
  2. 2for k = 1…l:
  3. 3a(k)=b(k)+W(k)h(k1)\boldsymbol{a}^{(k)} = \boldsymbol{b}^{(k)} + \boldsymbol{W}^{(k)}\boldsymbol{h}^{(k-1)}
  4. 4h(k)=f(a(k))\boldsymbol{h}^{(k)} = f(\boldsymbol{a}^{(k)})
  5. 5y^=h(l),J=L(y^,y)+λΩ(θ)\hat{\boldsymbol{y}} = \boldsymbol{h}^{(l)}, \quad J = L(\hat{\boldsymbol{y}}, \boldsymbol{y}) + \lambda\,\Omega(\boldsymbol{\theta})
Algorithm 6.4 — backward pass (click lines)
  1. 1gy^J=y^L(y^,y)\boldsymbol{g} \leftarrow \nabla_{\hat{\boldsymbol{y}}} J = \nabla_{\hat{\boldsymbol{y}}} L(\hat{\boldsymbol{y}}, \boldsymbol{y})
  2. 2for k = l, l−1, …, 1:
  3. 3ga(k)J=gf(a(k))\boldsymbol{g} \leftarrow \nabla_{\boldsymbol{a}^{(k)}} J = \boldsymbol{g} \odot f'(\boldsymbol{a}^{(k)})
  4. 4b(k)J=g+λb(k)Ω,W(k)J=gh(k1)+λW(k)Ω\nabla_{\boldsymbol{b}^{(k)}} J = \boldsymbol{g} + \lambda\nabla_{\boldsymbol{b}^{(k)}}\Omega,\quad \nabla_{\boldsymbol{W}^{(k)}} J = \boldsymbol{g}\,\boldsymbol{h}^{(k-1)\top} + \lambda\nabla_{\boldsymbol{W}^{(k)}}\Omega
  5. 5gh(k1)J=W(k)g\boldsymbol{g} \leftarrow \nabla_{\boldsymbol{h}^{(k-1)}} J = \boldsymbol{W}^{(k)\top}\boldsymbol{g}

The rhythm of the backward pass

Each layer does three things in order: push the gradient through the nonlinearity (f\odot f'), harvest the weight/bias gradients (outer product with the stored activation), and propagate the gradient down one layer (W\boldsymbol{W}^\top). Its cost mirrors the forward pass — multiply by each W\boldsymbol{W} going up, by each W\boldsymbol{W}^\top going down: O(w)O(w) compute; memory O(mnh)O(m\, n_h) to hold the stored pre-activations.

§6.5.5 Symbol-to-symbol differentiation

Two ways to implement all this:

Term by term

symbol-to-numbervssymbol-to-symbol\text{symbol-to-number} \quad\text{vs}\quad \text{symbol-to-symbol}
symbol-to-number\text{symbol-to-number}take the graph + numeric inputs, return numeric gradients (Torch, Caffe)numbers out
symbol-to-symbol\text{symbol-to-symbol}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 inputgraph out
higher-order\Rightarrow \text{higher-order}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?

5 questions