In 1943, Warren McCulloch and Walter Pitts described a mathematical model
of how a brain cell fires: if the sum of its inputs exceeds a threshold, it activates.
Otherwise it stays silent.
They had no idea they were describing
what would become the architecture behind GPT-4.
The idea is that simple. The consequences are that large.
01 — The Bridge: You Already Know Most of This
Before writing a single line of neural network code, let's locate where you actually are. Over the last 14 days you've learned: how gradient descent finds optimal weights (Day 3), how logistic regression maps a weighted sum of inputs to a probability (Day 5), how regularisation prevents overfitting (Day 10), and how ensembles of weak models build something stronger (Days 11–13).
A neural network uses all of these ideas simultaneously. It is, at its core, nothing more than a logistic regression unit — extended in two ways. First, you apply many of them in parallel, each looking at the input from a different angle. Second, you feed those intermediate outputs into another set of logistic regression units — creating a hierarchy of learned representations. That hierarchy is what makes neural networks universally powerful. The individual components are things you've already seen.
Day 5 (Logistic Regression): one unit, sigmoid activation, linear decision boundary, one output. Everything the model can express is a straight line through input space.
Today (Neural Network): many units in layers, non-linear activations (ReLU, tanh, sigmoid), non-linear decision boundaries of arbitrary complexity, multiple outputs possible. The weights are still learned by gradient descent. The loss function is still cross-entropy for classification, MSE for regression. The only structural difference is the composition of layers.
If you understand logistic regression, you understand 80% of a neural network. The remaining 20% — backpropagation and activation functions — is what we build today.
02 — The Biological Neuron vs the Mathematical Neuron
The biological inspiration is worth understanding — not because the analogy is perfect, but because it explains why the architecture is shaped the way it is.
Learning happens by strengthening connections that fire together repeatedly (Hebbian learning: "neurons that fire together wire together") and weakening unused connections.
Learning happens by adjusting weights using gradient descent on a loss function.
The key insight: the biological and mathematical versions are structurally identical. Dendrites = input weights. Cell body summation = weighted sum z. Firing threshold = activation function. Axon output = activation a = f(z). The translation from biology to algebra is almost one-to-one.
03 — The Neuron Equation: From Logistic Regression to General Neuron
This is the central connection of the entire post. Look at these two equations side by side:
Logistic Regression (Day 5):
z = w₁x₁ + w₂x₂ + … + wₙxₙ + b
ŷ = σ(z) = 1 / (1 + e⁻ᶻ)
A Single Neural Network Neuron:
z = w₁x₁ + w₂x₂ + … + wₙxₙ + b
a = f(z)
The pre-activation z is identical — a weighted sum plus bias. The only difference is the activation function f. Logistic regression uses sigmoid. A neural network neuron can use sigmoid, tanh, ReLU, GELU, or anything differentiable. Logistic regression is a special case of a neural network with one neuron and sigmoid activation.
A neural network is what happens when you have many of these neurons, arranged in layers, each passing its output a as input to the next layer.
04 — Why One Neuron Is Not Enough: The XOR Problem
A single neuron — regardless of what activation function it uses — can only learn a linear decision boundary. It draws one straight line (or hyperplane in higher dimensions) and classifies everything on one side as one class, and the other side as another class. This is the exact same limitation as logistic regression.
The XOR problem (exclusive OR) is the classic demonstration of why this is limiting:
import numpy as np
import matplotlib
matplotlib.use('Agg')
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier
# XOR: output is 1 when exactly one input is 1 — NOT linearly separable
X_xor = np.array([[0,0], [0,1], [1,0], [1,1]])
y_xor = np.array([0, 1, 1, 0 ])
# Single logistic regression unit (one neuron, sigmoid)
lr = LogisticRegression()
lr.fit(X_xor, y_xor)
# Neural network: 2 hidden neurons → ReLU → 1 output neuron
nn = MLPClassifier(hidden_layer_sizes=(2,), activation='relu',
max_iter=10000, random_state=42)
nn.fit(X_xor, y_xor)
print(f"{'Input':>10} {'True':>5} {'Logistic Reg':>14} {'Neural Net':>11}")
print("-" * 50)
for x, y in zip(X_xor, y_xor):
lr_pred = lr.predict([x])[0]
nn_pred = nn.predict([x])[0]
print(f"{str(x):>10} {y:>5} {lr_pred:>14} {nn_pred:>11}")
print(f"\nLogistic Regression accuracy: {lr.score(X_xor, y_xor):.0%}")
print(f"Neural Network accuracy: {nn.score(X_xor, y_xor):.0%}")
XOR is not linearly separable. If you plot the four points, there is no single straight line that puts the 1s on one side and the 0s on the other. [0,1] and [1,0] are 1; [0,0] and [1,1] are 0. Any line that separates [0,1] will include [1,1]; any line that separates [1,0] will include [0,0]. It's geometrically impossible with one line. Logistic regression gets 50% — effectively random.
The neural network with 2 hidden neurons solves it by creating an intermediate representation. The two hidden neurons each learn a linear boundary — but together they create a coordinate system where XOR becomes linearly separable. The first hidden neuron might learn "at least one input is 1" (OR), the second might learn "both inputs are 1" (AND). The output neuron then learns "OR but not AND" — which is exactly XOR. Three linear boundaries, combined, solve a non-linear problem.
This is what depth does: it transforms the input space into a new representation where previously non-linear problems become linear. Every layer in a neural network is a learned coordinate transformation.
05 — Architecture: Layers, Weights, and Shapes
A neural network is a directed graph of neurons arranged in layers. Data flows left to right — from input layer through hidden layers to output layer. Every arrow between neurons represents a weight.
For a network with an input layer, one hidden layer, and an output layer:
Layer 1 (hidden):
Z¹ = X · W¹ + b¹ A¹ = f(Z¹)
Layer 2 (output):
Z² = A¹ · W² + b² A² = f(Z²) = ŷ
Where X is the input matrix (batch_size × n_features), W¹ is the weight matrix (n_features × hidden_size), b¹ is the bias vector (hidden_size), and f is the activation function applied element-wise. The shapes must be consistent: output of each layer must match input shape of the next.
import numpy as np
# Build a 2-layer neural network from scratch
# Architecture: 2 inputs → 3 hidden neurons → 1 output
np.random.seed(42)
# ── Weight matrices (randomly initialised) ──────────────────────────────
W1 = np.random.randn(2, 3) * 0.1 # shape: (n_inputs=2, n_hidden=3)
b1 = np.zeros(3) # shape: (n_hidden=3,)
W2 = np.random.randn(3, 1) * 0.1 # shape: (n_hidden=3, n_output=1)
b2 = np.zeros(1) # shape: (n_output=1,)
# ── Activation functions ─────────────────────────────────────────────────
def sigmoid(z): return 1 / (1 + np.exp(-z))
def relu(z): return np.maximum(0, z)
# ── Forward pass ─────────────────────────────────────────────────────────
def forward(X, W1, b1, W2, b2):
Z1 = X @ W1 + b1 # (batch, 2) @ (2, 3) → (batch, 3)
A1 = relu(Z1) # non-linearity in hidden layer
Z2 = A1 @ W2 + b2 # (batch, 3) @ (3, 1) → (batch, 1)
A2 = sigmoid(Z2) # sigmoid at output → probability [0,1]
return Z1, A1, Z2, A2
# ── Single example ───────────────────────────────────────────────────────
X_sample = np.array([[0.5, -1.2]]) # 1 example, 2 features
Z1, A1, Z2, A2 = forward(X_sample, W1, b1, W2, b2)
print("── Forward Pass (single example) ──")
print(f"Input X: {X_sample[0]}")
print(f"Z1 (pre-relu): {Z1[0].round(4)}")
print(f"A1 (after relu): {A1[0].round(4)} ← negative values zeroed")
print(f"Z2 (pre-sigmoid): {Z2[0].round(4)}")
print(f"A2 (output prob): {A2[0].round(4)} ← final prediction")
print(f"\nShapes: X={X_sample.shape}, W1={W1.shape}, A1={A1.shape}, W2={W2.shape}, A2={A2.shape}")
# Count parameters
n_params = W1.size + b1.size + W2.size + b2.size
print(f"Total learnable parameters: {n_params} (W1:{W1.size} + b1:{b1.size} + W2:{W2.size} + b2:{b2.size})")
Z1 has a negative value (−0.0518). After ReLU, that value becomes exactly 0 — that neuron is "dead" for this input and contributes nothing to the next layer. The other two neurons pass their values forward. This is ReLU's role: introduce non-linearity by choosing which neurons activate for each input.
The output A2=0.5007 ≈ 0.5 — the model has random weights (just initialised), so it can't predict anything meaningful yet. After training, the weights W1, b1, W2, b2 will be adjusted to make A2 high for positive examples and low for negative ones. The forward pass structure never changes — only the numbers inside the weight matrices change during training.
13 parameters total. A GPT-4-class model has ~1.8 trillion parameters — same arithmetic, same matrix multiplications, incomprehensibly more weights. The mechanism is identical.
06 — Activation Functions: The Non-Linearity Menu
The activation function is what makes neural networks powerful. Without it — if every layer just applied a linear transformation — you could collapse the entire network into a single linear layer, no matter how many layers you stacked. Activation functions break this collapse by introducing non-linearity at every neuron.
import numpy as np
x = np.linspace(-3, 3, 7) # 7 sample values from -3 to 3
# ── All common activation functions ──────────────────────────────────────
sigmoid = lambda z: 1 / (1 + np.exp(-z))
tanh = lambda z: np.tanh(z)
relu = lambda z: np.maximum(0, z)
leaky = lambda z: np.where(z > 0, z, 0.01 * z)
gelu = lambda z: z * sigmoid(1.702 * z) # approximate GELU
softmax = lambda z: np.exp(z) / np.exp(z).sum() # for output layer, multi-class
print(f"{'x':>6} {'sigmoid':>8} {'tanh':>7} {'relu':>6} {'leaky':>7} {'gelu':>7}")
print("-" * 55)
for xi in x:
print(f"{xi:>6.1f} {sigmoid(xi):>8.4f} {tanh(xi):>7.4f} {relu(xi):>6.1f} {leaky(xi):>7.4f} {gelu(xi):>7.4f}")
print(f"\nKey derivatives at x=0 (gradient flow):")
print(f" sigmoid'(0) = 0.25 ← non-zero but small, vanishes at extremes")
print(f" tanh'(0) = 1.0 ← stronger gradient at centre")
print(f" relu'(0) = 0 or 1 ← 0 for negatives (dead neurons), 1 for positives")
print(f" gelu'(0) ≈ 0.5 ← smooth transition, used in Transformers/GPT")
07 — Backpropagation: How Gradients Flow Backwards
Training a neural network means adjusting all the weights to minimise the loss. Gradient descent needs the gradient of the loss with respect to every weight. Backpropagation is the efficient algorithm for computing all these gradients simultaneously using the chain rule of calculus.
Consider our 2-layer network. The forward pass computes:
X → Z1 → A1 → Z2 → A2 → Loss
We want ∂Loss/∂W1 — how does changing W1 affect the loss? The chain rule gives:
∂Loss/∂W1 = (∂Loss/∂A2) × (∂A2/∂Z2) × (∂Z2/∂A1) × (∂A1/∂Z1) × (∂Z1/∂W1)
Each term is a local gradient — computable at each layer. Backpropagation computes these local gradients during the forward pass, stores them, then multiplies them in reverse order (right to left) to get every weight's gradient. The key efficiency insight: intermediate gradients are computed once and reused — no redundant computation.
In matrix form for a single layer with ReLU activation:
δ² = (ŷ − y) (output error)
∂L/∂W² = A¹ᵀ · δ² (gradient for W2)
δ¹ = (δ² · W²ᵀ) ⊙ ReLU'(Z¹) (propagate error back through hidden layer)
∂L/∂W¹ = Xᵀ · δ¹ (gradient for W1)
The ⊙ symbol means element-wise multiplication. ReLU'(Z1) = 1 where Z1 > 0, else 0 — this is what "gates" the gradient through each neuron.
import numpy as np
np.random.seed(42)
# ── Dataset: XOR problem ─────────────────────────────────────────────────
X = np.array([[0,0], [0,1], [1,0], [1,1]], dtype=float)
y = np.array([[0], [1], [1], [0]], dtype=float)
# ── Weight initialisation ─────────────────────────────────────────────────
W1 = np.random.randn(2, 4) * 0.5 # 2 inputs → 4 hidden
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5 # 4 hidden → 1 output
b2 = np.zeros((1, 1))
sigmoid = lambda z: 1 / (1 + np.exp(-z))
relu = lambda z: np.maximum(0, z)
relu_d = lambda z: (z > 0).astype(float) # derivative of ReLU
bce_loss = lambda y, yh: -np.mean(y*np.log(yh+1e-8) + (1-y)*np.log(1-yh+1e-8))
lr = 0.5
print(f"{'Epoch':>6} {'Loss':>8} {'Predictions':>30}")
print("-" * 52)
for epoch in range(5001):
# ── Forward pass ────────────────────────────────────────────────────
Z1 = X @ W1 + b1
A1 = relu(Z1)
Z2 = A1 @ W2 + b2
A2 = sigmoid(Z2)
loss = bce_loss(y, A2)
# ── Backward pass (chain rule) ──────────────────────────────────────
dA2 = A2 - y # ∂Loss/∂A2 (BCE + sigmoid combined)
dW2 = A1.T @ dA2 / len(X) # ∂Loss/∂W2
db2 = dA2.mean(axis=0, keepdims=True)
dA1 = dA2 @ W2.T # propagate error back
dZ1 = dA1 * relu_d(Z1) # gate through ReLU derivative
dW1 = X.T @ dZ1 / len(X) # ∂Loss/∂W1
db1 = dZ1.mean(axis=0, keepdims=True)
# ── Weight update ───────────────────────────────────────────────────
W2 -= lr * dW2; b2 -= lr * db2
W1 -= lr * dW1; b1 -= lr * db1
if epoch % 1000 == 0:
preds = (A2 > 0.5).astype(int).flatten()
print(f"{epoch:>6} {loss:>8.4f} {preds.tolist()} vs {y.flatten().astype(int).tolist()}")
print(f"\nFinal predictions (probabilities):")
for xi, yi, ai in zip(X, y, A2):
print(f" Input {xi.astype(int)} → prob={ai[0]:.4f} → pred={int(ai[0]>0.5)} (true={int(yi[0])})")
Epoch 0 — random weights, wrong on 3 of 4. The network starts with no knowledge. Loss=0.74. Predictions are mostly garbage.
Epoch 1000 — still wrong on one. The gradient has found a partial solution. Two patterns learned; the third ([1,0]) is still being misclassified. Loss dropped to 0.42.
Epoch 2000 — all correct. The network found a weight configuration that solves XOR. Loss continues falling but the classification is already perfect. The remaining epochs are "fine-tuning" the probabilities toward 0.99 and 0.01.
Final probabilities: [0,0] → 0.98% chance of being 1 (almost certainly 0). [0,1] → 98.9% chance of being 1 (confident correct). The network isn't just correct — it's confident. This is what training does: it doesn't just find the right side of the decision boundary, it pushes probabilities toward the extremes as confidence builds.
The backprop code is 6 lines. Six lines of matrix math implement the entire gradient calculation for a 2-layer network with 17 parameters. Every deep learning framework — PyTorch, TensorFlow, JAX — is computing the same 6 steps, just generalised to arbitrary depth and parallelised across GPUs.
08 — The Vanishing Gradient Problem
Now that you've seen backpropagation, you can understand the most important historical obstacle in deep learning — and why ReLU solved it.
import numpy as np
# Demonstrate how gradients shrink through sigmoid layers
# At each layer, sigmoid derivative is max 0.25
sigmoid_d = lambda z: sigmoid(z) * (1 - sigmoid(z)) # max 0.25
sigmoid = lambda z: 1 / (1 + np.exp(-z))
relu_d = lambda z: (z > 0).astype(float)
print("Gradient magnitude after backprop through N layers")
print(f"{'Layers':>7} {'Sigmoid gradient':>18} {'ReLU gradient':>15}")
print("-" * 48)
grad = 1.0
for L in range(1, 11):
sig_grad = 0.25 ** L # worst case: max derivative per layer
relu_grad = 1.0 ** L # for active neurons: derivative = 1
print(f"{L:>7} {sig_grad:>18.8f} {relu_grad:>15.4f}")
print(f"\nSigmoid gradient after 10 layers: {0.25**10:.2e} (essentially zero)")
print(f"ReLU gradient after 10 layers: {1.0**10:.4f} (unchanged for active neurons)")
The sigmoid derivative is at most 0.25 per layer. After 10 layers of backpropagation, the gradient reaching the first layer is at most 0.25¹⁰ ≈ 9.5×10⁻⁷ — essentially zero. The weights in early layers receive a gradient that's a millionth of the original signal. They barely move. They barely learn. The network is "deep" in name only — the early layers are frozen by vanishing gradients.
This is why deep learning was considered impractical before 2010. Networks of more than 3–4 layers couldn't be trained effectively with sigmoid or tanh activations.
ReLU fixed this in one insight. For positive pre-activations, ReLU's derivative is exactly 1 — the gradient passes through unchanged. A gradient flowing through 10 ReLU layers with active neurons is multiplied by 1¹⁰ = 1. No shrinkage. Early layers can receive strong gradient signals and learn meaningful representations. This single change — replacing sigmoid with ReLU — is one of the most impactful ideas in the history of deep learning, enabling networks 10, 50, 150 layers deep.
09 — Complete Neural Network with PyTorch
Now the full picture — from data to trained model to evaluation, using PyTorch (the dominant deep learning framework):
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report
# ── Data preparation ─────────────────────────────────────────────────────
data = load_breast_cancer()
X, y = data.data, data.target
scaler = StandardScaler() # Neural networks need scaled features
X = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
X_tr = torch.FloatTensor(X_tr)
X_te = torch.FloatTensor(X_te)
y_tr = torch.FloatTensor(y_tr).unsqueeze(1) # shape: (N, 1)
y_te = torch.FloatTensor(y_te).unsqueeze(1)
# ── Model definition ─────────────────────────────────────────────────────
class NeuralNet(nn.Module):
def __init__(self, n_features):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_features, 64), # input → hidden 1 (64 neurons)
nn.ReLU(),
nn.Dropout(0.2), # randomly zero 20% of neurons during training
nn.Linear(64, 32), # hidden 1 → hidden 2 (32 neurons)
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(32, 1), # hidden 2 → output (1 neuron)
nn.Sigmoid() # output probability [0,1]
)
def forward(self, x):
return self.net(x)
model = NeuralNet(X_tr.shape[1]) # 30 input features
criterion = nn.BCELoss() # binary cross-entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)
# ── Count parameters ─────────────────────────────────────────────────────
n_params = sum(p.numel() for p in model.parameters())
print(f"Model: {n_params} trainable parameters")
print(f"Architecture: 30 → 64 → 32 → 1 (ReLU + Dropout + Sigmoid)\n")
# ── Training loop ─────────────────────────────────────────────────────────
print(f"{'Epoch':>6} {'Train Loss':>11} {'Train Acc':>10} {'Val Acc':>8}")
print("-" * 45)
for epoch in range(1, 101):
model.train()
optimizer.zero_grad()
y_pred = model(X_tr)
loss = criterion(y_pred, y_tr)
loss.backward() # compute all gradients (backprop)
optimizer.step() # update all weights
if epoch % 20 == 0:
model.eval()
with torch.no_grad():
tr_acc = ((y_pred > 0.5) == y_tr).float().mean().item()
val_acc = ((model(X_te) > 0.5) == y_te).float().mean().item()
print(f"{epoch:>6} {loss.item():>11.4f} {tr_acc:>10.4f} {val_acc:>8.4f}")
# ── Final evaluation ─────────────────────────────────────────────────────
model.eval()
with torch.no_grad():
y_pred_te = (model(X_te) > 0.5).numpy().astype(int).flatten()
print(f"\nFinal test accuracy: {accuracy_score(y_te.numpy(), y_pred_te):.4f}")
print("\nClassification Report:")
print(classification_report(y_te.numpy().astype(int), y_pred_te,
target_names=data.target_names))
2273 parameters learn to classify breast cancer with 98.25% accuracy. Each epoch is: forward pass (compute predictions) → loss (measure error) → loss.backward() (compute all 2273 gradients via backprop) → optimizer.step() (update all 2273 weights with Adam). The entire training loop is 6 lines of PyTorch code — the framework handles the chain rule automatically.
Dropout is key. During training, 20% of neurons in each hidden layer are randomly set to zero each forward pass — they contribute nothing and receive no gradient update. This forces other neurons to pick up the slack, preventing any single neuron from becoming critical. It acts as an ensemble of sub-networks — similar in spirit to Random Forest (Day 11). At test time, dropout is turned off (model.eval()) and all neurons contribute.
Adam optimizer is gradient descent with adaptive learning rates per parameter — parameters that receive large gradients get smaller updates; parameters that haven't moved much get larger pushes. Much faster convergence than vanilla SGD on most problems. Used by default in almost all modern neural network training.
Accuracy is 98.25% — matching XGBoost from Day 13. On this dataset, a properly tuned neural network and a properly tuned XGBoost reach similar performance. This is typical for tabular data: the models are competitive, and the choice between them comes down to speed, interpretability, and deployment constraints.
10 — Neural Network vs Logistic Regression: The Full Comparison
You now have everything needed to make the comparison precise:
Decision boundary: linear (one hyperplane)
Learning: gradient descent on binary cross-entropy
Non-linearity: sigmoid only at output — not in the computation
Expressiveness: can only solve linearly separable problems
Parameters: n_features + 1 (weights + bias)
Training: fast, stable, interpretable coefficients
XOR problem: 50% accuracy (cannot solve)
Decision boundary: arbitrarily non-linear (any shape)
Learning: same gradient descent, same cross-entropy, but with backprop computing gradients through layers
Non-linearity: ReLU/GELU at every hidden layer — creates hierarchical representations
Expressiveness: universal function approximator (can approximate any continuous function given enough neurons)
Parameters: potentially millions
Training: slower, needs scaling, more hyperparameters
XOR problem: 100% accuracy (solved with 2 hidden neurons)
Hornik, Stinchcombe, and White proved in 1989 that a neural network with a single hidden layer containing enough neurons can approximate any continuous function to arbitrary precision. This is the theoretical foundation for why neural networks work at all — they're not just another family of models, they're universal.
The practical implication: if there exists any pattern in your data that can be expressed as a function, a large enough neural network can learn to express it. The challenge shifts from "can the model learn this?" to "can the model learn this from the data you have, in the time you have, with the architecture you chose?" — which is the engineering challenge that makes deep learning hard in practice.
And depth helps dramatically: a deep network can express functions that would require exponentially more neurons if implemented in a single layer. This is why going deeper is almost always better than going wider, up to a point.
11 — What Comes Next: The Deep Learning Stack
Today you built the foundation. Every deep learning architecture you'll encounter in the coming days is a specialisation or extension of this same structure — neurons, layers, activations, backprop, gradient descent. Here's how what's coming connects back to today:
12 — A Thought to Sit With
The Universal Approximation Theorem says a neural network can approximate any function. But it says nothing about finding the right weights — just that they exist. Training is the search problem: gradient descent navigating a loss landscape of billions of dimensions, finding a set of weights that works. Most of the landscape is bad. The ones we find are staggeringly good.
Neural networks solve problems we can't specify — we show them examples and they infer the rule. But then we can't read the rule back out. The model knows, in some sense, how to detect tumours in a scan or predict a word in a sentence. It just can't tell us why, or in terms we can verify. Should we trust a decision made by a process we cannot read?
13 — Coming Up Next
Day 16 — CNNs from Scratch. Convolutional Neural Networks are what made computer vision tractable. Now that you understand layers, activations, and backprop, you'll see exactly why convolutions are just a special kind of linear layer — and how pooling, receptive fields, and feature maps emerge naturally from the same principles you learned today.
14 — Go Deeper
▶ 3Blue1Brown: But what is a neural network? (Chapter 1)
YouTube · 19 min · The most beautiful visual introduction to neural network structure and forward pass. Watch before reading §05.
▶ 3Blue1Brown: Gradient descent, how neural networks learn (Chapter 2)
YouTube · 21 min · Visualises the loss landscape and gradient descent as navigation. Perfect companion to §07.
▶ 3Blue1Brown: What is backpropagation really doing? (Chapter 3)
YouTube · 14 min · The clearest visual explanation of backprop and the chain rule in existence. Watch alongside §07–08.
🔗 TensorFlow Playground — playground.tensorflow.org
Interactive browser demo: draw your own datasets, choose architectures, watch the network learn a decision boundary in real time. The best tool for building intuition about layers and activations.
- A neuron = logistic regression with a different activation. z = Wx + b is identical. The difference is f(z) — sigmoid, ReLU, tanh, or GELU instead of always sigmoid. Logistic regression is a one-neuron neural network.
- Layers compose neurons in parallel. Each layer applies Z = XW + b, then A = f(Z) element-wise. The output of one layer is the input to the next. This is matrix multiplication — the entire forward pass is a sequence of matrix operations.
- Depth creates non-linearity. Without activation functions, any stack of linear layers collapses to a single linear layer. Activations break this — each layer transforms the space into a new representation where patterns become linearly separable that weren't before.
- XOR proves depth's value. A single neuron (logistic regression) gets 50%. Two hidden neurons + one output get 100%. Three linear boundaries combine to solve a non-linear problem.
- Backpropagation = chain rule applied to the forward pass. Computes ∂Loss/∂W for every weight by multiplying local derivatives from right to left. PyTorch does this automatically with loss.backward().
- Vanishing gradients: sigmoid's derivative ≤ 0.25 per layer. After 10 layers: 0.25¹⁰ ≈ 10⁻⁶. Early layers don't learn. ReLU: derivative = 1 for active neurons. After 10 layers: 1¹⁰ = 1. Gradients flow. This single change enabled modern deep learning.
- ReLU is the default hidden activation. max(0, z). Computationally trivial, no vanishing gradient for active neurons. Dead neuron problem for z≤0, mitigated by He initialisation and Leaky ReLU.
- GELU is the Transformer activation. Smooth probabilistic gating. Used in BERT, GPT-2/3/4, and most modern language models. When you work with Transformers (Days 21–22), you'll see GELU everywhere.
- Dropout = implicit ensemble. Randomly zeroes neurons during training. Forces distributed representations. Must call model.eval() at test time to disable it. Standard regularisation for neural networks.
- Universal Approximation Theorem: a network with one hidden layer and enough neurons can approximate any continuous function. Depth is more parameter-efficient: deep networks express functions that shallow networks would need exponentially more neurons to express.
- How is a neural network related to logistic regression?
Logistic regression is a special case of a neural network with one neuron, sigmoid activation, and no hidden layers. Both compute z = Wx + b and apply an activation function. A neural network extends this by stacking many such units in layers, using non-linear activations (typically ReLU) in hidden layers to create hierarchical representations that can express non-linear decision boundaries. - Why do we need activation functions, and why is ReLU preferred over sigmoid in hidden layers?
Without activation functions, any stack of linear layers collapses to a single linear transformation — depth adds no expressiveness. Activations introduce non-linearity, allowing the network to learn complex patterns. ReLU is preferred in hidden layers because its derivative is 1 for positive inputs, preventing the vanishing gradient problem that sigmoid causes (sigmoid derivative ≤ 0.25 → after 10 layers, gradient ≈ 10⁻⁶). Sigmoid is still used at the output layer for binary classification. - What is backpropagation and how does it work?
Backpropagation computes the gradient of the loss with respect to every weight in the network by applying the chain rule from output to input. The forward pass stores intermediate values at each layer. The backward pass multiplies local derivatives in reverse order — from loss back to each weight. In code: loss.backward() in PyTorch. The key insight is computational efficiency: intermediate gradients are computed once and reused, avoiding redundant calculation. - What is the vanishing gradient problem and how is it solved?
With sigmoid activation, backpropagation multiplies by the sigmoid derivative (≤ 0.25) at each layer. After 10 layers: 0.25¹⁰ ≈ 10⁻⁶ — early layers receive near-zero gradients and stop learning. ReLU solves this: derivative is 1 for active neurons, so gradients flow through without shrinkage. This single change enabled networks 10–150 layers deep, directly enabling modern deep learning. - Why does adding hidden layers help — what does depth actually do?
Each layer learns a new representation of the input space — a transformed coordinate system where the classification problem becomes simpler. Early layers learn low-level features (edges in images, letter patterns in text). Later layers combine these into high-level abstractions (faces, words, concepts). The Universal Approximation Theorem guarantees that sufficient depth and width can approximate any continuous function. In practice, depth is more parameter-efficient than width — a deep network can express functions that would require exponentially more neurons to express in a single wide layer.
No attachments for this topic yet.