fix(backbones): 修复 shufflenet 无法构建,并让换骨架不再踩配置合并的坑

- shufflenet_v2_x1_0 列在支持清单里却直接 AttributeError:ShuffleNetV2 既不是
  扁平 children 也没有 .features,需要按 conv1/maxpool/stage2..4/conv5 自己拼。
  该 bug 同时影响 SimCLR 与检测模型
- 新增参数化测试覆盖清单里的每个 arch:能前向、声明的 out_channels 与实际一致
- 配置支持 `_replace_: true`:子字典整块替换而不与 base 合并。换骨架时
  tiny_conv 的 widths/depths 会残留并传给新骨架,此前无论用 --set 还是 _base_
  都绕不开,只能改原文件
- Registry.build 提前校验字段,报错直接指出是哪些字段不被接受、该怎么办,
  取代裸 TypeError
- README 补充可选骨架表与三种改法(含不该用 --set 改 name 的说明)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-04 15:28:49 +08:00
co-authored by Claude Opus 5
parent e4bd794025
commit e581d9b3d7
7 changed files with 204 additions and 8 deletions
+21 -3
View File
@@ -10,6 +10,7 @@ from typing import Any
import yaml
BASE_KEY = "_base_"
REPLACE_KEY = "_replace_"
class Config(dict):
@@ -53,16 +54,33 @@ class Config(dict):
def deep_merge(base: Mapping, override: Mapping) -> dict:
"""递归合并;override 中的标量/列表直接覆盖 base。"""
"""递归合并;override 中的标量/列表直接覆盖 base。
子字典默认是**合并**而不是替换,这在换组件时会出问题:把 backbone 从
`tiny_conv` 换成 `torchvision`,base 里 tiny_conv 专有的 widths/depths
会残留下来,传给新组件直接报错。这时在该字典里写 `_replace_: true`,
表示整块替换掉 base 的同名字典。
"""
merged = dict(base)
for key, value in override.items():
if key in merged and isinstance(merged[key], Mapping) and isinstance(value, Mapping):
if isinstance(value, Mapping) and value.get(REPLACE_KEY):
merged[key] = _strip_replace_key(value)
elif key in merged and isinstance(merged[key], Mapping) and isinstance(value, Mapping):
merged[key] = deep_merge(merged[key], value)
else:
merged[key] = value
merged[key] = _strip_replace_key(value) if isinstance(value, Mapping) else value
return merged
def _strip_replace_key(value: Mapping) -> dict:
"""去掉 `_replace_` 标记本身,避免它漏进组件的构造参数。"""
return {
k: (_strip_replace_key(v) if isinstance(v, Mapping) else v)
for k, v in value.items()
if k != REPLACE_KEY
}
def _load_raw(path: Path, seen: set[Path]) -> dict:
path = path.resolve()
if path in seen:
+25 -1
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import inspect
from collections.abc import Callable, Iterator
from typing import Any, TypeVar
@@ -38,7 +39,30 @@ class Registry:
name = cfg.pop("name", None)
if name is None:
raise KeyError(f"构建 {self.name} 需要 'name' 字段: {cfg}")
return self.get(name)(**{**cfg, **kwargs})
target = self.get(name)
params = {**cfg, **kwargs}
self._check_params(name, target, params)
return target(**params)
@staticmethod
def _check_params(name: str, target: Callable[..., Any], params: dict) -> None:
"""提前挡下不认识的字段,给出比裸 TypeError 更可操作的提示。
最常见的触发方式:用 `--set xxx.name=其他骨架` 只改了名字,
yaml 里原骨架的专有字段(如 tiny_conv 的 widths/depths)仍然留在配置里。
"""
signature = inspect.signature(target)
if any(p.kind is p.VAR_KEYWORD for p in signature.parameters.values()):
return
unknown = sorted(set(params) - set(signature.parameters))
if not unknown:
return
accepted = sorted(p for p in signature.parameters if p != "self")
raise TypeError(
f"构建 '{name}' 时收到它不接受的字段 {unknown}。它接受的是 {accepted}。"
"如果你是用 --set 改了 name,注意 yaml 里原来那个组件的专有字段仍会保留——"
"换组件请直接编辑配置文件、整块替换。"
)
def __contains__(self, name: object) -> bool:
return name in self._entries