Files
blackbird/actpred/gen_calibration.py
T
qing 52c0ade3c4 fix(actpred): 定位 HEF 输入不敏感根因,补齐归一化预训练链路
根因:预训练全零图像占位使 BN running_var 停在初始小值,推理时
特征放大至 ±1e6,DFC 在 token merge 点按最大幅度分配共享量化
scale,pred_token/act_tokens 全部被压成 0,HEF 输出对任何输入
恒定(详见 docs/reports/actpred_hef_insensitivity_rootcause_20260813.md)

- export_deploy.py:新增 check_token_ranges 导出前 token 量级检查
  (|img|>100 中止导出并提示微调;act/img 比例过大警告;归一化
  模型在 z 空间采样,避免零方差列爆炸)
- pretrain_mt.py:新增 --random-images(ImageNet 归一化噪声占位,
  恢复 BN 统计)、--init-from(微调起点,含动作空间一致性警告)、
  --normalize-actions 完整支持(训练集 stats 计算与保存、验证集复用)
- gen_calibration.py:归一化模型的校准集自动 z-score 动作历史
- _onnx_validate.py / _npu_test.py:归一化模型适配(torch 参考
  z-score、HailoPredictor 传 stats_path)
- README:预训练章节补充新参数与 NPU 部署警告
- 单测 76/76 通过;冒烟验证:1 epoch 噪声微调后 |img| 2.46e5→8,
  归一化后 |act| 694→2.9
2026-08-13 13:57:38 +08:00

165 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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.
"""生成 DFC 量化校准数据集
两种模式:
默认模式(data/calibration.npz):为旧版 dfc_compile.py 的 ClientModel
流程准备,目前保留兼容。
--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) 学习值 ± 抖动
(常量校准会使量化范围退化,见 --pred-jitter)。
图像分辨率与 --model checkpoint 的配置一致。
数据源:
- 图像:无真实 episode 时用 [0,255] 均匀采样像素 + ImageNet 归一化
- 动作历史:从 bot4cj/scripts/*.mt 的真实 @Action 矩阵滑窗采样
(列不足 18 补零,与 pretrain_mt.py 语义一致)
真实数据到位后应替换为真实帧重新校准。
用法:
python gen_calibration.py --deploy --model checkpoints/pretrain_best_inference.pt
"""
import argparse
import os
import numpy as np
from config import DataConfig, parse_checkpoint_config
from mt_parser import parse_mt_directory
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)
def build_action_history_samples(mt_dir, n_samples, history_len, action_dim, rng):
sequences = []
for _path, matrix in parse_mt_directory(mt_dir):
n_cols = matrix.shape[1]
if n_cols < action_dim:
matrix = np.pad(matrix, ((0, 0), (0, action_dim - n_cols)))
elif n_cols > action_dim:
matrix = matrix[:, :action_dim]
if len(matrix) >= history_len:
sequences.append(matrix)
if not sequences:
raise ValueError(f"{mt_dir} 中没有可用的 @Action 序列")
windows = []
for matrix in sequences:
for i in range(len(matrix) - history_len + 1):
windows.append(matrix[i:i + history_len])
idx = rng.integers(0, len(windows), size=n_samples)
return np.stack([windows[i] for i in idx]).astype(np.float32)
def build_image_samples(n_samples, frame_len, height, width, rng):
# 存 uint8 原始像素值(0~255),加载时再做 ToTensor+Normalize,节省磁盘
return rng.integers(0, 256,
size=(n_samples, frame_len, 3, height, width),
dtype=np.uint8)
def build_deploy_set(args):
import torch
from model import ActionPredictor
ckpt = torch.load(args.model, map_location="cpu", weights_only=True)
cfg = parse_checkpoint_config(ckpt["config"]).data
rng = np.random.default_rng(args.seed)
h, w = cfg.image_height, cfg.image_width
raw = rng.integers(0, 256, size=(args.samples, h, w, 3),
dtype=np.uint8).astype(np.float32) / 255.0
norm = ((raw - IMAGENET_MEAN) / IMAGENET_STD).astype(np.float32)
frames = [norm.copy() for _ in range(cfg.frame_len)]
history = build_action_history_samples(
args.mt_dir, args.samples, cfg.history_len, cfg.action_dim, rng)
history = history[:, None, :, :] # (N, 1, H_len, D)
if cfg.normalize_actions:
# 归一化模型的 HEF 输入是 z-score 历史(hailo_infer --stats 同语义),
# 校准数据必须走同一变换,否则量化范围与推理分布不匹配
stats_path = cfg.action_stats_path
if not stats_path:
stats_path = os.path.join(
os.path.dirname(os.path.abspath(args.model)),
"action_stats.npy")
elif not os.path.isabs(stats_path):
stats_path = os.path.join(
os.path.dirname(os.path.abspath(args.model)), stats_path)
if not os.path.exists(stats_path):
raise FileNotFoundError(
f"模型训练时启用了动作归一化,但未找到统计文件: {stats_path}")
stats = np.load(stats_path)
if stats.shape != (2, cfg.action_dim):
raise ValueError(
f"action stats 形状应为 (2, {cfg.action_dim}),"
f"实际为 {stats.shape}: {stats_path}")
history = (history - stats[0]) / stats[1]
print(f" action_history: 已 z-score 归一化({stats_path})")
model = ActionPredictor(parse_checkpoint_config(ckpt["config"]).model,
cfg, pretrained=False)
model.load_state_dict(ckpt["model_state_dict"])
d = model.pred_token.shape[-1]
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,
**{f"actpred_deploy/input_layer{i + 1}": arr
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}"
f"(学习值 ±{args.pred_jitter:.0%} 抖动)")
def main():
parser = argparse.ArgumentParser(description="Generate DFC calibration dataset")
parser.add_argument("--deploy", action="store_true",
help="生成部署版(export_deploy.py)校准集")
parser.add_argument("--model", default="checkpoints/pretrain_best_inference.pt",
help="--deploy 模式的 checkpoint(决定分辨率与 pred_token)")
parser.add_argument("--mt-dir", default="../bot4cj/scripts")
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:
args.output = args.output or "data/calibration_deploy.npz"
build_deploy_set(args)
return
args.output = args.output or "data/calibration.npz"
cfg = DataConfig()
rng = np.random.default_rng(args.seed)
history = build_action_history_samples(
args.mt_dir, args.samples, cfg.history_len, cfg.action_dim, rng)
images = build_image_samples(
args.samples, cfg.frame_len, cfg.image_height, cfg.image_width, rng)
np.savez_compressed(args.output, images=images, action_history=history)
print(f"校准集已保存: {args.output}")
print(f" images: {images.shape} dtype={images.dtype}")
print(f" action_history: {history.shape} dtype={history.dtype}")
if __name__ == "__main__":
main()