pybot --loop HEF 后端异步闭环(执行上一步动作与 NPU 推理并行)与 actpred predict_async 流水线落地后未同步文档,本次补齐: - pybot/README:--loop 补异步推理路径说明(滞后一拍,~16ms→~12ms) - actpred/README:Hailo 节新增 predict_async 小节(缓冲语义/2.6× 实测) - 根 README、MODULES.md、ARCHITECTURE.md:pybot 行与机器人数据链同步 - MODULES.md:docs md 计数 70→76;ARCHITECTURE §9 挂 panel 设计稿规划项
328 lines
19 KiB
Markdown
328 lines
19 KiB
Markdown
# actpred — 动作预测模块
|
||
|
||
输入多帧摄像头图像 + 历史动作序列,预测下一时间步的连续动作(回归)。
|
||
|
||
纯神经网络实现(PyTorch):轻量 CNN 或 MobileNetV2 逐帧编码,图像/动作 token 进轻量
|
||
Transformer(pre-LN)自注意力融合,取 [PRED] token 回归下一步动作。
|
||
默认配置总参数量 ~450K。
|
||
|
||
## 架构
|
||
|
||
```
|
||
F 帧图像 (F×3×H×W) ─► 共享 CNN 编码器 ─► F 个 token ─► proj → d_model ┐
|
||
├─► [img×F, act×H, (IMU), PRED]
|
||
动作历史 (H×D) ─► Linear proj → d_model ─► H 个 token ────────┤ │ + 位置编码
|
||
IMU 姿态 (imu_dim) ─► Linear proj → 1 个 token(R2,可选)────────┤ ▼
|
||
可学习 [PRED] token ────────────────────────────────────────┘ TransformerEncoder (pre-LN, 2 层, 4 头, d_model=128)
|
||
│
|
||
取 PRED 位置输出 ─► MLP 头 ─► 预测动作 (action_dim)
|
||
```
|
||
|
||
token 序列长度固定为 F+H+1(默认 3+5+1=9;`--imu-dim`>0 时 IMU token 插到
|
||
PRED 之前,长度 F+H+2),除 batch 外无动态 shape,对 NPU(如树莓派
|
||
AI 扩展板)部署友好。
|
||
|
||
## 快速开始
|
||
|
||
```bash
|
||
# 安装(默认 CPU-only;如需 GPU 支持,编辑 _set.sh 按注释说明操作)
|
||
cd actpred && bash _set.sh
|
||
|
||
# 训练
|
||
bash _run.sh train --data-dir ./data --epochs 100 --action-dim 18
|
||
|
||
# 断点续训练(从 last.pt 恢复 model/optimizer/scheduler/epoch)
|
||
bash _run.sh train --data-dir ./data --epochs 100 --resume checkpoints/last.pt
|
||
|
||
# R1 改进:残差预测 + 多步展开训练(可叠加)
|
||
# --predict-delta 预测 Δ=actions[t]-actions[t-1](惯性先验,平滑动作;推理端自动加回 a_{t-1})
|
||
# --rollout-len K 多步展开训练(K>1 时 scheduled sampling,治 exposure bias)
|
||
# --ss-prob P scheduled sampling 真实历史概率(默认 1.0=全真;0.5 表示半数步用自预测回传)
|
||
bash _run.sh train --data-dir ./data --epochs 100 --action-dim 18 \
|
||
--predict-delta --rollout-len 4 --ss-prob 0.7
|
||
|
||
# R2 姿态反馈:--imu-dim 8 启用独立 IMU token(数据需含 imu.npy,见"数据格式")
|
||
bash _run.sh train --data-dir ./data --epochs 100 --action-dim 18 --imu-dim 8
|
||
|
||
# 归一化+逐列加权:固定物理范围归一化(免 stats 文件)+ 舵机列损失权重上浮
|
||
bash _run.sh train --data-dir ./data --epochs 100 --action-dim 18 \
|
||
--normalize-actions --normalize-mode fixed_range
|
||
# 自定义列权重(逗号分隔 action_dim 个浮点;默认舵机1.0/spk0.2/保留位0.0)
|
||
bash _run.sh train --data-dir ./data --epochs 100 --normalize-actions \
|
||
--normalize-mode fixed_range --column-weights "1,1,1,1,1,1,1,1,1,1,0.2,0.2,0,0,0,0,0,0"
|
||
|
||
# 推理(--images 按时间顺序旧→新传入 frame_len 个图像路径,冷启动时可重复传同一帧;
|
||
# --action-history 格式:冒号分隔各步,每步 action_dim 个值,共 history_len 步)
|
||
bash _run.sh --model checkpoints/best_inference.pt \
|
||
--images frame_t-2.jpg frame_t-1.jpg frame_t.jpg \
|
||
--action-history "45,0,45,0,45,0,45,0,90,90,10,200,0,0,0,0,0,0:45,0,45,0,45,0,45,0,90,90,0,20,0,0,0,0,0,0:45,0,45,0,45,0,45,0,90,90,0,0,0,0,0,0,0,0:45,0,45,0,45,0,45,0,90,90,0,0,0,0,0,0,0,0:45,0,45,0,45,0,45,0,90,90,0,0,0,0,0,0,0,0"
|
||
|
||
# 导出 ONNX(如安装了 onnxruntime,导出后自动验证一致性)
|
||
bash _run.sh export --model checkpoints/best_inference.pt --output model.onnx
|
||
|
||
# R4 离线评估:开环滚动预测,量化闭环代理指标(误差发散/平滑度/越界率)
|
||
python evaluate.py --model checkpoints/best_inference.pt --data-dir ./data
|
||
# A/B 对比两个 checkpoint(负值 = 主模型更优)
|
||
python evaluate.py --model checkpoints/r1.pt --compare checkpoints/baseline.pt \
|
||
--data-dir ./data --episodes 3 --horizon 50 --output report.json
|
||
|
||
# 运行单元测试
|
||
python test_actpred.py
|
||
```
|
||
|
||
> **注意**:torch ≥ 2.12 的 ONNX 导出依赖 `onnxscript`,`_set.sh` 已一并安装;
|
||
> 如单独使用 requirements.txt 安装,请额外执行 `pip install onnxscript onnxruntime`。
|
||
|
||
## 预训练(从 .mt 脚本,无需图像)
|
||
|
||
从 bot4cj 的 `.mt` 动作脚本中提取 `@Action` 矩阵,用全零多帧图像占位预训练动作动力学(history → next action),
|
||
保存的 checkpoint 与正常训练格式完全兼容,可直接用于推理或作为微调起点。
|
||
|
||
```bash
|
||
# 预训练(解析 ../bot4cj/scripts/*.mt)
|
||
bash _run.sh pretrain --mt-dir ../bot4cj/scripts --epochs 200 --noise-std 1.0
|
||
|
||
# 预训练断点续训
|
||
bash _run.sh pretrain --mt-dir ../bot4cj/scripts --epochs 200 --noise-std 1.0 \
|
||
--resume checkpoints/pretrain_last.pt
|
||
|
||
# 用预训练权重作为微调起点(pretrain 也支持 --init-from,行为同 train)
|
||
bash _run.sh train --data-dir ./data --epochs 50 \
|
||
--init-from checkpoints/pretrain_best_inference.pt
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `--mt-dir` | .mt 脚本目录(如 `../bot4cj/scripts`) |
|
||
| `--noise-std` | 高斯噪声标准差(数据增强,建议 0.5~2.0;与 `--normalize-actions` 联用时按 z-score 单位,建议 0.05~0.2) |
|
||
| `--random-images` | 用 ImageNet 归一化均匀噪声替代全零图像占位,**恢复 BN 统计**(详见下方警告) |
|
||
| `--init-from` | 预训练 checkpoint 路径,作为微调起点(strict=False;与当前运行动作空间不一致时打印 WARNING) |
|
||
|
||
> **注意**:.mt 脚本列数(12)与 `action_dim`(18)不一致时自动补零。预训练保存到 `checkpoints/pretrain_best_inference.pt`。
|
||
> 预训练与微调必须使用**同一动作空间**(`--normalize-actions` 同时开或同时关),
|
||
> 否则输入分布与权重不匹配,预训练收益会大打折扣。
|
||
|
||
> **警告(2026-08-13 实测)**:全零图像占位会让 BN `running_var` 停在初始小值,
|
||
> 推理时特征放大到 ±1e6,**HEF uint8 量化不可行**(输出对输入不敏感)。
|
||
> 面向 NPU 部署的预训练/微调请务必使用 `--random-images`(或真实数据微调),
|
||
> 导出时 `export_deploy.py` 的 `check_token_ranges` 会拦截退化的 checkpoint。
|
||
> 详见 [根因分析报告](../docs/reports/actpred_hef_insensitivity_rootcause_20260813.md)。
|
||
> 建议同时开启 `--normalize-actions`:z-score 后 act_tokens 与 img_tokens 同量级,
|
||
> 避免 HEF merge 点共享量化 scale 粗量化图像路径。
|
||
|
||
## 数据格式
|
||
|
||
```
|
||
data/
|
||
episode_001/
|
||
images/
|
||
0000.jpg
|
||
0001.jpg
|
||
...
|
||
actions.npy # shape: (T, action_dim) float32
|
||
imu.npy # 可选,shape: (T, 8×N) float32(N 个 MPU6050 姿态拼接,R2)
|
||
episode_002/
|
||
...
|
||
```
|
||
|
||
每个 timestep 的样本:帧 `[t-frame_len+1, t]`(旧→新)+ 前 `history_len` 步动作 → 当前步动作。
|
||
采样起点为 `max(history_len, frame_len-1)`。
|
||
|
||
`imu.npy` 每传感器占 8 槽 `[roll, pitch, ax, ay, az, gx, gy, gz]`(roll/pitch
|
||
角度制,其余为 MPU6050 原始 int16),多传感器(最多 4 个)按脚本
|
||
`@MPU6050_BIND` 行顺序拼接,由 pybot `--record` 逐帧同步落盘。**旧 episode
|
||
没有姿态数据,训练 IMU 模型需用新版 pybot 重新录制**;`--imu-dim`>0 时缺失
|
||
`imu.npy` 的 episode 按零向量兜底(向后兼容,但无姿态先验收益)。
|
||
姿态归一化按物理量程固定缩放(roll/pitch ÷180,原始 int16 ÷32768,8 槽模式
|
||
按传感器数平铺),训练与推理共用 `imu_norm.py`,无需统计文件。
|
||
录制/训练/闭环脚本的传感器行顺序必须一致,否则槽位错位。
|
||
|
||
### 数据采集
|
||
|
||
```python
|
||
from dataset import DataRecorder
|
||
|
||
recorder = DataRecorder("./data", action_dim=18)
|
||
for image, action in robot_loop():
|
||
recorder.record(image, action)
|
||
recorder.flush()
|
||
```
|
||
|
||
## 配置
|
||
|
||
所有超参数通过 CLI 参数覆盖,默认值见 `config.py`:
|
||
|
||
| 参数 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `--image-width` | 640 | 图像缩放宽度 |
|
||
| `--image-height` | 480 | 图像缩放高度 |
|
||
| `--action-dim` | 18 | 动作向量维度(对齐 bot4cj 18 列:10 舵机 + 2 云台 + 扬声器频率 + 延时 + 4 预留)|
|
||
| `--history-len` | 5 | 历史动作窗口长度 |
|
||
| `--frame-len` | 3 | 输入图像帧数(含当前帧,旧帧在前)|
|
||
| `--imu-dim` | 0 | R2 姿态输入维度(>0 启用独立 IMU token;8×N,N=传感器数≤4)|
|
||
| `--backbone` | simple | CNN 类型 (simple/mobilenet) |
|
||
| `--attn-dim` | 128 | 注意力 token 维度 d_model(须能被 attn-heads 整除)|
|
||
| `--attn-heads` | 4 | 注意力头数 |
|
||
| `--attn-layers` | 2 | TransformerEncoder 层数 |
|
||
| `--epochs` | 100 | 训练轮数 |
|
||
| `--lr` | 0.001 | 学习率 |
|
||
| `--batch-size` | 4 | 批次大小(默认分辨率 640×480;更高分辨率需调小,小分辨率可调大) |
|
||
| `--normalize-actions` | True | 动作归一化(`--no-normalize-actions` 关闭;2026-08-14 起默认开启——复现旧行为需显式关闭) |
|
||
| `--normalize-mode` | zscore | 归一化方式:`zscore`=数据统计(依赖 `action_stats.npy`);`fixed_range`=固定物理范围 min-max→[0,1](常量表见 `action_norm.py`,免 stats 文件,越界 clamp 防御) |
|
||
| `--column-weights` | 舵机1.0/spk0.2/保留位0.0 | 逐列损失权重(逗号分隔 action_dim 个浮点;仅影响训练损失,推理端无感知;**始终生效**——包括 `--no-normalize-actions` 的未归一化训练) |
|
||
| `--dropout` | 0.1 | Dropout 率 |
|
||
| `--weight-decay` | 0.0001 | 权重衰减 |
|
||
| `--val-split` | 0.15 | 验证集比例 |
|
||
| `--patience` | 15 | 早停耐心值 |
|
||
| `--max-grad-norm` | 1.0 | 梯度裁剪上限 |
|
||
| `--seed` | 42 | 随机种子 |
|
||
| `--num-workers` | 2 | DataLoader 工作进程数 |
|
||
| `--checkpoint-dir` | ./checkpoints | 检查点保存目录 |
|
||
| `--init-from` | 无 | 预训练 checkpoint 路径,作为微调起点(strict=False) |
|
||
| `--resume` | 无 | 从 last.pt 断点续训练(恢复 model/optimizer/scheduler/epoch;scheduler 的 T_max 一并恢复,resume 时改 `--epochs` 不影响 LR 余弦曲线,只影响循环边界) |
|
||
| `--save-config` | 无 | 保存配置到 JSON 文件 |
|
||
| `--predict-delta` | False | R1-C 残差预测(target 改 Δ,推理端自动加回 a_{t-1}) |
|
||
| `--rollout-len` | 1 | R1-B 多步展开步数 K(>1 时 scheduled sampling) |
|
||
| `--ss-prob` | 1.0 | scheduled sampling 真实历史概率(仅 rollout-len>1 生效) |
|
||
|
||
## 离线评估(R4)
|
||
|
||
`evaluate.py` 在留出 episode 上**开环滚动预测**(复现 pybot 闭环口径:图像用真值滚动、
|
||
动作用自预测回写),输出闭环代理指标。单步 MSE(teacher forcing)看不见闭环误差累积,
|
||
本评估补上这一盲区:
|
||
|
||
| 指标 | 含义 | 判读 |
|
||
|------|------|------|
|
||
| `overall_mse` | 全部滚动步的平均 MSE | 越低越好 |
|
||
| `error_growth` | 末段(后25%)误差 / 首段(前25%)误差(按 episode 分别计算后取中位数) | >1 表示误差随步数发散(闭环不稳) |
|
||
| `error_curve` | 误差随步数的增长曲线 | 看发散拐点 |
|
||
| `smoothness_ratio` | 预测平滑度 / 真值平滑度 | <1 表示比真值更平滑(残差模式的预期收益) |
|
||
| `oob_rate` | 预测落在物理范围外的比例 | 越高越危险(舵机越界风险) |
|
||
|
||
越界判定用固定物理界(`action_norm.py` 常量表:舵机 [0,180]、spk.F [0,5000] 等),
|
||
"越物理界"比"越经验界"更符合安全语义,且恒值列不会使经验界退化。`--compare` 用于 A/B 对比(如 R1 开/关)。
|
||
滚动复现闭环口径:模型带 imu 输入时按 episode 的 `imu.npy` 真值姿态逐步喂入
|
||
(缺失则零向量,与 pybot 闭环无 `@MPU6050_BIND` 块时的行为一致)。
|
||
|
||
## 部署注意
|
||
|
||
- 启用 `--normalize-actions` 且 `--normalize-mode zscore`(默认)训练的模型,其 `action_stats.npy`
|
||
保存在 checkpoint 同目录;部署时**必须将 stats 文件与 checkpoint 一起拷贝**(推理端优先按
|
||
checkpoint 内记录的路径查找,找不到时回退到 checkpoint 同目录的 `action_stats.npy`)。
|
||
- `--normalize-mode fixed_range` 用固定物理范围 min-max→[0,1](常量表见 `action_norm.py`),
|
||
**免 stats 文件**,消除 stats 路径类 bug 的整类风险;推理端(predict/onnx_infer/hailo_infer)
|
||
从 checkpoint/ONNX 元数据自动识别归一化方式(含 predict_delta 残差模式),无需额外参数。
|
||
注:旧版导出的 ONNX 无元数据,onnx_infer 回退 zscore + 非残差,需显式
|
||
`--normalize-mode/--predict-delta` 或重新导出。
|
||
- `actions.npy` 的列数必须与 `--action-dim` 一致;图像与动作帧数不一致时训练会打印 WARNING 并按较少者使用。
|
||
|
||
## 编程 API
|
||
|
||
```python
|
||
from predict import ActionPredictorInference
|
||
|
||
predictor = ActionPredictorInference("checkpoints/best_inference.pt")
|
||
print(predictor.action_dim, predictor.history_len, predictor.frame_len) # 公共属性
|
||
# images: 长度为 frame_len 的 RGB 图像列表(旧→新,末帧为当前帧;冷启动可重复同一帧)
|
||
action = predictor.predict(images, action_history) # np.ndarray (action_dim,)
|
||
# action_history 形状须为 (history_len, action_dim)
|
||
# 模型含 IMU 输入时(predictor.imu_dim 非 None)可传 imu= 原始姿态向量
|
||
# (roll,pitch,ax,ay,az,gx,gy,gz;内部自动归一化,None 等价零向量)
|
||
action = predictor.predict(images, action_history, imu=imu_vec)
|
||
```
|
||
|
||
## ONNX Runtime 后端(CPU 加速)
|
||
|
||
`onnx_infer.py` 的 `OnnxPredictor` 走 ONNX Runtime CPU 推理,接口与
|
||
`ActionPredictorInference` 对齐(含可选 `imu=` 入参),维度从 ONNX 输入元数据
|
||
自动推导(无需 checkpoint)。Pi 5 实测(224×224,2026-08-13 复测)11.4ms/次含预处理
|
||
(纯推理 10.7ms),PyTorch 21ms,约 2 倍加速,与 torch 前向输出差异
|
||
<1e-5。数据见
|
||
[测速报告](../docs/reports/actpred_inference_speed_benchmark_20260813.md) §6。
|
||
|
||
```bash
|
||
bash _run.sh export --model checkpoints/best_inference.pt --output model.onnx
|
||
```
|
||
|
||
```python
|
||
from onnx_infer import OnnxPredictor
|
||
|
||
with OnnxPredictor("model.onnx") as predictor: # stats_path= 仅归一化模型需要
|
||
action = predictor.predict(images, action_history)
|
||
```
|
||
|
||
```bash
|
||
python3 onnx_infer.py --model model.onnx --images f1.jpg f2.jpg f3.jpg \
|
||
--action-history "45,0,...:45,0,..." [--stats action_stats.npy]
|
||
```
|
||
|
||
## Hailo 部署(AI HAT 2 / Hailo-10H)
|
||
|
||
模型需先经 Hailo DFC(仅 x86 Linux,`venv_dfc`)编译为 HEF。
|
||
训练态 ONNX(5D 输入、AdaptiveAvgPool、MultiheadAttention 的 head
|
||
reshape)**无法被 DFC 解析**,必须用 `export_deploy.py` 的部署版导出
|
||
(按帧展开 CNN、固定 kernel AvgPool、注意力按头展开为独立 Linear、
|
||
全程 (1,1,S,d) 无 transpose 布局,权重数值不变)。
|
||
|
||
当前 NPU 目标分辨率为 **224×224**(pretrain checkpoint 配置),HEF 输入
|
||
为 frame0~2 各 (1,224,224,3) NHWC + action_history (1,1,5,18) +
|
||
pred_token (1,1,1,128)(`--imu-dim`>0 时追加 imu (1,1,1,imu_dim),共 6 输入),
|
||
形状与量化参数由 `hailo_infer.py` 从 vstream
|
||
info 自动推导,不硬编码。
|
||
|
||
```bash
|
||
# x86 主机(venv_dfc 内已装 hailo-sdk 5.3):
|
||
# 1. 导出部署版 ONNX + 学习到的 pred_token(推理端喂入用)
|
||
python3 export_deploy.py --model checkpoints/pretrain_best_inference.pt \
|
||
--pred-token-out checkpoints/actpred_deploy_pred_token.npy
|
||
# 2. 生成校准集(pred_token 按学习值 ±30% 抖动,避免常量量化退化)
|
||
python3 gen_calibration.py --deploy --model checkpoints/pretrain_best_inference.pt
|
||
# 3. 一键编译(parser → optimize → compiler,各步自动做输入连通性检查)
|
||
python3 dfc_compile.py # 产物 checkpoints/actpred_pretrain.hef
|
||
# 4. 将 HEF + actpred_deploy_pred_token.npy 拷回树莓派
|
||
```
|
||
|
||
已验证:完整 110 层网络(含 matmul/softmax/layernorm 注意力)解析、
|
||
量化、编译全部通过;hailo10h 目标。
|
||
编译要点与踩坑记录见 `dfc_compile.py` 文档字符串。
|
||
|
||
> **验证记录(2026-08-13 树莓派实测)**:归一化轮次(`--normalize-actions`
|
||
> + `--random-images` 预训练,224×224)重编译的 HEF 已通过 `_npu_test.py`
|
||
> 实机复验:img 敏感性 22.3、hist 敏感性 38.9(阈值 0.1),HEF vs torch
|
||
> max_diff 0.37(原始动作空间),约 12ms/次(含预处理)。此前两轮失败的
|
||
> 根因与修复均已闭环:
|
||
> - BN 统计退化 → `--random-images` 预训练 + `export_deploy.py`
|
||
> `check_token_ranges` 导出前拦截
|
||
> - pred_token 常量校准退化(scale=18571.7)→ 抖动校准,推理端喂学习值
|
||
> - 输入层断连 → `dfc_compile.py` parser/optimize 后连通性检查
|
||
> - vstream 报告顺序乱序 → `hailo_infer.py` 按 `input_layerN` 排序绑定
|
||
> - 帧归一化判定错误(把归一化帧当原始像素喂,图像路径饱和破坏)→
|
||
> 按量化网格 dequant(0) 判定,见 hailo_infer.py 注释
|
||
>
|
||
> 验证链:`export_deploy.py`(token 量级)→ `_onnx_validate.py`(ONNX
|
||
> 数值)→ `dfc_compile.py`(输入连通性)→ `_npu_test.py`(4 角敏感性)。
|
||
> 遗留:全零历史角(z≈-3.5)超出校准输入范围(-2.7)会被截断,
|
||
> 该角仅作连通性验证,非真实部署场景。
|
||
|
||
`hailo_infer.py` 的 `HailoPredictor` 适配 5/6 输入 HEF(含可选 imu 输入,
|
||
按形状自动归类绑定),输入布局与量化
|
||
参数(帧归一化由量化网格 dequant(0) 自动判定)从 vstream info 自动推导,
|
||
单元测试含 fake hailo_platform 的 5 输入与乱序报告顺序用例。CLI 支持
|
||
`--pred-token` 喂入学习值、`--stats` 喂入归一化统计、`--imu` 喂入姿态。
|
||
环境调研与 API 踩坑记录见
|
||
[docs/reports/actpred_hailo_hat2_inference_research_20260813.md](../docs/reports/actpred_hailo_hat2_inference_research_20260813.md)。
|
||
|
||
### 异步推理(`predict_async`,run_async 流水线)
|
||
|
||
`HailoPredictor.predict_async(images, action_history, pred_token=None, imu=None)`
|
||
提交异步推理并立即返回 `_AsyncJob`,需要结果时 `job.result(timeout)` 阻塞获取:
|
||
|
||
- 与同步 `predict` 语义/输出完全一致(公共 `_prepare_inputs/_fill_bindings/_postprocess`)
|
||
- 内部维护 4 个绑定缓冲池 + 完成回调;在途任务超过缓冲数时提交会阻塞等待
|
||
空闲缓冲,`job.result` 超时**不会**释放缓冲(缓冲由完成回调释放)
|
||
- 连续提交(≥2 个在途)时 NPU 保持满负荷:实测连续吞吐 6.0ms vs 同步
|
||
15.5ms(约 2.6×);扩展测试覆盖乱序取结果/混合调用/缓冲压力/300 次
|
||
稳定性/4 线程并发,输出与同步 0 差异
|
||
- pybot `--loop` 检测到 `predict_async` 即自动走异步闭环(推理期间并行
|
||
执行上一步动作),见 [pybot/README.md](../pybot/README.md)
|