资讯详情

资讯详情

LIBERO动作归一化:复现差异背后的隐藏约定与工程实践

上个月我在复现一个LIBERO baseline时遇到一件非常尴尬的事同样的网络结构、同样的训练步数、甚至同样的随机种子对照实验里别人写的脚本能跑到不错的成功率而我自己这条流水线上却几乎起不来。一开始怀疑是不是Robosuite版本不一致后来把dataloader里喂给网络的target action直接打印出来看了一眼才发现问题出在Action归一化上。这件事让我把LIBERO官方数据集的处理流程从头到尾仔细翻了一遍最后得出的结论是Action归一化在LIBERO里不是一个简单预处理步骤而是一整套“隐藏约定”官方没有把口径说透才有了这么多莫名其妙的复现差异。这篇文章就是我对这个问题的完整复盘和分析。如果你正在用LIBERO做模仿学习、行为克隆或者离线强化学习或者打算用它当benchmark跟别人对比成绩那么Action归一化的不透明性一定会影响你只是早晚的问题。1. 写在前面LIBERO的Action归一化为什么值得较真1.1 一次来自实验结果的分歧LIBERO是一个被广泛使用的机器人操作基准包含Spatial、Object、Goal、100等不同套件任务覆盖了桌面抓取、开箱、放书这类典型操作数据以HDF5格式发布每个trajectory里除了图像观测和状态外还有一条核心的actions序列。很多研究者拿到数据后的第一件事就是写一个dataloader把actions直接从文件里读出来作为策略网络的回归目标。问题就出在这个“直接读出来”上。实际训练时网络的输出层通常是有界的例如用tanh把动作限制在[-1,1]附近。如果我们直接拿原始action做回归就必须在损失函数、输出层scale、数据归一化三者之间做取舍。不同的取舍方式会直接改变梯度回传的尺度进而影响最终策略的行为。我在那个失败的复现里遇到的情况是对方在dataloader里加了per-dimension标准化动作被缩放到接近零均值单位方差的样子而我的脚本只做了全局范围缩放。两者网络结构完全相同但动作分布的形态完全不同训练曲线自然对不上。1.2 谁最需要弄懂这件事我觉得这篇内容适合三类人。第一类是用LIBERO跑baseline做消融的人。如果你发现自己的分数和论文报告差很多在怀疑模型实现之前先怀疑动作归一化的对齐情况。第二类是做多数据集对比的人。LIBERO、RoboMimic、Meta-World这类基准经常被放在一起比较但每个数据集的动作定义、频率、量纲都不一样直接把一套dataloader挪过去用隐藏差异非常大。第三类是想把LIBERO当“标准题库”用的人。任何标准题库的前提是测试口径统一。动作归一化这种预处理层面的口径不统一会导致同样的模型在同样任务上出现明显的分数漂移这在科研复现里是很伤信任的事。2. 我拆开HDF5看到的Action结构7维、量纲与分布全不一致2.1 七个维度到底是什么先回归一下LIBERO数据里动作的基本含义。我在实际读取官方HDF5文件时每个demo组下的actions数组形状通常是(T, 7)T是这条轨迹的step数。七个维度可以分成三组前3维是末端执行器的位置增量也就是当前时刻相对于上一时刻的笛卡尔位移。这个增量的量级很小通常一个控制周期里末端只移动几厘米甚至更少。中间3维是末端执行器的姿态增量一般以轴角形式表达。轴角的数值范围比位置增量大不少尤其是某些需要大范围转动手腕的任务里单步角度变化可以达到零点几甚至更高。最后一维是夹爪指令。多数LIBERO任务里夹爪只有开和闭两种状态序列上表现为0/1或者接近0/1的连续值。但有些任务的记录里夹爪状态在切换瞬间会出现中间值比如0.5、0.6这个细节对归一化影响非常大。2.2 实测统计位置增量、旋转增量、夹爪指令的分布差异我在本地挑了一个SPATIAL套件里的典型桌面操作任务把训练集轨迹的所有action拼接在一起计算了逐维度的统计量。为了避免误导大家我把数据做成了示例表格不同任务会有些差异但量级关系大体如此维度含义meanstdminmax0~2位置增量约0.002约0.04-0.250.233~5姿态增量约0.003约0.18-1.201.356夹爪指令约0.55约0.470.001.00从这个表格能直接看出三个重要事实。第一位置增量和姿态增量根本不是一个量纲量级。位置增量集中在-0.2到0.2之间姿态增量却能到-1.2到1.2方差差了将近一个数量级。如果对全维度做一个统一scale最后姿态维度的有效梯度会被位置维度完全淹没。第二夹爪维度是离散分布均值0.5左右方差接近0.5说明它基本在两个端点之间跳动。对这种分布做标准的z-score或min-max归一化得到的结果和图们平时熟悉的连续动作都不一样。第三不同任务的统计量差异也很大。OPEN类任务通常位置增量集中在较小的范围里而需要把物体搬运到指定位置的SPATIAL任务会有明显更大的位置增量峰值。这意味着用某个任务的统计量去归一化另一个任务本身就会引入偏差。2.3 跨任务之间动作分布甚至不同归一化无法一劳永逸这一点是我做跨套件对比实验时最意外的收获。LIBERO的GOAL套件和OBJECT套件看起来都是桌面操作但动作分布差异非常明显。GOAL套件的任务往往更注重末端姿态调整比如把杯子放到指定姿态旋转维度的单步变化更大而且变化方向非常集中。OBJECT套件的任务大多是抓取和移动物体位置增量占比高旋转变化相对平缓。SPATIAL套件则对位置精度要求高但动作范围通常比较小。如果训练时假设所有任务共享同一套动作归一化统计量会出现两种情况要么大部分任务的动作分布被压扁模型学不到细粒度控制要么为了少数任务拉大scale导致梯度爆炸或饱和。LIBERO官方没有在数据集里附带一个按任务划分的归一化参数表这就把每个研究者都推向了“自己猜一套”的境地。3. “不透明性”到底藏在哪官方数据里没有告诉你的四个边界3.1 边界一归一化参数来自训练集还是全量数据集这是最隐蔽的问题之一。假设你决定做z-score归一化那么均值和方差是从哪里算的如果是从全部训练轨迹算出来的全局统计量那这组参数是确定的推理时也能直接复用。但问题是很多脚本写得很粗糙在构建dataloader时直接对整个dataset求了一次均值方差然后不管后续训练集、验证集切片怎么取都复用同一个统计量。这样虽然方便但训练集和验证集的动作分布如果稍有偏移验证时的归一化结果就不是模型在训练时见到的样子。更危险的是反向操作有些实现里归一化参数是在每个batch内动态计算的。训练时模型看到的是被动态缩放的动作推理时又按全局参数反算目标和输入的空间都被改变了模型输出自然不稳定。我在检查自己那个复现脚本时找到的问题根源就在这里它把归一化统计量做成了局部变量在每次迭代时重新计算而且计算时还混入了同一个batch里未来时刻的数据造成了信息泄漏。3.2 边界二按维度归一化还是对整条向量做统一缩放这个选择直接影响模型的行为。按维度归一化意味着每个维度都有自己的scale和shift这在数学上等价于对action空间做了仿射变换通常能让每个维度的数值范围都落在网络输出层容易表达的范围里。但统一缩放就不同了。如果只是把整个7维向量乘一个常数比如0.1那么位置增量会被压得很小姿态增量依然偏大。网络为了拟合动作要么把权重调大要么趋向饱和。我在实验里观察到使用统一缩放的模型在评估时经常出现末端抖动因为网络要在一个很窄的范围里做出高精度的位置预测又要在较宽的范围里表达姿态变化两边很难同时兼顾。不是说统一缩放一定不行而是LIBERO官方从没交代过“我建议你用哪一种”。不同论文里看着都是“我们对动作做了归一化”实际上可能一个用的是per-dimension分位数变换另一个用的是全局缩放这种差异会被当成模型能力差异但实际上只是数据预处理不同。3.3 边界三旋转维度到底要不要限制模长旋转维度以轴角表示时有一个天然问题角度接近±π时存在周期性模长本身可能非常大。我在某个任务里甚至见过单步姿态增量超过1.5的情况这已经超过一个控制周期内物理上合理的转动量说明数据记录里混入了重试或异常动作。这种情况下如果对旋转维度做min-max归一化最大值和最小值完全由个别异常轨迹决定正常轨迹里的旋转变化会被压缩得非常微小。模型学习到的逆变换本身没问题但梯度传播经过这个压缩区间后对旋转细节的表达就会变得迟钝。我后来试过一种更稳健的做法对旋转维度不做数据驱动归一化而是直接除以一个固定常数比如π。这样所有旋转增量都被映射到[-1,1]左右避免了统计量被异常值污染的问题。这个办法不依赖数据集统计因此在跨任务复用时更稳定。3.4 边界四夹爪维度该不该做连续化处理夹爪维度是Action归一化里最容易被粗暴处理的地方。常见做法是先对夹爪维度做z-score或min-max然后当作连续目标回归推理时再用0.5阈值二值化。这样做的直接后果是模型在夹爪切换瞬间会有模糊输出。数据里如果已经出现了0.5这类中间值回归目标本身就带有不确定性模型会倾向于输出0.4到0.6之间的值最终是否夹住完全取决于阈值怎么选。这个阈值0.4还是0.6在实际评估中可能造成几个百分点的成功率差异但它完全不在模型结构里而是藏在数据预处理代码的某个常量里。我把这个问题归为“不透明性”是因为LIBERO的评估脚本里没有对夹爪维度的反变换阈值做统一规定。有些工作设置为0.5有些设置为0.3最后报告出来的“开箱成功率”实际上不是同一种东西。4. 我的一次逆向排查从训练曲线异常到归一化约定复现4.1 现象同样的baseline不同机器上结果对不上回到开头的那个问题。我在本地复现时用的是一套非常标准的BC训练代码ResNet视觉编码器、以状态加视觉作为condition的Transformer policy、MSE损失回归action。训练前30轮loss下降很正常验证集却不动成功率一直趴在地板上。于是我把评估时网络每一步输出的action直接打印出来和验证集里的目标action一起画了分布图。发现网络输出集中在-1附近明显是饱和状态。也就是说网络学到了一个非常保守的策略几乎不产生动作增量。那时候我就怀疑是target action的归一化尺度和网络输出层的范围不一致。4.2 排查把中间层的动作统计量打出来对比我做了一个很笨但很有效的排查步骤分别在训练脚本的dataloader出口和评估脚本的policy推理出口打印同一个task里同一批动作的统计量。训练脚本里target action在进入模型前已经被标准化到均值接近0、标准差接近1。评估时同一个模型输出的action被直接传给环境执行但这个输出又经过了环境侧的额外scale处理。两边对“动作”的定义不同一个是被缩放后的归一化空间一个是真实环境空间。真正的关键点是在checkpoint和dataloader之间训练时模型学习的是归一化空间的映射评估时却要它在真实动作空间里输出这中间缺少了一次逆变换。4.3 结论潜藏约定在checkpoint与dataloader之间最后我确认问题不是单一bug而是整套数据管线里没有把“归一化约定”固化成持久化对象。训练脚本里用到的统计参数没有随checkpoint一起保存评估脚本又用另一套逻辑去解释模型输出于是复现时只能靠手动对齐。这也是我把这口锅算到“不透明性”头上的原因。一个透明、可复现的Action归一化流程应该把统计量、逆变换逻辑、阈值、是否裁剪这些参数全部固化下来让它成为checkpoint的一部分而不是散落在dataloader代码里。5. 透明化改造我目前使用的Action归一化与逆变换方案5.1 前提与约定经过上面这些折腾我现在跑LIBERO时对Action归一化有了一套明确约定。提前声明这不是官方推荐配置而是我基于实践总结出的一个“确定性方案”它至少保证同一个checkpoint在任意机器上都能复现。方案的核心约定有三条第一所有统计量只从训练集轨迹里计算绝不使用任何验证集或测试集轨迹。第二归一化参数必须保存成独立文件并在推理阶段被加载不能重新计算。第三夹爪维度单独处理不参与连续的z-score或min-max变换。5.2 按维度分离的归一化流程在实际代码里我用了一个自定义的ActionNormalizer伪代码如下import numpy as np class ActionNormalizer: def __init__(self, action_stats): # action_stats 包含位置维度的分位数统计、旋转固定尺度、夹爪阈值 self.pos_low action_stats[pos_low] # shape (3,) self.pos_high action_stats[pos_high] # shape (3,) self.rot_scale action_stats[rot_scale] # 固定为 pi不使用数据统计 self.gripper_threshold 0.5 def normalize(self, action): # action: (T, 7) 或 (7,) act action.copy() # 位置维度min-max 映射到 [-1, 1] act[:, :3] 2.0 * (act[:, :3] - self.pos_low) / (self.pos_high - self.pos_low 1e-6) - 1.0 # 旋转维度固定除以 pi限制在 [-1, 1] 附近 act[:, 3:6] act[:, 3:6] / self.rot_scale # 夹爪维度直接二值化不参与连续归一化 act[:, 6] (act[:, 6] self.gripper_threshold).astype(np.float32) return act def unnormalize(self, norm_action): act norm_action.copy() act[:, :3] (act[:, :3] 1.0) * (self.pos_high - self.pos_low) / 2.0 self.pos_low act[:, 3:6] act[:, 3:6] * self.rot_scale act[:, 6] (act[:, 6] 0.0).astype(np.float32) return act这段代码的思路是对物理含义完全不同的三个子空间分别做归一化。位置增量用训练集的分位数而不是全局min-max因为分位数能抵抗个别异常trajectory的干扰旋转增量用固定尺度π保证跨任务复用时不依赖数据分布夹爪完全不连续化保留二值特性。其中位置维度我宁愿用p5和p95分位数作为low和high而不是全局min和max。这样即使数据里混入了几条因为仿真lag产生的超大增量也不会把正常动作区间压得太扁。5.3 逆变换与评测一致性逆变换是很多人真正栽跟头的地方。训练时网络输出的normalized action要经过unnormalize变成真实动作传给环境这个流程必须在评估脚本里与训练脚本严格一致。我在项目里专门做了一个强制约定normalize和unnormalize放在同一个模块里所有读取checkpoint的脚本引用同一个函数。避免一个仓库里出现三份不同的逆变换副本。我在检查别人的复现代码时见过一个人把归一化写在utils里把逆变换直接写在eval循环里两边差了0.2的scale导致输出动作一直偏大。另一个很实际的建议把pos_low、pos_high这些参数和模型的state_dict一起打包保存。每次评估时从checkpoint里读统计量而不是从某个看起来名字很像的json里猜。5.4 与官方benchmark对照时如何对齐口径如果你需要用LIBERO官方baseline的分数做对照那么重点不是“把自己方案换掉”而是先想清楚官方baseline的动作预处理是什么。我自己的习惯是先跑一个最小化的对照实验用官方公开代码里的数据加载方式测量target action的均值、方差、min、max再和自己的统计量对比。如果差异在可接受范围就用自己方案继续如果差异巨大就检查是不是对方用了完全不同的归一化逻辑。真正的对照实验里成功率差一两个百分点可以归因到随机性但差几十个百分点时大概率是动作空间定义都不一样了。这时候再去调模型结构是浪费时间。6. 这件事反映出的工具箱问题开放数据集缺少“预处理SOP”6.1 数据集的Action应该像图像一样有标准预处理吗现在机器人社区的benchmark在视觉输入部分普遍有约定比如ResNet系列会使用固定的ImageNet均值和方差做标准化。虽然没有官方强制但大家默认一致复现问题就少很多。Action归一化却没有类似公约。它跟图像标准化一样是直接影响模型输入分布的东西却完全没有标准预处理方式。LIBERO数据集做得很好的一点是提供了高质量的轨迹数据和统一的仿真环境但恰恰在动作空间处理上留了太多自由解释权。我不认为需要官方把某个特定归一化方案定成唯一标准但至少应该在数据发布时附带一份按任务划分的动作统计信息例如每个维度的均值、方差、分位数、样本数。这样研究者在自己的pipeline里做任何变换都能方便地校准回官方范围。6.2 给LIBERO使用者的三条实操建议如果你现在正准备用LIBERO我会给你三条非常具体的建议。第一拿到数据集后先花十分钟统计action分布不要直接开训练。写一个简单脚本输出每个任务的前3维、3~6维、夹爪维度的mean、std、min、max把结果存成一张表。这一步能让你后续排查很多无头绪的波动。第二把归一化参数纳入checkpoint管理。无论你选择什么归一化策略相关统计量、阈值、尺度常数都要和模型权重一起保存。宁可重复存储也不要让评估脚本去重新推导。第三在报告结果时同时报告你的action预处理细节包括是否按维度、统计量来自哪里、夹爪阈值是多少、推理时是否有clip。这些信息在论文里写两行就能显著降低复现成本。我自己现在跑LIBERO时的习惯是每次实验先跑一遍action统计脚本确认当前任务的分布没有异常再把归一化参数写死到实验配置文件里然后才开始训练。这个看似多余的行动帮我省掉了大量无效的调参时间。Action归一化在LIBERO里最大的坑不是它复杂而是它太容易被当成一段不重要的边缘代码等出了复现问题再回头看代价已经大了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →