feat(actpred): 训练时安全图像增强(ColorJitter),支持任意 frame_len

训练集启用 --augment 后对每帧施加亮度/对比度/饱和度/色调变换,
同一样本内所有帧共享参数(模拟连续帧光照不变),验证集不增强。
仅颜色空间变换,无空间变换,动作标签不受影响。
删除 t1.mt(@TEST 块无对应插件,录制时报错)。
This commit is contained in:
qing committed 2026-08-13 09:35:37 +00:00
1 parent 85133868f3
commit 5af863a2c5
5 files changed
+136 -23

No files matched your search

+4
View File
@@ -40,6 +40,7 @@ class TrainConfig:
num_workers: int = 2 num_workers: int = 2
seed: int = 42 seed: int = 42
max_grad_norm: float = 1.0 max_grad_norm: float = 1.0
augment: bool = False
@dataclass @dataclass
@@ -87,6 +88,8 @@ def build_argparser(description: str = "") -> argparse.ArgumentParser:
parser.add_argument("--num-workers", type=int, default=None) parser.add_argument("--num-workers", type=int, default=None)
parser.add_argument("--seed", type=int, default=None) parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--max-grad-norm", type=float, default=None) parser.add_argument("--max-grad-norm", type=float, default=None)
parser.add_argument("--augment", action=argparse.BooleanOptionalAction, default=None,
help="训练时启用安全图像增强(ColorJitter:亮度/对比度/饱和度/色调)")
parser.add_argument("--init-from", type=str, default=None, parser.add_argument("--init-from", type=str, default=None,
help="预训练 checkpoint 路径,作为微调起点(strict=False 加载兼容权重)") help="预训练 checkpoint 路径,作为微调起点(strict=False 加载兼容权重)")
parser.add_argument("--resume", type=str, default=None, parser.add_argument("--resume", type=str, default=None,
@@ -232,6 +235,7 @@ def apply_overrides(cfg: Config, args: argparse.Namespace) -> Config:
"num_workers": ("train", "num_workers"), "num_workers": ("train", "num_workers"),
"seed": ("train", "seed"), "seed": ("train", "seed"),
"max_grad_norm": ("train", "max_grad_norm"), "max_grad_norm": ("train", "max_grad_norm"),
"augment": ("train", "augment"),
} }
args_dict = vars(args) args_dict = vars(args)
for arg_name, (section, field) in mapping.items(): for arg_name, (section, field) in mapping.items():
+14 -1
View File
@@ -1,4 +1,5 @@
import os import os
import random
import re import re
import numpy as np import numpy as np
from pathlib import Path from pathlib import Path
@@ -7,6 +8,7 @@ from PIL import Image
import torch import torch
from torch.utils.data import Dataset from torch.utils.data import Dataset
from torchvision import transforms from torchvision import transforms
from torchvision.transforms import functional as TF
def _natural_key(p): def _natural_key(p):
@@ -19,12 +21,13 @@ class ActionDataset(Dataset):
image_width: int = 640, image_height: int = 480, image_width: int = 640, image_height: int = 480,
normalize_actions: bool = False, normalize_actions: bool = False,
action_stats_path: str = None, episode_indices: list = None, action_stats_path: str = None, episode_indices: list = None,
frame_len: int = 3): frame_len: int = 3, augment: bool = False):
self.history_len = history_len self.history_len = history_len
self.action_dim = action_dim self.action_dim = action_dim
self.image_width = image_width self.image_width = image_width
self.image_height = image_height self.image_height = image_height
self.frame_len = frame_len self.frame_len = frame_len
self.augment = augment
self.transform = transforms.Compose([ self.transform = transforms.Compose([
transforms.Resize((image_height, image_width)), transforms.Resize((image_height, image_width)),
@@ -92,8 +95,18 @@ class ActionDataset(Dataset):
def __getitem__(self, idx): def __getitem__(self, idx):
image_paths, action_history, target = self.samples[idx] image_paths, action_history, target = self.samples[idx]
frames = [] frames = []
if self.augment:
bf = random.uniform(0.8, 1.2)
cf = random.uniform(0.8, 1.2)
sf = random.uniform(0.8, 1.2)
hf = random.uniform(-0.05, 0.05)
for p in image_paths: for p in image_paths:
image = Image.open(p).convert("RGB") image = Image.open(p).convert("RGB")
if self.augment:
image = TF.adjust_brightness(image, bf)
image = TF.adjust_contrast(image, cf)
image = TF.adjust_saturation(image, sf)
image = TF.adjust_hue(image, hf)
frames.append(self.transform(image)) frames.append(self.transform(image))
images = torch.stack(frames) # (F, 3, S, S) images = torch.stack(frames) # (F, 3, S, S)
action_history = torch.tensor(action_history, dtype=torch.float32) action_history = torch.tensor(action_history, dtype=torch.float32)
+2 -1
View File
@@ -84,7 +84,8 @@ def main():
train_ds = ActionDataset( train_ds = ActionDataset(
cfg.data.data_dir, cfg.data.history_len, cfg.data.action_dim, cfg.data.data_dir, cfg.data.history_len, cfg.data.action_dim,
cfg.data.image_width, cfg.data.image_height, cfg.data.normalize_actions, cfg.data.image_width, cfg.data.image_height, cfg.data.normalize_actions,
cfg.data.action_stats_path, train_indices, cfg.data.frame_len) cfg.data.action_stats_path, train_indices, cfg.data.frame_len,
augment=cfg.train.augment)
if cfg.data.normalize_actions and not cfg.data.action_stats_path: if cfg.data.normalize_actions and not cfg.data.action_stats_path:
# 归一化统计量必须只用训练集计算,再供验证集复用,否则 val loss 不可信 # 归一化统计量必须只用训练集计算,再供验证集复用,否则 val loss 不可信
stats_path = os.path.join(cfg.train.checkpoint_dir, "action_stats.npy") stats_path = os.path.join(cfg.train.checkpoint_dir, "action_stats.npy")
-21
View File
@@ -1,21 +0,0 @@
# 无硬件情况下的测试脚本
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@TEST
name parameter0 patameter1;
t0 p0 p1;
t1 p2 p3;
@
@Action
t0.A t1.F;
10 20;
@
@@ -0,0 +1,116 @@
# actpred 训练时安全图像增强
> 文档类型:特性报告
> 模块:actpred
> 日期:2026-08-13
---
## 1. 背景
pybot dry-run 录制的 episode 数据样本量有限(11 个脚本 → 12 个 episode → ~728 样本),训练容易过拟合。直接增大数据集需要更多 `.mt` 脚本或更多轮次录制,成本较高。
图像增强(data augmentation)是低成本扩充样本的标准手段,但机器人动作预测场景有特殊约束:**空间变换(翻转/旋转/裁剪)会改变图像的语义坐标,而动作标签未随之变换,导致训练对错误**。例如水平翻转后,左右舵机角度应互换,但标签不变,形成错误训练信号。
## 2. 方案
### 2.1 增强范围
仅启用**颜色空间变换**(ColorJitter),不涉及任何空间变换:
| 变换 | 参数 | 说明 |
|------|------|------|
| 亮度 | `±20%`(factor ∈ [0.8, 1.2]) | 模拟不同光照条件 |
| 对比度 | `±20%`(factor ∈ [0.8, 1.2]) | 模拟不同摄像头增益 |
| 饱和度 | `±20%`(factor ∈ [0.8, 1.2]) | 模拟不同色温环境 |
| 色调 | `±5%`(factor ∈ [-0.05, 0.05]) | 轻微色偏,避免模型过依赖颜色 |
**不使用的变换**(因需同步变换动作标签):
| 变换 | 原因 |
|------|------|
| 水平/垂直翻转 | 左右舵机角度需互换,否则标签错误 |
| 旋转 | 重力方向变化,姿态语义改变 |
| 裁剪/平移 | 视野变化导致动作-画面对应关系改变 |
| 透视变换 | 改变空间关系 |
### 2.2 同一样本内帧间参数一致性
一个训练样本包含 `frame_len` 帧连续图像(默认 3 帧,间隔 10ms)。真实场景中连续帧的光照条件不变,因此**同一样本内所有帧共享同一组增强参数**,而非每帧独立随机:
```python
if self.augment:
bf = random.uniform(0.8, 1.2) # 亮度因子,所有帧共用
cf = random.uniform(0.8, 1.2) # 对比度因子
sf = random.uniform(0.8, 1.2) # 饱和度因子
hf = random.uniform(-0.05, 0.05) # 色调因子
for p in image_paths:
image = Image.open(p).convert("RGB")
if self.augment:
image = TF.adjust_brightness(image, bf)
image = TF.adjust_contrast(image, cf)
image = TF.adjust_saturation(image, sf)
image = TF.adjust_hue(image, hf)
frames.append(self.transform(image))
```
不同样本(不同 `__getitem__` 调用)使用独立随机参数,保证整个数据集的增强多样性。
### 2.3 自适应 frame_len
增强逻辑在 `for p in image_paths` 循环前生成参数、循环内逐帧施加。`image_paths` 的长度由 `frame_len` 决定(`image_files[t - frame_len + 1 : t + 1]`),循环自动适配。**`frame_len` 为 1、2、3、5、10 等任意值时均无需修改代码**。
### 2.4 训练/验证隔离
| 数据集 | 增强 | 说明 |
|--------|------|------|
| 训练集 | `augment=cfg.train.augment` | CLI `--augment` 控制 |
| 验证集 | `augment=False`(默认) | 验证 loss 反映真实分布表现 |
推理端(`predict.py` / `onnx_infer.py` / `hailo_infer.py`)不涉及增强,使用原始图像。
## 3. CLI 接口
```bash
# 训练时启用图像增强
bash _run.sh train --data-dir ./data --epochs 100 --augment
# 显式关闭(默认关闭)
bash _run.sh train --data-dir ./data --epochs 100 --no-augment
```
`--augment` 使用 `BooleanOptionalAction`,支持 `--augment` / `--no-augment` 双向开关。
## 4. 验证
### 4.1 功能验证
```
dataset size: 728 samples
augment diff (should be >0): 0.3767 # 增强前后图像有差异
two augment calls diff (should be >0): 0.2626 # 两次增强参数不同(随机性)
action_history diff (should be 0): 0.0 # 动作标签不受影响
target diff (should be 0): 0.0
```
### 4.2 端到端训练
`--augment --epochs 3`(64×64,batch=8)训练正常完成,loss 正常下降。
### 4.3 回归测试
全量单测 90/90 通过,无回归。
## 5. 改动文件
| 文件 | 改动 |
|------|------|
| `actpred/dataset.py` | `ActionDataset` 新增 `augment` 参数;`__getitem__` 在 Resize 前施加 ColorJitter(`torchvision.transforms.functional`) |
| `actpred/config.py` | `TrainConfig` 新增 `augment: bool = False`;`build_argparser` 新增 `--augment`;`apply_overrides` 新增映射 |
| `actpred/train.py` | 训练集构造传 `augment=cfg.train.augment`,验证集默认不增强 |
## 6. 后续方向
- **空间增强(需同步标签变换)**:水平翻转时自动交换左右舵机角度(s1↔s2, s3↔s4, s5↔s6, s7↔s8),可将训练数据量翻倍。需要了解舵机配对关系后实现。
- **动作噪声增强**:`pretrain_mt.py` 已支持 `--noise-std`,`train.py` 可同样引入,对 action_history 和 target 施加高斯扰动。
- **更多 `.mt` 脚本**:从根本上扩大动作多样性,是提升模型泛化能力的最有效手段。