The Backward Pass

Think with AI
Think with AI
%alireza rashidi data science%
Twelve ways to fine-tune an LLM
The Backward Pass — Backpropagation as a Sensitivity Program
Learning Systems Series · Backpropagation

The backwardpass.

Your loss goes down. Your gradients are wrong. Both can be true at once. To see why, follow one small network through its forward computation, reverse derivatives, and parameter update. The important part is a value used twice—and the contribution you might forget to add.

01— The forward program

One prediction.
Three separate jobs.#

A training step contains a model evaluation, a derivative computation, and a parameter update. Keeping those jobs separate makes the mathematics easier to understand—and the implementation easier to debug.

Backpropagation is reverse-mode automatic differentiation applied to a model’s computation graph. It calculates derivatives. Gradient descent uses those derivatives to change parameters. You can run the backward pass without changing a single weight.[1][2]

Use this deliberately small scalar network. The input is x = 2, the target is t = 1, and the trainable parameters are θ = (w, b, v) = (0.5, 0, 1). Every example below uses this same network and this same training pair.

u = wx   ·   z = u + b   ·   a = tanh(z) ŷ = va + uL(θ; x, t) = ½(ŷ − t)² The added u is a skip path. It reuses the same computed value, rather than introducing another parameter.

The model maps an input to a prediction: x ↦ ŷ. The training objective maps a parameter setting to a loss: θ ↦ L, with x and t held fixed. Those are different functions. Here, the prediction is 1.761594 and the loss is 0.290013, rounded to six decimals.

The factor ½ cancels the 2 introduced when you differentiate a square. It changes the loss scale, not the parameter settings where this squared error is minimized. This is a single-example objective; a dataset objective would aggregate losses across examples.

01 · EVALUATEForward computation

Calculate intermediate values, prediction, and loss at the current parameters.

02 · DIFFERENTIATEBackward computation

Calculate how the scalar loss changes locally with each parameter.

03 · UPDATEOptimizer

Choose how far and in which direction to change those parameters.

Notice what you need to retain: the backward rule for tanh uses its forward output a, while multiplication needs its operands. Autodiff systems save selected intermediate tensors for that reason. The graph records dependencies; it is not a diagram of biological neurons.[2]

Five operations, one reused value
Five operations, one reused value The input is multiplied by w and then used in both a nonlinear branch and a direct skip path before the squared-error loss is calculated. FORWARD · evaluate the model, then the loss The same u enters the prediction a second time u = wx 1.000000 z = u + b 1.000000 a = tanh(z) 0.761594 ŷ = va + u 1.761594 L = ½(ŷ − t)² 0.290013 Read left → right. Parameters: w = 0.5, b = 0, v = 1. Data: x = 2, t = 1.
How to read this: Each box calculates the expression above its value. Follow the lower branch: u affects ŷ directly as well as through tanh. Constants and parameters are written in the legend instead of repeated as separate nodes. All numbers come from the stated equations.
02— The reverse program

Multiply locally.
Accumulate globally.#

At each operation, incoming sensitivity is multiplied by a local derivative. When several downstream uses depend on the same value, their contributions add. That addition is the central event in our example.

Write q̄ = ∂L/∂q for each node. This is an adjoint, not a new forward activation. Initialize adjoints to zero and seed the loss with L̄ = 1, because ∂L/∂L = 1. Then visit operations in reverse topological order, after their downstream contributions are available.[3]

Start at the loss

e = ŷ − t = 0.761594ŷ̄ = ∂L/∂ŷ = e = 0.761594

The prediction is too high. A sufficiently small increase in ŷ would increase this loss. Now distribute that sensitivity through ŷ = va + u. Multiplication sends each operand the other operand times the incoming adjoint; addition sends the incoming adjoint to each input.

v̄ = ŷ̄a = 0.580026ā = ŷ̄v = 0.761594direct = ŷ̄ = 0.761594

Return through the nonlinear branch

The derivative of tanh(z) is 1 − tanh²(z). You already computed a = tanh(z) in the forward pass, so you can use 1 − a² = 0.419974. Continue through z = u + b:

z̄ = ā(1 − a²) = 0.319850b̄ = z̄ = 0.319850nonlinear = z̄ = 0.319850

The shared value needs both contributions

Changing u changes the direct term in ŷ and the input to tanh. Neither path cancels the other. The total derivative adds their effects before propagating through u = wx:

ū = ūdirect + ūnonlinear  = 0.761594 + 0.319850 = 1.081444w̄ = xū = 2.162888

This is why a tiny autodiff engine uses += when writing adjoints. An assignment that overwrites a previous contribution can silently differentiate the wrong computation. Karpathy’s micrograd makes both reverse traversal and accumulation visible in a small implementation.[3]

A node’s adjoint belongs to all its downstream uses.Our final parameter gradient, in the order (w, b, v), is (2.162888, 0.319850, 0.580026).
Sensitivities return through both uses of u
Sensitivities return through both uses of u The reverse pass starts with the loss seed one and adds the direct and nonlinear contributions at u before computing the weight gradient. BACKWARD · local sensitivity, at fixed parameters Direct contribution to ū = 0.761594 1.081444 0.319850 0.761594 ŷ̄ 0.761594 1.000000 Read right → left. Bars mean ∂L/∂node; displayed values are rounded.
How to read this: Start at L̄ = 1 on the right. The upper route contributes 0.319850 to ū; the lower route contributes 0.761594. Add them before multiplying by x to obtain ∂L/∂w. These are derivatives at the original parameter values, not updated weights.
03— The executable argument

A falling loss can
hide a broken gradient.#

Verify two separate claims: whether the backward pass computes the derivative of your forward program, and whether a particular optimizer step improves the objective. Neither test substitutes for the other.

For plain gradient descent, calculate θ′ = θ − ηg, where g = ∇L(θ) and η is the learning rate. Update all three parameters from the original gradient. At η = 0.10, the new parameters are approximately (0.283711, −0.031985, 0.941997), and a fresh forward pass gives L(θ′) = 0.000408.

At η = 0.20, the exact same correct gradient produces a loss of 0.322089—higher than the starting loss of 0.290013. The gradient is a local derivative, not a promise about every finite step. For this smooth objective and nonzero g:

d/dη L(θ − ηg) |η=0 = −gᵀg= −‖g‖² = −5.116820 < 0A sufficiently small positive step decreases the loss. This statement does not select a safe finite η for you.

Now deliberately omit the direct contribution to ū. The resulting incorrect gradient is (0.639700, 0.319850, 0.580026). At η = 0.10 it still reduces the loss to 0.134228. Its direction happens to remain downhill; its derivative of w is nevertheless wrong.

Interactive experiment · One fixed starting point

Change the step. Break the derivative.

Compare a complete backward pass with one that omits the skip-path contribution. Both use the original forward model to measure the resulting loss.

0.000408Loss after one step
1.05e−10Maximum gradient-check error
PASSDerivative check, tolerance 1e−7
Same starting weights. Different step sizes.
One-step loss along two fixed update directionsThe solid curve uses the complete gradient, while the dashed curve omits its skip-path contribution; the horizontal reference is the initial loss of 0.290013. Loss after one step 0.000.350.701.051.40 0.000.100.200.30 Initial loss = 0.290013 Learning rate η · fixed initial gradient Complete gradientSkip contribution omitted
How to read this: Choose η on the horizontal axis and read the resulting loss vertically. These are candidate single steps, not a training history. The curves are calculated from the displayed model at increments of 0.005; connecting segments are for reading. Default markers show η = 0.10.

Complete gradient, η = 0.10: the derivative check passes and loss decreases from 0.290013 to 0.000408.

Ask the forward program to check your work

Central finite differences estimate one parameter derivative by evaluating the loss on both sides of the current value. For parameter i, with ei the corresponding unit vector:

gi ≈ [L(θ + εei) − L(θ − εei)] / (2ε)

Use ε = 10−6 here. The analytical gradient agrees with this independent calculation to about 10−10 in double precision. The broken backward pass misses ∂L/∂w by about 1.523188. A gradient check tests the derivative; a falling loss does not.

Run it · Python standard library only

import math

theta = (0.5, 0.0, 1.0)  # w, b, v; x=2, target=1

def loss(p):
    w, b, v = p
    u = 2.0 * w
    prediction = v * math.tanh(u + b) + u
    return 0.5 * (prediction - 1.0) ** 2

w, b, v = theta
a = math.tanh(2.0 * w + b)
error = v * a + 2.0 * w - 1.0
bar_z = error * v * (1.0 - a * a)
bar_u = error + bar_z  # Both paths contribute.
gradient = (2.0 * bar_u, bar_z, error * a)

eps = 1e-6
for i, name in enumerate(("w", "b", "v")):
    plus, minus = list(theta), list(theta)
    plus[i] += eps
    minus[i] -= eps
    estimate = (loss(plus) - loss(minus)) / (2 * eps)
    print(name, gradient[i], estimate)
    assert abs(gradient[i] - estimate) < 1e-7

updated = tuple(p - 0.1 * g for p, g in zip(theta, gradient))
print("new loss:", loss(updated))  # about 0.000407694

Finite differences are a check, not the training algorithm here. Very small ε can amplify floating-point cancellation; large ε no longer probes the local derivative accurately. Checks near a nondifferentiable point or with changing random inputs require extra care. Keep the forward inputs fixed when comparing results.

04— From scalars to systems

The same rules.
A different scale.#

Our three-parameter example exposes the essential mechanism. Tensor libraries apply the same chain rule in blocks, reusing efficient operations instead of enumerating every scalar path.

You usually need a product, not a Jacobian

For y = f(x), with x ∈ ℝⁿ and y ∈ ℝᵐ, the Jacobian J has shape m × n. A reverse rule takes an upstream sensitivity ȳ and returns x̄ = Jᵀȳ. This is a vector–Jacobian product expressed using column vectors. You need its result, not necessarily the full matrix J.[1]

Forward: y = f(x),   J ∈ ℝm×nReverse: x̄ = JᵀȳFor a scalar loss, m = 1. Seeding the output with 1 yields the gradient with respect to all n inputs.

Our tanh rule is the scalar instance. A dense layer makes the tensor version concrete. For z = Wh + b, W has shape m × n, h has n entries, and z̄ has m entries. The reverse rules are:

h̄ = Wᵀz̄   ∈ ℝⁿW̄ = z̄hᵀ   ∈ ℝm×nb̄ = z̄   ∈ ℝᵐ

The outer product gives a gradient for every weight. If h also feeds another branch, add its contribution to h̄—exactly as we added the two contributions to ū. For a batch, reductions depend on whether the loss is a sum or a mean.

The time advantage comes with a memory bill

Coordinate-wise central differences need two model evaluations per parameter. Reverse mode instead traverses the executed graph backward once. When local derivative rules have costs comparable to their forward operations, its arithmetic cost is a constant-factor multiple of forward evaluation, rather than a separate forward run for every parameter.[1]

In return, reverse mode needs intermediate values. Our network retains a for 1 − a²; a deep network can retain many large activations. Activation checkpointing saves selected values and recomputes others during the backward pass. It trades extra computation for less activation storage; it does not remove parameter or optimizer-state memory.[4]

A correct derivative has boundaries

Local, not prophetic

A gradient describes local sensitivity. A zero gradient may mark a minimum, maximum, or saddle. Step-size conditions require assumptions about the objective.[5]

Conventions at kinks

ReLU has no classical derivative at zero. Frameworks use a defined backward convention; PyTorch’s choice there is zero.[2]

Scope matters

Backpropagation serves differentiable computations. It does not describe all machine learning, prove generalization, or establish how biological brains learn.[8]

The history is also broader than one invention story. Modern reverse accumulation has roots in Linnainmaa’s 1970 thesis and 1976 paper. Rumelhart, Hinton, and Williams’ 1986 work became a landmark in learning hidden representations with backpropagated errors.[6][7]

Trust the derivative only after checking the computation it differentiates.Our tiny network captures the larger discipline: define the objective, preserve every dependency, validate the backward rules, and evaluate the optimizer separately.
05— Sources

Inspect the machinery.#

The numerical example is fully specified and independently checked with central differences. Rounded labels aid reading; calculations retain floating-point precision. The three-parameter demonstration is not an empirical benchmark or a newly discovered learning algorithm.

  1. Forward- and reverse-mode autodiff. JAX documentation.Technical guideJVPs, VJPs, scalar gradients, and the computational trade-offs of reverse mode.
  2. Autograd mechanics. PyTorch documentation, version 2.14.Implementation guideExecuted graphs, saved tensors, and conventions for nondifferentiable operations.
  3. micrograd: the autograd engine. Andrej Karpathy.Original source codeReverse topological traversal, a scalar seed of one, and gradient accumulation with +=.
  4. Training Deep Nets with Sublinear Memory Cost. Tianqi Chen and colleagues, 2016.Research paperCheckpointing through recomputation; activation-storage savings depend on the computation and schedule.
  5. Gradient descent and smoothness. Cornell CS4787 course notes, 2025.Mathematical conditionsThe descent lemma and learning-rate conditions under smoothness assumptions.
  6. Taylor expansion of the accumulated rounding error. Seppo Linnainmaa · BIT Numerical Mathematics, 1976.Original paperReverse accumulation history; the paper references the author’s 1970 master’s thesis.
  7. Learning representations by back-propagating errors. David Rumelhart, Geoffrey Hinton, and Ronald Williams · Nature, 1986.Original paperA landmark for learning useful hidden representations, not the sole origin of reverse-mode differentiation.
  8. Random synaptic feedback weights support error backpropagation for deep learning. Timothy Lillicrap and colleagues · Nature Communications, 2016.Research paperFeedback alignment relaxes precise weight symmetry in artificial networks; it does not establish that brains implement backpropagation.
  9. The Most Important Algorithm in Machine Learning. Artem Kirsanov.Source videoThe conceptual starting point. This article develops a different worked example, a deliberate gradient bug, and a reproducible numerical experiment.

Ali Reza Rashidi
Ali Reza Rashidi
Ali Reza Rashidi, a Senior Data Scientist-Gen Al | Al Architect | MLOps with over ten years of experience, He is the author of three books that delve into the world of data and management.

Leave a Reply

Your email address will not be published. Required fields are marked *