GenAI Course · Module 3 · Session 11

Backpropagation

⏱ 14 min read🎯 Intermediate🖼 1 diagram🧪 1 live demo🐍 Python code

Gradient descent needs the gradient of the loss with respect to every weight. A modern LLM has billions of weights. Backpropagation computes all of those gradients in one backward sweep using a single idea from school calculus: the chain rule. Let us do it by hand once, so it never feels like magic again.

In this session

  • The chain rule in plain words
  • Computational graphs: breaking a network into small steps
  • A full backward pass with real numbers
  • Checking our maths against PyTorch autograd
  • One update step and the lower loss

01The chain rule

If A affects B and B affects C, then how much A affects C = (how much A affects B) × (how much B affects C). In symbols:

dC/dA = dC/dB × dB/dA
⛽

Like petrol, distance and money. Each litre takes you 15 km; each km costs your company ₹8 in wear. So each litre is worth 15 × ₹8 = ₹120 of wear. You multiplied the rates along the chain. Backprop does exactly this, layer by layer.

A neural network is a long chain of simple steps (multiply, add, activate). So the effect of any weight on the loss is the product of the local effects along the path from that weight to the loss.

02Computational graphs

We write the network as a graph of tiny operations. Our example: one input, one hidden neuron with sigmoid, one output, squared-error loss. With x = 2, target y = 1 and starting weights w₁ = 0.5, b₁ = 0, w₂ = 1.5, b₂ = −0.5:

x = 2w₁ = 0.5z = w₁x + b₁1.000h = σ(z)0.731ŷ = w₂h + b₂0.597w₂ = 1.5L = (ŷ − y)²0.1627backward: gradients flow from the loss back to every weight
A computational graph. Forward: values flow left to right. Backward: the chain rule sends gradients right to left.

03Backpropagation step by step

Demo · The backward pass by hand
Local derivativeRule
d(w·h)/dw = hmultiply: the gradient is the other input
d(a + b)/da = 1add: passes the gradient through unchanged
dσ(z)/dz = σ(z)(1 − σ(z))sigmoid: uses the saved output h
d(ŷ − y)²/dŷ = 2(ŷ − y)squared error

Now the full code: forward, backward by hand, a check with PyTorch, and one update step:

backprop_manual.py
import math

# tiny network: x -> z = w1*x + b1 -> h = sigmoid(z) -> y_hat = w2*h + b2 -> loss = (y_hat - y)^2
x, y = 2.0, 1.0
w1, b1, w2, b2 = 0.5, 0.0, 1.5, -0.5

# forward pass (save every intermediate value)
z = w1 * x + b1
h = 1 / (1 + math.exp(-z))
y_hat = w2 * h + b2
loss = (y_hat - y) ** 2
print(f"forward:  z={z:.3f}  h={h:.3f}  y_hat={y_hat:.3f}  loss={loss:.4f}")

# backward pass: chain rule, from the loss back to each weight
dL_dyhat = 2 * (y_hat - y)
dL_dw2 = dL_dyhat * h
dL_db2 = dL_dyhat * 1
dL_dh = dL_dyhat * w2
dL_dz = dL_dh * h * (1 - h)              # sigmoid'(z) = h(1-h)
dL_dw1 = dL_dz * x
dL_db1 = dL_dz * 1
print(f"manual:   dw1={dL_dw1:.4f}  db1={dL_db1:.4f}  dw2={dL_dw2:.4f}  db2={dL_db2:.4f}")

# check with PyTorch autograd
import torch
t = {k: torch.tensor(v, requires_grad=True) for k, v in dict(w1=w1, b1=b1, w2=w2, b2=b2).items()}
L = (t["w2"] * torch.sigmoid(t["w1"] * x + t["b1"]) + t["b2"] - y) ** 2
L.backward()
print("autograd: " + "  ".join(f"d{k}={v.grad.item():.4f}" for k, v in t.items()))

# one gradient-descent step
lr = 0.5
w1, b1, w2, b2 = w1 - lr * dL_dw1, b1 - lr * dL_db1, w2 - lr * dL_dw2, b2 - lr * dL_db2
y_hat = w2 / (1 + math.exp(-(w1 * x + b1))) + b2
print(f"after one step: loss={(y_hat - y) ** 2:.4f}")
forward: z=1.000 h=0.731 y_hat=0.597 loss=0.1627 manual: dw1=-0.4759 db1=-0.2379 dw2=-0.5898 db2=-0.8068 autograd: dw1=-0.4759 db1=-0.2379 dw2=-0.5898 db2=-0.8068 after one step: loss=0.1564

Our hand-written gradients match PyTorch exactly. After a single step the loss drops from 0.1627 to 0.1564. Repeat this thousands of times on millions of examples and you have trained a neural network.

04Autograd: why you rarely do this by hand

Frameworks like PyTorch and TensorFlow record the computational graph as your code runs and apply the chain rule automatically when you call loss.backward(). This is called automatic differentiation (autograd).

training_loop.py
for x_batch, y_batch in loader:          # mini-batches (Session 10)
    optimizer.zero_grad()                # clear old gradients
    loss = loss_fn(model(x_batch), y_batch)   # forward pass
    loss.backward()                      # backpropagation: fills p.grad for every weight
    optimizer.step()                     # gradient descent update (Session 13)
These five lines train everything from a tiny MLP to a 70B-parameter LLM. Only the model, the data and the scale change.

05Summary

06Quick quiz

1. Backpropagation is based on…

2. Why does the forward pass save intermediate values?

3. Derivative of sigmoid in terms of its output h?

4. If ∂L/∂w is negative, gradient descent will…

5. What does loss.backward() do in PyTorch?

07References

Created by Ajeetkumar