fix(configs): 修复 resnet18 训练配置无法构建,并补上对应的评估配置

- configs/train/resnet18.yaml 此前直接报错:它同时继承 tiny_conv 与 resnet18 的模型
  配置,深度合并后 backbone 混了两边字段。在 configs/model/resnet18.yaml 的 backbone
  上加 _replace_: true 整块替换
- _replace_ 标记此前不生效:_load_raw 对每个 base 都会先 deep_merge({}, data),
  标记在参与替换判断之前就被剥掉了。改为合并期间保留、load_config 末尾统一剥除
- 新增 configs/eval/resnet18.yaml:评估必须与训练用同一套模型配置,否则构建出的
  骨架与权重对不上,而此前只有 baseline 一个评估配置
- 新增参数化测试:仓库里每个配置都要能真的构建出组件(片段自动跳过),
  且训练/评估配置对的 model 段必须一致
- README 补充骨架与配置对的对应表

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-04 15:42:14 +08:00
co-authored by Claude Opus 5
parent e581d9b3d7
commit 47fb0f1125
5 changed files with 99 additions and 9 deletions
+51
View File
@@ -1,3 +1,5 @@
from pathlib import Path
import pytest
pytest.importorskip("yaml")
@@ -83,3 +85,52 @@ def test_registry_reports_incompatible_fields_actionably():
with pytest.raises(TypeError, match=r"不接受的字段.*widths"):
BACKBONES.build({"name": "torchvision", "arch": "resnet18", "widths": [32]})
@pytest.mark.parametrize(
"config_path",
sorted(str(p) for p in Path("configs").rglob("*.yaml")),
)
def test_every_shipped_config_builds_its_components(config_path):
"""仓库里每个配置都必须能真的构建出组件。
回归测试:configs/train/resnet18.yaml 曾经无法使用——它同时继承 tiny_conv 与
resnet18 的模型配置,合并后 backbone 混了两边的字段,构建时直接报错。
"""
torch = pytest.importorskip("torch")
from datasets.yolo import load_classes
from models import build_model
from models.registry import BACKBONES
cfg = load_config(config_path)
if "model" in cfg:
if "classes" not in cfg:
# configs/model/*.yaml 是片段,只能被 _base_ 组合,单独用没有 classes
pytest.skip(f"{config_path} 是配置片段,不含 classes")
model_cfg = dict(cfg["model"])
model_cfg.setdefault("num_classes", len(load_classes(cfg["classes"])))
# 单测里不下载 ImageNet 权重
if model_cfg.get("backbone", {}).get("name") == "torchvision":
model_cfg["backbone"] = {**model_cfg["backbone"], "pretrained": False}
model = build_model(model_cfg)
with torch.no_grad():
out = model(torch.randn(1, 3, 64, 64))
assert "heatmap" in out
simclr_backbone = cfg.get_path("simclr.backbone")
if simclr_backbone:
backbone_cfg = dict(simclr_backbone)
if backbone_cfg.get("name") == "torchvision":
backbone_cfg["pretrained"] = False
backbone = BACKBONES.build(backbone_cfg)
assert backbone.out_channels > 0
def test_eval_config_matches_its_training_backbone():
"""评估配置构建出的骨架必须与训练配置一致,否则权重根本加载不上。"""
for train_cfg, eval_cfg in [
("configs/train/baseline.yaml", "configs/eval/baseline.yaml"),
("configs/train/resnet18.yaml", "configs/eval/resnet18.yaml"),
]:
assert load_config(train_cfg)["model"] == load_config(eval_cfg)["model"], eval_cfg