@浙大疏錦行https://blog.csdn.net/weixin_45655710
知識點回顧:
- resnet結構解析
- CBAM放置位置的思考
- 針對預訓練模型的訓練策略
- 差異化學習率
- 三階段微調
作業:
- 好好理解下resnet18的模型結構
- 嘗試對vgg16+cbam進行微調策略
ResNet-18 結構核心思想
可以將ResNet-18想象成一個高效的“圖像信息處理流水線”,它分為三個核心部分:
-
“開胃菜” - 輸入預處理 (Stem):
-
組成:一個大的7x7卷積層 (
conv1
) + 一個最大池化層 (maxpool
)。 -
作用:對輸入的原始大尺寸圖像(如224x224)進行一次快速、大刀闊斧的特征提取和尺寸壓縮。它迅速將圖像尺寸減小到56x56,為后續更精細的處理做好準備,像是一道開胃菜,快速打開味蕾。
-
-
“主菜” - 四組殘差塊 (Layer1, 2, 3, 4):
-
組成:這是ResNet的心臟,由四組
Sequential
模塊構成,每組里面包含2個BasicBlock
(殘差塊)。 -
作用:這是真正進行深度特征提取的地方。其最精妙的設計在于:
-
層級遞進:從
layer1
到layer4
,特征圖的空間尺寸逐級減半(56→28→14→7),而通道數逐級翻倍(64→128→256→512)。這實現了“犧牲空間細節,換取更高層語義信息”的經典策略。 -
殘差連接:每個
BasicBlock
內部的“跳躍連接”(out += identity
)是其靈魂。它允許信息和梯度“抄近道”,直接從塊的輸入流向輸出,完美解決了深度網絡中因信息丟失導致的“網絡退化”和梯度消失問題。
-
-
-
“甜點” - 分類頭 (Head):
-
組成:一個全局平均池化層 (
avgpool
) + 一個全連接層 (fc
)。 -
作用:
-
avgpool
:將layer4
輸出的512x7x7
的復雜特征圖,暴力壓縮成一個512
維的特征向量,濃縮了整張圖最高級的語義信息。 -
fc
:扮演最終“裁判”的角色,將這個512維的特征向量映射到最終的類別得分上(例如,ImageNet的1000類)。
-
-
總結來說,ResNet-18的優雅之處在于其清晰的模塊化設計和革命性的殘差連接,它通過“尺寸減半,通道加倍”的策略逐層加深語義理解,并利用“跳躍連接”保證了信息流的暢通,從而能夠構建出既深又易于訓練的強大網絡。
對VGG16 + CBAM 進行微調
VGG16以其結構統一、簡單(全是3x3卷積和2x2池化)而著稱,但缺點是參數量巨大。我們將為其集成CBAM,并應用類似的分階段微調策略。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
import time
from tqdm import tqdm# --- 模塊定義 (CBAM 和數據加載器,與之前一致) ---
class ChannelAttention(nn.Module):def __init__(self, in_channels, ratio=16):super().__init__()self.avg_pool = nn.AdaptiveAvgPool2d(1)self.max_pool = nn.AdaptiveMaxPool2d(1)self.fc = nn.Sequential(nn.Linear(in_channels, in_channels // ratio, bias=False), nn.ReLU(),nn.Linear(in_channels // ratio, in_channels, bias=False))self.sigmoid = nn.Sigmoid()def forward(self, x):b, c, _, _ = x.shapeavg_out = self.fc(self.avg_pool(x).view(b, c))max_out = self.fc(self.max_pool(x).view(b, c))attention = self.sigmoid(avg_out + max_out).view(b, c, 1, 1)return x * attentionclass 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)pool_out = torch.cat([avg_out, max_out], dim=1)attention = self.conv(pool_out)return x * self.sigmoid(attention)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):return self.spatial_attn(self.channel_attn(x))def get_cifar10_loaders(batch_size=64, resize_to=224): # VGG需要224x224輸入print(f"--- 正在準備數據 (圖像將縮放至 {resize_to}x{resize_to}) ---")transform = transforms.Compose([transforms.Resize(resize_to),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)test_dataset = datasets.CIFAR10(root='./data', train=False, transform=transform)train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=2, pin_memory=True)print("? 數據加載器準備完成。")return train_loader, test_loader# --- 新增:VGG16 + CBAM 模型定義 ---
class VGG16_CBAM(nn.Module):def __init__(self, num_classes=10, pretrained=True):super().__init__()# 加載預訓練的VGG16的特征提取部分vgg_features = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1 if pretrained else None).features# 我們將VGG的特征提取層按池化層分割,并在每個塊后插入CBAMself.features = nn.ModuleList()self.cbam_modules = nn.ModuleList()current_channels = 3vgg_block = []for layer in vgg_features:vgg_block.append(layer)if isinstance(layer, nn.Conv2d):current_channels = layer.out_channelsif isinstance(layer, nn.MaxPool2d):self.features.append(nn.Sequential(*vgg_block))self.cbam_modules.append(CBAM(current_channels))vgg_block = [] # 開始新的塊# VGG的分類器部分self.avgpool = nn.AdaptiveAvgPool2d((7, 7))self.classifier = nn.Sequential(nn.Linear(512 * 7 * 7, 4096), nn.ReLU(True), nn.Dropout(),nn.Linear(4096, 4096), nn.ReLU(True), nn.Dropout(),nn.Linear(4096, num_classes),)def forward(self, x):for feature_block, cbam_module in zip(self.features, self.cbam_modules):x = feature_block(x)x = cbam_module(x)x = self.avgpool(x)x = torch.flatten(x, 1)x = self.classifier(x)return x# --- 訓練和評估框架 (復用) ---
def run_experiment(model_name, model, device, train_loader, test_loader, epochs):print(f"\n{'='*25} 開始實驗: {model_name} {'='*25}")model.to(device)total_params = sum(p.numel() for p in model.parameters())print(f"模型總參數量: {total_params / 1e6:.2f}M")criterion = nn.CrossEntropyLoss()# 差異化學習率:為不同的部分設置不同的學習率optimizer = optim.Adam([{'params': model.features.parameters(), 'lr': 1e-5}, # 特征提取層使用極低學習率{'params': model.cbam_modules.parameters(), 'lr': 1e-4}, # CBAM模塊使用中等學習率{'params': model.classifier.parameters(), 'lr': 1e-3} # 分類頭使用較高學習率])for epoch in range(1, epochs + 1):model.train()loop = tqdm(train_loader, desc=f"Epoch [{epoch}/{epochs}] Training", leave=False)for data, target in loop:data, target = data.to(device), target.to(device)optimizer.zero_grad()output = model(data)loss = criterion(output, target)loss.backward()optimizer.step()loop.set_postfix(loss=loss.item())loop.close()model.eval()test_loss, correct = 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() * data.size(0)pred = output.argmax(dim=1)correct += pred.eq(target).sum().item()avg_test_loss = test_loss / len(test_loader.dataset)accuracy = 100. * correct / len(test_loader.dataset)print(f"Epoch {epoch} 完成 | 測試集損失: {avg_test_loss:.4f} | 測試集準確率: {accuracy:.2f}%")# --- 主執行流程 ---
if __name__ == "__main__":DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")EPOCHS = 10 # 僅作演示,VGG需要更多輪次BATCH_SIZE = 32 # VGG參數量大,減小batch size防止顯存溢出train_loader, test_loader = get_cifar10_loaders(batch_size=BATCH_SIZE)vgg_cbam_model = VGG16_CBAM()run_experiment("VGG16+CBAM", vgg_cbam_model, DEVICE, train_loader, test_loader, EPOCHS)
VGG16+CBAM 微調策略解析
-
模型修改 (
VGG16_CBAM
):-
拆分與重組:VGG16的預訓練模型中,特征提取部分
model.features
是一個包含所有卷積和池化層的nn.Sequential
。我們不能直接在中間插入CBAM。因此,我們遍歷了vgg_features
中的所有層,以MaxPool2d
為界,將它們拆分成了5個卷積塊。 -
插入CBAM:在每個卷積塊之后,我們都插入了一個對應通道數的
CBAM
模塊。 -
保留分類頭:原始的
model.classifier
(全連接層)被保留,只修改最后一層以適應CIFAR-10的10個類別。
-
-
數據預處理適配:
-
VGG16在ImageNet上預訓練時,接收的是
224x224
的圖像。為了最大化利用預訓練權重,我們在get_cifar10_loaders
函數中,通過transforms.Resize(224)
將CIFAR-10的32x32
圖像放大到224x224
。
-
-
訓練策略:差異化學習率
-
由于VGG16的參數量巨大(超過1.3億),如果全局使用相同的學習率進行微調,很容易破壞已經學得很好的預訓練權重。
-
我們采用了一種更精細的差異化學習率 (Differential Learning Rates) 策略:
-
特征提取層 (
model.features
):這些是“資深專家”,權重已經很好了,我們給一個極低的學習率(1e-5
),讓它們只做微小的調整。 -
CBAM模塊 (
model.cbam_modules
):這些是新加入的“顧問”,需要學習,但不能太激進,給一個中等學習率(1e-4
)。 -
分類頭 (
model.classifier
):這是完全為新任務定制的“新員工”,需要從頭快速學習,給一個較高的學習率(1e-3
)。
-
-
這種策略通過
optim.Adam
接收一個參數組列表來實現,是微調大型模型時非常有效且常用的高級技巧。
-
-
Batch Size調整:
批次大小調整 :-
VGG16的參數量和中間激活值都非常大,對顯存的消耗遠超ResNet18。因此,我們將
BATCH_SIZE
減小到32
,以防止顯存溢出(OOM)錯誤。
-
通過這個實驗,不僅能實踐如何將注意力模塊集成到一個全新的經典網絡(VGG16)中,還能學習到微調大型模型時更高級、更精細的訓練策略,如差異化學習率。