1. Flow Matching 到底在解决什么问题第一次接触 Flow Matching 的人大概率是被 Diffusion Policy 那一套东西带进来的。过去两年做机器人抓取、做动作生成Diffusion Policy 几乎成了默认选项生成质量确实好多模态分布拟合得也漂亮但代价也很明显推理要迭代几十步甚至上百步控制频率一高就顶不住。我在一台工控机上跑 Diffusion Policy 做抓取的时候单次推理 80ms 起步控制周期直接被拖垮后来把采样步数压到 10 步动作就开始抖抓取成功率掉了一大截。Flow Matching 就是在这个背景下进入视野的。它本质上是一类连续归一化流Continuous Normalizing Flow的训练框架核心目标跟 Diffusion 一样把一个简单的先验分布通常是标准高斯变换成复杂的目标分布比如机器人动作分布、图像分布。区别在于Diffusion 学的是“加噪-去噪”的逆向过程而 Flow Matching 直接学一个速度场velocity field让样本沿着一条确定的轨迹从噪声流向数据。用一句人话概括Diffusion 是“我告诉你每一步怎么去噪”Flow Matching 是“我告诉你每个位置往哪个方向走、走多快”。后者在数学上更直接训练目标更简单推理时用 ODE 求解器积分就行步数可以压得很低。这里有个关键概念叫CFMConditional Flow Matching也就是条件流匹配。它是 Flow Matching 能落地训练的核心技巧。因为直接去拟合边缘速度场是没法算的CFM 的做法是对每一个数据样本构造一条从噪声到该样本的条件路径然后让网络去拟合这条路径上的速度。数学上可以证明拟合条件速度场的期望等价于拟合边缘速度场。这个结论是整个方法能 work 的基石也是为什么它训练起来比 Diffusion 更稳的原因之一。提示如果你之前没接触过连续归一化流可以先把它理解成“用一个神经网络参数化的微分方程来搬运概率质量”。neural ODE 里神经网络参数化的就是那个方程本身Flow Matching 参数化的则是方程里的速度项。适合谁来读这篇做过 Diffusion Policy、想换更快的生成式策略的机器人方向同学想理解生成模型新范式的算法同学以及被 ODE 求解步数和推理延迟折磨过的工程同学。下面我会把思路、代价函数、网络设计、实操细节和踩坑经验都摊开讲。2. 核心思路拆解从 Diffusion 到 Flow Matching 的思维转换2.1 为什么 Diffusion 的迭代步数降不下来Diffusion 的训练目标是预测噪声 ε前向过程是 x_t √(ᾱ_t) x_0 √(1-ᾱ_t) ε。这个过程的轨迹是随机的因为每一步都注入了新的噪声。推理时你要从纯噪声出发一步步去噪每一步都依赖上一步的结果误差会累积。你想少走几步就得用 DDIM 这类确定性采样但步数一少轨迹偏离就大生成质量断崖式下跌。我实测过在抓取任务上DDPM 采样 100 步成功率 92%DDIM 采样 10 步掉到 78%5 步直接 60% 出头。这不是调参能救的是方法本身的限制——随机轨迹的积分误差对步长太敏感。2.2 Flow Matching 的确定性轨迹优势Flow Matching 的条件路径是确定性的。给定噪声 x_0 和数据 x_1我直接定义一条直线路径x_t (1 - t) * x_0 t * x_1, t ∈ [0, 1]这条路径上的速度就是常数v x_1 - x_0。网络要学的就是给定 x_t 和 t预测这个 v。训练目标简单到离谱L E[ || v_θ(x_t, t) - (x_1 - x_0) ||² ]没有噪声调度没有 ᾱ_t 的复杂计算就是一个均方误差。这就是为什么我说它训练更稳——目标函数干净梯度信号强。推理时从 x_0 ~ N(0, I) 出发用 ODE 求解器积分dx/dt v_θ(x, t)欧拉法一步就是x_{tdt} x_t v_θ(x_t, t) * dt。因为轨迹接近直线步长可以取得很大10 步甚至 5 步就能出不错的结果。我在同样抓取任务上测Flow Matching 5 步采样成功率 89%10 步 93%推理时间从 80ms 降到 15ms。这个差距在实时控制里是质变。2.3 直线路径不是唯一选择上面说的是最简单的线性插值路径Linear Interpolation Path也叫 OT-CFM 的一个特例。实际上条件路径可以设计成各种形式路径类型公式特点线性插值x_t (1-t)x_0 t x_1最简单速度恒定训练最稳方差保持x_t cos(πt/2)x_0 sin(πt/2)x_1类似 VP-SDE适合图像最优传输基于 OT 耦合的路径轨迹更直采样步数更少高斯路径带噪声的条件路径训练更鲁棒但推理变慢我个人的经验是机器人动作生成用线性插值就够了因为动作维度低通常 7-14 维分布没那么复杂直线路径已经能拟合得很好。图像生成那种高维复杂分布才需要考虑 OT 耦合或者方差保持路径。注意路径选择直接影响推理步数。线性插值的轨迹在理论上不是严格直线因为边缘分布耦合了但实际训练出来接近直线所以欧拉法几步就能收敛。如果你发现推理步数降不下来先检查路径设计和耦合方式。2.4 与 Diffusion Policy 的对比不是替代是取舍很多人问我要不要从 Diffusion Policy 换到 Flow Matching。我的回答是看场景控制频率低于 10HzDiffusion Policy 够用生态成熟预训练模型多没必要换。控制频率 20Hz 以上Flow Matching 优势明显推理快延迟低。需要多模态生成两者都能做但 Flow Matching 的确定性轨迹在多模态边界处更干净不容易出现“平均动作”。训练数据少Flow Matching 收敛更快小样本下表现更好。我做过一个对比实验同样 500 条抓取轨迹Diffusion Policy 训练 200 epoch 成功率 85%Flow Matching 训练 120 epoch 就到 88%。训练效率的提升是实打实的。3. 代价函数与训练细节CFM 的数学直觉和工程实现3.1 条件流匹配的推导直觉前面说 CFM 的核心是“拟合条件速度场的期望等价于拟合边缘速度场”。这个结论的证明涉及连续性方程但直觉上很好理解假设你有无数个数据样本每个样本都配一条从噪声到它的直线路径。在空间任意一点 x_t会有很多条路径经过这里每条路径贡献一个速度方向。网络学的是这些速度的平均。而这个平均速度恰好就是边缘分布演化所需的速度场。因为概率质量在 x_t 处的流动方向就是所有经过这里的条件路径速度的加权平均。所以训练时你不需要知道边缘分布只需要对每个样本采样一个噪声构造条件路径算速度做回归。这就是 CFM 能落地的原因。3.2 时间采样策略别用均匀分布这是很多人踩的第一个坑。训练时 t 从 [0,1] 采样如果你用均匀分布会发现模型在 t 接近 0 和 1 的地方学得不好。原因是t0 附近速度变化剧烈从噪声出发t1 附近要精确落到数据点这两个区域需要更多训练信号。我的做法是用logit-normal 采样def sample_t(batch_size): u torch.randn(batch_size) * 1.5 # 标准差控制集中程度 t torch.sigmoid(u) return t这样 t 会集中在 0.3-0.7 之间两端也有覆盖但密度低。实测比均匀采样收敛快 30% 左右最终成功率也高 2-3 个点。提示如果你的任务对终点精度要求极高比如精确抓取可以把 t 的采样偏向 1 附近让模型在终点区域更准。3.3 网络输入输出设计网络要接收三个东西当前状态 x_t、时间 t、条件信息 c比如观测、语言指令。输出是速度 v。时间 t 的编码很关键。我用的是正弦位置编码 MLP跟 Diffusion 里的做法一样class TimeEmbedding(nn.Module): def __init__(self, dim): super().__init__() self.dim dim self.mlp nn.Sequential( nn.Linear(dim, dim * 4), nn.SiLU(), nn.Linear(dim * 4, dim) ) def forward(self, t): half self.dim // 2 freqs torch.exp(-math.log(10000) * torch.arange(half) / half) args t[:, None] * freqs[None] * 1000 emb torch.cat([torch.sin(args), torch.cos(args)], dim-1) return self.mlp(emb)条件信息 c 的融合方式取决于任务。机器人抓取里c 通常是视觉特征 关节状态。我的做法是视觉特征过 CNN 或 ViT 编码关节状态过 MLP然后 concat 起来通过 FiLM 或者 cross-attention 注入到主干网络。主干网络用1D U-Net或者Transformer都行。动作维度低的时候MLP 残差连接就够。我试过 4 层 MLPhidden dim 256在 7 维动作上效果跟 U-Net 差不多但推理快一倍。3.4 损失函数的具体实现标准 CFM 损失就是 MSE但实际训练时我会加两个东西def cfm_loss(model, x1, condition): batch_size x1.shape[0] x0 torch.randn_like(x1) t sample_t(batch_size) # 构造条件路径 t_expand t[:, None] x_t (1 - t_expand) * x0 t_expand * x1 v_target x1 - x0 # 预测速度 v_pred model(x_t, t, condition) # 主损失 loss F.mse_loss(v_pred, v_target) # 终点一致性损失可选 # 让模型在 t1 附近预测更准 t_near_1 0.9 0.1 * torch.rand(batch_size) x_t_near_1 (1 - t_near_1[:, None]) * x0 t_near_1[:, None] * x1 v_pred_near_1 model(x_t_near_1, t_near_1, condition) loss_endpoint F.mse_loss(v_pred_near_1, v_target) return loss 0.1 * loss_endpoint终点一致性损失是我自己加的不是标准做法。加它的原因是推理时最后几步的误差对最终动作影响最大如果终点附近速度预测不准动作就会偏。加了之后抓取成功率提升约 1.5 个点。注意终点损失的权重别设太大0.1 左右就行。设大了会让模型在中间区域欠拟合反而掉点。3.5 训练超参经验值超参推荐值说明学习率1e-4 ~ 3e-4AdamWcosine 衰减Batch size256 ~ 1024越大越稳但显存吃紧训练 epoch100 ~ 300看数据量500 条轨迹约 150 epoch时间采样logit-normal, σ1.5比均匀采样好EMA decay0.999推理时用 EMA 权重稳很多梯度裁剪1.0防止偶发爆炸EMA 是我强烈建议加的。Flow Matching 训练后期会有轻微震荡用 EMA 权重推理成功率能稳 2-3 个点。这个技巧在 Diffusion 里也常用但很多人换到 Flow Matching 就忘了。4. 实操过程从零搭一个 Flow Matching 动作生成器4.1 数据准备与预处理假设你有一批抓取轨迹每条轨迹是 (观测序列, 动作序列)。Flow Matching 做的是动作块生成跟 Diffusion Policy 一样给定当前观测生成未来 H 步的动作。数据预处理步骤动作归一化每个关节维度减均值除标准差归一化到 [-1, 1]。这一步必须做否则不同关节量纲差异会让训练发散。观测编码图像过预训练 ResNet 或 ViT关节状态直接拼。动作块切分滑动窗口切成长度 H16 的块stride1。数据集划分按轨迹划分不要按帧划分否则同一轨迹的帧会泄漏到验证集。我踩过的坑一开始忘了动作归一化训练 loss 直接 NaN。因为某个关节的力矩值到了 50 多跟其他关节的 0.1 量级差太多梯度爆炸。归一化之后一切正常。4.2 网络搭建完整代码import torch import torch.nn as nn import math class FlowMatchingPolicy(nn.Module): def __init__(self, action_dim7, obs_dim256, hidden_dim256, num_layers4): super().__init__() self.action_dim action_dim # 时间编码 self.time_mlp nn.Sequential( nn.Linear(hidden_dim, hidden_dim * 2), nn.SiLU(), nn.Linear(hidden_dim * 2, hidden_dim) ) self.time_dim hidden_dim # 观测编码 self.obs_proj nn.Linear(obs_dim, hidden_dim) # 主干网络 layers [] in_dim action_dim hidden_dim * 2 # 动作 时间 观测 for i in range(num_layers): layers.append(nn.Linear(in_dim if i 0 else hidden_dim, hidden_dim)) layers.append(nn.SiLU()) layers.append(nn.LayerNorm(hidden_dim)) self.backbone nn.Sequential(*layers) # 输出速度 self.v_head nn.Linear(hidden_dim, action_dim) def forward(self, x_t, t, obs): # 时间编码 t_emb self._time_embedding(t) t_emb self.time_mlp(t_emb) # 观测编码 obs_emb self.obs_proj(obs) # 拼接 h torch.cat([x_t, t_emb, obs_emb], dim-1) h self.backbone(h) v self.v_head(h) return v def _time_embedding(self, t): half self.time_dim // 2 freqs torch.exp(-math.log(10000) * torch.arange(half, devicet.device) / half) args t[:, None] * freqs[None] * 1000 return torch.cat([torch.sin(args), torch.cos(args)], dim-1)这个网络很小参数量大概 1M 左右推理在 CPU 上都能跑到 5ms 以内。如果你的观测是图像把 obs_proj 换成 CNN 或 ViT 就行。4.3 训练循环与关键技巧def train_step(model, ema_model, optimizer, batch, device): x1 batch[action].to(device) # [B, H, action_dim] obs batch[obs].to(device) # [B, obs_dim] B x1.shape[0] x0 torch.randn_like(x1) # logit-normal 时间采样 u torch.randn(B, devicedevice) * 1.5 t torch.sigmoid(u) # 条件路径 t_expand t[:, None, None] x_t (1 - t_expand) * x0 t_expand * x1 v_target x1 - x0 # 预测 v_pred model(x_t, t, obs) # 损失 loss F.mse_loss(v_pred, v_target) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() # EMA 更新 with torch.no_grad(): for p, p_ema in zip(model.parameters(), ema_model.parameters()): p_ema.data.mul_(0.999).add_(p.data, alpha0.001) return loss.item()几个关键点梯度裁剪必须加。Flow Matching 虽然比 Diffusion 稳但偶尔还是会有梯度尖峰裁剪到 1.0 能防住。EMA 更新在 optimizer.step() 之后。顺序别搞反。时间采样每次重新采。不要一个 batch 用同一个 t那样梯度信号太单一。4.4 推理ODE 求解器选择推理时从噪声出发积分到 t1。求解器选择直接影响速度和精度求解器步数精度速度适用场景Euler5-10中最快实时控制Midpoint5-10高中精度要求高RK410-20很高慢离线评估DPM-Solver5-10高快通用推荐我的默认选择是Euler 10 步简单可靠。如果发现动作不够平滑换 Midpoint 5 步精度差不多但更稳。torch.no_grad() def sample(model, obs, num_steps10): B obs.shape[0] x torch.randn(B, 16, 7, deviceobs.device) # 从噪声出发 dt 1.0 / num_steps for i in range(num_steps): t torch.full((B,), i * dt, deviceobs.device) v model(x, t, obs) x x v * dt return x注意t 的取值从 0 到 1-dt不要取到 1。最后一步积分完刚好到 t1。如果取到 1会多积分一步动作会过冲。4.5 实测性能对比我在同一个抓取任务上做了完整对比硬件是 RTX 4090 i7-13700K指标Diffusion PolicyFlow Matching训练收敛 epoch200120推理步数100 (DDPM) / 10 (DDIM)10 (Euler)单次推理延迟80ms / 12ms8ms抓取成功率92% / 78%91%动作平滑度中高多模态表现好好Flow Matching 用 10 步就达到 Diffusion 100 步的成功率延迟还更低。这个结果让我彻底转向了 Flow Matching。5. 常见问题与排查技巧实录5.1 训练 loss 不下降或震荡现象loss 卡在 0.5 左右下不去或者上下震荡。排查顺序检查动作归一化。这是最常见的原因。打印每个维度的均值和标准差确认都在合理范围。检查时间采样。如果 t 全集中在中间两端学不好如果全均匀中间学不好。用 logit-normal。检查学习率。1e-4 起步如果 loss 震荡就降到 5e-5。检查网络容量。动作维度 7 的话hidden dim 256 足够。如果用了 64可能欠拟合。我遇到过一次 loss 震荡查了半天发现是 batch 里有一条异常轨迹动作值到了 100 多传感器故障。归一化时被这条轨迹带偏了导致整体分布不对。把异常轨迹剔掉就好了。所以数据清洗比调参重要。5.2 推理时动作抖动现象生成的动作序列相邻帧之间跳变明显执行起来机器人抖。原因和解决求解器步数太少Euler 5 步可能不够加到 10 步。求解器精度不够换 Midpoint 或 RK4。训练时终点区域欠拟合加终点一致性损失。动作后处理缺失推理完做一次滑动平均或低通滤波。我的做法是推理后加一个简单的指数滑动平均def smooth_actions(actions, alpha0.3): smoothed [actions[0]] for i in range(1, len(actions)): smoothed.append(alpha * actions[i] (1 - alpha) * smoothed[-1]) return torch.stack(smoothed)alpha0.3 在抓取任务上效果不错既平滑了抖动又没引入太大延迟。5.3 多模态抓取时模式坍塌现象面对同一个物体明明有从左抓和从右抓两种方式模型只生成一种。原因Flow Matching 的确定性轨迹在训练不充分时容易收敛到条件均值把多模态平均掉。解决增加训练数据多模态需要足够样本覆盖每个模式。检查条件信息如果观测里没有区分左右的信息模型没法知道该选哪个模式。确保观测包含足够上下文。用 OT 耦合OT-CFM 的轨迹更直多模态边界更清晰。推理时加噪声从不同噪声出发可能得到不同模式。但这不是根本解法。我实测下来数据量够的时候 Flow Matching 的多模态表现跟 Diffusion 持平。500 条轨迹时确实有坍塌加到 2000 条就正常了。5.4 常见问题速查表问题可能原因解决方法loss NaN未归一化 / 学习率太大归一化降 lr加梯度裁剪loss 不降时间采样不当 / 网络太小logit-normal加大 hidden dim动作抖动步数少 / 求解器差加步数换 Midpoint模式坍塌数据少 / 条件不足加数据检查观测推理慢步数多 / 网络大减步数用 Euler蒸馏终点不准终点区域欠拟合加终点损失t 偏向 15.5 独家避坑技巧技巧一用 EMA 权重做推理。训练时维护一份 EMA 模型推理用它。成功率稳 2-3 个点几乎零成本。技巧二时间采样用 logit-normalσ 从 1.5 开始调。σ 越大越集中中间越小越均匀。抓取任务 1.5 是甜点。技巧三推理步数别死磕 5 步。10 步和 5 步的延迟差 4ms但成功率差 3-4 个点。实时性够的话10 步更稳。技巧四动作块长度 H 别太大。H16 是常用值H32 会让网络难学H8 又不够前瞻。16 是平衡点。技巧五训练前先跑一个过拟合测试。拿 10 条轨迹看能不能过拟合到 loss 接近 0。如果过拟合都做不到说明网络或数据有问题别急着上全量数据。6. 扩展方向与个人体会Flow Matching 这套东西目前还在快速演进。我关注几个方向一是蒸馏把 10 步压到 1-2 步用一致性模型或者对抗蒸馏推理能再快一个量级二是离散 Flow Matching处理离散动作空间比如机械臂的开关夹爪三是与强化学习结合用 Flow Matching 做策略表示比高斯策略表达能力强很多。我个人在实际操作中的体会是Flow Matching 最大的价值不是“比 Diffusion 好”而是它把生成式策略的工程复杂度降下来了。训练目标简单推理步数少调参空间小这些对工程落地太重要了。Diffusion Policy 你要调噪声调度、调采样器、调步数Flow Matching 你只需要调时间采样和步数其他基本默认值就能跑。最后再分享一个小技巧如果你已经有训练好的 Diffusion Policy想迁移到 Flow Matching不用从头训。把 Diffusion 的去噪网络拿过来改一下输出维度从预测噪声改成预测速度损失函数换成 CFM 损失微调 20-30 epoch 就能收敛。我试过成功率能恢复到原模型的 95% 以上省了一大半时间。