fix(actpred): 修复 Hailo HEF 编译链路的输入断连与封装问题
- hailo_infer: 帧输入按 input_layerN 排序绑定(vstream 报告顺序实测 为 1,5,4,3,2,按报告顺序会 frame1/frame2 错位),新增 --pred-token - gen_calibration: pred_token 校准改为学习值 ±30% 抖动,修复常量 校准量化退化(scale=18571.7,任何输入都被量化成 0) - export_deploy: 新增 --pred-token-out 保存学习值供推理端喂入 - dfc_compile: parser/optimize 后自动检查输入层连通性,断开即报错 - 新增 _npu_test.py(NPU 4 角敏感性复验)与 _onnx_validate.py (编译前 ONNX 数值门槛),test_actpred 补乱序帧序用例
This commit is contained in:
1 parent
2068319c3a
commit
14964b9574
13 files changed
+487
-958
No files matched your search
@@ -22,3 +22,6 @@ waf/certs/
|
||||
# 本地 IDE / 工具
|
||||
.qoder/
|
||||
.vscode/
|
||||
|
||||
# 运行时日志(HailoRT 等)
|
||||
hailort.log
|
||||
@@ -20,6 +20,7 @@ checkpoints/
|
||||
# DFC logs
|
||||
acceleras.log
|
||||
hailo_sdk.*.log
|
||||
hailort.log
|
||||
|
||||
# Dataset (too large for git)
|
||||
data/
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""actpred HEF NPU 输入敏感性复验脚本(重编译后的验收门槛)
|
||||
|
||||
用法: venv/bin/python _npu_test.py
|
||||
|
||||
4 角测试(真实/全零帧 × 真实/全零历史)+ 与 PyTorch 参考对比。
|
||||
HEF 尺寸/布局从 vstream info 自动推导,checkpoint 用
|
||||
checkpoints/pretrain_best_inference.pt。
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
sys.path.insert(0, "/opt/blackbird/actpred")
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from PIL import Image
|
||||
from hailo_platform import HEF
|
||||
|
||||
from config import parse_checkpoint_config
|
||||
from model import ActionPredictor
|
||||
from hailo_infer import HailoPredictor
|
||||
|
||||
HEF_PATH = "checkpoints/actpred_pretrain.hef"
|
||||
CKPT = "checkpoints/pretrain_best_inference.pt"
|
||||
IMG_DIR = "data/episode_0000/images"
|
||||
|
||||
hef = HEF(HEF_PATH)
|
||||
print("=== HEF vstream infos ===")
|
||||
for i in hef.get_input_vstream_infos():
|
||||
q = i.quant_info
|
||||
print(f"IN {i.name:16s} {tuple(i.shape)} {i.format.type} "
|
||||
f"zp={q.qp_zp} scale={q.qp_scale:.6g}")
|
||||
for o in hef.get_output_vstream_infos():
|
||||
q = o.quant_info
|
||||
print(f"OUT {o.name:16s} {tuple(o.shape)} {o.format.type} "
|
||||
f"zp={q.qp_zp} scale={q.qp_scale:.6g}")
|
||||
|
||||
imgs_real = [np.array(Image.open(f"{IMG_DIR}/000{i}.jpg").convert("RGB"))
|
||||
for i in range(3)]
|
||||
imgs_zero = [np.zeros_like(imgs_real[0]) for _ in range(3)]
|
||||
|
||||
hist_real = np.array([
|
||||
[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],
|
||||
], dtype=np.float32)
|
||||
hist_zero = np.zeros((5, 18), dtype=np.float32)
|
||||
|
||||
# PyTorch 参考(checkpoint 自带 config)
|
||||
ck = torch.load(CKPT, map_location="cpu", weights_only=True)
|
||||
cfg = parse_checkpoint_config(ck["config"])
|
||||
model = ActionPredictor(cfg.model, cfg.data, pretrained=False)
|
||||
model.load_state_dict(ck["model_state_dict"])
|
||||
model.eval()
|
||||
pred_learned = model.pred_token.data.unsqueeze(1).numpy() # (1,1,1,d)
|
||||
|
||||
from torchvision import transforms
|
||||
tf = transforms.Compose([
|
||||
transforms.Resize((cfg.data.image_height, cfg.data.image_width)),
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
|
||||
])
|
||||
|
||||
|
||||
def torch_pred(imgs, hist, use_learned_pred=True):
|
||||
x = torch.stack([tf(Image.fromarray(im)) for im in imgs]).unsqueeze(0)
|
||||
h = torch.tensor(hist).unsqueeze(0)
|
||||
if use_learned_pred:
|
||||
with torch.no_grad():
|
||||
return model(x, h).squeeze(0).numpy()
|
||||
saved = model.pred_token.data.clone()
|
||||
model.pred_token.data.zero_()
|
||||
with torch.no_grad():
|
||||
out = model(x, h).squeeze(0).numpy()
|
||||
model.pred_token.data.copy_(saved)
|
||||
return out
|
||||
|
||||
|
||||
with HailoPredictor(HEF_PATH) as p:
|
||||
print(f"\ndims: frame_len={p.frame_len} img={p.image_width}x{p.image_height} "
|
||||
f"history={p.history_len} action_dim={p.action_dim}")
|
||||
t0 = time.perf_counter()
|
||||
a = p.predict(imgs_real, hist_real, pred_token=pred_learned)
|
||||
b = p.predict(imgs_zero, hist_real, pred_token=pred_learned)
|
||||
c = p.predict(imgs_real, hist_zero, pred_token=pred_learned)
|
||||
d = p.predict(imgs_zero, hist_zero, pred_token=pred_learned)
|
||||
t1 = time.perf_counter()
|
||||
print(f"4x predict: {(t1 - t0) / 4 * 1000:.1f} ms/次")
|
||||
for name, r in [("real_img+real_hist", a), ("zero_img+real_hist", b),
|
||||
("real_img+zero_hist", c), ("zero_img+zero_hist", d)]:
|
||||
print(f"{name:20s} max_abs={np.abs(r).max():.4f} out={np.round(r[:6], 3)}...")
|
||||
|
||||
img_sens = np.abs(a - b).max()
|
||||
hist_sens = np.abs(a - c).max()
|
||||
print(f"\nimg sensitivity = {img_sens:.4f}(>0.1 视为输入连通)")
|
||||
print(f"hist sensitivity = {hist_sens:.4f}(>0.1 视为输入连通)")
|
||||
|
||||
ref = torch_pred(imgs_real, hist_real, True)
|
||||
ref_zero_pred = torch_pred(imgs_real, hist_real, False)
|
||||
print(f"torch learned_pred : {np.round(ref[:6], 3)}...")
|
||||
print(f"torch zero_pred : {np.round(ref_zero_pred[:6], 3)}...")
|
||||
print(f"HEF vs torch(learned) max_diff={np.abs(a - ref).max():.4f}")
|
||||
|
||||
ok = img_sens > 0.1 and hist_sens > 0.1
|
||||
print("\nPASS: HEF 输入连通,可继续与 torch 对比量化误差"
|
||||
if ok else "\nFAIL: HEF 输出对输入不敏感,编译仍有问题")
|
||||
@@ -0,0 +1,92 @@
|
||||
"""部署版 ONNX 数值验证:输入敏感性 + 与 PyTorch 一致性(重编译前门槛)
|
||||
|
||||
用法: venv/bin/python _onnx_validate.py
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/blackbird/actpred")
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torchvision import transforms
|
||||
import onnxruntime as ort
|
||||
|
||||
from config import parse_checkpoint_config
|
||||
from model import ActionPredictor
|
||||
|
||||
CKPT = "checkpoints/pretrain_best_inference.pt"
|
||||
ONNX = "checkpoints/actpred_deploy.onnx"
|
||||
IMG_DIR = "data/episode_0000/images"
|
||||
|
||||
ck = torch.load(CKPT, map_location="cpu", weights_only=True)
|
||||
cfg = parse_checkpoint_config(ck["config"])
|
||||
model = ActionPredictor(cfg.model, cfg.data, pretrained=False)
|
||||
model.load_state_dict(ck["model_state_dict"])
|
||||
model.eval()
|
||||
|
||||
sess = ort.InferenceSession(ONNX)
|
||||
in_names = [i.name for i in sess.get_inputs()]
|
||||
print("ONNX inputs:", in_names)
|
||||
|
||||
h, w = cfg.data.image_height, cfg.data.image_width
|
||||
tf = transforms.Compose([
|
||||
transforms.Resize((h, w)),
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
|
||||
])
|
||||
|
||||
imgs_real = [np.array(Image.open(f"{IMG_DIR}/000{i}.jpg").convert("RGB"))
|
||||
for i in range(3)]
|
||||
imgs_zero = [np.zeros_like(imgs_real[0]) for _ in range(3)]
|
||||
|
||||
hist_real = np.array([
|
||||
[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],
|
||||
], dtype=np.float32)
|
||||
hist_zero = np.zeros((5, 18), dtype=np.float32)
|
||||
|
||||
pred_learned = model.pred_token.data.unsqueeze(1).numpy() # (1,1,1,128)
|
||||
pred_none = np.zeros((1, 1, 1, 128), dtype=np.float32)
|
||||
|
||||
|
||||
def frames_nhwc(imgs):
|
||||
"""列表帧 → (1,H,W,3) 归一化 float32 NHWC × frame_len"""
|
||||
chw = np.stack([tf(Image.fromarray(im)).numpy() for im in imgs])
|
||||
return [chw[i].transpose(1, 2, 0)[None] for i in range(len(imgs))]
|
||||
|
||||
|
||||
def run_onnx(imgs, hist, pred):
|
||||
feeds = {f"frame{i}": f for i, f in enumerate(frames_nhwc(imgs))}
|
||||
feeds["action_history"] = hist[None, None]
|
||||
feeds["pred_token"] = pred
|
||||
return sess.run(None, feeds)[0].reshape(-1)
|
||||
|
||||
|
||||
def run_torch(imgs, hist):
|
||||
x = torch.stack([tf(Image.fromarray(im)) for im in imgs]).unsqueeze(0)
|
||||
hh = torch.tensor(hist).unsqueeze(0)
|
||||
with torch.no_grad():
|
||||
return model(x, hh).squeeze(0).numpy()
|
||||
|
||||
|
||||
a = run_onnx(imgs_real, hist_real, pred_learned)
|
||||
b = run_onnx(imgs_zero, hist_real, pred_learned)
|
||||
c = run_onnx(imgs_real, hist_zero, pred_learned)
|
||||
d = run_onnx(imgs_zero, hist_zero, pred_learned)
|
||||
p0 = run_onnx(imgs_real, hist_real, pred_none)
|
||||
|
||||
print(f"onnx real_img+real_hist: {np.round(a[:6], 3)}...")
|
||||
print(f"onnx pred_token=0 diff : max={np.abs(a - p0).max():.4f}")
|
||||
print(f"img sensitivity (a-b): max={np.abs(a - b).max():.4f}")
|
||||
print(f"hist sensitivity (a-c): max={np.abs(a - c).max():.4f}")
|
||||
|
||||
ref = run_torch(imgs_real, hist_real)
|
||||
print(f"torch reference : {np.round(ref[:6], 3)}...")
|
||||
print(f"onnx vs torch max_diff : {np.abs(a - ref).max():.6e}")
|
||||
|
||||
ok = (np.abs(a - b).max() > 1e-4 and np.abs(a - c).max() > 1e-4
|
||||
and np.abs(a - ref).max() < 1e-4)
|
||||
print("PASS" if ok else "FAIL")
|
||||
@@ -57,6 +57,44 @@ def har_input_layer_names(har_path: str) -> list:
|
||||
return [name for name, _ in layers]
|
||||
|
||||
|
||||
def check_input_connectivity(har_path: str, input_layers: list):
|
||||
"""检查 HAR 中输入层是否都有下游边
|
||||
|
||||
输入层被优化断开时编译照常成功,但 HEF 输出恒定值且无任何报错
|
||||
(2026-08-13 实测踩坑),必须在编译前拦截。.edges 文件缺失或格式
|
||||
无法识别时仅告警不阻断。
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with tarfile.open(har_path) as tf:
|
||||
tf.extractall(tmp)
|
||||
edge_files = [f for f in os.listdir(tmp) if f.endswith(".edges")]
|
||||
if not edge_files:
|
||||
print(f"WARNING: {har_path} 无 .edges 文件,跳过输入连通性检查")
|
||||
return
|
||||
with open(os.path.join(tmp, edge_files[0])) as f:
|
||||
edges = json.load(f)
|
||||
|
||||
if not isinstance(edges, list):
|
||||
print(f"WARNING: .edges 不是列表({type(edges).__name__}),"
|
||||
f"跳过连通性检查")
|
||||
return
|
||||
adjacency = set()
|
||||
for e in edges:
|
||||
if isinstance(e, dict) and "from" in e:
|
||||
adjacency.add(e["from"])
|
||||
elif isinstance(e, (list, tuple)) and len(e) == 2:
|
||||
adjacency.add(e[0])
|
||||
else:
|
||||
print(f"WARNING: 无法识别 .edges 条目格式: {e!r},跳过连通性检查")
|
||||
return
|
||||
disconnected = [n for n in input_layers if n not in adjacency]
|
||||
if disconnected:
|
||||
sys.exit(
|
||||
f"错误: {har_path} 中输入层无下游边(输入被优化断开,"
|
||||
f"编译出的 HEF 将输出恒定值): {disconnected}")
|
||||
print(f"输入连通性检查通过: {len(input_layers)} 个输入层均有下游边")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compile actpred deploy ONNX to HEF")
|
||||
parser.add_argument("--onnx", default="checkpoints/actpred_deploy.onnx")
|
||||
@@ -93,11 +131,13 @@ def main():
|
||||
names = har_input_layer_names(har_raw)
|
||||
if len(names) != len(START_NODES):
|
||||
sys.exit(f"错误: HAR 输入层数 {len(names)} != {len(START_NODES)},解析可能被截断")
|
||||
check_input_connectivity(har_raw, names)
|
||||
|
||||
print("[2/3] optimize: 量化校准")
|
||||
hailo("optimize", har_raw, "--hw-arch", args.hw_arch,
|
||||
"--calib-set-path", os.path.abspath(args.calib_npz),
|
||||
"--output-har-path", har_opt, venv_bin=venv_bin)
|
||||
check_input_connectivity(har_opt, har_input_layer_names(har_opt))
|
||||
|
||||
print("[3/3] compiler: HAR → HEF")
|
||||
hailo("compiler", har_opt, "--hw-arch", args.hw_arch,
|
||||
|
||||
@@ -128,6 +128,9 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Export Hailo-friendly ONNX")
|
||||
parser.add_argument("--model", required=True, help="checkpoint path")
|
||||
parser.add_argument("--output", default="checkpoints/actpred_deploy.onnx")
|
||||
parser.add_argument("--pred-token-out", default=None,
|
||||
help="保存学习到的 pred_token 到 .npy(HEF 推理端喂入用,"
|
||||
"配合抖动校准避免常量量化退化)")
|
||||
args = parser.parse_args()
|
||||
|
||||
checkpoint = torch.load(args.model, map_location="cpu", weights_only=True)
|
||||
@@ -156,6 +159,11 @@ def main():
|
||||
)
|
||||
print(f"部署版 ONNX 已导出: {args.output}({f} 个帧输入 (1,{h},{w},3) NHWC + action_history + pred_token)")
|
||||
|
||||
if args.pred_token_out:
|
||||
import numpy as np
|
||||
np.save(args.pred_token_out, model.pred_token.data.unsqueeze(1).numpy())
|
||||
print(f"pred_token 已保存: {args.pred_token_out} (1,1,1,{model.pred_token.shape[-1]})")
|
||||
|
||||
stacked = torch.stack([fr.permute(0, 3, 1, 2) for fr in dummy_frames], dim=1)
|
||||
with torch.no_grad():
|
||||
orig_out = model(stacked, dummy_history.squeeze(1))
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
--deploy 模式(data/calibration_deploy.npz):为 export_deploy.py 导出
|
||||
的部署版 ONNX 生成校准集,键名匹配 HN 层名 actpred_deploy/input_layerN,
|
||||
形状匹配 HEF 输入:frame0~2 (N,H,W,3) 已归一化 float32、
|
||||
action_history (N,1,H_len,D)、pred_token (N,1,1,d) 常量。
|
||||
action_history (N,1,H_len,D)、pred_token (N,1,1,d) 学习值 ± 抖动
|
||||
(常量校准会使量化范围退化,见 --pred-jitter)。
|
||||
图像分辨率与 --model checkpoint 的配置一致。
|
||||
|
||||
数据源:
|
||||
@@ -81,9 +82,15 @@ def build_deploy_set(args):
|
||||
cfg, pretrained=False)
|
||||
model.load_state_dict(ckpt["model_state_dict"])
|
||||
d = model.pred_token.shape[-1]
|
||||
pred = np.broadcast_to(
|
||||
model.pred_token.data.unsqueeze(1).numpy(),
|
||||
(args.samples, 1, 1, d)).astype(np.float32).copy()
|
||||
v = model.pred_token.data.unsqueeze(1).numpy() # (1, 1, d) 学习到的常量
|
||||
# 常量校准会使量化范围退化(上次实测 scale=18571.7,任何输入都被
|
||||
# 量化成 0);按学习值 ±相对抖动生成校准样本,运行时喂学习值即可
|
||||
# 无损还原(量化误差 ~1e-4)
|
||||
jitter = v * rng.uniform(-args.pred_jitter, args.pred_jitter,
|
||||
size=(args.samples, 1, 1, d)).astype(np.float32)
|
||||
jitter = jitter + rng.normal(0.0, args.pred_jitter_abs,
|
||||
size=jitter.shape).astype(np.float32)
|
||||
pred = np.broadcast_to(v, (args.samples, 1, 1, d)).astype(np.float32) + jitter
|
||||
|
||||
np.savez_compressed(
|
||||
args.output,
|
||||
@@ -91,7 +98,8 @@ def build_deploy_set(args):
|
||||
for i, arr in enumerate([*frames, history, pred])})
|
||||
print(f"部署校准集已保存: {args.output}")
|
||||
print(f" frame0~2: {frames[0].shape}(分辨率 {w}x{h},与 checkpoint 一致)")
|
||||
print(f" action_history: {history.shape} pred_token: {pred.shape}")
|
||||
print(f" action_history: {history.shape} pred_token: {pred.shape}"
|
||||
f"(学习值 ±{args.pred_jitter:.0%} 抖动)")
|
||||
|
||||
|
||||
def main():
|
||||
@@ -104,6 +112,10 @@ def main():
|
||||
parser.add_argument("--output", default=None)
|
||||
parser.add_argument("--samples", type=int, default=32)
|
||||
parser.add_argument("--seed", type=int, default=42)
|
||||
parser.add_argument("--pred-jitter", type=float, default=0.3,
|
||||
help="pred_token 校准相对抖动幅度(默认 ±30%)")
|
||||
parser.add_argument("--pred-jitter-abs", type=float, default=0.005,
|
||||
help="pred_token 校准绝对抖动标准差(近零元素保底)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.deploy:
|
||||
|
||||
+136
-40
@@ -3,30 +3,40 @@
|
||||
接口对齐 predict.ActionPredictorInference:predict(images, action_history)。
|
||||
|
||||
HEF 由 actpred ONNX 经 Hailo DFC 编译生成(量化校准后)。本封装不硬编码
|
||||
vstream 名称/形状/量化参数,全部从 HEF 的 input/output vstream info 动态推导:
|
||||
vstream 名称/形状/量化参数,全部从 HEF 的 input/output vstream info 动态推导,
|
||||
支持两种布局:
|
||||
|
||||
图像输入: (1, F, 3, H, W) → NHWC uint8 (1, F, H, W, 3)
|
||||
或 (1, H, W, F*3) → 帧沿通道拼接
|
||||
动作输入: (1, H, D) float32,uint8 边缘时按 quant_info 量化
|
||||
动作输出: (1, D),uint8 边缘时按 quant_info 反量化
|
||||
新版 5 输入(export_deploy.py + dfc_compile.py 产物):
|
||||
frame0~2 各 (H,W,3) NHWC uint8(ImageNet 归一化校准,负零偏自动识别)
|
||||
+ action_history (1,H_len,D) + pred_token (1,1,d)
|
||||
旧版单图像输入:
|
||||
(1, F, 3, H, W) → NHWC uint8 (1, F, H, W, 3)
|
||||
或 (1, H, W, F*3) → 帧沿通道拼接;action_history (1, H, D)
|
||||
|
||||
uint8 边缘按 quant_info 量化/反量化;pred_token 输入默认喂零,
|
||||
新编译 HEF(抖动校准,见 gen_calibration.py --pred-jitter)应喂
|
||||
checkpoint 的学习值(--pred-token npy 或 predict(pred_token=...)),
|
||||
否则该 token 语义与训练时不一致。
|
||||
|
||||
背景与踩坑记录见
|
||||
docs/reports/actpred_hailo_hat2_inference_research_20260813.md:
|
||||
- hailort 5.x 对 Hailo-10 仅支持新 API(create_infer_model),
|
||||
旧版 VDevice.configure(hef) 报 HAILO_NOT_IMPLEMENTED;
|
||||
- 绑定缓冲区 dtype/大小必须匹配 HEF 边缘格式(量化 uint8);
|
||||
- 图像输入按 Hailo 惯例为 uint8 原始像素(ImageNet 归一化由
|
||||
DFC 编译时折叠进网络)。
|
||||
- 绑定缓冲区 dtype/大小必须匹配 HEF 边缘格式(量化 uint8)。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
||||
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
||||
|
||||
# 与 predict.parse_action_history 相同逻辑;此处内联以保持
|
||||
# 本模块零 torch 依赖(HAT 部署环境只装 hailo_platform + numpy + PIL)
|
||||
|
||||
@@ -43,9 +53,17 @@ def parse_action_history(s: str, action_dim: int) -> np.ndarray:
|
||||
return np.array(history, dtype=np.float32)
|
||||
|
||||
|
||||
def _input_layer_index(name: str) -> int:
|
||||
m = re.search(r"input_layer(\d+)$", name)
|
||||
return int(m.group(1)) if m else 0
|
||||
|
||||
|
||||
class HailoPredictor:
|
||||
"""HEF 动作预测推理器(Hailo NPU 后端)
|
||||
|
||||
支持新版 5 输入 HEF(逐帧输入 + pred_token)与旧版单图像输入 HEF,
|
||||
布局与量化参数从 vstream info 自动推导。
|
||||
|
||||
用法:
|
||||
with HailoPredictor("actpred.hef") as predictor:
|
||||
action = predictor.predict(images, action_history)
|
||||
@@ -68,9 +86,9 @@ class HailoPredictor:
|
||||
if len(self._out_infos) != 1:
|
||||
raise ValueError(
|
||||
f"HEF 应只有一个输出 vstream,实际 {len(self._out_infos)}")
|
||||
self._image_info = self._find_image_input()
|
||||
self._history_info = self._find_history_input()
|
||||
self._out_info = self._out_infos[0]
|
||||
self._frame_infos, self._history_info, self._pred_info = \
|
||||
self._classify_inputs()
|
||||
self._derive_dims()
|
||||
|
||||
self._action_mean = None
|
||||
@@ -89,27 +107,49 @@ class HailoPredictor:
|
||||
self._stack.close() # 初始化失败也释放设备,避免 fd 泄漏
|
||||
raise
|
||||
|
||||
# ---------------- 形状推导 ----------------
|
||||
# ---------------- 输入分类与形状推导 ----------------
|
||||
|
||||
def _find_image_input(self):
|
||||
cands = [i for i in self._in_infos if len(i.shape) in (4, 5)]
|
||||
if len(cands) != 1:
|
||||
raise ValueError(
|
||||
"HEF 输入中无法唯一识别图像 vstream(期望一个 4/5 维输入),"
|
||||
f"实际: {[(i.name, tuple(i.shape)) for i in self._in_infos]}")
|
||||
return cands[0]
|
||||
def _classify_inputs(self):
|
||||
"""将 HEF 输入分为 帧列表 / 动作历史 / pred_token(可能为 None)
|
||||
|
||||
def _find_history_input(self):
|
||||
cands = [i for i in self._in_infos
|
||||
if len(i.shape) == 3 and i is not self._image_info]
|
||||
if len(cands) != 1:
|
||||
多帧输入的绑定顺序按 input_layerN 数字后缀排序,而非 vstream
|
||||
报告顺序:hailort 5.3.0 实测报告顺序被打乱(1,5,4,3,2),
|
||||
按报告顺序绑定会导致 frame1/frame2 错位。
|
||||
"""
|
||||
ins = list(self._in_infos)
|
||||
out_dim = tuple(self._out_info.shape)[-1]
|
||||
# 新版:每帧独立 (H,W,3) 输入
|
||||
frames = [i for i in ins if len(i.shape) == 3
|
||||
and i.shape[2] == 3 and i.shape[0] > 3]
|
||||
if frames:
|
||||
frames.sort(key=lambda i: _input_layer_index(i.name))
|
||||
else:
|
||||
# 旧版:单一 4/5 维图像输入
|
||||
frames = [i for i in ins if len(i.shape) in (4, 5)]
|
||||
rest = [i for i in ins if i not in frames]
|
||||
history, pred = None, None
|
||||
for i in rest:
|
||||
s = tuple(i.shape)
|
||||
if len(s) == 3 and s[-1] == out_dim:
|
||||
history = i
|
||||
elif len(s) == 3 and s[1] == 1:
|
||||
pred = i
|
||||
else:
|
||||
raise ValueError(
|
||||
"HEF 输入中无法唯一识别动作历史 vstream(期望一个 3 维输入),"
|
||||
f"实际: {[(i.name, tuple(i.shape)) for i in self._in_infos]}")
|
||||
return cands[0]
|
||||
f"无法识别 HEF 输入 {i.name} 形状 {s}(期望动作历史 "
|
||||
f"(1,H_len,D) 或 pred_token (1,1,d))")
|
||||
if not frames:
|
||||
raise ValueError("HEF 输入中未找到图像输入")
|
||||
if history is None:
|
||||
raise ValueError(
|
||||
f"HEF 输入中未找到动作历史输入(输出维度 {out_dim}),"
|
||||
f"实际: {[(i.name, tuple(i.shape)) for i in ins]}")
|
||||
return frames, history, pred
|
||||
|
||||
def _derive_dims(self):
|
||||
shape = tuple(self._image_info.shape)
|
||||
# 帧输入布局
|
||||
if len(self._frame_infos) == 1 and len(self._frame_infos[0].shape) in (4, 5):
|
||||
shape = tuple(self._frame_infos[0].shape)
|
||||
if len(shape) == 5:
|
||||
_, self.frame_len, c, self.image_height, self.image_width = shape
|
||||
if c != 3:
|
||||
@@ -123,6 +163,21 @@ class HailoPredictor:
|
||||
f"实际形状 {shape}")
|
||||
self.frame_len = c // 3
|
||||
self._image_layout = "nhwc_fused"
|
||||
else:
|
||||
# 新版:每帧独立 (H,W,3) 输入,形状须一致
|
||||
shapes = [tuple(i.shape) for i in self._frame_infos]
|
||||
if any(len(s) != 3 or s[2] != 3
|
||||
or s[0] != shapes[0][0] or s[1] != shapes[0][1]
|
||||
for s in shapes):
|
||||
raise ValueError(f"帧输入形状不一致: {shapes}")
|
||||
self.image_height, self.image_width, _ = shapes[0]
|
||||
self.frame_len = len(self._frame_infos)
|
||||
self._image_layout = "nhwc_frames"
|
||||
self._frame_quant = self._frame_infos[0].quant_info
|
||||
# 帧量化零偏为负 ⇒ 校准数据是 ImageNet 归一化后的浮点,
|
||||
# 推理端需先归一化再量化;零偏为 0 则为原始像素直通
|
||||
self._normalize_frames = \
|
||||
self._frame_quant.qp_zp * self._frame_quant.qp_scale < 0
|
||||
|
||||
h_shape = tuple(self._history_info.shape)
|
||||
self.history_len, self.action_dim = h_shape[1], h_shape[2]
|
||||
@@ -147,10 +202,12 @@ class HailoPredictor:
|
||||
|
||||
# ---------------- 推理 ----------------
|
||||
|
||||
def predict(self, images, action_history: np.ndarray) -> np.ndarray:
|
||||
def predict(self, images, action_history: np.ndarray,
|
||||
pred_token: np.ndarray = None) -> np.ndarray:
|
||||
"""images: 长度为 frame_len 的 RGB 图像列表(旧→新,末帧为当前帧)
|
||||
|
||||
返回 (action_dim,) float32 动作向量。
|
||||
返回 (action_dim,) float32 动作向量。pred_token 仅新版 5 输入 HEF
|
||||
需要;默认喂零,抖动校准的新 HEF 建议喂 checkpoint 学习值。
|
||||
"""
|
||||
if not isinstance(images, (list, tuple)):
|
||||
images = [images]
|
||||
@@ -170,9 +227,8 @@ class HailoPredictor:
|
||||
from hailo_platform import FormatType
|
||||
bindings = self._configured.create_bindings()
|
||||
|
||||
frames = np.stack([self._preprocess(img) for img in images])
|
||||
bindings.input(self._image_info.name).set_buffer(
|
||||
self._image_buffer(frames))
|
||||
frame_float = np.stack([self._frame_float(img) for img in images])
|
||||
self._bind_frames(bindings, frame_float)
|
||||
|
||||
h_shape = tuple(self._history_info.shape)
|
||||
if self._history_info.format.type == FormatType.UINT8:
|
||||
@@ -183,6 +239,18 @@ class HailoPredictor:
|
||||
buf = action_history.astype(np.float32).reshape(h_shape)
|
||||
bindings.input(self._history_info.name).set_buffer(buf)
|
||||
|
||||
if self._pred_info is not None:
|
||||
p_shape = tuple(self._pred_info.shape)
|
||||
if pred_token is None:
|
||||
values = np.zeros(p_shape, dtype=np.float32)
|
||||
else:
|
||||
values = np.asarray(pred_token, dtype=np.float32).reshape(p_shape)
|
||||
if self._pred_info.format.type == FormatType.UINT8:
|
||||
q = self._pred_info.quant_info
|
||||
values = np.clip(np.round(values / q.qp_scale + q.qp_zp),
|
||||
0, 255).astype(np.uint8)
|
||||
bindings.input(self._pred_info.name).set_buffer(values)
|
||||
|
||||
out_shape = tuple(self._out_info.shape)
|
||||
if self._out_info.format.type == FormatType.UINT8:
|
||||
out_buf = np.zeros(out_shape, dtype=np.uint8)
|
||||
@@ -203,19 +271,43 @@ class HailoPredictor:
|
||||
result = result * self._action_std + self._action_mean
|
||||
return result
|
||||
|
||||
def _preprocess(self, image) -> np.ndarray:
|
||||
def _frame_float(self, image) -> np.ndarray:
|
||||
"""Resize → float32 HWC;归一化 HEF(负零偏)再走 ImageNet 归一化"""
|
||||
img = Image.fromarray(image) if isinstance(image, np.ndarray) else image
|
||||
img = img.convert("RGB").resize(
|
||||
(self.image_width, self.image_height), Image.BILINEAR)
|
||||
return np.asarray(img, dtype=np.uint8) # HWC uint8
|
||||
x = np.asarray(img, dtype=np.float32) # HWC 0..255
|
||||
if self._normalize_frames:
|
||||
x = (x / 255.0 - IMAGENET_MEAN) / IMAGENET_STD
|
||||
return x
|
||||
|
||||
def _bind_frames(self, bindings, frame_float: np.ndarray):
|
||||
"""(F,H,W,3) float 帧 → HEF 帧输入缓冲区(按布局与量化)"""
|
||||
from hailo_platform import FormatType
|
||||
|
||||
def quantize(buf, q):
|
||||
return np.clip(np.round(buf / q.qp_scale + q.qp_zp),
|
||||
0, 255).astype(np.uint8)
|
||||
|
||||
def _image_buffer(self, frames: np.ndarray) -> np.ndarray:
|
||||
"""(F, H, W, 3) → HEF 图像输入缓冲区(按编译后布局)"""
|
||||
if self._image_layout == "ncfhw":
|
||||
return frames.transpose(0, 3, 1, 2)[np.newaxis] # (1,F,3,H,W)
|
||||
# nhwc_fused: 帧沿通道拼接 (1, H, W, F*3)
|
||||
return frames.reshape(1, self.image_height, self.image_width,
|
||||
self.frame_len * 3)
|
||||
buf = frame_float.transpose(0, 3, 1, 2)[np.newaxis] # (1,F,3,H,W)
|
||||
info = self._frame_infos[0]
|
||||
if info.format.type == FormatType.UINT8:
|
||||
buf = quantize(buf, info.quant_info)
|
||||
bindings.input(info.name).set_buffer(buf)
|
||||
elif self._image_layout == "nhwc_fused":
|
||||
buf = frame_float.reshape(
|
||||
1, self.image_height, self.image_width, self.frame_len * 3)
|
||||
info = self._frame_infos[0]
|
||||
if info.format.type == FormatType.UINT8:
|
||||
buf = quantize(buf, info.quant_info)
|
||||
bindings.input(info.name).set_buffer(buf)
|
||||
else: # nhwc_frames:每帧独立输入
|
||||
for i, info in enumerate(self._frame_infos):
|
||||
buf = frame_float[i]
|
||||
if info.format.type == FormatType.UINT8:
|
||||
buf = quantize(buf, info.quant_info)
|
||||
bindings.input(info.name).set_buffer(buf)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -227,10 +319,14 @@ def main():
|
||||
help="冒号分隔各步,每步 action_dim 个值")
|
||||
parser.add_argument("--stats", default=None,
|
||||
help="action_stats.npy 路径(训练时启用归一化才需要)")
|
||||
parser.add_argument("--pred-token", default=None,
|
||||
help="pred_token .npy 路径(export_deploy.py "
|
||||
"--pred-token-out 产物;缺省喂零)")
|
||||
parser.add_argument("--output-format", choices=["json", "raw"],
|
||||
default="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
pred_token = np.load(args.pred_token) if args.pred_token else None
|
||||
with HailoPredictor(args.hef, stats_path=args.stats) as predictor:
|
||||
if len(args.images) != predictor.frame_len:
|
||||
raise SystemExit(
|
||||
@@ -238,7 +334,7 @@ def main():
|
||||
f"实际为 {len(args.images)} 个")
|
||||
images = [np.array(Image.open(p).convert("RGB")) for p in args.images]
|
||||
history = parse_action_history(args.action_history, predictor.action_dim)
|
||||
predicted = predictor.predict(images, history)
|
||||
predicted = predictor.predict(images, history, pred_token=pred_token)
|
||||
|
||||
if args.output_format == "json":
|
||||
print(json.dumps({"predicted_action": predicted.tolist()}, indent=2))
|
||||
|
||||
@@ -1093,6 +1093,88 @@ class TestHailoPredictor(unittest.TestCase):
|
||||
np.testing.assert_allclose(result, [0, 0, 0])
|
||||
self.assertEqual(seen_shapes["image"], (1, 4, 4, 6))
|
||||
|
||||
def test_multiframe_inputs_layout(self):
|
||||
"""新版 5 输入 HEF:frame×3 (H,W,3) + history + pred_token 常量"""
|
||||
from hailo_infer import HailoPredictor
|
||||
q_frame = _FakeQuantInfo(qp_zp=114.0, qp_scale=0.018723)
|
||||
q_pred = _FakeQuantInfo(qp_zp=123.0, qp_scale=18571.0)
|
||||
infos_in = [
|
||||
_FakeVStreamInfo("net/input_layer1", (64, 64, 3),
|
||||
_FakeFormatType.UINT8, q_frame),
|
||||
_FakeVStreamInfo("net/input_layer2", (64, 64, 3),
|
||||
_FakeFormatType.UINT8, q_frame),
|
||||
_FakeVStreamInfo("net/input_layer3", (64, 64, 3),
|
||||
_FakeFormatType.UINT8, q_frame),
|
||||
_FakeVStreamInfo("net/input_layer4", (1, 5, 18),
|
||||
_FakeFormatType.UINT8,
|
||||
_FakeQuantInfo(qp_zp=0.0, qp_scale=1.960784)),
|
||||
_FakeVStreamInfo("net/input_layer5", (1, 1, 128),
|
||||
_FakeFormatType.UINT8, q_pred),
|
||||
]
|
||||
infos_out = [_FakeVStreamInfo(
|
||||
"net/conv49", (1, 1, 18), _FakeFormatType.UINT8,
|
||||
_FakeQuantInfo(qp_zp=2.0, qp_scale=0.837115))]
|
||||
seen = {}
|
||||
|
||||
def transform(b):
|
||||
for name in ("net/input_layer1", "net/input_layer2", "net/input_layer3"):
|
||||
seen[name] = np.asarray(b.input(name).get_buffer())
|
||||
seen["history"] = np.asarray(b.input("net/input_layer4").get_buffer())
|
||||
seen["pred"] = np.asarray(b.input("net/input_layer5").get_buffer())
|
||||
b.output("net/conv49").get_buffer()[...] = 2 # 反量化后为 0
|
||||
|
||||
_install_fake_hailo(self, infos_in, infos_out, transform)
|
||||
with HailoPredictor("fake.hef") as p:
|
||||
self.assertEqual((p.frame_len, p.history_len, p.action_dim), (3, 5, 18))
|
||||
self.assertEqual((p.image_width, p.image_height), (64, 64))
|
||||
result = p.predict(self._frames(3, size=64), np.zeros((5, 18), np.float32))
|
||||
self.assertEqual(seen["net/input_layer1"].dtype, np.uint8)
|
||||
self.assertEqual(seen["net/input_layer1"].shape, (64, 64, 3))
|
||||
self.assertEqual(seen["history"].shape, (1, 5, 18))
|
||||
np.testing.assert_array_equal(seen["history"], 0)
|
||||
np.testing.assert_array_equal(seen["pred"], 123) # 退化量化:喂 0 → q=123
|
||||
np.testing.assert_allclose(result, 0)
|
||||
|
||||
def test_multiframe_scrambled_report_order(self):
|
||||
"""HEF vstream 报告顺序可能被打乱(实测 1,5,4,3,2),
|
||||
帧输入必须按 input_layerN 排序绑定,否则 frame1/frame2 错位"""
|
||||
from hailo_infer import HailoPredictor
|
||||
infos_in = [
|
||||
_FakeVStreamInfo("net/input_layer1", (8, 8, 3),
|
||||
_FakeFormatType.FLOAT32),
|
||||
_FakeVStreamInfo("net/input_layer3", (8, 8, 3),
|
||||
_FakeFormatType.FLOAT32),
|
||||
_FakeVStreamInfo("net/input_layer2", (8, 8, 3),
|
||||
_FakeFormatType.FLOAT32),
|
||||
_FakeVStreamInfo("net/input_layer4", (1, 2, 3),
|
||||
_FakeFormatType.FLOAT32),
|
||||
_FakeVStreamInfo("net/input_layer5", (1, 1, 4),
|
||||
_FakeFormatType.FLOAT32),
|
||||
]
|
||||
infos_out = [_FakeVStreamInfo("net/conv49", (1, 1, 3),
|
||||
_FakeFormatType.FLOAT32)]
|
||||
seen = {}
|
||||
|
||||
def transform(b):
|
||||
for name in ("net/input_layer1", "net/input_layer2",
|
||||
"net/input_layer3"):
|
||||
seen[name] = np.asarray(b.input(name).get_buffer()).copy()
|
||||
_first_row_transform("net/input_layer4", "net/conv49")(b)
|
||||
|
||||
_install_fake_hailo(self, infos_in, infos_out, transform)
|
||||
frames = self._frames(3)
|
||||
frames[0][:] = 10
|
||||
frames[1][:] = 20
|
||||
frames[2][:] = 30
|
||||
with HailoPredictor("fake.hef") as p:
|
||||
p.predict(frames, np.zeros((2, 3), dtype=np.float32))
|
||||
np.testing.assert_array_equal(
|
||||
seen["net/input_layer1"], np.full((8, 8, 3), 10, np.float32))
|
||||
np.testing.assert_array_equal(
|
||||
seen["net/input_layer2"], np.full((8, 8, 3), 20, np.float32))
|
||||
np.testing.assert_array_equal(
|
||||
seen["net/input_layer3"], np.full((8, 8, 3), 30, np.float32))
|
||||
|
||||
def test_frame_count_mismatch_raises(self):
|
||||
from hailo_infer import HailoPredictor
|
||||
infos_in = [
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
# actpred 多帧注意力架构改造测试报告
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 日期 | 2026-08-09 |
|
||||
| 模块 | actpred |
|
||||
| 测试人 | qing |
|
||||
| 环境 | Intel Xeon E3-1200 v2 (Ivy Bridge), 4 核, Linux 24.04, Python 3.x, PyTorch (CPU-only) |
|
||||
| 关联文件 | `config.py`, `model.py`, `dataset.py`, `train.py`, `pretrain_mt.py`, `predict.py`, `export_onnx.py`, `test_actpred.py` |
|
||||
| 前置报告 | [actpred_pretrain_test_20260808.md](actpred_pretrain_test_20260808.md) |
|
||||
|
||||
---
|
||||
|
||||
## 一、改造背景与目标
|
||||
|
||||
原架构为"单帧 CNN + 历史动作 MLP/GRU + concat 融合",仅支持单帧图像输入。
|
||||
本次改造目标:
|
||||
|
||||
1. 引入注意力机制,支持**多帧图像 + 历史动作序列**联合建模,预测下一时间步动作
|
||||
2. 保持 checkpoint 格式(`model_state_dict` + `config`)、CLI 工作流与 ONNX 导出链路不变
|
||||
3. 保持除 batch 维外全静态 shape,面向树莓派 AI 扩展板(NPU)部署友好
|
||||
4. 直接替换旧架构(用户决策):删除 MLP/GRU 编码器与 concat 路径,旧 checkpoint 作废
|
||||
|
||||
## 二、新架构
|
||||
|
||||
```
|
||||
F 帧图像 (F×3×64×64) ─► 共享 CNN 编码器 ─► F 个 token ─► proj → d_model ┐
|
||||
├─► [img×F, act×H, PRED] + 位置编码
|
||||
动作历史 (H×D) ─► Linear proj → d_model ─► H 个 token ──────────┤ │
|
||||
可学习 [PRED] token ─────────────────────────────────────────────────┘ ▼
|
||||
TransformerEncoder (pre-LN, 2 层, 4 头, d_model=128)
|
||||
│
|
||||
取 PRED 位置输出 ─► MLP 头 ─► 预测动作 (action_dim)
|
||||
```
|
||||
|
||||
| 设计点 | 取值/说明 |
|
||||
|--------|----------|
|
||||
| token 序列 | `[img×F, act×H, PRED]`,长度固定 F+H+1(默认 3+5+1=9) |
|
||||
| 帧序约定 | 旧→新,末帧为当前帧;冷启动可重复传同一帧 |
|
||||
| 注意力 | `nn.TransformerEncoder`,pre-LN(norm_first),dim_feedforward=2×d_model |
|
||||
| 位置编码 | 可学习 `pos_embed (1, F+H+1, d)`,trunc_normal 初始化 |
|
||||
| 输出 | 取末位 PRED token 过 MLP 头(d → 128 → 64 → action_dim) |
|
||||
| 静态性 | forward 中 reshape 仅 batch 维动态,其余编译期固定 |
|
||||
| 参数量 | 409,042(旧架构 142,034,约 2.9 倍) |
|
||||
|
||||
### 新增配置参数
|
||||
|
||||
| 参数 | 默认值 | 校验范围 |
|
||||
|------|--------|---------|
|
||||
| `--frame-len` | 3 | [1, 1024] |
|
||||
| `--attn-dim` | 128 | [1, 4096],且须能被 attn-heads 整除 |
|
||||
| `--attn-heads` | 4 | [1, 64] |
|
||||
| `--attn-layers` | 2 | [1, 64] |
|
||||
|
||||
删除参数:`--history-encoder`、`--action-hidden-dim`。
|
||||
旧 checkpoint 因 config 含未知键(`history_encoder`/`action_hidden_dim`)在加载时被
|
||||
`parse_checkpoint_config` 以明确 ValueError 拒载,符合"作废"预期(有专项测试覆盖)。
|
||||
|
||||
## 三、单元测试
|
||||
|
||||
```
|
||||
$ python test_actpred.py
|
||||
Ran 56 tests in 5.959s
|
||||
OK
|
||||
```
|
||||
|
||||
**56 项测试全部通过**(安装 onnxscript/onnxruntime 后无跳过项)。本次新增/改写用例:
|
||||
|
||||
| 用例 | 验证点 |
|
||||
|------|--------|
|
||||
| `test_forward_multi_frame` | (2,3,3,64,64) + (2,5,18) → (2,18) |
|
||||
| `test_forward_single_frame_degenerate` | frame_len=1 退化单帧仍正常前向 |
|
||||
| `test_forward_dynamic_batch` | batch 1/4/8 动态(frame_len=2) |
|
||||
| `test_custom_attn_config` | attn_dim=64/heads=2/layers=1 可构建前向 |
|
||||
| `test_attn_dim_not_divisible_by_heads_raises` | 130 % 4 ≠ 0 → ValueError |
|
||||
| `test_dataset_loading` | 多帧样本 shape (3,3,32,32),batch key 改为 `images` |
|
||||
| `test_frame_len_gt_history_shifts_start` | 采样起点 max(history_len, frame_len-1) 正确 |
|
||||
| `test_legacy_checkpoint_config_rejected` | 旧架构 checkpoint 拒载 |
|
||||
| `test_wrong_frame_count_raises` | 推理帧数不符 → ValueError |
|
||||
| `test_end_to_end_predict` 等推理用例 | 多帧输入端到端、归一化等价性、确定性 |
|
||||
|
||||
## 四、预训练测试
|
||||
|
||||
### 4.1 训练配置
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 数据来源 | `../bot4cj/scripts/*.mt`(13 文件) |
|
||||
| train/val 划分 | **文件级**(11 训练文件 / 2 验证文件,杜绝滑动窗口泄漏) |
|
||||
| 输入 | 全零多帧图像 (B, 3, 3, 64, 64) + action_history (B, 5, 18) |
|
||||
| 优化器/调度 | AdamW (lr=0.001, wd=1e-4) + CosineAnnealingLR |
|
||||
| 数据增强 | Gaussian noise (std=1.0) |
|
||||
| batch_size / epochs / patience | 8 / 200 / 30 |
|
||||
| 随机种子 | 42 |
|
||||
|
||||
### 4.2 训练结果
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| 最终轮数 | 65(早停,patience=30) |
|
||||
| 最优 val_loss | **2476.75** |
|
||||
| 产出 | `checkpoints/pretrain_best.pt`(含 optimizer)、`checkpoints/pretrain_best_inference.pt` |
|
||||
|
||||
> **关于 val_loss 数值的说明**:本次 2476.75 与前次报告的 190.58 不可直接对比——
|
||||
> 前次数值系样本级滑动窗口划分导致的数据泄漏虚低值(已在 0808 审计中定性);
|
||||
> 本次为文件级划分(验证集是训练中完全未见过的动作脚本),且模型更大(409K vs 142K)
|
||||
> 而数据仅 13 个脚本,属"诚实的困难任务"数值。注意力架构的真实收益需在真实图像数据
|
||||
> 微调后评估。
|
||||
|
||||
## 五、推理链路验证
|
||||
|
||||
### 5.1 CLI 全链路
|
||||
|
||||
```
|
||||
$ bash _run.sh --model checkpoints/pretrain_best_inference.pt \
|
||||
--images /tmp/f0.jpg /tmp/f1.jpg /tmp/f2.jpg \
|
||||
--action-history "45,0,...(5 步 × 18 值)"
|
||||
18.3430 10.1614 17.3654 ... (18 维输出,数值有限)
|
||||
```
|
||||
|
||||
✅ 3 帧 + 5 步历史全链路输出正常。
|
||||
|
||||
### 5.2 错误场景
|
||||
|
||||
```
|
||||
$ ... --images /tmp/f0.jpg ...
|
||||
--images 需传入 3 个路径(旧→新),实际为 1 个。冷启动时可重复传同一帧路径。
|
||||
```
|
||||
|
||||
✅ 帧数不符时报错信息明确,含冷启动指引。
|
||||
|
||||
## 六、ONNX 导出验证
|
||||
|
||||
```
|
||||
$ python export_onnx.py --model checkpoints/pretrain_best_inference.pt --output /tmp/actpred_attn.onnx
|
||||
ONNX model exported to: /tmp/actpred_attn.onnx
|
||||
ONNX 一致性验证通过(最大差异: 7.629395e-06)
|
||||
```
|
||||
|
||||
| 项目 | 结果 |
|
||||
|------|------|
|
||||
| 导出 | ✅ 成功(opset 17,输入 `images (batch,3,3,64,64)` + `action_history (batch,5,18)`) |
|
||||
| onnxruntime 一致性 | ✅ 最大差异 7.63e-06(< 1e-4 阈值) |
|
||||
| 动态维 | 仅 batch,NPU 编译友好 |
|
||||
|
||||
注:本次为环境补装了 `onnxscript` 与 `onnxruntime`(torch ≥ 2.12 ONNX 导出依赖)。
|
||||
|
||||
## 七、CPU 推理性能基准
|
||||
|
||||
测试方法:warmup 20 次后测 200 次取平均,输入随机 3 帧 (64,64,3) uint8 + (5,18) float32。
|
||||
|
||||
| 环节 | 本次(注意力,3 帧) | 前次(concat,单帧) |
|
||||
|------|---------------------|---------------------|
|
||||
| 参数量 | 409,042 | 142,034 |
|
||||
| 端到端推理(含预处理) | **4.25 ms (235 FPS)** | 1.61 ms (622 FPS) |
|
||||
| 纯前向 | 2.77 ms (360 FPS) | 1.09 ms (916 FPS) |
|
||||
| image_encoder 占比 | 1.30 ms(47%,3 帧) | 0.82 ms(75%,1 帧) |
|
||||
|
||||
评估:
|
||||
|
||||
- 235 FPS 对 bot4cj 30 FPS 控制循环仍有 **7.8 倍余量**,实时性满足
|
||||
- 延迟增长主因:CNN 由 1 帧变 3 帧(约 3×0.43ms)+ Transformer 编码器(约 1.5ms)
|
||||
- image_encoder 占比由 75% 降至 47%,注意力部分成为新的次要热点;
|
||||
部署到树莓派 AI 扩展板后两者均可被 NPU 加速(静态 shape 已保证)
|
||||
|
||||
## 八、结论与下一步
|
||||
|
||||
### 8.1 测试结论
|
||||
|
||||
| 测试项 | 结果 |
|
||||
|--------|------|
|
||||
| 多帧注意力模型前向 | ✅ 通过(多帧/单帧退化/动态 batch/自定义 attn 配置) |
|
||||
| 配置校验 | ✅ 通过(frame_len/attn_* 值域 + 整除校验 + 旧 checkpoint 拒载) |
|
||||
| 多帧数据集采样 | ✅ 通过(采样起点与样本数断言) |
|
||||
| 单元测试 | ✅ 56/56 通过 |
|
||||
| 预训练 | ✅ 通过(65 epoch 早停,文件级划分无泄漏) |
|
||||
| 推理 CLI | ✅ 通过(正常 + 错误场景) |
|
||||
| ONNX 导出 | ✅ 通过(一致性 7.63e-06) |
|
||||
| CPU 性能 | ✅ 通过(235 FPS,7.8 倍实时余量) |
|
||||
|
||||
### 8.2 已知局限
|
||||
|
||||
1. **预训练图像分支未习得**:全零多帧占位下 image_encoder 与图像 token 的注意力权重
|
||||
无有效梯度信号,真实收益依赖后续真实图像微调
|
||||
2. **数据量极少**:13 个 .mt 脚本对 409K 参数模型明显不足,val_loss 绝对值偏高属预期
|
||||
3. **旧 checkpoint 全部作废**:已删除并重训预训练 checkpoint;如有外部持有旧
|
||||
`best_inference.pt` 需同步告知重训
|
||||
|
||||
### 8.3 下一步建议
|
||||
|
||||
1. **采集真实多帧数据**:用 `DataRecorder` 录制 bot4cj 摄像头 episode,以本次预训练
|
||||
checkpoint 为起点微调(勿开 `--normalize-actions`)
|
||||
2. **树莓派 AI 扩展板部署验证**:将 ONNX 模型走扩展板厂商工具链编译,实测 NPU 延迟
|
||||
3. **帧数消融**:真实数据到位后对比 frame_len=1/3/5 的验证误差,确认多帧收益
|
||||
4. **扩充 .mt 脚本**:增加动作脚本数量,缓解预训练数据不足
|
||||
|
||||
---
|
||||
|
||||
## 附录:文件变更清单
|
||||
|
||||
| 文件 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `actpred/config.py` | 修改 | 新增 frame_len/attn_*;删除 history_encoder/action_hidden_dim;整除校验 |
|
||||
| `actpred/model.py` | 重写 | 多帧 Transformer 注意力架构;删除 MLP/GRU 编码器与 concat 融合 |
|
||||
| `actpred/dataset.py` | 修改 | 多帧窗口采样;batch key `image` → `images` |
|
||||
| `actpred/train.py` | 修改 | 多帧适配(frame_len 透传、batch key) |
|
||||
| `actpred/pretrain_mt.py` | 修改 | 全零多帧占位;打印信息更新 |
|
||||
| `actpred/predict.py` | 修改 | `predict(images, history)` 多帧 API;CLI `--image` → `--images` |
|
||||
| `actpred/export_onnx.py` | 修改 | 多帧 dummy 输入;input_names 更新 |
|
||||
| `actpred/test_actpred.py` | 修改 | 56 项用例(新增多帧/校验/拒载/帧数错误等) |
|
||||
| `actpred/README.md` | 修改 | 架构图、CLI 示例、配置表、API 文档同步 |
|
||||
| `actpred/checkpoints/pretrain_best*.pt` | 重新生成 | 旧架构 checkpoint 已删除并重训 |
|
||||
@@ -1,124 +0,0 @@
|
||||
# 研究报告:actpred 与 bot4cj 项目
|
||||
|
||||
> 研究时间:2026-08-09
|
||||
> 仓库:`git.cancerai.top/qing/blackbird`(micro_servers 单体仓库)
|
||||
> 结论先行:**两个项目是同一套「具身智能 / 机器人」技术栈的两个互补半环**——
|
||||
> `actpred` 是「大脑」(学出来的动作预测器),`bot4cj` 是「身体」(把动作落到真实硬件的执行引擎)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话定位
|
||||
|
||||
| 项目 | 语言/框架 | 角色 | 输入 → 输出 |
|
||||
|------|-----------|------|-------------|
|
||||
| **actpred** | Python / PyTorch | 动作预测「大脑」 | 摄像头图像 + 历史动作序列 → 下一步连续动作(回归) |
|
||||
| **bot4cj** | 仓颉 Cangjie + C/C++/Lua | 硬件执行「身体」 | 矩阵式脚本(.mt) → 驱动舵机/IMU/扬声器 |
|
||||
|
||||
---
|
||||
|
||||
## 2. actpred 详解
|
||||
|
||||
### 2.1 它是什么
|
||||
纯神经网络实现的**动作预测模块**:给定一帧摄像头图像(3×64×64)和前 `history_len` 步动作,回归预测当前步的连续动作向量。默认参数量约 140K,最大可到 ~2.3M(mobilenet + gru)。
|
||||
|
||||
### 2.2 架构
|
||||
```
|
||||
图像(3×64×64) ──► CNN编码器(simple/mobilenet) ──► img_feat(128)
|
||||
│ concat
|
||||
动作历史(H×D) ──► MLP/GRU编码器 ──► act_feat(64) ──► 融合头 ──► 动作(action_dim)
|
||||
```
|
||||
- **图像编码器**:`SimpleCNN`(4 层卷积+自适应池化)或 `MobileNetEncoder`(ImageNet 预训练,带离线权重容错)。
|
||||
- **历史编码器**:`MLPHistoryEncoder`(默认)或 `GRUHistoryEncoder`。
|
||||
- **损失**:纯 MSE 回归;优化器 AdamW + CosineAnnealingLR;早停 + 梯度裁剪。
|
||||
|
||||
### 2.3 工程质量(亮点)
|
||||
- 全部超参通过 `config.py` 的 dataclass + CLI 覆盖,序列化进 checkpoint,推理时自动还原。
|
||||
- 数据层 `dataset.py` 设计扎实:自然排序键、按 episode 切分训练/验证、动作归一化统计量**只用训练集计算**(避免 val loss 失真)、`DataRecorder` 采集器。
|
||||
- `export_onnx.py` 导出 ONNX 并**自动用 onnxruntime 验证一致性**(最大差异打印)。
|
||||
- `test_actpred.py` 单元测试覆盖:排序、解析、前向、动态 batch、录制器、数据集、配置——质量明显高于一般研究脚本。
|
||||
|
||||
### 2.4 默认配置
|
||||
`image_size=64, action_dim=8, history_len=5, backbone=simple, history_encoder=mlp, epochs=100, lr=1e-3`。
|
||||
|
||||
---
|
||||
|
||||
## 3. bot4cj 详解
|
||||
|
||||
### 3.1 它是什么
|
||||
基于**仓颉(Cangjie)**编程语言 + Linux I2C 接口的**机器人硬件脚本解析器**。用一种「行列式表格」脚本(`.mt`)描述机器人的硬件构成与动作序列,解析后驱动真实硬件执行。目标平台是树莓派(aarch64),适配 Dashun 蜘蛛机器人(10 舵机 + 2 云台 + 扬声器)。
|
||||
|
||||
### 3.2 架构分层
|
||||
```
|
||||
.mt 矩阵脚本
|
||||
↓ MatrixParser (P.cj) —— FSM 逐字符解析 → MatrixObject 链表
|
||||
↓ RobotStruct (r.cj) —— createHardVars() 构建硬件变量(工厂模式)
|
||||
↓ 硬件实现 hd/*.cj:PCA9685 / Servo / MPU6050 / Speaker / Lua扩展
|
||||
↓ 系统库 cpp/ + cuda/:I2C / 矩阵 / FFT / CNN / 音频
|
||||
↓ 数学/NN 库 math/*.cj:Matrix1 / Tensor / Af / Nn1 / Adam / Kalman
|
||||
```
|
||||
|
||||
### 3.3 关键设计
|
||||
- **MatrixVars_Hardware 基类**:用三个闭包回调统一硬件接口——`build()`(rise,拿资源)、`play()`(things,执行,输入输出均为 JsonValue)、`free()`(destroy,放资源)。硬件与脚本通过 `JsonValue` 解耦。
|
||||
- **硬件变量工厂**:`MtVars_I2C / Servo / Speaker / MPU6050_BIND / Alias / Lua`,从 `@I2C/@PCA9685/@Servo/@Speaker/@MPU6050` 块按名构建。
|
||||
- **Lua 扩展口**:任意未知 `@Block` 会路由到 `modules/<Name>.lua`(已接 ADS1115/ADS1256/PCA9685/SPI),C 层注册了 i2c_*/spi_*/milli_sleep 给 Lua 调用——这是「通用机器人硬件描述语言」野心的落点。
|
||||
- **I2C 总线保护**:20 槽锁数组,多硬件共享总线时互斥。
|
||||
- **资源释放**:`main.cj` 用 `try/finally` 显式 `deactive()`,规避仓颉 GC 析构时机不确定的风险。
|
||||
|
||||
### 3.4 自带 NN 库(有意思的工程副产物)
|
||||
不依赖 PyTorch,纯仓颉实现了 `Nn1`(全连接,含前向/反向/权重更新)、`Matrix1`/`Tensor`、`Af`(6 种激活)、`Ini`(Xavier/Kaiming)、`Adam`、`cnn`、`fft`、`kalman`。配套 C/C++ 加速库(可切 CUDA)。这说明作者**试图把训练与推理都搬进仓颉生态**,为后续 DRL 端到端闭环做准备。
|
||||
|
||||
### 3.5 已支持的硬件
|
||||
- **PCA9685**(0x40,50Hz PWM,角度→脉宽 0.5~2.5ms→12 位 PWM)。
|
||||
- **Servo**(max/min/offset 校准,max<0 自动翻转反向安装舵机)。
|
||||
- **MPU6050**(±500°/s,±8g,后台 10ms 采样线程 + 卡尔曼融合)。
|
||||
- **Speaker**(正弦波发声,5ms 淡入淡出)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 两个项目的关系(核心结论)
|
||||
|
||||
1. **闭环互补**:`actpred` 感知+记忆→预测动作(连续向量);`bot4cj` 把动作变成舵机角度执行。
|
||||
这在代码里已有明确意图:
|
||||
- `bot4cj/src/main.cj` 预留了 DRL 分支(`--test`、注释掉的 `run_action_drl`、`-i/-o` 模型加载/保存)。
|
||||
- `bot4cj/src/r.cj:838` 注释:`TODO(20260518-qing): 使用神经网络RNN直接根据动作+传感器脚本生成下一步动作`。
|
||||
2. **尚未真正打通**:目前两者**没有共享数据格式**。`actpred` 默认 `action_dim=8`,`bot4cj` 是 10 舵机 + 云台;`actpred` 输出 numpy 向量,`bot4cj` 消费 `.mt` 矩阵。集成是明确的「未来工作」。
|
||||
3. **同作者、同仓库**:均署名 `qing`,同属 `blackbird` 单体仓库,是同一具身智能蓝图的「预测端」与「执行端」。
|
||||
|
||||
---
|
||||
|
||||
## 5. 发现的问题与不一致(值得跟进)
|
||||
|
||||
### 5.1 在代码中已修复、但 README 仍列为「已知问题」的旧项(README 滞后)
|
||||
- **D6(Softmax 雅可比错误)**:当前 `math/Af.cj:softmax_jacobian` 已实现正确——对角线 `y_i(1-y_i)`、非对角 `-y_i*y_j`,逻辑无误。
|
||||
- **D8(Tanh 导数错误)**:当前 `math/Af.cj:tanh_dot` 用 `1 - y_val*y_val`(`y` 即 tanh 输出),**这是正确的**。代码里 `NOTE(20260525-qing): 修复命名(x => y)` 表明作者已意识到此前是按输出 y 计算的。README 描述的旧 bug 已修。
|
||||
- → 建议:更新 `README.robot.md` 的「已知问题」表,删除/标注 D6、D8 已修复,避免误导。
|
||||
|
||||
### 5.2 真实存在的代码瑕疵(未在 README 列出)
|
||||
- **`src/P.cj:147` 调试残留**:`parse()` 主循环里有 `print(s)`,**逐字符把脚本内容打到 stdout**。运行脚本时会污染标准输出(若将来用 stdout 与上层通信会很致命)。应删除或改为 debug 开关。
|
||||
- **`src/P.cj:93` 运算符优先级 bug**:`if (!values.size == 0 && !grp.size == values[...].size)`。`!values.size` 实际是对 size 整数取反(非零→false,零→true),**并非** `!= 0`。正确写法应为 `values.size != 0 && grp.size != values[...].size`。当前该行恒为假分支,导致「行未对齐」检测 `DoesNotSync` 实际**从不触发**,矩阵列数不一致时不会报错而可能静默错位。
|
||||
- **`src/P.cj:202` 转义不一致**:同文件里换行判断混用 `r'\n'` 与 `r"\n"`(后者是字面两字符,非换行符),属复制粘贴隐患。
|
||||
- **PCA9685 双实现**:`hd/PCA9685.cj` 仍保留主路径,但已标注「由 Lua 取代」(`modules/PCA9685.lua`),存在代码冗余(D4)。
|
||||
|
||||
### 5.3 设计层面的待办(来自代码 TODO)
|
||||
- DRL 闭环未实现(`main.cj` 中 `run_action_drl`、`run_action` 均注释)。
|
||||
- `RobotStruct` 的 `@Action` 位置指示器(增量行动)未做,脚本路径写死 `./modules/`,未从环境变量/相对路径查找(r.cj:996, :998)。
|
||||
- `Tensor` 乘法仅支持 2D(D7),限制模型复杂度。
|
||||
|
||||
---
|
||||
|
||||
## 6. 总体评价
|
||||
|
||||
| 维度 | actpred | bot4cj |
|
||||
|------|---------|--------|
|
||||
| 代码成熟度 | 高(测试完备、可导出 ONNX) | 中(功能可用,有调试残留与 TODO) |
|
||||
| 实用性 | 可直接训练/推理/集成 | 强硬件依赖(树莓派+I2C),需真机验证 |
|
||||
| 创新性 | 常规回归网络 | 「矩阵式机器人硬件 DSL」+ 纯仓颉 NN 库,有想法 |
|
||||
| 集成度 | 独立 | 与 actpred 的物理闭环尚未打通 |
|
||||
|
||||
**建议下一步**:
|
||||
1. 修掉 `P.cj` 的 `print(s)` 与 `!size == 0` 两个硬 bug(影响解析正确性)。
|
||||
2. 更新 `README.robot.md`,把 D6/D8 标记为已修复。
|
||||
3. 定义 `actpred` 动作向量 ↔ `bot4cj` `.mt` 舵机角度的映射协议,打通预测—执行闭环(可先用 `action_dim=10` 对齐 10 舵机,或保留舵机+云台=12 维)。
|
||||
|
||||
---
|
||||
*本报告基于源码静态阅读(actpred 全部 .py、bot4cj 的 README/main/P/r/nn/Af + 构建脚本 + 示例脚本),未做真机/真模型运行验证。*
|
||||
@@ -1,265 +0,0 @@
|
||||
# actpred 模块效果评估报告
|
||||
|
||||
> 日期:2026-08-09
|
||||
> 评估范围:actpred 模块架构、代码质量、预训练模型实际预测能力、部署就绪度
|
||||
> 评估方法:源码审阅 + 单元测试 + 预训练 checkpoint 全量滑动窗口定量评估
|
||||
|
||||
---
|
||||
|
||||
## 一、模块定位
|
||||
|
||||
actpred 是 micro_servers 的独立/边缘模块,纯离线 CLI 工具,不参与微服务调用链。
|
||||
核心功能:输入多帧摄像头图像 + 历史动作序列 → 预测下一时间步的连续动作(回归)。
|
||||
面向树莓派 Dashun 蜘蛛机器人(配合 bot4cj 模块),目标部署场景为嵌入式机器人实时控制。
|
||||
|
||||
---
|
||||
|
||||
## 二、架构评估
|
||||
|
||||
### 2.1 模型架构
|
||||
|
||||
多帧 Transformer 注意力融合(2026-08 重写):
|
||||
|
||||
```
|
||||
F 帧图像 (F×3×64×64) → 共享 CNN 编码器 → F 个 token → proj → d_model ┐
|
||||
├→ [img×F, act×H, PRED] + 位置编码
|
||||
动作历史 (H×D) → Linear proj → d_model → H 个 token ─┤ │
|
||||
可学习 [PRED] token ─────────────────────────────────────────┘ ▼
|
||||
TransformerEncoder (pre-LN, 2层, 4头, d=128)
|
||||
│
|
||||
取 PRED 位置输出 → MLP 头 → 预测动作
|
||||
```
|
||||
|
||||
### 2.2 关键指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 参数量 | ~450K |
|
||||
| CPU 端到端推理 | 4.25 ms(235 FPS) |
|
||||
| CPU 纯 forward | 2.77 ms |
|
||||
| image_encoder 占比 | 47%(pure forward) |
|
||||
| ONNX 导出 | opset 17,仅 batch 维动态 |
|
||||
| ONNX 一致性 | max diff ~1.5e-8 |
|
||||
| 单元测试 | 56 个,全部通过 |
|
||||
|
||||
### 2.3 架构评价
|
||||
|
||||
| 维度 | 评级 | 说明 |
|
||||
|------|------|------|
|
||||
| 模型设计 | A | 多帧注意力融合合理,token 序列固定长度 F+H+1,NPU 友好 |
|
||||
| 推理速度 | A | 4.25ms 端到端,满足实时控制需求(>100 FPS) |
|
||||
| ONNX 部署 | A- | 静态 shape 导出正确,版本转换有轻微告警不影响功能 |
|
||||
| 安全性 | A | 零网络攻击面 + weights_only + 完整配置校验链 |
|
||||
|
||||
---
|
||||
|
||||
## 三、代码质量评估
|
||||
|
||||
### 3.1 代码规模
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| config.py | 205 | 三层 dataclass 配置 + CLI + 校验 |
|
||||
| model.py | 130 | SimpleCNN / MobileNetV2 / Transformer 模型 |
|
||||
| dataset.py | 151 | 数据集加载 + DataRecorder |
|
||||
| train.py | 187 | 训练入口(episode 级划分) |
|
||||
| pretrain_mt.py | 289 | .mt 预训练入口(文件级划分) |
|
||||
| predict.py | 138 | 推理封装 + CLI |
|
||||
| export_onnx.py | 58 | ONNX 导出 + 一致性验证 |
|
||||
| mt_parser.py | 133 | .mt @Action 矩阵解析器 |
|
||||
| test_actpred.py | 722 | 单元测试(56 cases) |
|
||||
| **合计** | **2013** | |
|
||||
|
||||
### 3.2 优点
|
||||
|
||||
1. **安全性设计扎实**
|
||||
- `torch.load(..., weights_only=True)` 全局使用
|
||||
- `parse_checkpoint_config` 实现键集合 + 值域 + 整除性三重校验
|
||||
- 旧架构 checkpoint 被未知键检查拒载
|
||||
|
||||
2. **数据泄漏已修复**
|
||||
- pretrain_mt.py 采用文件级 train/val 划分(非样本级滑动窗口)
|
||||
- train.py 采用 episode 级划分,归一化统计量仅用训练集计算
|
||||
|
||||
3. **工程化成熟**
|
||||
- Config dataclass 三层结构 + CLI override + checkpoint roundtrip
|
||||
- DataRecorder 原子 mkdir 防并发竞态
|
||||
- mt_parser `-` 前向填充取 @Servo 初始角度
|
||||
|
||||
4. **测试覆盖全面**
|
||||
- 56 单元测试,覆盖前向传播、数据加载、配置校验、mt 解析、推理端到端、ONNX 导出
|
||||
- 边界条件充分(空数据集、维度不匹配、帧数不一致、未知键、旧 checkpoint 拒载等)
|
||||
|
||||
### 3.3 可改进项
|
||||
|
||||
1. image_encoder 仍为推理瓶颈(占 pure forward 的 47%),后续可 INT8 量化
|
||||
2. 训练仅用 CosineAnnealingLR,未配置 warmup
|
||||
3. ONNX 导出 opset 17 版本转换有告警(`axes_input_to_attribute` 断言失败后 fallback),不影响正确性但值得跟进
|
||||
4. `requirements.txt` 版本范围宽松(`torch>=2.0,<3.0`),生产环境建议锁定
|
||||
|
||||
---
|
||||
|
||||
## 四、预训练模型定量评估
|
||||
|
||||
### 4.1 训练数据概况
|
||||
|
||||
```
|
||||
.mt 脚本文件数: 13
|
||||
总动作行数: 77
|
||||
训练数据列数: 12(补零到 18)
|
||||
文件级划分后: ~10 文件训练 / ~3 文件验证
|
||||
训练样本数: ~50(滑动窗口)
|
||||
模型参数量: 450K
|
||||
样本/参数比: 1:9000(严重欠定)
|
||||
```
|
||||
|
||||
各 .mt 文件数据量:
|
||||
|
||||
| 文件 | 行数 | 动作范围 |
|
||||
|------|------|----------|
|
||||
| jump.mt | 5 | [-180, 200] |
|
||||
| left-hand.mt | 4 | [-180, 1000] |
|
||||
| left-turn.mt | 11 | [-180, 200] |
|
||||
| lie-down.mt | 6 | [-180, 200] |
|
||||
| rght-hand.mt | 4 | [-180, 1000] |
|
||||
| std.mt | 1 | [-180, 200] |
|
||||
| stdd.mt | 7 | [-180, 200] |
|
||||
| t0.mt | 10 | [-180, 500] |
|
||||
| t1.mt | 1 | [10, 20] |
|
||||
| tilt-b/f/l/r.mt | 各 7 | [-180, 200] |
|
||||
|
||||
### 4.2 全量滑动窗口评估
|
||||
|
||||
使用预训练 checkpoint `pretrain_best_inference.pt`,对所有 .mt 文件做 history_len=5 滑动窗口预测:
|
||||
|
||||
| 脚本 | 预测数 | 平均 L2 误差 |
|
||||
|------|--------|-------------|
|
||||
| left-turn.mt | 6 | 117.9 |
|
||||
| lie-down.mt | 1 | 107.8 |
|
||||
| stdd.mt | 2 | 138.7 |
|
||||
| t0.mt | 5 | 239.1 |
|
||||
| tilt-b.mt | 2 | 193.9 |
|
||||
| tilt-f.mt | 2 | 201.4 |
|
||||
| tilt-l.mt | 2 | 210.9 |
|
||||
| tilt-r.mt | 2 | 183.4 |
|
||||
|
||||
### 4.3 与 Baseline 对比
|
||||
|
||||
| 方法 | Mean L2 Error | 相对比率 |
|
||||
|------|---------------|----------|
|
||||
| **模型预测** | **175.8** | **0.76x** |
|
||||
| Naive(复制上一步) | 230.0 | 1.0x(基准) |
|
||||
| 全局均值预测器 | 251.2 | 1.09x |
|
||||
|
||||
模型比 "复制上一步" 好 24%,比 "全局均值" 好 30%。但评估集仅 22 个预测点,统计意义有限。
|
||||
|
||||
### 4.4 预测分布分析
|
||||
|
||||
| 指标 | 预测 | 实际 |
|
||||
|------|------|------|
|
||||
| 数值范围 | [-164.5, 176.3] | [-180.0, 500.0] |
|
||||
| 各维 std(舵机 0-9) | 12~57 | 0~95 |
|
||||
| 各维 std(扬声器/延时 10-11) | 4~58 | 4~139 |
|
||||
| 各维 std(预留 12-17) | 1~6 | 0 |
|
||||
|
||||
**核心问题**:预测严重趋于均值(regression to the mean),预测 std 仅为实际 std 的 ~50%。极端值(如扬声器频率 1000Hz、延时 200ms)完全无法捕获。
|
||||
|
||||
### 4.5 典型预测案例
|
||||
|
||||
left-turn.mt 滑动窗口预测(前 6 维):
|
||||
|
||||
```
|
||||
t=5: pred=[18.0, 10.3, 17.1, -1.3, 16.5, 11.2] actual=[45, 0, 45, 45, 45, 0] L2=129.4
|
||||
t=6: pred=[16.2, 8.5, 15.5, -0.4, 15.1, 8.9] actual=[45, 0, 0, 45, 45, 0] L2=127.3
|
||||
t=7: pred=[15.7, 8.0, 15.0, -0.3, 14.7, 8.3] actual=[45, 0, 0, 0, 45, 0] L2=109.1
|
||||
```
|
||||
|
||||
预测值集中在 8~18 附近,而实际动作在 0~45 之间跳变,模型未能跟踪动作变化趋势。
|
||||
|
||||
---
|
||||
|
||||
## 五、分场景效果预测
|
||||
|
||||
### 场景 A:当前预训练模型直接部署
|
||||
|
||||
**评级:不可用**
|
||||
|
||||
- image_encoder 在零图像上训练,真实摄像头输入产生随机特征
|
||||
- 预测值被压缩到均值附近 ±15°,无法执行跳变动作
|
||||
- 扬声器频率预测几乎为 0
|
||||
- 预期表现:蜘蛛机器人缓慢无规律蠕动,无法完成有意义动作
|
||||
|
||||
### 场景 B:初步采集真实数据微调(~1K 帧,1-2 episodes)
|
||||
|
||||
**评级:有限可用**
|
||||
|
||||
- 450K 参数在 1K 帧上可学到基本运动模式
|
||||
- 平滑动作(站立→缓走)MAE ~15-25°/关节
|
||||
- 急变动作(跳跃、急转)MAE ~40-60°/关节
|
||||
- image_encoder 开始学习真实视觉特征
|
||||
|
||||
### 场景 C:充分数据采集(~10K 帧,10+ episodes,多场景)
|
||||
|
||||
**评级:基本可用**
|
||||
|
||||
- 平滑动作 MAE ~5-15°/关节
|
||||
- 急变动作 MAE ~20-40°/关节
|
||||
- 配合 noise_std 数据增强和早停,泛化可接受
|
||||
- SimpleCNN 表达力上限开始制约视觉理解
|
||||
|
||||
### 场景 D:场景 C + MobileNetV2 + 归一化动作
|
||||
|
||||
**评级:较好**
|
||||
|
||||
- MobileNetV2 ImageNet 预训练权重提供强视觉先验
|
||||
- 归一化动作空间让 MSE loss 对各维度公平
|
||||
- 平滑动作 MAE ~3-8°/关节
|
||||
|
||||
### 场景 E:理想状态(~100K 帧,充分多样性)
|
||||
|
||||
**评级:生产可用**
|
||||
|
||||
- MAE ~1-5°/关节
|
||||
- 可完成绝大部分预设动作
|
||||
|
||||
---
|
||||
|
||||
## 六、核心瓶颈排序
|
||||
|
||||
| 优先级 | 瓶颈 | 严重度 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | 数据量 | 致命 | 77 行 / 13 文件,450K 参数 vs ~50 样本,样本/参数比 1:9000 |
|
||||
| 2 | 零图像预训练的 image_encoder | 严重 | 47% 推理算力浪费在未训练的随机 CNN 上 |
|
||||
| 3 | 动作空间异质性 | 中等 | dim 0-9 角度 [0-200°]、dim 10-11 频率/延时 [0-1000]、dim 12-17 全零,未归一化导致 MSE 被大数值维度主导 |
|
||||
| 4 | 编舞脚本 vs 反应式行为 | 中等 | .mt 是预设动作编排,不含视觉反馈逻辑,模型学到的是 "动作惯性" 而非 "视觉→动作" 映射 |
|
||||
| 5 | SimpleCNN 表达力 | 轻微 | 4 层 Conv 对细粒度空间信息编码不足 |
|
||||
|
||||
---
|
||||
|
||||
## 七、量化预测总结
|
||||
|
||||
| 阶段 | 数据量 | 可用度 | 舵机 MAE | 能否完成预设动作 |
|
||||
|------|--------|--------|----------|-----------------|
|
||||
| 当前预训练 | 77 行 / 零图像 | 不可用 | >50° | 不能 |
|
||||
| 初步采集 | ~1K 帧 | 有限 | 15-40° | 部分(简单动作) |
|
||||
| 充分采集 | ~10K 帧 | 基本可用 | 5-20° | 大部分 |
|
||||
| 充分 + MobileNet | ~10K 帧 | 较好 | 3-10° | 大部分 |
|
||||
| 理想状态 | ~100K 帧 | 生产可用 | 1-5° | 绝大部分 |
|
||||
|
||||
---
|
||||
|
||||
## 八、建议优先级
|
||||
|
||||
1. **最高优先**:实现遥操作/示教采集,尽快积累真实 (图像, 动作) 配对数据
|
||||
2. **高优先**:采集前开启 `--normalize-actions`,平衡各维度 loss 权重
|
||||
3. **中优先**:用真实数据微调后再评估,决定是否换 backbone
|
||||
4. **低优先**:考虑预训练时冻结 image_encoder(减少无效梯度),或换恒等映射替代零图像
|
||||
|
||||
---
|
||||
|
||||
## 九、结论
|
||||
|
||||
actpred 模块架构设计合理,代码质量高,安全防护到位,推理速度满足实时需求。
|
||||
当前最大短板是 **数据量严重不足**(77 行零图像预训练),导致模型预测趋于均值、直接部署不可用。
|
||||
采集 10K 帧以上真实数据微调后,预计可达实用水平。
|
||||
模块已具备从 "实验原型" 向 "生产部署" 过渡的架构基础,下一步关键路径是 **数据采集 → 微调验证 → NPU 部署测试**。
|
||||
@@ -1,312 +0,0 @@
|
||||
# actpred 预训练与推理测试报告
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 日期 | 2026-08-08 |
|
||||
| 模块 | actpred |
|
||||
| 测试人 | qing |
|
||||
| 环境 | Intel Xeon E3-1200 v2 (Ivy Bridge), 4 核, Linux 24.04, Python 3.x, PyTorch (CPU-only) |
|
||||
| 关联文件 | `config.py`, `mt_parser.py`, `pretrain_mt.py`, `test_actpred.py`, `export_onnx.py` |
|
||||
|
||||
---
|
||||
|
||||
## 一、测试目标
|
||||
|
||||
1. 验证 `action_dim` 默认值从 8 改为 18 后模型结构与测试的正确性
|
||||
2. 验证 `.mt` 脚本 `@Action` 矩阵解析器的功能正确性
|
||||
3. 执行 `.mt` 脚本预训练并评估训练收敛情况
|
||||
4. 测量推理延迟与吞吐量
|
||||
5. 调研 PyTorch 对 NPU / 向量计算单元的支持现状
|
||||
|
||||
---
|
||||
|
||||
## 二、action_dim 参数变更验证
|
||||
|
||||
### 2.1 变更内容
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `config.py` | `action_dim: int = 8` → `18` |
|
||||
| `test_actpred.py` | `test_forward_simple_mlp` / `test_forward_simple_gru` 的 shape 断言 8 → 18;`test_default_config` 断言 8 → 18 |
|
||||
| `README.md` | 训练命令、推理示例、DataRecorder 示例、配置表说明 |
|
||||
|
||||
### 2.2 参数量对比
|
||||
|
||||
| action_dim | 参数量 | 增量 | 输出形状 |
|
||||
|------------|--------|------|---------|
|
||||
| 8 | 138,184 (0.138M) | 基准 | (batch, 8) |
|
||||
| 18 | 142,034 (0.142M) | +3,850 | (batch, 18) |
|
||||
| 30 | 146,654 (0.147M) | +8,470 | (batch, 30) |
|
||||
|
||||
每增加 1 维 action_dim 约多 385 参数(action_encoder 输入层 +5×64=320,head 输出层 +64+1=65),对 0.14M 的模型影响微乎其微。
|
||||
|
||||
### 2.3 单元测试
|
||||
|
||||
```
|
||||
$ python test_actpred.py
|
||||
|
||||
Ran 17 tests in 0.060s
|
||||
OK
|
||||
```
|
||||
|
||||
17 项测试全部通过,包括:
|
||||
- `test_forward_simple_mlp`(action_dim=18, output shape (2, 18))
|
||||
- `test_forward_simple_gru`(action_dim=18, output shape (2, 18))
|
||||
- `test_default_config`(assert action_dim == 18)
|
||||
- `test_forward_dynamic_batch`(action_dim=4, 未改,测试不同维度兼容性)
|
||||
|
||||
---
|
||||
|
||||
## 三、.mt 脚本解析器测试
|
||||
|
||||
### 3.1 解析器实现
|
||||
|
||||
`mt_parser.py` 实现了轻量级 `.mt` 脚本 `@Action` 矩阵解析器:
|
||||
|
||||
1. 正则提取 `@Action ... @` 块(先去除 `#` 注释行,避免注释中的 `@Action` 干扰)
|
||||
2. 按行解析,去除行尾 `;`,按空白分割列
|
||||
3. 第一行为表头(列名如 `s1.A`, `s2.A`),跳过
|
||||
4. `-` 占位符前向填充(沿用上一行同列值)
|
||||
5. 返回 `(n_data_rows, n_cols)` float32 矩阵
|
||||
|
||||
### 3.2 解析结果
|
||||
|
||||
从 `../bot4cj/scripts/` 目录解析到 13 个 `@Action` 矩阵:
|
||||
|
||||
| 文件 | shape | 列数 | 数据行 |
|
||||
|------|-------|------|--------|
|
||||
| jump.mt | (5, 12) | 12 | 5 |
|
||||
| left-hand.mt | (4, 12) | 12 | 4 |
|
||||
| left-turn.mt | (11, 12) | 12 | 11 |
|
||||
| lie-down.mt | (6, 12) | 12 | 6 |
|
||||
| rght-hand.mt | (4, 12) | 12 | 4 |
|
||||
| std.mt | (1, 12) | 12 | 1 |
|
||||
| stdd.mt | (7, 12) | 12 | 7 |
|
||||
| t0.mt | (10, 12) | 12 | 10 |
|
||||
| t1.mt | (1, 2) | 2 | 1 |
|
||||
| tilt-b.mt | (7, 12) | 12 | 7 |
|
||||
| tilt-f.mt | (7, 12) | 12 | 7 |
|
||||
| tilt-l.mt | (7, 12) | 12 | 7 |
|
||||
| tilt-r.mt | (7, 12) | 12 | 7 |
|
||||
|
||||
### 3.3 占位符填充验证
|
||||
|
||||
以 `jump.mt` 为例:
|
||||
|
||||
```
|
||||
原始(部分):
|
||||
45 - 45 - 45 - 45 - 90 90 10 200;
|
||||
- 10 - 10 - 10 - 10 - - - 20;
|
||||
|
||||
解析结果:
|
||||
row 0: [45. 0. 45. 0. 45. 0. 45. 0. 90. 90. 10. 200.] ← 首行 - 填 0
|
||||
row 1: [45. 10. 45. 10. 45. 10. 45. 10. 90. 90. 10. 20.] ← - 沿用上行
|
||||
```
|
||||
|
||||
前向填充正确。`-` 占位符被正确替换为上一行同列的值。
|
||||
|
||||
### 3.4 列数适配
|
||||
|
||||
`.mt` 脚本 12 列 vs `action_dim=18`:解析器自动补零至 18 列,不影响训练。`t1.mt`(2 列)同样补零至 18 列。
|
||||
|
||||
---
|
||||
|
||||
## 四、预训练测试
|
||||
|
||||
### 4.1 训练配置
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 数据来源 | `../bot4cj/scripts/*.mt` (13 文件) |
|
||||
| 总样本数 | 22(history_len=5 滑动窗口) |
|
||||
| 训练集 | 19 样本(85%) |
|
||||
| 验证集 | 3 样本(15%) |
|
||||
| 模型 | ActionPredictor (SimpleCNN + MLP, 142,034 参数) |
|
||||
| 输入 | 零图像占位 (1, 3, 64, 64) + action_history (1, 5, 18) |
|
||||
| 损失函数 | MSE Loss |
|
||||
| 优化器 | AdamW (lr=0.001, weight_decay=1e-4) |
|
||||
| 学习率调度 | CosineAnnealingLR (T_max=200) |
|
||||
| 数据增强 | Gaussian noise (std=1.0) |
|
||||
| batch_size | 8 |
|
||||
| 最大轮数 | 200 |
|
||||
| 早停耐心值 | 30 |
|
||||
| 随机种子 | 42 |
|
||||
|
||||
### 4.2 训练过程
|
||||
|
||||
| Epoch | train_loss | val_loss | 备注 |
|
||||
|-------|-----------|----------|------|
|
||||
| 1 | 3340.79 | 2125.46 | 初始 loss |
|
||||
| 7 | 2106.50 | 1051.22 | 快速下降阶段 |
|
||||
| 12 | 1404.49 | 917.81 | |
|
||||
| 20 | ~300 | ~400 | 进入收敛区 |
|
||||
| 48 | 171.64 | 228.02 | |
|
||||
| 63 | 124.30 | **190.58** | 最优 checkpoint |
|
||||
| 93 | 92.29 | 223.89 | 早停触发(patience=30) |
|
||||
|
||||
### 4.3 训练结果
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| 最终轮数 | 93(早停) |
|
||||
| 最优 val_loss | 190.58(epoch 63) |
|
||||
| val_loss 下降幅度 | 2125 → 190(-91%) |
|
||||
| RMSE | √190 ≈ 13.8 |
|
||||
| 训练耗时 | ~2-3 分钟(CPU) |
|
||||
|
||||
### 4.4 checkpoint 文件
|
||||
|
||||
| 文件 | 大小 | 说明 |
|
||||
|------|------|------|
|
||||
| `checkpoints/pretrain_best.pt` | 1.7 MB | 含 optimizer state,可恢复训练 |
|
||||
| `checkpoints/pretrain_best_inference.pt` | 572 KB | 仅模型权重 + config,直接推理用 |
|
||||
|
||||
### 4.5 checkpoint 兼容性验证
|
||||
|
||||
预训练 checkpoint 可被 `ActionPredictorInference` 直接加载:
|
||||
|
||||
```python
|
||||
predictor = ActionPredictorInference("./checkpoints/pretrain_best_inference.pt")
|
||||
# action_dim=18, history_len=5, backbone=simple, encoder=mlp
|
||||
# output shape: (1, 18)
|
||||
```
|
||||
|
||||
与现有 `predict.py` 推理管线、`export_onnx.py` ONNX 导出完全兼容。
|
||||
|
||||
---
|
||||
|
||||
## 五、推理性能基准
|
||||
|
||||
### 5.1 测试方法
|
||||
|
||||
- 模型:预训练 checkpoint(`pretrain_best_inference.pt`)
|
||||
- 输入:随机图像 (64, 64, 3) uint8 + 随机 history (5, 18) float32
|
||||
- warmup:10 次推理后计时
|
||||
- 测量:1000-2000 次取平均
|
||||
|
||||
### 5.2 结果
|
||||
|
||||
| 环节 | 耗时 (ms) | FPS | 占比 |
|
||||
|------|-----------|-----|------|
|
||||
| **端到端推理(含预处理)** | **1.61** | **622** | 100% |
|
||||
| 纯前向(含 CNN) | 1.09 | 916 | 100% |
|
||||
| ├ image_encoder (SimpleCNN) | 0.82 | — | 75% |
|
||||
| └ action_encoder + FusionHead | 0.27 | — | 25% |
|
||||
|
||||
### 5.3 性能评估
|
||||
|
||||
- 622 FPS 远超 bot4cj 控制循环所需的 30 FPS(20 倍余量)
|
||||
- 瓶颈在 image_encoder(75%),但 SimpleCNN 仅 98K 参数,进一步优化收益有限
|
||||
- 预处理(numpy → tensor + resize)仅占 0.52ms
|
||||
|
||||
---
|
||||
|
||||
## 六、PyTorch NPU / 向量计算单元支持现状
|
||||
|
||||
### 6.1 原生支持
|
||||
|
||||
| 硬件 | 支持程度 | 说明 |
|
||||
|------|----------|------|
|
||||
| x86 AVX2 | ✅ 最低要求 | PyTorch 2.x 起强制,不支持的 CPU 回退到非向量化代码 |
|
||||
| x86 AVX-512 | ✅ oneDNN 后端 | Intel Xeon Scalable+,`torch.compile` 自动利用 |
|
||||
| x86 AMX | ✅ oneDNN 后端 | Intel Sapphire Rapids+,矩阵乘法加速 |
|
||||
| ARM NEON | ✅ 长期支持 | 树莓派/手机端 |
|
||||
| ARM SVE/SVE2 | ✅ PyTorch 2.9+ | 2025 年新增,向量化长度自适应 |
|
||||
| Intel iGPU (XPU) | ✅ PyTorch 2.5+ | `device="xpu"` |
|
||||
|
||||
### 6.2 间接支持(需格式转换)
|
||||
|
||||
| 硬件 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| Intel NPU | PyTorch → ONNX → OpenVINO | Core Ultra 系列内置 NPU |
|
||||
| Apple ANE | PyTorch → CoreML | M1/M2/M3 芯片神经引擎 |
|
||||
| Qualcomm Hexagon | PyTorch → ExecuTorch | 手机端 NPU |
|
||||
| ARM SME2 | PyTorch → ExecuTorch + KleidiAI | ARMv9 旗舰核 |
|
||||
|
||||
### 6.3 当前环境分析
|
||||
|
||||
测试机器 CPU 为 Intel Xeon E3-1200 v2(Ivy Bridge, 2012 年):
|
||||
- 支持 AVX(128bit),不支持 AVX2(需 Haswell+)
|
||||
- PyTorch 可能回退到非向量化代码
|
||||
- 无独立 GPU,无 NPU
|
||||
|
||||
### 6.4 可行的加速路径
|
||||
|
||||
| 路径 | 预估推理 | 说明 |
|
||||
|------|---------|------|
|
||||
| 当前(PyTorch CPU) | 1.61 ms (622 FPS) | 可能未启用向量化 |
|
||||
| `torch.compile` | ~1.0 ms (~1000 FPS) | 自动融合 + oneDNN |
|
||||
| ONNX → OpenVINO | ~0.5 ms (~2000 FPS) | 自动利用 AVX2/AVX-512/AMX/NPU |
|
||||
| 换 AVX2 CPU | ~0.5 ms (~2000 FPS) | 任何 Haswell(2013)+ CPU |
|
||||
|
||||
### 6.5 结论
|
||||
|
||||
622 FPS 对 bot4cj 的 30 FPS 控制循环已有 20 倍余量,推理加速优先级低。加速投入应留给训练环节(微调阶段 batch_size=32 + MobileNetV2 2.4M 参数时 CPU 较慢)。
|
||||
|
||||
---
|
||||
|
||||
## 七、结论与下一步
|
||||
|
||||
### 7.1 测试结论
|
||||
|
||||
| 测试项 | 结果 |
|
||||
|--------|------|
|
||||
| action_dim=18 参数变更 | ✅ 通过(17 项单元测试全通过) |
|
||||
| .mt 解析器 | ✅ 通过(13 文件,22 样本,占位符正确) |
|
||||
| 预训练 | ✅ 通过(val_loss 2125→190,91% 下降) |
|
||||
| checkpoint 兼容性 | ✅ 通过(与现有推理管线完全兼容) |
|
||||
| 推理性能 | ✅ 通过(622 FPS,20 倍余量) |
|
||||
|
||||
### 7.2 已知局限
|
||||
|
||||
1. **数据量极少**:22 个样本(19 训练 / 3 验证),预训练仅学到基本的动作序列模式,val_loss 190(RMSE ≈ 14)仍有优化空间
|
||||
2. **列数不匹配**:.mt 脚本 12 列,action_dim=18,补零的 6 列在训练中贡献零梯度
|
||||
3. **无图像预训练**:image_encoder 在零图像上训练,学到的是常数映射,微调时需要重新训练
|
||||
|
||||
### 7.3 下一步建议
|
||||
|
||||
1. **用真实图像微调**:收集 bot4cj 运行时摄像头图像 + 动作序列数据,以预训练 checkpoint 为起点微调
|
||||
2. **增加 .mt 脚本**:编写更多动作脚本(当前仅 13 个),扩充预训练数据量
|
||||
3. **列数对齐**:扩展 .mt 脚本至 18 列(增加 6 列预留通道),或调整 action_dim 为 12
|
||||
4. **ONNX 导出验证**:测试 `export_onnx.py` 对预训练 checkpoint 的导出兼容性
|
||||
5. **bot4cj 集成**:修复 bot4cj 审计 P0 项(S01/S02/S07),打通 actpred → bot4cj 的动作执行闭环
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:训练曲线关键节点
|
||||
|
||||
```
|
||||
val_loss
|
||||
2100 │●
|
||||
│ ╲
|
||||
1800 │ ●
|
||||
│ ╲
|
||||
1500 │ ●
|
||||
│ ╲
|
||||
1200 │ ●
|
||||
│ ╲
|
||||
900 │ ●
|
||||
│ ╲___
|
||||
600 │ ●●
|
||||
│ ●●●
|
||||
300 │ ●●●●___
|
||||
│ ●●●●●●●●●●●●●
|
||||
100 │ ●●●●●●●●●●●●●●
|
||||
└──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──→ epoch
|
||||
10 20 30 40 50 60 70 80 90
|
||||
↑
|
||||
epoch 63: best (190.58)
|
||||
```
|
||||
|
||||
## 附录 B:文件清单
|
||||
|
||||
| 文件 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `actpred/config.py` | 修改 | action_dim 默认值 8 → 18 |
|
||||
| `actpred/mt_parser.py` | 新增 | .mt @Action 矩阵解析器 |
|
||||
| `actpred/pretrain_mt.py` | 新增 | .mt 脚本预训练入口 |
|
||||
| `actpred/_run.sh` | 修改 | 新增 pretrain 子命令 |
|
||||
| `actpred/README.md` | 修改 | 新增预训练文档 |
|
||||
| `actpred/test_actpred.py` | 修改 | 测试断言更新为 18 |
|
||||
| `actpred/checkpoints/pretrain_best.pt` | 生成 | 预训练 checkpoint(含 optimizer) |
|
||||
| `actpred/checkpoints/pretrain_best_inference.pt` | 生成 | 预训练 checkpoint(推理用) |
|
||||
Reference in new issue
Block a user