视频基准测试
问题
如何在以下几个方面找到最佳平衡:
- 最大化随机访问时的加载速度
- 最小化磁盘存储空间
- 最大化策略的成功率
- 在不同设备/平台上解码视频的兼容性(如视频播放器、网页浏览器)
如何编码视频?
- 使用哪种视频编解码器(
-vcodec
)? h264、h265 还是 AV1? - 使用什么像素格式(
-pix_fmt
)?yuv444p
还是yuv420p
? - 压缩程度(
-crf
)如何? 无压缩用0
,中等压缩用25
还是极端压缩用50+
? - 选择什么关键帧频率(
-g
)? 每10
帧一个关键帧?
如何解码视频?
- 使用哪个
解码器
?torchvision
、torchaudio
、ffmpegio
、decord
还是nvc
? - 基准测试中请求时间戳时使用什么场景?(
timestamps_mode
)
变量
图像内容和尺寸
我们预计不同类型的数据集会需要不同的最优设置,比如来自模拟环境的图像、来自公寓的真实场景、工厂场景、户外场景,或者场景中有大量移动物体等。同样,加载时间可能不会随图像尺寸(分辨率)线性变化。
因此,我们在四个具有代表性的数据集上运行这个基准测试:
lerobot/pusht_image
: (96 x 96 像素)简单几何形状的模拟环境,固定摄像头。aliberts/aloha_mobile_shrimp_image
: (480 x 640 像素)室内真实场景,移动摄像头。aliberts/paris_street
: (720 x 1280 像素)户外真实场景,移动摄像头。aliberts/kitchen
: (1080 x 1920 像素)室内真实场景,固定摄像头。
注意: 用于此基准测试的数据集需要是图像数据集,而不是视频数据集。
数据增强
如果我们使用各种数据增强来训练策略使其更加鲁棒(例如对颜色变化、压缩等更加鲁棒),我们可能需要重新进行这个基准测试并找到更好的设置。
编码参数
参数 | 值 |
---|---|
vcodec | libx264 , libx265 , libsvtav1 |
pix_fmt | yuv444p , yuv420p |
g | 1 , 2 , 3 , 4 , 5 , 6 , 10 , 15 , 20 , 40 , None |
crf | 0 , 5 , 10 , 15 , 20 , 25 , 30 , 40 , 50 , None |
注意不同的视频编解码器可能会对 crf
值有不同的解释。换句话说,在一个编解码器中使用的相同值不一定会在另一个编解码器中产生相同的压缩级别。实际上,默认值(None
)在不同的视频编解码器之间也不相同。重要的是,这种情况也适用于许多其他 ffmpeg 参数,比如指定关键帧频率的 g
。
有关这些参数的完整列表和文档,请参见根据使用的视频编解码器查看 ffmpeg 文档:
- h264: https://trac.ffmpeg.org/wiki/Encode/H.264
- h265: https://trac.ffmpeg.org/wiki/Encode/H.265
- AV1: https://trac.ffmpeg.org/wiki/Encode/AV1
解码参数
解码器
我们测试了 torchvision 的两个视频解码后端:
pyav
(默认)video_reader
(需要从源代码构建 torchvision)
请求的时间戳
考虑到视频解码的工作方式,一旦加载了关键帧,后续帧的解码就会很快。
这当然会受到编码时 -g
参数的影响,该参数指定关键帧的频率。考虑到我们在机器人策略中的典型用例可能会在不同的随机位置请求几个时间戳,我们想要用以下场景复现这些用例:
1_frame
: 1 帧2_frames
: 2 个连续帧(例如[t, t + 1 / fps]
)6_frames
: 6 个连续帧(例如[t + i / fps for i in range(6)]
)
注意这与观看电影等典型用例有很大不同,在那种情况下,每一帧都是从头到尾按顺序加载的,使用较大的 -g
值是可以接受的。
此外,由于某些策略可能会请求相隔几帧的单个时间戳,我们还有以下场景:
2_frames_4_space
: 2 帧之间间隔 4 个连续帧(例如[t, t + 5 / fps]
)
然而,由于 pyav
的视频解码实现方式,我们无法进行精确的定位,所以在实践中这个场景本质上与 6_frames
相同,因为 t
和 t + 5 / fps
之间的所有 6 帧都会被解码。
指标
数据压缩比(越低越好)
video_images_size_ratio
是编码视频占用的磁盘空间与原始图像占用的磁盘空间之比。例如,video_images_size_ratio=25%
意味着视频占用的磁盘空间比原始图像少 4 倍。
加载时间比(越低越好)
video_images_load_time_ratio
是从视频解码给定时间戳的帧所需时间与加载相同原始图像所需时间之比。越低越好。例如,video_images_load_time_ratio=200%
意味着从视频解码的速度比加载原始图像慢 2 倍。
平均均方误差(越低越好)
avg_mse
是每个解码帧与其对应原始图像之间的平均均方误差,在所有请求的时间戳上取平均,并除以图像中的像素数以便在切换到不同图像尺寸时具有可比性。
平均峰值信噪比(越高越好)
avg_psnr
衡量信号的最大可能功率与影响其表示保真度的噪声功率之比。更高的 PSNR 表示更好的质量。
平均结构相似性指数(越高越好)
avg_ssim
通过比较亮度、对比度和结构来评估图像的感知质量。SSIM 值范围从 -1 到 1,其中 1 表示完全相似。
这些指标无法衡量的一个方面是编码在各平台上的兼容性,特别是在网页浏览器上用于可视化目的。
h264、h265 和 AV1 都是常用的编解码器,应该不会造成问题。但是,色度二次采样(pix_fmt
)格式可能会影响兼容性:
yuv420p
在各种平台上得到更广泛的支持,包括网页浏览器。yuv444p
提供更高的色彩保真度,但可能不会得到如此广泛的支持。
基准测试的工作原理
基准测试在每个数据集的第一个回合上评估视频帧的编码和解码。
编码: 对于每个 vcodec
和 pix_fmt
组合,我们使用 g
和 crf
的默认值,然后改变其中一个值(要么是 g
要么是 crf
)为指定值之一(我们不测试所有组合,因为这在计算上太过繁重)。
这给出了一组唯一的编码参数,用于对回合进行编码。
解码: 然后,对于每个唯一的编码,我们遍历解码参数 backend
和 timestamps_mode
的每种组合。对于每种组合,我们记录多个样本(由 --num-samples
指定)的指标。为了提高效率,这个过程是并行的,进程数可以通过 --num-workers
控制。理想情况下,最好让 --num-samples
能被 --num-workers
整除。
中间结果保存在 csv 表格中,每个 vcodec
和 pix_fmt
组合一个表格。
然后将这些表格全部合并成一个用于分析的表格。
注意事项
我们尝试测量编码和解码最重要的参数。然而,出于计算原因,我们无法测试所有组合。
示例
要快速运行,你可以尝试这些参数:
python benchmark/video/run_video_benchmark.py \
--output-dir outputs/video_benchmark \
--repo-ids \
lerobot/pusht_image \
aliberts/aloha_mobile_shrimp_image \
--vcodec libx264 libx265 \
--pix-fmt yuv444p yuv420p \
--g 2 20 None \
--crf 10 40 None \
--timestamps-modes 1_frame 2_frames \
--backends pyav video_reader \
--num-samples 5 \
--num-workers 5 \
--save-frames 0
结果
复现
我们使用以下参数运行基准测试:
# h264 和 h265 编码
python benchmark/video/run_video_benchmark.py \
--output-dir outputs/video_benchmark \
--repo-ids \
lerobot/pusht_image \
aliberts/aloha_mobile_shrimp_image \
aliberts/paris_street \
aliberts/kitchen \
--vcodec libx264 libx265 \
--pix-fmt yuv444p yuv420p \
--g 1 2 3 4 5 6 10 15 20 40 None \
--crf 0 5 10 15 20 25 30 40 50 None \
--timestamps-modes 1_frame 2_frames 6_frames \
--backends pyav video_reader \
--num-samples 50 \
--num-workers 5 \
--save-frames 1
# av1 编码(仅兼容 yuv420p 和 pyav 解码器)
python benchmark/video/run_video_benchmark.py \
--output-dir outputs/video_benchmark \
--repo-ids \
lerobot/pusht_image \
aliberts/aloha_mobile_shrimp_image \
aliberts/paris_street \
aliberts/kitchen \
--vcodec libsvtav1 \
--pix-fmt yuv420p \
--g 1 2 3 4 5 6 10 15 20 40 None \
--crf 0 5 10 15 20 25 30 40 50 None \
--timestamps-modes 1_frame 2_frames 6_frames \
--backends pyav \
--num-samples 50 \
--num-workers 5 \
--save-frames 1
完整结果可在这里查看
LeRobotDataset 选择的参数
考虑到这些结果,我们选择了我们认为最好的编码参数集:
- vcodec:
libsvtav1
- pix-fmt:
yuv420p
- g:
2
- crf:
30
由于我们使用 av1 编码,我们选择 pyav
解码器,因为 video_reader
不支持它(而且 pyav
不需要自定义构建 torchvision
)。
总结
这些表格显示了 g=2
和 crf=30
时的结果,使用 timestamps-modes=6_frames
和 backend=pyav
video_images_size_ratio | vcodec | pix_fmt | |||
---|---|---|---|---|---|
libx264 | libx265 | libsvtav1 | |||
repo_id | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p |
lerobot/pusht_image | 16.97% | 17.58% | 18.57% | 18.86% | 22.06% |
aliberts/aloha_mobile_shrimp_image | 2.14% | 2.11% | 1.38% | 1.37% | 5.59% |
aliberts/paris_street | 2.12% | 2.13% | 1.54% | 1.54% | 4.43% |
aliberts/kitchen | 1.40% | 1.39% | 1.00% | 1.00% | 2.52% |
video_images_load_time_ratio | vcodec | pix_fmt | |||
---|---|---|---|---|---|
libx264 | libx265 | libsvtav1 | |||
repo_id | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p |
lerobot/pusht_image | 6.45 | 5.19 | 1.90 | 2.12 | 2.47 |
aliberts/aloha_mobile_shrimp_image | 11.80 | 7.92 | 0.71 | 0.85 | 0.48 |
aliberts/paris_street | 2.21 | 2.05 | 0.36 | 0.49 | 0.30 |
aliberts/kitchen | 1.46 | 1.46 | 0.28 | 0.51 | 0.26 |
vcodec | pix_fmt | |||||
---|---|---|---|---|---|---|
libx264 | libx265 | libsvtav1 | ||||
repo_id | metric | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p |
lerobot/pusht_image | avg_mse | 2.90E-04 | 2.03E-04 | 3.13E-04 | 2.29E-04 | 2.19E-04 |
avg_psnr | 35.44 | 37.07 | 35.49 | 37.30 | 37.20 | |
avg_ssim | 98.28% | 98.85% | 98.31% | 98.84% | 98.72% | |
aliberts/aloha_mobile_shrimp_image | avg_mse | 2.76E-04 | 2.59E-04 | 3.17E-04 | 3.06E-04 | 1.30E-04 |
avg_psnr | 35.91 | 36.21 | 35.88 | 36.09 | 40.17 | |
avg_ssim | 95.19% | 95.18% | 95.00% | 95.05% | 97.73% | |
aliberts/paris_street | avg_mse | 6.89E-04 | 6.70E-04 | 4.03E-03 | 4.02E-03 | 3.09E-04 |
avg_psnr | 33.48 | 33.68 | 32.05 | 32.15 | 35.40 | |
avg_ssim | 93.76% | 93.75% | 89.46% | 89.46% | 95.46% | |
aliberts/kitchen | avg_mse | 2.50E-04 | 2.24E-04 | 4.28E-04 | 4.18E-04 | 1.53E-04 |
avg_psnr | 36.73 | 37.33 | 36.56 | 36.75 | 39.12 | |
avg_ssim | 95.47% | 95.58% | 95.52% | 95.53% | 96.82% |
视频基准测试说明
一、基准测试的意义
视频基准测试(run_video_benchmark.py)主要解决以下关键问题:
- 数据存储效率
- 评估不同视频编码方案的压缩效率
- 平衡存储空间与视频质量
- 优化数据集的磁盘占用
- 加载性能优化
- 测试不同解码方案的加载速度
- 评估随机访问性能
- 优化训练过程中的数据加载
- 跨平台兼容性
- 确保在不同设备上的解码兼容性
- 评估不同编解码器的支持程度
- 验证在网页浏览器中的可视化效果
二、测试参数说明
1. 编码参数
BASE_ENCODING = {
"vcodec": "libx264", # 视频编码器
"pix_fmt": "yuv444p", # 像素格式
"g": 2, # 关键帧间隔
"crf": None, # 压缩质量
}
2. 测试场景
DATASETS = [
"lerobot/pusht_image", # 96x96 模拟环境
"aliberts/aloha_mobile_shrimp_image", # 480x640 室内场景
"aliberts/paris_street", # 720x1280 户外场景
"aliberts/kitchen" # 1080x1920 室内场景
]
TIMESTAMPS_MODES = [
"1_frame", # 单帧加载
"2_frames", # 连续2帧
"2_frames_4_space", # 间隔4帧的2帧
"6_frames" # 连续6帧
]
三、性能指标
- 存储效率
# 计算压缩比
video_images_size_ratio = video_size_bytes / images_size_bytes
- 加载性能
# 计算加载时间比
video_images_load_time_ratio = avg_load_time_video_ms / avg_load_time_images_ms
- 图像质量
# 计算质量指标
mse = mean_squared_error(original_frames, decoded_frames)
psnr = peak_signal_noise_ratio(original_frames, decoded_frames)
ssim = structural_similarity(original_frames, decoded_frames)
四、在项目中的作用
- 数据集优化
- 指导数据集的视频编码配置选择
- 优化数据集的存储效率
- 确保数据加载性能
- 训练加速
- 减少数据加载瓶颈
- 优化批处理数据读取
- 提高训练效率
- 部署支持
- 确保模型在不同平台上的可用性
- 优化在线推理的数据加载
- 支持web端可视化需求
五、使用示例
- 运行基准测试:
python benchmarks/video/run_video_benchmark.py \
--repo-ids lerobot/pusht_image \
--vcodecs libx264 libx265 libsvtav1 \
--pix-fmts yuv420p yuv444p \
--num-samples 50
- 分析测试结果:
python benchmarks/video/plot_results.py \
--results-dir results/benchmark_video
- 应用最佳实践:
# 使用测试验证的最优参数
encoding_config = {
"vcodec": "libsvtav1", # 最佳编码器
"pix_fmt": "yuv420p", # 最佳像素格式
"g": 2, # 最优关键帧间隔
"crf": 30 # 最佳压缩质量
}
这个基准测试工具帮助我们在数据效率、加载性能和质量之间找到最佳平衡,对整个项目的数据处理和训练效率有重要影响。
LeRobot 自定义策略开发指南
一、策略接入基础
1. 目录结构
lerobot/
├── common/
│ └── policies/
│ ├── base.py # 基础策略类
│ ├── normalize.py # 数据归一化实现
│ ├── act/ # ACT策略实现
│ ├── diffusion/ # Diffusion策略实现
│ └── your_policy/ # 你的自定义策略
└── configs/
└── policy/
├── act.yaml # ACT配置
├── diffusion.yaml # Diffusion配置
└── your_policy.yaml # 你的策略配置
2. 基础类继承
# 来自 lerobot/common/policies/act/modeling_act.py
class YourPolicy(BasePolicy, PyTorchModelHubMixin):
"""自定义策略类"""
name = "your_policy"
def __init__(self, config, dataset_stats=None):
super().__init__()
self.config = config
# 1. 构建归一化层
self.normalize_inputs = Normalize(
config.input_shapes,
config.input_normalization_modes,
dataset_stats
)
self.normalize_targets = Normalize(
config.output_shapes,
config.output_normalization_modes,
dataset_stats
)
self.unnormalize_outputs = Unnormalize(
config.output_shapes,
config.output_normalization_modes,
dataset_stats
)
# 2. 构建网络组件
self.build_networks()
3. 配置类定义
# 来自 lerobot/common/policies/act/configuration_act.py
@dataclass
class YourPolicyConfig:
"""策略配置类"""
# 输入输出配置
input_shapes: dict
output_shapes: dict
input_normalization_modes: dict
output_normalization_modes: dict
# 网络架构配置
vision_backbone: str = "resnet18"
pretrained_backbone_weights: str = None
dim_model: int = 512
n_heads: int = 8
# 训练配置
dropout: float = 0.1
learning_rate: float = 1e-4
二、数据集处理
1. 数据集格式
# 来自 lerobot/common/datasets/lerobot_dataset.py
dataset = {
"hf_dataset": { # Hugging Face dataset格式
"observation.image": VideoFrame, # 视频帧
"observation.state": List[float32], # 状态向量
"action": List[float32], # 动作向量
"episode_index": int64, # 回合索引
"frame_index": int64, # 帧索引
"timestamp": float32, # 时间戳
"next.done": bool, # 回合结束标志
"index": int64 # 全局索引
},
"episode_data_index": {
"from": Tensor[int64], # 回合起始索引
"to": Tensor[int64] # 回合结束索引
}
}
2. 视频编码最佳实践
# 来自 lerobot/common/datasets/video_utils.py
def encode_video_frames(frames, output_path, fps=30):
"""将图像帧编码为视频"""
encoding = {
"vcodec": "libsvtav1", # 最佳编解码器
"pix_fmt": "yuv420p", # 最佳像素格式
"g": 2, # 关键帧间隔
"crf": 30 # 压缩质量
}
3. 数据增强配置
# 来自 lerobot/common/datasets/factory.py
def get_image_transforms(cfg):
"""获取图像增强配置"""
transforms = []
if cfg.training.image_transforms.enable:
transforms.extend([
v2.ColorJitter(
brightness=cfg.brightness,
contrast=cfg.contrast,
saturation=cfg.saturation,
hue=cfg.hue
),
v2.RandomAdjustSharpness(
sharpness_factor=cfg.sharpness,
p=cfg.sharpness_prob
)
])
return v2.Compose(transforms)
三、策略实现示例
1. ACT策略
# 来自 lerobot/common/policies/act/modeling_act.py
class ACTPolicy(BasePolicy):
"""ACT策略实现"""
def __init__(self, config):
super().__init__()
self.config = config
# 构建编码器
self.encoder = self._build_encoder()
self.decoder = self._build_decoder()
# 构建Transformer
self.transformer = self._build_transformer()
def forward(self, batch):
# 归一化输入
normalized = self.normalize_inputs(batch)
# 特征提取
features = self.encoder(normalized)
# Transformer处理
output = self.transformer(features)
# 解码动作
actions = self.decoder(output)
return actions
2. Diffusion策略
# 来自 lerobot/common/policies/diffusion/modeling_diffusion.py
class DiffusionPolicy(BasePolicy):
"""Diffusion策略实现"""
def __init__(self, config):
super().__init__()
self.config = config
# 构建扩散模型
self.diffusion = DiffusionModel(config)
# 构建编码器
self.encoder = self._build_encoder()
def forward(self, batch):
# 归一化输入
normalized = self.normalize_inputs(batch)
# 特征提取
features = self.encoder(normalized)
# 扩散过程
actions = self.diffusion(features)
return actions
四、训练和评估
1. 训练配置
# 来自 lerobot/configs/policy/act.yaml
training:
offline_steps: 100000
eval_freq: 20000
save_freq: 20000
batch_size: 8
lr: 1e-5
weight_decay: 1e-4
grad_clip_norm: 10
delta_timestamps:
action: "[i / ${fps} for i in range(${policy.chunk_size})]"
2. 评估配置
eval:
n_episodes: 50
batch_size: 50
五、性能优化
1. 数据加载优化
- 使用视频格式存储图像序列
- 选择最优的视频编码参数
- 使用多进程数据加载
2. 训练优化
- 使用混合精度训练
- 梯度裁剪和归一化
- 使用适当的批次大小
3. 推理优化
- 模型量化
- TensorRT加速
- 批处理推理
六、策略训练与验证
1. 基本训练流程
使用训练脚本进行训练:
python lerobot/scripts/train.py \
policy=your_policy \ # 你的策略配置
env=pusht \ # 使用的环境
dataset_repo_id=lerobot/pusht # 训练数据集
训练输出目录结构:
outputs/train/2024-XX-XX/XX-XX-XX_env_policy_default/
└── checkpoints/
├── 000250/ # 训练步数检查点
│ ├── pretrained_model/ # Hugging Face预训练模型目录
│ │ ├── config.json # 模型配置
│ │ ├── config.yaml # 完整Hydra配置
│ │ ├── model.safetensors # 模型权重
│ │ └── README.md # 模型说明
│ └── training_state.pth # 优化器/调度器状态
2. 恢复训练
从检查点恢复训练:
python lerobot/scripts/train.py \
hydra.run.dir=your/original/experiment/dir \
resume=true
3. 策略评估
使用评估脚本进行验证:
python lerobot/scripts/eval.py \
-p outputs/train/checkpoints/last/pretrained_model \
eval.n_episodes=10 \
eval.batch_size=10
4. 性能监控
启用 WandB 监控训练过程:
python lerobot/scripts/train.py \
policy=your_policy \
wandb.enable=true
主要监控指标包括:
- 训练损失
- 评估成功率
- 模型预测准确度
- 资源使用情况
5. 数据集可视化
使用可视化工具检查数据:
python lerobot/scripts/visualize_dataset.py \
--repo-id lerobot/pusht \
--episode-index 0
这将在 rerun.io 中显示:
- 相机流
- 机器人状态
- 动作序列
这些工具和流程可以帮助你有效地训练和验证自定义策略。需要了解更多细节吗?
七、数据格式转换示例
以 fourier_format.py 为例说明如何处理和转换原始数据为 LeRobot 标准格式:
1. 数据格式要求
原始数据需要包含以下内容(HDF5格式):
# 必需的数据字段
required_fields = {
"/action/robot": np.ndarray, # 机器人动作数据
"/action/pose": np.ndarray, # 末端位姿数据
"/action/hand": np.ndarray, # 手部动作数据
"/state/robot": np.ndarray, # 机器人状态
"/state/pose": np.ndarray, # 末端位姿状态
"/state/hand": np.ndarray, # 手部状态
}
2. 数据转换流程
- 格式检查:
def check_format(raw_dir):
"""检查原始数据格式是否符合要求"""
hdf5_paths = list(raw_dir.glob("episode_*.hdf5"))
for hdf5_path in hdf5_paths:
with h5py.File(hdf5_path, "r") as data:
# 检查必需字段
assert "/action" in data
assert "/state/robot" in data
assert "/state/pose" in data
assert "/state/hand" in data
# 检查数据维度
num_action_frames = data["/action/robot"].shape[0]
num_state_frames = data["/state/robot"].shape[0]
- 视频编码:
def encode_video(image_dir, video_dir, episode_id, camera, encoding, fps):
"""将图像序列编码为视频"""
# 1. 准备临时目录
tmp_dir = video_dir / "tmp"
tmp_dir.mkdir(parents=True, exist_ok=True)
# 2. 复制并重命名图像
for i, image_path in enumerate(image_paths):
tmp_path = tmp_dir / f"frame_{i:06d}.png"
shutil.copy(image_path, tmp_path)
# 3. 编码视频
video_path = video_dir / f"episode_{episode_id}" / f"{camera}.mp4"
encode_video_frames(tmp_dir, video_path, fps, "libx264")
- 数据加载和转换:
def load_from_raw(raw_dir, videos_dir, fps, video):
"""加载并转换原始数据"""
ep_dicts = []
for ep_idx, ep_path in enumerate(hdf5_files):
with h5py.File(ep_path, "r") as ep:
# 合并状态数据
state = torch.from_numpy(np.concatenate([
ep["/state/robot"][:],
ep["/state/pose"][:],
ep["/state/hand"][:]
], axis=1))
# 合并动作数据
action = torch.from_numpy(np.concatenate([
ep["/action/robot"][:],
ep["/action/hand"][:]
], axis=1))
# 处理图像数据
for camera in get_cameras(ep_path):
if video:
# 编码为视频
fname = encode_video(...)
else:
# 保存为图像序列
imgs_array = get_imgs_array(...)
3. 使用方式
将原始数据转换为 LeRobot 格式:
# 1. 准备路径
raw_dir = Path("path/to/raw/data")
videos_dir = Path("path/to/output/videos")
# 2. 执行转换
hf_dataset, episode_data_index, info = from_raw_to_lerobot_format(
raw_dir=raw_dir,
videos_dir=videos_dir,
fps=30, # 视频帧率
video=True, # 是否编码为视频
use_qpos_action=True # 使用关节角度作为动作
)
转换后的数据结构:
{
"hf_dataset": {
"observation.images.{camera}": VideoFrame,
"observation.state": Tensor,
"action": Tensor,
"episode_index": Tensor,
"frame_index": Tensor,
"timestamp": Tensor,
"next.done": Tensor
},
"episode_data_index": {
"from": Tensor,
"to": Tensor
},
"info": {
"codebase_version": str,
"fps": int,
"video": bool,
"encoding": str
}
}
这个示例展示了如何将自定义格式的数据转换为 LeRobot 可用的标准格式。你可以参考这个实现来处理自己的数据格式。
LeRobot 测试目录说明文档
一、目录结构概览
tests/
├── data/ # 测试数据集
│ └── lerobot/ # LeRobot格式数据集
│ ├── aloha_mobile_chair/ # ALOHA移动椅子任务数据
│ └── aloha_mobile_elevator/ # ALOHA移动电梯任务数据
├── test_*.py # 单元测试文件
└── conftest.py # pytest配置文件
二、测试数据集组织
1. 标准数据集结构
每个数据集(如 aloha_mobile_elevator)包含:
aloha_mobile_elevator/
├── meta_data/ # 元数据目录
│ ├── episode_data_index.safetensors # 回合索引数据
│ ├── info.json # 数据集基本信息
│ └── stats.safetensors # 数据统计信息
├── train/ # 训练数据目录
│ ├── data-00000-of-00001.arrow # Arrow格式数据文件
│ ├── dataset_info.json # 数据集特征描述
│ └── state.json # 训练状态信息
└── videos/ # 视频数据目录
├── observation.images.cam_high_episode_*.mp4 # 顶部相机视频
├── observation.images.cam_left_wrist_episode_*.mp4 # 左手腕相机视频
└── observation.images.cam_right_wrist_episode_*.mp4 # 右手腕相机视频
2. 关键文件说明
meta_data/info.json
{
"codebase_version": "v1.6", # 代码库版本
"fps": 50, # 视频帧率
"video": true, # 是否使用视频格式
"encoding": { # 视频编码参数
"vcodec": "libsvtav1", # 编码器
"pix_fmt": "yuv420p", # 像素格式
"g": 2, # 关键帧间隔
"crf": 30 # 压缩质量
}
}
train/dataset_info.json
定义数据集特征格式:
{
"features": {
"observation.images.cam_high": {
"_type": "VideoFrame"
},
"observation.state": {
"feature": {"dtype": "float32"},
"length": 14,
"_type": "Sequence"
},
"action": {
"feature": {"dtype": "float32"},
"length": 14,
"_type": "Sequence"
}
}
}
三、在训练中使用测试数据
1. 直接使用测试数据集
from lerobot.common.datasets import LeRobotDataset
# 加载测试数据集
dataset = LeRobotDataset(
"tests/data/lerobot/aloha_mobile_elevator",
split="train"
)
# 获取单帧数据
frame = dataset[0]
2. 通过训练脚本使用
# 设置数据集根目录
export DATA_DIR="tests/data"
# 启动训练
python lerobot/scripts/train.py \
policy=act \
env=aloha \
dataset_repo_id=lerobot/aloha_mobile_elevator
3. 数据集验证
# 可视化测试数据集
python lerobot/scripts/visualize_dataset.py \
--repo-id lerobot/aloha_mobile_elevator \
--episode-index 0 \
--data-dir tests/data
四、测试用例编写
1. 数据集测试示例
def test_dataset_loading():
dataset = LeRobotDataset(
"tests/data/lerobot/aloha_mobile_elevator"
)
# 检查数据集大小
assert len(dataset) > 0
# 检查数据格式
sample = dataset[0]
assert "observation.state" in sample
assert "action" in sample
# 检查视频加载
assert "observation.images.cam_high" in sample
2. 策略测试示例
def test_policy_inference():
# 加载策略
policy = ACTPolicy.from_pretrained(
"tests/data/models/act_aloha"
)
# 准备输入数据
batch = {
"observation.state": torch.randn(1, 14),
"observation.images.cam_high": torch.randn(1, 3, 224, 224)
}
# 执行推理
with torch.no_grad():
output = policy(batch)
# 验证输出
assert output.shape == (1, 14)
五、测试配置说明
1. conftest.py 配置
# 来自 tests/conftest.py
@pytest.fixture
def is_robot_available(robot_type):
"""检查机器人硬件是否可用"""
if robot_type not in available_robots:
raise ValueError(
f"The robot type '{robot_type}' is not valid. Expected one of these '{available_robots}"
)
try:
config_path = ROBOT_CONFIG_PATH_TEMPLATE.format(robot=robot_type)
robot_cfg = init_hydra_config(config_path)
robot = make_robot(robot_cfg)
robot.connect()
return True
except Exception as e:
print(f"\nA {robot_type} robot is not available.")
return False
@pytest.fixture
def is_camera_available(camera_type):
"""检查相机硬件是否可用"""
if camera_type not in available_cameras:
raise ValueError(
f"The camera type '{camera_type}' is not valid. Expected one of these '{available_cameras}"
)
try:
camera = make_camera(camera_type)
camera.connect()
return True
except Exception as e:
print(f"\nA {camera_type} camera is not available.")
return False
2. 测试环境变量
# 设置测试设备
export DEVICE="cuda" # 或 "cpu", "mps"
# 设置测试数据目录
export DATA_DIR="tests/data"
# 设置测试机器人类型
export ROBOT_TYPE="aloha" # 或 "xarm", "koch"
六、常见测试场景
1. 硬件相关测试
def test_robot_control(is_robot_available):
"""测试机器人控制"""
if not is_robot_available:
pytest.skip("Robot hardware not available")
# 执行机器人控制测试
robot = make_robot(robot_cfg)
robot.connect()
robot.move_to_joint_positions([0] * 6)
def test_camera_capture(is_camera_available):
"""测试相机捕获"""
if not is_camera_available:
pytest.skip("Camera hardware not available")
# 执行相机捕获测试
camera = make_camera("realsense")
camera.connect()
frame = camera.capture()
2. 数据集测试
def test_dataset_conversion():
"""测试数据格式转换"""
# 准备原始数据
raw_dir = Path("tests/data/raw/demo_001")
videos_dir = Path("tests/data/processed/demo_001")
# 执行转换
dataset, episode_index, info = from_raw_to_lerobot_format(
raw_dir=raw_dir,
videos_dir=videos_dir,
fps=30,
video=True
)
# 验证转换结果
assert "observation.state" in dataset[0]
assert episode_index["from"].shape[0] > 0
assert info["fps"] == 30
3. 策略训练测试
def test_policy_training():
"""测试策略训练"""
# 准备训练配置
config = {
"policy": "act",
"env": "pusht",
"training": {
"batch_size": 8,
"num_epochs": 1,
"eval_freq": 100
}
}
# 执行训练
trainer = PolicyTrainer(config)
metrics = trainer.train()
# 验证训练结果
assert "loss" in metrics
assert "success_rate" in metrics
七、测试最佳实践
- 测试数据管理
- 使用小型测试数据集(1-2个回合)
- 使用git-lfs管理大文件
- 提供数据集的完整元信息
- 硬件测试处理
- 使用pytest.skip跳过不可用硬件
- 提供硬件模拟器用于CI测试
- 记录硬件相关的错误信息
- 测试覆盖率
- 使用pytest-cov检查覆盖率
- 关注核心功能的测试覆盖
- 包含正常和异常情况的测试
- 持续集成
- 在PR中运行所有测试
- 定期运行完整测试套件
- 保存测试结果和性能指标
这个文档提供了完整的测试指南,包括配置说明、常见测试场景和最佳实践。所有内容都基于代码库中的实际实现.
中文版本
本教程将解释训练脚本的使用方法,特别是如何使用 Hydra 来配置训练所需的一切。
训练脚本
LeRobot 提供了一个训练脚本 lerobot/scripts/train.py
。从高层次来看,它执行以下步骤:
- 加载 Hydra 配置文件用于后续步骤(稍后会详细介绍 Hydra)
- 创建一个仿真环境
- 创建与该仿真环境对应的数据集
- 创建一个策略(policy)
- 运行标准的训练循环,包括前向传播、反向传播、优化步骤,以及定期的日志记录、评估(在环境中评估策略)和检查点保存
Hydra 使用基础
完整解释 Hydra 的来龙去脉超出了本文档的范围,但这里我们会分享你需要了解的主要内容。
首先,lerobot/configs
的目录结构如下:
.
├── default.yaml
├── env
│ ├── aloha.yaml
│ ├── pusht.yaml
│ └── xarm.yaml
└── policy
├── act.yaml
├── diffusion.yaml
└── tdmpc.yaml
为了简洁起见,在本文档的其余部分中,我们将省略开头的 lerobot/configs
路径。所以 default.yaml
实际上指的是 lerobot/configs/default.yaml
。
当你运行训练脚本时:
python lerobot/scripts/train.py
Hydra 会被设置为读取 default.yaml
(通过 @hydra.main
装饰器)。如果你查看 @hydra.main
的参数,你会看到 config_path="../configs", config_name="default"
。在 default.yaml
的顶部,有一个 defaults
部分,看起来像这样:
defaults:
- _self_
- env: pusht
- policy: diffusion
这个逻辑告诉 Hydra 要包含来自 env/pusht.yaml
和 policy/diffusion.yaml
的配置参数。注意:要注意顺序,因为具有相同名称的配置参数会被覆盖。因此,default.yaml
会被 env/pusht.yaml
覆盖,后者又会被 policy/diffusion.yaml
覆盖。
然后,default.yaml
还包含一些通用配置参数,如 device: cuda
或 use_amp: false
(用于启用 fp16 训练)。一些其他参数被设置为 ???
,这表示它们应该在其他 yaml 文件中设置。例如,default.yaml
中的 training.offline_steps: ???
在 diffusion.yaml
中被设置为 200000
。
由于 default.yaml
中的这个 defaults
部分,如果你想用 PushT 训练 Diffusion Policy,你只需要运行:
python lerobot/scripts/train.py
不过,你也可以更明确地启动完全相同的 Diffusion Policy 在 PushT 上的训练:
python lerobot/scripts/train.py policy=diffusion env=pusht
通过命令行覆盖默认值的这种方式在你想要更改策略和/或环境时特别有用。例如,你可以在默认的 Aloha 环境上训练 ACT:
python lerobot/scripts/train.py policy=act env=aloha
这里有两点需要注意:
- 配置覆盖以
param_name=param_value
的形式传递 - 这里我们覆盖了默认部分。
policy=act
告诉 Hydra 使用policy/act.yaml
,而env=aloha
告诉 Hydra 使用env/aloha.yaml
顺便说一下:我们已经设置了所有配置,使它们能够重现文献中的最新研究成果。
在命令行中覆盖配置参数
现在假设我们想在 Aloha 环境中训练一个不同的任务。如果你查看 env/aloha.yaml
,你会看到类似这样的内容:
# lerobot/configs/env/aloha.yaml
env:
task: AlohaInsertion-v0
如果你查看 policy/act.yaml
,你会看到类似这样的内容:
# lerobot/configs/policy/act.yaml
dataset_repo_id: lerobot/aloha_sim_insertion_human
但我们的 Aloha 环境实际上也支持立方体传输任务。要训练这个任务,你可以手动修改这两个 yaml 配置文件。
首先,我们需要将 ALOHA 环境切换到使用立方体传输任务。
# lerobot/configs/env/aloha.yaml
env:
- task: AlohaInsertion-v0
+ task: AlohaTransferCube-v0
然后,我们还需要切换到使用立方体传输数据集。
# lerobot/configs/policy/act.yaml
-dataset_repo_id: lerobot/aloha_sim_insertion_human
+dataset_repo_id: lerobot/aloha_sim_transfer_cube_human
然后,你就可以运行:
python lerobot/scripts/train.py policy=act env=aloha
这样你就会在立方体传输任务上进行训练和评估。
除了编辑 yaml 配置文件外,还可以通过命令行覆盖默认值:
python lerobot/scripts/train.py \
policy=act \
dataset_repo_id=lerobot/aloha_sim_transfer_cube_human \
env=aloha \
env.task=AlohaTransferCube-v0
这里有一个新东西。注意用于遍历配置层次结构的 .
分隔符。但要注意 defaults
部分是一个例外。如你所见,我们不需要在命令行中写 defaults.policy=act
。policy=act
就足够了。
把所有这些知识放在一起,这里是用来训练 https://huggingface.co/lerobot/act_aloha_sim_transfer_cube_human 的命令。
python lerobot/scripts/train.py \
hydra.run.dir=outputs/train/act_aloha_sim_transfer_cube_human \
device=cuda
env=aloha \
env.task=AlohaTransferCube-v0 \
dataset_repo_id=lerobot/aloha_sim_transfer_cube_human \
policy=act \
training.eval_freq=10000 \
training.log_freq=250 \
training.offline_steps=100000 \
training.save_model=true \
training.save_freq=25000 \
eval.n_episodes=50 \
eval.batch_size=50 \
wandb.enable=false \
这里有一个新东西:hydra.run.dir=outputs/train/act_aloha_sim_transfer_cube_human
,它指定了保存训练输出的位置。
使用不在 lerobot/configs
中的配置文件
上面我们讨论了我们的训练脚本是如何设置的,使得 Hydra 在 lerobot/configs
中寻找 default.yaml
。但是,如果你在文件系统的其他地方有一个配置文件,你可以使用:
python lerobot/scripts/train.py --config-dir PARENT/PATH --config-name FILE_NAME_WITHOUT_EXTENSION
注意:这里我们使用常规语法为 Python 脚本提供命令行参数,而不是 Hydra 的 param_name=param_value
语法。
作为一个具体的例子,当你有一个包含训练输出的文件夹,并且想要重新运行训练时,这变得特别方便。例如,假设你之前用前面的命令之一运行了训练脚本,并且有 outputs/train/my_experiment/checkpoints/pretrained_model/config.yaml
。这个 config.yaml
文件将包含完整的配置参数集。要再次使用相同的配置运行训练,执行:
python lerobot/scripts/train.py --config-dir outputs/train/my_experiment/checkpoints/last/pretrained_model --config-name config
注意,你仍然可以使用常规语法进行配置参数覆盖(例如:通过添加 training.offline_steps=200000
)。
典型的日志和指标
当你开始训练过程时,你首先会看到你的完整配置被打印在终端中。你可以检查它以确保你的配置是正确的,并且你的配置没有被其他文件覆盖。最终的配置也会与检查点一起保存。
之后,你会看到像这样的训练日志:
INFO 2024-08-14 13:35:12 ts/train.py:192 step:0 smpl:64 ep:1 epch:0.00 loss:1.112 grdn:15.387 lr:2.0e-07 updt_s:1.738 data_s:4.774
或者评估日志:
INFO 2024-08-14 13:38:45 ts/train.py:226 step:100 smpl:6K ep:52 epch:0.25 ∑rwrd:20.693 success:0.0% eval_s:120.266
如果 wandb.enable
设置为 true
,这些日志也会保存在 wandb 中。以下是一些缩写的含义:
smpl
: 训练期间看到的样本数ep
: 训练期间看到的回合数。一个回合包含完整操作任务中的多个样本epch
: 所有唯一样本被看到的次数(epoch)grdn
: 梯度范数∑rwrd
: 计算每个评估回合中奖励的总和,然后取平均值success
: 评估回合的平均成功率。奖励和成功通常是不同的,除非在稀疏奖励设置中,只有当任务成功完成时奖励才为1eval_s
: 在环境中评估策略的时间,以秒为单位updt_s
: 更新网络参数的时间,以秒为单位data_s
: 加载一批数据的时间,以秒为单位
一些指标对于初始性能分析很有用。例如,如果你通过 nvidia-smi
命令发现当前 GPU 利用率很低,并且 data_s
有时太高,你可能需要修改批量大小或数据加载工作线程的数量来加速数据加载。我们也推荐使用 pytorch profiler 进行详细的性能探测。
到目前为止,我们已经看到了如何为 PushT 训练 Diffusion Policy 以及为 ALOHA 训练 ACT。现在,如果我们想为 PushT 训练 ACT 呢?好吧,ACT 配置中有一些特定于 ALOHA 环境的方面,这些方面恰好与 PushT 不兼容。因此,尝试运行以下命令几乎肯定会引发某种异常(例如:特征维度不匹配):
python lerobot/scripts/train.py policy=act env=pusht dataset_repo_id=lerobot/pusht
请查看我们的关于调整策略配置以适应各种环境的高级教程以了解更多信息。
在此期间,祝编码愉快! 🤗
为 PushT 环境适配 ACT 策略配置
在本教程中,我们将学习如何调整策略配置以适应新的环境和数据集。作为具体示例,我们将调整 ACT 的默认配置以适应 PushT 环境和数据集。
如果你还没有阅读我们关于训练脚本和配置工具的教程,请在开始本教程之前先阅读它。
让我们开始吧!
假设我们想要为 PushT 训练 ACT。然而,ACT 配置中有一些特定于 ALOHA 环境的方面,这些恰好与 PushT 不兼容。因此,尝试运行以下命令几乎肯定会引发某种异常(例如:特征维度不匹配):
python lerobot/scripts/train.py policy=act env=pusht dataset_repo_id=lerobot/pusht
我们需要调整 ACT 策略配置的参数以适应 PushT 环境。最重要的是图像键。
ALOHA 的数据集和环境通常使用可变数量的摄像头。在 lerobot/configs/policy/act.yaml
中你可能注意到两个相关部分。这里我们展示了调整到 PushT 所需的最小改动:
override_dataset_stats:
- observation.images.top:
+ observation.image:
# 来自 imagenet 的统计数据,因为我们使用预训练的视觉模型
mean: [[[0.485]], [[0.456]], [[0.406]]] # (c,1,1)
std: [[[0.229]], [[0.224]], [[0.225]]] # (c,1,1)
policy:
input_shapes:
- observation.images.top: [3, 480, 640]
+ observation.image: [3, 96, 96]
observation.state: ["${env.state_dim}"]
output_shapes:
action: ["${env.action_dim}"]
input_normalization_modes:
- observation.images.top: mean_std
+ observation.image: mean_std
observation.state: min_max
output_normalization_modes:
action: min_max
这里我们考虑了以下几点:
- PushT 使用 “observation.image” 作为其图像键。
- PushT 提供较小的图像。
旁注: 从技术上讲,我们可以通过命令行界面覆盖这些设置,但是对于许多更改来说会变得有点混乱,而且我们还面临一个挑战,即我们在观察键中使用 .
被 Hydra 视为层次分隔符。
为了方便起见,我们在本目录中提供了 act_pusht.yaml
。它包含了上述差异,以及其他一些(可选的)差异,这些在文件中都有解释。请使用以下命令将其复制到 lerobot/configs/policy
中:
cp examples/advanced/1_train_act_pusht/act_pusht.yaml lerobot/configs/policy/act_pusht.yaml
(记住从之前的教程中学到的,Hydra 会在 lerobot/configs
目录中查找)。现在尝试运行以下命令。
python lerobot/scripts/train.py policy=act_pusht env=pusht
注意这与教程开始时失败的命令很相似,只是:
- 现在我们使用
policy=act_pusht
指向我们的新配置文件。 - 我们可以删除
dataset_repo_id=lerobot/pusht
,因为这个更改已经包含在我们的新配置文件中。
太好了! 你现在正在为 PushT 环境训练 ACT。
本教程的要点是,当为不同的环境和数据集训练策略时,你需要了解策略配置中哪些部分是特定于这些环境和数据集的,并相应地进行更改。
祝编码愉快! 🤗
中文版本
本教程解释如何恢复使用训练脚本开始的训练运行。如果你不了解我们的训练脚本和配置系统是如何工作的,请先阅读 4_train_policy_with_script.md。
基本训练恢复
让我们以训练 ACT 用于 ALOHA 任务之一为例。这里有一个可以实现这一目标的命令:
python lerobot/scripts/train.py \
hydra.run.dir=outputs/train/run_resumption \
policy=act \
dataset_repo_id=lerobot/aloha_sim_transfer_cube_human \
env=aloha \
env.task=AlohaTransferCube-v0 \
training.log_freq=25 \
training.save_checkpoint=true \
training.save_freq=100
这里我们使用 ACT 的默认数据集和环境,并且我们特意将日志频率和检查点频率设置为较低的数值,以便我们可以测试恢复功能。你应该能够看到一些日志记录,并在 1 分钟内得到第一个检查点。请在第一个检查点之后中断训练。
要恢复训练,我们只需要运行训练脚本,提供运行目录和恢复选项:
python lerobot/scripts/train.py \
hydra.run.dir=outputs/train/run_resumption \
resume=true
你应该从日志中看到你的训练从中断的地方继续进行。
注意,使用 resume=true
时,会加载训练输出目录中最后一个检查点的配置文件。因此,我们没有提供之前命令中的所有其他配置参数并不重要(尽管可能会有警告通知你,你的命令与检查点的配置不同)。
现在你应该知道如何在训练运行被中断或你想要延长已完成的训练运行时恢复训练。
祝编码愉快! 🤗
中文版本
本教程演示如何在 LeRobotDataset 中使用图像变换进行数据增强。我们将展示如何在从数据集加载数据时对观察图像应用 torchvision 变换。
基本图像变换
首先,让我们创建一个没有任何变换的基本数据集:
from lerobot.common.datasets.lerobot_dataset import LeRobotDataset
dataset_repo_id = "lerobot/aloha_static_tape"
# 创建一个没有变换的 LeRobotDataset
dataset = LeRobotDataset(dataset_repo_id)
# 这等同于 `dataset = LeRobotDataset(dataset_repo_id, image_transforms=None)`
接下来,我们可以使用 torchvision 定义一些图像变换:
from torchvision.transforms import v2
# 定义变换
transforms = v2.Compose(
[
v2.ColorJitter(brightness=(0.5, 1.5)), # 亮度调整
v2.ColorJitter(contrast=(0.5, 1.5)), # 对比度调整
v2.RandomAdjustSharpness(sharpness_factor=2, p=1), # 锐度调整
]
)
# 创建一个带有变换的数据集
transformed_dataset = LeRobotDataset(dataset_repo_id, image_transforms=transforms)
要比较原始图像和变换后的图像,我们可以:
from pathlib import Path
from torchvision.transforms import ToPILImage
# 从每个数据集获取第一帧
first_idx = dataset.episode_data_index["from"][0].item()
original_frame = dataset[first_idx][dataset.camera_keys[0]]
transformed_frame = transformed_dataset[first_idx][transformed_dataset.camera_keys[0]]
# 保存图像
output_dir = Path("outputs/image_transforms")
output_dir.mkdir(parents=True, exist_ok=True)
to_pil = ToPILImage()
to_pil(original_frame).save(output_dir / "original_frame.png", quality=100)
to_pil(transformed_frame).save(output_dir / "transformed_frame.png", quality=100)
变换后的图像将被保存到 outputs/image_transforms
目录中。
这些图像变换可以帮助增加训练数据的多样性,提高模型的鲁棒性。在这个例子中,我们应用了亮度调整、对比度调整和锐度调整,但你可以根据需要添加更多的变换。
祝编码愉快! 🤗