BEVERLY QIN

Back to Projects

Flow Matching from Scratch

my role

Developer

year

2026

contribution

Generative Modeling, UNet Implementation, Training

tools

Python, PyTorch, torchvision

team

Individual project

Overview

Built and trained a flow-matching model on MNIST from scratch. The project begins with a single-step UNet denoiser, then adds time conditioning for iterative generation and class conditioning for controllable digit synthesis.

The model architecture, training setup, and all results below follow the original technical report.

Single-Step Denoising UNet

I implemented the prescribed UNet as a contracting encoder and expanding decoder connected with skip connections. Conv layers use 3×3 kernels; stride-2 downsampling maps 28×28 MNIST images to a 7×7 bottleneck; transposed convolutions restore spatial resolution.

The unconditional model uses hidden dimension 128 and is trained with Adam (lr 1e-4), MSE loss, batch size 256, σ=0.5 input noise, and five epochs. It recovers digits well at the trained noise level. At σ=0.8 and 1.0, outputs degrade because the model was never trained for those distributions.

Training-distribution denoising

These figures move from the injected noise, to loss over training, to test reconstructions and robustness outside σ=0.5.

Input digits with increasing Gaussian noise σ
Input digits with increasing Gaussian noise σ
MSE loss over five denoiser-training epochs
MSE loss over five denoiser-training epochs
Test denoising after epoch 1
Test denoising after epoch 1
Test denoising after epoch 5
Test denoising after epoch 5
Test results across unseen noise levels
Test results across unseen noise levels

Pure-noise failure case

Because pure noise contains no digit identity, MSE collapses toward an average-looking digit rather than a specific class.

Loss when predicting digits from pure noise
Loss when predicting digits from pure noise
Pure-noise outputs after epoch 1
Pure-noise outputs after epoch 1
Pure-noise outputs after epoch 5
Pure-noise outputs after epoch 5

Time-Conditioned Flow Matching

FCBlocks map scalar t to channel-wise signals at two decoder locations: after unflattening and after the first UpBlock. The model is trained on random timesteps with x_t = (1−t)x₀ + tx₁, predicting velocity x₁ − x₀.

The first tile is the training-loss curve; the next three are samples generated from pure noise after epochs 1, 5, and 10. The time-conditioned UNet uses hidden dimension 64, batch size 64, an initial 1e-2 learning rate with exponential decay, and 50 Euler steps at inference. Samples become legible by epoch 10, though they remain softer than class-conditioned outputs.

Time-conditioned flow-matching loss
Time-conditioned flow-matching loss
Time-conditioned samples after epoch 1
Time-conditioned samples after epoch 1
Time-conditioned samples after epoch 5
Time-conditioned samples after epoch 5
Time-conditioned samples after epoch 10
Time-conditioned samples after epoch 10

Class-Conditioned Generation

Two further FCBlocks consume one-hot class labels and combine their output with time conditioning at the same decoder stages. With p_uncond=0.1, labels are dropped during training so the model learns both conditional and unconditional predictions.

The first tile is the class-conditioned loss curve. The next three grids show four samples for every digit class after epochs 1, 5, and 10. During sampling, classifier-free guidance with γ=5 combines conditional and unconditional flows: u = u_uncond + γ(u_cond − u_uncond). By epoch 10, each row consistently corresponds to its target digit and is sharper than time-only generation.

Class-conditioned flow-matching loss
Class-conditioned flow-matching loss
Class-conditioned samples after epoch 1
Class-conditioned samples after epoch 1
Class-conditioned samples after epoch 5
Class-conditioned samples after epoch 5
Class-conditioned samples after epoch 10
Class-conditioned samples after epoch 10