

























Flow Matching 是一种连续归一化流 (Continuous Normalizing Flow, CNF) 生成模型。与扩散模型 (Diffusion Models) 不同,Flow Matching 直接在数据分布和噪声分布之间构建一条确定性的概率流路径,通过常微分方程 (ODE) 来描述这个变换过程。
给定:
Flow Matching 学习一个向量场 \(v_\theta(x_t, t)\),该向量场定义了从噪声到数据的 ODE:
\[\frac{dx_t}{dt} = v_\theta(x_t, t)\]
本实现采用最简单的条件概率路径——线性插值:
\[x_t = (1 - t) \cdot x_0 + t \cdot x_1\]
其中 \(x_0\) 是噪声,\(x_1\) 是目标数据。
对应的真实向量场(速度)为:
\[v(x_t, t) = x_1 - x_0 = \frac{dx_t}{dt}\]
即向量场指向从噪声到目标数据的方向,且大小恒定。
| 特性 | 扩散模型 (DDPM) | Flow Matching |
|---|---|---|
| 前向过程 | 逐步加噪(随机) | 线性插值(确定性) |
| 反向过程 | 去噪(随机 SDE) | ODE 积分(确定性) |
| 损失函数 | 噪声预测 MSE | 向量场预测 MSE |
| 采样方式 | 逐步去噪 | ODE 求解器(如 Euler 法) |
| 理论框架 | 随机微分方程 | 常微分方程 |
main.py
├── ConditionedDoubleConv — 条件双卷积模块
├── Down — 下采样模块
├── Up — 上采样模块
├── ConditionalUNet — 条件 U-Net 主模型
├── generate() — 采样/推理函数
├── sample_images() — 可视化保存函数
└── main (__main__) — 训练循环
device = "cuda" / "cpu" # 自动检测
batch_size = 256
lr = 1e-4 # AdamW 学习率
epochs = 100
num_classes = 10 # MNIST 0-9
class ConditionedDoubleConv(nn.Module):
def __init__(self, in_channels, out_channels, cond_dim):
# 第一层:普通卷积 + GroupNorm + SiLU
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
self.norm1 = nn.GroupNorm(8, out_channels)
# 第二层:拼接条件后卷积 + GroupNorm + SiLU
self.conv2 = nn.Conv2d(out_channels + cond_dim, out_channels, 3, padding=1)
self.norm2 = nn.GroupNorm(8, out_channels)
设计要点:
GroupNorm 而非 BatchNorm:在小 batch 下更稳定前向传播流程:
conv1 → GroupNorm → SiLU:提取基础特征cond 空间广播到与特征图相同尺寸[x, cond]conv2 → GroupNorm → SiLU:融合条件信息
class Down(nn.Module):
def forward(self, x, cond):
return self.conv(self.maxpool(x), cond) # MaxPool → DoubleConv
MaxPool2d(2) 将空间尺寸减半ConditionedDoubleConv 融合条件信息并变换通道数64 → 128 → 256
class Up(nn.Module):
def forward(self, x1, x2, cond):
x1 = self.up(x1) # 双线性插值上采样
# 处理尺寸不匹配(奇数特征图)
x1 = F.pad(x1, [diffX//2, diffX-diffX//2, diffY//2, diffY-diffY//2])
return self.conv(torch.cat([x2, x1], 1), cond) # 拼接跳跃连接
Upsample 双线性插值 2× 上采样x2384 → 128 → 64
class ConditionalUNet(nn.Module):
def __init__(self):
self.t_dim = 16 # 时间嵌入维度
self.label_dim = 16 # 标签嵌入维度
self.cond_dim = 32 # 总条件维度 = 16 + 16
# 嵌入层
self.time_embed = nn.Sequential(
nn.Linear(1, 32), nn.SiLU(), nn.Linear(32, 16))
self.label_embed = nn.Embedding(10, 16)
# 编码器:1 → 64 → 128 → 256
self.inc = ConditionedDoubleConv(1, 64, 32)
self.down1 = Down(64, 128, 32)
self.down2 = Down(128, 256, 32)
# 解码器:256 → 128 → 64 → 1
self.up1 = Up(256+128, 128, 32)
self.up2 = Up(128+64, 64, 32)
self.outc = nn.Conv2d(64, 1, kernel_size=1)
网络架构图:
输入 x (B, 1, 28, 28)
│
├── inc ──────────────► x1 (B, 64, 28, 28) ─────────────┐
│ │
├── down1 ────────────► x2 (B, 128, 14, 14) ──────────┐ │
│ │ │
├── down2 ────────────► x3 (B, 256, 7, 7) │ │
│ │ │ │
│ ┌────┴────┐ │ │
│ │ up1 │◄── cat(x3, x2) ───────┘ │
│ └────┬────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ up2 │◄── cat(up1_out, x1) ─────┘
│ └────┬────┘
│ │
└── outc ◄────────────────┘
│
输出 v_pred (B, 1, 28, 28) ← 预测的向量场
def forward(self, x, t, labels):
t_emb = self.time_embed(t.view(-1, 1)) # (B,) → (B, 16)
lbl_emb = self.label_embed(labels) # (B,) → (B, 16)
cond = torch.cat([t_emb, lbl_emb], dim=1) # (B, 32)
cond = cond.unsqueeze(-1).unsqueeze(-1) # (B, 32, 1, 1)
Flow Matching 的训练目标是最小化预测向量场与真实向量场之间的 MSE:
\[\mathcal{L}(\theta) = \mathbb{E}_{t \sim U(0,1), x_1 \sim p_{data}, x_0 \sim \mathcal{N}(0,I)} \left[ \| v_\theta(x_t, t) - (x_1 - x_0) \|^2 \right]\]
代码实现:
# 1. 采样噪声和时间
noise = torch.randn_like(images) # x_0 ~ N(0,I)
t = torch.rand(images.size(0), device=device) # t ~ U(0,1)
# 2. 线性插值构造 x_t
xt = (1 - t) * noise + t * images # x_t = (1-t)·x_0 + t·x_1
# 3. 预测向量场
vt_pred = model(xt, t, labels) # v_θ(x_t, t)
# 4. 计算损失
loss = F.mse_loss(vt_pred, images - noise) # target = x_1 - x_0
for epoch in range(epochs):
for images, labels in train_loader:
# 数据预处理:归一化到 [-1, 1]
# transforms.Lambda(lambda x: 2*x - 1)
# 梯度裁剪,防止训练不稳定
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
关键设计:
max_norm=1.0,防止梯度爆炸训练完成后,通过数值积分求解 ODE 生成新样本:
\[\frac{dx}{dt} = v_\theta(x, t), \quad x(0) \sim \mathcal{N}(0, I)\]
使用 Euler 法(一阶 ODE 求解器):
\[x_{t+\Delta t} = x_t + v_\theta(x_t, t) \cdot \Delta t\]
代码实现:
@torch.no_grad()
def generate(label, num_samples=16, num_steps=100):
model.eval()
x = torch.randn(num_samples, 1, 28, 28) # x_0 ~ N(0,I)
labels = torch.full((num_samples,), label, dtype=torch.long)
dt = 1.0 / num_steps # 步长
for i in range(num_steps):
t = torch.full((num_samples,), i * dt) # 当前时间
x = x + model(x, t, labels) * dt # Euler 步进
return (x.clamp(-1, 1) + 1) / 2 # 映射回 [0, 1]
采样过程示意:
t=0.0: x ~ N(0,I) ← 纯噪声
│ v_θ(x, 0.0) * dt
│
t=0.5: x ≈ 半噪半图
│ v_θ(x, 0.5) * dt
│
t=1.0: x ≈ 干净图片 → clamp + 去归一化 → [0,1] 图像
num_steps=100:100 步 Euler 积分,步长 dt = 0.01clamp(-1, 1) 防止数值溢出,再映射回 [0, 1] 用于可视化
【训练阶段】
MNIST 数据 x_1 噪声 x_0 ~ N(0,I)
│ │
└───────┬───────────────┘
│
线性插值: x_t = (1-t)·x_0 + t·x_1
真实向量场: v = x_1 - x_0
│
┌───────▼───────┐
│ ConditionalUNet│ ← 条件:t, label
│ 预测 v_θ │
└───────┬───────┘
│
Loss = MSE(v_θ, v) ← 梯度下降
【推理阶段】
噪声 x_0 ~ N(0,I) + 目标 label "7"
│
│ for t = 0 → 1 step dt:
│ x_{t+dt} = x_t + v_θ(x_t, t, label) * dt
▼
生成数字 "7" 的图片
| 描述 | 公式 |
|---|---|
| 插值路径 | \(x_t = (1-t)x_0 + t x_1\) |
| 真实向量场 | \(v = x_1 - x_0\) |
| 条件嵌入 | \(c = [\text{MLP}(t), \text{Embed}(y)]\) |
| 训练损失 | \(\mathcal{L} = \|v_\theta(x_t, t, y) - (x_1 - x_0)\|^2\) |
| Euler 采样 | \(x_{t+\Delta t} = x_t + v_\theta(x_t, t, y) \cdot \Delta t\) |
训练过程中每个 epoch 生成 10 个类别 × 8 个样本 = 80 张图片,保存为 flow_match_sample_{epoch}.png。
早期 epoch 生成的数字模糊,随着训练的进行:
import torch import torch.nn as nn import torch.nn.functional as F import torchvision from torchvision import transforms from torchvision.utils import save_image from torch.utils.data import DataLoader device = torch.device("cuda" if torch.cuda.is_available() else "cpu") batch_size = 256 lr = 1e-4 epochs = 100 num_classes = 10 class ConditionedDoubleConv(nn.Module): def __init__(self, in_channels, out_channels, cond_dim): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1) self.norm1 = nn.GroupNorm(8, out_channels) self.conv2 = nn.Conv2d(out_channels + cond_dim, out_channels, kernel_size=3, padding=1) self.norm2 = nn.GroupNorm(8, out_channels) def forward(self, x, cond): x = F.silu(self.norm1(self.conv1(x))) cond = cond.expand(-1, -1, x.size(2), x.size(3)) x = torch.cat([x, cond], dim=1) return F.silu(self.norm2(self.conv2(x))) class Down(nn.Module): def __init__(self, in_channels, out_channels, cond_dim): super().__init__() self.maxpool = nn.MaxPool2d(2) self.conv = ConditionedDoubleConv(in_channels, out_channels, cond_dim) def forward(self, x, cond): return self.conv(self.maxpool(x), cond) class Up(nn.Module): def __init__(self, in_channels, out_channels, cond_dim): super().__init__() self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True) self.conv = ConditionedDoubleConv(in_channels, out_channels, cond_dim) def forward(self, x1, x2, cond): x1 = self.up(x1) diffY = x2.size()[2] - x1.size()[2] diffX = x2.size()[3] - x1.size()[3] x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2]) return self.conv(torch.cat([x2, x1], dim=1), cond) class ConditionalUNet(nn.Module): def __init__(self): super().__init__() self.t_dim = 16 self.label_dim = 16 self.cond_dim = self.t_dim + self.label_dim self.time_embed = nn.Sequential( nn.Linear(1, 32), nn.SiLU(), nn.Linear(32, self.t_dim) ) self.label_embed = nn.Embedding(num_classes, self.label_dim) self.inc = ConditionedDoubleConv(1, 64, self.cond_dim) self.down1 = Down(64, 128, self.cond_dim) self.down2 = Down(128, 256, self.cond_dim) self.up1 = Up(256 + 128, 128, self.cond_dim) self.up2 = Up(128 + 64, 64, self.cond_dim) self.outc = nn.Conv2d(64, 1, kernel_size=1) def forward(self, x, t, labels): t_emb = self.time_embed(t.view(-1, 1)) lbl_emb = self.label_embed(labels) cond = torch.cat([t_emb, lbl_emb], dim=1).unsqueeze(-1).unsqueeze(-1) x1 = self.inc(x, cond) x2 = self.down1(x1, cond) x3 = self.down2(x2, cond) x = self.up1(x3, x2, cond) x = self.up2(x, x1, cond) return self.outc(x) @torch.no_grad() def generate(label, num_samples=16, num_steps=100): model.eval() x = torch.randn(num_samples, 1, 28, 28, device=device) labels = torch.full((num_samples,), label, device=device, dtype=torch.long) dt = 1.0 / num_steps for i in range(num_steps): t = torch.full((num_samples,), i * dt, device=device) x = x + model(x, t, labels) * dt return (x.clamp(-1, 1) + 1) / 2 def sample_images(epoch): samples = [] for label in range(10): samples.append(generate(label, num_samples=8)) samples = torch.cat(samples, dim=0) save_image(samples, f'flow_match_sample_{epoch}.png', nrow=8) if __name__ == '__main__': transform = transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: 2 * x - 1) ]) train_dataset = torchvision.datasets.MNIST( root='./data', train=True, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) model = ConditionalUNet().to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=lr) for epoch in range(epochs): model.train() total_loss = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) noise = torch.randn_like(images) t = torch.rand(images.size(0), device=device) xt = (1 - t.view(-1, 1, 1, 1)) * noise + t.view(-1, 1, 1, 1) * images vt_pred = model(xt, t, labels) loss = F.mse_loss(vt_pred, images - noise) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss += loss.item() print(f'Epoch [{epoch + 1}/{epochs}], Loss: {total_loss / len(train_loader):.4f}') sample_images(epoch + 1)
第31次生成结果:

此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。