Files
ModelTrainEval/models/backbones/resnet.py
T
Shaun ZhouandClaude Opus 5 e581d9b3d7 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>
2026-09-04 15:28:49 +08:00

90 lines
3.1 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.
"""torchvision 卷积骨架封装(resnet / mobilenet 等纯卷积模型)。"""
from __future__ import annotations
import torch
from torch import nn
from models.registry import BACKBONES
# 仅允许纯卷积骨架;ViT/Swin 等不在部署范围内。
_SUPPORTED = {
"resnet18": 512,
"resnet34": 512,
"resnet50": 2048,
"mobilenet_v3_small": 576,
"mobilenet_v3_large": 960,
"shufflenet_v2_x1_0": 1024,
}
@BACKBONES.register("torchvision")
class TorchvisionBackbone(nn.Module):
"""输出 stride=32 的单尺度特征。
Args:
arch: `_SUPPORTED` 中的模型名。
pretrained: 是否加载 ImageNet 权重(需要联网或本地缓存)。
in_channels: 输入通道,非 3 时会把第一层卷积权重按通道求均值复制。
frozen: 是否冻结骨架参数。
"""
def __init__(
self,
arch: str = "resnet18",
pretrained: bool = True,
in_channels: int = 3,
frozen: bool = False,
) -> None:
super().__init__()
if arch not in _SUPPORTED:
raise ValueError(f"不支持的骨架 {arch},可选 {sorted(_SUPPORTED)}")
import torchvision
weights = "DEFAULT" if pretrained else None
net = getattr(torchvision.models, arch)(weights=weights)
self.features = self._extract_features(net, arch)
if in_channels != 3:
self._patch_first_conv(in_channels)
self.out_channels = _SUPPORTED[arch]
self.out_stride = 32
if frozen:
for param in self.parameters():
param.requires_grad_(False)
@staticmethod
def _extract_features(net: nn.Module, arch: str) -> nn.Sequential:
"""取到最后一个卷积 stage 为止,丢掉分类头。
不同系列的 torchvision 模型结构不一样:resnet 是扁平的 children,
mobilenet 有现成的 `.features`,而 ShuffleNetV2 两者都不是——它把层
散放在 conv1/maxpool/stage2..4/conv5 上,得按顺序自己拼。
"""
if arch.startswith("resnet"):
return nn.Sequential(*list(net.children())[:-2])
if arch.startswith("shufflenet"):
return nn.Sequential(
net.conv1, net.maxpool, net.stage2, net.stage3, net.stage4, net.conv5
)
features = getattr(net, "features", None)
if features is None:
raise ValueError(f"不知道如何从 {arch} 取特征,请在 _extract_features 里补一条分支")
return features
def _patch_first_conv(self, in_channels: int) -> None:
for module in self.features.modules():
if isinstance(module, nn.Conv2d):
weight = module.weight.data.mean(dim=1, keepdim=True).repeat(1, in_channels, 1, 1)
module.in_channels = in_channels
module.weight = nn.Parameter(weight * (3.0 / in_channels))
return
raise RuntimeError("骨架中未找到 Conv2d,无法调整输入通道")
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.features(x)