docs: pybot×actpred 训练闭环评估实验报告与归一化逐列权重开发方案

实验报告:dry-run 批量录制 13 episodes/804 样本,未归一化 vs z-score
两模型对照训练,R4 闭环评估定位 spk.F 为主导误差列,记录 oob/error_growth
指标退化与 stats 路径 bug 修复。

开发方案:固定物理范围归一化(actpred 侧常量表,舵机 [0,180°])+
逐列加权损失,附评估器修正与 rollout/scheduled sampling 配套计划。
This commit is contained in:
qing committed 2026-08-14 08:59:53 +00:00
1 parent aa271e395a
commit adfa61aa3c
2 files changed
+257

No files matched your search

@@ -0,0 +1,101 @@
# actpred 动作归一化与逐列权重开发方案
> 文档类型:开发方案
> 模块:actpred(联动 pybot 数据契约)
> 作者:自动生成
> 日期:2026-08-14
> 前置实验:`pybot_actpred_dataset_training_eval_20260814.md`
---
## 1. 问题陈述
闭环评估(R4)暴露:z-score 归一化把 18 列方差拉平,隐式实现"全列等权",导致**舵机精度 ↔ spk.F 频率跟踪的权衡**——未归一化模型舵机准但 spk.F 崩(21k MSE),归一化模型 spk.F 改善 3× 但简单动作恶化 1.3-25×。
本质:**缩放方式(数值稳定性)与列重要性(优化方向)是两个正交维度**,当前被隐式耦合。方案目标:固定物理范围归一化(数值稳定、免 stats 文件)+ 显式逐列权重(舵机优先),两者解耦。
## 2. 方案设计
### 2.1 固定物理范围 min-max 归一化(actpred 侧)
位置决策:**归一化保留在 actpred 侧,pybot 继续记录原始量纲**。理由:
- **raw at rest**:episode 是数据资产,原始角度/频率可复用于逆运动学、碰撞检测等分析,归一化不可逆
- **反归一化链条**:若 pybot 侧归一化,`--loop` 执行前需反归一化,min/max 表要同时存在于 recorder 与 loop 两端;现状 `predict()` 返回原始量纲,pybot 无感知,边界更干净
- **代码库先例**:IMU 即"pybot 记录原始、actpred `imu_norm.py` 按物理量程归一化",动作走同一条路保持一致
- **pybot 零依赖承诺**:归一化逻辑入 recorder 会加深 pybot 对 numpy 的依赖面
固定范围常量表(新增 `actpred/action_norm.py`,类比 `imu_norm.py`):
| 列 | 物理范围 | 依据 |
|---|---|---|
| 0-9 舵机角 | **[0, 180]** | `pybot/hd/servo.py` `MAX_ANGLE=180.0`(校准后全行程 0-180°) |
| 10 spk.F | [0, 5000] | `@Speaker max_f=5000` |
| 11 spk.delay | [0, 2000] | 实用上限(脚本用 10-500ms) |
| 12-17 保留位 | 恒 0 | 不参与归一化,损失权重 0 |
min-max 映射:`x_norm = (x - lo) / (hi - lo)` → [0,1](或映射到 [-1,1],与 IMU 归一化输出区间一致,待实现时定)。
优点:免 action_stats.npy(消除 stats 路径类 bug 的整类风险)、零状态部署、数值区间有界。风险:min-max 对离群值敏感——若真实数据某列越界,需 clamp 到 [0,1] 并告警(防御口径与 `_action_bounds` 一致)。
可选细化(后续):按各舵机 `|max|`(s1=90°、s2=180°…)分列归一化,90° 舵机能用满 [0,1],分辨率更高;需要读取 @Servo 块的 max 列构建表。
### 2.2 逐列加权损失
```python
COLUMN_WEIGHTS = np.array([
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, # 0-9 舵机(安全关键)
0.2, 0.2, # 10-11 spk.F / spk.delay(声音与节奏,不影响运动)
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, # 12-17 保留位(恒 0,纯噪声)
], dtype=np.float32)
# loss = mean(w * (pred - target)^2) / mean(w)
```
实施点:
- `train.py` / `pretrain_mt.py` 的 `train_one_epoch` / `validate`:MSE 处乘权重向量(权重存 DataConfig,随 checkpoint 序列化,推理端无需感知)
- 权重可经 CLI `--column-weights` 覆盖(默认用上表)
- 训练集/验证集用同一权重;`evaluate.py` 的逐列误差报告不受影响(报告仍按无权重口径,便于对照)
### 2.3 归一化与权重的组合矩阵
| 配置 | 数值稳定性 | 舵机精度 | spk.F 跟踪 | 预期适用 |
|---|---|---|---|---|
| raw + 等权(模型 A) | 差(loss 数百量级) | 好 | 崩 | 仅演示/对照 |
| z-score + 等权(模型 B) | 好 | 差 | 好 | 现状 |
| **固定范围 + 加权(本方案)** | 好 | 好 | 中(w=0.2 保留梯度) | **目标** |
## 3. 配套改进(同批实施)
### 3.1 R4 评估器修正(指标失效修复)
1. **oob_rate 恒值列退化**:span<阈值(如 1e-3)的列不参与越界判定;或改用固定物理界(舵机 [0,180]、spk.F [0,5000])。推荐后者——与 §2.1 常量表共用,且"越物理界"比"越经验界"更符合安全语义
2. **error_growth 长度混叠**:按 episode 归一化后聚合(每 episode 自身首末段比值取中位数),避免短 episode 尖峰主导曲线
### 3.2 R1-B rollout + scheduled sampling
闭环误差累积源于 teacher forcing 训练与自预测推理的不匹配。已实现的 `--rollout-len K --ss-prob p` 直接对标:建议 `K=4, p=0.5`,与加权损失同批训练对照。
### 3.3 数据扩展
- **实机录制**(根本瓶颈):dry-run 帧为噪声、IMU 全零,视觉/姿态通路无真实信号。实机 `--record` 后复跑本实验
- 验证集 1 个 episode 选模噪声大:val_split 0.15→0.3
- 补充站立/保持类长序列(当前 std 类仅 1-81 步)
## 4. 实施步骤
| 步骤 | 内容 | 产出 |
|---|---|---|
| 1 | 新增 `actpred/action_norm.py`:固定范围常量表 + normalize/denormalize + clamp 防御 | 独立纯 numpy 模块 + 单测 |
| 2 | DataConfig 增 `normalize_mode`(`zscore`/`fixed_range`)与 `column_weights`;dataset/train 接入 | 训练链路支持 + 单测 |
| 3 | `evaluate.py` oob 改物理界、error_growth 按 episode 归一化 | 评估器修正 + 单测 |
| 4 | 训练对照:固定范围+加权 vs z-score+等权(同种子、同数据) | 模型 C/D checkpoint |
| 5 | 复跑 R4 闭环评估,验证权衡解耦 | 对比报告 |
步骤 1-3 可并行;步骤 4-5 依赖前置完成。预计步骤 1-3 改动集中在 `actpred/`(`action_norm.py` 新增、`config.py`/`dataset.py`/`train.py`/`evaluate.py` 小改),不影响 pybot 与部署链(归一化在推理端内部完成,`predict()` 输出仍为原始量纲)。
## 5. 决策记录
- **归一化位置**:讨论过"移入 pybot 按列 min/max 归一化"。结论:不采纳(raw at rest、反归一化链条变长、IMU 先例不一致),但采纳其"固定物理范围"内核,落在 actpred 侧常量表实现
- **舵机范围**:±180° 为误述,实际全行程 0-180°(`servo.py MAX_ANGLE=180.0`),归一化表按 [0,180]
- **spk.F 对数尺度**:备选未定。若加权后 spk.F 跟踪仍不足,再考虑 log 变换(频率感知是对数的,10→1000 跨两个数量级)
@@ -0,0 +1,156 @@
# pybot×actpred 数据集录制与训练闭环评估实验报告
> 文档类型:实验报告
> 模块:pybot → actpred
> 作者:自动生成
> 日期:2026-08-14
---
## 1. 概述
### 1.1 背景
在微动作分解与自监督录制特性(见 `pybot_micro_action_and_recording_20260813.md`)基础上,本实验走通完整链路:dry-run 批量录制全部 `.mt` 动作脚本 → actpred 训练(未归一化 vs z-score 归一化两个版本)→ R4 离线闭环评估,并基于评估结果给出下一步开发方案(见 `actpred_action_norm_weighting_plan_20260814.md`)。
### 1.2 实验目标
1. 批量录制全部 12 个动作脚本,产出 actpred 格式数据集
2. 训练两个对照模型:`normalize_actions=False`(旧默认)与 `=True`(新默认,commit `a2c3a57`)
3. 用 `evaluate.py`(R4 闭环代理评估)对比两模型,定位误差主导列
4. 暴露并修复训练/评估链路的缺陷
### 1.3 关键结论速览
- **闭环舵机角无发散**,误差主导列是 `spk.F`(扬声器频率,10→1000Hz 大跨度)
- 归一化使聚合 MSE 降低 2.6×(6447→2445),但存在**舵机精度 ↔ 频率跟踪的权衡**
- `oob_rate` / `error_growth` 两个指标在含恒值列的退化数据集上失效,需修正(见开发方案 §3)
- 修复 1 个潜伏 bug:`normalize_actions=True` 时 val_ds 找不到 action_stats(commit `7ec8c9d`)
---
## 2. 数据集批量录制
### 2.1 方法
`scripts/*.mt` 均无 `@Camera` 块(该块为 pybot 新增,原脚本与 bot4cj 共享故未改)。采用**注入式录制**:不改动原脚本,为每个 `.mt` 在 `@Action` 前注入 `@Camera`(640×480@30fps)生成变体到 `data/pybot_record_scripts/`,再逐个 dry-run 录制。批处理脚本:`data/record_all.sh`(可重复执行)。
新增录制脚本 `bot4cj/scripts/record_stand.mt`(commit `af0ff56`):基于 std.mt 扩展 @Camera + MPU6050 绑定 + 5 行 @Action(delay=200),产出 81 步插值序列。
### 2.2 录制结果
dry-run 下帧为合成随机噪声、IMU 全零(Mock I2C 读零)。共 13 个 episode、865 帧、804 个训练样本(history_len=5, frame_len=3):
| Episode | 脚本 | 步数 | Episode | 脚本 | 步数 |
|---|---|---|---|---|---|
| 0000 | record_stand | 81 | 0007 | stdd | 46 |
| 0001 | jump | 27 | 0008 | t0 | 211 |
| 0002 | left-hand | 123 | 0009 | tilt-b | 46 |
| 0003 | left-turn | 58 | 0010 | tilt-f | 46 |
| 0004 | lie-down | 11 | 0011 | tilt-l | 46 |
| 0005 | rght-hand | 123 | 0012 | tilt-r | 46 |
| 0006 | std | 1 | | | |
注:episode_0006 仅 1 步(单行 @Action 首行不分解,属设计行为),对训练无贡献。仅 episode_0000 有 imu.npy(全零),其余无 IMU 文件,`ActionDataset` 按零向量处理。
### 2.3 数据验证
- `ActionDataset`(imu_dim=0 与 =8 两种模式)加载通过,804 样本 0 形状异常
- 微动作插值验证:s1 舵机 45°→60° 按 20 微步线性插值(每步 0.75°),`spk.delay` 微步期 10ms,与设计一致
- 每步帧独立(`bfa6d8d` 同步 capture 修复生效)
---
## 3. 训练实验
### 3.1 实验设置
| 项 | 值 |
|---|---|
| 分辨率 | 640×480(不降分辨率) |
| 增强 | ColorJitter(--augment) |
| batch / workers | 4 / 2(CPU 训练,~4 分钟/epoch) |
| epochs / patience | 100 / 15(early stopping) |
| 训练/验证 | 12 / 1 episode(763 / 41 样本,seed 42) |
### 3.2 模型 A:未归一化(normalize_actions=False,旧默认)
- 50 epochs 触发 early stopping,**best val_loss = 19.02**(原始量纲 MSE,数百量级)
- loss 轨迹:4344(epoch 1)→ 296(epoch 50),val 收敛至 ~20-30
### 3.3 模型 B:z-score 归一化(normalize_actions=True,新默认)
- 26 epochs 触发 early stopping,**best val_loss = 0.0288**(归一化量纲 <1.0,符合 commit `a2c3a57` 预期)
- loss 轨迹:0.188(epoch 1)→ 0.0288(epoch 11 最优)
### 3.4 训练链路缺陷修复
归一化默认开启后暴露潜伏 bug:`train.py` 将 action_stats 保存到 checkpoint_dir 后,cfg 只留裸文件名 "action_stats.npy",val_ds 按 cwd 解析 → `FileNotFoundError`。修复:验证集构建用完整路径,checkpoint 配置存裸文件名(推理端按 checkpoint 目录解析)。同步修正 `test_warn_init_from_mismatch` 对旧默认值的隐式依赖(commit `7ec8c9d`,141 测试全过)。
### 3.5 推理冒烟
模型 B 加载正常,接口契约 `frame_len=3 / history_len=5 / action_dim=18 / imu_dim=None` 与 pybot 闭环鸭子类型约定一致;单次推理 ~84ms(x64 CPU)。真实帧+真实历史预测 t=5 动作:s1 误差 1.78°(模型 A 为 4.39°),数值范围合理。
---
## 4. R4 闭环评估
### 4.1 聚合指标对比
| 指标 | 模型 A(未归一化) | 模型 B(归一化) |
|---|---|---|
| overall_mse | 6446.7 | **2445.4**(2.6× 优) |
| oob_rate | 100% | **28.1%** |
| smoothness_ratio | 0.147(近常数输出,过度平滑) | **1.047**(≈真值变化,正常) |
| error_growth | 0.005 | 0.082(均 <1,无发散) |
### 4.2 逐 episode 对比(闭环滚动 mean_mse)
| Episode | 步数 | 模型 A | 模型 B | 比值 |
|---|---|---|---|---|
| 0000 stand | 76 | 25.5 | 134.0 | 5.26× 恶化 |
| 0001 jump | 22 | 20.5 | 221.1 | 10.78× 恶化 |
| **0002 left-hand** | 118 | 21252.2 | **6849.7** | **0.32× 改善** |
| 0003 left-turn | 53 | 276.0 | 641.0 | 2.32× 恶化 |
| 0004 lie-down | 6 | 35.2 | 46.9 | 1.33× 恶化 |
| **0005 rght-hand** | 118 | 21340.0 | **8022.9** | **0.38× 改善** |
| 0007 stdd | 41 | 49.8 | 115.9 | 2.33× 恶化 |
| 0008 t0 | 206 | 644.5 | 553.3 | 0.86× 改善 |
| 0009-0012 tilt-* | 41×4 | 29.8-34.2 | 59.6-751.7 | 1.74-25.12× 恶化 |
### 4.3 误差主导列定位
对 left-hand 逐列分析(模型 A,mean_mse 21252):
- **舵机列(0-9)预测全部在合理范围**(35-85°),无物理危险
- **列 10(spk.F)是 MSE 主导源**:该脚本真值 10→1000Hz,模型预测 ~340Hz 折中值 → 单列平方误差 10万-44万
- 列 8、12-17 为恒值列(s9=90°、保留位 0),预测有微小偏差但绝对值无害
### 4.4 指标退化发现
1. **oob_rate=100%(模型 A)不可信**:数据集中 7 个恒值列(span=1e-6)经验界宽度为零,任何微小偏差都计越界。模型 B 的 28.1% 改善主要来自 spk.F 落入经验界,指标本身仍需修正(见开发方案 §3)
2. **error_growth 有误导性**:不同长度 episode 混叠使曲线非单调(中期尖峰来自 123 帧 hand episode,结束后回落),末段/首段比值掩盖了中期爆涨
### 4.5 核心结论
归一化把各列方差拉平,隐式实现了"全列等权",带来 spk.F 跟踪改善(hand 类 3× 提升),代价是舵机角跟踪精度下降(简单动作 1.3-25× 恶化)。缩放方式与列重要性是**正交**的两个维度,需要显式逐列权重解耦——详见开发方案 `actpred_action_norm_weighting_plan_20260814.md`。
---
## 5. 产物与提交
| 产物 | 位置 |
|---|---|
| 数据集(865 帧 / 804 样本) | `data/pybot_record/`(231MB,不入 git) |
| 模型 A checkpoint | `actpred/checkpoints/recorded_run/` |
| 模型 B checkpoint | `actpred/checkpoints/recorded_run_norm/` |
| 评估报告 JSON | `data/eval_recorded.json` / `data/eval_recorded_norm.json` |
| 批录制脚本 | `data/record_all.sh` |
相关提交:`af0ff56`(record_stand.mt)、`7ec8c9d`(stats 路径 bug 修复)。
## 6. 局限
- 帧为合成噪声、IMU 全零:视觉/姿态通路未学习到真实信号,结论仅覆盖动作动力学部分
- 验证集仅 1 个 episode(41 样本):模型选择噪声大,最优 epoch 有偶然性
- 两个模型各 13 万步级样本、CPU 训练 2-3 小时:结论在更大数据与实机数据上需复核