☰
LIBERO动作归一化:复现差异背后的隐藏约定与工程实践
2026/10/8 3:09:07 网站建设 项目流程

上个月我在复现一个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拼接在一起,计算了逐维度的统计量。为了避免误导大家,我把数据做成了示例表格,不同任务会有些差异,但量级关系大体如此:

维度含义meanstdminmax
0~2位置增量约0.002约0.04-0.250.23
3~5姿态增量约0.003约0.18-1.201.35
6夹爪指令约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里最大的坑不是它复杂,而是它太容易被当成一段不重要的边缘代码,等出了复现问题再回头看,代价已经大了。

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

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

立即咨询