@浙大疏锦行
DAY 50 预训练模型+CBAM模块
知识点回顾:
- resnet结构解析
- CBAM放置位置的思考
- 针对预训练模型的训练策略
- 差异化学习率
- 三阶段微调
ps:今日的代码训练时长较长,3080ti大概需要40min的训练时长
作业:
- 好好理解下resnet18的模型结构
- 尝试对vgg16+cbam进行微调策略
我会用 “汽车生产线”“医院分诊系统”“游戏难度调整” 等生活化比喻,帮你理解这些深度学习概念,再整理成结构化的学习笔记。
一、知识点通俗解释 + 趣味比喻
1. ResNet 结构解析
含义:ResNet(残差网络)通过 “跳跃连接” 解决深层网络训练时的梯度消失问题,允许网络学习残差映射而非直接映射。
比喻:把 ResNet 想象成 “汽车生产线” :
- 传统网络:每道工序(网络层)必须完全加工上一道工序的半成品,一旦某工序出错,后续全乱(梯度消失)。
- ResNet:在工序间增加 “捷径传送带”(跳跃连接),如果某工序加工效果不好,直接通过捷径将半成品送到下工序,保证生产线畅通。
- 残差块:相当于 “可选择加工的工作站”,既能对半成品加工(卷积操作),也能直接放行(跳跃连接)。
- 优势:像灵活的生产线,即使部分工序效率低,整体仍能正常运转,网络可以训练得更深。
2. CBAM 放置位置的思考
含义:CBAM(注意力模块)可插入网络不同位置,放置位置影响模型性能。
比喻:把 CBAM 想象成 “医院分诊系统” :
- 放置在浅层:如急诊室入口,快速筛选危急病人(提取基础特征),但可能忽略病情复杂的患者(深层语义)。
- 放置在深层:如专科诊室,针对特定疾病(语义特征)深入分析,但可能遗漏早期症状(浅层特征)。
- 均匀放置:如在各科室都设分诊台,全面优化诊疗流程,但增加系统复杂度。
- 最佳实践:通常在中间层放置(如 ResNet 的 Block3 之后),平衡浅层细节与深层语义。
3. 针对预训练模型的训练策略
a. 差异化学习率
含义:对预训练模型的不同层设置不同学习率,底层(特征提取)设小学习率,顶层(任务适应)设大学习率。
比喻:把模型训练想象成 “游戏难度调整” :
- 底层:像游戏新手教程,已训练好基础技能(如识别边缘、纹理),只需微调(小学习率)。
- 顶层:像游戏最终关卡,需针对新任务(如猫狗分类)大幅调整策略(大学习率)。
- 优势:避免破坏底层已学习的通用特征,同时快速适应新任务。
b. 三阶段微调
含义:分阶段解冻模型层,逐步调整训练强度:
- 冻结所有层:只训练分类器(如全连接层)。
- 解冻部分层:如解冻最后几个残差块。
- 解冻全部层:整体微调。
比喻:把模型微调想象成 “装修老房子” : - 第一阶段:只换家具(调整分类器),不改动墙体(冻结底层)。
- 第二阶段:翻新部分房间(解冻部分层),如厨房、卫生间。
- 第三阶段:全面改造(解冻全部层),包括水电线路。
- 优势:从易到难,避免灾难性遗忘,提高训练稳定性。
二、学习笔记
1. 核心概念对比表
知识点 | 核心定义 | 比喻场景 | 关键操作 / 作用 |
---|---|---|---|
ResNet 结构 | 通过跳跃连接解决梯度消失问题 | 汽车生产线捷径传送带 | 允许网络深度增加,提升性能 |
CBAM 放置位置 | 影响模型对不同层次特征的关注 | 医院分诊系统 | 中间层放置平衡细节与语义 |
差异化学习率 | 不同层设置不同学习率 | 游戏难度调整 | 保护底层特征,加速顶层适应 |
三阶段微调 | 分阶段解冻模型层 | 装修老房子 | 逐步优化,提高训练稳定性 |
2. 代码框架(简化版)
python
运行
import torch
import torch.nn as nn
from torchvision.models import resnet50# 1. ResNet结构(关键部分)
class ResidualBlock(nn.Module):def forward(self, x):identity = x # 保存原始输入out = self.conv1(x) # 卷积操作out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out += identity # 跳跃连接:残差相加return self.relu(out)# 2. 在ResNet中插入CBAM
model = resnet50(pretrained=True)
# 在layer3后插入CBAM
model.layer3.add_module('cbam', CBAM(in_channels=1024))# 3. 差异化学习率
optimizer = torch.optim.Adam([{'params': model.conv1.parameters(), 'lr': 1e-5}, # 底层小学习率{'params': model.layer4.parameters(), 'lr': 1e-4}, # 中层中等学习率{'params': model.fc.parameters(), 'lr': 1e-3}, # 顶层大学习率
])# 4. 三阶段微调(伪代码)
def three_stage_finetune(model):# 阶段1:冻结所有层,只训练分类器for param in model.parameters():param.requires_grad = Falsemodel.fc.requires_grad = Truetrain(model, epochs=5)# 阶段2:解冻layer4for param in model.layer4.parameters():param.requires_grad = Truetrain(model, epochs=10)# 阶段3:解冻全部层for param in model.parameters():param.requires_grad = Truetrain(model, epochs=20)
3. 记忆口诀
ResNet:
深层训练梯度难,跳跃连接把路开,
残差块里做选择,信息传递更顺畅。
CBAM 放置:
注意力放哪里好?浅层深层要权衡,
中间位置最常见,细节语义两不误。
差异化学习率:
底层小步慢慢走,顶层大步向前冲,
预训练权保护好,新任务中快速调。
三阶段微调:
一冻分类器先训,二解部分再优化,
三全解冻细打磨,步步为营效果佳。
三、总结
通过这些比喻,可以把复杂的网络结构和训练策略想象成生活中的生产、管理和优化流程,帮助你理解它们的设计思路和作用。这些技术都是为了让模型训练更高效、性能更优,在实际应用中可根据任务需求灵活组合使用。
下面我将通过详细注释和可视化方式帮助你理解 ResNet18 的结构,并实现 VGG16+CBAM 的三阶段微调策略。
一、ResNet18 模型结构解析
1. 整体架构可视化
plaintext
ResNet18
├── Conv1: 3x3, 64 channels, stride=2
├── MaxPool: 3x3, stride=2
├── Layer1: 2个残差块 (每个块包含2个3x3卷积)
│ ├── Block1: 64→64
│ └── Block2: 64→64
├── Layer2: 2个残差块 (降维+通道翻倍)
│ ├── Block1: 64→128 (stride=2, 带维度调整)
│ └── Block2: 128→128
├── Layer3: 2个残差块 (通道翻倍)
│ ├── Block1: 128→256 (stride=2)
│ └── Block2: 256→256
├── Layer4: 2个残差块 (通道翻倍)
│ ├── Block1: 256→512 (stride=2)
│ └── Block2: 512→512
├── AvgPool: 7x7
└── FC: 512→1000 (ImageNet分类)
2. 关键代码理解
python
运行
import torch
import torch.nn as nn
from torchvision.models import resnet18# 加载预训练模型
model = resnet18(pretrained=True)# 打印模型结构(简化版)
print("ResNet18核心组件:")
print(f"输入层: {model.conv1}")
print(f"第一层残差块: {model.layer1}")
print(f"最后一层残差块: {model.layer4}")
print(f"分类器: {model.fc}")# 残差块内部结构
first_block = model.layer1[0]
print("\n第一个残差块结构:")
print(f"卷积1: {first_block.conv1}") # 3x3卷积, 64→64
print(f"卷积2: {first_block.conv2}") # 3x3卷积, 64→64
print(f"跳跃连接: {first_block.downsample}") # None,因为输入输出维度相同
3. 残差连接的作用
残差块通过out += identity
实现跳跃连接,允许网络学习 “残差映射”:
python
运行
def forward(self, x):identity = x # 保存原始输入out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)if self.downsample is not None: # 当需要调整维度时identity = self.downsample(x)out += identity # 核心: 残差连接out = self.relu(out)return out
二、VGG16+CBAM 的三阶段微调策略
1. 模型定义与 CBAM 插入
python
运行
import torch
import torch.nn as nn
from torchvision.models import vgg16# 定义CBAM模块(简化版)
class ChannelAttention(nn.Module):def __init__(self, in_channels, reduction=16):super().__init__()self.avg_pool = nn.AdaptiveAvgPool2d(1)self.mlp = nn.Sequential(nn.Linear(in_channels, in_channels // reduction, bias=False),nn.ReLU(),nn.Linear(in_channels // reduction, in_channels, bias=False),nn.Sigmoid())def forward(self, x):b, c, _, _ = x.size()avg_out = self.mlp(self.avg_pool(x).view(b, c))return x * avg_out.view(b, c, 1, 1)class SpatialAttention(nn.Module):def __init__(self, kernel_size=7):super().__init__()self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2, bias=False)self.sigmoid = nn.Sigmoid()def forward(self, x):avg_out = torch.mean(x, dim=1, keepdim=True)max_out, _ = torch.max(x, dim=1, keepdim=True)out = torch.cat([avg_out, max_out], dim=1)out = self.conv(out)return x * self.sigmoid(out)class CBAM(nn.Module):def __init__(self, in_channels, reduction=16, kernel_size=7):super().__init__()self.channel_att = ChannelAttention(in_channels, reduction)self.spatial_att = SpatialAttention(kernel_size)def forward(self, x):x = self.channel_att(x)x = self.spatial_att(x)return x# 修改VGG16结构,插入CBAM
def vgg16_with_cbam(pretrained=True):model = vgg16(pretrained=pretrained)# 在每个MaxPooling后插入CBAMfeatures = list(model.features)new_features = []cbam_idx = 0for layer in features:new_features.append(layer)# 在特定层后插入CBAMif isinstance(layer, nn.MaxPool2d):in_channels = new_features[-2].out_channels # 获取前一层的输出通道数new_features.append(CBAM(in_channels))cbam_idx += 1model.features = nn.Sequential(*new_features)return model
2. 三阶段微调策略实现
python
运行
def three_stage_finetune(model, train_loader, test_loader, device):model.to(device)criterion = nn.CrossEntropyLoss()# 阶段1: 只训练分类器print("===== 阶段1: 冻结所有特征层 =====")for param in model.features.parameters():param.requires_grad = Falseoptimizer = torch.optim.Adam(model.classifier.parameters(), lr=0.001)train(model, train_loader, test_loader, criterion, optimizer, device, epochs=5)# 阶段2: 解冻最后两个block + CBAMprint("===== 阶段2: 解冻最后两个block =====")# VGG16的features分为5个block,每个block以MaxPool结束# 解冻最后两个block (从第24层到最后)for i, param in enumerate(model.features.parameters()):if i >= 24: # VGG16的第24层之后是最后两个blockparam.requires_grad = True# 对不同部分设置不同学习率optimizer = torch.optim.Adam([{'params': model.features.parameters(), 'lr': 1e-5}, # 低学习率{'params': model.classifier.parameters(), 'lr': 1e-4} # 较高学习率])train(model, train_loader, test_loader, criterion, optimizer, device, epochs=10)# 阶段3: 解冻全部层,小学习率微调print("===== 阶段3: 解冻全部层 =====")for param in model.parameters():param.requires_grad = Trueoptimizer = torch.optim.Adam(model.parameters(), lr=1e-6) # 非常小的学习率train(model, train_loader, test_loader, criterion, optimizer, device, epochs=15)return model# 训练函数(简化版)
def train(model, train_loader, test_loader, criterion, optimizer, device, epochs):model.train()for epoch in range(epochs):running_loss = 0.0for inputs, labels in train_loader:inputs, labels = inputs.to(device), labels.to(device)optimizer.zero_grad()outputs = model(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()running_loss += loss.item()# 每5个epoch评估一次if (epoch + 1) % 5 == 0:val_acc = evaluate(model, test_loader, device)print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.2f}%")# 评估函数
def evaluate(model, test_loader, device):model.eval()correct = 0total = 0with torch.no_grad():for inputs, labels in test_loader:inputs, labels = inputs.to(device), labels.to(device)outputs = model(inputs)_, predicted = torch.max(outputs, 1)total += labels.size(0)correct += (predicted == labels).sum().item()return 100 * correct / total
3. 主函数调用示例
python
运行
# 初始化模型
model = vgg16_with_cbam(pretrained=True)# 准备数据(示例,需根据实际情况修改)
from torchvision import datasets, transforms
from torch.utils.data import DataLoadertransform = transforms.Compose([transforms.Resize(224),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])train_dataset = datasets.CIFAR10(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.CIFAR10(root='./data', train=False, transform=transform)train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32)# 训练模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = three_stage_finetune(model, train_loader, test_loader, device)
三、总结与优化建议
-
ResNet18 关键点:
- 残差连接通过
out += identity
实现,解决深层网络梯度消失问题 - 共有 8 个残差块,分为 4 组,每组通道数翻倍
- 最后通过全局平均池化和全连接层进行分类
- 残差连接通过
-
VGG16+CBAM 微调策略:
- 阶段 1 冻结所有特征层,只训练分类器(快速适应新任务)
- 阶段 2 解冻最后两个卷积块 + CBAM,使用差异化学习率
- 阶段 3 解冻全部层,用极小学习率微调(防止过拟合)
-
优化建议:
- 根据具体任务调整 CBAM 的插入位置和数量
- 可使用学习率调度器(如 ReduceLROnPlateau)自动调整学习率
- 在每个阶段保存模型,方便回滚和比较
这样的微调策略可以在保持预训练模型优势的同时,高效地适应新任务,通常能显著提升训练效率和最终性能。