一句话理解 Flow Matching

Flow Matching 核心概念:从噪声到数据的直线路径 —— ODE 速度场学习

Flow Matching vs Diffusion 路径对比:OT 路径直(左)、扩散路径弯(右)

上图对比了 Flow Matching(OT 路径)和 Diffusion(VP/VE 路径)的生成轨迹。Flow Matching 的直线路径意味着更少的采样步数和更稳定的训练。

Flow Matching = 学一个"风场",让随机噪声顺着风飘到数据样本的位置。

扩散模型(Diffusion)是先加噪再去噪,路径弯弯绕绕;Flow Matching(流匹配)则是直接学一条把噪声"搬"到数据的直线路径。路径越直,跑完这条路需要的步数就越少,训练也就越稳。这就是它近两年在图像生成、动作生成、轨迹规划里快速取代扩散头的根本原因。

形式化地说,Flow Matching 学习的是一个速度场 $v_\theta(x,t)$,它告诉你在时刻 $t$、位置 $x$ 处,应该沿哪个方向、以多快的速度移动,才能从噪声分布 $p_0$(通常是高斯)“流"到目标数据分布 $p_1$。


🔄 从 Diffusion 说起:为什么需要 Flow Matching

要理解 Flow Matching,先得明白扩散模型"慢"在哪里。

扩散模型的两条路径:

  • 前向过程:把一张干净图像逐步加高斯噪声,最后变成纯噪声。这是一个固定的、不学习的马尔可夫链。
  • 逆向过程:训练一个网络 $v_\theta$(或预测噪声 $\epsilon_\theta$)去反转这个过程,从纯噪声一步步去噪回图像。

Flow Matching 生成轨迹示例:噪声粒子沿直线路径逐步走向数据分布

问题出在哪? 扩散前向过程的轨迹是弯曲的——它对应一个非线性时变的随机微分方程(SDE)。逆向去噪必须用很小的步长才能跟上这条弯路,所以经典 DDPM 要采样 1000 步,DDIM 也要 20–50 步。即便后续有 DPM-Solver、一致性模型(Consistency Model)等各种加速技巧,弯曲路径这个根因始终没消除。

Flow Matching 的洞察: 既然路径弯曲是病根,那为什么不直接构造一条直的路径?一条直线只需要学一个平滑的速度场,推理时用大步长的 ODE solver,几步甚至一步就能走完。这就是 Flow Matching 的核心动机——它不是对扩散的修补,而是从更一般的视角重新定义生成过程,扩散只是它的一个特例。


📐 数学直觉:向量场、ODE 与"流”

这一节是 Flow Matching 的数学核心,但我们会用最直白的方式讲。

什么是向量场?

想象一张地图,每个城市上空都插着一个小箭头,告诉你"如果从这里出发,该往哪个方向、以多大速度走"。这些布满空间的箭头集合,就叫向量场

在 Flow Matching 里,向量场 $v(x,t)$ 是依赖于时间 $t$ 的:同一个位置,在 $t=0$ 和 $t=0.5$ 的箭头方向可能不同。它的物理含义是——“流"在演化过程中,风向是会变的。

什么是 ODE 与"流”?

有了向量场,我们就能写出一个常微分方程(ODE),描述一个粒子如何随时间运动:

$$\frac{dx}{dt} = v_\theta(x,t), \quad x(0) \sim \mathcal{N}(0,I)$$

这条公式的意思是:“粒子在每一瞬间的速度,等于向量场在当前位置和当前时刻的取值”。从 $t=0$ 的噪声点出发,顺着这个 ODE 一路积分到 $t=1$,粒子到达的位置就构成一个样本。这个由向量场定义的、把分布从 $p_0$ 推到 $p_1$ 的演化过程,就叫流(Flow)

用最通俗的话说: 训练阶段,网络在学"风怎么吹";推理阶段,随机撒一把噪声粒子,让它们顺着风飘,飘到 $t=1$ 时聚集成的形状就是数据分布。

Flow Matching 的训练目标

核心问题是:怎么训练这个向量场 $v_\theta$? Flow Matching 的答案是——匹配。构造一个"理想的风场" $u_t(x)$(它把噪声沿直线路径推到数据),然后让网络去逼近它:

$$\mathcal{L}_{FM}(\theta) = \mathbb{E}_{t,\,x_t}\left[\,\big\|\,v_\theta(x_t,t) - u_t(x_t)\,\big\|^2\,\right]$$

其中:

  • $t \sim \mathcal{U}(0,1)$ 是随机采样的时刻
  • $x_t = (1-t)\,x_0 + t\,x_1$ 是噪声 $x_0$ 和数据 $x_1$ 之间的线性插值(这就是"直线路径"的来源)
  • $u_t(x_t) = x_1 - x_0$ 是沿这条直线的恒定速度

Flow Matching 的线性插值路径:噪声 x₀ 到数据 x₁ 的直线概率路径

换句话说,理想的"风向"就是"从当前插值点指向数据点"的方向,网络只要学会预测这个方向即可。这个形式简单到令人惊讶——没有马尔可夫链,没有 SDE,没有繁琐的噪声调度,就是一个回归。


🆚 Flow Matching vs Diffusion

两者都属于基于分数/速度场的生成模型,但建模哲学截然不同。

维度扩散模型(Diffusion)Flow Matching
前向路径形状弯曲(非线性噪声调度)直线(线性插值)
底层方程SDE(含随机项)ODE(确定性)
采样步数DDPM 1000 步,DDIM 20–50 步几步到十几步即可
训练目标预测噪声 $\epsilon$ 或分数 $\nabla\log p$预测速度场 $v$
路径可设计性固定的加噪过程任意可设计(条件流)
理论统一性Flow Matching 的一个特例更一般的框架
训练稳定性一般通常更稳(目标更平滑)
多模态支持✅(天然支持)

三个关键差异的深入解读:

  • 路径直 vs 弯 是最本质的差别。直线路径意味着 ODE 的曲率小,数值积分可以用大步长——这就是 Flow Matching 能用 5–10 步采样的根本原因,而 Diffusion 想做到同样质量往往要更多步。
  • 确定性 vs 随机性 也带来工程影响。Flow Matching 的推理是一条确定性 ODE,容易缓存、容易蒸馏(把多步蒸馏成一步);扩散的 SDE 有随机项,控制起来更复杂。
  • 统一视角 在理论上很优雅:当 Flow Matching 的路径取特定的非线性形式时,它就退化为扩散模型。所以学界常说"Diffusion 是 Flow Matching 的特例"。

💡 一句话记忆:Diffusion 是"弯路慢走",Flow Matching 是"直路快跑"。


🔀 主要变体:OT-CFM 等

Flow Matching 的一个强大之处是路径和边际分布可以自由设计,由此衍生出多个变体。下面从数学直觉和工程意义两个维度,逐一拆解每个变体。

FM(原始 Flow Matching)——理论奠基

原始 FM 的直接目标是学习一个边缘向量场 $u_t(x)$,使得沿 $u_t$ 积分能实现从噪声分布 $p_0$ 到数据分布 $p_1$ 的"流"。它的损失函数:

$$\mathcal{L}_{FM}(\theta) = \mathbb{E}_{t \sim \mathcal{U}(0,1),\, x \sim p_t} \left[ \|v_\theta(x,t) - u_t(x)\|^2 \right]$$

问题:$p_t$ 和 $u_t$ 的真实边缘分布是未知的(我们只知道数据样本 $x_1$,不知道所有 $x_t$ 的分布)。所以 FM 只是理论框架,不能直接训练。

CFM(Conditional Flow Matching)——可训练的真正起点

CFM 解决了 FM “不可训练"的问题。核心思想:不直接构造边缘路径,而是以单个数据点 $x_1$ 为条件构造直线路径。对于给定的数据点 $x_1$,条件路径定义为:

$$x_t = (1-t)\,x_0 + t\,x_1, \quad u_t(x|x_1) = x_1 - x_0$$

其中 $x_0 \sim \mathcal{N}(0,I)$ 是随机噪声。对应的条件损失:

$$\mathcal{L}_{CFM}(\theta) = \mathbb{E}_{t,\,x_0,\,x_1} \left[ \|v_\theta(x_t,t) - (x_1 - x_0)\|^2 \right]$$

关键洞察:虽然条件路径是以单个 $x_1$ 为条件定义的,但对所有数据点取期望后,条件路径的期望恰好等于原始 FM 想要的边缘路径

$$\mathbb{E}_{x_1}[\,u_t(x|x_1)\,] = u_t(x)$$

这就是 CFM 名字中"Conditional"的来源——以数据点为条件构造路径,但期望效果等价于原始 FM。CFM 的出现让 Flow Matching 从理论优雅变成了真正可训练的算法。所有后续变体都是以 CFM 为基础构建的。

OT-CFM(Optimal Transport CFM)——最常用的变体

CFM 中噪声 $x_0$ 和数据 $x_1$ 是随机独立配对的。这意味着噪声点可能需要穿过其他数据点才能到达目标位置,路径不是最优的。OT-CFM 的改进:用最优传输将 $x_0$ 和 $x_1$ 配对——找一个一一映射 $\pi$,使得 $\sum \|x_0 - \pi(x_0)\|^2$ 最小。直观理解:

  • CFM(随机配对):100 个人在 A 点,100 辆出租车在 B 点,随机上车 → 有人要走很远才能到车的位置
  • OT-CFM(最优传输):100 个人在 A 点,100 辆出租车在 B 点,全局调度让第 $i$ 近的人上第 $i$ 近的车 → 所有人的平均路程最短

配对后 $x_0$ 和 $x_1$ 之间的路径更短更直,训练时梯度方向更一致,收敛更快。在实际项目中,OT-CFM 是默认选择(Stable Diffusion 3、Bagel 都使用它)。

Rectified Flow(整流流)——追求一步生成

Rectified Flow 走了一条不同且极具实用价值的路。它的核心是一个 “拉直 → 重训 → 再拉直” 的迭代过程:

  1. 训练:用 CFM 训练一个流量模型 $v_\theta^{(1)}$
  2. 整流(Rectify):用训练好的模型生成一批配对 $(x_0, x_1)$——噪声 $x_0$ 沿学到的路径走到终点 $\hat{x}_1$。用这些新配对 $(x_0, \hat{x}_1)$ 取代原始数据配对
  3. 重训:用整流后的配对重新训练一个模型 $v_\theta^{(2)}$
  4. 迭代:重复上述过程,每次整流后路径都更接近直线

为什么这有效?第一次训练时路径是直的(线性插值),但模型预测有误差,实际轨迹略有弯曲。整流后的新配对把"起点"和"终点"直接连起来,第二次训练的路径就比第一次更直。迭代几次后,路径接近完美直线,模型可以做到一步生成(从 $x_0$ 直接跳到 $x_1$,不需要中间步)。

Rectified Flow 的工程价值巨大:一步生成意味着推理延迟从"多步迭代"降到"单次前向”,对实时性敏感的应用(自动驾驶、机器人控制)意义非凡。

Stochastic FM(随机流匹配)——在 ODE 中加回随机性

纯 ODE 的 Flow Matching 是确定性的——同一个噪声必然生成同一个结果。这会损失样本多样性。Stochastic FM 在 ODE 的每一步注入适量噪声:

$$\mathrm{d}x = v_\theta(x,t)\,\mathrm{d}t + \sigma(t)\,\mathrm{d}w$$

其中 $\mathrm{d}w$ 是维纳过程增量,$\sigma(t)$ 控制噪声强度。这实际上把 ODE 重新变成了 SDE,兼顾了:

  • 确定性近似($\sigma=0$):快速稳定的生成
  • 随机采样($\sigma>0$):多样化的生成结果,避免 mode collapse

在 Flow-GRPO 的推理代码中,noise_level 参数就控制这个 $\sigma(t)$:noise_level=0 退化为纯 ODE,noise_level=0.8 是常见的 SDE 折中值

变体选择指南

变体选型场景核心优缺点
CFM教学、最小实现最简单的可训练形式,但路径非最优
OT-CFM生产环境(默认推荐)训练最快最稳,路径最短
Rectified Flow需要超快推理(一步生成)迭代训练开销大,但推理极快
Stochastic FM追求样本多样性牺牲部分速度换取多样性

实践建议:先用 OT-CFM 快速出效果;推理延迟要求苛刻时升级到 Rectified Flow;需要探索多样性时开启 Stochastic FM 的噪声注入。


💻 真实项目代码讲解:Bagel / Flow-GRPO

下面所有代码节选自 ByteDance-Seed/BAGEL(一个全模态理解+生成的 VLM)以及 Flow-GRPO 扩展。这是目前最完整的将 Flow Matching 融入 LLM 做生成的工业级实现,比伪代码有营养得多。

模型架构概览

Bagel 模型是一个"通才"——它用一个 Qwen2 LLM 做骨干,同时支持视觉理解(SIGLIP ViT 编码图像)和视觉生成(Flow Matching 解码图像)。生成部分的核心组件:

Bagel 模型架构:Flow Matching 生成流程

关键配置(BagelConfig):

class BagelConfig(PretrainedConfig):
    def __init__(
        self,
        visual_gen=True,        # 开启 Flow Matching 生成
        visual_und=True,        # 开启视觉理解
        llm_config=None,        # Qwen2 config
        vit_config=None,        # SIGLIP config
        vae_config=None,        # VAE config(输出 latent)
        latent_patch_size=2,    # 每个 latent patch 的尺寸
        max_latent_size=32,     # 最大 latent 网格
        timestep_shift=1.0,     # timestep 偏移(推理时常用 3.0)
        ...
    )

训练阶段:真正的 Flow Matching 前向

训练时,模型接收 VAE 编码后的 latent、文本 token、以及随机 timestep,经过 LLM 后预测速度场。代码如下(Bagel.forward 视觉生成部分):

# -----------------------------------------------------------
# Step 1: 从 VAE latent 构造干净的 packed_latent
# -----------------------------------------------------------
p = self.latent_patch_size          # 通常为 2
packed_latent = []
for latent, (h, w) in zip(padded_latent, patchified_vae_latent_shapes):
    # VAE latent shape: [C, H, W] → reshape to patches
    # 把 latent 切成 p×p 的 patch,每个 patch 展平成向量
    latent = latent[:, :h * p, :w * p].reshape(self.latent_channel, h, p, w, p)
    latent = torch.einsum("chpwq->hwpqc", latent).reshape(-1, p * p * self.latent_channel)
    packed_latent.append(latent)
packed_latent_clean = torch.cat(packed_latent, dim=0)

# -----------------------------------------------------------
# Step 2: 采样噪声 + 线性插值(CFM 核心)
# -----------------------------------------------------------
noise = torch.randn_like(packed_latent_clean)          # x₀: 高斯噪声
packed_timesteps = torch.sigmoid(packed_timesteps)     # t: 0→1 的 timestep
# timestep shift: 让模型在 t 接近 0 时"看得更细"
packed_timesteps = self.timestep_shift * packed_timesteps / \
                   (1 + (self.timestep_shift - 1) * packed_timesteps)
# x_t = (1-t) * x₀ + t * x₁  直线插值
packed_latent = (1 - packed_timesteps[:, None]) * packed_latent_clean \
                + packed_timesteps[:, None] * noise

# -----------------------------------------------------------
# Step 3: 把 latent patch 映射到 LLM 隐空间 + 加 timestep/位置编码
# -----------------------------------------------------------
packed_timestep_embeds = self.time_embedder(packed_timesteps)
latent_token_pos_emb = self.latent_pos_embed(packed_latent_position_ids)
# vae2llm: Linear 把 patch 投影到 hidden_size
packed_latent = self.vae2llm(packed_latent) \
                + packed_timestep_embeds + latent_token_pos_emb
# 插入到 packed_sequence 的对应位置(与文本 token 拼接)
packed_sequence[packed_vae_token_indexes] = packed_latent

# -----------------------------------------------------------
# Step 4: 过 LLM backbone,得到隐状态
# -----------------------------------------------------------
last_hidden_state = self.language_model(packed_sequence=packed_sequence, ...)

# -----------------------------------------------------------
# Step 5: 预测速度场 + MSE loss
# -----------------------------------------------------------
packed_mse_preds = self.llm2vae(last_hidden_state[mse_loss_indexes])
# 注意这里的 target 方向:x₁ - x₀  即"从数据指向噪声"
# (与论文中 u_t = x₁ - x₀ 一致,符号取决于路径定义方向)
target = noise - packed_latent_clean
has_mse = packed_timesteps > 0
mse = (packed_mse_preds - target[has_mse]) ** 2

和伪代码的区别: 真实代码里多了timestep shift(把 timestep 重新分布)和patchify(把 dense latent 切成 patch 序列以适配 LLM 的 token 输入格式)。核心逻辑 (1-t)*x₀ + t*x₁ 和 MSE loss 是完全一致的。

Timestep 编码:正弦位置嵌入

Flow Matching 的 $t$ 是连续浮点数,需要编码成向量才能送入网络。真实实现使用了 DiT 式正弦编码:

class TimestepEmbedder(nn.Module):
    def __init__(self, hidden_size, frequency_embedding_size=256):
        super().__init__()
        self.mlp = nn.Sequential(
            nn.Linear(frequency_embedding_size, hidden_size),
            nn.SiLU(),
            nn.Linear(hidden_size, hidden_size),
        )

    @staticmethod
    def timestep_embedding(t, dim, max_period=10000):
        half = dim // 2
        freqs = torch.exp(
            -math.log(max_period) *
            torch.arange(start=0, end=half, dtype=torch.float32) / half
        ).to(device=t.device)
        args = t[:, None].float() * freqs[None]
        embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
        return embedding

    def forward(self, t):
        t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
        t_emb = self.mlp(t_freq)          # 256 → hidden_size
        return t_emb

timestep_shift 是一个容易被忽视但很重要的技巧。原始的 timestep $t$ 均匀分布在 $[0,1]$,但实际生成时 $t$ 靠近 0 的步(细节塑造)比靠近 1 的步(轮廓布局)需要更多分辨率。shift 公式 shift * t / (1 + (shift-1) * t)把更多步压缩到细节区域shift=3.0 是常见的经验值。

推理阶段(1):ODE 求解 + SDE 噪声注入

Flow Matching 推理的金标准不是纯 ODE 而是带噪声注入的 SDE——每一步在预测方向上加适量随机性,兼顾确定性和多样性。核心 step 实现:

def _sde_step_with_logprob(self, model_output, timestep, prev_timestep,
                           d_timestep, sample, prev_sample=None,
                           noise_level=0.8):
    model_output = model_output.float()
    sample = sample.float()

    # 计算当前步的噪声标准差
    # std_dev_t = sqrt(t / (1 - t')) * noise_level
    # 其中 t' = max(sigma_max, t) 防止除零
    std_dev_t = torch.sqrt(
        timestep / (1 - torch.where(timestep == 1, sigma_max, timestep))
    ) * noise_level

    # 预测下一步均值(Euler 步 + 噪声修正项)
    #   x_{t-1} = x_t*(1 + σ²/(2t)*dt) + v_t*(1 + σ²*(1-t)/(2t))*dt
    # 当 noise_level=0 时退化为纯 Euler: x_{t-1} = x_t + v_t * dt
    prev_sample_mean = (
        sample * (1 + std_dev_t**2 / (2 * timestep) * d_timestep)
        + model_output * (1 + std_dev_t**2 * (1 - timestep) / (2 * timestep)) * d_timestep
    )

    if prev_sample is None:
        variance_noise = randn_tensor(model_output.shape, ...)
        prev_sample = prev_sample_mean + std_dev_t * torch.sqrt(-d_timestep) * variance_noise

    # log_prob 用于 RL 策略梯度(Flow-GRPO 需要)
    log_prob = -((prev_sample.detach() - prev_sample_mean) ** 2) \
               / (2 * (std_dev_t * torch.sqrt(-d_timestep))**2)
    log_prob = log_prob.mean()

    return prev_sample, log_prob, prev_sample_mean, std_dev_t

理解这个代码的三个层次:

  • noise_level=0 → 纯 Euler ODE,$x_{t-1} = x_t + v_t \cdot dt$
  • noise_level>0 → 加噪声的 SDE,$x_{t-1} = \text{mean} + \sigma \cdot \epsilon$
  • log_prob 计算了每一步 transition 的高斯 log-probability,这是 Flow-GRPO 能做 RL 的关键——没有它就没法算策略梯度

推理阶段(2):完整采样循环 + CFG

完整的采样循环从 $t=1$ 的纯噪声积分到 $t=0$ 的生成结果,中间可以选择性使用 Classifier-Free Guidance (CFG) 提升条件控制强度。

def generate_image(self, ...):
    x_t = packed_init_noises                      # 起点:纯噪声

    # 准备离散 timestep 序列(从 1 → 0)
    timesteps = torch.linspace(1, 0, num_timesteps)  # 例如 50 步
    timesteps = timestep_shift * timesteps / \
                (1 + (timestep_shift - 1) * timesteps)
    dts = timesteps[1:] - timesteps[:-1]

    for i, t in enumerate(timesteps[:-1]):
        # -----------------------------------------------------------
        # 预测速度场(含可选的 CFG)
        # -----------------------------------------------------------
        v_t = self._forward_flow(
            x_t=x_t, timestep=torch.tensor([t]),
            cfg_text_scale=cfg_text_scale,   # 文本 CFG 强度
            cfg_img_scale=cfg_img_scale,     # 图像 CFG 强度
            ...
        )
        # -----------------------------------------------------------
        # SDE step(含可选的噪声注入)
        # -----------------------------------------------------------
        x_t, log_prob, _, _ = self._sde_step_with_logprob(
            v_t, timesteps[i], timesteps[i+1], dts[i], x_t,
            noise_level=cur_noise_level
        )

    return x_t  # 生成结果

CFG 在 _forward_flow 中实现(简化版):

def _forward_flow(self, x_t, timestep, ..., cfg_text_scale, cfg_img_scale):
    # 1) 条件前向(有 text/image 条件)
    v_t = self.llm2vae(llm_output)

    # 2) 无条件前向(text CFG:用空文本做条件)
    if cfg_text_scale > 1.0:
        cfg_text_v_t = self.llm2vae(cfg_text_llm_output)

    # 3) 图像无条件前向(image CFG)
    if cfg_img_scale > 1.0:
        cfg_img_v_t = self.llm2vae(cfg_img_llm_output)

    # 4) CFG 插值 + renormalization(防止 CFG 导致向量范数爆炸)
    #    v = v_uncond + scale * (v_cond - v_uncond)
    v_t = cfg_text_v_t + cfg_text_scale * (v_t - cfg_text_v_t)
    v_t = v_t * (norm(v_t) / norm(v_t_))  # CFG-Renorm 保持速度尺度

    return v_t

CFG 的核心: 同时推理有条件和无条件两个分支,然后外推 v = v_uncond + s * (v_cond - v_uncond)。$s>1$ 时模型会更"卖力"地满足条件,但同时速度场范数会膨胀,所以需要 renormalization 把速度尺度拉回合理范围。

推理超参数速查

参数作用典型值
num_timesteps采样步数10–50(步数越少越快,但质量下降)
cfg_text_scale文本条件强度4.0–8.0(1.0 = 关闭)
cfg_img_scale图像条件强度(编辑/图生图)1.0–2.0
cfg_intervalCFG 生效的 t 范围[0, 1] 全程或 [0.4, 1] 后半段
timestep_shift步数分布偏移3.0(t→0 细节区域分配更多步)
noise_levelSDE 噪声强度0.7–0.8(0 = 纯 ODE)

🚗 自动驾驶中的应用:多模态轨迹生成

为什么轨迹规划需要多模态?

这是理解 Flow Matching 在自动驾驶价值的起点。考虑一个场景:前方有静止障碍物,左车道空、右车道也空。 此时合理的选择有至少两种——向左变道、向右变道。如果用普通的回归头输出一条"平均轨迹",结果往往是压着障碍物正中间走(mode averaging),这比任何一个合理选择都更危险。

多模态意味着输出分布有多个峰(多个合理轨迹),模型需要能表达"这两种选择都合理",而不是强行取平均。这正是生成式模型(扩散、Flow Matching)的强项——它们输出的是一个分布,采样多次能得到不同的合理轨迹。

代表工作:DiffusionDrive 与 Flow 头规划

工作团队动作头核心特点
DiffusionDrive华中科大等扩散头把规划建模成条件去噪,输出多模态轨迹分布
CTG++CMU条件扩散用代价函数引导扩散,融合规则约束
GameFormer上交等迭代预测多智能体博弈下的层次化轨迹预测
Flow-based Planner多家Flow Matching 头用直线流路径替代扩散,采样更快

为什么 Flow Matching 在规划里比 Diffusion 更有吸引力? 答案还是速度。规划在车端是高频任务(10 Hz 以上),扩散头的多步去噪常常是延迟瓶颈;Flow Matching 的几步 ODE 采样能让规划耗时压到一个量级的提升。此外,规划对多模态可控性都有强需求——前者通过采样多个初始噪声得到多条轨迹,后者通过条件注入(自车状态、地图、障碍)控制生成。

一个具体的轨迹生成例子

假设要生成未来 3 秒的轨迹(30 个时间点 × 2 维坐标,共 60 维向量):

  1. 噪声 $x_0$:从 60 维高斯分布采样
  2. 条件 $c$:当前自车状态、HDMap、周围障碍物编码成特征
  3. 训练:让模型学速度场 $v_\theta(x_t, t, c)$,目标是把噪声沿直线推到真实轨迹 $x_1$
  4. 推理:用 5 步 Euler 法积分,得到一条候选轨迹;采样多个 $x_0$ 得到多条候选轨迹,再用代价函数(碰撞、舒适度、合规)挑最优的一条下发

整个流程清晰、高效、天然多模态。


🤖 在 VLA 中的应用:作为 Action Head

在 VLA(Vision-Language-Action)模型里,Action Head 是把 LLM 的推理结果变成连续动作的关键。Flow Matching 正在成为最强 Action Head 的代名词,代表就是 π0

π0:Flow Matching 动作头的标杆

Physical Intelligence 的 π0 把 Flow Matching 用作通用机器人的动作生成头,是目前 VLA 能力的天花板之一。它的设计要点:

  • 骨干:PaLI/Gemma 风格的 VLM 做视觉-语言理解
  • 动作头:一个Flow Matching 网络,以 VLM 的隐状态为条件,从噪声生成一段动作序列(action chunk)
  • 训练:用大规模示教数据(含跨本体、跨任务)训练,损失就是上文那个简单的 MSE 流匹配损失
  • 推理:用 ODE solver 生成一段未来 N 步动作,按需执行(action chunking)

为什么 π0 选 Flow Matching 而不是 Diffusion 或回归?

  • 比回归强:机器人动作高度多模态(同一个"把杯子放到架子上"有多种抓取和放置路径),回归会 mode averaging
  • 比扩散快:机器人控制需要 10–50 Hz,Flow Matching 的少步采样对实时性更友好
  • 比扩散稳:Flow Matching 的训练目标更平滑,大规模数据上收敛更可靠

VLA Action Head 三大流派回顾

方案精度速度多模态代表
离散化 TokenRT-2、OpenVLA
连续回归早期 SFT 头
Flow Matching较快(可优化)π0、π0.5

Flow Matching 正在成为高精度 VLA 动作头的事实标准。


🔗 与 GRPO 的结合:Flow-GRPO 的真实训练代码

Flow-GRPO 是 ByteDance Bagel 项目的一个扩展,把 Flow Matching 的图像生成和 GRPO 策略优化结合起来。我们直接从实际训练代码中拆解其核心逻辑。

核心挑战:Flow Matching 策略的 log-prob 从哪来?

GRPO 需要 $\log p_\theta(a)$ 计算 importance ratio。Flow Matching 的策略分布通过 ODE 定义,没有显式密度。但SDE 的每一步 transition 是高斯分布,因此整个轨迹的 log-prob 就是各步 log-prob 之和。

_sde_step_with_logprob 中,我们早就埋好了 log-prob 计算:

# transition: x_{t+1} ~ N(mean_t, σ_t²)
# log p(x_{t+1} | x_t, θ) = -(x_{t+1} - mean_t)² / (2σ_t²)
log_prob = -((prev_sample.detach() - prev_sample_mean) ** 2) \
           / (2 * (std_dev_t * torch.sqrt(-d_timestep))**2)
log_prob = log_prob.mean()

关键洞察:Flow Matching + SDE 的每一步 transition 恰好是高斯分布,所以 log-prob 有闭式解,不需要复杂的瞬时变量替换公式。每一步的 log_prob 累加就是整条轨迹的 $\log p_\theta(\text{轨迹})$。

Flow-GRPO 训练循环

训练时,模型先生成一组候选图像(多条轨迹),用奖励模型打分,然后做 GRPO 策略更新。

Step 1: 采样生成 + 保存中间 latent

# generate_image() 中保存每一步的 latent 和 log_prob
all_latents = []     # 每一步的 latent
all_log_probs = []   # 每一步的对数概率
all_timesteps = []   # 对应的时间步

for i, t in enumerate(timesteps):
    v_t = self._forward_flow(x_t, ...)
    x_t, log_prob, _, _ = self._sde_step_with_logprob(v_t, ...)
    # 在指定窗口内保存中间结果用于 RL 训练
    if i >= sde_timestep_begin and i < sde_timestep_begin + window_size:
        all_latents.append(x_t)
        all_log_probs.append(log_prob)
        all_timesteps.append(t)

生成完成后,把图像送入奖励模型得到标量奖励 $r$,同 prompt 的 $G$ 条轨迹做组内归一化得到 advantage $A^{(i)} = \frac{r^{(i)} - \bar{r}}{\sigma_r}$。

Step 2: 逐 timestep 做 PPO-style 策略更新

def generate_image_learn(self, sample, grpo_config, accelerator, optimizer, ...):
    latents = sample["latents"]           # 采样时保存的 latent 序列
    prev_latents = sample["prev_latents"] # 上一步的 latent
    timesteps = sample["timesteps"]
    advantages = torch.clamp(
        sample["advantages"],
        -grpo_config.train.adv_clip_max,
        grpo_config.train.adv_clip_max,
    )

    for i, t in enumerate(timesteps):
        with accelerator.accumulate(transformer):
            # 用当前策略预测速度场(online)
            v_t = self._forward_flow(x_t=latents[i], timestep=t, ...)

            # 计算 log_prob(含重参数化)
            _, log_prob, prev_sample_mean, std_dev_t = self._sde_step_with_logprob(
                v_t, timesteps[i], timesteps[i+1], dts[i],
                latents[i], prev_sample=prev_latents[i], ...
            )

            # KL 正则:对 reference model 也做一步推理
            if grpo_config.train.beta > 0:
                v_t_ref = self._forward_flow(..., ref_model=True)
                _, _, prev_sample_mean_ref, _ = self._sde_step_with_logprob(
                    v_t_ref, ...
                )

            # -----------------------------------------------------------
            # GRPO 策略梯度(核心 4 行)
            # -----------------------------------------------------------
            ratio = torch.exp(log_prob - sample["log_probs"][i])
            unclipped_loss = -advantages * ratio
            clipped_loss = -advantages * torch.clamp(
                ratio,
                1.0 - grpo_config.train.clip_range_lt,
                1.0 + grpo_config.train.clip_range_gt,
            )
            policy_loss = torch.mean(torch.maximum(unclipped_loss, clipped_loss))

            # KL 散度(高斯分布的 KL 有闭式解)
            if grpo_config.train.beta > 0:
                kl_loss = ((prev_sample_mean - prev_sample_mean_ref) ** 2).mean() \
                          / (2 * std_dev_t ** 2)
                loss = policy_loss + grpo_config.train.beta * kl_loss
            else:
                loss = policy_loss

            accelerator.backward(loss)
            optimizer.step()
            optimizer.zero_grad()

代码对应的 GRPO 公式

上面的核心 4 行代码对应 GRPO 的裁剪 surrogate 目标

$$L(\theta) = -\mathbb{E}\left[\min\left(r_t(\theta) A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) A_t\right)\right]$$

其中 $r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)} = e^{\log p_\theta - \log p_{\theta_{\text{old}}}}$,$A_t$ 是组内归一化优势。

和 PPO 的区别: GRPO 不用 critic network(价值网络),优势直接从组内奖励归一化得到

$$\hat{A}_i = \frac{r_i - \mu_\text{group}}{\sigma_\text{group}}$$

。这个简化对 Flow Matching 尤其友好——不用额外训一个价值网络去逼近连续动作空间的价值函数。

为什么 Flow + RL 特别契合自动驾驶?

契合点说明
多模态探索Flow 采样天然给出多条不同轨迹,正是 RL 探索所需的样本多样性
连续动作平滑ODE 生成的动作平滑连续,比离散 token 更适合车辆控制
奖励稀疏可处理GRPO 的组内比较把"绝对奖励"变"相对优势",缓解驾驶奖励极度稀疏的问题
世界模型协同可用世界模型做 rollout 评估,无需真实路测,安全且低成本

💡 一句话理解 Flow-GRPO:用 Flow Matching 的 SDE 采样提供多模态候选轨迹 + 高斯 log-prob 闭式解,用 GRPO 的组内归一化做免价值网络策略优化。


⚖️ 什么时候该用 Flow Matching?

场景推荐度理由
多模态动作/轨迹生成⭐⭐⭐⭐⭐天然支持多峰分布
实时性要求高的生成⭐⭐⭐⭐⭐少步采样,速度远胜扩散
单模态精确回归⭐⭐杀鸡用牛刀,普通回归头更快
离散决策(如左转/右转)适合分类,不必用生成模型
大规模图像/视频生成⭐⭐⭐⭐SD3、Meta MovieGen 都在用
高频控制(机器人/车端)⭐⭐⭐⭐⭐少步采样 + action chunking 是当前最优解

✅ 小结

记住这三个要点,就能抓住 Flow Matching 的精髓:

  1. 本质 = 学习一个速度场 $v_\theta(x,t)$,通过 ODE 把噪声分布"流"到数据分布,路径是直线
  2. 优势 = 路径直 → 采样步数少;目标平滑 → 训练稳定;理论上是扩散的更一般框架。
  3. 落地 = 在自动驾驶做多模态轨迹生成(DiffusionDrive 等)、在 VLA 做连续动作头(π0)、与 GRPO 结合做偏好对齐(Flow-GRPO)。

一句话总结:Flow Matching 把生成式建模从"弯路慢走"升级为"直路快跑",正在成为连续动作生成的事实标准,也是连接"模仿学习"和"强化学习"的关键桥梁。


📚 延伸阅读

奠基论文:

  • Flow Matching for Generative Modeling(Lipman et al., ICLR 2023)—— Flow Matching 原始论文
  • Stochastic Interpolants(Albergo & Vanden-Eijnden, 2023)—— 同期独立工作,与 FM 等价
  • Flow Straight and Fast: Rectified Flow(Liu et al., ICLR 2023)—— 整流流,路径拉直
  • Optimal Transport CFM(Tong et al., 2023)—— OT-CFM 变体

应用论文:

  • π0 / π0.5(Physical Intelligence, 2024)—— Flow Matching VLA 标杆
  • Diffusion Policy(Chi et al., 2023)—— 扩散动作头奠基,理解 FM 动作头的基础
  • DiffusionDrive(华中科大等)—— 扩散头轨迹规划
  • Stable Diffusion 3(Stability AI, 2024)—— Rectified Flow 用于图像生成

代码仓库(本文的代码来源):

  • ByteDance-Seed/BAGEL(https://github.com/ByteDance-Seed/BAGEL)—— 全模态 VLM,含 Flow Matching 图像生成的完整工业级实现
  • Flow-GRPO(https://github.com/anomalyco/Flow-GRPO)—— 在 Bagel 基础上扩展 GRPO 策略优化,支持 RL 训练

博客与教程:

  • Lily Yang 的 Flow Matching for Generative Modeling 教程(直观图解)
  • torchcfm(https://github.com/atong01/conditional-flow-matching)—— 官方 CFM 最小实现
  • HuggingFace Diffusers 库已原生支持 Flow Matching / Rectified Flow

💡 新手建议:先读 torchcfm 的最小示例(100 行就能跑通),然后在 Bagel 仓库里看实际的 bagel.py forward 函数,再回来看本文的代码讲解,会非常通透。


💡 觉得有用?这是「知识点拆解」系列的第 4 篇,后续会继续讲强化学习(GRPO)和世界模型如何与这些生成模型协同。点个关注不迷路。