Deep Learning πŸ“‚ Artificial Neural Networks (ANN) Β· 5 of 7 47 min read

Backpropagation Algorithm

A comprehensive, example-driven tutorial on the backpropagation algorithm. Covers the computation graph, error signal delta per layer, weight gradients dL/dW, and bias gradients dL/db β€” with full numerical step-by-step mathematics, layer diagrams, Python from-scratch implementation, and a PyTorch autograd walkthrough.

Section 01

The Story That Makes Backpropagation Click

The Archery Coach and the Chain of Blame
Imagine an archery team where three students shoot in sequence β€” each one adjusts their aim based on the last person's shot. The arrow misses the bullseye by 30 cm to the right. Who is to blame?

The coach doesn't just blame the last archer. She walks backwards down the line, asking: how much did each archer contribute to that final error? Archer 3 contributed 60% of the drift. Archer 2's bad stance caused 30%. Archer 1's grip caused the remaining 10%. Each archer gets a personalised correction β€” not a generic one.

That is backpropagation. The network fires forward, produces an error, then the coach (the algorithm) walks backwards through every layer, assigning a precise share of blame to every single weight β€” then corrects them all in one efficient backward pass.

Backpropagation (short for "backward propagation of errors") is the algorithm that makes neural network learning possible. It computes how much each weight in the network contributed to the final loss, using the chain rule of calculus applied efficiently through the computation graph β€” all weights updated in a single backward sweep.

Why Backprop Changed Everything

Before backpropagation (formalised by Rumelhart, Hinton & Williams in 1986), there was no efficient way to train deep networks. Computing gradients by perturbing each weight individually requires two forward passes per weight β€” absurdly slow for millions of parameters. Backprop does it in one forward + one backward pass, regardless of network depth. This is the algorithmic core of every modern deep learning system.


Section 02

The Four Core Concepts

📈
Computation Graph
A directed acyclic graph (DAG) where each node is an operation and edges carry values. Every forward pass traces a path through this graph. Every backward pass retraces it in reverse.
🔴
Error Signal δ (Delta)
The "blame" flowing backward through each layer. Delta at layer l is the partial derivative of the loss with respect to the pre-activation (weighted sum) at that layer: δ = dL/dz.
⚖️
Weight Gradient dL/dW
How much the loss changes when you nudge a specific weight. Computed as the outer product of the error signal and the previous layer's activations: dL/dW = δ Β· aα΅€.
Bias Gradient dL/db
How much the loss changes when you nudge a bias term. Simpler than weights β€” it equals the error signal directly: dL/db = δ. The bias has no input multiplier.

Section 03

The Computation Graph β€” Your Network's Blueprint

Every neural network operation can be drawn as a graph of primitive computations. During the forward pass, data flows left to right. During the backward pass, gradients flow right to left. The graph makes it possible to apply the chain rule mechanically, node by node.

📊 Computation Graph β€” Simple 2-Layer Network (Forward Pass)
INPUT x MULTIPLY z₁ = W₁x + b₁ linear transform ACTIVATE a₁ = Οƒ(z₁) sigmoid / ReLU MULTIPLY zβ‚‚ = Wβ‚‚a₁ + bβ‚‚ output layer ACTIVATE Ε· = Οƒ(zβ‚‚) prediction LOSS L(Ε·,y) MSE / CrossEntr β†’ FORWARD PASS (values) ← BACKWARD PASS (gradients)

Each node stores its output during the forward pass so the backward pass can reuse it. This is why backprop only needs one forward + one backward sweep.


Section 04

The Chain Rule β€” The Mathematical Engine

Gears Inside a Clock
Imagine three interlocked gears. When you turn gear A, it turns gear B, which turns gear C. The question is: how fast does gear C spin when I give gear A one full rotation?

The answer is the product of each gear ratio along the chain. If A→B has ratio 2×, and B→C has ratio 3×, then A→C is 6×. You multiply the local rates. The chain rule is precisely this — multiply local derivatives along a path in the computation graph.

The chain rule states: if L depends on z through a, then:

Chain Rule (scalar)
dL/dx = (dL/dz) Γ— (dz/dx)
Multiply the downstream gradient by the local derivative of this node.
Chain Rule (through activation)
dL/dz = (dL/da) Γ— (da/dz) = Ξ΄ Γ— Οƒ'(z)
Ξ΄ is the error signal from the layer above; Οƒ'(z) is the activation's derivative at z.
Weight Gradient
dL/dW = Ξ΄ Β· aα΅€
Outer product of the error signal and the activations from the previous layer.
Bias Gradient
dL/db = Ξ΄
The bias gradient equals the error signal β€” no activation multiplier needed since bias has coefficient 1.
🔑
Why "One Backward Pass" Is Enough

At each node, backprop reuses the stored forward-pass values (activations and pre-activations). It computes the local gradient and multiplies it by the incoming gradient β€” then passes the result to the next node upstream. Every weight gets its gradient in exactly one pass. No redundant computation. This is the genius of the algorithm.


Section 05

Full Numerical Example β€” Step-by-Step Mathematics

We will build a tiny neural network with 2 inputs β†’ 1 hidden neuron β†’ 1 output neuron and walk through every single number in both the forward and backward pass. No hand-waving, no skipping β€” every calculation shown.

📌
Our Network Setup

Input: x = [0.5, 0.8] | True label: y = 1.0 | Activation: Sigmoid Οƒ(z) = 1/(1+e⁻ᢻ) | Loss: MSE = Β½(Ε· βˆ’ y)Β²

📊 Network Architecture with Numerical Values
x₁ = 0.5 Input 1 xβ‚‚ = 0.8 Input 2 W₁₁=0.4 W₁₂=0.6 Hidden Neuron z₁ = 0.68 a₁ = Οƒ(0.68) = 0.664 b₁ = 0.1 Wβ‚‚ = 0.9 Output Neuron zβ‚‚ = 0.698 Ε· = Οƒ(0.698) = 0.668 bβ‚‚ = 0.1 Loss (MSE) L = 0.0554 INPUT LAYER HIDDEN LAYER OUTPUT LAYER

👉 Step 1 β€” Forward Pass

▶ Forward Pass β€” All Calculations
1.1
Hidden pre-activation z₁:
z₁ = W₁₁·x₁ + W₁₂·xβ‚‚ + b₁
z₁ = (0.4)(0.5) + (0.6)(0.8) + 0.1
z₁ = 0.20 + 0.48 + 0.10 = 0.68
1.2
Hidden activation a₁ = Οƒ(z₁):
a₁ = 1 / (1 + e⁻⁰·⁢⁸) = 1 / (1 + 0.5066) = 1 / 1.5066 = 0.6637
1.3
Output pre-activation zβ‚‚:
zβ‚‚ = Wβ‚‚Β·a₁ + bβ‚‚
zβ‚‚ = (0.9)(0.6637) + 0.1 = 0.5973 + 0.1 = 0.6973
1.4
Output activation Ε· = Οƒ(zβ‚‚):
ŷ = 1 / (1 + e⁻⁰·⁢⁹⁷³) = 1 / (1 + 0.4977) = 0.6682
1.5
Loss L = Β½(Ε· βˆ’ y)Β²:
L = Β½(0.6682 βˆ’ 1.0)Β² = Β½(βˆ’0.3318)Β² = Β½(0.1101) = 0.0550

👉 Step 2 β€” Backward Pass

Now we work backwards from the loss, computing gradients layer by layer using the chain rule. We start at the output and propagate the error signal upstream.

📊 Backward Pass β€” Error Signal Flow
LOSS dL/dΕ· = βˆ’0.3318 OUTPUT Οƒ Ξ΄β‚‚ = dL/dzβ‚‚ = βˆ’0.0735 HIDDEN Οƒ δ₁ = dL/dz₁ = βˆ’0.0441 INPUTS x₁=0.5, xβ‚‚=0.8 dL/dW₁ computed Γ—Οƒ'(zβ‚‚) Γ—Wβ‚‚Γ—Οƒ'(z₁) Γ—xα΅€ dL/dW₁₁ = βˆ’0.022 dL/dW₁₂ = βˆ’0.035 dL/dWβ‚‚ = βˆ’0.0488 dL/dbβ‚‚ = βˆ’0.0735 dL/db₁ = βˆ’0.0441

Error signal Ξ΄ flows backward. Each layer multiplies the incoming Ξ΄ by its own local derivative (chain rule), then passes the result upstream.

◀ Backward Pass β€” Every Calculation Shown
2.1
dL/dΕ· β€” gradient of loss w.r.t. prediction:
L = Β½(Ε· βˆ’ y)Β² β†’ dL/dΕ· = Ε· βˆ’ y = 0.6682 βˆ’ 1.0 = βˆ’0.3318
2.2
Sigmoid derivative Οƒ'(zβ‚‚):
Οƒ'(z) = Οƒ(z)Β·(1 βˆ’ Οƒ(z)) = Ε·Β·(1 βˆ’ Ε·)
Οƒ'(zβ‚‚) = 0.6682 Γ— (1 βˆ’ 0.6682) = 0.6682 Γ— 0.3318 = 0.2217
2.3
Output error signal Ξ΄β‚‚ = dL/dzβ‚‚:
Ξ΄β‚‚ = (dL/dΕ·) Γ— Οƒ'(zβ‚‚) = (βˆ’0.3318) Γ— 0.2217 = βˆ’0.0736
2.4
Weight gradient dL/dWβ‚‚:
dL/dWβ‚‚ = Ξ΄β‚‚ Γ— a₁ = (βˆ’0.0736) Γ— 0.6637 = βˆ’0.0488
2.5
Bias gradient dL/dbβ‚‚:
dL/dbβ‚‚ = Ξ΄β‚‚ = βˆ’0.0736
2.6
Propagate error to hidden layer:
dL/da₁ = Ξ΄β‚‚ Γ— Wβ‚‚ = (βˆ’0.0736) Γ— 0.9 = βˆ’0.0662
2.7
Sigmoid derivative Οƒ'(z₁):
Οƒ'(z₁) = a₁ Γ— (1 βˆ’ a₁) = 0.6637 Γ— (1 βˆ’ 0.6637) = 0.6637 Γ— 0.3363 = 0.2232
2.8
Hidden error signal δ₁ = dL/dz₁:
δ₁ = (dL/da₁) Γ— Οƒ'(z₁) = (βˆ’0.0662) Γ— 0.2232 = βˆ’0.0148
2.9
Hidden weight gradients dL/dW₁₁ and dL/dW₁₂:
dL/dW₁₁ = δ₁ Γ— x₁ = (βˆ’0.0148) Γ— 0.5 = βˆ’0.0074
dL/dW₁₂ = δ₁ Γ— xβ‚‚ = (βˆ’0.0148) Γ— 0.8 = βˆ’0.0118
2.10
Hidden bias gradient dL/db₁:
dL/db₁ = δ₁ = βˆ’0.0148

👉 Step 3 β€” Weight Update (Gradient Descent)

↻ Gradient Descent Update: W_new = W_old βˆ’ Ξ· Γ— (dL/dW)   [Ξ· = 0.5]
Wβ‚‚
0.9 βˆ’ 0.5 Γ— (βˆ’0.0488) = 0.9 + 0.0244 = 0.9244
bβ‚‚
0.1 βˆ’ 0.5 Γ— (βˆ’0.0736) = 0.1 + 0.0368 = 0.1368
W₁₁
0.4 βˆ’ 0.5 Γ— (βˆ’0.0074) = 0.4 + 0.0037 = 0.4037
W₁₂
0.6 βˆ’ 0.5 Γ— (βˆ’0.0118) = 0.6 + 0.0059 = 0.6059
b₁
0.1 βˆ’ 0.5 Γ— (βˆ’0.0148) = 0.1 + 0.0074 = 0.1074
What Just Happened?

All gradients were negative, so all weights increased slightly. Since the prediction (0.668) was below the true label (1.0), the network needed to output a higher value next time β€” and increasing the weights achieves exactly that. Gradient descent nudged every weight in the right direction, all at once.


Section 06

Layer-by-Layer Diagram β€” What Each Layer Computes

① INPUT LAYER β€” No computation, passes raw data
x₁ = 0.5 feature 1 xβ‚‚ = 0.8 feature 2 x = [x₁, xβ‚‚] = [0.5, 0.8] passes directly to hidden layer Forward: output = x Backward: no gradient here (inputs are fixed data)
② HIDDEN LAYER β€” Linear transform + Sigmoid activation
Linear: W₁x + b₁ z₁ = 0.68 Sigmoid Οƒ(z₁) a₁ = 0.6637 FORWARD FORMULAS z₁ = W₁₁x₁ + W₁₂xβ‚‚ + b₁ a₁ = Οƒ(z₁) = 1/(1 + e⁻ᢻ¹) BACKWARD FORMULAS δ₁ = (Ξ΄β‚‚ Β· Wβ‚‚) Γ— Οƒ'(z₁) = βˆ’0.0148 dL/dW₁ = δ₁ Β· xα΅€   |   dL/db₁ = δ₁
③ OUTPUT LAYER β€” Linear transform + Sigmoid + Loss
Linear: Wβ‚‚a₁ + bβ‚‚ zβ‚‚ = 0.6973 Οƒ(zβ‚‚) = Ε· Ε· = 0.6682 Loss L Β½(Ε· βˆ’ y)Β² = 0.0550 FORWARD FORMULAS zβ‚‚ = Wβ‚‚ Β· a₁ + bβ‚‚ Ε· = Οƒ(zβ‚‚) L = Β½(Ε· βˆ’ y)Β² BACKWARD FORMULAS Ξ΄β‚‚ = (Ε· βˆ’ y) Γ— Οƒ'(zβ‚‚) = βˆ’0.0736 dL/dWβ‚‚ = Ξ΄β‚‚ Β· a₁   |   dL/dbβ‚‚ = Ξ΄β‚‚

Section 07

Complete Gradient Summary

Parameter Old Value Gradient (dL/dΞΈ) Update (Ξ·=0.5) New Value Direction
Wβ‚‚ (output weight) 0.9000 βˆ’0.0488 +0.0244 0.9244 ↑ increase
bβ‚‚ (output bias) 0.1000 βˆ’0.0736 +0.0368 0.1368 ↑ increase
W₁₁ (hidden weight 1) 0.4000 βˆ’0.0074 +0.0037 0.4037 ↑ increase
W₁₂ (hidden weight 2) 0.6000 βˆ’0.0118 +0.0059 0.6059 ↑ increase
b₁ (hidden bias) 0.1000 βˆ’0.0148 +0.0074 0.1074 ↑ increase
💡
Reading the Signs

All gradients are negative because the loss decreases when all weights increase (our prediction was below the target). Gradient descent subtracts the gradient, so W_new = W_old βˆ’ Ξ· Γ— (negative) = W_old + positive β†’ weights go up. The network learns to predict higher, closer to the true label of 1.0.


Section 08

Python Implementation β€” From Scratch

Below is a clean, well-commented Python implementation of the exact network we computed by hand above. It verifies all our numbers programmatically.

import numpy as np

# ── Network weights (matching our manual example) ──────────
W1 = np.array([[0.4, 0.6]])   # shape (1, 2) β€” 1 hidden neuron, 2 inputs
b1 = np.array([[0.1]])         # shape (1, 1)
W2 = np.array([[0.9]])         # shape (1, 1) β€” 1 output, 1 hidden neuron
b2 = np.array([[0.1]])         # shape (1, 1)

x  = np.array([[0.5], [0.8]])  # shape (2, 1) β€” column vector
y  = np.array([[1.0]])         # true label

# ── Activation functions ───────────────────────────────────
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def sigmoid_deriv(z):
    s = sigmoid(z)
    return s * (1 - s)          # Οƒ'(z) = Οƒ(z)(1 - Οƒ(z))

# ── FORWARD PASS ──────────────────────────────────────────
z1 = W1 @ x + b1              # pre-activation hidden:  (1,1)
a1 = sigmoid(z1)              # activation hidden

z2 = W2 @ a1 + b2             # pre-activation output: (1,1)
y_hat = sigmoid(z2)           # prediction Ε·

loss = 0.5 * (y_hat - y)**2   # MSE loss

print(f"z1    = {z1[0,0]:.4f}")
print(f"a1    = {a1[0,0]:.4f}")
print(f"z2    = {z2[0,0]:.4f}")
print(f"y_hat = {y_hat[0,0]:.4f}")
print(f"Loss  = {loss[0,0]:.4f}")

# ── BACKWARD PASS ─────────────────────────────────────────
# Output layer
dL_dyhat = y_hat - y                    # dL/dΕ·
delta2   = dL_dyhat * sigmoid_deriv(z2) # Ξ΄β‚‚ = dL/dzβ‚‚
dL_dW2   = delta2 @ a1.T               # dL/dWβ‚‚
dL_db2   = delta2                       # dL/dbβ‚‚

# Hidden layer
dL_da1 = W2.T @ delta2                 # propagate error upstream
delta1 = dL_da1 * sigmoid_deriv(z1)   # δ₁ = dL/dz₁
dL_dW1 = delta1 @ x.T                  # dL/dW₁  shape (1,2)
dL_db1 = delta1                         # dL/db₁

print(f"\nGradients:")
print(f"delta2  = {delta2[0,0]:.4f}  (output error signal)")
print(f"dL/dW2  = {dL_dW2[0,0]:.4f}")
print(f"dL/db2  = {dL_db2[0,0]:.4f}")
print(f"delta1  = {delta1[0,0]:.4f}  (hidden error signal)")
print(f"dL/dW11 = {dL_dW1[0,0]:.4f}")
print(f"dL/dW12 = {dL_dW1[0,1]:.4f}")
print(f"dL/db1  = {dL_db1[0,0]:.4f}")

# ── GRADIENT DESCENT UPDATE ───────────────────────────────
lr = 0.5  # learning rate Ξ·

W2 = W2 - lr * dL_dW2
b2 = b2 - lr * dL_db2
W1 = W1 - lr * dL_dW1
b1 = b1 - lr * dL_db1

print(f"\nUpdated Weights:")
print(f"W2  = {W2[0,0]:.4f}")
print(f"b2  = {b2[0,0]:.4f}")
print(f"W11 = {W1[0,0]:.4f}  W12 = {W1[0,1]:.4f}")
print(f"b1  = {b1[0,0]:.4f}")
OUTPUT
z1 = 0.6800 a1 = 0.6637 z2 = 0.6973 y_hat = 0.6682 Loss = 0.0550 Gradients: delta2 = -0.0736 (output error signal) dL/dW2 = -0.0488 dL/db2 = -0.0736 delta1 = -0.0148 (hidden error signal) dL/dW11 = -0.0074 dL/dW12 = -0.0118 dL/db1 = -0.0148 Updated Weights: W2 = 0.9244 b2 = 0.1368 W11 = 0.4037 W12 = 0.6059 b1 = 0.1074
🎯
Python Output Matches Our Manual Calculations Exactly

Every number from the hand-calculation appears in the program output. This confirms the correctness of the chain rule application and the backpropagation logic. In practice you would run this in a loop over thousands of training examples and iterations β€” that loop is training.


Section 09

The Full Training Loop β€” 1000 Iterations

import numpy as np
import matplotlib.pyplot as plt

# ── Data: XOR problem ─────────────────────────────────────
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float).T  # (2,4)
Y = np.array([[0,1,1,0]], dtype=float)              # (1,4)

# ── Initialise weights with small random values ───────────
np.random.seed(42)
W1 = np.random.randn(4, 2) * 0.5   # 4 hidden neurons
b1 = np.zeros((4, 1))
W2 = np.random.randn(1, 4) * 0.5
b2 = np.zeros((1, 1))

lr     = 2.0
losses = []

for epoch in range(5000):
    # ── Forward ────────────────────────────────────────────
    z1    = W1 @ X + b1             # (4,4)
    a1    = 1 / (1 + np.exp(-z1))  # sigmoid
    z2    = W2 @ a1 + b2            # (1,4)
    y_hat = 1 / (1 + np.exp(-z2))

    loss  = np.mean(0.5 * (y_hat - Y)**2)
    losses.append(loss)

    # ── Backward ───────────────────────────────────────────
    m = X.shape[1]                   # number of samples = 4

    delta2 = (y_hat - Y) * y_hat * (1 - y_hat) / m
    dW2    = delta2 @ a1.T
    db2    = np.sum(delta2, axis=1, keepdims=True)

    dA1    = W2.T @ delta2
    delta1 = dA1 * a1 * (1 - a1)
    dW1    = delta1 @ X.T
    db1    = np.sum(delta1, axis=1, keepdims=True)

    # ── Update ─────────────────────────────────────────────
    W2 -= lr * dW2;  b2 -= lr * db2
    W1 -= lr * dW1;  b1 -= lr * db1

    if epoch % 1000 == 0:
        print(f"Epoch {epoch:5d}  Loss: {loss:.5f}")

# ── Final predictions ─────────────────────────────────────
print(f"\nFinal predictions:")
print(np.round(y_hat, 2), "  (target: [0,1,1,0])")
OUTPUT
Epoch 0 Loss: 0.12834 Epoch 1000 Loss: 0.08241 Epoch 2000 Loss: 0.02314 Epoch 3000 Loss: 0.00812 Epoch 4000 Loss: 0.00401 Final predictions: [[0.03 0.97 0.97 0.03]] (target: [0,1,1,0])

The network learned the XOR function β€” a classic non-linear problem β€” purely through repeated forward + backward passes. No magic: just the chain rule, applied thousands of times.


Section 10

Common Pitfalls in Backpropagation

⚠️
Vanishing Gradients β€” The Deep Network Killer

Each layer multiplies the gradient by Οƒ'(z) ≀ 0.25. With 10 layers: 0.25¹⁰ β‰ˆ 0.000001. The gradient at layer 1 is a millionth of the gradient at layer 10. Early layers learn nothing. Fix: use ReLU (derivative is 1 for positive z), batch normalisation, or residual connections.

🔴
Exploding Gradients β€” The Training Instability

The opposite problem: if weights are large, gradients blow up exponentially through layers. The loss oscillates wildly or returns NaN. Fix: gradient clipping (cap the norm of the gradient before the update), or careful weight initialisation (Xavier, He).

📌
Dead Neurons (ReLU)

A neuron using ReLU outputs 0 for any negative input. Its gradient is also 0. If a neuron always gets a negative pre-activation, it will never update β€” it is "dead." Fix: use Leaky ReLU (small slope for negatives) or ELU, or lower the learning rate to prevent neurons from drifting into permanent negative territory.


Section 11

Golden Rules of Backpropagation

⚡ Backpropagation β€” Non-Negotiable Rules
1
Always store forward-pass values. The backward pass needs the activations a and pre-activations z computed during the forward pass. Frameworks like PyTorch do this automatically with the computation graph. If you implement from scratch, cache them explicitly.
2
Apply chain rule in the correct order. Start at the loss, work backwards layer by layer. Never skip a layer. Each layer receives the gradient from the layer above and multiplies by its local derivative before passing upstream.
3
The bias gradient is just the error signal. dL/db = Ξ΄. No input term. This is because bias has coefficient 1 in the linear transform β€” βˆ‚(Wx + b)/βˆ‚b = 1. Beginners often overcomplicate bias gradients.
4
Gradient check your implementation. Numerically verify gradients by computing (L(ΞΈ + Ξ΅) βˆ’ L(ΞΈ βˆ’ Ξ΅)) / (2Ξ΅) for each parameter. If it doesn't match your analytical gradient to within ~1e-7, there's a bug. Always do this when implementing from scratch.
5
Zero gradients before each batch. In frameworks: optimizer.zero_grad() in PyTorch, or tape management in TensorFlow. Accumulated gradients across batches will cause incorrect updates.
6
Watch your activation derivatives. Sigmoid saturates (Οƒ' β†’ 0) for large |z|. ReLU has Οƒ'=0 for z<0 (dead neuron risk). Tanh saturates but is zero-centred (better for hidden layers than sigmoid). Choose activations based on the layer role and expected input range.
7
One backward pass per forward pass. The computational cost of backprop is approximately 2–3Γ— the forward pass. Total cost of training is thus O(passes Γ— (forward + backward)) β€” not O(passes Γ— weights), which is what naΓ―ve finite-difference methods would cost.

Section 12

Activation Functions & Their Derivatives

Activation Formula Οƒ(z) Derivative Οƒ'(z) Range Gradient Issue Use Case
Sigmoid 1 / (1 + e⁻ᢻ) Οƒ(z)(1 βˆ’ Οƒ(z)) (0, 1) Vanishes for |z|>5 Binary output
Tanh (eαΆ» βˆ’ e⁻ᢻ)/(eαΆ» + e⁻ᢻ) 1 βˆ’ tanhΒ²(z) (βˆ’1, 1) Less severe vanishing Hidden layers
ReLU max(0, z) 1 if z>0 else 0 [0, ∞) Dead neurons (z<0) Deep hidden layers
Leaky ReLU z if z>0 else 0.01z 1 if z>0 else 0.01 (βˆ’βˆž, ∞) No dead neurons Default hidden
Softmax eαΆ»α΅’ / Ξ£eαΆ»β±Ό Jacobian matrix (0, 1) None (output only) Multi-class output

Section 13

PyTorch β€” Automatic Backpropagation

In practice, frameworks like PyTorch compute all gradients automatically. But understanding the manual derivation is essential β€” it tells you why the API works the way it does.

import torch
import torch.nn as nn

# ── Reproduce our manual example in PyTorch ───────────────
x     = torch.tensor([[0.5], [0.8]], dtype=torch.float32)
y     = torch.tensor([[1.0]])

# Weights β€” require_grad=True tracks the computation graph
W1 = torch.tensor([[0.4, 0.6]], requires_grad=True)
b1 = torch.tensor([[0.1]], requires_grad=True)
W2 = torch.tensor([[0.9]], requires_grad=True)
b2 = torch.tensor([[0.1]], requires_grad=True)

# ── Forward pass (PyTorch builds the computation graph) ───
z1    = W1 @ x + b1
a1    = torch.sigmoid(z1)
z2    = W2 @ a1 + b2
y_hat = torch.sigmoid(z2)
loss  = 0.5 * (y_hat - y)**2

# ── Backward pass (ONE LINE β€” PyTorch does all the chain rule)
loss.backward()

# ── Gradients are now in .grad attributes ─────────────────
print(f"dL/dW2  = {W2.grad.item():.4f}")
print(f"dL/db2  = {b2.grad.item():.4f}")
print(f"dL/dW11 = {W1.grad[0,0].item():.4f}")
print(f"dL/dW12 = {W1.grad[0,1].item():.4f}")
print(f"dL/db1  = {b1.grad.item():.4f}")

# ── Gradient descent update ───────────────────────────────
with torch.no_grad():
    W2 -= 0.5 * W2.grad
    b2 -= 0.5 * b2.grad
    W1 -= 0.5 * W1.grad
    b1 -= 0.5 * b1.grad

print(f"\nUpdated: W2={W2.item():.4f}  b2={b2.item():.4f}")
print(f"         W1={W1.detach().numpy()}  b1={b1.item():.4f}")
OUTPUT
dL/dW2 = -0.0488 dL/db2 = -0.0736 dL/dW11 = -0.0074 dL/dW12 = -0.0118 dL/db1 = -0.0148 Updated: W2=0.9244 b2=0.1368 W1=[[0.4037 0.6059]] b1=0.1074
🌟
PyTorch Matches Our Manual Calculation β€” Exactly

loss.backward() does in one line what we computed across 10 manual steps. Behind the scenes, PyTorch's autograd engine traverses the computation graph in reverse, applying the chain rule at every node β€” exactly the algorithm we implemented by hand. Knowing the manual version means you understand what every framework does internally.