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:
co-authored by
Claude Opus 5
parent
e4bd794025
commit
e581d9b3d7
@@ -46,3 +46,40 @@ def test_project_configs_are_loadable():
|
||||
assert cfg["model"]["name"] == "defect_net"
|
||||
assert cfg["data"]["train"]["dataset"]["name"] == "film_defect"
|
||||
assert len(cfg["classes"]) > 0
|
||||
|
||||
|
||||
def test_replace_key_swaps_a_whole_subdict(tmp_path):
|
||||
"""换组件时要能整块替换,否则旧组件的专有字段会残留下来传给新组件。"""
|
||||
(tmp_path / "base.yaml").write_text(
|
||||
"backbone:\n name: tiny_conv\n widths: [32, 64]\n depths: [1, 2]\n block: dw\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "child.yaml").write_text(
|
||||
"_base_: ./base.yaml\nbackbone:\n _replace_: true\n name: torchvision\n arch: resnet18\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
cfg = load_config(tmp_path / "child.yaml")
|
||||
|
||||
assert cfg["backbone"] == {"name": "torchvision", "arch": "resnet18"}
|
||||
assert "_replace_" not in cfg["backbone"] # 标记本身不能漏进构造参数
|
||||
|
||||
|
||||
def test_without_replace_key_subdicts_still_merge(tmp_path):
|
||||
(tmp_path / "base.yaml").write_text("backbone:\n name: tiny_conv\n block: dw\n", encoding="utf-8")
|
||||
(tmp_path / "child.yaml").write_text(
|
||||
"_base_: ./base.yaml\nbackbone:\n block: res\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
cfg = load_config(tmp_path / "child.yaml")
|
||||
assert cfg["backbone"] == {"name": "tiny_conv", "block": "res"}
|
||||
|
||||
|
||||
def test_registry_reports_incompatible_fields_actionably():
|
||||
"""--set 只改 name 时旧字段会残留,报错要说清楚是哪些字段、该怎么办。"""
|
||||
import pytest
|
||||
|
||||
from models.registry import BACKBONES
|
||||
|
||||
with pytest.raises(TypeError, match=r"不接受的字段.*widths"):
|
||||
BACKBONES.build({"name": "torchvision", "arch": "resnet18", "widths": [32]})
|
||||
|
||||
@@ -3,6 +3,7 @@ import pytest
|
||||
torch = pytest.importorskip("torch")
|
||||
|
||||
from models import build_model # noqa: E402
|
||||
from models.backbones.resnet import _SUPPORTED as _SUPPORTED_ARCHS # noqa: E402
|
||||
from models.postprocess import decode_heatmap # noqa: E402
|
||||
|
||||
|
||||
@@ -90,3 +91,57 @@ def test_losses_decrease_on_a_single_batch(model_cfg):
|
||||
last = total
|
||||
|
||||
assert last < first
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arch", sorted(_SUPPORTED_ARCHS))
|
||||
def test_every_supported_torchvision_arch_actually_works(arch):
|
||||
"""清单里列出的每个骨架都必须真能前向,且声明的 out_channels 与实际一致。
|
||||
|
||||
回归测试:shufflenet_v2_x1_0 曾被列为支持,但它没有 `.features` 属性,
|
||||
构造时直接 AttributeError。
|
||||
"""
|
||||
from models.registry import BACKBONES
|
||||
|
||||
backbone = BACKBONES.build({"name": "torchvision", "arch": arch, "pretrained": False}).eval()
|
||||
with torch.no_grad():
|
||||
feat = backbone(torch.randn(1, 3, 64, 64))
|
||||
|
||||
assert feat.dim() == 4
|
||||
assert feat.shape[1] == backbone.out_channels == _SUPPORTED_ARCHS[arch]
|
||||
assert backbone.out_stride == 32
|
||||
|
||||
|
||||
def test_unsupported_arch_is_rejected():
|
||||
from models.registry import BACKBONES
|
||||
|
||||
with pytest.raises(ValueError, match="不支持的骨架"):
|
||||
BACKBONES.build({"name": "torchvision", "arch": "vit_b_16"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arch", ["resnet18", "shufflenet_v2_x1_0", "mobilenet_v3_small"])
|
||||
def test_alternative_backbones_drive_simclr(arch):
|
||||
"""换骨架后 SimCLR 的投影头要能接上(out_channels 必须准)。"""
|
||||
from models.heads.projection import ProjectionHead
|
||||
from models.registry import BACKBONES
|
||||
|
||||
backbone = BACKBONES.build({"name": "torchvision", "arch": arch, "pretrained": False}).eval()
|
||||
head = ProjectionHead(backbone.out_channels, 32, 16)
|
||||
with torch.no_grad():
|
||||
assert head(backbone(torch.randn(2, 3, 64, 64))).shape == (2, 16)
|
||||
|
||||
|
||||
def test_alternative_backbone_drives_defect_net():
|
||||
"""检测模型换骨架同样要能跑通(neck 的 in_channels 由 backbone 决定)。"""
|
||||
model = build_model(
|
||||
{
|
||||
"name": "defect_net",
|
||||
"num_classes": 3,
|
||||
"backbone": {"name": "torchvision", "arch": "shufflenet_v2_x1_0", "pretrained": False},
|
||||
"neck": {"name": "upsample", "channels": 16, "num_upsamples": 3},
|
||||
"head": {"hidden_channels": 16},
|
||||
}
|
||||
).eval()
|
||||
assert model.out_stride == 4
|
||||
with torch.no_grad():
|
||||
out = model(torch.randn(1, 3, 64, 64))
|
||||
assert out["heatmap"].shape == (1, 3, 16, 16)
|
||||
|
||||
Reference in New Issue
Block a user