拓冰建站拓冰建站
首页 / 资讯中心 / 正文

iDP3 Learning模块代码解析:从3D点云到动作预测的完整闭环

看了iDP3的论文之后大多数人最想看的就是Learning模块的代码到底怎么把“3D点云”和“动作预测”串起来的。网上搜代码实现从OCR文字识别到FIFO的Verilog实现都一堆但iDP3这种3D模仿学习的训练与推理闭环确实需要静下心来拆。我花了两周时间把iDP3的Learning模块从数据集构建到3D动作预测完整跑通这篇博文就把这条链路上的代码逻辑、关键设计和踩坑记录一次性讲清楚。先说结论iDP3的Learning模块并不只是一个train.py它是由数据加载、模型构建、训练器、推理器四个部分组成的整套闭环。只有把每一步的shape变化、条件注入方式和动作还原逻辑都理解透才算真正吃透了这套代码。接下来我按实际阅读代码的顺序分五个部分展开。1. Learning模块整体设计与数据流1.1 Learning模块在iDP3里的定位iDP3全称是implicit Diffusion Policy with 3D point cloud它的核心管线可以概括成一句话用PointNet把点云观测编码成条件特征再交给条件扩散模型来生成机器人动作。整个流程里Learning模块承担的是“数据到模型再到动作”的组装工作。它不关心你的轨迹是从仿真环境导出的还是真机遥操作采集的只要数据落成hdf5文件它就能完成训练和推理。和DP3相比iDP3的Learning模块有两个关键变化一是动作预测目标从绝对动作改成了delta action也就是相邻帧动作的差值二是在前向传播时强制做了时间因果对齐预测第k步动作时不允许模型偷看未来的点云观测。这两个改动直接影响了数据集的构造方式和训练时的loss计算后面我会展开讲代码。1.2 数据流全景从HDF5到Loss再到动作先给一张总览表把训练和推理两个阶段的数据形态变化列出来这样后面看代码不会迷路。阶段输入处理过程输出训练hdf5中的点云序列和动作序列窗口采样、点云预处理、normalizer归一化、加噪噪声预测值与真实噪声计算MSE loss推理当前时刻的点云观测点云编码、条件注入、逐步去噪delta action序列累积还原为真实动作训练时数据流是读hdf5 - 随机取一段轨迹 - 按obs_horizon和action_horizon截取窗口 - 点云下采样到固定点数 - 计算delta action - 全局normalizer归一化 - 随机采样扩散时间步t - 给动作加高斯噪声 - 模型预测噪声 - 计算loss。推理时数据流是读取最新一帧点云 - 点云编码成条件特征 - 初始化纯噪声动作 - 在scheduler的timesteps上逐步去噪 - 得到delta action序列 - 累积成绝对动作 - 取前action_step步执行。这里最容易绕晕的是时间维对齐。训练时一条轨迹里随机切一段obs窗口和action窗口在时间上是连续的推理时只有“当前”这一刻的观测动作序列则是未来若干步的预测值。iDP3用delta action和因果mask把这两者的语义统一了起来。1.3 为什么这块代码不好读我自己读iDP3代码时卡了好几次主要原因有三个。第一是工程依赖多pointnet2的CUDA扩展、diffusers库、hdf5存储都耦合在一起光环境就得折腾一阵。第二是时间维度的对齐非常容易绕晕obs_horizon、action_horizon、pred_horizon三个参数一旦没搞清看代码就是一团浆糊。第三是obs_dict的键结构很隐性代码里到处是obs_dict[point_cloud]、obs_dict[agent_pos]这种访问但键名是在dataset里定义的不翻到数据加载部分根本对不上。所以这篇文章我索性把整个Learning模块拆成“数据集构建-模型前向-训练推理”三段每段都给出核心代码和shape说明争取让读完的朋友能直接对着自己的项目改。2. 数据集构建从原始轨迹到可训练样本2.1 HDF5数据格式与字段设计iDP3的原始数据通常存成hdf5原因很简单单文件、支持按需读取、能放得下动辄几千帧的点云序列。一个标准的demo文件内部结构大致是import h5py with h5py.File(demo.hdf5, r) as f: print(f.keys()) # 常见的键observations, actions # observations下面一般还有 point_clouds、state 等子键 point_clouds f[observations/point_clouds][:] # (T, N, 3) 或 (T, M, N, 3) states f[observations/state][:] # (T, D_state) actions f[actions][:] # (T, Da)这里的T是轨迹帧数N是每帧点数M是多视角相机数量。如果你的数据来自多相机iDP3一般会先把多个视角的点云拼接起来再统一降采样后面模型看到的是一个完整场景点云而不是逐视角分开处理。存储格式上有两个建议。第一点云尽量用float16存一张2048点的点云帧在float32下就是24KB一条上万帧的轨迹能差出几百MBfloat16配合hdf5的压缩可以省很多空间。第二加载后一定要转成float32再进模型PyTorch的绝大多数算子不支持float16点云直接输入PointNet。2.2 窗口采样与delta action构造训练时不可能把一整条轨迹都塞进模型所以核心操作是随机窗口采样。iDP3的做法和DP3类似从轨迹里随机选一个起始点然后连续截取obs_horizon帧观测和action_horizon帧动作。import numpy as np def sample_window(point_clouds, actions, obs_horizon, action_horizon): T point_clouds.shape[0] start np.random.randint(0, T - obs_horizon - action_horizon 1) obs_pc point_clouds[start : start obs_horizon] # (obs_horizon, N, 3) act_seq actions[start : start action_horizon] # (action_horizon, Da) # 计算delta action相邻时刻的动作差值 delta_act np.diff(act_seq, axis0, prependact_seq[:1]) # (action_horizon, Da) return obs_pc, act_seq, delta_act这里有个细节值得注意np.diff的prependact_seq[:1]保证了delta序列和原动作序列长度一致第一个delta是0。这个边界处理很关键如果漏掉prepend序列长度会少一帧后面对齐直接崩。那为什么iDP3要用delta action而不是直接预测绝对动作我在实际实验里的体会是机器人动作的绝对值往往分布在一个较大的范围里而且不同轨迹之间的绝对位置差异很大但相邻帧的动作变化量就稳定得多更接近一个零均值的小范围分布。扩散模型在这种目标上收敛更快生成的轨迹也更平滑。推理时只需要把delta累积回去就能还原真实动作代价非常小。2.3 点云预处理、normalizer与数据增强点云不能直接扔进网络首先要统一点数。iDP3一般用FPS或者随机采样把每帧点云固定到N个点我实测2048是性价比最高的选择1024会丢细节4096则明显拖慢训练。def downsample_points(points, num_points): # points: (M, 3) if len(points) num_points: idx np.random.choice(len(points), num_points, replaceFalse) else: idx np.random.choice(len(points), num_points, replaceTrue) return points[idx]然后是坐标系归一化。如果你在真机上采集数据点云坐标一般来自相机外参标定不同轨迹之间可能存在整体偏移。常见的做法是把点云转到末端执行器坐标系或者减掉一个workspace中心点让网络学到的策略不依赖于绝对坐标。normalizer在DP系列里是标配iDP3也一样。它维护一个全局的均值和方差估计在训练过程中不断更新推理时用训练结束时的统计量来做归一化。class RunningNormalizer: def __init__(self, dim): self.mean np.zeros(dim) self.var np.ones(dim) self.count 0 def update(self, data): batch data.shape[0] batch_mean data.mean(axis0) batch_var data.var(axis0) # 增量合并均值和方差 new_count self.count batch self.mean (self.count * self.mean batch * batch_mean) / new_count self.var (self.count * self.var batch * batch_var) / new_count self.count new_count def normalize(self, data): return (data - self.mean) / np.sqrt(self.var 1e-5)这里有个我踩过的坑normalizer的统计量必须在整个训练集上大致过一遍再开始正式训练或者至少在训练初期同步更新。如果你只在当前batch上做归一化模型会不断看到分布漂移的输入训练很难稳定下来。数据增强方面iDP3常用的就两招点云随机dropout和加少量高斯噪声。这俩对真机数据的泛化帮助很明显因为真机点云天然带噪声和遮挡训练时模拟一点进去推理时就不至于被传感器噪声带偏。3. 模型构建与前向传播条件扩散模型的代码架构3.1 模型三件套PointNet、FiLM、ConditionalUnet1DiDP3的模型主体由三个组件拼成点云编码器、条件投影层、条件扩散主干。点云编码器用的是PointNet它的作用是把一帧(N, 3)的点云编码成一个全局特征向量。条件投影层把多帧观测特征拼起来映射到扩散模型能接受的条件维度。扩散主干用的是diffusers库里的ConditionalUnet1D它接收带噪动作序列和全局条件通过FiLM方式把条件注入到每一层。模型初始化的核心代码大致长这样import torch.nn as nn from diffusers.models import ConditionalUnet1D from diffusers.schedulers.scheduling_ddpm import DDPMScheduler class iDP3Model(nn.Module): def __init__(self, obs_horizon, action_horizon, action_dim, point_feature_dim128, num_points2048): super().__init__() self.obs_horizon obs_horizon self.action_horizon action_horizon self.action_dim action_dim # 点云编码器把每帧 (N, 3) 点云编码成 (D,) 特征 self.point_encoder PointNet2Encoder( in_channels3, out_channelspoint_feature_dim, num_pointsnum_points, ) # 条件投影obs_horizon 帧特征拼接后映射到 unet 条件维度 cond_dim 256 self.cond_proj nn.Sequential( nn.Linear(obs_horizon * point_feature_dim, cond_dim), nn.GELU(), nn.Linear(cond_dim, cond_dim), ) # 扩散模型主干 self.model ConditionalUnet1D( input_dimaction_dim, global_cond_dimcond_dim, down_block_types(DownBlock1DNoTimestep, DownBlock1DNoTimestep, DownBlock1DNoTimestep), up_block_types(UpBlock1DNoTimestep, UpBlock1DNoTimestep, UpBlock1DNoTimestep), block_out_channels(64, 128, 256), ) # 噪声调度器 self.noise_scheduler DDPMScheduler( num_train_timesteps100, beta_schedulesquaredcos_cap_v2, clip_sampleTrue, prediction_typeepsilon, )这里最值得说的就是ConditionalUnet1D的condition注入方式。它内部用的是FiLM也就是把global_cond通过一个线性层映射成每个block的scale和shift然后作用在特征上。这种设计比直接把条件拼在输入里要高效因为每一层都能感知到条件信息而不仅仅是第一层。3.2 前向传播细节动作序列如何和点云条件对齐前向传播是理解整个模块的关键。我们一步一步看shape变化。def forward(self, point_clouds, action, timesteps): # point_clouds: (B, obs_horizon, N, 3) # action: (B, action_horizon, action_dim) B point_clouds.shape[0] # 1. 每一帧点云独立编码 # 合并batch和obs_horizon维度逐帧编码 pc point_clouds.reshape(B * self.obs_horizon, -1, 3) # (B*obs_horizon, N, 3) point_feats self.point_encoder(pc) # (B*obs_horizon, D) point_feats point_feats.reshape(B, self.obs_horizon * point_feats.shape[-1]) # 2. 条件投影 global_cond self.cond_proj(point_feats) # (B, cond_dim) # 3. 给动作加噪 noise torch.randn_like(action) noisy_action self.noise_scheduler.add_noise(action, noise, timesteps) # 4. 扩散模型前向 # noisy_action: (B, action_horizon, action_dim) - unet 内部转成 (B, action_dim, action_horizon) noise_pred self.model(noisy_action, timesteps, global_condglobal_cond) return noise_pred, noise点云编码这一步有个显存优化技巧不要直接让PointNet接收(B, obs_horizon, N, 3)的输入因为很多PointNet实现不支持五维张量。把B和obs_horizon合并成一个大batch编码效率更高显存占用也更可控。ConditionalUnet1D内部处理的是序列数据它把动作序列看作一个“时间维为action_horizon、通道维为action_dim”的信号用一维卷积来建模序列内部的依赖关系。所以加噪和预测都是针对整个动作序列进行的而不是单步动作。3.3 iDP3和DP3前向的区别因果mask与时间对齐iDP3在论文里强调了一个DP3没处理好的问题训练时随机取了obs窗口和action窗口但如果不加约束模型在预测第k步动作时可能隐式地用到了未来帧的点云信息。这就像考试时提前看到了答案平时练得再好真上场就露馅。iDP3的做法是在前向时对观测特征做因果mask。代码上可以这样理解假设obs_horizon2action_horizon8那么预测前4步动作时只能用第1帧点云的特征预测后4步才能同时用第1帧和第2帧的特征。实现时可以构造一个mask矩阵乘在融合后的特征上也可以在数据加载时直接把obs窗口和action窗口对齐成“前k步动作对应前k帧观测”的格式。我用一个简单的类比解释这个设计开车时踩油门你只能根据当前和过去的路况来决定脚上的动作不能“提前看到”几秒后的路况再决定现在该踩多深。iDP3的因果mask就是在强制模型遵守这个自然规律。实际效果是推理时的动作预测更加稳定不会因为点云的微小抖动产生剧烈跳变。4. 3D动作预测的训练与推理闭环4.1 训练循环噪声采样、Loss计算与梯度更新训练核心是compute_loss这块逻辑。diffusion policy的训练目标不是直接回归动作而是让模型学会预测“加进去的噪声”。这看起来有点绕但正是扩散模型能生成多峰动作分布的原因。def compute_loss(self, batch): point_clouds batch[point_clouds] # (B, obs_horizon, N, 3) delta_action batch[delta_action] # (B, action_horizon, Da) B delta_action.shape[0] # 随机采样扩散时间步 timesteps torch.randint( 0, self.noise_scheduler.config.num_train_timesteps, (B,), devicedelta_action.device ).long() # 模型前向得到噪声预测 noise_pred, noise self.model(point_clouds, delta_action, timesteps) # MSE loss loss nn.functional.mse_loss(noise_pred, noise) return loss这里的核心逻辑很简单随机选一个时间步t把纯噪声逐步叠加到真实动作上让模型根据带噪动作和点云条件去预测噪声。训练稳定后模型就学会了“看到多噪的动作序列和观测就知道该怎么把它恢复成干净动作”。训练时还有一个很容易忽略的点add_noise的timesteps参数必须是long类型的tensor不能是int。很多复现bug就出在这里diffusers内部会根据timesteps去查询噪声调度表传错类型会直接报错或者算出错误结果。超参数方面我用的配置是超参数数值说明num_train_timesteps100扩散步数100是速度和质量的平衡点beta_schedulesquaredcos_cap_v2余弦噪声调度训练更稳定prediction_typeepsilon预测噪声最常用的训练目标obs_horizon2输入多少帧点云观测action_horizon8一次预测多少步动作learning_rate1e-3配合cosine decaybatch_size8取决于点云数量和显存大小4.2 推理循环从纯噪声到动作序列的逐步去噪推理和训练正好是相反的过程。训练时是“加噪-预测噪声”推理时是“从纯噪声出发-逐步去噪”。代码如下def predict_action(self, point_clouds): # 点云编码得到条件 pc point_clouds.unsqueeze(0) # (1, obs_horizon, N, 3) point_feats self.point_encoder(pc.reshape(-1, pc.shape[-2], pc.shape[-1])) global_cond self.cond_proj(point_feats.reshape(1, -1)) # 初始化纯噪声动作序列 noisy_action torch.randn( (1, self.action_horizon, self.action_dim), deviceself.device ) # 设置推理步数 self.noise_scheduler.set_timesteps(self.num_inference_steps) # 逐步去噪 for t in self.noise_scheduler.timesteps: t_tensor t.unsqueeze(0).long() noise_pred self.model(noisy_action, t_tensor, global_condglobal_cond) noisy_action self.noise_scheduler.step( noise_pred, t, noisy_action ).prev_sample # 去噪结果delta action序列 (1, action_horizon, Da) delta_action noisy_action.squeeze(0) # 累积还原成真实动作 action torch.cumsum(delta_action, dim0) return action注意最后一步torch.cumsum把delta action累积成绝对动作。这一步是整个推理里最容易出错的地方。我见过不少人在复现时漏掉这个累积操作导致机器人动作忽大忽小完全不可用。原因就是训练时用的是delta目标推理时却直接输出了delta而没有还原。推理时的set_timesteps也很关键。如果训练时用了100步推理时可以只用10步或20步DDPM的调度器会均匀抽取这些时间步。实测下来灵巧手任务10步去噪质量已经够用20步更稳100步纯属浪费计算。4.3 多步滚动与性能优化机器人控制是实时的不能每次都生成一整段动作再全部执行。标准做法是一次预测action_horizon步但只执行前action_step步然后重新采集点云再次预测。这个“预测-执行-再预测”的滚动方式在DP系列里是标配。action self.predict_action(current_obs) exec_action action[:action_step] # 只执行前面的动作性能优化方面我实测下来有三个点收益最大。第一推理时用torch.no_grad()和torch.cuda.amp.autocast()能省不少显存和计算。第二点云编码可以缓存如果你的机器人本体基本不动、只有末端在动历史帧的点云特征可以复用不用每帧都重算。第三把点云下采样点数从2048降到1024推理延迟能降低将近一半代价是策略精度略有下降具体取舍看你的任务。5. 实操踩坑与调优心得5.1 我遇到的五个典型报错与排查把我在跑iDP3时遇到的高频问题整理成一张速查表方便大家直接对照排查。症状可能原因解决办法hdf5读取时KeyError键名和代码里不一致先打印hdf5所有keys逐层核对observations下面的子键名PointNet2编译失败CUDA版本与PyTorch不匹配优先用预编译wheel或按README用Docker镜像loss下降正常但推理动作为0delta action没有累积还原检查推理最后是否做了cumsum点云输入维度报错数据存的是float16或带额外通道转float32检查最后一维是3还是4训练效果好但真机效果差点云坐标系/单位不一致确认训练和推理用同一套坐标系归一化参数最后一个问题是最隐蔽的。如果你从仿真迁移到真机点云尺度、相机视角、末端坐标原点都可能变了但normalizer还是训练时的统计量等于输入分布整个偏移了。这时候策略一定会崩。5.2 调参经验哪些超参数最值得动调参这件事我的经验是可动参数越少越好。iDP3里最值得动的四个参数按优先级排参数建议范围我的实测结论num_train_timesteps50-200100起步步数太少动作粗糙太多训练变慢且收益递减num_inference_steps10-5010步能跑20步稳追求延迟就压到10obs_horizon1-4灵巧手任务用2带记忆的环境用3-4点云点数1024-40962048性价比最高输入太稀疏会丢几何细节学习率这块我建议固定1e-3配合cosine decay不要一上来就调。它很少是效果差的根因数据质量和观测对齐才是。还有一个容易忽视的点动作维度。如果你的机器人是双手灵巧手action_dim可能会到20以上这时候action_horizon建议调大一点因为高维动作空间的生成需要更长的序列来保持平滑。我自己在双手任务上把action_horizon从8调到16效果明显改善。5.3 最后分享一点个人体会跑完整个iDP3的Learning模块我最大的感受是这类3D模仿学习的难点不在数学公式而在工程细节的串通。数据集的窗口怎么切、delta怎么算、训练加噪和推理去噪怎么对齐、normalizer统计量怎么更新任何一环出错结果都会很离谱。如果你是第一次接触这套代码我的建议是先在已有的demo数据上把训练和推理完整跑通再逐步替换成自己的数据。不要一上来就想着从零手写整个pipeline先把diffusers的scheduler源码读一遍把add_noise和step这两个函数的输入输出形状吃透就已经超过了大部分复现者。iDP3这套代码后续的扩展空间也很大比如把PointNet换成更强的3D backbone、把DDPM换成flow matching、或者把单帧点云改成多视角融合Learning模块的骨架基本不用动。把这套数据流和训练推理逻辑吃透后面做任何3D模仿学习项目都会顺手很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门