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。
This commit is contained in:
qing committed 2026-08-14 18:26:03 +08:00
1 parent adfa61aa3c
commit 85bf68f505
19 files changed
+874 -167

No files matched your search

+11 -2
View File
@@ -141,7 +141,8 @@ def _load_predictor(checkpoint):
"""
import os
try:
if checkpoint.endswith(".hef"):
lower = checkpoint.lower()
if lower.endswith(".hef"):
from actpred.hailo_infer import HailoPredictor as _P
hef_dir = os.path.dirname(os.path.abspath(checkpoint))
@@ -160,6 +161,7 @@ def _load_predictor(checkpoint):
# predict_delta:从同目录 checkpoint 读取训练配置
predict_delta = False
normalize_mode = "zscore"
for name in ("pretrain_best_inference.pt",
"best_inference.pt"):
ckpt_path = os.path.join(hef_dir, name)
@@ -171,9 +173,15 @@ def _load_predictor(checkpoint):
cfg = ck.get("config", {})
predict_delta = cfg.get("train", {}).get(
"predict_delta", False)
normalize_mode = cfg.get("data", {}).get(
"normalize_mode", "zscore")
if predict_delta:
logging.info(
"predict_delta=True from %s", ckpt_path)
if normalize_mode != "zscore":
logging.info(
"normalize_mode=%s from %s",
normalize_mode, ckpt_path)
except Exception:
pass
break
@@ -181,7 +189,8 @@ def _load_predictor(checkpoint):
try:
return _P(checkpoint, stats_path=stats_path,
predict_delta=predict_delta,
pred_token=pred_token)
pred_token=pred_token,
normalize_mode=normalize_mode)
except Exception:
logging.exception("加载 HEF 推理器失败: %s", checkpoint)
return None
+2 -7
View File
@@ -14,6 +14,8 @@ import logging
import time
from collections import deque
import numpy as np
from . import plugin
from .hardvars import (AliasVar, CameraVar, I2CVar, MPU6050Var, ServoVar,
SpeakerVar)
@@ -238,7 +240,6 @@ class RobotStruct:
(会阻塞 ~1/framerate 秒,把 hz 锁死在相机帧率之下)。dry_run
或队列空时回退 _grab_frame 合成帧。
"""
import numpy as np
camera_var = self._find_camera()
if camera_var is None and not self.dry_run:
raise ValueError("闭环需要 @Camera 块(或使用 --dry-run 生成合成帧)")
@@ -357,7 +358,6 @@ class RobotStruct:
帧源与主循环一致(_loop_recent_frames),避免冷启动与运行期帧源
不一致。history 用零向量填满是保守初值,避免随机姿态跳变损坏舵机。
"""
import numpy as np
frames.extend(self._loop_recent_frames(camera_var, frames.maxlen))
for _ in range(history.maxlen):
history.append(np.zeros(action_dim, dtype=np.float32))
@@ -368,7 +368,6 @@ class RobotStruct:
SpeakerVar.play 强制要求 delay 键;闭环已剔除 delay 列(避免发声
阻塞卡死节拍),此处为其补 delay=0 —— 发声但不阻塞,节拍由 hz 掌控。
"""
import numpy as np
if not np.all(np.isfinite(act)):
log.warning("action vector contains NaN/Inf, skipping step")
return
@@ -442,7 +441,6 @@ class RobotStruct:
"""
if not imu_vars:
return None
import numpy as np
nk = len(IMU_KEYS)
out = np.zeros(nk * len(imu_vars), dtype=np.float32)
for k, var in enumerate(imu_vars):
@@ -458,7 +456,6 @@ class RobotStruct:
@staticmethod
def _pad_imu(vec, imu_dim):
"""拼接姿态向量 → pad/truncate 到 predictor 的 imu_dim(None → 全零)"""
import numpy as np
out = np.zeros(imu_dim, dtype=np.float32)
if vec is not None:
src = np.asarray(vec, dtype=np.float32).ravel()
@@ -468,7 +465,6 @@ class RobotStruct:
def _build_action_vec(self, vals, action_dim):
"""从值列表构建动作向量,pad/truncate 到 action_dim"""
import numpy as np
vec = np.zeros(action_dim, dtype=np.float32)
n = min(len(vals), action_dim)
vec[:n] = vals[:n]
@@ -476,7 +472,6 @@ class RobotStruct:
def _grab_frame(self, camera_var):
"""从摄像头同步抓一帧;dry_run 或无帧时返回随机噪声帧"""
import numpy as np
if camera_var is not None:
frame = camera_var.camera.capture()
if frame is not None: