fix(actpred): 修复动作归一化统计量未覆盖 history 导致的 z-score 爆炸

两个问题(本轮归一化编译链路上实测暴露):

1. 统计量只按 targets 计算:targets 中恒定的列 std 落到 1e-8
   地板值,而 history 窗口可能含该列的其他取值(.mt 脚本前几行),
   z-score 爆炸(实测 (90-45)/1e-8=4.5e9)→ act_tokens ±1e9 →
   DFC optimize 报 NegativeSlopeExponentNonFixable(desired shift
   23 > 15 data bits)。pretrain_mt.py 与 dataset.py 的统计量改为
   history∪target 联合计算,修复后校准数据 max|z| = 4.77。

2. action_stats_path 存成含目录的相对路径("./checkpoints/
   action_stats.npy"),推理端(predict/export_deploy/gen_calibration)
   按 checkpoint 目录解析时二次拼接成 checkpoints/./checkpoints,
   导出直接 FileNotFoundError。train.py/pretrain_mt.py 改存裸文件名
   "action_stats.npy"(stats 与 checkpoint 同目录,语义一致)。

新增回归测试 test_stats_cover_history_values(history 独有取值不
得被 1e-8 地板值打爆),全量 77 通过。

归一化重编译全链路通过:token 量级 |img|=0.53 |act|=2.93 健康,
optimize 通过,连通性检查 5/5,HEF 2.02MB 已部署 Pi 待 _npu_test
复验。
This commit is contained in:
qing committed 2026-08-13 06:43:51 +00:00
1 parent 52c0ade3c4
commit 316758d80e
4 files changed
+41 -4

No files matched your search

+3 -1
View File
@@ -76,7 +76,9 @@ class ActionDataset(Dataset):
raise ValueError(
f"数据集为空(0 样本),无法计算归一化统计量。"
f"请检查 data_dir={data_dir} 中的 episode 数据。")
all_actions = np.stack([s[2] for s in self.samples])
all_actions = np.stack(
[np.concatenate([s[1], s[2][None]]) for s in self.samples]
).reshape(-1, self.action_dim)
self._action_mean = all_actions.mean(axis=0)
self._action_std = all_actions.std(axis=0) + 1e-8
+12 -2
View File
@@ -217,13 +217,23 @@ def main():
f"{cfg.data.action_stats_path}")
stats = np.load(cfg.data.action_stats_path)
else:
all_actions = np.stack([s[1] for s in train_ds.samples])
# 统计量必须覆盖 history 与 target 并集:只按 targets 算时,
# targets 中恒定的列 std 落到 1e-8 地板值,而 history 窗口
# 可能含该列的其他取值(如 .mt 脚本前几行),z-score 会爆炸
# (实测 (90-45)/1e-8=4.5e9),后续 act_tokens ±1e9 直接
# 打爆 DFC 量化(NegativeSlopeExponentNonFixable)
all_actions = np.stack(
[np.concatenate([s[0], s[1][None]]) for s in train_ds.samples]
).reshape(-1, cfg.data.action_dim)
stats = np.stack([all_actions.mean(axis=0),
all_actions.std(axis=0) + 1e-8])
stats_path = os.path.join(cfg.train.checkpoint_dir,
"action_stats.npy")
np.save(stats_path, stats)
cfg.data.action_stats_path = stats_path
# 只存裸文件名:推理端按 checkpoint 目录解析相对路径
# (predict.py/export_deploy.py/gen_calibration.py 同语义),
# 存 "./checkpoints/..." 会被二次拼接成 checkpoints/./checkpoints
cfg.data.action_stats_path = "action_stats.npy"
print(f"action_stats 已保存: {stats_path}(训练集 targets 统计)")
if stats.shape != (2, cfg.data.action_dim):
raise ValueError(
+24
View File
@@ -261,6 +261,30 @@ class TestActionDataset(unittest.TestCase):
self.assertIn("WARNING", buf.getvalue())
self.assertEqual(len(ds), 8 - 3) # 按较少者(8 帧图像)截断
def test_stats_cover_history_values(self):
"""归一化统计量必须覆盖 history 独有取值,否则 z-score 爆炸
col0 仅在 history(首行)出现 100,targets 全为 0:只按 targets
算统计时 std 落到 1e-8 地板值,z=(100-0)/1e-8=1e10。
"""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir)
ep_dir = base / "episode_0001"
images_dir = ep_dir / "images"
images_dir.mkdir(parents=True, exist_ok=True)
for i in range(7):
Image.fromarray(np.random.randint(0, 255, (16, 16, 3), dtype=np.uint8)
).save(images_dir / f"{i:04d}.jpg")
actions = np.zeros((7, 4), dtype=np.float32)
actions[0, 0] = 100.0
np.save(ep_dir / "actions.npy", actions)
ds = ActionDataset(tmpdir, history_len=5, action_dim=4,
normalize_actions=True)
self.assertGreater(ds._action_std[0], 1.0) # 而非 1e-8 地板值
z_max = (np.abs(actions[:, 0] - ds._action_mean[0])
/ ds._action_std[0]).max()
self.assertLess(z_max, 10.0)
class TestConfig(unittest.TestCase):
"""测试配置和 CLI 参数"""
+2 -1
View File
@@ -89,7 +89,8 @@ def main():
stats_path = os.path.join(cfg.train.checkpoint_dir, "action_stats.npy")
np.save(stats_path, train_ds.get_action_stats())
print(f"Action stats saved to: {stats_path}")
cfg.data.action_stats_path = stats_path
# 只存裸文件名:推理端按 checkpoint 目录解析相对路径
cfg.data.action_stats_path = "action_stats.npy"
val_ds = ActionDataset(
cfg.data.data_dir, cfg.data.history_len, cfg.data.action_dim,
cfg.data.image_width, cfg.data.image_height, cfg.data.normalize_actions,