Back to writing

Tracing an MLP

10 min read

Forward Pass · Backward Pass · Gradient Descent · Batches & Epochs

Contents

  1. Network Architecture
  2. What the Weight Matrices Actually Mean
  3. Dataset and Training Setup
  4. The Forward Pass
  5. The Backward Pass
  6. Gradient Descent: Where Learning Happens
  7. Batches — Aggregating Gradients Before Updating
  8. Epochs — The Full Training Loop
  9. Summary: Every Step and Its Role

1. Network Architecture

A small network:

1.1 The Full Network Diagram

Complete 2-2-1 MLP network diagram
Figure 1: Complete 2-2-1 MLP. Every solid arrow is a learnable weight. Every dashed purple arrow is a bias.

1.2 How to Read the Diagram

Every arrow represents one number: a weight. There are exactly 6 weight arrows and 3 bias arrows. The matrices W1\mathbf{W_1}, W2\mathbf{W_2}, b1\mathbf{b_1}, b2\mathbf{b_2} are just a compact way of writing down all those arrows.

2. What the Weight Matrices Actually Mean

2.1 W1 — Input-to-Hidden Weights

W1\mathbf{W_1} is shape 2×22 \times 2 because there are 2 hidden neurons (rows) and 2 inputs (columns). The rule is: row = destination, column = source.

W1=(0.50.30.20.8)row 0 = weights arriving at h0,  row 1 = weights arriving at h1\mathbf{W_1} = \underbrace{\begin{pmatrix} 0.5 & -0.3 \\ 0.2 & 0.8 \end{pmatrix}}_{\text{row 0 = weights arriving at }h_0,\ \ \text{row 1 = weights arriving at }h_1}
EntryValueArrow in Figure 1What it means
W1[0,0]W_1[0,0]+0.5+0.5x0h0x_0 \to h_0x0x_0 moderately encourages h0h_0 to fire
W1[0,1]W_1[0,1]0.3-0.3x1h0x_1 \to h_0x1x_1 suppresses h0h_0; negative weights are inhibitory
W1[1,0]W_1[1,0]+0.2+0.2x0h1x_0 \to h_1x0x_0 weakly encourages h1h_1
W1[1,1]W_1[1,1]+0.8+0.8x1h1x_1 \to h_1x1x_1 strongly drives h1h_1

Sign rule: positive weight \Rightarrow larger input \Rightarrow neuron fires more. Negative weight \Rightarrow larger input \Rightarrow neuron fires less. Magnitude \Rightarrow strength of the influence.

2.2 b1 — Hidden Layer Biases

b1=(+0.10.1)\mathbf{b_1} = \begin{pmatrix} +0.1 \\ -0.1 \end{pmatrix}

The bias is an always-on input of 1 multiplied by this stored value. It shifts the neuron's firing threshold independently of any input.

Without biases, every decision boundary in every neuron would be forced to pass through the origin — a severe restriction on what the network can represent.

2.3 W2 — Hidden-to-Output Weights

Only one output neuron, so W2\mathbf{W_2} has only one row — effectively a flat vector:

W2=(+0.70.4)\mathbf{W_2} = \begin{pmatrix} +0.7 & -0.4 \end{pmatrix}
EntryValueArrowWhat it means
W2[0]W_2[0]+0.7+0.7h0y^h_0 \to \hat{y}When h0h_0 fires strongly, y^\hat{y} increases (votes positive)
W2[1]W_2[1]0.4-0.4h1y^h_1 \to \hat{y}When h1h_1 fires strongly, y^\hat{y} decreases (votes negative)

h0h_0 and h1h_1 pull the output in opposite directions. The final prediction is their weighted tug-of-war.

2.4 b2 — Output Bias

b2=+0.2b_2 = +0.2

Single scalar. Before any input is considered, the output pre-activation starts at +0.2+0.2, which pushes y^\hat{y} slightly above 0.5 (towards predicting the positive class by default). Training will adjust this.

2.5 Why We Pick Any Values At All — The Symmetry Problem

We cannot start all weights at zero. If every weight is identical, every neuron in a layer produces the same output, receives the same gradient, and gets the same update — they are stuck in perfect symmetry forever and the network cannot specialise.

We initialise with small random values to break this symmetry. The specific values here are chosen for readability, not realism. In practice, schemes like Xavier or He initialisation are used. But whatever we start with, training will overwrite them.

3. Dataset and Training Setup

Samplex0x_0x1x_1yy (truth)Batch
11.00.51 (positive)1
20.20.90 (negative)1
30.80.31 (positive)2
40.10.70 (negative)2

Batch size =2= 2 (two samples per weight update). Learning rate α=0.1\alpha = 0.1.

4. The Forward Pass

Purpose: Given the current weights, compute the network's prediction y^\hat{y} for one input xx, then measure how wrong that prediction is using the loss LL. No weights change during a forward pass. It is purely a measurement.

We trace Sample 1: x=[1.0, 0.5]x = [1.0,\ 0.5], true label y=1y = 1.

4.1 Step 1 — Hidden Pre-Activations z1

Each hidden neuron takes a weighted sum of all inputs plus its bias. This is literally just the dot product of one row of W1\mathbf{W_1} with the input vector, plus the bias.

z1[0]=W1[0,0]x0h0x0+W1[0,1]x1h0x1+b1[0]=(0.5)(1.0)+(0.3)(0.5)+0.1=0.5000.150+0.100=0.45\begin{aligned} z_1[0] &= \underbrace{W_1[0,0]}_{x_0 \to h_0} \cdot x_0 + \underbrace{W_1[0,1]}_{x_1 \to h_0} \cdot x_1 + b_1[0] \\ &= (0.5)(1.0) + (-0.3)(0.5) + 0.1 \\ &= 0.500 - 0.150 + 0.100 = \mathbf{0.45} \end{aligned} z1[1]=W1[1,0]x0+W1[1,1]x1+b1[1]=(0.2)(1.0)+(0.8)(0.5)+(0.1)=0.200+0.4000.100=0.50\begin{aligned} z_1[1] &= W_1[1,0] \cdot x_0 + W_1[1,1] \cdot x_1 + b_1[1] \\ &= (0.2)(1.0) + (0.8)(0.5) + (-0.1) \\ &= 0.200 + 0.400 - 0.100 = \mathbf{0.50} \end{aligned}

z1z_1 is called the pre-activation: the raw weighted sum, before any squashing.

4.2 Step 2 — Hidden Activations a1

We pass each z1z_1 through the sigmoid: σ(z)=11+ez\displaystyle \sigma(z) = \frac{1}{1 + e^{-z}}

a1[0]=σ(0.45)=11+e0.45=11+0.63760.6106a_1[0] = \sigma(0.45) = \frac{1}{1 + e^{-0.45}} = \frac{1}{1 + 0.6376} \approx \mathbf{0.6106} a1[1]=σ(0.50)=11+e0.50=11+0.60650.6225a_1[1] = \sigma(0.50) = \frac{1}{1 + e^{-0.50}} = \frac{1}{1 + 0.6065} \approx \mathbf{0.6225}

Why apply a nonlinearity? Without it, stacking two linear layers gives W2(W1x)=(W2W1)x\mathbf{W_2}(\mathbf{W_1}x) = (\mathbf{W_2 W_1})x — one big linear layer. No matter how many layers you add, the network can only represent a hyperplane. Sigmoid (or any nonlinearity) makes each layer irreducible — every additional layer genuinely expands what the network can represent.

4.3 Step 3 — Output Pre-Activation z2

z2=W2[0]a1[0]+W2[1]a1[1]+b2=(0.7)(0.6106)+(0.4)(0.6225)+0.2=0.42740.2490+0.200=0.3784\begin{aligned} z_2 &= W_2[0] \cdot a_1[0] + W_2[1] \cdot a_1[1] + b_2 \\ &= (0.7)(0.6106) + (-0.4)(0.6225) + 0.2 \\ &= 0.4274 - 0.2490 + 0.200 = \mathbf{0.3784} \end{aligned}

4.4 Step 4 — Output ŷ

y^=σ(0.3784)=11+e0.37840.5935\hat{y} = \sigma(0.3784) = \frac{1}{1 + e^{-0.3784}} \approx \mathbf{0.5935}

The network predicts a 59.35% probability for the positive class. The true label is 1. The prediction is wrong-ish — not confident enough in the right direction.

4.5 Step 5 — Loss L

Binary cross-entropy. For y=1y = 1, the second term vanishes:

L=ylogy^(1y)log(1y^)=log(0.5935)0.522L = -y \log \hat{y} - (1-y)\log(1 - \hat{y}) = -\log(0.5935) \approx \mathbf{0.522}

The loss is a single scalar measuring how bad the prediction was. The job of the backward pass is to figure out which weights caused this 0.522 and by how much.

5. The Backward Pass

Purpose: Compute Lw\dfrac{\partial L}{\partial w} for every weight ww in the network. This gradient tells us the direction in which changing ww would increase LL. So to decrease LL, we will later move each weight opposite to its gradient.

The backward pass does not change any weights. It only produces gradient numbers.

The mechanism is the chain rule. The loss LL depends on y^\hat{y}, which depends on z2z_2, which depends on W2\mathbf{W_2} and a1\mathbf{a_1}, which depends on z1z_1, which depends on W1\mathbf{W_1} and xx. We unwind this chain in reverse.

Important distinction: the backward pass must calculate gradients for the parameters only: W2[0]W_2[0], W2[1]W_2[1], b2b_2, the four entries of W1W_1, and the two entries of b1b_1. Quantities like z2z_2, a1[0]a_1[0], and z1[0]z_1[0] are not learned, but we still compute derivatives with respect to them as intermediate stepping stones. The "error signals" δ2\delta_2 and δ1\delta_1 are exactly these stepping-stone derivatives.

For Sample 1, the scalar computation graph is:

Backward-pass computation graph

So the full reverse route is:

Ly^z2{W2,b2,a1z1W1,b1.L \to \hat{y} \to z_2 \to \begin{cases} W_2, b_2,\\ a_1 \to z_1 \to W_1, b_1. \end{cases}

The output error signal is not a new assumption. It is a shortcut name for one derivative:

δ2=Lz2.\delta_2 = \frac{\partial L}{\partial z_2}.

Once δ2\delta_2 is known, the output-layer parameter gradients become one-line chain-rule calculations.

We define the error signal δ\boldsymbol{\delta} at each layer:

δ(l)=Lz(l)\delta^{(l)} = \frac{\partial L}{\partial z^{(l)}}

This answers: "how urgently does this layer's pre-activation need to change?" It is the central quantity flowing backwards through the network.

5.1 Step 1 — Output Error Signal δ2

For a general binary label y{0,1}y \in \{0,1\}, the BCE loss is

L=(ylog(y^)+(1y)log(1y^)).L = -\Big(y\log(\hat{y}) + (1-y)\log(1-\hat{y})\Big).

Therefore

Ly^=yy^+1y1y^.\frac{\partial L}{\partial \hat{y}} = -\frac{y}{\hat{y}} + \frac{1-y}{1-\hat{y}}.

For Sample 1, y=1y=1, so this reduces to L/y^=1/y^\partial L/\partial\hat{y}=-1/\hat{y}.

δ2=Lz2=Ly^1/y^y^z2y^(1y^)\delta_2 = \frac{\partial L}{\partial z_2} = \underbrace{\frac{\partial L}{\partial \hat{y}}}_{-1/\hat{y}} \cdot \underbrace{\frac{\partial \hat{y}}{\partial z_2}}_{\hat{y}(1-\hat{y})}

For BCE loss + sigmoid output, these factors cancel exactly:

δ2=y^y=0.59351=0.4065\boxed{\delta_2 = \hat{y} - y = 0.5935 - 1 = \mathbf{-0.4065}}

If we do not use the shortcut, the Sample 1 calculation is:

Lz2=(10.5935)(0.5935)(10.5935)=0.4065.\frac{\partial L}{\partial z_2} = \left(-\frac{1}{0.5935}\right)(0.5935)(1-0.5935) = -0.4065.

So yes: y^y\hat{y}-y is just a compact shortcut for the full chain Ly^z2L \to \hat{y} \to z_2.

Interpretation: the negative sign says z2z_2 must increase (the network was too low). The magnitude 0.4065 says how urgently.

This cancellation (y^y\hat{y} - y) is one reason BCE + sigmoid is preferred over MSE + sigmoid. With MSE the gradient contains an extra σ(z2)\sigma'(z_2) factor which approaches zero when the network is confidently wrong — learning stalls at exactly the worst moment.

5.2 Step 2 — Gradients for W2 and b2

Since z2=W2[0]a1[0]+W2[1]a1[1]+b2z_2 = W_2[0]\cdot a_1[0] + W_2[1]\cdot a_1[1] + b_2, we have z2/W2[i]=a1[i]\partial z_2 / \partial W_2[i] = a_1[i]. By chain rule:

LW2[i]=Lz2z2W2[i]=δ2a1[i],Lb2=Lz2z2b2=δ21.\frac{\partial L}{\partial W_2[i]} = \frac{\partial L}{\partial z_2}\frac{\partial z_2}{\partial W_2[i]} = \delta_2 a_1[i], \qquad \frac{\partial L}{\partial b_2} = \frac{\partial L}{\partial z_2}\frac{\partial z_2}{\partial b_2} = \delta_2 \cdot 1. LW2[0]=δ2a1[0]=(0.4065)(0.6106)=0.2482\frac{\partial L}{\partial W_2[0]} = \delta_2 \cdot a_1[0] = (-0.4065)(0.6106) = \mathbf{-0.2482} LW2[1]=δ2a1[1]=(0.4065)(0.6225)=0.2530\frac{\partial L}{\partial W_2[1]} = \delta_2 \cdot a_1[1] = (-0.4065)(0.6225) = \mathbf{-0.2530} Lb2=δ2=0.4065\frac{\partial L}{\partial b_2} = \delta_2 = \mathbf{-0.4065}

All negative: we should increase all three parameters to reduce the loss. Makes sense — the output was too low (y^=0.594\hat{y} = 0.594) for a true positive case.

Universal pattern: weight gradient == (downstream error signal) ×\times (the input that weight received). This is always an outer product. Every layer in every architecture follows this.

5.3 Step 3 — Propagate Error Signal Back Through W2

Before we can compute gradients for W1\mathbf{W_1}, we need to know how each hidden activation a1[i]a_1[i] contributed to the loss. By chain rule:

La1[i]=Lz2z2a1[i]=δ2W2[i].\frac{\partial L}{\partial a_1[i]} = \frac{\partial L}{\partial z_2}\frac{\partial z_2}{\partial a_1[i]} = \delta_2 W_2[i]. La1[0]=δ2W2[0]=(0.4065)(+0.7)=0.2846\frac{\partial L}{\partial a_1[0]} = \delta_2 \cdot W_2[0] = (-0.4065)(+0.7) = \mathbf{-0.2846} La1[1]=δ2W2[1]=(0.4065)(0.4)=+0.1626\frac{\partial L}{\partial a_1[1]} = \delta_2 \cdot W_2[1] = (-0.4065)(-0.4) = \mathbf{+0.1626}

h0h_0 needs to increase its output (negative gradient means increasing a1[0]a_1[0] decreases LL). h1h_1 needs to decrease its output — because its weight to the output is 0.4-0.4, increasing h1h_1 would push y^\hat{y} down, which makes things worse.

5.4 Step 4 — Pass Back Through the Sigmoid

To get the error signal at the hidden pre-activations z1z_1, we multiply by the sigmoid's derivative.

Recall that if a=σ(z)a = \sigma(z), then σ(z)=a(1a)\sigma'(z) = a(1 - a).

δ1[i]=Lz1[i]=La1[i]a1[i]z1[i]=La1[i]σ(z1[i]).\delta_1[i] = \frac{\partial L}{\partial z_1[i]} = \frac{\partial L}{\partial a_1[i]}\frac{\partial a_1[i]}{\partial z_1[i]} = \frac{\partial L}{\partial a_1[i]}\sigma'(z_1[i]). σ(z1[0])=a1[0](1a1[0])=(0.6106)(0.3894)=0.2378\sigma'(z_1[0]) = a_1[0]\,(1 - a_1[0]) = (0.6106)(0.3894) = 0.2378 σ(z1[1])=a1[1](1a1[1])=(0.6225)(0.3775)=0.2350\sigma'(z_1[1]) = a_1[1]\,(1 - a_1[1]) = (0.6225)(0.3775) = 0.2350 δ1[0]=La1[0]σ(z1[0])=(0.2846)(0.2378)=0.0677\delta_1[0] = \frac{\partial L}{\partial a_1[0]} \cdot \sigma'(z_1[0]) = (-0.2846)(0.2378) = \mathbf{-0.0677} δ1[1]=La1[1]σ(z1[1])=(+0.1626)(0.2350)=+0.0382\delta_1[1] = \frac{\partial L}{\partial a_1[1]} \cdot \sigma'(z_1[1]) = (+0.1626)(0.2350) = \mathbf{+0.0382}

The Vanishing Gradient Problem, live: σ(z)0.25\sigma'(z) \leq 0.25 always — it peaks at 0.25 when z=0z = 0. Every sigmoid layer attenuates the error signal by at least 4×4\times.

In our two-layer network: δ2=0.4065δ10.068|\delta_2| = 0.4065 \to |\delta_1| \approx 0.068. That is already a 6×6\times reduction in one layer. With ten sigmoid layers, δ\delta at layer 1 approaches zero — weights there receive near-zero gradient and stop learning entirely. This is why modern networks use ReLU instead of sigmoid in hidden layers: ReLU(z)=1\text{ReLU}'(z) = 1 for z>0z > 0, so gradient passes through unchanged.

5.5 Step 5 — Gradients for W1 and b1

Finally, each hidden pre-activation is

z1[i]=W1[i,0]x0+W1[i,1]x1+b1[i].z_1[i] = W_1[i,0]x_0 + W_1[i,1]x_1 + b_1[i].

Thus

LW1[i,j]=Lz1[i]z1[i]W1[i,j]=δ1[i]xj,Lb1[i]=δ1[i].\frac{\partial L}{\partial W_1[i,j]} = \frac{\partial L}{\partial z_1[i]}\frac{\partial z_1[i]}{\partial W_1[i,j]} = \delta_1[i]x_j, \qquad \frac{\partial L}{\partial b_1[i]} = \delta_1[i]. LW1[0,0]=δ1[0]x0=(0.0677)(1.0)=0.0677\frac{\partial L}{\partial W_1[0,0]} = \delta_1[0] \cdot x_0 = (-0.0677)(1.0) = \mathbf{-0.0677} LW1[0,1]=δ1[0]x1=(0.0677)(0.5)=0.0339\frac{\partial L}{\partial W_1[0,1]} = \delta_1[0] \cdot x_1 = (-0.0677)(0.5) = \mathbf{-0.0339} LW1[1,0]=δ1[1]x0=(+0.0382)(1.0)=+0.0382\frac{\partial L}{\partial W_1[1,0]} = \delta_1[1] \cdot x_0 = (+0.0382)(1.0) = \mathbf{+0.0382} LW1[1,1]=δ1[1]x1=(+0.0382)(0.5)=+0.0191\frac{\partial L}{\partial W_1[1,1]} = \delta_1[1] \cdot x_1 = (+0.0382)(0.5) = \mathbf{+0.0191} Lb1=δ1=[0.0677, +0.0382]\frac{\partial L}{\partial \mathbf{b_1}} = \boldsymbol{\delta}_1 = [\mathbf{-0.0677},\ \mathbf{+0.0382}]

We now have a gradient for every single parameter in the network. The backward pass is complete for Sample 1.

Checklist of gradients that must be calculated for Sample 1:

LW2[0],LW2[1],Lb2,\frac{\partial L}{\partial W_2[0]},\quad \frac{\partial L}{\partial W_2[1]},\quad \frac{\partial L}{\partial b_2},

LW1[0,0],LW1[0,1],LW1[1,0],LW1[1,1],\frac{\partial L}{\partial W_1[0,0]},\quad \frac{\partial L}{\partial W_1[0,1]},\quad \frac{\partial L}{\partial W_1[1,0]},\quad \frac{\partial L}{\partial W_1[1,1]},

Lb1[0],Lb1[1].\frac{\partial L}{\partial b_1[0]},\quad \frac{\partial L}{\partial b_1[1]}.

The intermediate derivatives δ2\delta_2, L/a1[0]\partial L/\partial a_1[0], L/a1[1]\partial L/\partial a_1[1], δ1[0]\delta_1[0], and δ1[1]\delta_1[1] are calculated only to make those nine parameter gradients easy to obtain.

6. Gradient Descent: Where Learning Happens

The backward pass measured how each weight contributed to the error. Gradient descent is the step that acts on that measurement. These are two separate steps.

Forward pass: measure error.   Backward pass: measure blame.   Gradient descent: correct weights.

The update rule:

w    wαLww \;\leftarrow\; w - \alpha \cdot \frac{\partial L}{\partial w}

Move each weight a small step opposite to its gradient:

α\alpha is the learning rate: the step size. Too large and you overshoot and diverge. Too small and training takes forever. We use α=0.1\alpha = 0.1.

We do not apply this update yet. In mini-batch gradient descent we first average gradients across all samples in a batch, then apply one update. This is explained in the next section.

7. Batches — Aggregating Gradients Before Updating

Why batch? Gradient descent on a single sample is noisy — one unusual sample could point in the wrong direction. Averaging gradients across a batch gives a more stable estimate of which direction actually reduces loss across the data distribution. One weight update per batch, not per sample.

7.1 Forward and Backward Pass for Sample 2: x = [0.2, 0.9], y = 0

Forward pass:

z1[0]=(0.5)(0.2)+(0.3)(0.9)+0.1=0.1000.270+0.100=0.070z_1[0] = (0.5)(0.2) + (-0.3)(0.9) + 0.1 = 0.100 - 0.270 + 0.100 = -0.070 z1[1]=(0.2)(0.2)+(0.8)(0.9)+(0.1)=0.040+0.7200.100=+0.660z_1[1] = (0.2)(0.2) + (0.8)(0.9) + (-0.1) = 0.040 + 0.720 - 0.100 = +0.660 a1[0]=σ(0.070)0.4825a1[1]=σ(0.660)0.6593a_1[0] = \sigma(-0.070) \approx 0.4825 \qquad a_1[1] = \sigma(0.660) \approx 0.6593 z2=(0.7)(0.4825)+(0.4)(0.6593)+0.2=0.33780.2637+0.200=0.2741z_2 = (0.7)(0.4825) + (-0.4)(0.6593) + 0.2 = 0.3378 - 0.2637 + 0.200 = 0.2741 y^=σ(0.2741)0.5681\hat{y} = \sigma(0.2741) \approx 0.5681 L=log(10.5681)=log(0.4319)0.840L = -\log(1 - 0.5681) = -\log(0.4319) \approx 0.840

The network predicts 56.8% positive for a true-negative sample. Both samples in Batch 1 are currently wrong.

Backward pass:

δ2=y^y=0.56810=+0.5681\delta_2 = \hat{y} - y = 0.5681 - 0 = +0.5681 LW2[0]=(+0.5681)(0.4825)=+0.2741,LW2[1]=(+0.5681)(0.6593)=+0.3745,Lb2=+0.5681\begin{aligned} \frac{\partial L}{\partial W_2[0]} &= (+0.5681)(0.4825) = +0.2741, \\ \frac{\partial L}{\partial W_2[1]} &= (+0.5681)(0.6593) = +0.3745, \\ \frac{\partial L}{\partial b_2} &= +0.5681 \end{aligned} La1[0]=(+0.5681)(0.7)=+0.3977,La1[1]=(+0.5681)(0.4)=0.2272\frac{\partial L}{\partial a_1[0]} = (+0.5681)(0.7) = +0.3977, \quad \frac{\partial L}{\partial a_1[1]} = (+0.5681)(-0.4) = -0.2272 σ(z1[0])=(0.4825)(0.5175)=0.2497,σ(z1[1])=(0.6593)(0.3407)=0.2246\sigma'(z_1[0]) = (0.4825)(0.5175) = 0.2497, \quad \sigma'(z_1[1]) = (0.6593)(0.3407) = 0.2246 δ1[0]=(+0.3977)(0.2497)=+0.0993,δ1[1]=(0.2272)(0.2246)=0.0510\delta_1[0] = (+0.3977)(0.2497) = +0.0993, \quad \delta_1[1] = (-0.2272)(0.2246) = -0.0510 LW1[0,0]=+0.0199,LW1[0,1]=+0.0894,LW1[1,0]=0.0102,LW1[1,1]=0.0459\begin{aligned} \frac{\partial L}{\partial W_1[0,0]} &= +0.0199, & \frac{\partial L}{\partial W_1[0,1]} &= +0.0894, \\ \frac{\partial L}{\partial W_1[1,0]} &= -0.0102, & \frac{\partial L}{\partial W_1[1,1]} &= -0.0459 \end{aligned} Lb1=[+0.0993, 0.0510]\frac{\partial L}{\partial \mathbf{b_1}} = [+0.0993,\ -0.0510]

7.2 Average the Gradients Across Batch 1

ParameterSample 1Sample 2Batch average gˉ\bar{g}
W2[0]W_2[0]0.2482-0.2482+0.2741+0.2741+0.0130
W2[1]W_2[1]0.2530-0.2530+0.3745+0.3745+0.0608
b2b_20.4065-0.4065+0.5681+0.5681+0.0808
W1[0,0]W_1[0,0]0.0677-0.0677+0.0199+0.0199−0.0239
W1[0,1]W_1[0,1]0.0339-0.0339+0.0894+0.0894+0.0278
W1[1,0]W_1[1,0]+0.0382+0.03820.0102-0.0102+0.0140
W1[1,1]W_1[1,1]+0.0191+0.01910.0459-0.0459−0.0134
b1[0]b_1[0]0.0677-0.0677+0.0993+0.0993+0.0158
b1[1]b_1[1]+0.0382+0.03820.0510-0.0510−0.0064

Sample 1 says "push W2[0]W_2[0] down." Sample 2 says "push W2[0]W_2[0] up." They partially disagree. The batch average computes the net direction: slightly up (+0.013+0.013). With thousands of samples per batch this averaging cancels individual noise and gives a stable, reliable gradient estimate.

7.3 Apply Gradient Descent: One Weight Update

wwαgˉ,α=0.1w \leftarrow w - \alpha \cdot \bar{g}, \qquad \alpha = 0.1
ParameterOld valueαgˉ-\,\alpha \cdot \bar{g}New value
W2[0]W_2[0]+0.7000+0.7000(0.1)(+0.0130)=0.0013-(0.1)(+0.0130) = -0.0013+0.6987
W2[1]W_2[1]0.4000-0.4000(0.1)(+0.0608)=0.0061-(0.1)(+0.0608) = -0.0061−0.4061
b2b_2+0.2000+0.2000(0.1)(+0.0808)=0.0081-(0.1)(+0.0808) = -0.0081+0.1919
W1[0,0]W_1[0,0]+0.5000+0.5000(0.1)(0.0239)=+0.0024-(0.1)(-0.0239) = +0.0024+0.5024
W1[0,1]W_1[0,1]0.3000-0.3000(0.1)(+0.0278)=0.0028-(0.1)(+0.0278) = -0.0028−0.3028
W1[1,0]W_1[1,0]+0.2000+0.2000(0.1)(+0.0140)=0.0014-(0.1)(+0.0140) = -0.0014+0.1986
W1[1,1]W_1[1,1]+0.8000+0.8000(0.1)(0.0134)=+0.0013-(0.1)(-0.0134) = +0.0013+0.8013
b1[0]b_1[0]+0.1000+0.1000(0.1)(+0.0158)=0.0016-(0.1)(+0.0158) = -0.0016+0.0984
b1[1]b_1[1]0.1000-0.1000(0.1)(0.0064)=+0.0006-(0.1)(-0.0064) = +0.0006−0.0994

The changes are tiny. That is expected at step 1 with a cautious learning rate. Over hundreds of epochs these small, consistent nudges accumulate into weights that produce accurate predictions.

8. Epochs — The Full Training Loop

One epoch = one complete pass through every training sample exactly once. With 4 samples and batch size 2: one epoch == 2 batches == 2 weight updates.

8.1 The Nested Structure

EPOCH 1
|
|-- BATCH 1  {Sample 1, Sample 2}   (starting weights)
|   |-- Forward pass: Sample 1  -->  y_hat=0.5935, L=0.522
|   |-- Forward pass: Sample 2  -->  y_hat=0.5681, L=0.840
|   |-- Backward pass: Sample 1 -->  gradient set A
|   |-- Backward pass: Sample 2 -->  gradient set B
|   |-- average(A, B)           -->  batch gradient
|   `-- Weight update           -->  weights shift (update #1)
|
`-- BATCH 2  {Sample 3, Sample 4}   (UPDATED weights from Batch 1)
    |-- Forward pass: Sample 3  -->  y_hat=?, L=?
    |-- Forward pass: Sample 4  -->  y_hat=?, L=?
    |-- Backward pass: Sample 3 -->  gradient set C
    |-- Backward pass: Sample 4 -->  gradient set D
    |-- average(C, D)           -->  batch gradient
    `-- Weight update           -->  weights shift (update #2)

EPOCH 1 DONE: 4 samples seen, weights updated twice.

EPOCH 2: repeat from scratch, using the weights epoch 1 left behind.

8.2 Critical Point: Reset Gradients Across Batches

Within a batch: gradients are averaged across samples, then one update is applied.

Across batches within an epoch: there is no accumulation. Batch 2 runs on whatever weights Batch 1 left behind. The gradients computed in Batch 1 do not directly influence Batch 2 — they influenced the weights that Batch 2 inherits. The effect is indirect, via the updated weight values.

8.3 What the Epoch Loss Number in Training Logs Means

When training prints:

Epoch 1/100 --- loss: 0.680

That 0.680 is the average of all per-sample losses collected during forward passes of that epoch:

Lepoch=1Ni=1NLi=L1+L2+L3+L44L_{\text{epoch}} = \frac{1}{N} \sum_{i=1}^{N} L_i = \frac{L_1 + L_2 + L_3 + L_4}{4}

This is a monitoring number. It is computed from forward passes. It does not control the weight updates. Its only purpose: tell you whether the model is improving over time. If this number decreases epoch-to-epoch, training is working.

9. Summary: Every Step and Its Role

StepChanges weights?Purpose
Forward passNoPush input xx through the network to produce y^\hat{y}. Compute loss LL. This is measurement only.
Backward passNoUse the chain rule to compute L/w\partial L / \partial w for every weight. Determines how much each weight is "to blame" for the error.
Gradient descentYesApply wwαww \leftarrow w - \alpha \nabla w. This is the only step that actually changes the weights. It is where learning happens.
BatchThe set of samples over which gradients are averaged before one weight update. Larger batches give smoother gradients; smaller batches introduce noise that can help escape local minima.
EpochOne complete pass through the full training set. Contains multiple batches. Contains multiple weight updates. The epoch loss is a scalar monitoring metric computed from the forward passes of that epoch.

The complete training loop in pseudocode:

for epoch in 1..num_epochs:
    shuffle(dataset)
    epoch_losses = []

    for batch in chunks(dataset, size=batch_size):

        # Forward pass --- no weight change
        for sample in batch:
            y_hat = forward(sample.x, weights)
            L     = loss(y_hat, sample.y)
            epoch_losses.append(L)

        # Backward pass --- no weight change
        grads = mean([backprop(sample, weights) for sample in batch])

        # Gradient descent --- ONLY step that changes weights
        weights = weights - alpha * grads

    print("Epoch loss:", mean(epoch_losses))   # monitoring only