diff --git a/actpred/action_norm.py b/actpred/action_norm.py index 0ca3155..4655792 100644 --- a/actpred/action_norm.py +++ b/actpred/action_norm.py @@ -85,15 +85,13 @@ def denormalize_action(norm_action, action_dim: int = None, def default_column_weights(action_dim: int = 18) -> np.ndarray: - """逐列损失权重默认表:舵机 1.0(安全关键)、spk.F 0.2(保留梯度)、 - spk.delay 0.0(步级数据恒 10ms——微动作分解的隐式节拍,时长信息 - 已编码在步数中)、保留位 0.0(恒 0 纯噪声,参与只会稀释有效梯度)。 - action_dim>18 的扩展列默认 1.0:超出当前 18 列语义布局的通道视为 - 新执行器,静默给 0 会让其完全不参与训练(P2-a1)。""" + """逐列损失权重默认表:舵机 1.0(安全关键);spk.F / spk.delay 0.0 + (只影响声音/节奏不影响运动,推理端固定常量,见 bins.FIXED_OUTPUT); + 保留位 0.0(恒 0 纯噪声)。action_dim>18 的扩展列默认 1.0:超出 + 当前 18 列语义布局的通道视为新执行器,静默给 0 会让其完全不参与 + 训练(P2-a1)。""" w = np.zeros(action_dim, dtype=np.float32) w[:min(10, action_dim)] = 1.0 - if action_dim > 10: - w[10] = 0.2 if action_dim > 18: w[18:] = 1.0 return w diff --git a/actpred/bins.py b/actpred/bins.py index c7bce83..3df5abd 100644 --- a/actpred/bins.py +++ b/actpred/bins.py @@ -29,6 +29,26 @@ TOTAL_BINS = int(START[-1]) # 1864 LOG_COLS = (10, 11) # 对数分箱列 LOG_HI = {10: 5000.0, 11: 2000.0} +# 权重 0 列的输出固定值(原始量纲):spk.F 固定 0(只影响声音不影响 +# 运动,闭环中持续发声无意义);spk.delay 在步级数据中恒 10ms(微动作 +# 分解的隐式节拍)。无监督头在闭环中会输出中段垃圾污染历史, +# 推理/反馈时直接固定为常量。其余权重 0 列(保留位)固定 0。 +FIXED_OUTPUT = {10: 0.0, 11: 10.0} + + +def fixed_zero_columns(weights, action_dim: int = 18) -> np.ndarray: + """按列权重表返回权重<=0 列的固定输出向量(其余列 0) + + 与 default_column_weights 联动:列权重 0 → 推理输出固定为 + FIXED_OUTPUT 常量(缺省 0),避免无监督头在闭环反馈中输出垃圾。 + """ + w = np.asarray(weights, dtype=np.float32) + fixed = np.zeros(action_dim, dtype=np.float32) + for i in range(min(action_dim, len(w))): + if w[i] <= 0.0: + fixed[i] = FIXED_OUTPUT.get(i, 0.0) + return fixed + def bin_edges(col: int) -> np.ndarray: """列 col 的箱边界数组(长度 bins+1),原始量纲递增""" diff --git a/actpred/predict.py b/actpred/predict.py index 9ed2480..9c3e77e 100644 --- a/actpred/predict.py +++ b/actpred/predict.py @@ -10,7 +10,7 @@ from config import parse_checkpoint_config from imu_norm import normalize_imu from action_norm import (normalize_action, denormalize_action, is_range_mode) -from bins import decode as bin_decode +from bins import decode as bin_decode, fixed_zero_columns from model import ActionPredictor from utils import parse_action_history @@ -120,8 +120,16 @@ class ActionPredictorInference: ).unsqueeze(0).to(self._device) pred = self._model(img_tensor, history, imu_tensor).squeeze(0) # 归一化空间 if self._onehot: - # 多头 logits → 期望值解码 → 原始量纲(箱生活在原始空间) + # 多头 logits → 期望值解码 → 原始量纲(箱生活在原始空间); + # 权重 0 列固定为常量(delay=10ms 等),防闭环垃圾回写 result = bin_decode(pred.cpu().numpy(), self.action_dim) + col_w = self._data_cfg.column_weights + if col_w is None: + from action_norm import default_column_weights + col_w = default_column_weights(self.action_dim) + fixed = fixed_zero_columns(col_w, self.action_dim) + m = fixed != 0.0 + result[m] = fixed[m] if not np.all(np.isfinite(result)): result = np.nan_to_num(result, nan=0.0, posinf=0.0, neginf=0.0) return result diff --git a/actpred/test_actpred.py b/actpred/test_actpred.py index 95c5827..a798f96 100644 --- a/actpred/test_actpred.py +++ b/actpred/test_actpred.py @@ -2004,14 +2004,12 @@ class TestFixedRangeNorm(unittest.TestCase): from action_norm import default_column_weights w18 = default_column_weights(18) np.testing.assert_allclose(w18[:10], 1.0) - self.assertAlmostEqual(float(w18[10]), 0.2, places=6) # spk.F 保留梯度 - np.testing.assert_allclose(w18[11:], 0.0) # spk.delay 步级恒值 + 保留位 + np.testing.assert_allclose(w18[10:], 0.0) # spk 两列 + 保留位全 0 # 小 action_dim:全部落在舵机段 → 全 1.0 np.testing.assert_allclose(default_column_weights(4), 1.0) - # 12 列:舵机 1.0 + spk.F 0.2,delay 0 + # 12 列:舵机 1.0,spk 两列 0 w12 = default_column_weights(12) - self.assertAlmostEqual(float(w12[10]), 0.2, places=6) - self.assertEqual(float(w12[11]), 0.0) + np.testing.assert_allclose(w12[10:], 0.0) # 20 列:超出 18 列语义布局的扩展列默认 1.0(新执行器须参与训练) w20 = default_column_weights(20) np.testing.assert_allclose(w20[12:18], 0.0) @@ -2502,6 +2500,19 @@ class TestOneHot(unittest.TestCase): with self.assertRaises(ValueError): validate_config_values(cfg.data, cfg.model, cfg.train) + def test_fixed_zero_columns(self): + """权重 0 列固定常量:spk.F→0、spk.delay→10ms、保留位→0""" + from bins import fixed_zero_columns + from action_norm import default_column_weights + fixed = fixed_zero_columns(default_column_weights(18), 18) + np.testing.assert_allclose(fixed[:10], 0.0) + self.assertEqual(float(fixed[10]), 0.0) + self.assertEqual(float(fixed[11]), 10.0) + np.testing.assert_allclose(fixed[12:], 0.0) + # 全 1 权重 → 无固定列 + np.testing.assert_allclose( + fixed_zero_columns(np.ones(4), 4), 0.0) + def test_onehot_model_output_shape(self): """onehot 模型前向输出 TOTAL_BINS logits""" cfg = Config() diff --git a/actpred/train.py b/actpred/train.py index 73254d5..de18767 100644 --- a/actpred/train.py +++ b/actpred/train.py @@ -9,7 +9,8 @@ from tqdm import tqdm from config import (Config, build_argparser, apply_overrides, validate_config_values, warn_init_from_mismatch) from action_norm import normalize_action, is_range_mode -from bins import encode as bin_encode, decode as bin_decode, START, HEAD_COLS +from bins import (encode as bin_encode, decode as bin_decode, + fixed_zero_columns, START, HEAD_COLS) from model import ActionPredictor from dataset import ActionDataset from action_norm import default_column_weights @@ -126,6 +127,11 @@ def train_one_epoch(model, loader, optimizer, device, max_grad_norm: float = Non if onehot: raw = bin_decode(pred.detach().cpu().numpy(), action_dim) + # 权重 0 列固定为常量,防自预测垃圾进入历史 + fixed = fixed_zero_columns( + weights.cpu().numpy(), action_dim) + m = fixed != 0.0 + raw[m] = fixed[m] abs_action = torch.from_numpy(normalize_action( raw, action_dim, mode=range_mode)).to(device) else: diff --git a/docs/reports/actpred_zscore_baseline_20260815.md b/docs/reports/actpred_zscore_baseline_20260815.md new file mode 100644 index 0000000..f195932 --- /dev/null +++ b/docs/reports/actpred_zscore_baseline_20260815.md @@ -0,0 +1,91 @@ +# actpred 模型 B 方案文档——zscore 等权稳定基线 + +> 文档类型:方案文档 +> 模块:actpred +> 作者:自动生成 +> 日期:2026-08-15 +> 相关实验:`pybot_actpred_dataset_training_eval_20260814.md`(A/B 对照)、 +> `actpred_fixed_range_weighted_experiment_20260814.md`(C/D/E 失败系列)、 +> `actpred_onehot_multihead_dev_plan_20260814.md`(F 系列结构性方案) + +--- + +## 1. 定位 + +模型 B 是六模型闭环评估(A/B/C/D/E/F)中**唯一稳定**的配置:overall_mse 2445、error_growth 0.082,远优于其余五个(6447~968459)。本文档固化其完整方案,作为: + +1. 部署与实机测试的**首选基线** +2. 后续所有实验的**对照基准** +3. 归一化系列失败教训的**参照系** + +## 2. 完整配置(checkpoint 实读) + +来源:`actpred/checkpoints/recorded_run_norm/best_inference.pt`(epoch 26 早停,best val=0.0288) + +### 2.1 数据侧(DataConfig) + +| 项 | 值 | +|---|---| +| 分辨率 | 640×480 | +| action_dim / history_len / frame_len | 18 / 5 / 3 | +| normalize_actions | **True(z-score)** | +| action_stats_path | `action_stats.npy`(272B,逐列 (2,18) mean/std,训练集统计) | +| imu_dim | 0 | +| column_weights | None(**等权**) | + +### 2.2 训练侧(TrainConfig) + +| 项 | 值 | +|---|---| +| 数据 | v1(`data/pybot_record`,804 样本,12/1 episode 划分,seed 42) | +| 训练范式 | **单步 teacher forcing**(rollout_len=1, ss_prob=1.0) | +| 增强 | ColorJitter(--augment) | +| batch / lr / patience | 4 / 1e-3 / 15 | +| predict_delta | False | + +### 2.3 模型侧(ModelConfig) + +SimpleCNN(128) → Transformer(d=128, 4 头, 2 层, pre-LN) → MLP 头。409K 参数。 + +### 2.4 闭环表现(v1 数据,R4) + +- overall_mse **2445.4**,error_growth **0.082**(<1 无发散),oob_rate 28.1% +- 逐 episode:stand 134 / jump 221 / hand 6850·8023 / tilt 60-752(唯一全部 episode 不爆表的模型) + +## 3. 为什么 zscore 稳定(失败系列的对照结论) + +| 方案 | 操作点 | 闭环行为 | 机理 | +|---|---|---|---| +| raw 等权(A) | 原始量纲 | 稳定但 spk.F 崩 | 舵机误差数值占优,spk.F 被忽略 | +| **zscore(B)** | **数据均值 0** | **稳定** | **各列单位方差,归一化误差等量纲;误差收缩** | +| fixed_range(C/D) | [0,1] 低端聚集 | 发散(growth 13.4/3.4) | 低端精度错配:小绝对误差=大相对误差 | +| log_range(E) | log 空间 | 爆炸(growth 157) | 指数反变换在高端放大:0.81→1.0 = 1000→5000Hz | +| onehot(F) | 箱(有界) | 部分稳定(oob=0,MSE 33K) | 有界生效,但 spk 无监督头闭环污染 | + +**结论**:反馈回路需要**误差有界且收缩**的映射。z-score 的零均值单位方差恰好满足;固定范围类方案各自引入一端(低/高)的放大;one-hot 的有界性正确但需配 spk 列固定(见 F2)。 + +## 4. 已知局限 + +1. **依赖 stats 文件**:推理端需随 checkpoint 携带 action_stats.npy(与 fixed_range/onehot 的零状态部署相反) +2. **训练于 v1 数据**:delay 语义混杂(首行 200ms 未分解),spk.delay 权重 0.2——两者已被后续修复(v2 均匀数据 + delay 权重 0) +3. **spk.F 仍弱**:hand 类 episode 8023/6850 的 MSE 主导源是 spk.F(10→1000Hz 大跨度,等权下被均值化) + +## 5. 演进建议:B2 + +模型 B 的两处已知缺陷(v1 数据、delay 权重)都已有修复就绪,B2 = **zscore + v2 数据 + 新权重(spk 列 0)**: + +- 预期:闭环 MSE 进一步下降(delay 垃圾不再进历史) +- 若 B2 稳定,作为部署基线导出 ONNX/HEF +- 若 B2 需要 spk.F 频率跟踪(未来语音反馈需求),再叠加加权或 one-hot 方案 + +## 6. 复现命令 + +```bash +# 模型 B(v1 数据,旧默认权重) +./venv/bin/python train.py --data-dir /opt/micro_servers/data/pybot_record \ + --checkpoint-dir checkpoints/recorded_run_norm --augment --epochs 100 + +# B2(v2 数据,新权重:spk 两列 0) +./venv/bin/python train.py --data-dir /opt/micro_servers/data/pybot_record_v2 \ + --checkpoint-dir checkpoints/recorded_run_norm_v2 --augment --epochs 100 +```