1. 项目概述与背景
在计算机视觉领域,图像分类任务一直是基础且重要的研究方向。本次项目基于经典的"Dogs vs Cats"数据集,探索如何通过卷积神经网络(CNN)的改进策略提升分类精度。初始使用简单的三层卷积网络结构,测试集准确率约为80%,经过引入注意力机制和迁移学习技术后,最终将准确率提升至95%左右。
这个项目特别适合以下几类读者:
- 刚掌握CNN基础想进一步提升实战能力的学习者
- 对注意力机制在CV中的应用感兴趣的研究者
- 需要优化现有图像分类模型的工程人员
2. 技术方案设计与选型
2.1 基线模型分析
初始采用的简单CNN结构包含:
- 3个卷积层(通道数32→64→128)
- 每个卷积层后接ReLU激活和2×2最大池化
- 两个全连接层(512→2)
这种结构虽然简单,但存在明显局限:
- 感受野固定,无法自适应关注关键区域
- 浅层网络特征提取能力有限
- 从头训练需要大量数据
注意:输入图像统一resize到224×224,经过3次池化后特征图尺寸为28×28(224/2³)
2.2 改进方案设计
2.2.1 CBAM注意力机制
Convolutional Block Attention Module (CBAM) 包含两个子模块:
- 通道注意力:通过全局平均/最大池化生成通道权重
- 使用两层MLP(带ReLU)计算注意力
- 压缩比为16(如输入通道32→中间层2)
- 空间注意力:通过通道维度的平均/最大池化生成空间权重
- 使用7×7卷积计算注意力图
数学表达式:
python复制# 通道注意力
avg_out = MLP(AvgPool(x))
max_out = MLP(MaxPool(x))
channel_att = σ(avg_out + max_out)
# 空间注意力
avg_out = Mean(x, dim=1)
max_out = Max(x, dim=1)
spatial_att = σ(Conv7x7([avg_out; max_out]))
2.2.2 迁移学习策略
采用ResNet18预训练模型,配合三阶段微调:
- 阶段一(1-5 epoch):
- 仅训练CBAM模块和最后的FC层
- 学习率1e-3
- 阶段二(6-20 epoch):
- 增加训练高层卷积(layer3/layer4)
- 学习率降为1e-4
- 阶段三(21-50 epoch):
- 解冻全部参数
- 学习率1e-5
3. 代码实现详解
3.1 模型架构实现
3.1.1 CBAM模块
python复制class CBAM(nn.Module):
def __init__(self, in_channels, ratio=16, kernel_size=7):
super().__init__()
# 通道注意力
self.channel_att = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(in_channels, in_channels//ratio),
nn.ReLU(),
nn.Linear(in_channels//ratio, in_channels),
nn.Sigmoid()
)
# 空间注意力
self.spatial_att = nn.Sequential(
nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2),
nn.Sigmoid()
)
def forward(self, x):
# 通道注意力
channel_weight = self.channel_att(x).unsqueeze(-1).unsqueeze(-1)
x = x * channel_weight
# 空间注意力
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out = torch.max(x, dim=1, keepdim=True)[0]
spatial_weight = self.spatial_att(torch.cat([avg_out, max_out], dim=1))
return x * spatial_weight
3.1.2 ResNet18集成CBAM
关键修改点:
- 在每个残差块组(layer1-layer4)后插入CBAM
- 保持原始ResNet结构不变
- 替换最后的全连接层
python复制class ResNet18_CBAM(nn.Module):
def __init__(self, num_classes=2):
super().__init__()
self.backbone = models.resnet18(pretrained=True)
# 插入CBAM模块
self.cbam1 = CBAM(64)
self.cbam2 = CBAM(128)
self.cbam3 = CBAM(256)
self.cbam4 = CBAM(512)
# 修改分类头
self.backbone.fc = nn.Linear(512, num_classes)
def forward(self, x):
x = self.backbone.conv1(x)
x = self.backbone.bn1(x)
x = self.backbone.relu(x)
x = self.backbone.maxpool(x)
x = self.backbone.layer1(x)
x = self.cbam1(x)
x = self.backbone.layer2(x)
x = self.cbam2(x)
x = self.backbone.layer3(x)
x = self.cbam3(x)
x = self.backbone.layer4(x)
x = self.cbam4(x)
x = self.backbone.avgpool(x)
x = torch.flatten(x, 1)
x = self.backbone.fc(x)
return x
3.2 训练流程优化
3.2.1 三阶段训练控制
python复制def set_trainable_layers(model, trainable_keys):
for name, param in model.named_parameters():
param.requires_grad = any(key in name for key in trainable_keys)
def train_phase(phase, model, optimizer, train_loader):
if phase == 1: # 仅CBAM+FC
set_trainable_layers(model, ['cbam', 'fc'])
lr = 1e-3
elif phase == 2: # 增加高层卷积
set_trainable_layers(model, ['cbam', 'fc', 'layer3', 'layer4'])
lr = 1e-4
else: # 全参数训练
for param in model.parameters():
param.requires_grad = True
lr = 1e-5
optimizer = optim.Adam(
filter(lambda p: p.requires_grad, model.parameters()),
lr=lr
)
# 训练循环...
3.2.2 学习率调度策略
配合三阶段训练,采用余弦退火学习率:
python复制scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=len(train_loader)*5, # 每个阶段5个epoch
eta_min=lr*0.01
)
3.3 可视化分析
3.3.1 Grad-CAM实现
python复制class GradCAM:
def __init__(self, model, target_layer):
self.model = model
self.gradients = None
self.activations = None
# 注册前向/反向钩子
target_layer.register_forward_hook(self.save_activations)
target_layer.register_backward_hook(self.save_gradients)
def save_activations(self, module, input, output):
self.activations = output
def save_gradients(self, module, grad_input, grad_output):
self.gradients = grad_output[0]
def generate(self, input_tensor, target_class=None):
output = self.model(input_tensor)
if target_class is None:
target_class = output.argmax()
self.model.zero_grad()
one_hot = torch.zeros_like(output)
one_hot[0][target_class] = 1
output.backward(gradient=one_hot)
# 计算权重
weights = torch.mean(self.gradients, dim=[2,3])
cam = torch.sum(weights * self.activations, dim=1)
cam = F.relu(cam)
cam = F.interpolate(cam, input_tensor.shape[2:], mode='bilinear')
cam = (cam - cam.min()) / (cam.max() - cam.min())
return cam.squeeze().cpu().numpy()
3.3.2 可视化效果对比
通过Grad-CAM可以观察到:
- 基线模型:注意力分散在整只动物
- CBAM模型:更集中于头部等判别性区域
- ResNet+CBAM:精准定位眼睛、鼻子等关键特征
4. 实验结果与分析
4.1 精度对比
| 模型类型 | 参数量 | 测试准确率 | 训练时间(50epoch) |
|---|---|---|---|
| SimpleCNN | 2.1M | 80.2% | 25min |
| CNN+CBAM | 2.3M | 88.7% | 32min |
| ResNet18+CBAM | 11.2M | 95.3% | 68min |
4.2 消融实验
| 改进措施 | 准确率提升 |
|---|---|
| 基线模型 | 80.2% |
| +CBAM | +8.5% |
| +预训练 | +12.1% |
| +三阶段微调 | +3.7% |
4.3 关键发现
- CBAM在浅层网络提升更明显(+8.5% vs +3.2%)
- 三阶段���调比直接微调高2-3个点
- 可视化验证了注意力机制的有效性
5. 工程实践建议
5.1 部署优化技巧
- 模型量化:
python复制quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
- ONNX导出:
python复制torch.onnx.export(
model,
dummy_input,
"model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
'input': {0: 'batch'},
'output': {0: 'batch'}
}
)
5.2 常见问题排查
-
准确率波动大:
- 检查数据增强是否合理
- 调整Batch Size(建议32-64)
- 添加梯度裁剪(
nn.utils.clip_grad_norm_)
-
显存不足:
- 使用混合精度训练
python复制scaler = torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): outputs = model(inputs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() -
过拟合处理:
- 增加Dropout层(p=0.2-0.5)
- 添加L2正则化
- 使用早停策略
6. 扩展方向
-
尝试其他注意力机制:
- SE模块
- Non-local Networks
- Transformer中的多头注意力
-
模型轻量化:
- MobileNetV3+CBAM
- 知识蒸馏
-
多任务学习:
- 联合训练分类和分割任务
- 添加辅助损失函数
在实际部署中发现,将CBAM模块放在残差连接之后(而非卷积层之后)效果更好,这可能是由于残差结构本身已经包含特征选择机制。另外对于小数据集,适当降低CBAM的压缩比(如从16降到8)可以提升约1-2个点的准确率。
