Flow-Based Generative Models · UFRJ · 2026.2
Same code, same data; only the seed changed. Today the variance is merely annoying. In a badly tuned run it decides whether training converges at all.
Question of the day: which settings (optimizer, schedule, initialization, normalization) make training reliably work, and what are our defaults?
Full-batch GD on the empirical risk \(\widehat{R}_n(\theta)\) costs one pass over the dataset per step. Instead: sample a minibatch \(B \subset \{1,\dots,n\}\), step on its gradient.
The empirical measure of the minibatch, \(\widehat{q}_B = \frac{1}{|B|}\sum_{i\in B}\delta_{(x_i, y_i)}\), puts mass \(1/|B|\) on each sampled pair, so an expectation under it is simply the batch average: \[ \mathcal{L}_B(\theta) = \mathbb{E}_{(x,y)\sim \widehat{q}_B}\big[\ell\big(f_\theta(x),\,y\big)\big] = \frac{1}{|B|} \sum_{i \in B} \ell\big(f_\theta(x_i),\, y_i\big), \] and SGD iterates \(\theta_{k+1} = \theta_k - \eta_k\, g_k\), \(\;g = \nabla_\theta\, \mathcal{L}_B(\theta)\), with learning rate \(\eta_k\).
With \(B\) sampled uniformly, \(\;\mathbb{E}_{B}\big[g\big] = \nabla_\theta \widehat{R}_n(\theta)\): an unbiased estimator. Noisy, but pointing the right way on average.
How noisy is \(g\)? Across random draws of \(B\) it scatters around its mean, and \(\operatorname{Cov}_B[g]\) measures that scatter. Averaging \(|B|\) independent per-sample gradients divides the covariance by \(|B|\): \[ \operatorname{Cov}_B\big[g\big] \;\approx\; \frac{1}{|B|}\, \Sigma(\theta), \qquad \Sigma(\theta) = \operatorname{Cov}_{(x,y)\sim \widehat{q}_n}\! \big[\nabla_\theta\, \ell\big(f_\theta(x), y\big)\big], \] with \(\Sigma(\theta)\) the covariance of a single sample’s gradient; the noise level shrinks like \(1/\sqrt{|B|}\).
Practical rule (recipe card): batch size is chosen by memory, then reported.
A valley, steep in \(\theta_2\) and flat in \(\theta_1\): GD’s single step size must be safe across the valley, hence agonizing along it. Real networks are far worse than \(\kappa = 25\).
Average the gradients instead of trusting the last one: \[ m_k = \beta\, m_{k-1} + (1-\beta)\, g_k, \qquad \theta_{k+1} = \theta_k - \eta\, m_k, \qquad \beta \approx 0.9 . \]
SGD(momentum=0.9) uses the un-normalized recursion \(m_k = \beta m_{k-1} + g_k\): same trajectory family, learning rate rescaled.\[ \begin{aligned} m_k &= \beta_1\, m_{k-1} + (1-\beta_1)\, g_k &&\text{(first moment: direction)}\\ v_k &= \beta_2\, v_{k-1} + (1-\beta_2)\, g_k^{\,2} &&\text{(second moment: scale)}\\ \widehat{m}_k &= \frac{m_k}{1-\beta_1^{\,k}}, \quad \widehat{v}_k = \frac{v_k}{1-\beta_2^{\,k}} &&\text{(bias correction: next slide)}\\ \theta_{k+1} &= \theta_k - \eta_k\, \frac{\widehat{m}_k}{\sqrt{\widehat{v}_k} + \varepsilon} &&\text{(defaults: } \beta_1 = 0.9,\ \beta_2 = 0.999,\ \varepsilon = 10^{-8}) \end{aligned} \]
Each coordinate gets its own effective step \(\eta_k / (\sqrt{\widehat{v}_k} + \varepsilon)\): coordinates with historically large gradients step small, and vice versa. That is the ill-conditioning fix, per coordinate and for free.
The EMAs start at \(m_0 = 0\), so early estimates are biased toward zero. Unroll, then take expectations, assuming the gradient distribution is roughly stationary over the window (\(\mathbb{E}_{B}[g_i] \approx g\)): \[ m_k = (1-\beta) \sum_{i=1}^{k} \beta^{\,k-i}\, g_i \quad\Longrightarrow\quad \mathbb{E}_{B}[m_k] \;\approx\; g\,(1-\beta)\sum_{i=1}^{k}\beta^{\,k-i} = g\,\big(1 - \beta^{\,k}\big). \] Dividing by \(1-\beta^{\,k}\) removes the startup bias; as \(k\) grows the correction retires. \(\square\)
L2 regularization adds \(\frac{\lambda}{2}\lVert\theta\rVert_2^2\) to the loss; its gradient \(\lambda\theta\) shrinks every weight each step (“weight decay”). Under SGD the two views coincide. Under Adam they do not:
The course optimizer is AdamW, with today’s recipe-card defaults, from U0.L2 through U3. Deviations must be justified in writing.
Every flow / CFM / DDPM lab in this course (U1.L1 … U3.L3) trains with AdamW under today’s defaults, and PS0 requires them. When a later loss \(\mathbb{E}_{t,\,x_1\sim q,\,x\sim p_t(\cdot\mid x_1)}[\cdots]\) mixes easy and hard time steps in one batch, the per-coordinate scaling is doing quiet work.
The bias-correction slide had a hidden warning: for small \(k\), \(\widehat{v}_k\) is an estimate built from a handful of samples.
Fix: start the learning rate near zero and ramp it up linearly while the moment estimates fill up.
Terminology: a step is one optimizer update on one minibatch; an epoch is a full pass over the data, \(\approx n/|B|\) steps. Course schedules are defined per step.
Linear warmup over 1–5% of total steps, then cosine decay to \(\approx 0\). Anything else in a later lab must be justified in writing.
Depth multiplies scales: a per-layer factor \(c \ne 1\) becomes \(c^L\), geometric growth or death, inherited by the gradients through the backward pass. At depth 50, the wrong constant costs \(10^{7}\).
Setup: \(y = Wx\), entries of \(W\) i.i.d. with mean \(0\) and variance \(\sigma_w^2\), independent of \(x\), whose coordinates are i.i.d. with mean \(0\).
\[ \operatorname{Var}[y_i] = \sum_{j=1}^{d} \operatorname{Var}[w_{ij} x_j] = d\; \sigma_w^2 \operatorname{Var}[x] \qquad (d = \text{fan}_{\text{in}}). \] A ReLU halves the second moment of a symmetric input, \(\;\mathbb{E}\big[\operatorname{ReLU}(z)^2\big] = \tfrac{1}{2}\mathbb{E}[z^2]\), so one linear + ReLU layer maps \(\operatorname{Var}[x] \mapsto \tfrac{1}{2}\, d\, \sigma_w^2\, \operatorname{Var}[x]\); the scale is preserved iff \[ \sigma_w^2 = 2 / \text{fan}_{\text{in}} \qquad \textbf{(He initialization).} \]
Xavier/Glorot (\(\sigma_w^2 = 2/(\text{fan}_{\text{in}}+\text{fan}_{\text{out}})\)) is the tanh/linear analogue (no ReLU halving to undo). Stated, not derived.
nn.Linear init is not He: check what your framework does before assuming what a paper assumed. (You will verify this in U0.L2.)U0.L2’s sabotaged runs include a bad-init variant: you will be handed loss curves and asked to name the disease. Today’s figure (flat vs. dying vs. exploding scale) is the diagnostic chart.
Folklore first: BatchNorm was introduced (2015) to fix “internal covariate shift”, the story that layer-input distributions drifting during training is the core problem. The explanation did not survive; the method did.
What normalization demonstrably does:
Mechanism on the next slide; then the knife.
\[ \mu_c = \mathbb{E}_{(x,y)\sim \widehat{q}_B}\big[x_c\big], \quad \sigma_c^2 = \operatorname{Var}_{(x,y)\sim \widehat{q}_B}\big[x_c\big], \quad \text{BN}(x_c) = \gamma_c\, \frac{x_c - \mu_c}{\sqrt{\sigma_c^2 + \varepsilon}} + \beta_c, \] with learned \(\gamma_c, \beta_c\); statistics taken over the batch and spatial positions.
model.train() vs model.eval(). Forgetting the switch is a classic silent bug (U0.L2 will make you feel it).Every sample’s output now depends on who else is in the batch:
So the models we build need per-sample normalization. Two candidates, one picture →
LN: per sample, all channels (transformers / DiT). GN: per sample, channel groups (U-Nets). Neither couples the batch; no train/eval split.
Generative models here use GroupNorm (U-Nets) or LayerNorm (transformers); BatchNorm only in this bootcamp’s CIFAR-10 classifier.
Deep stacks fail even when each layer is healthy: by depth 50, plain stacks are barely trainable. The fix is one plus sign: \[ x_{k+1} \;=\; x_k + f_k(x_k). \]
The gradient highway, in one line: the Jacobian \(\;\partial x_{k+1} / \partial x_k = I + \partial f_k / \partial x_k\;\) has an identity term, so the backward cotangent always has a direct path. Products of Jacobians no longer die like Board 2’s geometric chain, and depth stops being the enemy: this is what makes 50-block U-Nets and 30-block DiTs trainable.
Keep this innocuous plus sign in mind. In U1.T2 it becomes a theorem, and in U2 it becomes the whole course.
A convolution layer is a linear map, with two constraints baked in:
The striking number, CIFAR-10 sized (\(32 \times 32 \times 3 = 3072\) values):
| Layer | Parameters |
|---|---|
| Fully connected, \(3072 \to 3072\) | \(\approx 9.4\)M |
| Conv \(3{\times}3\), \(3 \to 64\) channels | 1,792 |
Three orders of magnitude fewer parameters, not by shrinking the model but by encoding an assumption about images. (Where 1,792 comes from: next slide.)
Parameter count: \(\;k \cdot k \cdot C_{\text{in}} \cdot C_{\text{out}} + C_{\text{out}}\;\) (one bias per output channel). The table’s conv layer: \(3 \cdot 3 \cdot 3 \cdot 64 + 64 = 1{,}792\).
The receptive field of a unit: the input region that can influence it. Strides multiply how fast it widens.
\[ r_L \;=\; 1 + \sum_{l=1}^{L} (k_l - 1) \prod_{i=1}^{l-1} s_i \qquad (k_l, s_l = \text{kernel size, stride of layer } l). \]
Stack: conv \(3{\times}3\) → conv \(3{\times}3\) → maxpool \(2{\times}2\) stride 2 → conv \(3{\times}3\) → conv \(3{\times}3\). Stride products before each layer: \(1, 1, 1, 2, 2\), so \[ r = 1 + 2 + 2 + 1\cdot 1 + 2\cdot 2 + 2\cdot 2 = 14 . \] Two cheap \(3{\times}3\) convs after a downsample buy what one expensive \(9{\times}9\) would before it: striding is how deep layers get to see the whole image.
Note the pool’s own term: kernel 2, stride product 1 before it, so it contributes \((2-1)\cdot 1 = 1\).
The U-Net (U0.T4 / U0.L4) is exactly CNN inductive bias + multiscale processing: today’s RF arithmetic is why its deep bottleneck sees the whole image, which a velocity field at low \(t\) genuinely needs.
See you next session.