Stat 577 — Statistical Learning Theory
Spring 2026
Over Weeks 9 and 11 we built the full neural network foundation:
Today we add three innovations that transformed neural networks from a research curiosity into a practical tool: a better optimizer, a normalization trick, and a regularization method that sounds crazy but works.
Two fundamental limitations of vanilla SGD:
1. Oscillation in high-curvature directions. When the loss surface is much steeper in one direction than another, SGD oscillates back and forth across the steep direction while making slow progress along the shallow direction.
2. One learning rate for all parameters. Some parameters need large steps (sparse gradients, infrequent features), others need small steps (dense gradients, frequent features). Vanilla SGD uses the same \(\eta\) for everything.
Fix for oscillation. Accumulate a running average of past gradients:
\[\mathbf{m}^{(t)} = \gamma \mathbf{m}^{(t-1)} + \eta \nabla R(\boldsymbol{\theta}^{(t)})\]
\[\boldsymbol{\theta}^{(t+1)} = \boldsymbol{\theta}^{(t)} - \mathbf{m}^{(t)}\]
Typical \(\gamma = 0.9\).
Intuition: A ball rolling downhill accumulates speed in consistent directions and dampens oscillations. Gradients that point the same way reinforce each other; gradients that flip sign cancel out.
The modern default optimizer. Maintains per-parameter learning rates by tracking two quantities:
First moment (mean of gradients — like momentum):
\[m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t\]
Second moment (mean of squared gradients):
\[v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2\]
Bias correction (since \(m_0 = v_0 = 0\), early estimates are biased toward zero):
\[\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \qquad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}\]
Update:
\[\theta \leftarrow \theta - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}\]
Defaults: \(\beta_1 = 0.9\), \(\beta_2 = 0.999\), \(\epsilon = 10^{-8}\).
Parameters with consistently large gradients get smaller effective learning rates — the denominator \(\sqrt{\hat{v}_t}\) is large.
Parameters with small, noisy gradients get larger effective learning rates — the denominator is small.
Adam adapts automatically. No manual tuning of per-layer learning rates.
Tip
Practical advice: Adam is the default. Start with lr=1e-3. SGD+momentum can sometimes find better solutions (especially for vision tasks) but requires more tuning. When in doubt, use Adam.
The learning rate \(\eta\) does not have to stay constant during training.
These are refinements, not essentials. Adam with a fixed lr=1e-3 is a perfectly reasonable starting point.
During training, each layer’s input distribution changes because the layers below are also updating their weights.
Layer 3 is trying to learn a mapping from its inputs (outputs of layer 2) to its targets. But those inputs keep shifting as layer 2’s weights change.
Like trying to hit a moving target.
Even if layer 3 makes progress on one minibatch, the distribution it learned from may be different on the next minibatch — not because the data changed, but because the preceding layers changed.
For each minibatch, normalize the pre-activations to mean 0 and variance 1:
\[\hat{z}_j = \frac{z_j - \mu_{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}^2 + \epsilon}}\]
where \(\mu_{\mathcal{B}}\) and \(\sigma_{\mathcal{B}}^2\) are the mean and variance of \(z_j\) computed over the current minibatch.
Then add learnable parameters \(\gamma\) (scale) and \(\beta\) (shift):
\[\tilde{z}_j = \gamma \hat{z}_j + \beta\]
Why add \(\gamma\) and \(\beta\) back? Because forcing mean 0 / variance 1 might be too restrictive — the network should be able to choose to undo the normalization if that is what is optimal. The identity transformation (\(\gamma = \sigma_{\mathcal{B}}\), \(\beta = \mu_{\mathcal{B}}\)) is always recoverable.
Important
The honest answer: The original paper (Ioffe & Szegedy, 2015) argued BatchNorm works by reducing internal covariate shift. Later work (Santurkar et al., 2018) showed this explanation is likely wrong — BatchNorm actually works by smoothing the loss landscape, making gradients more predictable and allowing larger learning rates.
The practical effects are clear even if the theory is unsettled:
Where to place it? Before activation (original paper) or after? Both work. No definitive answer.
During inference: No minibatch at test time. Use running averages of \(\mu\) and \(\sigma^2\) computed during training.
In PyTorch: nn.BatchNorm1d(num_features) — one line.
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
# Generate data
X, y = make_moons(n_samples=1000, noise=0.25, random_state=42)
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
# Model WITHOUT BatchNorm
class PlainMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 64), nn.ReLU(),
nn.Linear(64, 64), nn.ReLU(),
nn.Linear(64, 64), nn.ReLU(),
nn.Linear(64, 1), nn.Sigmoid()
)
def forward(self, x):
return self.net(x)
# Model WITH BatchNorm
class BNormMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 64), nn.BatchNorm1d(64), nn.ReLU(),
nn.Linear(64, 64), nn.BatchNorm1d(64), nn.ReLU(),
nn.Linear(64, 64), nn.BatchNorm1d(64), nn.ReLU(),
nn.Linear(64, 1), nn.Sigmoid()
)
def forward(self, x):
return self.net(x)
# Train both and record loss curves
def train(model, X_t, y_t, epochs=200, lr=0.01):
opt = torch.optim.Adam(model.parameters(), lr=lr)
loss_fn = nn.BCELoss()
losses = []
for _ in range(epochs):
pred = model(X_t)
loss = loss_fn(pred, y_t)
opt.zero_grad(); loss.backward(); opt.step()
losses.append(loss.item())
return losses
torch.manual_seed(42)
plain = PlainMLP(); losses_plain = train(plain, X_t, y_t)
torch.manual_seed(42)
bnorm = BNormMLP(); losses_bnorm = train(bnorm, X_t, y_t)
# Plot
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(losses_plain, color="#D62828", label="Without BatchNorm", linewidth=2)
ax.plot(losses_bnorm, color="#06A77D", label="With BatchNorm", linewidth=2)
ax.set_xlabel("Epoch"); ax.set_ylabel("BCE Loss")
ax.set_title("Effect of Batch Normalization on Training")
ax.legend(); ax.set_ylim(0, 0.8)
plt.tight_layout()
plt.savefig("../figures/week12/03_batchnorm_effect.png", dpi=200, bbox_inches="tight")
plt.show()Neurons can become overly dependent on each other — “neuron A only works when neuron B is active.”
This creates fragile representations. The network learns a solution that relies on specific combinations of neurons rather than robust individual features.
Dropout breaks co-adaptation by force.
During training: Randomly set each neuron’s output to zero with probability \(p\).
During testing: Use all neurons. But their outputs are now \(p\) times too large (since during training only a fraction \(1 - p\) were active on average).
Two equivalent fixes:
Important
Key insight (course connection): A network with \(n\) neurons and dropout is implicitly training \(2^n\) different subnetworks (one for each dropout pattern) that share weights. At test time, you average over all subnetworks.
This is bagging (Week 7) applied at the neuron level — same variance-reduction principle, different mechanism.
Tip
The Modern Training Recipe
lr=1e-3 (default starting point)These five choices, combined with the foundations from Weeks 9 & 11, are enough to train competitive deep networks on most problems.
Preview — Obrey will go much deeper next week.
Fully connected networks treat every input pixel as independent. For a \(256 \times 256\) image, that is 65,536 input units, each connected to every neuron in the first hidden layer.
This ignores the fact that images have spatial structure. CNNs exploit two properties:
A small filter (e.g., \(3 \times 3\)) slides across the image. At each position, it computes a dot product between the filter weights and the local image patch.
Same weights everywhere — weight sharing — so a pattern detector learned in one location works in all locations.
Dramatically reduces parameters: a \(3 \times 3\) filter has 9 parameters regardless of image size.
\[\text{Input} \to \underbrace{\text{Conv} \to \text{ReLU} \to \text{Pool}}_{\text{repeat}} \to \text{Flatten} \to \text{FC} \to \text{Output}\]
The result: early layers detect edges, middle layers textures and parts, late layers objects and concepts — the hierarchical feature learning we discussed in Week 11, now with architectural structure.
In 2012, AlexNet (Krizhevsky, Sutskever, Hinton) won the ImageNet Large Scale Visual Recognition Challenge. The error rate dropped from 26% to 16% — more than 10 percentage points in a single year.
This was the moment the deep learning revolution started. Every subsequent year, the winning architecture was a deeper CNN.
Note
Obrey presents the full CNN story — LeNet to AlexNet — on Monday of Week 13. Wooyoung follows on Wednesday with residual networks.
CNNs assume fixed-size input with spatial structure. But many problems involve sequences of variable length where order matters:
For all of these, the position of each element in the sequence carries information. We need architectures that respect sequential structure.
Process one element at a time, maintaining a hidden state \(\mathbf{h}_t\) that summarizes everything seen so far:
\[\mathbf{h}_t = \sigma(\mathbf{W}_h \mathbf{h}_{t-1} + \mathbf{W}_x \mathbf{x}_t + \mathbf{b})\]
At each time step \(t\): combine the previous hidden state \(\mathbf{h}_{t-1}\) (memory) with the current input \(\mathbf{x}_t\) (new information).
The same weights \(\mathbf{W}_h\) and \(\mathbf{W}_x\) are used at every time step — weight sharing across time, just as CNNs share weights across space.
Note
Jackie presents the full RNN/LSTM story — including Hochreiter & Schmidhuber’s solution to the vanishing gradient problem in sequences — on Monday of Week 14.
In the standard encoder-decoder architecture, the encoder processes the entire input sequence and compresses it into a single fixed-length vector \(\mathbf{h}_T\):
\[\underbrace{x_1, x_2, \ldots, x_T}_{\text{input sequence}} \;\xrightarrow{\text{encoder}}\; \mathbf{h}_T \;\xrightarrow{\text{decoder}}\; \underbrace{y_1, y_2, \ldots, y_{T'}}_{\text{output sequence}}\]
The decoder must reconstruct the entire output from this one vector.
Imagine summarizing a 500-word paragraph into a single number. That is what the encoder-decoder bottleneck asks the network to do.
For short sequences, this works. For long sequences, too much information is lost. This is the problem attention solves.
Bahdanau, Cho & Bengio (2015). Instead of compressing everything into one vector, let the decoder attend to all encoder hidden states at every decoding step.
When translating “le chat est sur le tapis” and generating the word “cat,” the decoder should focus on “chat.”
When generating “mat,” it should focus on “tapis.”
The decoder dynamically decides where to look in the input, depending on what it is currently generating.
Three steps:
1. Score: How relevant is each encoder state \(\mathbf{h}_j\) to the current decoder state \(\mathbf{s}_t\)?
\[e_{tj} = \text{score}(\mathbf{s}_t, \mathbf{h}_j)\]
2. Normalize: Softmax converts scores to weights that sum to 1:
\[\alpha_{tj} = \text{softmax}_j(e_{tj}) = \frac{\exp(e_{tj})}{\sum_{j'} \exp(e_{tj'})}\]
3. Aggregate: Context vector is a weighted average of all encoder states:
\[\mathbf{c}_t = \sum_j \alpha_{tj} \mathbf{h}_j\]
A soft lookup — instead of selecting one encoder state (hard attention), we blend all of them with learned, context-dependent weights.
Vaswani et al. (2017) generalized the attention mechanism into three roles:
Tip
Analogy: Think of a library. The query is your search term. The keys are the book titles. The values are the book contents. You match your search term against titles (query-key), get relevance scores, and blend the contents (values) accordingly.
On Friday, we formalize all of this — the exact equation, why it is scaled, and what happens when Q, K, and V all come from the same sequence (self-attention).
In Week 15, we build the full transformer architecture.
Wednesday we motivated attention — the encoder-decoder bottleneck problem and the QKV abstraction:
Today we formalize the exact equation, explain the \(\sqrt{d_k}\) scaling, and see what happens when Q, K, and V all come from the same sequence.
The core computation of modern attention (Vaswani et al., 2017):
\[\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]
Let’s unpack this piece by piece.
Step 1: \(QK^T\) — a matrix of dot products between every query and every key.
Entry \((i, j) = \mathbf{q}_i^T \mathbf{k}_j\) = how much query \(i\) matches key \(j\).
Step 2: \(\div \sqrt{d_k}\) — scaling factor. Why? (next slide)
Step 3: softmax — normalize each row to sum to 1. Each row corresponds to one query — it distributes its attention across all keys, so each row sums to 1.
Step 4: \(\times V\) — weighted combination of values. Each query’s output is a weighted average of all value vectors, where the weights come from step 3.
The entire operation: matrix multiply \(\to\) scale \(\to\) normalize \(\to\) aggregate.
Important
Connection to initialization (Week 11): If query and key vectors have entries with variance 1, their dot product has variance \(d_k\) (sum of \(d_k\) products of independent terms — the same calculation as our Xavier initialization argument!).
For large \(d_k\), dot products become large, pushing softmax into saturation where gradients vanish.
Dividing by \(\sqrt{d_k}\) restores unit variance. Same principle as He/Xavier initialization — control the variance to keep gradients healthy.
\(d_k = 4\), three positions. Small concrete numbers:
\[Q = \begin{bmatrix} 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 1 & 1 & 0 & 0 \end{bmatrix}, \quad K = \begin{bmatrix} 1 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 1 & 0 \end{bmatrix}, \quad V = \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 0.5 & 0.5 \end{bmatrix}\]
Step 1: \(QK^T = \begin{bmatrix} 1 & 1 & 2 \\ 1 & 1 & 0 \\ 2 & 0 & 1 \end{bmatrix}\)
Step 2: Scale by \(\sqrt{4} = 2\): \(\frac{QK^T}{\sqrt{d_k}} = \begin{bmatrix} 0.5 & 0.5 & 1.0 \\ 0.5 & 0.5 & 0.0 \\ 1.0 & 0.0 & 0.5 \end{bmatrix}\)
Step 3: Softmax each row: \(\approx \begin{bmatrix} 0.27 & 0.27 & 0.45 \\ 0.38 & 0.38 & 0.23 \\ 0.51 & 0.19 & 0.31 \end{bmatrix}\)
Step 4: Multiply by \(V\):
\[\text{Output} = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right) V \approx \begin{bmatrix} 0.50 & 0.50 \\ 0.50 & 0.50 \\ 0.66 & 0.34 \end{bmatrix}\]
Each row is a weighted average of the value vectors.
The point: this is matrix multiplication \(\to\) normalize \(\to\) weighted average. Nothing mysterious.
In cross-attention (Bahdanau), queries come from the decoder, keys and values from the encoder.
In self-attention, Q, K, V are all derived from the same input sequence \(\mathbf{X}\):
\[Q = \mathbf{X}W^Q, \qquad K = \mathbf{X}W^K, \qquad V = \mathbf{X}W^V\]
where \(W^Q, W^K, W^V\) are learned projection matrices.
Every position attends to every other position in the same sequence. Each token asks: “what other tokens in this sentence are relevant to me?”
Consider the sentence: “The animal didn’t cross the street because it was too tired.”
When processing “it,” self-attention attends strongly to “animal” — resolving the pronoun.
This requires looking far back in the sequence — something RNNs struggle with due to vanishing gradients.
Self-attention gives every position a direct connection to every other position. No bottleneck, no sequential processing.
| Property | Fully Connected | CNN | RNN | Self-Attention |
|---|---|---|---|---|
| Connectivity | all-to-all | local | sequential | all-to-all |
| Weight sharing | none | spatial | temporal | via Q, K, V projections |
| Variable length | no | with tricks | yes | yes |
| Long-range deps | limited by depth | limited by kernel | vanishing gradients | direct |
| Parallelizable | yes | yes | no | yes |
Self-attention computes a score for every pair of positions: \(O(n^2 d)\) time, \(O(n^2)\) memory.
| Sequence length \(n\) | Number of pairwise scores |
|---|---|
| 100 | 10,000 |
| 1,000 | 1,000,000 |
| 10,000 | 100,000,000 |
| 100,000 | 10,000,000,000 |
This quadratic cost is the main limitation of self-attention — and an active research area (linear attention, sparse attention, FlashAttention).
import numpy as np
def scaled_dot_product_attention(Q, K, V):
"""Scaled dot-product attention (Vaswani et al., 2017)."""
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k) # (n_queries, n_keys)
scores = scores - scores.max(axis=-1, keepdims=True) # numerical stability
weights = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True) # softmax
return weights @ V, weights # (n_queries, d_v), (n_queries, n_keys)
# Small example: 4 tokens, d_k = d_v = 3
np.random.seed(42)
X = np.random.randn(4, 3) # 4 tokens, embedding dim 3
W_Q = np.random.randn(3, 3) * 0.5
W_K = np.random.randn(3, 3) * 0.5
W_V = np.random.randn(3, 3) * 0.5
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
output, attn_weights = scaled_dot_product_attention(Q, K, V)
# Visualize attention weights
fig, ax = plt.subplots(figsize=(6, 5))
tokens = ["The", "cat", "sat", "down"]
im = ax.imshow(attn_weights, cmap="Blues", vmin=0, vmax=1)
ax.set_xticks(range(4)); ax.set_xticklabels(tokens)
ax.set_yticks(range(4)); ax.set_yticklabels(tokens)
ax.set_xlabel("Key (attending to)"); ax.set_ylabel("Query (attending from)")
ax.set_title("Self-Attention Weights")
for i in range(4):
for j in range(4):
ax.text(j, i, f"{attn_weights[i,j]:.2f}", ha="center", va="center",
color="white" if attn_weights[i,j] > 0.5 else "black", fontsize=12)
plt.colorbar(im, ax=ax, label="Attention Weight")
plt.tight_layout()
plt.savefig("../figures/week12/09_attention_demo.png", dpi=200, bbox_inches="tight")
plt.show()Note: These are random, untrained weights — the attention pattern is meaningless. In a trained model, the weights would reflect learned relationships between tokens.
A single attention head learns one notion of “relevance.” But relevance is multifaceted:
Multi-head attention runs \(h\) attention operations in parallel, each with its own learned projections \(W^Q_i, W^K_i, W^V_i\), then concatenates:
\[\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)\, W^O\]
Think of multiple “experts,” each attending to different aspects of the input.
We will build this out fully in Week 15.
Important
Week 12 Takeaways:
Week 11 Connections
The \(\sqrt{d_k}\) scaling is the same variance argument as Xavier/He initialization
BatchNorm reduces sensitivity to initialization — same problem, different solution
Dropout’s ensemble interpretation connects to bagging (Weeks 7–8)
What’s Coming
Weeks 13–14: Your classmates go deep on CNNs (Obrey), ResNets (Wooyoung), double descent (Alex), RNNs/LSTMs (Jackie), transfer learning (Garrett), and scaling laws (Ephraim)
Week 15: We build the full transformer architecture