Diffusion Policy 学习课

Visuomotor Policy Learning via Action Diffusion · RSS 2023 / IJRR 2024

1. 是什么
2. 原理与公式
3. 架构与数据流
4. 代码精读
5. 三大设计
6. 实验与对比
7. 自测

1.1 一句话理解

Diffusion Policy 把"机器人看图选动作"这件事,建模成一个去噪过程:从纯随机噪声出发,以当前观测为条件,迭代地把噪声"擦"成一段平滑的未来动作序列。

核心类比:就像 Stable Diffusion 从噪声生成图片(以文本为条件),Diffusion Policy 从噪声生成动作序列(以视觉观测为条件)。生成的不是像素,而是机器人未来 16 步要怎么动。

1.2 为什么需要它?传统方法的痛点

方法怎么做痛点
显式策略 (回归)网络直接输出动作 a = f(o)多模态崩溃:左绕/右绕两条路被平均成"撞墙"
隐式策略 (IBC)学能量函数 E(o,a),argmin 取动作训练不稳定:需负采样估计配分函数 Z
BET动作离散化 + Transformer无法 commit:多模态间摇摆
Diffusion Policy学分数函数,迭代去噪生成天然多模态 + 训练稳定 + 高维友好

1.3 多模态问题——最关键的动机

假设演示数据里,人遇到障碍物有时往左绕、有时往右绕。这是多模态动作分布(同一观测对应多个合理动作)。

  • 回归网络:训练目标是最小化 MSE,会输出左和右的平均值——直接撞上障碍物。
  • Diffusion Policy:学的是整个动作分布,采样时随机落入"左绕"或"右绕"某一个模式,且在一次 rollout 中坚持到底(commit)。
这就是为什么 Diffusion Policy 在 Push-T 等需要多种策略的任务上能到 95%,而回归基线只有 20%。

2.1 DDPM 基础:前向加噪 & 反向去噪

扩散模型有两个方向:

前向过程(加噪,训练时用)

给干净动作序列 A⁰ 逐步加高斯噪声,K 步后变成纯噪声 Aᴷ ~ N(0, I)。任意一步可一次性算出:

Aᵏ = √(ᾱₖ)·A⁰ + √(1−ᾱₖ)·ε,   ε ~ N(0, I)

ᾱₖ 由噪声调度(noise schedule)决定,k 越大噪声越多。

反向过程(去噪,推理时用)

从噪声 Aᴷ 出发,训练一个网络 ε_θ 预测"这一步加了多少噪声",然后减掉它:

Aᵏ⁻¹ = α·(Aᵏ − γ·ε_θ(Oₜ, Aᵏ, k)) + N(0, σ²I)

迭代 K 次,噪声被逐步擦除,得到干净动作 A⁰。

2.2 训练目标:超级简单的 MSE

训练时不需要跑完整的去噪链,只需:

  1. 取一段真实动作 A⁰
  2. 随机采一个步数 k ~ U(1, K) 和噪声 ε ~ N(0, I)
  3. 构造带噪动作 Aᵏ = √(ᾱₖ)·A⁰ + √(1−ᾱₖ)·ε
  4. 让网络预测噪声,算 MSE
L = MSE( ε,   ε_θ(Oₜ, Aᵏ, k) )
为什么稳定? 这个 loss 不涉及配分函数 Z(o,θ),不需要负采样(对比 IBC)。本质上学的是分数函数 ∇ₐ log p(a|o) = −ε_θ(a,o)/σ,训练曲线平滑,不用精挑 checkpoint。

2.3 条件去噪:观测是"条件"不是"被生成对象"

关键设计:视觉观测 Oₜ 是去噪网络的条件输入,而不是要生成的东西。

p(Aₜ | Oₜ)  —— 只生成动作,观测当条件

对比"联合分布 p(Aₜ, Oₜ)"的做法(如原始 inpainting 方案),这样设计有两大好处:

  • 视觉编码器只前向一次,与 K 次去噪解耦 → 推理快得多
  • 可以端到端训练视觉编码器

2.4 噪声调度 & 推理加速

训练推理
去噪步数K = 100 步DDIM 10~16 步
调度iDDPM 的 Square Cosine Schedule
延迟~0.1s (RTX 3080)

训练用 DDPM 100 步保证质量;推理换成 DDIM(确定性采样)只需 10~16 步,满足 10Hz 实时控制。

3.1 整体数据流(CNN 变体, 图像观测)

=== 训练阶段 ===

2× RGB [B,To,3,240,320]
   │
   ▼ ResNet-18 ×2 (每个相机独立)
   │   GroupNorm + SpatialSoftmax 替代 BN + 全局池化
   ▼
obs_feat [B, To×128]  ──────────────┐
                                     │ (作为 global_cond)
GT actions A⁰ [B,Tp,Da]             │
   │ 采样 k~U(1,100), ε~N(0,I)      │
   │ Aᵏ = √ᾱ·A⁰ + √(1-ᾱ)·ε        │
   ▼                                 │
┌──────────────────────────────┐    │
│  Conditional UNet1D (ε_θ)    │◄───┘ FiLM 注入
│  输入: Aᵏ [B,Tp,Da]          │◄─── diffusion step k (正弦嵌入)
│  Conv1D→GroupNorm→FiLM→Mish  │
│  下采样[256,512,1024]+上采样  │
└──────────┬───────────────────┘
           ▼
      ε_pred [B,Tp,Da]
           │
      Loss = MSE(ε, ε_pred)

=== 推理阶段 (Receding Horizon) ===

obs [B,To,...] ──→ ResNet ──→ obs_feat (只算一次)
                                  │
Aᴷ ~ N(0,I) [B,Tp,Da]            │
   │  DDIM 去噪 10~16 步          │
   ▼  Aᵏ⁻¹ = scheduler.step(...) ◄┘
  ...迭代
   ▼
A⁰ [B,Tp=16,Da]
   │
   ├─ 执行前 Ta=8 步 ──→ 机器人
   └─ 丢弃后 8 步
   ↻ 每 8 步重新规划

3.2 关键张量维度表

张量维度含义
RGB 输入[B, To, 3, 240, 320]To=2 步观测, 2 路相机
视觉特征(每路)[B, To, 64]ResNet-18+SpatialSoftmax
观测条件[B, To×128]多路拼接, 作 global_cond
噪声动作序列[B, Tp, Da]Tp=16, Da=2~14
扩散步嵌入[B, 256]标量 k → 正弦向量
ε_pred[B, Tp, Da]预测噪声
执行子序列[B, Ta, Da]Ta=8, 实际下发

3.3 两种去噪网络

CNN (ConditionalUnet1D)Transformer
基础1D 时间卷积 (Janner 2022)minGPT decoder
条件注入FiLM (scale/bias 调制)交叉注意力
优点开箱即用, 超参稳定高频动作、长horizon更好
缺点偏低频, 过度平滑超参敏感(dropout/wd)
适用大部分任务的默认选择ToolHang(高精度)、Kitchen

3.4 视觉编码器的两个关键改动

① SpatialSoftmax 替代全局平均池化:保留空间位置信息(物体在哪),对操作任务至关重要。

② GroupNorm 替代 BatchNorm:BatchNorm 的统计量与 batch 耦合,和 EMA(指数滑动平均)冲突;GroupNorm 不依赖 batch,训练更稳定。

此外:每路相机用独立编码器,每个时间步独立编码后拼接。ResNet-18 从头训练效果最好(也可用 CLIP ViT-B/16 微调,50 epoch 达 98%)。

4.1 代码地图

功能文件
策略(lowdim)policy/diffusion_unet_lowdim_policy.py
策略(图像,主力)policy/diffusion_unet_hybrid_image_policy.py
去噪网络 UNet1Dmodel/diffusion/conditional_unet1d.py
视觉编码器model/vision/multi_image_obs_encoder.py
Transformer 变体model/diffusion/transformer_for_diffusion.py
训练循环workspace/train_diffusion_unet_hybrid_workspace.py
噪声调度diffusers 的 DDPMScheduler

4.2 训练前向 compute_loss 逐行精读

policy/diffusion_unet_lowdim_policy.py:169-238

def compute_loss(self, batch):
    # ═══ 1. 归一化 ═══
    nbatch = self.normalizer.normalize(batch)
    obs = nbatch['obs']        # [B, T, Do]
    action = nbatch['action']  # [B, T, Da]

    # ═══ 2. 准备条件 (三种方式) ═══
    local_cond = None; global_cond = None
    trajectory = action
    if self.obs_as_global_cond:
        # 主力方式: 观测拍平成 global 条件
        global_cond = obs[:,:self.n_obs_steps,:].reshape(obs.shape[0], -1)
        # [B, To*Do] —— 只取前 To=2 步观测!
    else:
        # inpainting 方式: 动作和观测拼一起
        trajectory = torch.cat([action, obs], dim=-1)

    # ═══ 3. 采样噪声 + 随机时间步 ═══
    noise = torch.randn(trajectory.shape, device=trajectory.device)
    timesteps = torch.randint(0, 100, (bsz,)).long()  # k ~ U(0,100)

    # ═══ 4. 前向加噪 (一步到位) ═══
    noisy_trajectory = self.noise_scheduler.add_noise(
        trajectory, noise, timesteps)
    #  = √ᾱ·trajectory + √(1-ᾱ)·noise

    # ═══ 5. 预测噪声 ═══
    pred = self.model(noisy_trajectory, timesteps,
        local_cond=local_cond, global_cond=global_cond)

    # ═══ 6. MSE Loss (预测 epsilon) ═══
    target = noise  # prediction_type='epsilon'
    loss = F.mse_loss(pred, target, reduction='none')
    loss = loss * loss_mask  # 只在动作维度算 loss
    return loss.mean()
整个训练逻辑只有 7 步,核心就是 add_noise → predict → MSE。注意 global_cond 只取前 n_obs_steps=2 步观测,这呼应了消融实验里 To=2 最优的结论。

4.3 推理采样 conditional_sample 逐行精读

policy/diffusion_unet_lowdim_policy.py:60-96

def conditional_sample(self, condition_data, condition_mask,
        local_cond=None, global_cond=None, **kwargs):
    # ═══ 1. 从纯噪声起步 ═══
    trajectory = torch.randn(size=condition_data.shape, ...)  # Aᴷ ~ N(0,I)

    # ═══ 2. 设置去噪步数 (推理只 10~16 步) ═══
    scheduler.set_timesteps(self.num_inference_steps)

    # ═══ 3. 迭代去噪 ═══
    for t in scheduler.timesteps:        # 从 K 递减到 0
        # 3a. 强制已知部分 (inpainting 时用)
        trajectory[condition_mask] = condition_data[condition_mask]
        # 3b. 预测噪声
        model_output = model(trajectory, t,
            local_cond=local_cond, global_cond=global_cond)
        # 3c. 去噪一步: Aᵏ → Aᵏ⁻¹
        trajectory = scheduler.step(
            model_output, t, trajectory).prev_sample

    trajectory[condition_mask] = condition_data[condition_mask]
    return trajectory

然后在 predict_action 中切片取出要执行的 Ta 步:

start = To          # To=2
end = start + self.n_action_steps   # +Ta=8
action = action_pred[:, start:end]  # 只执行中间 8 步

4.4 去噪网络 ConditionalUnet1D.forward 逐行精读

model/diffusion/conditional_unet1d.py:175-235

def forward(self, sample, timestep, local_cond=None, global_cond=None):
    # sample: [B,T,Da] 噪声动作; timestep: [B] 扩散步
    sample = rearrange(sample, 'b h t -> b t h')  # 转成 conv1d 格式

    # ═══ 1. 编码扩散步 k → 正弦嵌入 ═══
    global_feature = self.diffusion_step_encoder(timesteps)  # [B,256]
    # SinusoidalPosEmb → Linear → Mish → Linear

    # ═══ 2. 拼接观测条件 ═══
    if global_cond is not None:
        global_feature = torch.cat([global_feature, global_cond], axis=-1)
        # [B, 256 + To*obs_dim]  ← 时间步 + 观测 合成总条件

    # ═══ 3. U-Net 下采样 (encoder) ═══
    x = sample; h = []
    for resnet, resnet2, downsample in self.down_modules:
        x = resnet(x, global_feature)   # FiLM 注入条件!
        x = resnet2(x, global_feature)
        h.append(x)                     # 存 skip connection
        x = downsample(x)

    # ═══ 4. 中间层 ═══
    for mid in self.mid_modules:
        x = mid(x, global_feature)

    # ═══ 5. 上采样 (decoder) + skip ═══
    for resnet, resnet2, upsample in self.up_modules:
        x = torch.cat((x, h.pop()), dim=1)  # U-Net skip
        x = resnet(x, global_feature)
        x = resnet2(x, global_feature)
        x = upsample(x)

    x = self.final_conv(x)
    return rearrange(x, 'b t h -> b h t')  # [B,T,Da]

4.5 FiLM 条件注入——核心机制

model/diffusion/conditional_unet1d.py:48-66 (ConditionalResidualBlock1D.forward)

def forward(self, x, cond):
    out = self.blocks[0](x)             # Conv1d → GroupNorm → Mish
    embed = self.cond_encoder(cond)     # cond → scale & bias
    if self.cond_predict_scale:
        embed = embed.reshape(B, 2, out_channels, 1)
        scale = embed[:,0,...]
        bias  = embed[:,1,...]
        out = scale * out + bias        # ← FiLM: 逐通道线性调制
    else:
        out = out + embed               # 简化版: 只加 bias
    out = self.blocks[1](out)
    return out + self.residual_conv(x)  # 残差连接
FiLM (Feature-wise Linear Modulation):把"观测+扩散步"这个条件,转换成对每个卷积通道的 scalebias,逐通道地调制特征。这是把条件信息注入 CNN 的高效方式——不改变网络结构,只调制激活值。

5.1 设计一:闭环动作序列预测 (Receding Horizon)

预测 16 步 ──→ 只执行前 8 步 ──→ 丢弃后 8 步 ──→ 重新观测预测
   Tp=16          Ta=8                            ↻ 循环
参数作用
To 观测 horizon2看几步历史观测
Tp 预测 horizon16一次预测多少步动作
Ta 执行 horizon8实际执行多少步后重规划

三大好处:

  • 时间一致性:序列预测避免连续动作来自不同模态导致抖动
  • 抗空闲过拟合:单步策略易困在遥操作暂停段,序列预测不会
  • 延迟鲁棒:预测未来动作天然补偿图像处理/网络延迟
Ta=8 是大多数任务的最优平衡:太小→重规划频繁、计算贵、抖动;太大→响应慢、无法及时纠错。

5.2 设计二:视觉条件化 (Visual Conditioning)

把视觉观测当条件而非生成对象:

  • 视觉编码器只前向一次,K 次去噪复用同一份 obs_feat
  • 对比"联合 inpainting"方案,计算量大幅下降,实现实时控制
  • 视觉编码器可端到端训练

5.3 设计三:时间序列扩散(CNN vs Transformer)

两种去噪网络(见架构 tab)。CNN 用 FiLM、开箱即用;Transformer 用交叉注意力、适合高频动作。

经验法则:先用 CNN 变体(超参稳定)。如果任务需要高频/突变动作(如敲击、ToolHang 精密插入),再换 Transformer 变体并仔细调 dropout/weight decay。

5.4 配套设计:位置控制优先

论文发现 位置控制 (position control) 一致优于速度控制

  • 位置控制下动作多模态更显著,正好是 DP 的强项
  • 位置控制累积误差更小,更适合序列预测

有趣的是:基线方法(LSTM-GMM 等)反而在速度控制下更好——这是 DP 的独特协同效应。

6.1 仿真结果

15 个任务 / 4 个 benchmark(Robomimic, Push-T, Block Push, Franka Kitchen),平均提升 46.9% 超越所有基线(LSTM-GMM, IBC, BET),在所有任务和观测模态(state/image)均一致领先。

6.2 真机结果

任务成功率看点
Push-T95%vs LSTM-GMM 20%, IBC 0%
Mug Flip90%3D旋转+多模态抓取
Sauce Pour79%处理空闲动作+精细调整
Sauce Spread100%周期动作+自终止
Shirt Folding75%双臂9步长horizon

6.3 消融研究关键结论

  • 观测 horizon:state 不敏感;image 偏好 To=2
  • 动作 horizon:Ta=8 最优平衡
  • 延迟鲁棒性:位置控制在 4 步延迟内保持峰值
  • 数据效率:所有数据量下均优于 LSTM-GMM
  • 视觉编码器:端到端训练最佳;CLIP ViT-B/16 微调 50 epoch 达 98%

6.4 与其它方法对比

维度Diffusion PolicyLSTM-GMMIBCBET
多模态天然支持+commit偏一侧偏一侧无法commit
训练稳定平滑(无Z)不稳定(需负采样)
动作生成迭代去噪10-16步单步优化argmin分类+offset
推理速度~0.1s

6.5 局限性

  • 继承行为克隆局限:演示数据不足时性能受限
  • 推理延迟高于单步方法(LSTM-GMM)
  • Transformer 变体超参数敏感
  • 原论文未探索 RL 微调(后续工作填补)

训练/评测资源配置 🟢论文+仓库核对

数据来源:Diffusion Policy 论文(Chi et al.)实验附录 + 仓库 dp/README。DP 是轻量策略网络(CNN-based 或 Transformer-based,~百 M 参数级),资源门槛在本站所有工作里属最低。

配置
推理延迟约 0.1s @ NVIDIA 3080(得益于 receding-horizon + 少步去噪)
训练 GPU论文未统一列型号;仓库说明 单张 Nvidia GPU 即可复现仿真结果;多卡可选(ray 集群,CUDA_VISIBLE_DEVICES=0,1,2 + ray start --num-gpus=3)
Batch sizestate-based 实验 batch 256
训练时长epoch 数很多:state-based 训到 4500 epoch、image-based 3000 epoch(每 50 epoch 存 ckpt+评测);部分 benchmark 用固定 12h 训练后取最后 ckpt 对比
门槛解读:DP 是"单卡友好"的代表——一张消费级/单张数据中心卡就能训练和推理,这也是它成为社区基线的原因之一。注意 epoch 数字很大(几千)不代表很慢,因为每个 epoch 的策略网络很小、数据量(演示轨迹)也有限;真正的墙钟时间取决于任务数据规模,仿真单任务常在数小时到一天级别。想复现优先用仓库提供的 config,单卡起步即可。
与本站其它页对照:DP(单卡 3080 推理 0.1s) 是资源谱系的最轻端;DynaGuide 在 DP 之上加引导也只用单张 3090;到 VLA(8× A100)、视频 WAM(8× H200) 才逐级变重。理解 DP 的轻量,有助于体会为什么"引导冻结策略/图像编辑 cache"这类省算力路线有吸引力。

7. 自测:10 道题检验你学会了没

选完会即时判分并给解析。最后有总分。