☰
Flow Matching 动作生成实战:从 Diffusion Policy 到实时机器人控制
2026/9/26 6:08:42 网站建设 项目流程

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 求解器积分就行,步数可以压得很低。

这里有个关键概念叫CFM(Conditional 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_{t+dt} = 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。我的回答是看场景:

  • 控制频率低于 10Hz:Diffusion 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 的地方学得不好。原因是:t=0 附近速度变化剧烈(从噪声出发),t=1 附近要精确落到数据点,这两个区域需要更多训练信号。

我的做法是用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 层 MLP,hidden 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) # 终点一致性损失(可选) # 让模型在 t=1 附近预测更准 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-4AdamW,cosine 衰减
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, 1]。这一步必须做,否则不同关节量纲差异会让训练发散。
  2. 观测编码:图像过预训练 ResNet 或 ViT,关节状态直接拼。
  3. 动作块切分:滑动窗口切成长度 H=16 的块,stride=1。
  4. 数据集划分:按轨迹划分,不要按帧划分,否则同一轨迹的帧会泄漏到验证集。

我踩过的坑:一开始忘了动作归一化,训练 loss 直接 NaN。因为某个关节的力矩值到了 50 多,跟其他关节的 0.1 量级差太多,梯度爆炸。归一化之后一切正常。

4.2 网络搭建完整代码

import torch import torch.nn as nn import math class FlowMatchingPolicy(nn.Module): def __init__(self, action_dim=7, obs_dim=256, hidden_dim=256, num_layers=4): 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, device=t.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, device=device) * 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, alpha=0.001) return loss.item()

几个关键点:

  • 梯度裁剪必须加。Flow Matching 虽然比 Diffusion 稳,但偶尔还是会有梯度尖峰,裁剪到 1.0 能防住。
  • EMA 更新在 optimizer.step() 之后。顺序别搞反。
  • 时间采样每次重新采。不要一个 batch 用同一个 t,那样梯度信号太单一。

4.4 推理:ODE 求解器选择

推理时从噪声出发,积分到 t=1。求解器选择直接影响速度和精度:

求解器步数精度速度适用场景
Euler5-10中最快实时控制
Midpoint5-10高中精度要求高
RK410-20很高慢离线评估
DPM-Solver5-10高快通用推荐

我的默认选择是Euler 10 步,简单可靠。如果发现动作不够平滑,换 Midpoint 5 步,精度差不多但更稳。

@torch.no_grad() def sample(model, obs, num_steps=10): B = obs.shape[0] x = torch.randn(B, 16, 7, device=obs.device) # 从噪声出发 dt = 1.0 / num_steps for i in range(num_steps): t = torch.full((B,), i * dt, device=obs.device) v = model(x, t, obs) x = x + v * dt return x

注意:t 的取值从 0 到 1-dt,不要取到 1。最后一步积分完刚好到 t=1。如果取到 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 左右下不去,或者上下震荡。

排查顺序:

  1. 检查动作归一化。这是最常见的原因。打印每个维度的均值和标准差,确认都在合理范围。
  2. 检查时间采样。如果 t 全集中在中间,两端学不好;如果全均匀,中间学不好。用 logit-normal。
  3. 检查学习率。1e-4 起步,如果 loss 震荡就降到 5e-5。
  4. 检查网络容量。动作维度 7 的话,hidden dim 256 足够。如果用了 64,可能欠拟合。

我遇到过一次 loss 震荡,查了半天发现是 batch 里有一条异常轨迹,动作值到了 100 多(传感器故障)。归一化时被这条轨迹带偏了,导致整体分布不对。把异常轨迹剔掉就好了。所以数据清洗比调参重要。

5.2 推理时动作抖动

现象:生成的动作序列相邻帧之间跳变明显,执行起来机器人抖。

原因和解决:

  • 求解器步数太少:Euler 5 步可能不够,加到 10 步。
  • 求解器精度不够:换 Midpoint 或 RK4。
  • 训练时终点区域欠拟合:加终点一致性损失。
  • 动作后处理缺失:推理完做一次滑动平均或低通滤波。

我的做法是推理后加一个简单的指数滑动平均:

def smooth_actions(actions, alpha=0.3): smoothed = [actions[0]] for i in range(1, len(actions)): smoothed.append(alpha * actions[i] + (1 - alpha) * smoothed[-1]) return torch.stack(smoothed)

alpha=0.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 偏向 1

5.5 独家避坑技巧

技巧一:用 EMA 权重做推理。训练时维护一份 EMA 模型,推理用它。成功率稳 2-3 个点,几乎零成本。

技巧二:时间采样用 logit-normal,σ 从 1.5 开始调。σ 越大越集中中间,越小越均匀。抓取任务 1.5 是甜点。

技巧三:推理步数别死磕 5 步。10 步和 5 步的延迟差 4ms,但成功率差 3-4 个点。实时性够的话,10 步更稳。

技巧四:动作块长度 H 别太大。H=16 是常用值,H=32 会让网络难学,H=8 又不够前瞻。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% 以上,省了一大半时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询