Electric Sheaves

Lecture 3 — Backpropagation and Stochastic Optimization

Project connection. Project Step 3 first asks you to build a scalar reverse-mode automatic-differentiation engine. You will validate it on a graph with fan-out, against finite differences, and on the hand-wired diamond network from Step 2. You will then use PyTorch’s tensor-valued version of the same algorithm to train the neural nn-gram model introduced in Lecture 2.

Chapter overview. The cross-entropy objective of a neural network is a scalar function of many parameters. We will represent its evaluation as a computational graph and use the chain rule in two orders. Forward mode carries one input perturbation toward the output. Reverse mode carries one output sensitivity toward every input, which is exactly the shape needed for neural-network training. We then turn from differentiation to optimization: a minibatch supplies a cheap random estimate of the full-corpus gradient, and stochastic gradient descent uses that estimate to update the parameters. These are distinct algorithms. Backpropagation computes a gradient; stochastic gradient descent decides which loss to differentiate and how to use the result.

0. The computational task at hand

We begin with a review of our setting. Let

θ=(θ1,,θP)RP\theta=(\theta_1,\ldots,\theta_P)^\top\in\mathbb R^P

collect all PP trainable parameters of a model. A parameter is a number whose value is chosen during training, such as an entry of a weight matrix, bias vector, or embedding matrix. Suppose the training set contains NN examples and the loss of example ii is i(θ)\ell_i(\theta). As in Lecture 2, the empirical loss is the average

L(θ)=1Ni=1Ni(θ).\mathcal L(\theta) = \frac1N\sum_{i=1}^N\ell_i(\theta).

The word empirical means that the average is taken over observed data. The function being minimized is also called the objective function, or simply the objective.

The gradient is the column vector of first partial derivatives

θL(θ)=(L/θ1L/θP).\nabla_\theta\mathcal L(\theta) = \begin{pmatrix} \partial\mathcal L/\partial\theta_1\\ \vdots\\ \partial\mathcal L/\partial\theta_P \end{pmatrix}.

Lecture 2 defined gradient descent. Written with a possibly changing learning rate ηt>0\eta_t>0, one update is

θ(t+1)=θ(t)ηtL(θ(t)).\theta^{(t+1)} = \theta^{(t)}-\eta_t\nabla\mathcal L(\theta^{(t)}).

The superscript (t)(t) is an index, not an exponent. One execution of the update is an iteration, and ηt\eta_t is the learning rate, or step size. If the gradient uses all NN examples, the method is full-batch gradient descent.

There are now two computational problems.

  1. Differentiation: given a particular loss computation and a particular value of θ\theta, compute its gradient.
  2. Optimization: choose which examples to use, choose a learning rate, and update θ\theta so that the objective becomes small.

Reverse-mode automatic differentiation solves the first problem. Full-batch gradient descent or stochastic gradient descent solves the second.

Think of differentiation as drawing an accurate local arrow on a map: the gradient says which infinitesimal direction raises the loss fastest. Optimization is the travel policy that uses those arrows. It decides how far to move, when to draw a new arrow, and whether to estimate the arrow from the entire dataset or from a small random sample.

Why not differentiate every formula by hand, as we did for logistic and softmax regression in Lecture 2? A neural network may contain millions or billions of parameters, but the more important difficulty is structural: loss along many paths, and the program changes as the architecture changes. We want an algorithm that differentiates the program itself.


1. Computational graphs

A directed graph consists of nodes and directed edges. A directed path is a sequence of directed edges, followed in their arrow directions. A directed cycle is a nonempty directed path that returns to its starting node. A directed graph is acyclic if it has no directed cycle; a finite directed acyclic graph is abbreviated DAG.

A node with no incoming edges is a source. A node with no outgoing edges is a sink. The parents of a node are the nodes with edges into it, and its children are the nodes with edges out of it. A topological ordering of a finite DAG is an ordered list of its nodes so that every parent occurs before its child. Every finite DAG has at least one topological ordering, though it need not have only one.

An edge uvu\to v says that the value at node vv directly depends on the value at node uu.

Definition 1.1 (scalar computational graph). A scalar computational graph is a finite DAG with topologically ordered nodes v1,,vMv_1,\ldots,v_M. Each source node is labelled by a variable. Every non-source node is labelled by a differentiable elementary function of its parents:

vi=φi(vj:jpa(i)),pa(i){1,,i1}.v_i = \varphi_i\bigl(v_j:j\in\operatorname{pa}(i)\bigr), \qquad \operatorname{pa}(i)\subseteq\{1,\ldots,i-1\}.

Here pa(i)\operatorname{pa}(i) is the set of parent indices of node ii, and φi\varphi_i is called the node’s primitive operation, or primitive. The last scalar sink vMv_M is designated as the output.

A computational graph is designed to model the process of computing a complicated function. Assigning values (typically scalars) to the source variables first and then evaluating the primitives in topological order is called the forward pass. The values created between the inputs and output are intermediate values.

A neural network evaluating its loss on one example is exactly such a computational graph. The source nodes hold the inputs and the parameters: the example’s features and target, together with every entry of each weight matrix, bias vector, and embedding matrix. The primitives are the elementary operations the network performs: the multiplications and additions inside each affine layer, the coordinatewise nonlinearity such as tanh\tanh, and the exponentials, sums, and divisions inside softmax and the loss. Each layer is a subgraph, composing layers chains those subgraphs together, and the scalar loss is the output node vMv_M. In a neural network the intermediates are often called activations, meaning values produced by units or layers during the forward pass.

It’s also possible to assign vectors, matrices or higher-dimensional tensors to the source variables (a tensor is a multidimensional rectangular array of numbers). Expanding every tensor entry into its own scalar node recovers a more complicated computational graph, so without loss of generality, we can consider assigning scalars to the variables labelling the source nodes.

We will use one expression throughout the lecture as a running example.

L=(ab+a)tanhb.\mathcal L=(ab+a)\tanh b.

Introduce the intermediate values

u=ab,v=u+a,w=tanhb,L=vw.u=ab, \qquad v=u+a, \qquad w=\tanh b, \qquad \mathcal L=vw.

The following picture is the corresponding computational graph. The formula is nested, but the graph makes every direct dependence explicit.

a b u = ab v = u + a w = tanh b L = vw sources intermediate values scalar sink
The forward pass moves left to right. The input a is used by both multiplication and addition, while b is used by multiplication and tanh. This branching is fan-out; the reverse pass must later add the derivative contributions returning along every branch.

A node has fan-out if it has more than one child. Fan-out means that one value influences the output along more than one directed path. In the figure, both aa and bb have fan-out.

At the Project Step 3 test point (a,b)=(2,1)(a,b)=(2,-1), the forward pass gives

nodeprimitivevalueainput2binput1uab2vu+a0wtanhb0.761594Lvw0.\begin{array}{c|c|c} \text{node} & \text{primitive} & \text{value}\\ \hline a & \text{input} & 2\\ b & \text{input} & -1\\ u & ab & -2\\ v & u+a & 0\\ w & \tanh b & -0.761594\ldots\\ \mathcal L & vw & 0. \end{array}

The local derivative of a primitive is its derivative with respect to one parent while its other parents are held fixed. The local derivative of the primitive vv with respect to parent uu is naturally associated to the edge uvu \rightarrow v, so we can think of this as a labelling of the directed edges. The local derivatives in this graph are

ua=b,ub=a,\frac{\partial u}{\partial a}=b, \qquad \frac{\partial u}{\partial b}=a, vu=1,va=1,wb=1w2,\frac{\partial v}{\partial u}=1, \qquad \frac{\partial v}{\partial a}=1, \qquad \frac{\partial w}{\partial b}=1-w^2,

and

Lv=w,Lw=v.\frac{\partial\mathcal L}{\partial v}=w, \qquad \frac{\partial\mathcal L}{\partial w}=v.

These are deliberately small facts. Automatic differentiation obtains a large derivative by composing local derivatives according to the graph.

Here is the graph with the local derivatives labelling the edges:

a b u = ab v = u + a w = tanh b L = vw ∂u/∂a = b ∂u/∂b = a ∂v/∂u = 1 ∂v/∂a = 1 ∂w/∂b = 1 − w² ∂L/∂v = w ∂L/∂w = v
The same graph, with each edge uv labelled by the local derivative ∂v/∂u of the child with respect to that parent. Automatic differentiation combines exactly these edge labels.

2. The chain rule and forward-mode automatic differentiation

The multivariable chain rule says that if a scalar rr influences a scalar qq through intermediate variables s1,,sms_1,\ldots,s_m, then

dqdr=j=1mqsjsjr.\frac{dq}{dr} = \sum_{j=1}^m \frac{\partial q}{\partial s_j} \frac{\partial s_j}{\partial r}.

The sum is essential: it combines every immediate route from rr through the sjs_j to qq.

Automatic differentiation (AD) is the algorithmic evaluation of derivatives by applying exact local derivative rules and the chain rule to a numerical computation. It is termed automatic because a program constructs and traverses the derivative computation.

Our principal application is to compute the gradient of L\mathcal{L} with respect to the parameters θ=(θ1,,θP)\theta = (\theta_1, \ldots, \theta_P) for gradient descent.

Fix one parameter coordinate θs\theta_s. For every graph node define its tangent

v˙i:=viθs.\dot v_i := \frac{\partial v_i}{\partial\theta_s}.

We initialize the input tangents by

θ˙r=1{r=s},\dot\theta_r=\mathbf1_{\{r=s\}},

where the indicator 1E\mathbf1_E equals 11 when statement EE is true and 00 otherwise. More concretely, this sets θ˙r\dot\theta_r to 11 when r=sr=s and to 00 otherwise.

This initialization is called a seed. It selects the input variable (in this case θs\theta_s) or combination of variables whose influence we want to follow.

Algorithm 2.1 (forward-mode automatic differentiation). Visit the nodes of the graph in topological order. At non-source node viv_i, compute the non-source value

vi=φi(vj:jpa(i)),v_i=\varphi_i(v_j:j\in\operatorname{pa}(i)),

and also compute the tangent

v˙i=jpa(i)φivjv˙j.\dot v_i = \sum_{j\in\operatorname{pa}(i)} \frac{\partial\varphi_i}{\partial v_j}\dot v_j.

The process is called forward mode because values and their tangents move together in the forward topological direction.

By design, at the output node,

v˙M=Lθs.\dot v_M = \frac{\partial\mathcal L}{\partial\theta_s}.

The rule in the definition, of course, is just the chain rule at each node. Its computational efficiency comes from storing the tangent of every intermediate, so each shared subcomputation is performed exactly once per seed.

Example 2.2 (two forward sweeps). For L=(ab+a)tanhb\mathcal L=(ab+a)\tanh b, we perform the full computation needed for the gradient of L\mathcal{L} at point (2,1)(2,-1). This gradient has two entries, namely L/a\partial \mathcal{L} / \partial a and L/b\partial \mathcal{L} / \partial b. To compute the first of these, we use seed a˙=1\dot a = 1, b˙=0\dot b = 0. For the second, we use seed a˙=0\dot a = 0, b˙=1\dot b = 1.

Here is the full computation of L/a\partial \mathcal{L} / \partial a, shown on the computational graph. Each node is labelled with its corresponding value and tangent.

a = 2 ȧ = 1 b = −1 ḃ = 0 u = ab = −2 u̇ = bȧ + aḃ = −1 v = u + a = 0 v̇ = u̇ + ȧ = 0 w = tanh b = −0.7616 ẇ = (1 − w²)ḃ = 0 L = vw = 0 L̇ = wv̇ + vẇ = 0 seed ȧ = 1 seed ḃ = 0 L̇ = ∂L/∂a
One forward sweep with seed ȧ = 1, ḃ = 0 at (a,b) = (2,−1). Each node carries its value (top line) and its tangent (bottom line), computed together in topological order. The output tangent is ∂L/∂a = 0.

The following table shows all the computations for both seeds. The third column corresponds to the computation with seed a˙=1\dot a = 1, b˙=0\dot b = 0 and the fourth column for the other seed.

nodevalue at (2,1)node˙=(node)/anode˙=(node)/ba210b101u=ab2b=1a=2v=u+a01+1=02w=tanhb0.76159401w2L=vw0w(0)+v(0)=0w(2)+v(1w2)=1.523188\begin{array}{c|c|c|c} \text{node} & \text{value at }(2,-1) & \dot{\text{node}} = \partial(\text{node})/\partial a & \dot{\text{node}} = \partial(\text{node})/\partial b\\ \hline a & 2 & 1 & 0\\ b & -1 & 0 & 1\\ u=ab & -2 & b=-1 & a=2\\ v=u+a & 0 & -1+1=0 & 2\\ w=\tanh b & -0.761594 & 0 & 1-w^2\\ \mathcal L=vw & 0 & w(0)+v(0)=0 & w(2)+v(1-w^2)=-1.523188 \end{array}

Therefore

(a,b)L(2,1)=(01.523188).\nabla_{(a,b)}\mathcal L(2,-1) = \begin{pmatrix} 0\\ -1.523188\ldots \end{pmatrix}.

Forward mode need not seed only one coordinate. For an input direction rRPr\in\mathbb R^P, seed θ˙=r\dot\theta=r. The output tangent becomes

L˙=ddεL(θ+εr)ε=0=L(θ)r.\dot{\mathcal L} = \left.\frac{d}{d\varepsilon} \mathcal L(\theta+\varepsilon r) \right|_{\varepsilon=0} = \nabla\mathcal L(\theta)^\top r.

This scalar is the directional derivative of L\mathcal L in direction rr.

To compare algorithmic costs, for positive functions A(s)A(s) and B(s)B(s) we write A(s)=O(B(s))A(s)=O(B(s)) when A(s)/B(s)A(s)/B(s) is bounded above for all sufficiently large ss, and A(s)=Θ(B(s))A(s)=\Theta(B(s)) when it is bounded both above and below by positive constants for all sufficiently large ss. These notations ignore fixed multiplicative constants and describe growth as the problem size increases.

Proposition 2.3 (cost of a full forward-mode gradient). Suppose the output is scalar and the input has PP coordinates. Computing θL\nabla_\theta\mathcal L by coordinatewise forward mode requires PP forward sweeps and therefore costs Θ(P)\Theta(P) times one evaluation, where the hidden constants are determined by the primitive operations.

Proof

One seed ese_s, the ssth standard basis vector, produces only L/θs\partial\mathcal L/\partial\theta_s. Here a standard basis vector has one coordinate equal to 11 and every other coordinate equal to 00. Repeating for s=1,,Ps=1,\ldots,P produces all PP gradient coordinates. Each sweep performs a constant amount of derivative work beside each forward primitive, so its cost is a constant multiple of evaluation. \square

Forward mode is excellent when there are few inputs and many outputs. The proposition above treats a single scalar output, but notice that one forward sweep computes the tangent v˙i\dot v_i of every node in the graph, not only of vMv_M. If a computation has many outputs — say QQ sink nodes rather than one — then a single sweep with seed ese_s delivers the derivatives of all QQ outputs with respect to θs\theta_s simultaneously. The cost of forward mode therefore scales with the number of inputs that must be seeded, and is independent of the number of outputs.

Neural-network training has the opposite shape: perhaps billions of input parameters and one scalar loss. This mismatch motivates reverse mode, which we will cover shortly.


3. Reverse mode and backpropagation

Forward mode asks, “Fix an input. How does this node change with respect to that input?” In other words, we compute vi/a\partial v_i / \partial a for a fixed input aa at each node viv_i. Reverse mode asks the complementary question, “Fix an output. How does this node affect that output?” In other words, we compute L/vi\partial \mathcal{L} / \partial v_i for a fixed output L\mathcal{L} at each node viv_i.

For the fixed scalar output L\mathcal L, define the adjoint of node viv_i by

vˉi:=Lvi.\bar v_i := \frac{\partial\mathcal L}{\partial v_i}.

The bar marks sensitivity of the output to that node. Some software calls the same quantity the node’s gradient.

Theorem 3.1 (reverse accumulation). Set vˉM=1\bar v_M=1 and initialize every other adjoint vˉi=0\bar v_i = 0. Traverse the nodes in reverse topological order. For every edge from parent jj to child ii, perform

vˉj=vˉj+vˉiφivj.\boxed{ \bar v_j = \bar v_j + \bar v_i \frac{\partial\varphi_i}{\partial v_j}. }

Here the equals sign denotes assignment: the right-hand side is computed and stored as the new value of vˉj\bar v_j. After the reverse sweep,

vˉj=Lvj\bar v_j = \frac{\partial\mathcal L}{\partial v_j}

for every node jj. In particular, the adjoints of the parameter nodes are the complete gradient θL\nabla_\theta\mathcal L.

Proof

The output seed is correct because L/L=1\partial\mathcal L/\partial\mathcal L=1. Suppose that the adjoints of all children ii of node jj are correct. The multivariable chain rule gives

Lvj=i:jpa(i)Lvivivj.\frac{\partial\mathcal L}{\partial v_j} = \sum_{i:j\in\operatorname{pa}(i)} \frac{\partial\mathcal L}{\partial v_i} \frac{\partial v_i}{\partial v_j}.

Reverse topological order guarantees that each child’s adjoint is ready before jj is processed. The additive updates give exactly the terms in this sum. Induction backward through the ordering proves the claim. \square

The algorithm is called reverse accumulation because derivative contributions accumulate while moving opposite the forward edges. It is called reverse-mode automatic differentiation because it applies AD in that direction. Backpropagation is the customary neural-network name for reverse-mode AD applied to a loss.

Imagine that the loss sends a one-unit “responsibility message” backward. At an operation, the incoming message is multiplied by each local derivative before being sent to the corresponding parent. If a value was used in several places, several messages return to it and must be added.

The local derivative answers how strongly the child reacts to its parent. The child’s adjoint answers how strongly the loss reacts to the child. Their product answers how strongly the loss reacts along that one edge.

Let us revisit our running example, the graph from Section 1. At (a,b)=(2,1)(a,b)=(2,-1), the values were

u=2,v=0,w=0.761594,L=0.u=-2, \qquad v=0, \qquad w=-0.761594\ldots, \qquad \mathcal L=0.

(This is computed by one simple forward pass.)

Seed Lˉ=1\bar{\mathcal L}=1. We think of each node sending a message back along each of its incoming edges: the message from child ii to parent jj is the child’s adjoint times the local derivative on that edge, and a node’s adjoint is the sum of all the messages it receives. From the primitive L=vw\mathcal L=vw, the messages are

vˉ=Lˉw=0.761594,wˉ=Lˉv=0.\bar v = \bar{\mathcal L}\,w =-0.761594\ldots, \qquad \bar w = \bar{\mathcal L}\,v =0.

(Each of vv and ww has only one child, so a single message is the whole adjoint.) The tanh\tanh node sends bb the message

wˉ(1w2)=0.\bar w(1-w^2)=0.

The addition v=u+av=u+a has local derivative 11 toward each parent, so it sends its adjoint unchanged to both: the message vˉ=0.761594\bar v=-0.761594\ldots goes to uu, and the same message goes to aa. Since vv is the only child of uu,

uˉ=vˉ=0.761594.\bar u =\bar v=-0.761594\ldots.

Finally, u=abu=ab sends aa the message

uˉb=+0.761594,\bar u\, b =+0.761594\ldots,

and sends bb the message

uˉa=1.523188.\bar u\, a =-1.523188\ldots.

Both inputs received two messages, and each adjoint is the sum of the messages received. The two contributions to aˉ\bar a cancel:

aˉ=0.761594+0.761594=0,\bar a = -0.761594\ldots+0.761594\ldots =0,

while

bˉ=0+(1.523188)=1.523188.\bar b = 0+(-1.523188\ldots) =-1.523188\ldots.

Thus reverse mode reproduces the two-sweep forward-mode answer

(a,b)L(2,1)=(0,1.523188),\nabla_{(a,b)}\mathcal L(2,-1) = (0,-1.523188\ldots)^\top,

but it obtains both coordinates in one reverse sweep.

a b u = ab v = u + a w = tanh b L = vw ū·b = +0.7616 ū·a = −1.5232 v̄·1 = −0.7616 v̄·1 = −0.7616 w̄·(1 − w²) = 0 L̄·w = −0.7616 L̄·v = 0 ā = −0.7616 + 0.7616 = 0 b̄ = 0 − 1.5232 ū = −0.7616 v̄ = −0.7616 w̄ = 0 L̄ = 1 (seed)
The reverse sweep at (a,b) = (2,−1). Each green arrow runs backward along a graph edge and carries the message (child adjoint) × (local derivative); a node's adjoint is the sum of the messages it receives. The two messages arriving at a cancel, giving ā = 0, while b receives 0 and −1.5232.

Real neural-network primitives act on tensors rather than scalars. A tensor is just a finite multidimensional array of numbers: a vector is a 11-tensor, and a matrix is a 22-tensor. The shape of the tensor isn’t as important as the number of scalar entries, so let us consider nodes to be labelled with vectors. Let xyx \mapsto y be one directed edge, where xx and yy are the vector labels of the nodes. We write

y=φ(x),xRm,yRq,y=\varphi(x), \qquad x\in\mathbb R^m, \quad y\in\mathbb R^q,

with φ\varphi differentiable. The Jacobian of φ\varphi at xx is the q×mq\times m matrix of partial derivatives

Jφ(x)=[φixj]1iq,1jm.J_\varphi(x) = \left[ \frac{\partial\varphi_i}{\partial x_j} \right]_{1\leq i\leq q,\,1\leq j\leq m}.

In forward mode, we compute y˙\dot y from x˙\dot x. If the input tangent x˙Rm\dot x\in\mathbb R^m is known, the output tangent is

y˙=Jφ(x)x˙,\dot y = J_\varphi(x)\,\dot x,

called a Jacobian—vector product (JVP). This is exactly the directional derivative of φ\varphi at xx in direction x˙\dot x (Section 2).

In reverse mode, we compute xˉ\bar x from yˉ\bar y. If yˉ=yLRq\bar y=\nabla_y\mathcal L\in\mathbb R^q is known, the chain rule gives

xˉ=Jφ(x)yˉ.\bar x = J_\varphi(x)^\top\bar y.

Equivalently, in row-vector notation, yˉJφ(x)\bar y^\top J_\varphi(x) is a vector—Jacobian product (VJP). Reverse-mode libraries implement a VJP rule for each primitive.

An arithmetic circuit is a computational graph whose primitives are arithmetic operations such as addition, multiplication, and division where the denominator is nonzero. Its size is its number of operation nodes. We first state the classical result for such circuits; standard AD systems extend the same idea with derivative rules for primitives such as exponential, logarithm, and tanh\tanh.

Theorem 3.2 (cheap-gradient principle; Baur—Strassen). Suppose a scalar function is evaluated by an arithmetic circuit of size ss. Its full gradient with respect to every input can be evaluated by a circuit of size O(s)O(s). Consequently, reverse-mode differentiation costs a constant multiple of the forward evaluation, independent of the number PP of inputs.

Proof sketch

Store the value produced by every node during the forward evaluation. In reverse topological order, visit each edge once. Each visit multiplies a child adjoint by one local derivative and adds the result to a parent adjoint. An arithmetic primitive has a fixed number of parents, and computing each local derivative takes a fixed amount of work. The total reverse work is therefore at most a constant times the number of forward operations. The Baur—Strassen theorem makes this construction precise for rational arithmetic circuits, circuits whose operation nodes use addition, subtraction, multiplication, and division. \square


4. Backpropagation through softmax and a neural nn-gram

We now write every step of backpropagation for a concrete model: the Bengio-style neural nn-gram language model of Lecture 2, Section 4.4, which is also the model trained in Project Step 3. The model predicts the next token from the previous kk tokens. It looks up a learned embedding vector for each context token, concatenates the kk embeddings into one vector xx, passes xx through a single tanh\tanh hidden layer, and applies one more affine map followed by softmax to produce a probability for each of the VV tokens in the vocabulary. The loss is the negative log of the probability assigned to the token that actually came next. Schematically,

context tokens    x  affine  u  tanh  hctx  affine  z  softmax  p    =logpy.\text{context tokens} \;\longmapsto\; x \;\overset{\text{affine}}{\longmapsto}\; u \;\overset{\tanh}{\longmapsto}\; h_{\mathrm{ctx}} \;\overset{\text{affine}}{\longmapsto}\; z \;\overset{\text{softmax}}{\longmapsto}\; p \;\longmapsto\; \ell=-\log p_y.

Section 4.1 derives the reverse rule for the softmax cross-entropy loss, a short interlude records the local rules for affine maps and coordinatewise tanh\tanh, and Section 4.2 states the architecture precisely and assembles the rules into the complete backward pass.

4.1 Backpropagation: loss with respect to logit

Let z=(z1,,zV)RVz=(z_1,\ldots,z_V)^\top\in\mathbb R^V be the logit vector: one unconstrained score for each of VV possible target tokens. Our goal in this subsection is to compute z\nabla_z \ell.

Lecture 2 defined

pj=softmax(z)j=ezjc=1Vezc.p_j = \operatorname{softmax}(z)_j = \frac{e^{z_j}}{\sum_{c=1}^V e^{z_c}}.

Let y{1,,V}y\in\{1,\ldots,V\} be the target-token index. Its one-hot vector eyRVe_y\in\mathbb R^V has a 11 in coordinate yy and zeros elsewhere. Define the log-sum-exp function

lse(z)=logc=1Vezc.\operatorname{lse}(z) = \log\sum_{c=1}^V e^{z_c}.

The single-example categorical cross-entropy loss (Lecture 2, Definition 3.2) is

(z,y)=logpy=zy+lse(z).\ell(z,y) = -\log p_y = -z_y+\operatorname{lse}(z).

Proposition 4.1 (softmax cross-entropy gradient). With p=softmax(z)p=\operatorname{softmax}(z),

z=pey.\boxed{ \nabla_z\ell=p-e_y. }

The coordinates of this gradient sum to zero.

Proof

For coordinate jj,

zjlse(z)=ezjcezc=pj,\frac{\partial}{\partial z_j}\operatorname{lse}(z) = \frac{e^{z_j}}{\sum_c e^{z_c}} =p_j,

while

(zy)zj=1{j=y}.\frac{\partial(-z_y)}{\partial z_j} = -\mathbf1_{\{j=y\}}.

Thus /zj=pj1{j=y}\partial\ell/\partial z_j=p_j-\mathbf1_{\{j=y\}}, which is the jjth coordinate of peyp-e_y. Both pp and eye_y have coordinate sum one, so their difference has sum zero. \square

The vector peyp-e_y is often called the output error signal, meaning the adjoint at the logit vector from which the rest of backpropagation starts. It is not the difference between a predicted token and a token number; token indices have no numerical distance. It is a difference between two probability vectors.

Example 4.2 (reading the error signal). Take

z=(0,log2,log3),p=(16,26,36),z=(0,\log2,\log3)^\top, \qquad p=\left(\frac16,\frac26,\frac36\right)^\top,

and suppose the second label is correct. Then

z=pe2=(16,46,36).\nabla_z\ell = p-e_2 = \left(\frac16,-\frac46,\frac36\right)^\top.

A negative-gradient update lowers logits 11 and 33 and raises target logit 22. The amount assigned to each wrong label is its current probability, while the target coordinate receives p21p_2-1. The update is largest when the model assigns little probability to the target.

Backpropagation: derivatives of affine maps and tanh

We need only affine maps and coordinatewise tanh\tanh to propagate the output signal through the Step 3 network. Here are some useful equations for relevant derivatives.

First suppose

q=Wr+b,q=Wr+b,

where WRm×nW\in\mathbb R^{m\times n}, rRnr\in\mathbb R^n, and b,qRmb,q\in\mathbb R^m. If the incoming adjoint is qˉ=q\bar q=\nabla_q\ell, then

rˉ=Wqˉ,W=qˉr,b=qˉ.\boxed{ \bar r=W^\top\bar q, \qquad \nabla_W\ell=\bar q\,r^\top, \qquad \nabla_b\ell=\bar q. }

The matrix qˉr\bar q\,r^\top is an outer product: the product of a column vector and a row vector. Its (i,j)(i,j) entry is qˉirj\bar q_i r_j, as required because qi=jWijrj+biq_i=\sum_jW_{ij}r_j+b_i.

Next suppose h=tanhuh=\tanh u coordinatewise. Since d(tanhs)/ds=1tanh2sd(\tanh s)/ds=1-\tanh^2s,

uˉ=hˉ(1hh).\boxed{ \bar u = \bar h\odot(1-h\odot h). }

The symbol \odot denotes the Hadamard product, or elementwise product: (rs)i=risi(r\odot s)_i=r_is_i. The 11 in the formula denotes the vector of ones of the appropriate length.

These rules illustrate a general principle: the forward shapes determine the backward shapes. If WW is m×nm\times n, then its gradient must also be m×nm\times n; if rr has length nn, then WqˉW^\top\bar q has length nn. Checking shapes catches many backpropagation mistakes before checking numbers.

4.2 One complete backward pass

Let us recall the notation of the neural nn-gram model of Lecture 2 (Example 4.7). Let CRV×dC\in\mathbb R^{V\times d} be an embedding matrix. An embedding is a learned vector representation, and row CaRdC_a\in\mathbb R^d represents token aa. For a context (a1,,ak)(a_1,\ldots,a_k), an embedding lookup reads those rows and concatenates them:

x=concat(Ca1,,Cak)Rkd.x = \operatorname{concat}(C_{a_1},\ldots,C_{a_k}) \in\mathbb R^{kd}.

Concatenation joins vectors end to end. The network then computes

u=W1x+b1,hctx=tanhu,u=W_1x+b_1, \qquad h_{\mathrm{ctx}}=\tanh u, z=W2hctx+b2,p=softmax(z),=logpy.z=W_2h_{\mathrm{ctx}}+b_2, \qquad p=\operatorname{softmax}(z), \qquad \ell=-\log p_y.

Here

W1Rh×kd,b1Rh,W2RV×h,b2RV.W_1\in\mathbb R^{h\times kd}, \quad b_1\in\mathbb R^h, \quad W_2\in\mathbb R^{V\times h}, \quad b_2\in\mathbb R^V.

The vector uu is the hidden preactivation, the value before the nonlinear tanh\tanh is applied. The vector hctxh_{\mathrm{ctx}} is the hidden representation of this context.

lookup C[aᵢ] and concatenate x ∈ Rᵏᵈ context vector u = W₁x + b₁ hctx = tanh(u) z = W₂hctx + b₂ V logits p = softmax(z) ℓ = −log pᵧ forward: values and activations zℓ = p − eᵧ u x scatter-add backward: adjoints, parameter gradients, and embedding-row updates shared matrix hidden width h one scalar loss
The forward pass follows the upper arrows. The reverse pass (backpropagation) is shown by the lower arrows.

The logit gradient, also called the output error signal, is

z=pey.\nabla_z\ell =p-e_y.

Applying the tanh and affine rules above gives

W2=(z)hctx,b2=z,\nabla_{W_2}\ell = (\nabla_z\ell)h_{\mathrm{ctx}}^\top, \qquad \nabla_{b_2}\ell = \nabla_z\ell, hˉctx=W2z,\bar h_{\mathrm{ctx}} = W_2^\top\nabla_z\ell, u=hˉctx(1hctxhctx),\nabla_u\ell = \bar h_{\mathrm{ctx}} \odot (1-h_{\mathrm{ctx}}\odot h_{\mathrm{ctx}}), W1=(u)x,b1=u,\nabla_{W_1}\ell = (\nabla_u\ell)x^\top, \qquad \nabla_{b_1}\ell = \nabla_u\ell,

and

xˉ=W1u.\bar x = W_1^\top\nabla_u\ell.

Finally, split xˉRkd\bar x\in\mathbb R^{kd} into kk consecutive blocks of length dd. The block for position rr is added to the gradient of row CarC_{a_r}. If the same token occurs in two context positions, both blocks are added to the same row. This operation is often called a scatter-add: values associated with indices are added back into the corresponding locations of a larger array.

The embedding matrix exhibits parameter sharing. Parameter sharing means that the same parameter participates in several parts of the computation. Every occurrence of token aa reads the same row CaC_a, so the row has fan-out across positions and examples. Its gradient is the sum of all returning contributions.

For a minibatch, each example acquires a leading batch coordinate. The same equations then use batched matrix multiplication, and parameter gradients sum or average over the batch coordinate.

Backpropagation is modular. Softmax cross-entropy knows only how to turn a target and logits into z\nabla_z\ell. The second affine layer knows only how to turn z\nabla_z\ell into gradients for W2,b2W_2,b_2, and its input. Tanh knows only its local derivative. The first affine layer and embedding lookup do the same. Each module receives the sensitivity of its output and returns the sensitivity of its inputs.

This modularity is why changing one layer does not require re-deriving the entire network by hand. The AD library needs one correct local rule for the new primitive.


5. Autograd in practice

Autograd is the common name for a software system that records differentiable operations and automatically computes their derivatives. It is an implementation of automatic differentiation, not a different calculus algorithm.

5.1 What a scalar Value object stores

Project Step 3 wraps every scalar in a Value object. Conceptually, each object stores four things:

  1. data: the node’s forward value;
  2. grad: its adjoint, initialized to zero;
  3. _parents: the nodes on which it directly depends; and
  4. _backward: the local VJP rule that adds this node’s contribution to its parents.

A leaf node is a source node created directly by the user, such as a parameter or input. A root for a backward pass is the output node from which reverse traversal begins—normally the scalar loss. A topological sort is an algorithm that constructs a topological ordering. A depth-first search follows one unvisited dependency path as far as it can before returning to try another. The small engine performs such a search from the loss, appends a node after visiting its parents, and reverses the resulting list for backpropagation.

For multiplication

q=rs,q=rs,

the closure stored on qq implements

r.grad+=q.grads.data,s.grad+=q.gradr.data.r.\mathrm{grad}\mathrel{+}=q.\mathrm{grad}\,s.\mathrm{data}, \qquad s.\mathrm{grad}\mathrel{+}=q.\mathrm{grad}\,r.\mathrm{data}.

A closure is a function that retains access to values from the scope in which it was created. Here the local backward closure remembers rr, ss, and qq even after the forward multiplication has returned.

Calling loss.backward() then performs exactly Theorem 3.1:

  1. topologically order the reachable graph;
  2. set the loss gradient to 11;
  3. traverse the order backward; and
  4. call each node’s local backward rule.

5.2 Why gradients accumulate and must be cleared

AD systems use addition for two different kinds of accumulation.

  • Within one graph, fan-out creates several paths to the same node.
  • Across several backward calls, a user may intentionally sum gradients from several losses or microbatches. A microbatch is one small part of a larger logical batch processed separately to save memory.

For this reason, PyTorch normally adds into .grad rather than replacing it. A training loop that wants a fresh minibatch gradient must clear old parameter gradients before the next backward pass. Forgetting this step silently changes the update into a sum over the current and previous minibatches.

The parameter update itself should not become part of the differentiated model computation. PyTorch’s torch.no_grad() context temporarily disables graph recording. A tensor is detached when it shares numerical data with a computation but is treated as having no derivative connection to that computation. Detaching in the middle of a model accidentally cuts the backward path; detaching a diagnostic value or disabling recording during the parameter update is intentional.

Algorithm 5.1 (the autograd training cycle). Repeat:

  1. Forward: evaluate a scalar minibatch loss while recording the graph.
  2. Clear: set parameter gradients from the previous iteration to zero or to “not yet allocated.”
  3. Backward: seed the loss adjoint with 11 and reverse the graph.
  4. Update: with graph recording disabled, replace each parameter by parameter minus learning rate times gradient.

The next forward pass builds a new graph from the updated parameter values.

5.3 Three independent checks

A derivative implementation is most convincing when independent methods agree.

  1. Hand calculation: derive a small graph with the chain rule.
  2. Finite differences: compare selected coordinates with nearby loss evaluations.
  3. Independent systems: compare your scalar engine, finite differences, and PyTorch on one input to the Step 2 diamond network.

For scalars gg and g^\widehat g, the absolute error is gg^|g-\widehat g|. A scale-aware comparison often uses the relative error

gg^max(1,g,g^).\frac{|g-\widehat g|} {\max(1,|g|,|\widehat g|)}.

The denominator avoids declaring two tiny, harmless numbers wildly different. No single numerical error threshold fits every data type and graph, but Step 3’s small float64 checks should agree much more closely than a large low-precision training run.

Common failure patterns are diagnostic:

  • correct chain graphs but incorrect fan-out graphs suggest assignment instead of addition;
  • gradients missing from an early node suggest a wrong traversal order or an accidental detach;
  • gradients exactly multiplied by the number of iterations suggest that old gradient values were not cleared;
  • plausible AD gradients that disagree with finite differences only at extreme step sizes suggest numerical, rather than chain-rule, error.

6. Connection to Project Step 3

6.1 From the scalar engine to the neural model

Step 3 first validates Theorem 3.1 on L=(ab+a)tanhb\mathcal L=(ab+a)\tanh b. It then rebuilds one forward pass of the Step 2 diamond classifier from scalar Value\texttt{Value} objects and differentiates its loss with respect to the input. The closed formula, finite differences, your engine, and PyTorch all produce the same gradient. This closes the loop between a network assembled by hand and an algorithm that differentiates its computational graph.

The project next trains the neural nn-gram model of Section 4 with minibatch SGD. Its parameter count is

Vd+(kd)h+h+hV+V.Vd+(kd)h+h+hV+V.

The five terms count the embedding matrix CC, first-layer weights W1W_1, first-layer bias b1b_1, second-layer weights W2W_2, and second-layer bias b2b_2. The count grows linearly with context length kk, whereas an unrestricted next-token matrix indexed by every length-kk context has Vk+1V^{k+1} entries.

The training set is the set used to update parameters. A disjoint validation set is held out from parameter updates and used to estimate performance on unseen examples during model development. Training loss and validation loss are the same loss formula averaged over these two sets.

6.2 Reading the context-length sweep

The reference run in Project Step 3 reports:

context length kkparameterstraining lossvalidation lossbits/character
111,6012.47062.48053.579
315,6971.91711.98212.860
519,7931.90542.00182.888
825,9371.93722.03292.933

A nat is the information unit obtained when cross-entropy uses natural logarithms. A bit is the corresponding unit for base-22 logarithms; dividing loss in nats by log2\log2 gives bits per predicted character.

When k=1k=1, the model sees only the immediately preceding character, just as a bigram model does. The hidden network is wide enough to assign an independent logit vector to each of the finitely many input characters, so its model family contains the bigram behavior. Its validation loss should therefore be near the earlier bigram result when optimization succeeds. A model family, or hypothesis class, is the set of functions obtainable as its parameters vary.

Moving from k=1k=1 to k=3k=3 supplies useful context and sharply improves validation loss. Moving to k=5k=5 or k=8k=8 does not help at the fixed hidden width and training budget. Model capacity is the range and complexity of functions a model family can represent. Flattening the longer vector spreads the fixed hidden capacity across more inputs and also makes the optimization problem harder.

An optimization bottleneck occurs when the training procedure fails to find a low-loss member of the model family. A representational bottleneck occurs when the family itself cannot express the needed function. Both can produce underfitting, meaning that important patterns remain unfitted even in the training data. Overfitting has a different signature: training loss improves while validation performance worsens because the model has specialized too strongly to the training set.

The k=8k=8 run has worse training loss than the k=3k=3 run, so its poorer validation result cannot be explained by classic overfitting alone. The small k=5k=5 training improvement leaves room for a mixture of effects, but the matrix as a whole points toward representational and optimization bottlenecks rather than a large train—validation gap.

More available context is not automatically more usable context. The Step 3 model flattens all kk embeddings into one fixed vector and gives every context the same pattern of connections. A longer window increases the amount the first layer must disentangle, but does not give it a way to select which position matters for this particular prediction.

Lecture 4 introduces attention: a mechanism that forms content-dependent weighted combinations of context positions. The weights can change from one input to another, allowing the model to retrieve relevant earlier information without treating the entire context as one undifferentiated address.

6.3 What to carry forward

The scalar engine is intentionally small, but it exposes principles that remain true in large systems:

  • computations form a DAG for one forward pass;
  • local VJP rules compose into a global gradient;
  • fan-out requires addition;
  • reverse order is a dependency requirement, not a coding preference;
  • tensor backpropagation avoids materializing giant Jacobians;
  • minibatching changes the gradient estimator, not the chain rule; and
  • clearing gradients and excluding updates from the graph are explicit parts of the training algorithm.

Summary

A differentiable program can be represented as a computational graph. Forward mode propagates a tangent from selected inputs toward all later nodes and naturally computes Jacobian—vector products. Computing every coordinate of a scalar-output gradient this way requires one sweep per parameter.

Reverse mode seeds the scalar loss with adjoint 11 and propagates sensitivities in reverse topological order. Each edge contributes a child adjoint times a local derivative, and fan-out contributions are added. This produces every parameter derivative in one reverse sweep. The cheap-gradient principle says the arithmetic cost is a constant multiple of the forward computation, while stored or recomputed activations account for the memory cost.

For softmax cross-entropy, the logit adjoint is peyp-e_y. Affine, tanh\tanh, and embedding-lookup VJPs route this signal through the Bengio neural nn-gram model. The same rules operate on scalars in your Value engine and on tensors in PyTorch.

In practice, training pairs backpropagation with minibatch stochastic gradient descent: a random batch of BB examples supplies a cheap estimate of the full-corpus gradient that is correct on average, and the parameters move against it by a learning rate. Batch size, learning-rate schedule, and hardware throughput are tuned together; Lecture 6 takes up these practical choices.

Exercises (paired with Step 3)

A star marks a Project Step 3 task.

  1. ★ For L=(ab+a)tanhb\mathcal L=(ab+a)\tanh b at (a,b)=(2,1)(a,b)=(2,-1), reproduce Example 2.2 and the reverse sweep of Section 3 without looking at the tables. Label every local derivative and every accumulated contribution.
  2. ★ Implement scalar Value primitives for addition, multiplication, tanh\tanh, exponential, logarithm, and powers. Explain which forward values each backward closure must retain.
  3. Construct the smallest computational graph you can find on which replacing addition by assignment during reverse accumulation gives a wrong derivative. Give values for which the wrong answer does not accidentally equal the right one.
  4. Give a DAG with two valid topological orderings. Reverse one valid ordering and verify Theorem 3.1. Then exhibit an invalid reverse order and identify the contribution it loses.
  5. Compute the directional derivative of the running example at (a,b)=(2,1)(a,b)=(2,-1) in direction r=(3,2)r=(3,-2)^\top in two ways: one forward-mode sweep and the dot product Lr\nabla\mathcal L^\top r.
  6. ★ Prove Proposition 4.1. Verify it with your scalar engine and with PyTorch for a randomly chosen five-logit vector.
  7. Derive the affine-layer VJP coordinate by coordinate. Check the shapes of all three outputs rˉ\bar r, W\nabla_W\ell, and b\nabla_b\ell.
  8. For a context (a,b,a)(a,b,a), write explicitly how the three blocks of xˉ\bar x scatter-add into the embedding-matrix gradient. Which rows are exactly zero for this one example?
  9. Count the activations stored by an LL-layer width-hh MLP on a batch of size BB. Propose a checkpointing scheme and state what it stores and recomputes.
  10. Show that permuting the hh hidden coordinates of a one-hidden-layer network, together with the corresponding rows and columns of its weight matrices, preserves the represented function.
  11. ★ At x=(0.5,0.25)\mathbf{x}=(0.5,0.25)^\top and γ=4\gamma=4, use the Step 2 diamond network with target “inside.” Derive /x1=/x2=2γp(outsidex)\partial\ell/\partial x_1=\partial\ell/\partial x_2 =2\gamma p(\mathrm{outside}\mid\mathbf{x}). Reproduce both derivatives with your scalar engine, central finite differences, and PyTorch autograd, and report the maximum absolute disagreement.
  12. In Algorithm 5.1, deliberately omit gradient clearing for three identical forward/backward passes without updating parameters. Predict the result before running it, then explain the observed gradient values.
  13. The k=8k=8 reference model has worse training and validation loss than the k=3k=3 model. Explain why this evidence points toward underfitting rather than overfitting, and name at least two interventions that would distinguish an optimization bottleneck from a representational bottleneck.

Pointers

Baydin, Pearlmutter, Radul, and Siskind, Automatic Differentiation in Machine Learning: a Survey; Griewank and Walther, Evaluating Derivatives; Baur and Strassen (1983); Robbins and Monro (1951); Bengio, Ducharme, Vincent, and Jauvin, A Neural Probabilistic Language Model (2003); and Olah’s backpropagation essay. See the resources page, Project Step 3, and Lecture 2.