@浙大疏錦行
DAY54
一、 inception網絡介紹
今天我們介紹inception,也就是GoogleNet
傳統計算機視覺的發展史
從上面的鏈接,可以看到其實inceptionnet是在resnet之前的,那為什么我今天才說呢?因為他要引出我們后面的特征融合和特征并行處理這些思想。
Inception 網絡,也被稱為 GoogLeNet,是 Google 團隊在 2014 年提出的經典卷積神經網絡架構。它的核心設計理念是 “并行的多尺度融合”,通過在同一層網絡中使用多個不同大小的卷積核(如 1x1、3x3、5x5)以及池化操作,從不同尺度提取圖像特征,然后將這些特征進行融合,從而在不增加過多計算量的情況下,獲得更豐富的特征表達。
Inception 模塊是 Inception 網絡的基本組成單元。
在同樣的步長下,卷積核越小,下采樣率越低,保留的圖片像素越多;卷積核越大,越能捕捉像素周圍的信息。
一個典型的 Inception 模塊包含以下幾個并行的分支:
- 1x1 卷積分支:用于降維,減少后續卷積操作的計算量,同時提取局部特征。?(像素下采樣率低,但是可以修改通道數)
- 3x3 卷積分支:捕捉中等尺度的特征。?
- 5x5 卷積分支:捕捉較大尺度的特征。?
- 池化分支:通常使用最大池化或平均池化,用于保留圖像的全局信息。
二、 inception網絡架構
2.1 定義inception模塊
import torch
import torch.nn as nnclass Inception(nn.Module):def __init__(self, in_channels):"""Inception模塊初始化,實現多尺度特征并行提取與融合參數:in_channels: 輸入特征圖的通道數"""super(Inception, self).__init__()# 1x1卷積分支:降維并提取通道間特征關系# 減少后續卷積的計算量,同時保留局部特征信息self.branch1x1 = nn.Sequential(nn.Conv2d(in_channels, 64, kernel_size=1), # 降維至64通道nn.ReLU() # 引入非線性激活)# 3x3卷積分支:通過1x1卷積降維后使用3x3卷積捕捉中等尺度特征# 先降維減少計算量,再進行空間特征提取self.branch3x3 = nn.Sequential(nn.Conv2d(in_channels, 96, kernel_size=1), # 降維至96通道nn.ReLU(),nn.Conv2d(96, 128, kernel_size=3, padding=1), # 3x3卷積,保持空間尺寸不變nn.ReLU())# 5x5卷積分支:通過1x1卷積降維后使用5x5卷積捕捉大尺度特征# 較大的感受野用于提取更全局的結構信息self.branch5x5 = nn.Sequential(nn.Conv2d(in_channels, 16, kernel_size=1), # 大幅降維至16通道nn.ReLU(),nn.Conv2d(16, 32, kernel_size=5, padding=2), # 5x5卷積,保持空間尺寸不變nn.ReLU())# 池化分支:通過池化操作保留全局信息并降維# 增強特征的平移不變性self.branch_pool = nn.Sequential(nn.MaxPool2d(kernel_size=3, stride=1, padding=1), # 3x3最大池化,保持尺寸nn.Conv2d(in_channels, 32, kernel_size=1), # 降維至32通道nn.ReLU())def forward(self, x):"""前向傳播函數,并行計算四個分支并在通道維度拼接參數:x: 輸入特征圖,形狀為[batch_size, in_channels, height, width]返回:拼接后的特征圖,形狀為[batch_size, 256, height, width]"""# 注意,這里是并行計算四個分支branch1x1 = self.branch1x1(x) # 輸出形狀: [batch_size, 64, height, width]branch3x3 = self.branch3x3(x) # 輸出形狀: [batch_size, 128, height, width]branch5x5 = self.branch5x5(x) # 輸出形狀: [batch_size, 32, height, width]branch_pool = self.branch_pool(x) # 輸出形狀: [batch_size, 32, height, width]# 在通道維度(dim=1)拼接四個分支的輸出# 總通道數: 64 + 128 + 32 + 32 = 256outputs = [branch1x1, branch3x3, branch5x5, branch_pool]return torch.cat(outputs, dim=1)
上述模塊變化為[B, C, H, W]-->[B, 256, H, W]
model = Inception(in_channels=64)
input = torch.randn(32, 64, 28, 28)
output = model(input)
print(f"輸入形狀: {input.shape}")
print(f"輸出形狀: {output.shape}")
輸入形狀: torch.Size([32, 64, 28, 28]) 輸出形狀: torch.Size([32, 256, 28, 28])
inception模塊中不同的卷積核和步長最后輸出同樣尺寸的特征圖,這是經過精心設計的,才能在空間上對齊,才能在維度上正確拼接(concat)。
2.2 特征融合方法
這里我們注意到,它是對把不同尺度的特征融合在一起。concat這種增加通道數的方法是一種經典的特征融合方法。通道數增加,空間尺寸(H, W)保持不變,每個通道的數值保持獨立,沒有加法運算。相當于把不同特征圖 “并排” 放在一起,形成更 “厚” 的特征矩陣。
在深度學習中,特征融合的尺度有以下方式:
- 逐元素相加:將相同形狀的特征圖對應位置的元素直接相加,比如殘差連接:
output = x + self.residual_block(x)
不改變特征圖尺寸和通道數,計算高效,但需保證輸入形狀一致。
-
逐元素相乘:通過乘法對特征進行權重分配,抑制無關特征,增強關鍵特征。比如注意力機制、門控機制(如 LSTM 中的遺忘門、輸入門),例如
attention = self.ChannelAttention(features) # 生成通道權重 weighted_features = features * attention # 逐元素相乘
其他的特征融合方法我們后面有機會介紹
2.3 InceptionNet網絡定義
class InceptionNet(nn.Module):def __init__(self, num_classes=10):super(InceptionNet, self).__init__()self.conv1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2, padding=1))self.inception1 = Inception(64)self.inception2 = Inception(256)self.avgpool = nn.AdaptiveAvgPool2d((1, 1))self.fc = nn.Linear(256, num_classes)def forward(self, x):x = self.conv1(x)x = self.inception1(x)x = self.inception2(x)x = self.avgpool(x)x = torch.flatten(x, 1)x = self.fc(x)return x
# 創建網絡實例
model = InceptionNet()
# 創建一個隨機輸入張量,模擬圖像數據,這里假設輸入圖像是3通道,尺寸為224x224
input_tensor = torch.randn(1, 3, 224, 224)
# 前向傳播
output = model(input_tensor)
print(output.shape)
torch.Size([1, 10])
inception網絡有著很多變體,Inception v1版本就是 GoogLeNet,他還有v2到v4,還版本,還可以引入殘差連接,如 Inception-ResNet-v2 在 ImageNet 上 top-1 準確率達 96.4%
多尺度融合等其他技巧我們將在科研班中展開了,因為單純從講義的形式來說,實在是不方便。將會在科研班的板塊中,提到backbone-neck-head這樣的范式架構設計,這將帶你開啟搭積木的道路。
今天的內容稍微有點單薄,我們再補充點小知識點。
三、卷積核的變體
3.1 感受野
我們發現,經常會有不同尺寸的卷積核來在特征圖上滑動進一步提取信息,那么卷積核的尺寸如何選取比較合適呢?在思考這個問題前你需要理解下感受野的概念。
感受野是指在卷積神經網絡(CNN)中,神經元在原始輸入圖像上所對應的區域大小。通俗來說,卷積層中的每個輸出特征圖上的一個像素點,其信息來源于輸入圖像中的某個特定區域,這個區域的大小就是該像素點的感受野。
假設我們有一個 3×3 的卷積核,對一張 5×5 的圖像進行步長為 1 的卷積操作:
輸出特征圖的每個像素點,都由輸入圖像中 3×3 的區域計算得到,因此該層的感受野為 3×3。 如果再疊加一層 3×3 卷積(步長 1),第二層的每個像素點會融合第一層 3×3 區域的信息,而第一層的每個區域又對應原始圖像的 3×3 區域,因此第二層的感受野擴展為 5×5(即 3+3-1=5)
為了方便大家理解這個5怎么來的,找了一個博主的視頻方便大家理解:?感受野的理解視頻解析
所以,在對應同等感受野的情況下,卷積核尺寸小有2個顯著的優勢:
- 能讓參數變少,簡化計算
- 能夠引入更多的非線性(多經過幾次激活函數),讓擬合效果更好
這也是為什么像 VGG 網絡就用多層 3×3 卷積核替代大卷積核,平衡模型性能與復雜度 。
3.2 卷積的變體
卷積也是有很多變體的,除了我們之前說過的基礎的卷積,還有空洞卷積、幻影卷積等等變體。我們以空洞卷積舉例:
空洞卷積(也叫擴張卷積、膨脹卷積 ),是對標準卷積的 “升級”—— 在卷積核元素間插入空洞(間隔),用 空洞率(dilation rate,記為d) 控制間隔大小。
標準卷積(d=1):卷積核元素緊密排列,直接覆蓋輸入特征圖相鄰區域。 空洞卷積(d>1):卷積核元素間插入?d-1?個空洞,等效擴大卷積核的 “感受野范圍”,但不增加參數數量(僅改變計算時的采樣間隔)。也就是無需增大卷積核尺寸或疊加多層卷積,僅通過調整?d,就能指數級提升感受野。
對比池化(Pooling)或下采樣,空洞卷積不丟失空間信息,能在擴大感受野的同時,維持特征圖尺寸,特別適合語義分割、目標檢測等需要精準像素 / 目標定位的任務。
所以不同的設計,其實是為了不同的任務,比如你雖然可以捕捉不同尺度的信息,但是對于圖像分類這個任務來說沒用,我的核心是整個圖像的類別,如果你是目標檢測,對于小目標的檢測中小尺度的設計就很有用。
3.3 空洞卷積示例
其實就是多了一個參數,代碼上僅僅也是多了一個參數
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=2, dilation=2)
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader# 數據預處理
transform = transforms.Compose([transforms.ToTensor(), # 轉為張量transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 歸一化
])# 加載CIFAR-10數據集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=128, shuffle=True)testset = torchvision.datasets.CIFAR10(root='./data', train=False,download=True, transform=transform)
testloader = DataLoader(testset, batch_size=128, shuffle=False)# 定義含空洞卷積的CNN模型
class SimpleCNNWithDilation(nn.Module):def __init__(self):super(SimpleCNNWithDilation, self).__init__()# 第一層:普通3×3卷積,捕捉基礎特征self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) # 第二層:空洞卷積,dilation=2,感受野擴大(等效5×5普通卷積感受野)self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=2, dilation=2) # 第三層:普通3×3卷積,恢復特征對齊self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) # 池化層self.relu = nn.ReLU()# 全連接層,根據CIFAR-10尺寸計算:32×32→池化后16×16→...→最終特征維度需匹配self.fc1 = nn.Linear(64 * 8 * 8, 256) self.fc2 = nn.Linear(256, 10) def forward(self, x):# 輸入: [batch, 3, 32, 32]x = self.conv1(x) # [batch, 16, 32, 32]x = self.relu(x)x = self.pool(x) # [batch, 16, 16, 16]x = self.conv2(x) # [batch, 32, 16, 16](dilation=2 + padding=2 保持尺寸)x = self.relu(x)x = self.pool(x) # [batch, 32, 8, 8]x = self.conv3(x) # [batch, 64, 8, 8]x = self.relu(x)x = x.view(-1, 64 * 8 * 8) # 展平x = self.fc1(x)x = self.relu(x)x = self.fc2(x)return x# 初始化模型、損失函數、優化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNNWithDilation().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)# 訓練函數
def train(epoch):model.train()running_loss = 0.0for i, data in enumerate(trainloader, 0):inputs, labels = data[0].to(device), data[1].to(device)optimizer.zero_grad()outputs = model(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()running_loss += loss.item()if i % 100 == 99: # 每100個batch打印一次print(f'Epoch: {epoch + 1}, Batch: {i + 1}, Loss: {running_loss / 100:.3f}')running_loss = 0.0# 測試函數
def test():model.eval()correct = 0total = 0with torch.no_grad():for data in testloader:images, labels = data[0].to(device), data[1].to(device)outputs = model(images)_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()print(f'Accuracy on test set: {100 * correct / total:.2f}%')# 訓練&測試流程
for epoch in range(5): # 簡單跑5個epoch示例train(epoch)test()
Files already downloaded and verified Files already downloaded and verified Epoch: 1, Batch: 100, Loss: 1.816 Epoch: 1, Batch: 200, Loss: 1.501 Epoch: 1, Batch: 300, Loss: 1.385 Accuracy on test set: 55.62% Epoch: 2, Batch: 100, Loss: 1.210 Epoch: 2, Batch: 200, Loss: 1.182 Epoch: 2, Batch: 300, Loss: 1.115 Accuracy on test set: 60.03% Epoch: 3, Batch: 100, Loss: 1.013 Epoch: 3, Batch: 200, Loss: 0.991 Epoch: 3, Batch: 300, Loss: 0.976 Accuracy on test set: 65.74% Epoch: 4, Batch: 100, Loss: 0.877 Epoch: 4, Batch: 200, Loss: 0.877 Epoch: 4, Batch: 300, Loss: 0.856 Accuracy on test set: 68.51% Epoch: 5, Batch: 100, Loss: 0.765 Epoch: 5, Batch: 200, Loss: 0.767 Epoch: 5, Batch: 300, Loss: 0.754 Accuracy on test set: 69.82%
局部替換成空洞卷積,在不顯著增加計算量的情況下,增強模型對長距離特征的捕捉能力,尤其適合想在小數據集(CIFAR-10)里嘗試擴大感受野的場景。
可以嘗試在不同層設置不同dilation(比如dilation=[1,2,1] ),讓模型從多個感受野維度提取特征。
所以其實對于這些模塊和類的參數本身能力的理解,才能幫助你更好的搭積木,而不是單純的無腦的試,未來你做什么任務就積累這方面的能力即可。