Python——day46通道注意力(SE注意力)

一、 什么是注意力

???注意力機制是一種讓模型學會「選擇性關注重要信息」的特征提取器,就像人類視覺會自動忽略背景,聚焦于圖片中的主體(如貓、汽車)。 transformer中的叫做自注意力機制,他是一種自己學習自己的機制,他可以自動學習到圖片中的主體,并忽略背景。我們現在說的很多模塊,比如通道注意力、空間注意力、通道注意力等等,都是基于自注意力機制的。

從數學角度看,注意力機制是對輸入特征進行加權求和,輸出=∑(輸入特征×注意力權重),其中注意力權重是學習到的。所以他和卷積很像,因為卷積也是一種加權求和。但是卷積是 “固定權重” 的特征提取(如 3x3 卷積核)--訓練完了就結束了,注意力是 “動態權重” 的特征提取(權重隨輸入數據變化)---輸入數據不同權重不同。

問:為什么需要多種注意力模塊? 答:因為不同場景下的關鍵信息分布不同。例如,識別鳥類和飛機時,需關注 “羽毛紋理”“金屬光澤” 等特定通道的特征,通道注意力可強化關鍵通道;而物體位置不確定時(如貓出現在圖像不同位置),空間注意力能聚焦物體所在區域,忽略背景。復雜場景中,可能需要同時關注通道和空間(如混合注意力模塊 CBAM),或處理長距離依賴(如全局注意力模塊 Non-local)。

問:為什么不設計一個‘萬能’注意力模塊? 答:主要受效率和靈活性限制。專用模塊針對特定需求優化計算,成本更低(如通道注意力僅需處理通道維度,無需全局位置計算);不同任務的核心需求差異大(如醫學圖像側重空間定位,自然語言處理側重語義長距離依賴),通用模塊可能冗余或低效。每個模塊新增的權重會增加模型參數量,若訓練數據不足或優化不當,可能引發過擬合。因此實際應用中需結合輕量化設計(如減少全連接層參數)、正則化(如 Dropout)或結構約束(如共享注意力權重)來平衡性能與復雜度。

通道注意力(Channel Attention)屬于注意力機制(Attention Mechanism)的變體,而非自注意力(Self-Attention)的直接變體。可以理解為注意力是一個動物園算法,里面很多個物種,自注意力只是一個分支,因為開創了transformer所以備受矚目。我們今天的內容用通道注意力舉例

常見注意力模塊的歸類如下

注意力模塊所屬類別核心功能
自注意力(Self-Attention)自注意力變體建模同一輸入內部元素的依賴(如序列位置、圖像塊)
通道注意力(Channel Attention)普通注意力變體(全局上下文)建模特征圖通道間的重要性,通過全局池化壓縮空間信息
空間注意力(Spatial Attention)普通注意力變體(全局上下文)建模特征圖空間位置的重要性,關注“哪里”更重要
多頭注意力(Multi-Head Attention)自注意力/普通注意力的增強版將query/key/value投影到多個子空間,捕捉多維度依賴
編碼器-解碼器注意力(Encoder-Decoder Attention)普通注意力變體建模編碼器輸出與解碼器輸入的跨模態交互(如機器翻譯中句子與譯文的對齊)

二、 特征圖的提取

2.1 簡單CNN的訓練

昨天我已經介紹了cnn,為了好演示,我就重新訓練了之前的cnn代碼,你可以直接加載之前保存好的權重試試,一般重新訓練1-2輪就會恢復效果。


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")
Epoch 50/50 完成 | 訓練準確率: 85.42% | 測試準確率: 84.68%

訓練完成!最終測試準確率: 84.68%

可以看到測試集一定程度上收斂了,在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高度抽象,聚焦全局語義特征識別類別相關核心模式大腦最終整合信息判斷“這是青蛙”
    • 特征逐層抽象:從“看得見的細節”(conv1)→ “局部結構”(conv2)→ “類別相關的抽象模式”(conv3),模型通過這種方式實現從“看圖像”到“理解語義”的跨越。
    • 通道分工明確:不同通道在各層聚焦不同特征(如有的通道負責邊緣,有的負責顏色,有的負責全局語義),共同協作完成分類任務。
    • 下采樣的作用:通過縮小空間尺寸,換取更高的語義抽象能力(“犧牲細節,換取理解”)。

    三、通道注意力

    現在我們引入通道注意力,來觀察精度是否有變化,并且進一步可視化。

    想要把通道注意力插入到模型中,關鍵步驟如下:

    1. 定義注意力模塊
    2. 重寫之前的模型定義部分,確定好模塊插入的位置

    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)壓縮為一個標量,保留通道的全局信息。
    • 物理意義:計算每個通道在整個圖像中的 “平均響應強度”,例如,“邊緣檢測通道” 在有物體邊緣的圖像中響應值會更高。
    1. Excitation(激發):
    • 通過全連接層 + Sigmoid 激活,學習通道間的依賴關系,輸出 0-1 之間的權重值。
    • 物理意義:讓模型自動判斷哪些通道更重要(權重接近 1),哪些通道可忽略(權重接近 0)。
    1. 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}%")
    Epoch 50/50 完成 | 訓練準確率: 86.14% | 測試準確率: 85.38%
    

    訓練完成!最終測試準確率: 85.38%
    

    在同樣50個epoch后精度略有提升

    我們關注的不只是精度的差異,還包含了同精度下訓練時長的差異等,在大規模數據集上推理時長、訓練時長都非常重要。因為資源是有限的。

    可視化部分同理,在訓練完成后通過鉤子函數取出權重or梯度,即可進行特征圖的可視化、Grad-CAM可視化、注意力熱圖可視化

    # 可視化空間注意力熱力圖(顯示模型關注的圖像區域)
    def visualize_attention_map(model, test_loader, device, class_names, num_samples=3):"""可視化模型的注意力熱力圖,展示模型關注的圖像區域"""model.eval()  # 設置為評估模式with torch.no_grad():for i, (images, labels) in enumerate(test_loader):if i >= num_samples:  # 只可視化前幾個樣本breakimages, labels = images.to(device), labels.to(device)# 創建一個鉤子,捕獲中間特征圖activation_maps = []def hook(module, input, output):activation_maps.append(output.cpu())# 為最后一個卷積層注冊鉤子(獲取特征圖)hook_handle = model.conv3.register_forward_hook(hook)# 前向傳播,觸發鉤子outputs = model(images)# 移除鉤子hook_handle.remove()# 獲取預測結果_, predicted = torch.max(outputs, 1)# 獲取原始圖像img = images[0].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)# 獲取激活圖(最后一個卷積層的輸出)feature_map = activation_maps[0][0].cpu()  # 取第一個樣本# 計算通道注意力權重(使用SE模塊的全局平均池化)channel_weights = torch.mean(feature_map, dim=(1, 2))  # [C]# 按權重對通道排序sorted_indices = torch.argsort(channel_weights, descending=True)# 創建子圖fig, axes = plt.subplots(1, 4, figsize=(16, 4))# 顯示原始圖像axes[0].imshow(img)axes[0].set_title(f'原始圖像\n真實: {class_names[labels[0]]}\n預測: {class_names[predicted[0]]}')axes[0].axis('off')# 顯示前3個最活躍通道的熱力圖for j in range(3):channel_idx = sorted_indices[j]# 獲取對應通道的特征圖channel_map = feature_map[channel_idx].numpy()# 歸一化到[0,1]channel_map = (channel_map - channel_map.min()) / (channel_map.max() - channel_map.min() + 1e-8)# 調整熱力圖大小以匹配原始圖像from scipy.ndimage import zoomheatmap = zoom(channel_map, (32/feature_map.shape[1], 32/feature_map.shape[2]))# 顯示熱力圖axes[j+1].imshow(img)axes[j+1].imshow(heatmap, alpha=0.5, cmap='jet')axes[j+1].set_title(f'注意力熱力圖 - 通道 {channel_idx}')axes[j+1].axis('off')plt.tight_layout()plt.show()# 調用可視化函數
    visualize_attention_map(model, test_loader, device, class_names, num_samples=3)

    這個注意力熱圖是通過構子機制:?register_forward_hook?捕獲最后一個卷積層(conv3)的輸出特征圖。

    1. 通道權重計算:對特征圖的每個通道進行全局平均池化,得到通道重要性權重。
    2. 熱力圖生成:將高權重通道的特征圖縮放至原始圖像尺寸,與原圖疊加顯示。

    熱力圖(紅色表示高關注,藍色表示低關注)半透明覆蓋在原圖上。主要從以下方面理解:

    • 高關注區域(紅色):模型認為對分類最重要的區域。
      例如:
      • 在識別“狗”時,熱力圖可能聚焦狗的面部、身體輪廓或特征性紋理。
      • 若熱力圖錯誤聚焦背景(如紅色區域在無關物體上),可能表示模型過擬合或訓練不足。

    多通道對比

    • 不同通道關注不同特征
      例如:
      • 通道1可能關注整體輪廓,通道2關注紋理細節,通道3關注顏色分布。
      • 結合多個通道的熱力圖,可全面理解模型的決策邏輯。

    可以幫助解釋

    • 檢查模型是否關注正確區域(如識別狗時,是否聚焦狗而非背景)。
    • 發現數據標注問題(如標簽錯誤、圖像噪聲)。
    • 向非技術人員解釋模型決策依據(如“模型認為這是狗,因為關注了眼睛和嘴巴”)。

    知識點回顧:

    1. 不同CNN層的特征圖:不同通道的特征圖
    2. 什么是注意力:注意力家族,類似于動物園,都是不同的模塊,好不好試了才知道。
    3. 通道注意力:模型的定義和插入的位置
    4. 通道注意力后的特征圖和熱力圖

    內容參考

    作業:

    1. 今日代碼較多,理解邏輯即可
    2. 對比不同卷積層特征圖可視化的結果(可選)

    ps:

    • 我這里列出來的是通道注意力中的一種,SE注意力
    • 為了保證收斂方便對比性能,今日代碼訓練輪數較多,比較耗時
    • 目前我們終于接觸到了模塊,模塊本質上也是對特征的進一步提取,整個深度學習就是在圍繞特征提取展開的,后面會是越來越復雜的特征提取和組合步驟
    • 新增八股部分,在本講義目錄中可以看到----用問答的形式記錄知識點

    @浙大疏錦行

    本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
    如若轉載,請注明出處:http://www.pswp.cn/bicheng/83987.shtml
    繁體地址,請注明出處:http://hk.pswp.cn/bicheng/83987.shtml
    英文地址,請注明出處:http://en.pswp.cn/bicheng/83987.shtml

    如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

    相關文章

    入門AJAX——XMLHttpRequest(Post)

    一、前言 在上篇文章中,我們已經介紹了 HMLHttpRequest 的GET 請求的基本用法,并基于我提供的接口練習了兩個簡單的例子。如果你還沒有看過第一篇文章,強烈建議你在學習完上篇文章后再學習本篇文章: 🔗入門AJAX——XM…

    ?BEV和OCC學習-3:mmdet3d 坐標系

    目錄 坐標系 轉向角 (yaw) 的定義 框尺寸的定義 與支持的數據集的原始坐標系的關系 KITTI Waymo NuScenes Lyft ScanNet SUN RGB-D S3DIS 坐標系 坐標系 — MMDetection3D 1.4.0 文檔https://mmdetection3d.readthedocs.io/zh-cn/latest/user_guides/coord_sys_tuto…

    Redis高可用架構

    概述 Redis作為常用的緩存中間件,因其高性能,豐富的數據結構,使用簡單等,常被用在需要一定高性能的To C業務場景中,如「秒殺場景」「用戶信息中心」「帖子」「群聊」等等大家常見的業務場景中,以提高服務的…

    使用WPF的Microsoft.Xaml.Behaviors.Wpf中通用 UI 元素事件

    Nuget下載之后記得要先引用下面的 xmlns:i"http://schemas.microsoft.com/xaml/behaviors" <!-- 鼠標事件 --> <i:EventTrigger EventName"MouseEnter"/> <!-- 鼠標進入 --> <i:EventTrigger EventName"MouseLeave"/&g…

    敏捷開發中如何避免過度加班

    在敏捷開發過程中避免過度加班&#xff0c;需要明確敏捷原則、合理規劃迭代任務、加強團隊溝通、優化流程效率、設定合理的工作負荷、注重團隊士氣和成員健康。明確敏捷原則&#xff0c;即保證可持續發展的步調&#xff0c;避免頻繁地變更需求、過度承諾任務量。合理規劃迭代任…

    JSON解析崩潰原因及解決方案

    問題記錄&#xff1a; /************************************************| * 描述: 將ID124執行NFC操作-JSON解析為結構體* 函數名: cJSON_ID124_to_struct* 參數[ I]: *json_string 待解析的指針* 參數[II]: *wireless_rxd 結構體指針* 返回: 成功返回0 失…

    業務系統對接大模型的基礎方案:架構設計與關鍵步驟

    業務系統對接大模型&#xff1a;架構設計與關鍵步驟 在當今數字化轉型的浪潮中&#xff0c;大語言模型&#xff08;LLM&#xff09;已成為企業提升業務效率和創新能力的關鍵技術之一。將大模型集成到業務系統中&#xff0c;不僅可以優化用戶體驗&#xff0c;還能為業務決策提供…

    Edge(Bing)自動領積分腳本部署——基于python和Selenium(附源碼)

    微軟的 Microsoft Rewards 計劃可以通過 Bing 搜索賺取積分&#xff0c;積分可以兌換禮品卡、游戲等。每天的搜索任務不多&#xff0c;我們可以用腳本自動完成&#xff0c;提高效率&#xff0c;解放雙手。 本文將手把手教你如何部署一個自動刷積分腳本&#xff0c;并解釋其背…

    前端基礎之《Vue(19)—狀態管理》

    一、什么是狀態管理 1、Vue版本問題 Vue2 Vuex3 Vue3 Vuex4 / Pinia2 在使用任何技術的時候&#xff0c;都先要去搜索一下版本&#xff0c;你的版本和腳手架環境是否兼容。 2、安裝Vuex yarn add vuex3.6.2 3、狀態管理 狀態&#xff0c;在應用程序中表示數據&#xff0c…

    【圖像處理基石】如何進行圖像畸變校正?

    圖像畸變校正常用于計算機視覺、攝影測量學和機器人導航等領域&#xff0c;能夠修正因鏡頭光學特性或傳感器排列問題導致的圖像失真。下面我將介紹幾種常用的圖像畸變校正算法&#xff0c;并提供Python實現和測試用例。 常用算法及Python實現 1. 徑向畸變校正 徑向畸變是最常…

    藍橋杯_DS18B20溫度傳感器---新手入門級別超級詳細解析

    目錄 一、引言 DS18B20的原理圖 單總線簡介&#xff1a; ?編輯暫存器簡介&#xff1a; DS18B20的溫度轉換與讀取流程 二、代碼配置 maic文件 疑問 關于不同格式化輸出符號的使用 為什么要rd_temperature()/16.0&#xff1f; onewire.h文件 這個配置為什么要先讀lo…

    MySQL的并發事務問題及事務隔離級別

    一、并發事務問題 1). 贓讀&#xff1a;一個事務讀到另外一個事務還沒有提交的數據。 比如 B 讀取到了 A 未提交的數據。 2). 不可重復讀&#xff1a;一個事務先后讀取同一條記錄&#xff0c;但兩次讀取的數據不同&#xff0c;稱之為不可重復讀。 事務 A 兩次讀取同一條記錄&…

    密碼學基礎——SM4算法

    博客主頁&#xff1a;christine-rr-CSDN博客 ????專欄主頁&#xff1a;密碼學 &#x1f4cc; 【今日更新】&#x1f4cc; 對稱密碼算法——SM4 目錄 一、國密SM系列算法概述 二、SM4算法 2.1算法背景 2.2算法特點 2.3 基本部件 2.3.1 S盒 2.3.2 非線性變換 ?編輯…

    練習:對象數組 4

    定義數組存儲 4 個女朋友的對象。女朋友的屬性&#xff1a;姓名、年齡、性別、愛好&#xff1b;要求1&#xff1a;計算出四個女朋友的平均年齡&#xff1b;要求2&#xff1a;統計年齡比平均值低的女朋友有幾個&#xff1f;并把他們的所有信息打印出來。 代碼&#xff1a; //對…

    React Hooks 基礎指南

    React Hooks 是 React 16.8 引入的重要特性&#xff0c;它允許開發者在函數組件中使用狀態和其他 React 特性。本文將詳細介紹 6 個最常用的 React Hooks。 1. useState useState 是最常用的 Hook&#xff0c;用于在函數組件中添加 state。 import React, { useState } from…

    【Python 算法零基礎 4.排序 ⑥ 快速排序】

    既有錦繡前程可奔赴&#xff0c;亦有往日歲月可回首 —— 25.5.25 選擇排序回顧 ① 遍歷數組&#xff1a;從索引 0 到 n-1&#xff08;n 為數組長度&#xff09;。 ② 每輪確定最小值&#xff1a;假設當前索引 i 為最小值索引 min_index。從 i1 到 n-1 遍歷&#xff0c;若找到…

    處理git沒做修改,但是文件顯示變更的情況

    使用 TortoiseGit&#xff08;小烏龜 Git&#xff09; 時遇到 “文件內容沒改&#xff0c;但顯示為變更&#xff0c;提示有 n 行刪除、n 行添加”&#xff0c;你可以按照以下步驟操作來排查并解決問題&#xff1a; ? 一、定位問題根源&#xff08;是否為行尾差異&#xff09;…

    智慧貨運飛船多維度可視化管控系統

    圖撲搭建智慧貨運飛船可視化系統&#xff0c;借數字孿生技術&#xff0c;高精度復刻貨運飛船外觀、結構與運行場景。整合多維度數據&#xff0c;實時呈現飛行狀態、設備參數等信息&#xff0c;助力直觀洞察貨運飛船運行邏輯&#xff0c;為航天運維、任務推演及決策提供數字化支…

    maven微服務${revision}依賴打包無法識別

    1、場景描述 我現在又一個微服務項目&#xff0c;父pom的版本&#xff0c;使用<properties>定義好&#xff0c;如下所示&#xff1a; <name>ypsx-finance-center</name> <artifactId>ypsx-finance</artifactId> <packaging>pom</pack…

    詳解代理型RAG與MCP服務器集成

    檢索增強型生成(RAG)將語言模型與外部知識檢索相結合,讓模型的回答基于最新的事實,而不僅僅是其訓練數據呢。 RAG(高級別) 在 RAG 流程中,用戶查詢用于搜索知識庫(通常通過向量數據庫中的嵌入來實現),并將檢索到的最相關文檔“增強”到模型的提示中,以幫助生成事實…