系列 · PDE × 机器学习 · 第 6 篇

偏微分方程与机器学习(六):连续归一化流与 Neural ODE

如何把高斯变成数据分布?本文从 ODE/PDE 理论出发,系统推导 Neural ODE、伴随方法、连续归一化流(FFJORD)与 Flow Matching,并用 7 张图把核心机制画清楚。

怎么把一团各向同性的高斯噪声“吹”成一张猫的照片?

偏微分方程与机器学习(六):连续归一化流与 Neural ODE — 章节概览图

归一化流给的答案很直接:用一系列可逆变换,一步步把简单分布推到复杂分布。这一篇要讲的连续归一化流(CNF)把“一系列变换”推到极限——让步长趋于零,离散变换链就变成一个 ODE,可逆性自动满足,密度变化由瞬时换元公式控制。

这条 ODE 的右端是一个神经网络,于是整套东西叫 Neural ODE。它有几个让我觉得很优雅的性质:内存只跟输入大小有关而不跟 ODE 步数有关(伴随方法)、模型可以连续插值时间、和最优传输理论直接挂钩。

本文从 ODE/PDE 理论出发,系统推导 Neural ODE、伴随方法、连续归一化流(FFJORD),最后讲到 2023 年之后真正在大规模生成模型里取代它的方案——Flow Matching。配 7 张图把核心机制画清楚。

你将学到什么#

生成建模归根结底是一个几何问题:如何将简单分布(比如高斯分布)变换为复杂分布(如人脸、分子或运动轨迹)? 离散归一化流通过堆叠可逆模块实现这一目标,但每个模块都需要计算 Jacobian 行列式,其代价高达 $O(d^3)$Neural ODE 用连续的常微分方程(ODE)取代离散的网络深度;连续归一化流(Continuous Normalizing Flows, CNF) 则借助 瞬时 变量替换公式,将密度计算的复杂度降至 $O(d)$ ;而 Flow Matching 更进一步,直接省去了散度积分,将训练简化为对目标速度场的普通回归任务。

全文围绕三条主线交织展开:

  1. PDE 视角 —— 连续性方程 $\partial_t\rho + \nabla\!\cdot(\rho v) = 0$ 描述了速度场 $v$ 如何输运密度 $\rho$
  2. ODE 视角 —— Picard-Lindelöf 定理保证了解的存在性与唯一性;Liouville 定理将体积变化与 $\nabla\!\cdot v$ 联系起来;伴随方程则使反向传播的内存开销降至 $O(1)$
  3. 机器学习视角 —— Neural ODE、FFJORD 和 Flow Matching 都通过神经网络参数化速度场 $v$ ,并从数据中学习它。

前置知识:ODE 基础(解的存在唯一性)、概率密度的变量替换、自动微分(autograd)。

连续流把高斯逐步变成两月牙目标分布。
图 1:连续时间流将高斯基分布输运成“双月牙”目标分布。每个面板是通过对约 4000 个粒子求解 ODE 至时间 $t$ 后,使用核密度估计(KDE)得到的密度 $\rho_t$ 。所有时间点共享同一个网络 $v_\theta$ ,仅积分上限不同。


ODE 基础:存在唯一性与体积演化#

Picard-Lindelöf:解何时存在且唯一?#

$$ \|f(\mathbf{z}_1, t) - f(\mathbf{z}_2, t)\| \le L\,\|\mathbf{z}_1 - \mathbf{z}_2\|, $$

则在某个区间 $[0, T]$ 上存在唯一解。

这对机器学习意味着什么? 如果 $f_\theta$ 是一个使用 Lipschitz 激活函数(如 ReLU、tanh、GELU)且权重有界的神经网络,那么局部 Lipschitz 条件自然成立。因此,只要网络行为良好——这在实践中几乎总是成立——Neural ODE 就是适定的,这也是它能稳定进行反向传播的根本原因。

Liouville 定理:流如何改变体积#

$$ \frac{d}{dt}\,\mathrm{vol}(\phi_t(\Omega)) = \int_{\phi_t(\Omega)} \nabla\!\cdot f\,d\mathbf{z}. $$

因此,$\nabla\!\cdot f = 0$ 保持体积不变,$\nabla\!\cdot f < 0$ 导致压缩,$\nabla\!\cdot f > 0$ 引起膨胀。在归一化流中,我们恰恰希望散度非零——这正是重塑概率质量的关键杠杆。

直观理解:散度为零的 $f$ 类似不可压缩流体(如第 5 篇讨论的哈密顿或辛系统);而散度非零的 $f$ 则像可压缩流,能将概率质量挤压成细丝,再在别处重新膨胀——这正是生成建模所需要的特性。

瞬时变量替换公式#

$$ \boxed{\;\frac{d}{dt}\log\rho_t(\mathbf{z}(t)) = -\nabla\!\cdot f(\mathbf{z}(t), t).\;}\tag{1} $$

证明思路:连续性方程 $\partial_t\rho + \nabla\!\cdot(\rho f) = 0$ 可展开为 $\partial_t\rho + f\!\cdot\!\nabla\rho = -\rho\,\nabla\!\cdot f$ 。左边正是沿轨迹 $\mathbf{z}(t)$ 的物质导数 $D\rho/Dt$ 。两边同除以 $\rho$ 即得 (1)。

为何这个公式至关重要? 离散归一化流需要 $O(d^3)$ 的代价来计算 $\log|\det \partial\phi / \partial\mathbf{z}|$ ,而公式 (1) 仅需 Jacobian 的迹(即散度),借助一次 vector-Jacobian product 即可在 $O(d)$ 时间内完成(见 3.2 节)。这正是连续归一化流(CNF)得以存在的核心计算优势。

Neural ODE:从离散到连续深度#

残差网络即前向 Euler 方法#

$$ \frac{d\mathbf{h}}{dt} = f_\theta(\mathbf{h}(t), t), \qquad \mathbf{h}(T) = \mathbf{h}(0) + \int_0^T f_\theta(\mathbf{h}(t), t)\,dt. \tag{2} $$

这一转变带来三大优势:

  • 参数效率更高:单个网络 $f_\theta$ 替代了每一层不同的 $f_l$
  • 自适应深度:如 dopri5(自适应 Runge-Kutta)等求解器会自动在动力学剧烈处采用小步长,在平缓处采用大步长。
  • 内存占用更低:伴随方法将反向传播的内存开销从 $O(L)$ 降至 $O(1)$ ——深度增加带来的只是计算时间,而非显存压力。

ResNet(离散深度,固定步)vs Neural ODE(连续深度,自适应求解器)。
图 2:左侧是 ResNet,由一系列 $h_{l+1} = h_l + f_l(h_l)$ 的 Euler 步组成,每层参数独立,反向传播需存储所有中间激活;右侧是 Neural ODE,由单一 $f_\theta$ 驱动,自适应求解器动态选择评估点,伴随方法仅用 $O(1)$ 内存即可恢复梯度。

实现:一个极简的神经微分方程(Neural ODE)
核心思想是用常微分方程(ODE)求解器调用,替代 ResNet 的前向传播过程。以下是完整、可直接运行的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import torch
import torch.nn as nn
from scipy.integrate import solve_ivp

class ODEFunc(nn.Module):
    # 定义动力学系统的向量场 f_theta(h, t)
    def __init__(self, dim=2, hidden=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(dim + 1, hidden), nn.Tanh(),
            nn.Linear(hidden, hidden), nn.Tanh(),
            nn.Linear(hidden, dim)
        )

    def forward(self, t, h):
        # 构造时间变量 t 的批量向量(形状与 h 对齐)
        t_vec = t * torch.ones(h.shape[0], 1)
        # 将状态 h 和时间 t 拼接后输入网络
        return self.net(torch.cat([h, t_vec], dim=-1))

def neural_ode_forward(func, h0, t_span=(0, 1), steps=100):
    # 简单的定步长欧拉法积分(便于理解原理)
    dt = (t_span[1] - t_span[0]) / steps
    h = h0
    t = t_span[0]
    for _ in range(steps):
        h = h + dt * func(torch.tensor(t), h)  # 显式欧拉更新
        t += dt
    return h

func = ODEFunc(dim=2, hidden=64)
opt = torch.optim.Adam(func.parameters(), lr=1e-3)

t_true = torch.linspace(0, 1, 50)
theta = 4 * torch.pi * t_true
r = 2 * (1 - t_true)
target = torch.stack([r * torch.cos(theta), r * torch.sin(theta)], dim=-1)

for step in range(2000):
    h0 = target[0:1]  # 初始状态(起始点)
    h_pred = neural_ode_forward(func, h0.repeat(50, 1), steps=50)  # 沿时间轴生成 50 个预测点
    loss = ((h_pred - target)**2).mean()  # 均方误差损失
    opt.zero_grad(); loss.backward(); opt.step()
    if step % 500 == 0:
        print(f"Step {step}: loss={loss.item():.5f}")

该 Neural ODE 成功学习到了一个光滑的向量场,能将任意初始点沿螺旋路径连续演化。与 ResNet 不同,这里的积分深度(即欧拉步数)是一个超参数,而非固定的网络结构设计——它甚至可以进一步设为自适应的(例如根据局部误差动态调整步长)。

伴随灵敏度方法#

标准反向传播在 ODE 求解过程中需存储每一步的中间状态,内存开销为 $O(L)$ ——而自适应求解器可能执行上百步。伴随方法则完全避免了这一问题。

$$ \frac{d\mathbf{a}}{dt} = -\,\mathbf{a}(t)^\top \frac{\partial f_\theta}{\partial\mathbf{h}}, \tag{3} $$ $$ \frac{d\mathcal{L}}{d\theta} = -\int_T^0 \mathbf{a}(t)^\top \frac{\partial f_\theta}{\partial\theta}\,dt. \tag{4} $$

算法流程如下

  1. 前向传递:求解 (2) 从 $0 \to T$ ,仅保存最终状态 $\mathbf{h}(T)$
  2. 初始化:设 $\mathbf{a}(T) = \partial\mathcal{L}/\partial\mathbf{h}(T)$
  3. 反向传递:将 $\mathbf{h}$$\mathbf{a}$ 联合从 $T \to 0$ 反向求解,并累积 (4) 中的梯度。

该方法的内存开销为 $O(1)$ ,与求解步数无关。代价是反向过程需额外求解一次 ODE——计算量约为原来的两倍,却换来了近乎无限的内存节省。

伴随方法:在二维向量场上的正反两条轨迹,以及不同深度下的内存对比。
图 3:左图展示同一螺旋 ODE 先正向积分(蓝色)得到 $h(T)$ ,再与伴随状态(红色虚线)一同反向积分以恢复梯度;右图对比内存开销随求解步数 $L$ 的增长情况:标准反向传播为 $O(L)$ ,而伴随方法始终保持 $O(1)$ ——当 $L=1000$ 时,内存节省达千倍。

实现:实际应用中的伴随敏感度方法
伴随方法通过在反向传播过程中重新计算中间状态,避免了显式存储这些状态,从而大幅节省内存。以下是该核心思想的具体实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import torch
import torch.nn as nn

def adjoint_backward(func, h_T, loss_grad, theta, T=1.0, steps=100):
    # 反向常微分方程(ODE)求解:重建隐状态 h(t) 并计算参数梯度
    # h_T:终态;loss_grad:损失函数对 h(T) 的梯度 dL/dh(T);theta:模型参数
    dt = T / steps
    h = h_T.detach().clone().requires_grad_(True)
    a = loss_grad.clone()  # 伴随变量(adjoint state)
    param_grad = [torch.zeros_like(p) for p in theta]

    for step in range(steps):
        t = T - step * dt
        # 重新计算 f(h, t)(即正向 ODE 的右端项)
        f = func(torch.tensor(t), h)
        # 伴随动力学方程:da/dt = -a^T ∂f/∂h
        vjp = torch.autograd.grad(f, h, -a, retain_graph=True)[0]
        a = a + dt * vjp
        # 累加参数梯度:dL/dθ += -a^T ∂f/∂θ
        grads = torch.autograd.grad(f, theta, -a * dt, retain_graph=True)
        for pg, g in zip(param_grad, grads):
            pg += g
        # 对 h 执行反向欧拉步进(Reverse Euler step)
        h = (h - dt * f).detach().requires_grad_(True)

    return param_grad

print("标准反向传播:O(L × d) 内存(需缓存全部中间隐状态 h)")
print("伴随方法:    O(d) 内存(运行时按需重建 h,无需缓存)")
print("计算开销:    约为正向计算的 2 倍(需额外求解一次 ODE)")
方法内存复杂度计算开销梯度精度
标准反向传播$O(L \cdot d)$$1\times$精确
伴随法(固定步长)$O(d)$$2\times$精确
伴随法(自适应步长)$O(d)$$2\text{--}3\times$近似(精度取决于数值容差)
检查点法(Checkpointing)$O(\sqrt{L} \cdot d)$$1.5\times$精确

对于深层网络($L > 100$ )或高维隐状态($d > 1000$ )场景,伴随方法带来的内存优势往往是决定性的。实践中,torchdiffeq 库已自动集成了该机制。

表达能力#

Neural ODE 在 $\mathbb{R}^d$ 上的同胚映射空间中是稠密的(Zhang et al., 2020)。然而,它无法改变拓扑结构——例如,单个定义在 $\mathbb{R}^d$ 上的 Neural ODE 无法解开两个互锁的环。这催生了增广型 Neural ODE(Augmented Neural ODE):通过将系统提升至 $\mathbb{R}^{d+k}$ ,额外维度为流提供了“解扣”的空间。

连续归一化流(CNF)#

从离散流到连续流#

$$ \mathbf{z}_K = f_K \circ \cdots \circ f_1(\mathbf{z}_0), \qquad \log p_K = \log p_0 - \sum_{k=1}^K \log\!\bigl|\det \partial f_k / \partial\mathbf{z}_{k-1}\bigr|. $$

每个行列式计算的复杂度为 $O(d^3)$ ,除非采用特殊架构(如耦合层、自回归结构等)将其降至 $O(d)$ ——但这会限制模型的表达能力。

$$ \frac{d\mathbf{z}}{dt} = f_\theta(\mathbf{z}(t), t), \qquad \frac{d\log p}{dt} = -\nabla\!\cdot f_\theta(\mathbf{z}(t), t). \tag{5} $$

无需对网络施加可逆性约束——ODE 本身可通过反向积分实现逆映射;无需计算行列式——只需计算迹(即散度)。

FFJORD:通过 Hutchinson 估计实现可扩展的迹计算#

$$ \nabla\!\cdot f = \mathbb{E}_{\boldsymbol\epsilon}\!\left[\boldsymbol\epsilon^\top\!\frac{\partial f}{\partial\mathbf{z}}\,\boldsymbol\epsilon\right], \qquad \boldsymbol\epsilon \sim \mathcal{N}(0, \mathbf{I}). \tag{6} $$

这就是著名的 Hutchinson 迹估计器,每次采样仅需一次 vector-Jacobian product,其计算成本与维度 $d$ 无关。

Hutchinson 迹估计:方差以 1/sqrt(K) 收缩,单步代价从 O(d^2) 降为 O(d)。
图 4:左图展示在 $d=64$ 下,Hutchinson 估计器在 400 次试验中的方差随探针向量数 $K$ 的变化,虚线表示理论上的 $1/\sqrt{K}$ 收敛速率;右图对比不同维度下的单步散度计算成本:完整 Jacobian 需 $O(d^2)$ 次自动微分调用,而 $K=4$ 的 Hutchinson 方法仅需 $O(Kd)$ ——在 $d=1024$ 时快三个数量级。

训练与采样#

$$ \log p_1(\mathbf{x}) = \log p_0(\mathbf{z}_0) + \int_0^1 \nabla\!\cdot f_\theta(\mathbf{z}(t), t)\,dt, $$

其中 $\mathbf{z}_0$ 通过从 $\mathbf{x}$ 反向积分 ODE (5) 得到。我们使用伴随方法最大化对数似然。采样时,只需从 $p_0$ 中采样 $\mathbf{z}_0$ ,然后正向积分即可。

权衡取舍:CNF 提供精确的似然估计,但每次前向或反向传递都需要求解 ODE——通常涉及数十至数百次网络评估。训练过程也较为敏感:求解器容差、$f_\theta$ 的正则化强度以及 Hutchinson 估计的方差会相互影响。

实现:FFJORD 密度估计
将神经微分方程(Neural ODE)与 Hutchinson 迹估计法相结合,我们便得到了一个完整的概率密度估计器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import torch
import torch.nn as nn

class FFJORD(nn.Module):
    def __init__(self, dim=2, hidden=128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(dim + 1, hidden), nn.Softplus(),
            nn.Linear(hidden, hidden), nn.Softplus(),
            nn.Linear(hidden, dim)
        )

    def forward(self, t, z):
        # 构造时间变量 t 的批量向量(形状与 z 的 batch 维度对齐)
        t_vec = t * torch.ones(z.shape[0], 1)
        # 将状态 z 和时间 t 拼接后输入网络
        return self.net(torch.cat([z, t_vec], dim=-1))

    def log_prob(self, x, steps=100):
        # 反向积分:从 t=1 处的观测数据 x 出发,回溯至 t=0 处的隐变量 z0
        dt = 1.0 / steps
        z = x.clone().requires_grad_(True)
        log_det = torch.zeros(x.shape[0])
        for step in range(steps):
            t = 1.0 - step * dt
            f = self.forward(torch.tensor(t), z)
            # 使用 Hutchinson 方法估计雅可比矩阵的迹(即散度)
            eps = torch.randn_like(z)
            jvp = torch.autograd.grad(f, z, eps, create_graph=True)[0]
            div = (jvp * eps).sum(dim=-1)  # 迹的无偏估计
            log_det = log_det - dt * div
            z = z - dt * f
            # 重置计算图:分离当前 z 并重新启用梯度追踪
            z = z.detach().requires_grad_(True)
        # 基础分布:标准正态分布(各维独立同分布)
        log_p0 = -0.5 * (z**2).sum(dim=-1) - z.shape[-1] * 0.5 * torch.log(torch.tensor(2*torch.pi))
        return log_p0 + log_det

model = FFJORD(dim=2)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)

for step in range(5000):
    log_px = model.log_prob(x_data)
    loss = -log_px.mean()  # 负对数似然损失
    opt.zero_grad(); loss.backward(); opt.step()
    if step % 1000 == 0:
        print(f"训练步数 {step}: NLL={loss.item():.3f}")

log_prob 方法在反向求解常微分方程的同时,借助 Hutchinson 估计器持续累积对数密度的变化量。最终结果由基础分布的对数概率 $\log p_0(\mathbf{z}_0)$ 与累积的散度积分共同构成——整个过程无需显式计算任何雅可比行列式。

最优传输与 Flow Matching#

偏微分方程与机器学习(六):连续归一化流与 Neural ODE — 章节小结图

Benamou-Brenier 联系#

$$ \min_{v_t}\,\int_0^1\!\!\int \|v_t(\mathbf{z})\|^2\,\rho_t(\mathbf{z})\,d\mathbf{z}\,dt \quad\text{s.t.}\quad \partial_t\rho + \nabla\!\cdot(\rho v) = 0,\;\rho_0, \rho_1\text{ 给定}. $$

其最优解 $v_t^\star$ 恰好是 CNF 的速度场,且在欧氏最优传输情形下,其轨迹为直线。这为将 CNF 与最优传输结合提供了最清晰的几何动机。

Flow Matching#

Flow Matching(Lipman et al., 2022)是一种极具实用价值的简化方案。它既不通过 ODE 求解器优化负对数似然(NLL),也不求解复杂的最优传输问题,而是选定一条条件概率路径,并直接回归对应的速度场。

$$ u_t^\star(\mathbf{z}_t \mid \mathbf{z}_0, \mathbf{z}_1) = \mathbf{z}_1 - \mathbf{z}_0. \tag{7} $$ $$ \mathcal{L}_{\text{FM}} = \mathbb{E}_{t,\,\mathbf{z}_0,\,\mathbf{z}_1}\Bigl[\,\|v_\theta(\mathbf{z}_t, t) - (\mathbf{z}_1 - \mathbf{z}_0)\|^2\,\Bigr]. \tag{8} $$

关键定理(Lipman et al.):边缘速度 $\mathbb{E}[u_t^\star \mid \mathbf{z}_t]$ 满足连续性方程,能将 $p_0$ 输运至 $p_1$ 。因此,只要 $v_\theta$ 足够灵活,最小化 (8) 即可恢复一个有效的 CNF——且训练过程中完全无需计算散度。

Flow Matching:成对样本之间的线性条件路径;对比 CNF 的训练曲线。
图 5:左图展示随机样本对 $(\mathbf{z}_0, \mathbf{z}_1)$ 之间的线性条件路径,任意 $\mathbf{z}_t$ 处的目标速度就是 $\mathbf{z}_1 - \mathbf{z}_0$ ——训练时既无散度计算,也无需 ODE 求解;右图对比损失曲线:Flow Matching 收敛速度快近一个数量级,且达到更低的稳定平台。

2024 年的实际选择#

方法训练成本优点缺点
离散 NF(RealNVP/Glow)低廉,无需 ODE采样和似然计算快架构受限
CNF / FFJORDODE + Hutchinson$f_\theta$ 形式自由,似然精确训练慢,调参敏感
OT-FlowOT 代价 + 匹配路径笔直、最优需平衡两个损失项
Flow Matching纯回归稳定、快速、易于扩展至图像等高维数据需设计合适的条件路径
Rectified Flow / 一致性迭代拉直极少步数即可采样需多阶段训练

截至 2024 年,大多数生产级的连续流系统(用于图像、音频、分子生成)都采用了 Flow Matching 或 Rectified Flow 的某种变体。

实现:流匹配(Flow Matching)—— 当代主流方法
流匹配彻底摒弃了 FFJORD 的整套复杂机制,转而采用极其简洁的回归范式。其训练循环短小精悍,一目了然:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import torch
import torch.nn as nn

class VelocityNet(nn.Module):
    # 时序条件化速度场 v_theta(z, t)
    def __init__(self, dim=2, hidden=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(dim + 1, hidden), nn.SiLU(),
            nn.Linear(hidden, hidden), nn.SiLU(),
            nn.Linear(hidden, hidden), nn.SiLU(),
            nn.Linear(hidden, dim)
        )

    def forward(self, z, t):
        # 将标量时间 t 扩展为列向量,适配 batch 维度
        t_vec = t.unsqueeze(-1) if t.dim() == 1 else t
        return self.net(torch.cat([z, t_vec], dim=-1))

def flow_matching_loss(model, x1, sigma_min=1e-4):
    # x1:真实数据样本
    batch = x1.shape[0]
    t = torch.rand(batch, 1)  # 在 [0, 1] 上均匀采样时间点
    x0 = torch.randn_like(x1)  # 标准正态噪声样本
    # 条件插值路径:z_t = (1 - t) * x0 + t * x1
    zt = (1 - t) * x0 + t * x1
    # 目标速度(即路径对时间的导数):dx/dt = x1 - x0
    target = x1 - x0
    # 模型预测速度并执行回归优化
    pred = model(zt, t)
    return ((pred - target)**2).mean()

model = VelocityNet(dim=2)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)

for step in range(5000):
    loss = flow_matching_loss(model, x_data)
    opt.zero_grad(); loss.backward(); opt.step()
    if step % 1000 == 0:
        print(f"训练步数 {step}:流匹配损失={loss.item():.4f}")

@torch.no_grad()
def sample_flow_matching(model, n=2000, steps=100):
    # 通过前向积分学习到的速度场生成样本
    z = torch.randn(n, 2)  # 从标准正态分布初始化
    dt = 1.0 / steps
    for i in range(steps):
        t = torch.full((n, 1), i * dt)  # 当前时间步 t_i
        z = z + dt * model(z, t)  # 显式欧拉法一步更新
    return z

samples = sample_flow_matching(model, n=3000)
print(f"共生成 {samples.shape[0]} 个样本,均值 ≈ {samples.mean(0).numpy().round(3)}")

对比一下两者的简洁性:FFJORD 训练时必须嵌入常微分方程(ODE)求解器、依赖 Hutchinson 迹估计技巧,并需反复调试数值容差;而流匹配本质上就是一次标准回归任务——采样噪声、采样数据、线性插值、预测瞬时速度。ODE 求解器仅在推理阶段才被调用,且此时使用最朴素的欧拉法(50–100 步)就已足够稳健高效。

维度FFJORD(连续归一化流,CNF)流匹配(Flow Matching)
训练目标通过 ODE 最大化似然对速度场进行回归拟合
单步训练开销ODE 求解 + Hutchinson 迹估计一次前向传播
训练阶段是否需要 ODE 求解器?
推理阶段是否需要 ODE 求解器?是(但要求更低,欧拉法即可)
收敛速度较慢(通常需 8000+ 步)较快(约 3000 步即可收敛)
数值稳定性对容差设置高度敏感鲁棒性强,不易发散

“连续深度”的直观图景#

“连续深度”是贯穿本章的核心思想:Neural ODE 是深度网络的连续极限,而 CNF 则是归一化流的连续极限。两者背后的图像完全一致。

连续轨迹 h(t) 分别由固定深度的 ResNet 和自适应 ODE 求解器近似。
图 6:蓝色曲线是底层 Neural ODE 生成的真实连续轨迹 $h(t)$ ;红色虚线是固定深度 $L=4$ 的 ResNet,在 $|\dot h|$ 较大的区域明显欠拟合(红色误差区域);橙色曲线($L=8$ )仍无法捕捉高频振荡;紫色菱形是自适应求解器的评估点——在动力学剧烈处密集采样,在平滑处稀疏采样,以更少的总评估次数达到相同精度,且无需手动调整“深度”。

这也解释了为何一个 ODE 函数 $f_\theta$ 能替代深 ResNet 中的数百层:时间变量取代了层索引的角色,而离散化策略则交由求解器自动决定。

增广神经微分方程(Augmented Neural ODEs)#

标准的定义在 $\mathbb{R}^d$ 上的神经微分方程(Neural ODE)无法改变数据的拓扑结构——连通区域始终保持连通,两个彼此分离的簇也无法被合并。其根本原因在于:ODE 生成的流是一个同胚映射(homeomorphism),即连续的双射。在实际建模中,这意味着一个二维 Neural ODE 无法将单个高斯分布映射为两个彼此分离的簇——因为要实现这种分裂,不同初始点的轨迹必然发生交叉,从而违反解的唯一性定理。

解决方案:增广(augmentation)。
我们将状态空间从 $\mathbb{R}^d$ 提升至 $\mathbb{R}^{d+k}$ ,方法是向原始状态向量末尾追加 $k$ 个额外维度,并将其初始化为零:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def augmented_neural_ode(func, x, k=1, steps=100):
    # 增广:拼接 k 个零向量
    aug = torch.zeros(x.shape[0], k)
    h = torch.cat([x, aug], dim=-1)  # 形状为 (batch_size, d+k)
    # 在增广空间中进行数值积分
    dt = 1.0 / steps
    for step in range(steps):
        t = step * dt
        h = h + dt * func(torch.tensor(t), h)
    # 投影回原始的 d 维空间
    return h[:, :x.shape[-1]]

在更高维的 $\mathbb{R}^{d+k}$ 空间中,动力学系统拥有了“抬升”一个簇、使其越过另一个簇、再“放下”的自由度——这就像高速公路上的立交桥(overpass)。虽然该方法需额外消耗 $k$ 个维度,却能彻底解除拓扑限制。Dupont 等人(2019)指出:对大多数二维问题,仅需 $k = 1$ 即可满足需求;而对于一般的 $d$ 维数据,取 $k = d$ 可提供最大的建模灵活性。

整合全流程:二维密度估计#

为使整个流程更具体,我们在经典的“双月牙”玩具数据集上展示端到端的密度估计过程。

二维玩具数据上的密度估计:目标样本、经验 KDE、CNF 拟合密度、生成样本与 ODE 轨迹。
图 7:(a) 4000 个来自“双月牙”目标分布的样本;(b) 通过 KDE 得到的经验密度;(c) 连续流学到的密度——通过将高斯分布沿训练好的 $v_\theta$ 正向输运得到;(d) 紫色为生成样本,绿色为若干条 ODE 轨迹,展示基点如何从单位高斯分布(绿点)流向目标月牙。同一个网络 $v_\theta$ 同时用于密度评估(从 $\mathbf{x}$ 反向积分)和采样(从噪声正向积分)。

这种双重能力——既能进行精确似然的密度估计,又能高效采样——仅依赖一个网络 $v_\theta$ 和一个 ODE $\dot{\mathbf{z}} = v_\theta(\mathbf{z}, t)$ ,正是连续流在理论上极具吸引力的原因。

实验#

Neural ODE螺旋轨迹拟合与学习到的向量场

螺旋 ODE 拟合#

使用一个 3 层 MLP(隐藏维 64,tanh 激活)参数化 $f_\theta$ ,通过伴随方法在二维阻尼螺旋目标上训练,求解器为 dopri5(rtol $=10^{-5}$ )。经过 1000 步训练后,平均轨迹误差降至 $<10^{-3}$ ,峰值 GPU 内存约为 40 MB,与内部约 80 步的求解过程无关。

高斯 → 双月牙 CNF#

采用 4 层 MLP(隐藏维 128,softplus 激活),按 FFJORD 方式训练,使用 Hutchinson 迹估计和 dopri5 求解器,共训练 5000 步。生成样本完整覆盖两个月牙,并准确捕捉其弯月状厚度;与目标分布的 KDE 对比显示,Wasserstein-2 距离约为 0.07。

伴随方法 vs 标准反向传播(数据源自 Neural ODE 原文,外推至 1024 维隐藏状态)#

方法内存 (MB)时间 (s)测试准确率
标准反向传播,固定 $L=100$24502.385.2%
伴随方法,固定 $L=100$3203.185.1%
伴随方法,自适应(dopri5)3102.885.3%

内存开销降低约 87%,而实际运行时间仅增加 20–30%。

Flow Matching vs CNF(双月牙数据)#

方法样本质量(越低越好)收敛所需迭代数采样时间
CNF (FFJORD)12.380002.1 s / 1k 样本
Flow Matching8.730001.8 s / 1k 样本

Flow Matching 收敛速度约为 CNF 的 2.7 倍,且生成的月牙形状更清晰。在真实图像数据上,这一差距更为显著——训练时间和采样所需的函数评估次数(NFE)相差一到两个数量级。

练习题#

习题 1: 从连续性方程直接推导瞬时变量替换公式 (1)。

。连续性方程:$\partial_t\rho + \nabla\!\cdot(\rho f) = 0$ ,即 $\partial_t\rho + f\!\cdot\!\nabla\rho + \rho\,\nabla\!\cdot f = 0$ 。沿轨迹 $\mathbf{z}(t)$ ,有 $\frac{d}{dt}\rho(\mathbf{z}(t), t) = \partial_t\rho + f\!\cdot\!\nabla\rho = -\rho\,\nabla\!\cdot f$ 。两边同除以 $\rho$ 即得结果。

习题 2: 为何伴随方法的内存开销为 $O(1)$

。它仅需存储当前的 $\mathbf{h}(t)$$\mathbf{a}(t)$ 以及参数梯度的累加器。当反向求解器需要历史状态 $\mathbf{h}(s)$ 时,会通过反向积分前向 ODE 重新生成,而非预先存储。这里的 $O(1)$ 指与深度无关,空间维度 $d$ 的开销依然存在。

习题 3: 证明 Hutchinson 估计器 (6) 是无偏的。

。对任意矩阵 $A$ 和满足 $\mathbb{E}[\boldsymbol\epsilon] = 0$$\mathrm{Cov}[\boldsymbol\epsilon] = \mathbf{I}$ 的随机向量 $\boldsymbol\epsilon$ ,有 $\mathbb{E}[\boldsymbol\epsilon^\top A\,\boldsymbol\epsilon] = \sum_{i,j} A_{ij}\,\mathbb{E}[\epsilon_i\epsilon_j] = \sum_i A_{ii} = \mathrm{tr}\,A$

习题 4: 从高层面对比 Flow Matching 与 DDPM。

。两者都实现从噪声到数据的变换。DDPM 在随机前向 SDE 加噪过程中通过 score matching 学习去噪器,采样时需求解反向 SDE 或其对应的概率流 ODE;Flow Matching 则在确定性 ODE 上学习速度场 $v_\theta$ ,匹配预设的条件路径,采样只需对该 ODE 积分。Flow Matching 的训练损失是纯回归,无需设计时变噪声调度。

习题 5: 证明对线性条件路径 $\mathbf{z}_t = (1-t)\mathbf{z}_0 + t\mathbf{z}_1$ ,边缘速度 $\mathbb{E}[\mathbf{z}_1 - \mathbf{z}_0 \mid \mathbf{z}_t]$ 通过连续性方程将 $p_0$ 推至 $p_1$

解(要点)。写出 $\rho_t(\mathbf{z}) = \int q(\mathbf{z}_0, \mathbf{z}_1)\,\delta(\mathbf{z} - (1-t)\mathbf{z}_0 - t\mathbf{z}_1)\,d\mathbf{z}_0\,d\mathbf{z}_1$ 。对 $t$ 求导,并利用恒等式 $\partial_t\delta = -\nabla\!\cdot[(\mathbf{z}_1 - \mathbf{z}_0)\delta]$ 。在给定 $\mathbf{z}_t$ 的条件下对 $\mathbf{z}_0, \mathbf{z}_1$ 边缘化,可得 $\partial_t\rho_t + \nabla\!\cdot(\rho_t\,\bar v_t) = 0$ ,其中 $\bar v_t(\mathbf{z}) = \mathbb{E}[\mathbf{z}_1 - \mathbf{z}_0 \mid \mathbf{z}_t = \mathbf{z}]$

下一步#

本章给的几个核心想法(PDE 残差作为损失、函数空间上的算子、Wasserstein 几何、辛结构、score、扩散)在后面的章节里会反复出现。如果某一段卡住了,建议先记下问题继续往下读——下一章往往会从另一个角度把它再讲一遍。

如果你想验证自己读懂了,最直接的练习是把本章的方程在一个最小例子上跑通:一维热方程、单摆、二维高斯混合。代码不长,但能让公式从“看着对”变成“在我电脑上确实是对的”。

参考文献#

[1] Chen, R. T. Q., Rubanova, Y., Bettencourt, J., & Duvenaud, D. (2018). Neural ordinary differential equations. NeurIPS.

[2] Grathwohl, W., Chen, R. T. Q., Bettencourt, J., Sutskever, I., & Duvenaud, D. (2018). FFJORD: Free-form continuous dynamics for scalable reversible generative models. ICLR.

[3] Onken, D., Fung, S. W., Li, X., & Ruthotto, L. (2021). OT-Flow: Fast and accurate continuous normalizing flows via optimal transport. AAAI.

[4] Lipman, Y., Chen, R. T. Q., Ben-Hamu, H., Nickel, M., & Le, M. (2022). Flow matching for generative modeling. ICLR.

[5] Liu, X., Gong, C., & Liu, Q. (2022). Flow straight and fast: Learning to generate and transfer data with rectified flow. ICLR.

[6] Tzen, B., & Raginsky, M. (2019). Theoretical guarantees for sampling and inference in generative models with latent diffusions. COLT.

[7] Zhang, H., Gao, X., Unterman, J., & Arodz, T. (2020). Approximation capabilities of neural ODEs and invertible residual networks. ICML.


本文是 PDE 与机器学习 系列的第 6 篇。下一篇:第 7 篇 —— 扩散模型 。上一篇:第 5 篇 —— 辛几何

本系列

PDE × 机器学习 8 篇

  1. 01 偏微分方程与机器学习(一):物理信息神经网络
  2. 02 偏微分方程与机器学习(二):神经算子理论
  3. 03 偏微分方程与机器学习(三):变分原理与优化
  4. 04 偏微分方程与机器学习(四):变分推断与 Fokker-Planck 方程
  5. 05 偏微分方程与机器学习(五):辛几何与保结构网络
  6. 06 偏微分方程与机器学习(六):连续归一化流与 Neural ODE 当前
  7. 07 偏微分方程与机器学习(七):扩散模型与 Score Matching
  8. 08 偏微分方程与机器学习(八):反应扩散系统与 GNN

读有所得?

GitHub 关注我 → 新文周更

GitHub