DAY 50 預訓練模型+CBAM模塊
知識點回顧:
- resnet結構解析
- CBAM放置位置的思考
- 針對預訓練模型的訓練策略
- 差異化學習率
- 三階段微調
ps:今日的代碼訓練時長較長,3080ti大概需要40min的訓練時長
作業:
- 好好理解下resnet18的模型結構
- 嘗試對vgg16+cbam進行微調策略
1.CBMA定義
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np# 定義通道注意力
class ChannelAttention(nn.Module):def __init__(self, in_channels, ratio=16):"""通道注意力機制初始化參數:in_channels: 輸入特征圖的通道數ratio: 降維比例,用于減少參數量,默認為16"""super().__init__()# 全局平均池化,將每個通道的特征圖壓縮為1x1,保留通道間的平均值信息self.avg_pool = nn.AdaptiveAvgPool2d(1)# 全局最大池化,將每個通道的特征圖壓縮為1x1,保留通道間的最顯著特征self.max_pool = nn.AdaptiveMaxPool2d(1)# 共享全連接層,用于學習通道間的關系# 先降維(除以ratio),再通過ReLU激活,最后升維回原始通道數self.fc = nn.Sequential(nn.Linear(in_channels, in_channels // ratio, bias=False), # 降維層nn.ReLU(), # 非線性激活函數nn.Linear(in_channels // ratio, in_channels, bias=False) # 升維層)# Sigmoid函數將輸出映射到0-1之間,作為各通道的權重self.sigmoid = nn.Sigmoid()def forward(self, x):"""前向傳播函數參數:x: 輸入特征圖,形狀為 [batch_size, channels, height, width]返回:調整后的特征圖,通道權重已應用"""# 獲取輸入特征圖的維度信息,這是一種元組的解包寫法b, c, h, w = x.shape# 對平均池化結果進行處理:展平后通過全連接網絡avg_out = self.fc(self.avg_pool(x).view(b, c))# 對最大池化結果進行處理:展平后通過全連接網絡max_out = self.fc(self.max_pool(x).view(b, c))# 將平均池化和最大池化的結果相加并通過sigmoid函數得到通道權重attention = self.sigmoid(avg_out + max_out).view(b, c, 1, 1)# 將注意力權重與原始特征相乘,增強重要通道,抑制不重要通道return x * attention #這個運算是pytorch的廣播機制## 空間注意力模塊
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) # 平均池化:(B,1,H,W)max_out, _ = torch.max(x, dim=1, keepdim=True) # 最大池化:(B,1,H,W)pool_out = torch.cat([avg_out, max_out], dim=1) # 拼接:(B,2,H,W)attention = self.conv(pool_out) # 卷積提取空間特征return x * self.sigmoid(attention) # 特征與空間權重相乘## 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 ximport torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np# 設置中文字體支持
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams['axes.unicode_minus'] = False # 解決負號顯示問題# 檢查GPU是否可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用設備: {device}")# 數據預處理(與原代碼一致)
train_transform = transforms.Compose([transforms.RandomCrop(32, padding=4),transforms.RandomHorizontalFlip(),transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),transforms.RandomRotation(15),transforms.ToTensor(),transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])test_transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])# 加載數據集(與原代碼一致)
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, transform=test_transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
2.VGG16模型結構
VGG16 的模型結構如下2:
?- 輸入層:輸入圖片的尺寸(imagenet)是 224×224×3,即彩色圖像,通道為 3。
- 第一層卷積層:
- 卷積:使用 64 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 224×224×64。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:使用 64 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 224×224×64。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 池化:使用 2×2,stride=2 的池化單元進行最大池化操作,輸出為 112×112×64。
- 第二層卷積層:
- 卷積:輸入是 112×112×64,使用 128 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 112×112×128。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 112×112×128,使用 128 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 112×112×128。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 池化:使用 2×2,stride=2 的池化單元進行最大池化操作,輸出為 56×56×128。
- 第三層卷積層:
- 卷積:輸入是 56×56×128,使用 256 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 56×56×256。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 56×56×256,使用 256 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 56×56×256。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 56×56×256,使用 256 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 56×56×256。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 池化:使用 2×2,stride=2 的池化單元進行最大池化操作,輸出為 28×28×256。
- 第四層卷積層:
- 卷積:輸入是 28×28×256,使用 512 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 28×28×512。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 28×28×512,使用 512 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 28×28×512。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 28×28×512,使用 512 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 28×28×512。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 池化:使用 2×2,stride=2 的池化單元進行最大池化操作,輸出為 14×14×512。
- 第五層卷積層:
- 卷積:輸入是 14×14×512,使用 512 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 14×14×512。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 14×14×512,使用 512 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 14×14×512。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 卷積:輸入是 14×14×512,使用 512 個 3×3 的卷積核進行卷積,padding=1,stride=1,輸出是 14×14×512。
- ReLU:將卷積層輸出的 FeatureMap 輸入到 ReLU 函數中。
- 池化:使用 2×2,stride=2 的池化單元進行最大池化操作,輸出為 7×7×512。
- 全連接層:
- FC-4096:第一個全連接層,包含 4096 個神經元(工程上的經驗)。
- FC-4096:第二個全連接層,包含 4096 個神經元。
- FC-1000:第三個全連接層,包含 1000 個神經元,用于輸出分類結果。
3.VGG16_CBAM的定義
class VGG16_CBAM(nn.Module):def __init__(self, num_classes=10, pretrained=True, cbam_ratio=16, cbam_kernel=7):super().__init__()# 加載預訓練VGG16vgg16 = models.vgg16(pretrained=pretrained)# 提取特征部分self.features = vgg16.features# 修改首層卷積以適應32x32輸入(CIFAR10)self.features[0] = nn.Conv2d(3, 64, kernel_size=3, padding=1)# 在每個最大池化層后添加CBAM模塊self.cbam1 = CBAM(in_channels=64, ratio=cbam_ratio, kernel_size=cbam_kernel)self.cbam2 = CBAM(in_channels=128, ratio=cbam_ratio, kernel_size=cbam_kernel)self.cbam3 = CBAM(in_channels=256, ratio=cbam_ratio, kernel_size=cbam_kernel)self.cbam4 = CBAM(in_channels=512, ratio=cbam_ratio, kernel_size=cbam_kernel)self.cbam5 = CBAM(in_channels=512, ratio=cbam_ratio, kernel_size=cbam_kernel)# 分類部分self.classifier = nn.Sequential(nn.Linear(512 * 1 * 1, 4096), # 因為輸入尺寸為32x32,經過多次下采樣后變為1x1nn.ReLU(True),nn.Dropout(),nn.Linear(4096, 4096),nn.ReLU(True),nn.Dropout(0.5),nn.Linear(4096, num_classes),)def forward(self, x):# 第一組卷積 + 池化 + CBAMx = self.features[:5](x) # 到第一個MaxPool2dx = self.cbam1(x)# 第二組卷積 + 池化 + CBAMx = self.features[5:10](x) # 到第二個MaxPool2dx = self.cbam2(x)# 第三組卷積 + 池化 + CBAMx = self.features[10:17](x) # 到第三個MaxPool2dx = self.cbam3(x)# 第四組卷積 + 池化 + CBAMx = self.features[17:24](x) # 到第四個MaxPool2dx = self.cbam4(x)# 第五組卷積 + 池化 + CBAMx = self.features[24:31](x) # 到第五個MaxPool2dx = self.cbam5(x)# 全局平均池化x = nn.AdaptiveAvgPool2d((1, 1))(x)# 展平并分類x = torch.flatten(x, 1)x = self.classifier(x)return x
# 初始化模型并移至設備
model = VGG16_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)
在每一組卷積后+CBAM模塊
4.凍結和微調策略
3階段解凍策略:
- 階段 1:epoch=1 時,解凍注意力模塊 (cbam) 和分類頭 (classifier),使用較高的學習率 (1e-3)
- 階段 2:epoch=6 時,解凍高層特征提取層 (features.24 及之后),學習率降低 (1e-4)
- 階段 3:epoch=21 時,解凍所有層,進行全局微調,學習率進一步降低 (1e-5)
-
階段 解凍時機(epoch) 解凍模塊 學習率 代碼對應邏輯 階段 1 1 cbam、classifier 1e-3 if epoch == 1
分支,調用set_trainable_layers
并設置優化器階段 2 6 新增 features.24 及之后的高層卷積層 1e-4 elif epoch == 6
分支,擴展解凍范圍并降低學習率階段 3 21 所有層 1e-5 elif epoch == 21
分支,設置param.requires_grad = True
并進一步降低學習率
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 = Falsefor part in trainable_parts:if part in name:param.requires_grad = Truebreakdef 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", "classifier"])optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)elif epoch == 6:print("\n" + "="*50 + "\n?? **階段 2:解凍高層特征提取層 (features.24及之后)**\n" + "="*50)set_trainable_layers(model, ["cbam", "classifier", "features.24", "features.25", "features.26", "features.27", "features.28", "features.29", "features.30"])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 = Trueoptimizer = optim.Adam(model.parameters(), lr=1e-5)# --- 訓練循環 ---model.train()running_loss, correct, total = 0.0, 0, 0for 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 / totaltrain_loss_history.append(epoch_train_loss)train_acc_history.append(epoch_train_acc)# --- 測試循環 ---model.eval()test_loss, correct_test, total_test = 0, 0, 0with 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_testtest_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# ======================================================================
# 3. 繪圖函數定義
# ======================================================================
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(Batch序號)')plt.ylabel('損失值')plt.title('每個 Iteration 的訓練損失')plt.legend()plt.grid(True)plt.tight_layout()plt.show()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='訓練準確率')plt.plot(epochs, test_acc, 'r-', label='測試準確率')plt.xlabel('Epoch')plt.ylabel('準確率 (%)')plt.title('訓練和測試準確率')plt.legend(); plt.grid(True)plt.subplot(1, 2, 2)plt.plot(epochs, train_loss, 'b-', label='訓練損失')plt.plot(epochs, test_loss, 'r-', label='測試損失')plt.xlabel('Epoch')plt.ylabel('損失值')plt.title('訓練和測試損失')plt.legend(); plt.grid(True)plt.tight_layout()plt.show()# ======================================================================
# 6. 執行訓練
# ======================================================================
model = VGG16_CBAM().to(device)
criterion = nn.CrossEntropyLoss()
epochs = 50print("開始使用帶分階段微調策略的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")
@浙大疏錦行