今日任务:
- resnet 结构解析
- CBAM 放置位置的思考
- 针对预训练模型的训练策略:差异化学习率;三阶段微调
作业:理解Resnet18 的模型结构;尝试对 vgg16 + CBAM 进行微调策略
Resnet 结构解析
使用torchinfo.summary 查看resnet18的结构:
import torch
import torchvision.models as models
from torchinfo import summary #之前的内容说了,推荐用他来可视化模型结构,信息最全
# 加载 ResNet18(预训练)
model = models.resnet18(pretrained=True) # 加载ImageNet的预训练权重
model.eval()
# 输出模型结构和参数概要
summary(model, input_size=(1, 3, 224, 224))
组成
![]()
resnet-18 包含三个部分:
- 输入预处理:一个卷积层 + 一个最大池化
- 核心特征提取(主体):4组残差块,每个layer包含2个basicblock
- 分类输出:全局平均池化 + 全连接层输出
下图为源代码的一部分:


BasicBlock
核心:让网络层学习映射与输入之间的差值,residual F(x) = H(x) - x,即H(x) = F(x) + x

上图是残差块的基本结构:
- 主路 (Main Path):这是学习“残差” F(x) 的部分。在 ResNet-18 中,它由两个 3x3 的卷积层构成。具体过程:Conv2d: 3-1 (3x3 卷积) --> BatchNorm2d: 3-2 (批归一化) --> ReLU: 3-3 (激活函数) --> Conv2d: 3-4 (3x3 卷积) --> BatchNorm2d: 3-5 (批归一化)
- 捷径 (Shortcut Path):这就是 + x 的部分,直接将输入 x 传递过来。这避免了在层层传递中信息丢失或梯度消失的问题。
两种残差块:

CBAM 放置位置
CBAM 包含通道注意力和空间注意力,其中空间注意力受到空间维度的影响,再考虑到CBAM是对特征提取的加强,故而CBAM放置位置应该是在全局池化之前。因为全局池化操作会将空间维度压缩为1*1,使得空间注意力失效。
公认正确的做法是在每一个残差块的输出上应用CBAM。这里需要考虑,在预训练模型中加入CBAM后(相当于改变了模型架构)会破坏原有权重吗?
问题:在预训练ResNet中加CBAM会不会破坏原有特征?
↓
答案:不会,因为:
↓
原因1:CBAM初始状态是"保守"的
- 数学上:output = x * sigmoid(attention)
- 初始化时:attention ≈ 0 → sigmoid(0) = 0.5(sigmoid函数的特性)
- 所以:output ≈ x * 0.5(只是均匀缩放)
↓
原因1(补充):这种均匀缩放保留了所有特征结构
- 相对关系不变,只是强度减半
- 预训练层仍能识别熟悉的模式
↓
原因2:网络可以自主选择CBAM的效用(失效 or 有效,两种路径)
- CBAM没用:学习让CBAM退化为恒等映射(output = x*1),attention等于很大的正数
- CBAM有用:学习有效的注意力机制,重要特征输出趋于1,不重要趋于0
↓
结论:因此可以安全插入,不会破坏预训练权重
关于自主学习是否使用CBAM,在反向传播时会自动判断CBAM的效用:
- 训练初期:CBAM权重随机,attention_scores ≈ 0,此时 output ≈ x * 0.5
- 训练数据表明CBAM能提升性能(有效):梯度会推动attention_scores学习有意义的模式
- CBAM退化为恒等映射性能更好时(无效):梯度会推动所有attention_scores → 正大数
CBAM放置位置的小结:
- 每个残差块后:推荐,特征提取的每个阶段都精炼,逐步优化,效果通常最好
- 最后一个卷积块后,全局池化前:推荐,轻量化,计算成本低
- 全局池化后,全连接层前:不推荐,此时空间注意力失效,无法发挥模块的全部作用
import torch
import torch.nn as nn
from torchvision import models
# 自定义ResNet18模型,插入CBAM模块
class ResNet18_CBAM(nn.Module):
def __init__(self, num_classes=10, pretrained=True, cbam_ratio=16, cbam_kernel=7):
super().__init__()
# 加载预训练ResNet18
self.backbone = models.resnet18(pretrained=pretrained)
# 修改首层卷积以适应32x32输入(CIFAR10)
self.backbone.conv1 = nn.Conv2d(
in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False
)
self.backbone.maxpool = nn.Identity() # 移除原始MaxPool层(因输入尺寸小)
# 在每个残差块组后添加CBAM模块
self.cbam_layer1 = CBAM(in_channels=64, ratio=cbam_ratio, kernel_size=cbam_kernel)
self.cbam_layer2 = CBAM(in_channels=128, ratio=cbam_ratio, kernel_size=cbam_kernel)
self.cbam_layer3 = CBAM(in_channels=256, ratio=cbam_ratio, kernel_size=cbam_kernel)
self.cbam_layer4 = CBAM(in_channels=512, ratio=cbam_ratio, kernel_size=cbam_kernel)
# 修改分类头
self.backbone.fc = nn.Linear(in_features=512, out_features=num_classes)
def forward(self, x):
# 主干特征提取
x = self.backbone.conv1(x)
x = self.backbone.bn1(x)
x = self.backbone.relu(x) # [B, 64, 32, 32]
# 第一层残差块 + CBAM
x = self.backbone.layer1(x) # [B, 64, 32, 32]
x = self.cbam_layer1(x)
# 第二层残差块 + CBAM
x = self.backbone.layer2(x) # [B, 128, 16, 16]
x = self.cbam_layer2(x)
# 第三层残差块 + CBAM
x = self.backbone.layer3(x) # [B, 256, 8, 8]
x = self.cbam_layer3(x)
# 第四层残差块 + CBAM
x = self.backbone.layer4(x) # [B, 512, 4, 4]
x = self.cbam_layer4(x)
# 全局平均池化 + 分类
x = self.backbone.avgpool(x) # [B, 512, 1, 1]
x = torch.flatten(x, 1) # [B, 512]
x = self.backbone.fc(x) # [B, 10]
return x
# 初始化模型并移至设备
model = ResNet18_CBAM().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=3, factor=0.5)
预训练模型的训练策略
差异化学习率
预训练模型结构经验丰富,学习的东西少(lr低);新模块经验少,需要多学习(lr高)

三阶段微调
这里的思想在之前应用resnet18预训练时出现过:先进行初步学习(建立基础、框架),再进行深入学习(细节)。
一些名词:
- 底层或浅层:靠近输入图像的层,学习通用、基础的视觉元素(如边缘、颜色等),特征高度可复用;
- 高层或深层:靠近最终输出(分类头)的层,学习将基础元素组合成更复杂、更具语义的部件或概念,与具体任务高度相关

阶段:分类规则 → 物体的整体特征(新数据) → 微调细节(底层特征)。
这里在解冻主干卷积层采用的是先解冻高层,后解冻底层。为什么是先解冻的是高层(layer3,layer4),而不是底层(layer1,layer2)呢?
- 破坏预训练特征:底层随机调整会破坏预训练学到的边缘检测器等基础特征
- 梯度不稳定:底层梯度要经过很多层传播,容易梯度消失/爆炸
- 收敛缓慢:同时调整所有层,优化目标过于复杂
因此,对于底层,采取的策略是在微调初期冻结(因为存在污染通用特征的可能);对于高层,重新训练和调整(具有任务特异性),对具体任务构建特定的概念。
import time
# ======================================================================
# 4. 结合了分阶段策略和详细打印的训练函数
# ======================================================================
def set_trainable_layers(model, trainable_parts):
print(f"\n---> 解冻以下部分并设为可训练: {trainable_parts}")
for name, param in model.named_parameters():
param.requires_grad = False
for part in trainable_parts:
if part in name:
param.requires_grad = True
break
def train_staged_finetuning(model, criterion, train_loader, test_loader, device, epochs):
optimizer = None
# 初始化历史记录列表,与你的要求一致
all_iter_losses, iter_indices = [], []
train_acc_history, test_acc_history = [], []
train_loss_history, test_loss_history = [], []
for epoch in range(1, epochs + 1):
epoch_start_time = time.time()
# --- 动态调整学习率和冻结层 ---
if epoch == 1:
print("\n" + "="*50 + "\n🚀 **阶段 1:训练注意力模块和分类头**\n" + "="*50)
set_trainable_layers(model, ["cbam", "backbone.fc"])
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)
elif epoch == 6:
print("\n" + "="*50 + "\n✈️ **阶段 2:解冻高层卷积层 (layer3, layer4)**\n" + "="*50)
set_trainable_layers(model, ["cbam", "backbone.fc", "backbone.layer3", "backbone.layer4"])
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4)
elif epoch == 21:
print("\n" + "="*50 + "\n🛰️ **阶段 3:解冻所有层,进行全局微调**\n" + "="*50)
for param in model.parameters(): param.requires_grad = True
optimizer = optim.Adam(model.parameters(), lr=1e-5)
# --- 训练循环 ---
model.train()
running_loss, correct, total = 0.0, 0, 0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# 记录每个iteration的损失
iter_loss = loss.item()
all_iter_losses.append(iter_loss)
iter_indices.append((epoch - 1) * len(train_loader) + batch_idx + 1)
running_loss += iter_loss
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
# 按你的要求,每100个batch打印一次
if (batch_idx + 1) % 100 == 0:
print(f'Epoch: {epoch}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} '
f'| 单Batch损失: {iter_loss:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}')
epoch_train_loss = running_loss / len(train_loader)
epoch_train_acc = 100. * correct / total
train_loss_history.append(epoch_train_loss)
train_acc_history.append(epoch_train_acc)
# --- 测试循环 ---
model.eval()
test_loss, correct_test, total_test = 0, 0, 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += criterion(output, target).item()
_, predicted = output.max(1)
total_test += target.size(0)
correct_test += predicted.eq(target).sum().item()
epoch_test_loss = test_loss / len(test_loader)
epoch_test_acc = 100. * correct_test / total_test
test_loss_history.append(epoch_test_loss)
test_acc_history.append(epoch_test_acc)
# 打印每个epoch的最终结果
print(f'Epoch {epoch}/{epochs} 完成 | 耗时: {time.time() - epoch_start_time:.2f}s | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%')
# 训练结束后调用绘图函数
print("\n训练完成! 开始绘制结果图表...")
plot_iter_losses(all_iter_losses, iter_indices)
plot_epoch_metrics(train_acc_history, test_acc_history, train_loss_history, test_loss_history)
# 返回最终的测试准确率
return epoch_test_acc
# ======================================================================
# 5. 绘图函数定义
# ======================================================================
def plot_iter_losses(losses, indices):
plt.figure(figsize=(10, 4))
plt.plot(indices, losses, 'b-', alpha=0.7, label='Iteration Loss')
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('The Loss of Each Iteration')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# 6. 绘制每个 epoch 的准确率和损失曲线
def plot_epoch_metrics(train_acc, test_acc, train_loss, test_loss):
epochs = range(1, len(train_acc) + 1)
plt.figure(figsize=(12, 4))
# 绘制准确率曲线
plt.subplot(1, 2, 1)
plt.plot(epochs, train_acc, 'b-', label='Train Accuracy')
plt.plot(epochs, test_acc, 'r-', label='Test Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Accuracy Curve')
plt.legend()
plt.grid(True)
# 绘制损失曲线
plt.subplot(1, 2, 2)
plt.plot(epochs, train_loss, 'b-', label='Train Loss')
plt.plot(epochs, test_loss, 'r-', label='Test Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Loss Curve')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# ======================================================================
# 6. 执行训练
# ======================================================================
model = ResNet18_CBAM().to(device)
criterion = nn.CrossEntropyLoss()
epochs = 50
print("开始使用带分阶段微调策略的ResNet18+CBAM模型进行训练...")
final_accuracy = train_staged_finetuning(model, criterion, train_loader, test_loader, device, epochs)
print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")
# torch.save(model.state_dict(), 'resnet18_cbam_finetuned.pth')
# print("模型已保存为: resnet18_cbam_finetuned.pth")
使用4090训练时间总共39min,最终准确率90.94%
第一阶段为56.10%,第二阶段87.00%,第三阶段90.94%


810

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



