Files
blackbird/docs/reviews/pybot_actpred_AUDIT_20260814_v2.md
T
qing 85bf68f505 feat(actpred): 固定范围归一化 + 逐列加权损失,附 pybot*actpred 审计修复
新增 action_norm.py 固定物理范围 min-max 归一化,无 stats 文件零状态部署;train.py 引入 _weighted_mse 逐列加权(舵机 1.0/spk 0.2/保留位 0.0)。审计修复: P0-1 pybot 读 normalize_mode, R2-1 NaN 阻断 best, P2-a 后端大小写不敏感, P1-3 utf-8-sig BOM, R1-1 data_dir 覆盖, R2-7 _run.sh 报错, R2-3 空 val 存 best.pt, P2-a2 误报警, R1-2 合并 parse_action_history, P2-b 合并 numpy import。评估修正 oob 物理界+error_growth 按 episode 中位数。推理部署全链支持 fixed_range。
2026-08-14 18:26:03 +08:00

191 lines
10 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pybot & actpred 审计报告(第二版 —— 归一化+逐列加权 落地后)
> 审查日期:2026-08-14 (Asia/Shanghai)
> 审查人:AI Assistant(SOLO Coder)
> 审查范围:`pybot/`(机器人运行时)与 `actpred/`(动作预测)两模块
> 代码基线:归一化+逐列加权方案落地后(12 个文件改动,531 行新增)
>
> 本报告对照三份历史审计报告追踪修复状态,并审查本轮新增代码。
---
## 一、审计结论摘要
| 级别 | 新发现 | 历史遗留 | 说明 |
|------|--------|---------|------|
| **P0(崩溃/数据损坏)** | 2 | 0 | 见 §二 |
| P1(功能风险) | 0 | 5 | 见 §三 |
| P2(代码质量) | 3 | 9 | 见 §四 |
**重点:2 个 P0 必须在提交前修复。**
---
## 二、P0 新发现
### P0-1 pybot HailoPredictor 缺失 normalize_mode 传递
**位置**:[pybot/main.py:180-183](../../pybot/pybot/main.py#L180-L183)
```python
return _P(checkpoint, stats_path=stats_path,
predict_delta=predict_delta,
pred_token=pred_token)
```
**问题**:`HailoPredictor.__init__` 的 `normalize_mode` 参数默认 `"zscore"`,pybot 调用时未传入此参数。对于 `fixed_range` 训练的 HEF,推理端应按 `action_norm.py` 固定物理范围对历史动作做 min-max→[0,1] 归一化,但 pybot 未传参导致 `_fixed_range=False`,历史动作以原始量纲送入 HEF。
**影响**:`fixed_range` 模式训练的 HEF 在 pybot 闭环中推理输出完全错误(舵机会被推到安全范围外)。`zscore` 模式不受影响(默认值偶然正确)。
**修复建议**:pybot 从同目录 checkpoint 读取 `normalize_mode` 并传入 `HailoPredictor`:
```python
# 在 _load_predictor 中,从 companion checkpoint 读取 normalize_mode
normalize_mode = "zscore"
for name in ("pretrain_best_inference.pt", "best_inference.pt"):
ckpt_path = os.path.join(hef_dir, name)
if os.path.exists(ckpt_path):
try:
import torch
ck = torch.load(ckpt_path, map_location="cpu", weights_only=True)
normalize_mode = ck.get("config", {}).get("data", {}).get(
"normalize_mode", "zscore")
except Exception:
pass
break
return _P(checkpoint, stats_path=stats_path,
predict_delta=predict_delta,
pred_token=pred_token,
normalize_mode=normalize_mode)
```
### P0-2 DataConfig normalize_actions 默认值变更,与 README 不一致
**位置**:[actpred/config.py:14](../../actpred/config.py#L14)
```python
normalize_actions: bool = True # 旧默认值:False
```
**问题**:默认值从 `False` 改为 `True`,但 [README.md 配置表](../../actpred/README.md#L163) 仍标注 `False`。CLI 不经 `--normalize-actions` 参数时,`apply_overrides` 会使用 dataclass 默认值 `True`,导致行为变更:
- 旧脚本(`bash _run.sh train --data-dir ./data --epochs 100`)之前不归一化,现在开启 zscore 归一化,且会自动计算 `action_stats.npy`。
- 对预训练(`pretrain_mt.py`)同样影响:预训练数据从 .mt 脚本提取,zscore 归一化后分布与旧 checkpoint 不同。
**影响**:所有无 `--normalize-actions` 或无 `--no-normalize-actions` 的训练命令行为改变;旧 checkpoint 做 `--init-from` 时 `warn_init_from_mismatch` 会警告但不会阻止。
**修复建议**:二选一:
- **方案 A**:恢复默认值 `False`(保持向后兼容,推荐);
- **方案 B**:改 README 为 `True`,并在 CHANGELOG 中标注 breaking change。
---
## 三、P1 历史遗留(未修复)
| # | 来源 | 位置 | 问题 | 优先级 |
|---|------|------|------|--------|
| P1-3 | 闭环审计 | [actpred/mt_parser.py:33](../../actpred/mt_parser.py#L33) | `read_text(encoding="utf-8")` 不剥离 BOM,`@Action` 为首块时表头列名可能粘附 `` | 低(低概率触发) |
| P2-a | 闭环审计 | [pybot/main.py:143](../../pybot/pybot/main.py#L143) | `_load_predictor` 仅用 `endswith(".hef")` 区分后端,大小写敏感,无 ONNX 分支 | 中 |
| R2-1 | actpred 审计 | [actpred/train.py:270](../../actpred/train.py#L270) | NaN val_loss 永久阻断 best checkpoint 保存(`NaN < x` 恒 False) | 中 |
| R1-1 | actpred 审计 | [actpred/pretrain_mt.py:185](../../actpred/pretrain_mt.py#L185) | `cfg.data.data_dir = args.mt_dir` 覆盖,`--save-config` 导出语义错误 JSON | 低 |
| R2-7 | actpred 审计 | [actpred/_run.sh:14-16](../../actpred/_run.sh#L14-L16) | 未知子命令静默回退到 `predict.py`,拼写错误难以发现 | 低 |
---
## 四、P2 历史遗留(未修复)
| # | 来源 | 位置 | 问题 |
|---|------|------|------|
| P2-b | 闭环审计 | pybot/robot.py 等 3 处 | numpy 在闭环路径 3 个函数各自 `import` |
| P2-c | 闭环审计 | pybot/tests/test_loop.py | 未覆盖闭环 + recorder 集成、predictor 异常清理 |
| P2-d | 闭环审计 | actpred/dataset.py | 图像 Resize 不保持宽高比 |
| P2-e | 闭环审计 | actpred/train.py | resume 时改 `--epochs` 会重算 lr scheduler |
| P2-f | 闭环审计 | actpred/export_deploy.py | ManualAttnBlock 依赖 PyTorch 内部布局约定 |
| R2-3 | actpred 审计 | actpred/train.py | 空验证集时 `best.pt` 从不创建 |
| R2-4 | actpred 审计 | actpred/_set.sh | CUDA 版 torch 误装风险 |
| R1-2 | actpred 审计 | predict/onnx_infer/hailo_infer | `parse_action_history` 三处重复 |
| R2-9 | actpred 审计 | actpred/predict.py | `--action-dim` override 参数无用且是陷阱 |
---
## 五、已完成修复的审计项
| # | 来源 | 修复内容 |
|---|------|---------|
| P1-1 | 闭环审计 | 闭环节拍 overrun 告警已实现([robot.py:307-320](../../pybot/pybot/robot.py#L307-L320)),含比例统计 |
| P1-2 | 闭环审计 | 闭环帧源改走 `_loop_recent_frames`([robot.py:323-338](../../pybot/pybot/robot.py#L323-L338)),从守护线程队列取帧 |
| P1-4 | 闭环审计 | DataRecorder output_dir 已有 `mkdir(parents=True, exist_ok=True)`([recorder.py:46](../../pybot/pybot/recorder.py#L46)) |
| R2-2 | actpred 审计 | ActionDataset 外部 stats 文件已补 shape 校验([dataset.py:92-95](../../actpred/dataset.py#L92-L95)) |
---
## 六、本轮新增代码审查(归一化+逐列加权方案)
### 6.1 action_norm.py — 通过,有 1 处 P2 建议
**总体评价**:常量表清晰、文档完善、clamp 防御到位。18 个单测覆盖正常值/边界/越界/多维度。
**P2-a1(建议)**:`default_column_weights` 的 `action_dim` 默认为 18,但与 `action_bounds` 的行为不同——`action_bounds` 对 `action_dim > 18` 自动补零,`default_column_weights` 对此类情况返回全零权重(因为 `min(10, action_dim)` 和 `min(12, action_dim)` 仅覆盖前 12 列,其余列保持 `np.zeros` 初始值)。建议对齐行为:`action_dim > 18` 时对超出列也赋默认权重(如 0.2)。
### 6.2 config.py — 通过,有 1 处 P2
**总体评价**:`normalize_mode` 和 `column_weights` 的 CLI 解析、值域校验、checkpoint 兼容性警告均实现完整。
**P2-a2(建议)**:`warn_init_from_mismatch` 中 `init_mode = init_data.get("normalize_mode", "zscore")` 对旧 checkpoint 正确回退。但旧 checkpoint 的 `normalize_actions=False` 时,`normalize_mode` 无意义,此时警告 `init_mode != current.data.normalize_mode` 是误报。建议仅在 `init_norm and current.data.normalize_actions` 时比较 `normalize_mode`。
### 6.3 dataset.py — 通过
固定范围归一化集成正确:`_fixed_range` 标志位在 `__init__` 中设置,`_norm` 方法按标志位分发。stats shape 校验(R2-2 修复)已补。
### 6.4 train.py — 通过,有 1 处 P2 行为变更
**总体评价**:`_weighted_mse` 实现正确,`weights.mean()` 归一化保证加权 Loss 与无权重 Loss 量级可比。`column_weights` 为 None 时自动取 `default_column_weights`。
**P2-a3(行为变更)**:`column_weights` 现在**始终生效**(None 时取默认值),即使 `normalize_actions=False`。这意味着不归一化训练的模型也会按列加权损失——模型对舵机列(权重 1.0)的优化力度高于 spk 列(0.2)。这并非 bug,但行为变更需在 README 注明。
### 6.5 evaluate.py — 通过
物理界 OOB 与 `action_norm.py` 常量表共用(`_action_bounds` 调用 `action_bounds`)。error_growth 按 episode 分别计算取中位数,消除长短 episode 混杂偏差。实现正确。
### 6.6 推理/部署链 — 通过,P0-1 除外
| 文件 | 审查结论 |
|------|---------|
| predict.py | `_fixed_range` 从 checkpoint config 读取,normalize/denormalize 正确 |
| onnx_infer.py | `normalize_mode` 构造参数,predict 中 normalize/denormalize 正确 |
| hailo_infer.py | `normalize_mode` 构造参数,predict 中 normalize/denormalize 正确 |
| gen_calibration.py | `fixed_range` 模式校准数据走 `normalize_action` 变换,与推理端一致 |
| export_deploy.py | `check_token_ranges` 按 `normalize_mode` 在正确空间采样,stats_path 逻辑正确 |
**唯独 pybot 的 `_load_predictor` 未传 `normalize_mode` 给 HailoPredictor(P0-1)**。
### 6.7 test_actpred.py — 通过
新增 `TestFixedRangeNorm` 类 18 个测试用例,覆盖:
- 已知值归一化(舵机中位/边界、spk 中位)
- 越界 clamp(>180→1.0、<0→0.0)
- denormalize 往返一致性
- 默认列权重结构
- 自定义列权重
- 多维度批次归一化
- 全 0 输出验证
全量 148 passed / 11 skipped。
---
## 七、修复优先级
| 优先级 | 项 | 理由 |
|--------|-----|------|
| **P0** | P0-1 HailoPredictor normalize_mode 缺失 | `fixed_range` HEF 在 pybot 闭环中推理完全错误 |
| **P0** | P0-2 normalize_actions 默认值变更 | 所有无此参数的命令行为改变,README 不一致 |
| P1 | R2-1 NaN val_loss | 训练健壮性,一行 isfinite 修复 |
| P1 | P2-a 后端检测 | 路径大小写敏感,ONNX 无分支 |
| P2 | P2-a2 误报警告 | 旧 checkpoint 的不必要警告 |
| P2 | P2-a1 action_dim>18 | 未来扩展性 |
| P2 | P2-a3 行为变更文档 | README 注明 column_weights 始终生效 |
---
*本报告为归一化+逐列加权方案落地后的专项审计。建议先修复 2 个 P0 再提交。*