在 Day 44 到 Day 50 的学习中,我们掌握了预训练模型、TensorBoard 监控以及 CBAM 注意力机制。今天,我们将这些“零散”的知识点熔于一炉,对“狗品种识别”这一实战项目进行深度优化。
本文将从设计思路、核心技术原理到代码实现剖析进行详细讲解。
一、 优化思路:为什么这么改?
1. 从 ResNet 到 ResNet + CBAM
- 痛点:原始的 ResNet50 虽然强大,但它在提取特征时是“平铺直叙”的——它对图片中的每一个像素点(空间)和每一个特征通道(语义)的关注度是相对均匀的。但在狗品种识别中,背景(如草地、沙发)往往是干扰项,我们需要模型更聚焦于狗的眼睛、耳朵、毛色纹理等关键部位。
- 解法:引入 CBAM (Convolutional Block Attention Module)。
- Channel Attention (通道注意力):告诉模型“看什么”。比如在识别哈士奇时,模型应该更关注“黑白毛色”、“蓝色眼睛”这些特征通道,而抑制背景噪音通道。
- Spatial Attention (空间注意力):告诉模型“看哪里”。直接在特征图上加权,让模型聚焦于狗的躯干部位,忽略周围的草地。
2. 从统一学习率到差异化学习率
- 痛点:我们在做迁移学习。ResNet50 的主干部分(Backbone)已经在 ImageNet 的 1400 万张图片上训练得非常好了,是“资深专家”;而我们新加入的 CBAM 模块和全连接分类层(Head)是随机初始化的“实习生”。如果用同样大的步子(学习率)去训练,容易把“专家”带偏;如果用同样小的步子,"实习生"学得太慢。
- 解法:差异化学习率 (Differential Learning Rate)。
- 专家 (Backbone):用极小的学习率 (
1e-4或更低),以此微调,保持其强大的特征提取能力。 - 实习生 (Head + CBAM):用较大的学习率 (
1e-3),让他们快速收敛,跟上专家的节奏。
- 专家 (Backbone):用极小的学习率 (
二、 代码剖析
1. CBAM 模块实现
CBAM 由两个子模块串联而成:通道注意力 -> 空间注意力。
class CBAM(nn.Module):
def __init__(self, in_channels, ratio=16, kernel_size=7):
super().__init__()
# 通道注意力:关注“是什么特征”
self.channel_attn = ChannelAttention(in_channels, ratio)
# 空间注意力:关注“在哪个位置”
self.spatial_attn = SpatialAttention(kernel_size)
def forward(self, x):
# 串联结构:先通道,后空间
x = self.channel_attn(x)
x = self.spatial_attn(x)
return x
- 细节:为什么是
x = self.channel_attn(x)而不是out = ...; return x + out?- CBAM 是一种加权机制,它输出的是一个 0~1 之间的权重图(Mask),通过乘法直接作用于原始特征
x,从而起到增强或抑制的作用。这与 ResNet 的残差相加(Addition)不同。
- CBAM 是一种加权机制,它输出的是一个 0~1 之间的权重图(Mask),通过乘法直接作用于原始特征
2. ResNet50 集成 CBAM
这是本次优化的核心架构。我们没有破坏 ResNet 的内部结构(Block),而是在每个 Stage 的输出端“外挂”了一个 CBAM。
class ResNet50_CBAM(nn.Module):
def __init__(self, num_classes=120, pretrained=True):
super().__init__()
# 1. 加载预训练的“专家”
self.backbone = models.resnet50(pretrained=pretrained)
# ResNet50 四个 Stage 的输出通道数
layer_channels = [256, 512, 1024, 2048]
# 2. 在每个 Stage 后定义 CBAM 模块
self.cbam_layer1 = CBAM(in_channels=layer_channels[0])
self.cbam_layer2 = CBAM(in_channels=layer_channels[1])
# ... (layer3, layer4 同理)
# 3. 重写分类头,适应 120 种狗的分类
self.backbone.fc = nn.Linear(in_features=2048, out_features=num_classes)
def forward(self, x):
# 手动重写前向传播,插入 CBAM
# Stem (输入层)
x = self.backbone.conv1(x)
x = self.backbone.bn1(x)
x = self.backbone.relu(x)
x = self.backbone.maxpool(x)
# Layer 1
x = self.backbone.layer1(x) # 原始 ResNet 提取特征
x = self.cbam_layer1(x) # CBAM 进行精炼和加权
# Layer 2 ... (同理)
# Head (分类)
x = self.backbone.avgpool(x)
x = torch.flatten(x, 1)
x = self.backbone.fc(x)
return x
3. 差异化学习率配置
在 PyTorch 中,optimizer 可以接收一个参数列表,每个元素是一个字典,可以单独指定 lr。
# 1. 筛选出 Backbone 的参数 ID
backbone_params = list(map(id, model.backbone.parameters()))
# 2. 筛选出新添加层(CBAM + FC)的参数
# 逻辑:只要参数 ID 不在 backbone_params 里,就是新参数
new_params = filter(lambda p: id(p) not in backbone_params, model.parameters())
# 3. 定义优化器
optimizer = optim.Adam([
# 组1:专家层,学习率低 (1e-4)
{'params': model.backbone.parameters(), 'lr': 1e-4},
# 组2:新层,学习率高 (1e-3)
{'params': new_params, 'lr': 1e-3}
])
三、 训练与监控
我们引入了 TensorBoard,这是工业界最常用的可视化工具。
from torch.utils.tensorboard import SummaryWriter
# 初始化记录器
writer = SummaryWriter('runs/dog_breed_resnet50_cbam')
# 在训练循环中记录
writer.add_scalar('Training Loss', loss.item(), global_step)
writer.add_scalar('Training Accuracy', epoch_acc, epoch)
这样,我们就不需要盯着控制台滚动的数字,而是可以打开浏览器,看到 Loss 曲线是否收敛、是否存在震荡,从而科学地调整超参数。

366

被折叠的 条评论
为什么被折叠?



