如何在Python中提取PyTorch预训练模型的特征层_利用nn.Sequential进行模型切片
PyTorch新版预训练模型无统一features属性,需用nn.Sequential精准切片提取特征层。ResNet需将conv1至layer4逐段拼接为特征提取器;VGG16通过列表索引对features切片;EfficientNet注意MBConv嵌套结构。切片后必须手动调用eval()确保BatchNorm行为正常。
大家在使用PyTorch预训练模型做特征提取时,最常见的问题就是:直接用 model.features 结果报错了。我来梳理一下,这背后到底是怎么回事,以及正确做法是什么。
一句话总结:PyTorch新版的预训练模型,根本没有统一的 features 属性。旧版VGG或某些第三方封装里常见这个写法,但已经不再适用了。ResNet需要手动从conv1到layer4逐一拼接;VGG16虽然有features属性,但要切片还得靠列表索引;EfficientNet_b0虽然有features,但内部是MBConv嵌套结构,切片时需要格外小心,并且最后一定要记得加上eval()。

为什么直接用 model.features 会报错
原因很简单:像 resnet50、vgg16、efficientnet_b0 这些官方预训练模型,并没有统一的 features 属性。这个写法在 torchvision 0.12 以前的旧版VGG或某些第三方封装里确实存在,但新版中已经被移除了。直接访问,系统会直接给你一个 AttributeError: 'ResNet' object has no attribute 'features'。
所以,最可靠的方式,是先把模型结构搞清楚,定位到特征提取的终点(通常是最后一个全局池化层之前),然后用 nn.Sequential 精准地截取子模块。
resnet50的主干从model.layer1到model.layer4,后面是model.a vgpool和model.fc。特征层应该截到layer4的输出为止。vgg16的卷积部分在model.features(确实有这个属性!),但它包含所有卷积、激活和池化。你很可能只想截取到某一层,比如第29层(倒数第二个卷积)。efficientnet_b0也没有统一的features属性。虽然它的主干也叫model.features(这里确实有!),并且是nn.Sequential,可以直接索引,但内部模块命名不一致(比如包含Conv2d、SiLU、MBConv),操作起来需要多留个心眼。
用 nn.Sequential 切 resnet50:别漏掉 model.conv1 和 model.bn1
很多人只切 layer1 到 layer4,但ResNet的第一层是独立的 conv1 + bn1 + relu + maxpool,它们不属于任何 layerX,却是特征提取流程的关键部分。漏掉它们,会导致输入维度不匹配,甚至特征信息直接丢失。
正确的做法是把前四块全部打包在一起:
import torch.nn as nn
from torchvision.models import resnet50
model = resnet50(pretrained=True)
model.eval()
# 提取完整特征主干(不含 a vgpool 和 fc)
feature_extractor = nn.Sequential(
model.conv1,
model.bn1,
model.relu,
model.maxpool,
model.layer1,
model.layer2,
model.layer3,
model.layer4
)
注意,model.layer4 的输出是 [B, 2048, H, W]。如果你后续需要一个固定长度的向量,还得自己加 nn.AdaptiveA vgPool2d(1) 或者调用 F.adaptive_a vg_pool2d。
对 vgg16 做精细切片:用 list(model.features) 查索引
vgg16.features 是 nn.Sequential,但它内部是按顺序堆叠起来的模块列表,没有明确的层级名。想取“前20层”该怎么办?答案是把它转成Python列表,看清楚结构:
model = vgg16(pretrained=True) modules = list(model.features) print(len(modules)) # 输出 31 print(modules[0]) # Conv2d(3, 64, kernel_size=(3, 3), ...) print(modules[29]) # Conv2d(512, 512, kernel_size=(3, 3), ...)
几个常见的需求场景:
- 取backbone到最后一个卷积(不含最后两个
Conv2d+ReLU):用nn.Sequential(*modules[:29]) - 想保留空间分辨率(比如做检测或分割),避免最后的
MaxPool2d:用nn.Sequential(*modules[:-1])(去掉末尾的池化层) - 注意,
modules[28]是ReLU,modules[29]是Conv2d。这个顺序千万不能反,否则激活会在卷积之前,语义就完全错了。
切 efficientnet_b0 时小心 MBConv 和 nn.Sequential 嵌套
efficientnet_b0.features 看起来是一个扁平的 Sequential,但实际内部第1到第7层是 MBConv 模块,而每个 MBConv 自身又是一个 nn.Sequential。如果你直接索引 model.features[5],拿到的是整个 MBConv,无法进一步拆解。
如果你的需求只是截取到第5个 MBConv 的输出(也就是stage 3结束),那可以安全地这样切:
feature_extractor = nn.Sequential(*model.features[:6]) # 包含第0~5层(共6个模块)
但如果你想精确停在某个 MBConv 内部(比如只取它的depthwise卷积之后),那 nn.Sequential 切片就做不到了。这时必须重写该模块,或者使用 register_forward_hook。
还有一点要注意:efficientnet_b0 默认输入尺寸是224×224,它的stem(前两层)对小尺寸图像比较敏感。切片之后,如果输入变小(比如112×112),一定要确认前几层的stride是否会导致输出尺寸为0(比如 torch.Size([1, 32, 0, 0]))。
最后,提醒一个容易忽略的细节:所有切片后的模型,默认仍然处于 training=True 模式。即使原模型已经调过 eval(),切片后也务必手动调用 feature_extractor.eval(),否则BatchNorm和Dropout的行为会异常,导致特征很不稳定。


































