Files
blackbird/actpred/config.py
T
qing 823e91a30e fix(actpred): 修复 HEF 帧归一化误判与部署图数值不等价,补齐 P3 审计项
- hailo_infer: 帧归一化判定改为量化网格 dequant(0)(旧启发式把归一化
  帧当原始像素喂入,图像路径饱和破坏;实机 max_diff 2.55→0.37,
  img 敏感性 1.44→22.26)
- export_deploy: pool kernel 由卷积真实输出尺寸探测(H//16 在 1080
  高度下静默丢弃末行);mobilenet backbone 给出明确报错
- mt_parser: @Action 为文件末位块时解析成功,不再静默丢数据
- config: checkpoint train 段未知键/非 dict 校验;init-from 兼容性
  警告助手(normalize/action_dim),train 与 pretrain 共用
- dataset: stats 文件形状校验;predict: 移除误导性 --action-dim
- dfc_compile: --frame-count 参数化(原硬编码 3 帧)
- gen_calibration: 旧模式校准图像改分块 memmap 生成(1080p 原一次性
  分配 ~600MB)
- _npu_test: torch 参考反归一化对齐后再与 HEF 比较
- 单测 81→90 全过;README 与测速报告更新为验证轮次数据
2026-08-13 15:22:34 +08:00

242 lines
10 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.
import argparse
from dataclasses import dataclass, asdict, field
from typing import Optional
@dataclass
class DataConfig:
image_width: int = 1920
image_height: int = 1080
action_dim: int = 18
history_len: int = 5
frame_len: int = 3
data_dir: str = "./data"
normalize_actions: bool = False
action_stats_path: Optional[str] = None
@dataclass
class ModelConfig:
cnn_backbone: str = "simple"
cnn_feature_dim: int = 128
fused_hidden_dim: int = 128
dropout: float = 0.1
attn_dim: int = 128
attn_heads: int = 4
attn_layers: int = 2
@dataclass
class TrainConfig:
# 默认输入分辨率 1920×1080,batch=32 在 CPU/Pi 上内存不可行;
# 高分辨率训练取小 batch,小分辨率可用 --batch-size 调大
batch_size: int = 4
epochs: int = 100
lr: float = 1e-3
weight_decay: float = 1e-4
val_split: float = 0.15
checkpoint_dir: str = "./checkpoints"
patience: int = 15
num_workers: int = 2
seed: int = 42
max_grad_norm: float = 1.0
@dataclass
class Config:
data: DataConfig = field(default_factory=DataConfig)
model: ModelConfig = field(default_factory=ModelConfig)
train: TrainConfig = field(default_factory=TrainConfig)
def to_dict(self):
return asdict(self)
@classmethod
def from_dict(cls, d):
return cls(
data=DataConfig(**d.get("data", {})),
model=ModelConfig(**d.get("model", {})),
train=TrainConfig(**d.get("train", {})),
)
def build_argparser(description: str = "") -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=description)
parser.add_argument("--image-width", type=int, default=None)
parser.add_argument("--image-height", type=int, default=None)
parser.add_argument("--action-dim", type=int, default=None)
parser.add_argument("--history-len", type=int, default=None)
parser.add_argument("--frame-len", type=int, default=None)
parser.add_argument("--data-dir", type=str, default=None)
parser.add_argument("--normalize-actions", action=argparse.BooleanOptionalAction, default=None)
parser.add_argument("--action-stats-path", type=str, default=None)
parser.add_argument("--backbone", type=str, default=None, choices=["simple", "mobilenet"])
parser.add_argument("--cnn-feature-dim", type=int, default=None)
parser.add_argument("--fused-hidden-dim", type=int, default=None)
parser.add_argument("--dropout", type=float, default=None)
parser.add_argument("--attn-dim", type=int, default=None)
parser.add_argument("--attn-heads", type=int, default=None)
parser.add_argument("--attn-layers", type=int, default=None)
parser.add_argument("--batch-size", type=int, default=None)
parser.add_argument("--epochs", type=int, default=None)
parser.add_argument("--lr", type=float, default=None)
parser.add_argument("--weight-decay", type=float, default=None)
parser.add_argument("--val-split", type=float, default=None)
parser.add_argument("--checkpoint-dir", type=str, default=None)
parser.add_argument("--patience", type=int, default=None)
parser.add_argument("--num-workers", type=int, default=None)
parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--max-grad-norm", type=float, default=None)
parser.add_argument("--init-from", type=str, default=None,
help="预训练 checkpoint 路径,作为微调起点(strict=False 加载兼容权重)")
parser.add_argument("--resume", type=str, default=None,
help="从 last.pt 断点续训(恢复 model/optimizer/scheduler/epoch)")
parser.add_argument("--save-config", type=str, default=None, help="保存配置到 JSON 文件")
return parser
def validate_config_values(data_cfg: DataConfig, model_cfg: ModelConfig,
train_cfg: TrainConfig = None):
"""校验从 checkpoint 恢复或 CLI 传入的配置值域合理性
防止恶意/损坏 checkpoint 注入异常值(如超大维度导致 OOM),
也防止训练 CLI 越界参数(如 --history-len 0 / --lr -1)静默进入训练。
"""
checks = [
("image_width", data_cfg.image_width, 1, 4096, int),
("image_height", data_cfg.image_height, 1, 4096, int),
("history_len", data_cfg.history_len, 1, 1024, int),
("frame_len", data_cfg.frame_len, 1, 1024, int),
("action_dim", data_cfg.action_dim, 1, 1024, int),
("cnn_feature_dim", model_cfg.cnn_feature_dim, 1, 4096, int),
("fused_hidden_dim", model_cfg.fused_hidden_dim, 1, 4096, int),
("dropout", model_cfg.dropout, 0.0, 1.0, float),
("attn_dim", model_cfg.attn_dim, 1, 4096, int),
("attn_heads", model_cfg.attn_heads, 1, 64, int),
("attn_layers", model_cfg.attn_layers, 1, 64, int),
]
for name, val, lo, hi, typ in checks:
if typ is int:
ok = isinstance(val, int) and not isinstance(val, bool) and lo <= val <= hi
else:
ok = isinstance(val, (int, float)) and not isinstance(val, bool) and lo <= val <= hi
if not ok:
raise ValueError(
f"checkpoint config {name}={val} 超出合理范围 [{lo}, {hi}]")
if model_cfg.attn_dim % model_cfg.attn_heads != 0:
raise ValueError(
f"attn_dim={model_cfg.attn_dim} 必须能被 attn_heads="
f"{model_cfg.attn_heads} 整除")
if train_cfg is not None:
t_checks = [
("batch_size", train_cfg.batch_size, 1, 65536, int),
("epochs", train_cfg.epochs, 1, 100000, int),
("lr", train_cfg.lr, 1e-9, 10.0, float),
("weight_decay", train_cfg.weight_decay, 0.0, 1.0, float),
("val_split", train_cfg.val_split, 0.0, 1.0, float),
("patience", train_cfg.patience, 0, 100000, int),
("num_workers", train_cfg.num_workers, 0, 256, int),
("max_grad_norm", train_cfg.max_grad_norm, 0.0, 1e6, float),
]
for name, val, lo, hi, typ in t_checks:
if typ is int:
ok = isinstance(val, int) and not isinstance(val, bool) and lo <= val <= hi
else:
ok = isinstance(val, (int, float)) and not isinstance(val, bool) and lo <= val <= hi
if not ok:
raise ValueError(
f"配置 {name}={val} 超出合理范围 [{lo}, {hi}]")
def warn_init_from_mismatch(init_cfg: dict, current: Config):
"""--init-from 加载预训练权重前的兼容性警告(train.py / pretrain_mt.py 共用)
normalize_actions 不一致时输入分布与权重不匹配,预训练收益大打折扣;
action_dim 不一致时 act_proj 无法加载对应权重(strict=False 静默跳过,
该层随机初始化)。
"""
if not isinstance(init_cfg, dict):
return
init_data = init_cfg.get("data") or {}
init_norm = init_data.get("normalize_actions", False)
if init_norm != current.data.normalize_actions:
print(f"WARNING: init-from checkpoint 的 normalize_actions="
f"{init_norm} 与当前运行 {current.data.normalize_actions} "
f"不一致,输入分布不匹配会损失预训练收益(见 README 预训练说明)")
init_dim = init_data.get("action_dim")
if init_dim is not None and init_dim != current.data.action_dim:
print(f"WARNING: init-from checkpoint 的 action_dim={init_dim} "
f"与当前运行 {current.data.action_dim} 不一致,"
f"act_proj 权重将随机初始化")
def parse_checkpoint_config(cfg_dict: dict) -> Config:
"""从 checkpoint 恢复 Config,校验键集合与值域
- 顶层缺 data/model 键 → ValueError(带明确信息)
- data/model/train 段含未知键 → ValueError(列出未知键,避免裸 TypeError)
- 值域非法 → ValueError(复用 validate_config_values)
"""
if not isinstance(cfg_dict, dict):
raise ValueError(f"checkpoint config 应为 dict,实际为 {type(cfg_dict).__name__}")
section_dataclasses = {
"data": DataConfig,
"model": ModelConfig,
"train": TrainConfig,
}
for section, dataclass in section_dataclasses.items():
if section not in cfg_dict:
if section in ("data", "model"):
raise ValueError(f"checkpoint config 缺少 '{section}' 段")
continue
section_dict = cfg_dict[section]
if not isinstance(section_dict, dict):
raise ValueError(
f"checkpoint config['{section}'] 应为 dict,"
f"实际为 {type(section_dict).__name__}")
unknown = set(section_dict) - set(dataclass.__dataclass_fields__)
if unknown:
raise ValueError(
f"checkpoint config['{section}'] 含未知键: {sorted(unknown)}")
cfg = Config.from_dict(cfg_dict)
validate_config_values(cfg.data, cfg.model, cfg.train)
return cfg
def apply_overrides(cfg: Config, args: argparse.Namespace) -> Config:
mapping = {
"image_width": ("data", "image_width"),
"image_height": ("data", "image_height"),
"action_dim": ("data", "action_dim"),
"history_len": ("data", "history_len"),
"frame_len": ("data", "frame_len"),
"data_dir": ("data", "data_dir"),
"normalize_actions": ("data", "normalize_actions"),
"action_stats_path": ("data", "action_stats_path"),
"backbone": ("model", "cnn_backbone"),
"cnn_feature_dim": ("model", "cnn_feature_dim"),
"fused_hidden_dim": ("model", "fused_hidden_dim"),
"dropout": ("model", "dropout"),
"attn_dim": ("model", "attn_dim"),
"attn_heads": ("model", "attn_heads"),
"attn_layers": ("model", "attn_layers"),
"batch_size": ("train", "batch_size"),
"epochs": ("train", "epochs"),
"lr": ("train", "lr"),
"weight_decay": ("train", "weight_decay"),
"val_split": ("train", "val_split"),
"checkpoint_dir": ("train", "checkpoint_dir"),
"patience": ("train", "patience"),
"num_workers": ("train", "num_workers"),
"seed": ("train", "seed"),
"max_grad_norm": ("train", "max_grad_norm"),
}
args_dict = vars(args)
for arg_name, (section, field) in mapping.items():
val = args_dict.get(arg_name)
if val is not None:
setattr(getattr(cfg, section), field, val)
return cfg