DAY 50: 预训练模型与 CBAM 模块的融合与微调
今天,我们将把之前学到的知识融会贯通,探讨如何将 CBAM 这样的注意力模块应用到强大的预训练模型(如 ResNet)中,并学习如何高效地对这些模型进行微调,以适应我们自己的任务。
知识点回顾
- ResNet 结构解析:深入理解 ResNet 的核心思想——残差连接,并剖析其经典模型 ResNet18 的具体结构。
- CBAM 放置位置的思考:探讨在 ResNet 这类复杂结构中,将 CBAM 模块放置在何处才能最大化其效果。
- 针对预训练模型的训练策略:学习两种高级微调(Fine-tuning)技巧:
- 差异化学习率 (Differential Learning Rates)
- 三阶段微调 (Progressive Unfreezing)
1. ResNet18 模型结构解析
在深入研究如何修改模型之前,我们必须先透彻理解其内部构造。以 ResNet18 为例,它的成功主要归功于解决了深度网络训练中的一个关键问题:网络退化。
1.1 核心思想:残差学习 (Residual Learning)
随着网络层数的增加,模型的性能非但没有提升,反而出现了下降,这被称为“退化”现象。ResNet 的作者提出,让网络直接学习输入与输出之间的残差 (Residual),会比学习完整的输出映射更容易。
这就是跳跃连接 (Skip Connection) 的由来:将输入 x
直接加到网络层的输出 F(x)
上,得到最终结果 H(x) = F(x) + x
。这样,如果某一网络层 F(x)
发现自己是多余的,它只需将自己的权重学习为0,输出就直接等于输入 x
,实现了恒等映射,保证了网络性能不会因为加深而退化。
1.2 ResNet18 的基本构建块 (BasicBlock)
ResNet18 由多个 BasicBlock
堆叠而成。每个 BasicBlock
包含:
- 两个 3x3 的卷积层。
- 每个卷积层后接一个批归一化 (BatchNorm) 和 ReLU 激活函数。
- 一个跳跃连接,将输入
x
与第二个卷积层的输出相加。 - 如果输入的通道数或尺寸与输出不匹配,跳跃连接会通过一个 1x1 的卷积 (downsample) 来进行适配。
1.3 ResNet18 整体结构
ResNet18 的结构可以清晰地分为几个部分:
- 初始卷积层 (conv1):一个 7x7 的大卷积核,用于初步提取图像的宏观特征,并进行第一次下采样。
- 四个残差层 (layer1-4):由多个
BasicBlock
组成。每个layer
的第一个BasicBlock
可能会进行下采样(步长为2),以减小特征图尺寸并加倍通道数。layer1
: 2个 BasicBlock, 64通道layer2
: 2个 BasicBlock, 128通道layer3
: 2个 BasicBlock, 256通道layer4
: 2个 BasicBlock, 512通道
- 全局平均池化 (avgpool):将最后的特征图在空间维度上进行平均,得到一个向量。
- 全连接分类层 (fc):根据任务类别数进行最终的分类。
2. CBAM 放置位置的思考
将 CBAM 这样的“即插即用”模块添加到现有模型中时,其放置位置至关重要,直接影响模型的性能。
核心原则:在特征提取最充分的地方应用注意力。
在 ResNet 的 BasicBlock
中,特征经过两个卷积层 conv1
和 conv2
的连续提取。因此,最佳的放置位置是在第二个卷积层之后,与跳跃连接的输入相加之前。
这样做的逻辑是:
- 让
BasicBlock
内的卷积网络充分提取特征。 - 使用 CBAM 对这些提取出的特征进行通道和空间上的“精炼”,增强重要特征,抑制无关特征。
- 最后,将这个“精炼”过的特征
F'(x)
与原始输入x
通过跳跃连接相加。
3. 针对预训练模型的训练策略
直接使用一个大型的预训练模型并在我们自己的(通常较小的)数据集上从头开始训练,既耗时又容易过拟合。更高效的方法是微调 (Fine-tuning),即利用预训练模型已经学到的通用特征,只对模型进行微小的调整以适应新任务。
a. 差异化学习率
思想:对模型的不同部分使用不同的学习率。
- 特征提取层 (Backbone):这些层在 ImageNet 等大型数据集上已经学习到了非常通用的特征(如边缘、纹理、形状)。我们只需要对它们进行微调,因此使用一个较小的学习率(如
1e-4
)。 - 分类层 (Classifier/Head):这是我们为了适应新任务而新添加的层,其权重是随机初始化的,需要从头开始学习。因此,我们为它设置一个较大的学习率(如
1e-3
)。
这样做可以防止较大的学习率破坏已经训练好的骨干网络权重,同时保证新加的分类层能够快速收敛。
b. 三阶段微调 (Progressive Unfreezing)
这是一种更稳定、更精细的微调策略,通过“渐进式解冻”来训练模型。
-
第一阶段:只训练分类层
- 操作:冻结骨干网络的所有参数,只更新我们新添加的分类层的参数。
- 目的:让随机初始化的分类层快速学习,以适应新数据集的特征分布,而不会因其初始梯度过大而破坏骨干网络。
-
第二阶段:微调部分骨干网络
- 操作:解冻骨干网络的后面几层(如 ResNet 的
layer3
,layer4
),并与分类层一起训练。此时,骨干网络的学习率应设得非常小。 - 目的:让网络的高层语义特征(更接近特定任务)也开始适应新数据。
- 操作:解冻骨干网络的后面几层(如 ResNet 的
-
第三阶段:微调整个网络
- 操作:解冻所有网络层,以一个极小的学习率(如
1e-5
)对整个模型进行训练。 - 目的:对整个网络进行整体的、细微的调整,以达到最佳性能。
- 操作:解冻所有网络层,以一个极小的学习率(如
4. 实践:对 VGG16 + CBAM 模型进行微调
虽然上面我们分析了 ResNet,但这些思想是通用的。下面我们以 VGG16 为例,演示如何为其添加 CBAM 模块并应用微调策略。
步骤:
- 加载预训练的 VGG16 模型。
- 修改模型结构:遍历 VGG16 的特征提取层 (
features
),在每个MaxPool2d
层之后插入一个 CBAM 模块。 - 替换分类头:将 VGG16 原本用于1000类分类的全连接层替换为适用于我们自己任务(如 CIFAR-10 的10类分类)的新层。
- 设置差异化学习率:为骨干网络和新的分类层分别设置不同的学习率。
- (可选)实现冻结训练:通过设置参数的
requires_grad
属性为False
来冻结特定层。
核心代码示例(仅展示结构修改与学习率设置):
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision.models import vgg16# 假设 CBAM 模块已经定义好 (如 DAY 49 的代码)
# class CBAM(nn.Module): ...# 1. 加载预训练VGG16
model = vgg16(pretrained=True)
features = list(model.features)# 2. 在VGG的MaxPool2d层后插入CBAM模块
vgg_cbam_features = []
channel_map = {4: 64, 9: 128, 16: 256, 23: 512, 30: 512} # VGG16 MaxPool层索引 -> 通道数for i, layer in enumerate(features):vgg_cbam_features.append(layer)if isinstance(layer, nn.MaxPool2d):# 插入CBAMin_channels = channel_map.get(i)if in_channels:vgg_cbam_features.append(CBAM(in_channels))model.features = nn.Sequential(*vgg_cbam_features)# 3. 替换分类头 (假设为CIFAR-10)
num_features = model.classifier[0].in_features
model.classifier = nn.Sequential(nn.Linear(num_features, 4096),nn.ReLU(True),nn.Dropout(),nn.Linear(4096, 4096),nn.ReLU(True),nn.Dropout(),nn.Linear(4096, 10), # 新的分类头,10个类别
)# 4. 设置差异化学习率
# 将参数分为两组:骨干网络和分类头
backbone_params = model.features.parameters()
classifier_params = model.classifier.parameters()optimizer = optim.Adam([{'params': backbone_params, 'lr': 1e-4}, # 骨干网络使用小学习率{'params': classifier_params, 'lr': 1e-3} # 分类头使用大学习率
])# 5. (可选)冻结训练示例
# 冻结所有骨干网络参数
for param in model.features.parameters():param.requires_grad = False# 此时,只有分类头的参数会被更新
optimizer_frozen = optim.Adam(model.classifier.parameters(), lr=1e-3)# 在训练一段时间后,可以解冻
# for param in model.features.parameters():
# param.requires_grad = True
通过以上步骤,我们就成功地将 CBAM 模块集成到了 VGG16 中,并为其设置了高效的微调策略。这种“理解架构 -> 策略性修改 -> 智能训练”的流程,是提升模型性能和训练效率的核心方法。
@浙大疏锦行