DAY 46 通道注意力(SE注意力)
內容:
- 不同CNN層的特征圖:不同通道的特征圖
- 什么是注意力:注意力家族,類似于動物園,都是不同的模塊,好不好試了才知道。
- 通道注意力:模型的定義和插入的位置
- 通道注意力后的特征圖和熱力圖
一、什么是注意力
注意力機制(Attention Mechanism)是深度學習中的一種重要技術,最初應用于自然語言處理(NLP),后來擴展到計算機視覺、語音識別等多個領域。它的核心思想是讓模型能夠動態地關注輸入數據中最相關的部分,從而提高模型的性能和解釋性。
1. 核心思想
- 選擇性關注:模仿人類注意力機制,在處理信息時對不同部分賦予不同的權重,忽略無關信息,聚焦關鍵部分。
- 動態權重:權重不是固定的,而是根據輸入數據的內容動態計算。
2. 基本原理
注意力機制通過以下步驟實現:
- 計算注意力分數:衡量輸入中每個部分與當前任務的相關性。? ? ? ?常用方法:點積(Dot-Product)、加性(Additive)、縮放點積(Scaled Dot-Product)等。
- 生成注意力權重:對分數進行歸一化(如Softmax),得到權重分布。
- 加權求和:用權重對輸入進行加權融合,得到上下文向量(Context Vector)。
其中注意力機制是一種讓模型學會「選擇性關注重要信息」的特征提取器,就像人類視覺會自動忽略背景,聚焦于圖片中的主體(如貓、汽車)。
transformer中的叫做自注意力機制,他是一種自己學習自己的機制,他可以自動學習到圖片中的主體,并忽略背景。我們現在說的很多模塊,比如通道注意力、空間注意力、通道注意力等等,都是基于自注意力機制的。
從數學角度看,注意力機制是對輸入特征進行加權求和,輸出=∑(輸入特征×注意力權重),其中注意力權重是學習到的。所以他和卷積很像,因為卷積也是一種加權求和。但是卷積是 “固定權重” 的特征提取(如 3x3 卷積核)--訓練完了就結束了,注意力是 “動態權重” 的特征提取(權重隨輸入數據變化)---輸入數據不同權重不同。
問:為什么需要多種注意力模塊?
答:因為不同場景下的關鍵信息分布不同。例如,識別鳥類和飛機時,需關注 “羽毛紋理”“金屬光澤” 等特定通道的特征,通道注意力可強化關鍵通道;而物體位置不確定時(如貓出現在圖像不同位置),空間注意力能聚焦物體所在區域,忽略背景。復雜場景中,可能需要同時關注通道和空間(如混合注意力模塊 CBAM),或處理長距離依賴(如全局注意力模塊 Non-local)。
問:為什么不設計一個‘萬能’注意力模塊?
答:主要受效率和靈活性限制。專用模塊針對特定需求優化計算,成本更低(如通道注意力僅需處理通道維度,無需全局位置計算);不同任務的核心需求差異大(如醫學圖像側重空間定位,自然語言處理側重語義長距離依賴),通用模塊可能冗余或低效。每個模塊新增的權重會增加模型參數量,若訓練數據不足或優化不當,可能引發過擬合。因此實際應用中需結合輕量化設計(如減少全連接層參數)、正則化(如 Dropout)或結構約束(如共享注意力權重)來平衡性能與復雜度。
通道注意力(Channel Attention)屬于注意力機制(Attention Mechanism)的變體,而非自注意力(Self-Attention)的直接變體。可以理解為注意力是一個動物園算法,里面很多個物種,自注意力只是一個分支,因為開創了transformer所以備受矚目。我們今天的內容用通道注意力舉例
- CNN普通卷積:像一個人無差別地聽所有音樂頻道。
- 通道注意力:像用遙控器調音量——放大流行音樂,調低新聞頻道。
注意力模塊 | 所屬類別 | 核心功能 |
自注意力(Self-Attention) | 自注意力變體 | 建模同一輸入內部元素的依賴關系(如序列中的詞與詞、圖像中的區塊之間)。 |
通道注意力(Channel Attention) | 普通注意力變體(全局上下文) | 動態評估特征圖各通道的重要性,通過全局池化壓縮空間信息,強化關鍵特征通道。 |
空間注意力(Spatial Attention) | 普通注意力變體(全局上下文) | 聚焦特征圖的空間關鍵區域(如圖像中物體的位置),回答“哪里更重要”。 |
多頭注意力(Multi-Head Attention) | 自注意力/普通注意力的增強版 | 將輸入投影到多個子空間并行計算注意力,捕捉不同維度的依賴關系(如局部和全局特征)。 |
編碼器-解碼器注意力(Encoder-Decoder Attention) | 普通注意力變體(交叉注意力) | 建立編碼器輸出與解碼器輸入的跨模態對齊(如機器翻譯中源語言與目標語言的詞對應)。 |
二、特征圖的提取
2.1 簡單CNN的訓練
先看看之前原始的CNN
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# 設置中文字體支持
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}")# 1. 數據預處理
# 訓練集:使用多種數據增強方法提高模型泛化能力
train_transform = transforms.Compose([# 隨機裁剪圖像,從原圖中隨機截取32x32大小的區域transforms.RandomCrop(32, padding=4),# 隨機水平翻轉圖像(概率0.5)transforms.RandomHorizontalFlip(),# 隨機顏色抖動:亮度、對比度、飽和度和色調隨機變化transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),# 隨機旋轉圖像(最大角度15度)transforms.RandomRotation(15),# 將PIL圖像或numpy數組轉換為張量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))
])# 2. 加載CIFAR-10數據集
train_dataset = datasets.CIFAR10(root='./data',train=True,download=True,transform=train_transform # 使用增強后的預處理
)test_dataset = datasets.CIFAR10(root='./data',train=False,transform=test_transform # 測試集不使用增強
)# 3. 創建數據加載器
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 4. 定義CNN模型的定義(替代原MLP)
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__() # 繼承父類初始化# ---------------------- 第一個卷積塊 ----------------------# 卷積層1:輸入3通道(RGB),輸出32個特征圖,卷積核3x3,邊緣填充1像素self.conv1 = nn.Conv2d(in_channels=3, # 輸入通道數(圖像的RGB通道)out_channels=32, # 輸出通道數(生成32個新特征圖)kernel_size=3, # 卷積核尺寸(3x3像素)padding=1 # 邊緣填充1像素,保持輸出尺寸與輸入相同)# 批量歸一化層:對32個輸出通道進行歸一化,加速訓練self.bn1 = nn.BatchNorm2d(num_features=32)# ReLU激活函數:引入非線性,公式:max(0, x)self.relu1 = nn.ReLU()# 最大池化層:窗口2x2,步長2,特征圖尺寸減半(32x32→16x16)self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # stride默認等于kernel_size# ---------------------- 第二個卷積塊 ----------------------# 卷積層2:輸入32通道(來自conv1的輸出),輸出64通道self.conv2 = nn.Conv2d(in_channels=32, # 輸入通道數(前一層的輸出通道數)out_channels=64, # 輸出通道數(特征圖數量翻倍)kernel_size=3, # 卷積核尺寸不變padding=1 # 保持尺寸:16x16→16x16(卷積后)→8x8(池化后))self.bn2 = nn.BatchNorm2d(num_features=64)self.relu2 = nn.ReLU()self.pool2 = nn.MaxPool2d(kernel_size=2) # 尺寸減半:16x16→8x8# ---------------------- 第三個卷積塊 ----------------------# 卷積層3:輸入64通道,輸出128通道self.conv3 = nn.Conv2d(in_channels=64, # 輸入通道數(前一層的輸出通道數)out_channels=128, # 輸出通道數(特征圖數量再次翻倍)kernel_size=3,padding=1 # 保持尺寸:8x8→8x8(卷積后)→4x4(池化后))self.bn3 = nn.BatchNorm2d(num_features=128)self.relu3 = nn.ReLU() # 復用激活函數對象(節省內存)self.pool3 = nn.MaxPool2d(kernel_size=2) # 尺寸減半:8x8→4x4# ---------------------- 全連接層(分類器) ----------------------# 計算展平后的特征維度:128通道 × 4x4尺寸 = 128×16=2048維self.fc1 = nn.Linear(in_features=128 * 4 * 4, # 輸入維度(卷積層輸出的特征數)out_features=512 # 輸出維度(隱藏層神經元數))# Dropout層:訓練時隨機丟棄50%神經元,防止過擬合self.dropout = nn.Dropout(p=0.5)# 輸出層:將512維特征映射到10個類別(CIFAR-10的類別數)self.fc2 = nn.Linear(in_features=512, out_features=10)def forward(self, x):# 輸入尺寸:[batch_size, 3, 32, 32](batch_size=批量大小,3=通道數,32x32=圖像尺寸)# ---------- 卷積塊1處理 ----------x = self.conv1(x) # 卷積后尺寸:[batch_size, 32, 32, 32](padding=1保持尺寸)x = self.bn1(x) # 批量歸一化,不改變尺寸x = self.relu1(x) # 激活函數,不改變尺寸x = self.pool1(x) # 池化后尺寸:[batch_size, 32, 16, 16](32→16是因為池化窗口2x2)# ---------- 卷積塊2處理 ----------x = self.conv2(x) # 卷積后尺寸:[batch_size, 64, 16, 16](padding=1保持尺寸)x = self.bn2(x)x = self.relu2(x)x = self.pool2(x) # 池化后尺寸:[batch_size, 64, 8, 8]# ---------- 卷積塊3處理 ----------x = self.conv3(x) # 卷積后尺寸:[batch_size, 128, 8, 8](padding=1保持尺寸)x = self.bn3(x)x = self.relu3(x)x = self.pool3(x) # 池化后尺寸:[batch_size, 128, 4, 4]# ---------- 展平與全連接層 ----------# 將多維特征圖展平為一維向量:[batch_size, 128*4*4] = [batch_size, 2048]x = x.view(-1, 128 * 4 * 4) # -1自動計算批量維度,保持批量大小不變x = self.fc1(x) # 全連接層:2048→512,尺寸變為[batch_size, 512]x = self.relu3(x) # 激活函數(復用relu3,與卷積塊3共用)x = self.dropout(x) # Dropout隨機丟棄神經元,不改變尺寸x = self.fc2(x) # 全連接層:512→10,尺寸變為[batch_size, 10](未激活,直接輸出logits)return x # 輸出未經過Softmax的logits,適用于交叉熵損失函數# 初始化模型
model = CNN()
model = model.to(device) # 將模型移至GPU(如果可用)criterion = nn.CrossEntropyLoss() # 交叉熵損失函數
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam優化器# 引入學習率調度器,在訓練過程中動態調整學習率--訓練初期使用較大的 LR 快速降低損失,訓練后期使用較小的 LR 更精細地逼近全局最優解。
# 在每個 epoch 結束后,需要手動調用調度器來更新學習率,可以在訓練過程中調用 scheduler.step()
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, # 指定要控制的優化器(這里是Adam)mode='min', # 監測的指標是"最小化"(如損失函數)patience=3, # 如果連續3個epoch指標沒有改善,才降低LRfactor=0.5 # 降低LR的比例(新LR = 舊LR × 0.5)
)
# 5. 訓練模型(記錄每個 iteration 的損失)
def train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs):model.train() # 設置為訓練模式# 記錄每個 iteration 的損失all_iter_losses = [] # 存儲所有 batch 的損失iter_indices = [] # 存儲 iteration 序號# 記錄每個 epoch 的準確率和損失train_acc_history = []test_acc_history = []train_loss_history = []test_loss_history = []for epoch in range(epochs):running_loss = 0.0correct = 0total = 0for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device) # 移至GPUoptimizer.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 * 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個批次打印一次訓練信息if (batch_idx + 1) % 100 == 0:print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} 'f'| 單Batch損失: {iter_loss:.4f} | 累計平均損失: {running_loss/(batch_idx+1):.4f}')# 計算當前epoch的平均訓練損失和準確率epoch_train_loss = running_loss / len(train_loader)epoch_train_acc = 100. * correct / totaltrain_acc_history.append(epoch_train_acc)train_loss_history.append(epoch_train_loss)# 測試階段model.eval() # 設置為評估模式test_loss = 0correct_test = 0total_test = 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_acc_history.append(epoch_test_acc)test_loss_history.append(epoch_test_loss)# 更新學習率調度器scheduler.step(epoch_test_loss)print(f'Epoch {epoch+1}/{epochs} 完成 | 訓練準確率: {epoch_train_acc:.2f}% | 測試準確率: {epoch_test_acc:.2f}%')# 繪制所有 iteration 的損失曲線plot_iter_losses(all_iter_losses, iter_indices)# 繪制每個 epoch 的準確率和損失曲線plot_epoch_metrics(train_acc_history, test_acc_history, train_loss_history, test_loss_history)return epoch_test_acc # 返回最終測試準確率# 6. 繪制每個 iteration 的損失曲線
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()# 7. 繪制每個 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='訓練準確率')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()# 8. 執行訓練和測試
epochs = 50 # 增加訓練輪次為了確保收斂
print("開始使用CNN訓練模型...")
final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs)
print(f"訓練完成!最終測試準確率: {final_accuracy:.2f}%")# # 保存模型
# torch.save(model.state_dict(), 'cifar10_cnn_model.pth')
# print("模型已保存為: cifar10_cnn_model.pth")
可以看到測試集一定程度上收斂了,在85%左右(還可以繼續訓練的),我們后續和加了通道注意力的該模型作對比,這也意味著我們進入到了消融實驗的部分了。
過去我們都是在同一個數據集上對比不同的模型的差異,或者同一個模型不同參數下的差異,這種實驗叫做對比實驗。
在同一個數據集上,對同一個模型進行模塊的增加和減少,這種實驗我們稱之為消融實驗。通過消融實驗,研究者能更清晰地理解模型各部分的作用,而對比實驗則用于評估模型的整體競爭力。兩者常結合使用,以全面驗證模型設計的合理性。
2.2 特征圖可視化
為了方便觀察,我們先嘗試提取下特征圖。特征圖本質就是不同的卷積核的輸出,淺層指的是離輸入圖近的卷積層,淺層卷積層的特征圖通常較大,而深層特征圖會經過多次下采樣,尺寸顯著縮小,尺寸差異過大時,小尺寸特征圖在視覺上會顯得模糊或丟失細節。
步驟邏輯如下:
1.初始化設置:
- 將模型設為評估模式,準備類別名稱列表(如飛機、汽車等)。
2.數據加載與處理:
- 從測試數據加載器中獲取圖像和標簽。
- 僅處理前 `num_images` 張圖像(如2張)。
3.注冊鉤子捕獲特征圖:
- 為指定層(如 `conv1`, `conv2`, `conv3`)注冊前向鉤子。
- 鉤子函數將這些層的輸出(特征圖)保存到字典中。
4.前向傳播與特征提取:
- 模型處理圖像,觸發鉤子函數,獲取并保存特征圖。
- 移除鉤子,避免后續干擾。
5.可視化特征圖:
對每張圖像:
- 恢復原始像素值并顯示。
- 為每個目標層創建子圖,展示前 `num_channels` 個通道的特征圖(如9個通道)。
- 每個通道的特征圖以網格形式排列,顯示通道編號。
關鍵細節
特征圖布局:原始圖像在左側,各層特征圖按順序排列在右側。
通道選擇:默認顯示前9個通道(按重要性或索引排序)。
顯示優化:
- 使用 `inset_axes` 在大圖中嵌入小網格,清晰展示每個通道。
- 層標題與通道標題分開,避免重疊。
- 反標準化處理恢復圖像原始色彩。
def visualize_feature_maps(model, test_loader, device, layer_names, num_images=3, num_channels=9):"""可視化指定層的特征圖(修復循環冗余問題)參數:model: 模型test_loader: 測試數據加載器layer_names: 要可視化的層名稱(如['conv1', 'conv2', 'conv3'])num_images: 可視化的圖像總數num_channels: 每個圖像顯示的通道數(取前num_channels個通道)"""model.eval() # 設置為評估模式class_names = ['飛機', '汽車', '鳥', '貓', '鹿', '狗', '青蛙', '馬', '船', '卡車']# 從測試集加載器中提取指定數量的圖像(避免嵌套循環)images_list, labels_list = [], []for images, labels in test_loader:images_list.append(images)labels_list.append(labels)if len(images_list) * test_loader.batch_size >= num_images:break# 拼接并截取到目標數量images = torch.cat(images_list, dim=0)[:num_images].to(device)labels = torch.cat(labels_list, dim=0)[:num_images].to(device)with torch.no_grad():# 存儲各層特征圖feature_maps = {}# 保存鉤子句柄hooks = []# 定義鉤子函數,捕獲指定層的輸出def hook(module, input, output, name):feature_maps[name] = output.cpu() # 保存特征圖到字典# 為每個目標層注冊鉤子,并保存鉤子句柄for name in layer_names:module = getattr(model, name)hook_handle = module.register_forward_hook(lambda m, i, o, n=name: hook(m, i, o, n))hooks.append(hook_handle)# 前向傳播觸發鉤子_ = model(images)# 正確移除鉤子for hook_handle in hooks:hook_handle.remove()# 可視化每個圖像的各層特征圖(僅一層循環)for img_idx in range(num_images):img = images[img_idx].cpu().permute(1, 2, 0).numpy()# 反標準化處理(恢復原始像素值)img = img * np.array([0.2023, 0.1994, 0.2010]).reshape(1, 1, 3) + np.array([0.4914, 0.4822, 0.4465]).reshape(1, 1, 3)img = np.clip(img, 0, 1) # 確保像素值在[0,1]范圍內# 創建子圖num_layers = len(layer_names)fig, axes = plt.subplots(1, num_layers + 1, figsize=(4 * (num_layers + 1), 4))# 顯示原始圖像axes[0].imshow(img)axes[0].set_title(f'原始圖像\n類別: {class_names[labels[img_idx]]}')axes[0].axis('off')# 顯示各層特征圖for layer_idx, layer_name in enumerate(layer_names):fm = feature_maps[layer_name][img_idx] # 取第img_idx張圖像的特征圖fm = fm[:num_channels] # 僅取前num_channels個通道num_rows = int(np.sqrt(num_channels))num_cols = num_channels // num_rows if num_rows != 0 else 1# 創建子圖網格layer_ax = axes[layer_idx + 1]layer_ax.set_title(f'{layer_name}特征圖 \n')# 加個換行讓文字分離上去layer_ax.axis('off') # 關閉大子圖的坐標軸# 在大子圖內創建小網格for ch_idx, channel in enumerate(fm):ax = layer_ax.inset_axes([ch_idx % num_cols / num_cols, (num_rows - 1 - ch_idx // num_cols) / num_rows, 1/num_cols, 1/num_rows])ax.imshow(channel.numpy(), cmap='viridis')ax.set_title(f'通道 {ch_idx + 1}')ax.axis('off')plt.tight_layout()plt.show()# 調用示例(按需修改參數)
layer_names = ['conv1', 'conv2', 'conv3']
visualize_feature_maps(model=model,test_loader=test_loader,device=device,layer_names=layer_names,num_images=5, # 可視化5張測試圖像 → 輸出5張大圖num_channels=9 # 每張圖像顯示前9個通道的特征圖
)
上面的圖為提取CNN不同卷積層輸出的特征圖,我們以第五張圖片-青蛙 進行解讀。
由于經過了不斷的下采樣,特征變得越來越抽象,人類已經無法理解。
核心作用
通過可視化特征圖,可直觀觀察:
- 淺層卷積層(如 `conv1`)如何捕獲邊緣、紋理等低級特征。
- 深層卷積層(如 `conv3`)如何組合低級特征形成語義概念(如物體部件)。
- 模型對不同類別的關注區域差異(如鳥類的羽毛紋理 vs. 飛機的金屬光澤)。
conv1 特征圖(淺層卷積)
特點:
- 保留較多原始圖像的細節紋理(如植物葉片、青蛙身體的邊緣輪廓)。
- 通道間差異相對小,每個通道都能看到類似原始圖像的基礎結構(如通道 1 - 9 都能識別邊緣、紋理)。
意義:
- 提取低級特征(邊緣、顏色塊、簡單紋理),是后續高層特征的“原材料”。
- 類似人眼初步識別圖像的輪廓和基礎結構。
conv2 特征圖(中層卷積)
特點:
- 空間尺寸(高、寬)比 conv1 更小(因卷積/池化下采樣),但語義信息更抽象。
- 通道間差異更明顯:部分通道開始聚焦局部關鍵特征(如通道 5、8 中黃色高亮區域,可能對應青蛙身體或植物的關鍵紋理)。
意義:
- 對 conv1 的低級特征進行組合與篩選,提取中級特征(如局部形狀、紋理組合)。
- 類似人眼從“邊緣輪廓”過渡到“識別局部結構”(如青蛙的身體塊、植物的葉片簇)。
conv3 特征圖(深層卷積)
特點:
- 空間尺寸進一步縮小,抽象程度最高,肉眼難直接對應原始圖像細節。
- 通道間差異極大,部分通道聚焦全局語義特征(如通道 4、7 中黃色區域,可能對應模型判斷“青蛙”類別的關鍵特征)。
意義:
- 對 conv2 的中級特征進行全局整合,提取高級語義特征(如物體類別相關的抽象模式)。
- 類似人眼最終“識別出這是青蛙”的關鍵依據,模型通過這些特征判斷類別。
逐層對比總結
特征逐層抽象:從“看得見的細節”(conv1)→ “局部結構”(conv2)→ “類別相關的抽象模式”(conv3),模型通過這種方式實現從“看圖像”到“理解語義”的跨越。
通道分工明確:不同通道在各層聚焦不同特征(如有的通道負責邊緣,有的負責顏色,有的負責全局語義),共同協作完成分類任務。
下采樣的作用:通過縮小空間尺寸,換取更高的語義抽象能力(“犧牲細節,換取理解”)。
三、通道注意力
現在引入通道注意力,來觀察精度是否有變化,并且進一步可視化。
想要把通道注意力插入到模型中,關鍵步驟如下:
- 定義注意力模塊
- 重寫之前的模型定義部分,確定好模塊插入的位置
3.1 通道注意力的定義
# ===================== 新增:通道注意力模塊(SE模塊) =====================
class ChannelAttention(nn.Module):"""通道注意力模塊(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_ratio=16):"""參數:in_channels: 輸入特征圖的通道數reduction_ratio: 降維比例,用于減少參數量"""super(ChannelAttention, self).__init__()# 全局平均池化 - 將空間維度壓縮為1x1,保留通道信息self.avg_pool = nn.AdaptiveAvgPool2d(1)# 全連接層 + 激活函數,用于學習通道間的依賴關系self.fc = nn.Sequential(# 降維:壓縮通道數,減少計算量nn.Linear(in_channels, in_channels // reduction_ratio, bias=False),nn.ReLU(inplace=True),# 升維:恢復原始通道數nn.Linear(in_channels // reduction_ratio, in_channels, bias=False),# Sigmoid將輸出值歸一化到[0,1],表示通道重要性權重nn.Sigmoid())def forward(self, x):"""參數:x: 輸入特征圖,形狀為 [batch_size, channels, height, width]返回:加權后的特征圖,形狀不變"""batch_size, channels, height, width = x.size()# 1. 全局平均池化:[batch_size, channels, height, width] → [batch_size, channels, 1, 1]avg_pool_output = self.avg_pool(x)# 2. 展平為一維向量:[batch_size, channels, 1, 1] → [batch_size, channels]avg_pool_output = avg_pool_output.view(batch_size, channels)# 3. 通過全連接層學習通道權重:[batch_size, channels] → [batch_size, channels]channel_weights = self.fc(avg_pool_output)# 4. 重塑為二維張量:[batch_size, channels] → [batch_size, channels, 1, 1]channel_weights = channel_weights.view(batch_size, channels, 1, 1)# 5. 將權重應用到原始特征圖上(逐通道相乘)return x * channel_weights # 輸出形狀:[batch_size, channels, height, width]
通道注意力模塊的核心原理
1. Squeeze(壓縮):
- 通過全局平均池化將每個通道的二維特征圖(H×W)壓縮為一個標量,保留通道的全局信息。
- 物理意義:計算每個通道在整個圖像中的 “平均響應強度”,例如,“邊緣檢測通道” 在有物體邊緣的圖像中響應值會更高。
2. Excitation(激發):
- 通過全連接層 + Sigmoid 激活,學習通道間的依賴關系,輸出 0-1 之間的權重值。
- 物理意義:讓模型自動判斷哪些通道更重要(權重接近 1),哪些通道可忽略(權重接近 0)。
3. Reweight(重加權):
- 將學習到的通道權重與原始特征圖逐通道相乘,增強重要通道,抑制不重要通道。
- 物理意義:類似人類視覺系統聚焦于關鍵特征(如貓的輪廓),忽略無關特征(如背景顏色)
通道注意力插入后,參數量略微提高,增加了特征提取能力
3.2 模型的重新定義(通道注意力的插入)
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__() # ---------------------- 第一個卷積塊 ----------------------self.conv1 = nn.Conv2d(3, 32, 3, padding=1)self.bn1 = nn.BatchNorm2d(32)self.relu1 = nn.ReLU()# 新增:插入通道注意力模塊(SE模塊)self.ca1 = ChannelAttention(in_channels=32, reduction_ratio=16) self.pool1 = nn.MaxPool2d(2, 2) # ---------------------- 第二個卷積塊 ----------------------self.conv2 = nn.Conv2d(32, 64, 3, padding=1)self.bn2 = nn.BatchNorm2d(64)self.relu2 = nn.ReLU()# 新增:插入通道注意力模塊(SE模塊)self.ca2 = ChannelAttention(in_channels=64, reduction_ratio=16) self.pool2 = nn.MaxPool2d(2) # ---------------------- 第三個卷積塊 ----------------------self.conv3 = nn.Conv2d(64, 128, 3, padding=1)self.bn3 = nn.BatchNorm2d(128)self.relu3 = nn.ReLU()# 新增:插入通道注意力模塊(SE模塊)self.ca3 = ChannelAttention(in_channels=128, reduction_ratio=16) self.pool3 = nn.MaxPool2d(2) # ---------------------- 全連接層(分類器) ----------------------self.fc1 = nn.Linear(128 * 4 * 4, 512)self.dropout = nn.Dropout(p=0.5)self.fc2 = nn.Linear(512, 10)def forward(self, x):# ---------- 卷積塊1處理 ----------x = self.conv1(x) x = self.bn1(x) x = self.relu1(x) x = self.ca1(x) # 應用通道注意力x = self.pool1(x) # ---------- 卷積塊2處理 ----------x = self.conv2(x) x = self.bn2(x) x = self.relu2(x) x = self.ca2(x) # 應用通道注意力x = self.pool2(x) # ---------- 卷積塊3處理 ----------x = self.conv3(x) x = self.bn3(x) x = self.relu3(x) x = self.ca3(x) # 應用通道注意力x = self.pool3(x) # ---------- 展平與全連接層 ----------x = x.view(-1, 128 * 4 * 4) x = self.fc1(x) x = self.relu3(x) x = self.dropout(x) x = self.fc2(x) return x # 重新初始化模型,包含通道注意力模塊
model = CNN()
model = model.to(device) # 將模型移至GPU(如果可用)criterion = nn.CrossEntropyLoss() # 交叉熵損失函數
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam優化器# 引入學習率調度器,在訓練過程中動態調整學習率--訓練初期使用較大的 LR 快速降低損失,訓練后期使用較小的 LR 更精細地逼近全局最優解。
# 在每個 epoch 結束后,需要手動調用調度器來更新學習率,可以在訓練過程中調用 scheduler.step()
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, # 指定要控制的優化器(這里是Adam)mode='min', # 監測的指標是"最小化"(如損失函數)patience=3, # 如果連續3個epoch指標沒有改善,才降低LRfactor=0.5 # 降低LR的比例(新LR = 舊LR × 0.5)
)# 訓練模型(復用原有的train函數)
print("開始訓練帶通道注意力的CNN模型...")
final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs=50)
print(f"訓練完成!最終測試準確率: {final_accuracy:.2f}%")
在同樣50個epoch后精度略有提升
我們關注的不只是精度的差異,還包含了同精度下訓練時長的差異等,在大規模數據集上推理時長、訓練時長都非常重要。因為資源是有限的。
可視化部分同理,在訓練完成后通過鉤子函數取出權重or梯度,即可進行特征圖的可視化、Grad-CAM可視化、注意力熱圖可視化
@浙大疏錦行