Research Scientist
Deep Learning Theory
Research Scientist interviews probe deep theoretical understanding โ not just what works but why. You must derive backpropagation from first principles, explain initialization theory, understand the failure modes of normalization schemes, and engage with open research questions like double descent and the neural tangent kernel.
Core Theory
- 1Backpropagation derivation: the chain rule applied to a computational graph. For a composed function f(g(x)), df/dx = (df/dg)(dg/dx). In a neural network, we compute gradients by propagating the upstream gradient backward through each operation. For linear layer y=Wx+b with upstream gradient ฮด=dL/dy: dL/dW = ฮด^T x (outer product), dL/dx = W^T ฮด, dL/db = ฮด. The backward pass is O(same) as the forward pass in FLOPs.
- 2Initialization theory: the goal is to start with weights such that the signal neither vanishes nor explodes through many layers. Xavier/Glorot initialization: Var(W) = 2/(fan_in + fan_out). Derived by requiring Var(output) = Var(input) for a linear activation. He initialization: Var(W) = 2/fan_in. Derived for ReLU (which zeros out half the activations on average, halving the effective fan_in). Bad initialization โ vanishing gradients (weights too small) or exploding activations (weights too large) โ slow or failed training.
- 3Universal Approximation Theorem: a single hidden layer neural network with sufficient width can approximate any continuous function on a compact set to arbitrary accuracy (Cybenko 1989 for sigmoid, Hornik 1991 for general activations). What it does NOT say: (1) how wide you need (can be exponential), (2) that you can find these weights by gradient descent, (3) that it will generalize. The theorem is about expressiveness, not learnability or generalization.
- 4Vanishing/exploding gradients in deep networks: in a deep network, gradients are products of many Jacobians. If ||W_i|| < 1 for each layer, gradients vanish exponentially with depth. If ||W_i|| > 1, they explode. ReLU helps (gradient is 1 or 0, not < 1). Residual connections (ResNets) fix this by providing a gradient highway: dL/dx = dL/d(x + F(x)) = dL/dx + dL/dF(x). The identity path ensures gradient magnitude never drops below 1.
- 5BatchNorm derivation and training vs. inference: forward: BN(x) = (x - ฮผ_B) / โ(ฯยฒ_B + ฮต) ร ฮณ + ฮฒ, where ฮผ_B and ฯยฒ_B are computed over the batch. Backward: the gradient must account for the fact that ฮผ_B and ฯยฒ_B depend on x. During inference: use exponential moving averages of ฮผ and ฯยฒ from training (the 'running statistics'). This different behavior between train and eval is the source of many production bugs.
- 6Double descent phenomenon: classical bias-variance predicts a U-shaped test error as model size increases (too simple = underfitting, too complex = overfitting). Double descent shows a second descent for very large models: test error peaks at the interpolation threshold (where the model exactly fits training data) then decreases again in the overparameterized regime. This explains why making neural networks larger often improves generalization even without regularization.
- 7Neural Tangent Kernel (NTK): in the infinite-width limit, a neural network trained with gradient descent behaves like kernel regression with a fixed kernel (the NTK). This provides theoretical understanding of why neural networks generalize and why they can be trained with simple gradient descent. Practical implication: wide networks are more linear and easier to train, narrow networks are more nonlinear but harder to optimize. The NTK theory is an active research area explaining neural network behavior.
- 8Pre-norm vs. post-norm in transformers: pre-norm (LN before attention/FFN, residual around the whole block) vs. post-norm (LN after the residual addition). Empirically, pre-norm trains more stably for deep transformers because gradients flow through the residual connection without passing through the LN operation, which can distort gradient magnitudes. GPT-2, GPT-3, LLaMA all use pre-norm. The original Transformer paper used post-norm.
Key Formulas
| 1 | import numpy as np |
| 2 | |
| 3 | np.random.seed(42) |
| 4 | |
| 5 | class MLP: |
| 6 | """ |
| 7 | 3-layer MLP with backprop implemented from scratch. |
| 8 | Architecture: Input โ Linear โ ReLU โ Linear โ ReLU โ Linear โ Softmax |
| 9 | Loss: Cross-entropy |
| 10 | """ |
| 11 | def __init__(self, d_in: int, d_hid: int, d_out: int): |
| 12 | # He initialization for ReLU layers |
| 13 | self.W1 = np.random.randn(d_in, d_hid) * np.sqrt(2.0 / d_in) |
| 14 | self.W2 = np.random.randn(d_hid, d_hid) * np.sqrt(2.0 / d_hid) |
| 15 | self.W3 = np.random.randn(d_hid, d_out) * np.sqrt(2.0 / d_hid) |
| 16 | self.b1 = np.zeros(d_hid) |
| 17 | self.b2 = np.zeros(d_hid) |
| 18 | self.b3 = np.zeros(d_out) |
| 19 | |
| 20 | def relu(self, x): return np.maximum(0, x) |
| 21 | def d_relu(self, x): return (x > 0).astype(float) # ReLU gradient |
| 22 | |
| 23 | def softmax(self, x): |
| 24 | e = np.exp(x - x.max(axis=1, keepdims=True)) # numerically stable |
| 25 | return e / e.sum(axis=1, keepdims=True) |
| 26 | |
| 27 | def forward(self, X): |
| 28 | # Layer 1 |
| 29 | self.z1 = X @ self.W1 + self.b1 |
| 30 | self.a1 = self.relu(self.z1) |
| 31 | # Layer 2 |
| 32 | self.z2 = self.a1 @ self.W2 + self.b2 |
| 33 | self.a2 = self.relu(self.z2) |
| 34 | # Output layer |
| 35 | self.z3 = self.a2 @ self.W3 + self.b3 |
| 36 | self.probs = self.softmax(self.z3) |
| 37 | return self.probs |
| 38 | |
| 39 | def loss(self, probs, y): |
| 40 | # Cross-entropy: -1/n * ฮฃ log p(correct class) |
| 41 | n = y.shape[0] |
| 42 | return -np.log(probs[np.arange(n), y] + 1e-8).mean() |
| 43 | |
| 44 | def backward(self, X, y): |
| 45 | n = X.shape[0] |
| 46 | |
| 47 | # --- Output layer gradient --- |
| 48 | # Softmax + cross-entropy gradient: dL/dz3 = (probs - one_hot(y)) / n |
| 49 | delta3 = self.probs.copy() |
| 50 | delta3[np.arange(n), y] -= 1 |
| 51 | delta3 /= n # (n, d_out) |
| 52 | |
| 53 | # --- Layer 3 weights --- |
| 54 | dW3 = self.a2.T @ delta3 # (d_hid, d_out) |
| 55 | db3 = delta3.sum(axis=0) |
| 56 | |
| 57 | # --- Propagate through Layer 2 --- |
| 58 | delta2 = delta3 @ self.W3.T * self.d_relu(self.z2) # (n, d_hid) |
| 59 | dW2 = self.a1.T @ delta2 |
| 60 | db2 = delta2.sum(axis=0) |
| 61 | |
| 62 | # --- Propagate through Layer 1 --- |
| 63 | delta1 = delta2 @ self.W2.T * self.d_relu(self.z1) # (n, d_hid) |
| 64 | dW1 = X.T @ delta1 |
| 65 | db1 = delta1.sum(axis=0) |
| 66 | |
| 67 | return {"dW1": dW1, "dW2": dW2, "dW3": dW3, |
| 68 | "db1": db1, "db2": db2, "db3": db3} |
| 69 | |
| 70 | # โโโ Training โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| 71 | X = np.random.randn(200, 10) |
| 72 | y = np.random.randint(0, 5, 200) |
| 73 | |
| 74 | model = MLP(d_in=10, d_hid=64, d_out=5) |
| 75 | lr = 0.01 |
| 76 | |
| 77 | for epoch in range(500): |
| 78 | probs = model.forward(X) |
| 79 | loss = model.loss(probs, y) |
| 80 | grads = model.backward(X, y) |
| 81 | |
| 82 | # SGD update |
| 83 | for param, grad in [('W1', 'dW1'), ('W2', 'dW2'), ('W3', 'dW3'), |
| 84 | ('b1', 'db1'), ('b2', 'db2'), ('b3', 'db3')]: |
| 85 | setattr(model, param, getattr(model, param) - lr * grads[grad]) |
| 86 | |
| 87 | if (epoch + 1) % 100 == 0: |
| 88 | acc = (probs.argmax(axis=1) == y).mean() |
| 89 | print(f"Epoch {epoch+1:4d}: loss={loss:.4f}, acc={acc:.3f}") |
Complete backpropagation implementation from scratch. Key details: (1) He initialization (โ(2/fan_in)) for ReLU layers โ ensures variance is preserved through many layers; (2) numerically stable softmax (subtract max before exp to prevent overflow); (3) the beautiful simplification: cross-entropy + softmax gradient = (probs - one_hot(y))/n; (4) ReLU backward pass: multiply upstream gradient by indicator (x > 0); (5) chain rule applied sequentially from output layer to input.
Worked Interview Problems
3 problemsProblem
Given logits z โ R^C and one-hot target y, derive โL/โz where L = -log(softmax(z)_y). Show that it simplifies to ลท - y.
Solution
Softmax: ลท_c = exp(z_c) / S where S = ฮฃ_j exp(z_j). Cross-entropy: L = -log ลท_y = -(z_y - log S) where y is the correct class.
โL/โz_c for the correct class: โL/โz_y = -1 + exp(z_y)/S = ลท_y - 1.
โL/โz_c for other classes (c โ y): โL/โz_c = -(โlog S/โz_c) = -(exp(z_c)/S) = -ลท_c. But L = -log ลท_y so โL/โz_c = ลท_c.
Combining: โL/โz_c = ลท_c - y_c where y_c = 1 if c = correct class, else 0.
Vectorized: โL/โz = ลท - y. This elegant form means: for the correct class, gradient = ลท_y - 1 (negative, pulls logit down if ลท_y < 1). For wrong classes, gradient = ลท_c (positive, pushes logit down).
Answer
. The gradient is simply the difference between predicted probabilities and the one-hot target. This beautiful simplification is why softmax + cross-entropy is the universal classification loss.
Common Mistakes
Saying 'the gradient of ReLU is 0 everywhere' โ it's 0 for negative inputs, 1 for positive inputs. The derivative at 0 is undefined (usually set to 0 in implementations). The 'dying ReLU' problem is when neurons get stuck in the all-negative regime permanently, receiving only 0 gradients.
Confusing Xavier and He initialization. Xavier: for tanh/sigmoid activations, Var = 2/(fan_in + fan_out). He: for ReLU, Var = 2/fan_in (larger because ReLU zeros half the activations on average, reducing the effective signal by half, so you need 2ร larger weights to compensate).
Not understanding BatchNorm's train/eval difference. During training, BN uses batch statistics (ฮผ_B, ฯ_B). During eval, it uses running averages accumulated during training. Setting model.eval() in PyTorch is not optional โ it switches BN to eval mode. Forgetting this is a common production bug.
Claiming Universal Approximation Theorem means neural networks are guaranteed to generalize. The UAT only says a solution exists that fits the data โ it says nothing about whether gradient descent finds it, how wide the network needs to be, or whether it will generalize to new data.
Not knowing the cross-entropy + softmax gradient simplification: โL/โz = ลท - y. If a research interviewer asks you to implement the backward pass for a classification network, you must know this โ computing the full Jacobian of softmax separately is unnecessary and inefficient.