Neural Networks and Backpropagation
A neural network is a composition of simple differentiable functions whose parameters are trained by gradient descent. Interviews ask you to explain the forward pass, derive backpropagation for a small network, choose activations and losses, and reason about vanishing gradients and architecture choices. The most convincing answer is one you can back with a short from-scratch implementation.
1. The building block
A layer computes : a linear map followed by a nonlinearity . Stacking layers gives a network:
Without nonlinearities, any stack of linear layers collapses into one linear map (), so depth would add nothing. The nonlinearity is what lets the network represent curved decision boundaries.
<!--fig:mlp-->The universal approximation theorem says a network with one sufficiently wide hidden layer can approximate any continuous function on a compact set. It is an existence result: it says nothing about how to find the weights or how wide is enough. In practice, depth is far more parameter-efficient than width.
2. Activation functions
| Activation | Formula | Notes |
|---|---|---|
| Sigmoid | Output in (0, 1); saturates, gradient up to 0.25; used for binary outputs, rarely in hidden layers | |
| Tanh | Zero-centred; still saturates | |
| ReLU | Cheap, no saturation for , sparse; units can "die" if stuck negative | |
| Leaky ReLU / PReLU | small slope for | Avoids dead units |
| GELU / SiLU (Swish) | smooth gated variants | Standard in transformers |
| Softmax | Output layer for multiclass |
The output activation and loss pair:
| Task | Output layer | Loss |
|---|---|---|
| Regression | linear | MSE (or Huber) |
| Binary classification | sigmoid | binary cross-entropy |
| Multiclass | softmax | categorical cross-entropy |
| Multilabel | independent sigmoids | binary cross-entropy per label |
3. Backpropagation
Training needs for every weight. Backpropagation is the chain rule applied efficiently on the computation graph: do a forward pass storing intermediate values, then a backward pass that reuses them, computing every gradient in time proportional to the forward pass.
For a two-layer network with , , , (regression, squared error ):
Each layer's error signal is the next layer's signal sent backwards through the transposed weights and multiplied by the local derivative. The cost of backward is a small constant multiple of forward.
Here is a complete network that learns XOR, which a single linear layer cannot, with the gradients verified numerically:
import numpy as np
rng = np.random.default_rng(0)
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([[0], [1], [1], [0]], dtype=float)
def init(h=8):
return {"W1": rng.normal(0, 1, (2, h)), "b1": np.zeros(h),
"W2": rng.normal(0, 1, (h, 1)), "b2": np.zeros(1)}
def forward(p, X):
z1 = X @ p["W1"] + p["b1"]; a1 = np.tanh(z1)
z2 = a1 @ p["W2"] + p["b2"]; out = 1 / (1 + np.exp(-z2))
return z1, a1, z2, out
def loss_fn(p):
out = forward(p, X)[3]
return -np.mean(y * np.log(out) + (1 - y) * np.log(1 - out))
def backward(p):
z1, a1, z2, out = forward(p, X)
n = len(X)
d2 = (out - y) / n # sigmoid + cross-entropy: the gradient simplifies to (p - y)
g = {"W2": a1.T @ d2, "b2": d2.sum(0)}
d1 = (d2 @ p["W2"].T) * (1 - a1 ** 2) # tanh'(z) = 1 - tanh(z)^2
g["W1"] = X.T @ d1; g["b1"] = d1.sum(0)
return g
# gradient check: compare against finite differences
p = init()
g = backward(p)
eps = 1e-6
for name in ("W1", "b1", "W2", "b2"):
flat = p[name].reshape(-1)
for i in range(min(3, flat.size)):
old = flat[i]
flat[i] = old + eps; up = loss_fn(p)
flat[i] = old - eps; down = loss_fn(p)
flat[i] = old
num = (up - down) / (2 * eps)
assert abs(num - g[name].reshape(-1)[i]) < 1e-6, (name, i)
for _ in range(5000):
g = backward(p)
for k in p:
p[k] -= 0.5 * g[k]
pred = forward(p, X)[3]
assert ((pred > 0.5) == y).all() # XOR solved
assert loss_fn(p) < 0.05
Two habits worth showing in an interview: the gradient check (numerical differentiation to validate your derivation) and the sigmoid-plus-cross-entropy simplification to .
4. Vanishing and exploding gradients
Backprop multiplies many Jacobians together. If their typical size is below 1, the gradient shrinks exponentially with depth (vanishing); above 1, it grows (exploding). Sigmoid and tanh saturate and have derivatives under 1, so deep stacks of them barely learn in the early layers.
Remedies:
- ReLU-family activations, whose derivative is 1 on the active side.
- Careful initialisation (He for ReLU, Xavier for tanh).
- Normalisation layers (batch or layer norm).
- Residual connections: gives gradients a direct path, which made very deep networks trainable.
- Gradient clipping against explosions.
- Gated architectures (LSTM, GRU) for recurrent nets.
import numpy as np
# gradient magnitude through 30 sigmoid layers: each local derivative is at most 0.25
max_local = 0.25
assert max_local ** 30 < 1e-17 # vanishes long before reaching the first layer
# a residual path keeps a gradient of exactly 1 along the skip connection
depth, grad = 30, 1.0
for _ in range(depth):
grad *= 1.0 + 0.0 # d(x + F(x))/dx = 1 + F'(x); the 1 is always there
assert grad == 1.0
5. Architectures at a glance
| Architecture | Inductive bias | Typical use |
|---|---|---|
| MLP | none beyond smoothness | tabular features, small heads |
| CNN | locality, translation equivariance, weight sharing | images, audio, local patterns |
| RNN / LSTM / GRU | sequential state | older sequence models, streaming |
| Transformer | attention over all positions | text, code, vision, multimodal |
| Autoencoder / VAE | compression, latent structure | representation learning, generation |
| GAN / diffusion | learn a data distribution | image and media generation |
| Graph neural network | message passing on a graph | molecules, social and transport networks |
Convolution in two sentences
A convolutional layer slides a small learned filter over the input, producing a feature map. Because the same filter is applied everywhere, parameters are shared and the layer is translation equivariant; pooling or striding then adds approximate invariance and reduces size. The output size for a square input of size , kernel , padding , stride is .
def conv_out(n, k, p=0, s=1):
return (n + 2 * p - k) // s + 1
assert conv_out(32, 3, p=1, s=1) == 32 # "same" padding with a 3x3 kernel
assert conv_out(32, 3, p=0, s=1) == 30
assert conv_out(224, 7, p=3, s=2) == 112 # a typical first layer of a ResNet
Parameter count
A dense layer with inputs and outputs has parameters. A convolution with input channels, filters and a kernel has parameters, independent of image size, which is the efficiency win.
6. Training recipe
- Normalise inputs. Shuffle data each epoch.
- Start small: overfit a tiny batch to catch bugs.
- Pick Adam or AdamW with a sensible learning rate (around to ), plus warmup and decay.
- Monitor training and validation loss, and the metric you care about.
- Regularise as needed: weight decay, dropout, augmentation, early stopping.
- Tune learning rate first, then batch size, width and depth.
- Check the data: label noise and leakage cause more failures than architecture.
7. When not to use a neural network
Small tabular datasets, a need for strict interpretability, tight latency or memory budgets, or little data and no pretrained model to fine-tune. Gradient-boosted trees or linear models are usually faster to build and as accurate.
8. Common mistakes
- Forgetting nonlinearities between layers.
- Wrong loss and output pairing, such as softmax followed by a loss that applies softmax again.
- Not shuffling or leaking validation data through augmentation or preprocessing.
- All-zero initialisation, which keeps every hidden unit identical (symmetry is never broken).
- Evaluating in training mode with dropout or batch norm active.
- Inspecting only the loss, not examples, calibration or per-slice metrics.
- Assuming deeper is always better. Without residuals and normalisation it can train worse.
9. Practice questions
- Why are nonlinearities needed? What does the universal approximation theorem say and not say?
- Derive backpropagation for a one-hidden-layer network.
- Why does softmax with cross-entropy give the gradient ?
- Explain vanishing gradients and three ways to mitigate them.
- Why does initialising all weights to zero fail?
- Compare ReLU, sigmoid and GELU.
- How many parameters does a 3x3 convolution with 64 input and 128 output channels have?
- Your network's training loss is flat from the start. How do you debug it?