目录
- [1 问题说明](#1 问题说明)
- [2 recompute_stats的具体计算逻辑](#2 recompute_stats的具体计算逻辑)
-
- [2.1 统计信息的存储位置](#2.1 统计信息的存储位置)
- [2.2 默认计算路径](#2.2 默认计算路径)
- [2.3 relative action 的定义](#2.3 relative action 的定义)
- [2.4 合法 chunk 的筛选](#2.4 合法 chunk 的筛选)
- [2.5 `relative_exclude_joints`](#2.5
relative_exclude_joints) - [2.6 为什么同一个action会被多次统计?](#2.6 为什么同一个action会被多次统计?)
- [2.7 计算哪些统计量?](#2.7 计算哪些统计量?)
- [2.8 为什么普通路径要跳过action?](#2.8 为什么普通路径要跳过action?)
- [2.9 参数要求](#2.9 参数要求)
1 问题说明
默认情况下, π 0.5 \pi_{0.5} π0.5输出的是绝对动作。启用relative actions后,模型将预测相对于机器人当前状态的动作增量,而不是绝对目标动作。在某些任务或机器人配置下,这种方式能够提升训练的稳定性。
如果要使用相对动作,首先需要使用命令行工具(CLI)重新计算数据集在相对动作空间下的统计信息。
bash
lerobot-edit-dataset \
--repo_id your_dataset \
--operation.type recompute_stats \
--operation.relative_action true \
--operation.chunk_size 50 \
--operation.relative_exclude_joints "['gripper']" \
--push_to_hub true
或者,使用 Python 的等效实现如下:
python
from lerobot.datasets import LeRobotDataset, recompute_stats
dataset = LeRobotDataset("your_dataset")
recompute_stats(dataset, relative_action=True, chunk_size=50, relative_exclude_joints=["gripper"])
dataset.push_to_hub()
chunk_size应与策略模型的chunk_size参数保持一致( π 0.5 \pi_{0.5} π0.5的默认值为 50)。relative_exclude_joints用于指定哪些关节仍采用绝对动作表示,而不转换为相对动作(例如夹爪的开合控制)。如果需要将更新后的数据集统计信息上传到Hugging Face Hub,请添加参数--push_to_hub true。
然后,在训练时启用相对动作:
bash
lerobot-train \
--dataset.repo_id=your_dataset \
--policy.type=pi05 \
--policy.use_relative_actions=true \
--policy.relative_exclude_joints='["gripper"]' \
...
2 recompute_stats的具体计算逻辑
代码位置:src/lerobot/datasets/dataset_tools.py中的recompute_stats()函数。它用于重新计算 LeRobot 数据集的全局统计信息,并写入:
text
meta/stats.json
它不会修改原始数据文件,也不会更新:
text
data/**/*.parquet
meta/episodes/**/*.parquet
2.1 统计信息的存储位置
数据集中有两类 stats:
| 文件 | 内容 | 主要用途 |
|---|---|---|
meta/stats.json |
整个数据集的全局统计量 | 训练时 action/state 的归一化 |
meta/episodes/**/*.parquet |
每个 episode 的局部统计量 | 数据集录制、聚合、子集等元数据操作 |
训练实际读取的是 dataset.meta.stats,其来源为 meta/stats.json。
因此,recompute_stats() 的目标是修正或重建全局训练统计,而不是回写 episode 元数据中的历史 stats。
2.2 默认计算路径
函数先从 dataset.meta.features 筛选需要统计的数值特征:
python
numeric_features = {
k: v
for k, v in features.items()
if v["dtype"] not in ["image", "video", "string"]
and k not in {"index", "episode_index", "task_index", "frame_index", "timestamp"}
}
默认 skip_image_video=True,因此只重新计算数值特征,例如:
text
action
observation.state
其他数值 observation
随后它会:
- 读取
data/**/*.parquet - 按
episode_index切分每个 episode - 为每个 feature 计算 episode 内的
min/max/mean/std/quantiles - 将所有 episode 的统计聚合为数据集全局统计
- 写入
meta/stats.json
对于普通 absolute action,逻辑相当于:
text
stats["action"] = statistics(all absolute actions)
relative_action=True 的计算逻辑
当以下条件都成立时,函数会改用相对动作的方式计算 action stats:
python
relative_action is True
ACTION in features
OBS_STATE in features
也就是说,数据集至少必须同时包含:
text
action
observation.state
2.3 relative action 的定义
对于一个 action chunk,其起始帧为 t,长度为 K:
text
action chunk:
[a_t, a_(t+1), ..., a_(t+K-1)]
current state:
s_t
相对动作定义为:
text
relative_action(t, j) = action(t + j) - state(t)
其中:
text
j = 0, 1, ..., K - 1
重点是:chunk 内所有未来 action 都减去 chunk 起始帧的 state。
它不是:
text
action(t + j) - state(t + j)
而是:
text
action(t + j) - state(t)
这与 action-chunk policy 的训练输入一致:模型基于当前状态 state(t),预测未来连续 K 步动作。
2.4 合法 chunk 的筛选
假设:
text
chunk_size = 3
episode 0 的帧为:
text
0, 1, 2, 3, 4
则合法 chunk 为:
text
起点 0 -> [0, 1, 2]
起点 1 -> [1, 2, 3]
起点 2 -> [2, 3, 4]
如果 episode 1 从帧 5 开始,则下面的 chunk 无效:
text
起点 3 -> [3, 4, 5]
起点 4 -> [4, 5, 6]
因为它们跨越了 episode 边界。
实现仅比较 chunk 首尾帧的 episode_index:
python
valid = episode_indices[starts] == episode_indices[starts + chunk_size - 1]
一个长度为 L 的 episode 可提供的合法 chunk 数量为:
text
max(L - chunk_size + 1, 0)
2.5 relative_exclude_joints
默认情况下:
python
relative_exclude_joints = ["gripper"]
这意味着名称中包含 gripper 的 action 维度不做相对化,仍保留绝对值。
例如:
text
action_names = ["shoulder", "elbow", "wrist", "gripper"]
relative_mask = [1, 1, 1, 0]
含义:
text
1 -> action - state
0 -> 保持原 action
若:
text
state(t) = [10, 20, 30, 0.4]
原始 action chunk 为:
text
a_t = [11, 19, 31, 0.5]
a_(t+1) = [13, 18, 34, 0.6]
a_(t+2) = [16, 17, 38, 0.7]
转换后:
text
relative a_t = [1, -1, 1, 0.5]
relative a_(t+1) = [3, -2, 4, 0.6]
relative a_(t+2) = [6, -3, 8, 0.7]
前三个关节维度减去起始 state;gripper 维度保持绝对值。
若 action metadata 中没有 action.names,代码无法按名称识别 gripper,此时掩码会默认使全部维度相对化。
2.6 为什么同一个action会被多次统计?
chunk 使用滑动窗口,因此相邻 chunk 会重叠。
例如帧 2 会出现在:
text
chunk [0, 1, 2]: action(2) - state(0)
chunk [1, 2, 3]: action(2) - state(1)
chunk [2, 3, 4]: action(2) - state(2)
虽然是同一个绝对 action,但参考 state 不同,所以它们是不同的相对动作样本。
最终参与统计的样本数量是:
text
合法 chunk 数 × chunk_size
这刻意模拟了训练中所有可能的「当前 state + 未来 action chunk」组合。
2.7 计算哪些统计量?
所有相对 action chunk 会被展平为:
text
(num_valid_chunks × chunk_size, action_dim)
并逐维计算:
text
min
max
mean
std
count
q01
q10
q50
q90
q99
其中 quantile 使用运行中的直方图近似计算。
最终结果写入:
text
meta/stats.json -> action
2.8 为什么普通路径要跳过action?
普通统计路径会针对原始数据计算:
text
statistics(absolute action)
但启用相对动作训练后,模型接收到的是:
text
statistics(action - chunk_start_state)
这两个分布通常不同,不能混用。
因此在相对 action stats 计算完成后,代码会执行:
python
features_to_compute.pop(ACTION, None)
避免后续普通的逐 episode absolute-action 统计覆盖相对 action 的结果。
最后结构为:
text
meta/stats.json:
observation.state:
# 原始绝对 state 的统计量
action:
# 相对 action 的统计量
# relative_exclude_joints 的维度仍是绝对 action 统计量
2.9 参数要求
chunk_size 应与训练 policy 的 action chunk 长度一致:
python
recompute_stats(
dataset,
relative_action=True,
chunk_size=policy.chunk_size,
)
若两者不同,计算 stats 时的相对 action 分布与训练时实际输入模型的分布不一致,可能导致归一化不正确。