JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

更深也更省:VGG、1×1 卷積與 Inception

了解為何堆疊小小的 3×3 濾波器勝過一個大濾波器、1×1 卷積如何混合通道,以及 Inception 為何乾脆不挑單一濾波器大小。

VGG:把一件事做到底的美學

在上一篇我們看到 AlexNet 靠著把一堆點子全丟進去而獲勝:這裡放 11×11 的濾波器、那裡放 5×5、再來幾個 3×3,步幅和池化也是一層一層各自決定。它確實有效,但有點像廚房裡每種菜都配一把不同的刀。VGG 背後的團隊問了一個更安靜的問題:如果我們不再讓每樣東西都變來變去,選一個最小而合理的濾波器,然後就只是重複它呢?他們的答案,成了電腦視覺史上最有影響力的設計選擇之一。

VGG 的哲學是極致的一致性。網路裡幾乎每一個卷積都長得一樣:3×3 的濾波器,步幅 1,填補(padding)1。填補設為 1 這個細節,正是讓卷積前後空間尺寸保持不變的關鍵,所以一整個區塊的這些層既不會縮小也不會放大特徵圖,只會把分析變得更深。唯一改變空間尺寸的,是擺在區塊之間的 2×2 最大池化層,它把高和寬各砍一半。比起 AlexNet 那種大小濾波器混搭的不規則做法,VGG 幾乎像修道院一般:一個動作,有紀律地重複。

一個步幅 1、填補 1 的 3×3 卷積:它在輸入上滑動,因為四周補了一圈 1 像素的邊,輸出的特徵圖高和寬與輸入完全相同。

一張示意圖,3×3 的卷積核在補過邊的輸入網格上滑動,產生大小相同的輸出網格。

讓 VGG 如此好想像的,是它的這套節奏。假設你餵進一張 224×224、3 個顏色通道的影像。第一區塊跑幾個 3×3 卷積、產生 64 個通道,然後池化:現在是 112×112×64。第二區塊把通道加倍到 128,再池化:56×56×128。第三區塊 → 28×28×256,第四區塊 → 14×14×512,第五區塊 → 7×7×512。注意這兩條簡單規則如何同步運作:每次池化把空間尺寸減半,每個區塊把通道數加倍(直到 512 封頂)。空間縮小、深度增長——網路一路往上,用『在哪裡』換取『是什麼』。

為何兩個 3×3 勝過一個 5×5

如果小濾波器這麼好,你可能會擔心它們近視——一個 3×3 濾波器永遠只看一個小小的 3×3 區塊。拯救 VGG 的關鍵,是感受野(receptive field):把底下所有層都算進去之後,某個神經元最終能『看到』的那塊原始輸入區域。令人意外的是,把小濾波器疊起來,這個視窗會很快變大,而一疊 3×3 能看到的範圍,竟然和一個大濾波器一模一樣。

我們親手把這個視窗一步步搭起來。一個 3×3 卷積:每個輸出看到一個 3×3 區塊——這很直觀。現在在上面再疊第二個 3×3 卷積。第二層的每個神經元,看的是『第一層』輸出的一個 3×3 區塊。但第一層的每個值,本身就已經總結了輸入的一個 3×3 區塊,而相鄰的值因為每次只平移一個像素而互相重疊。把它們攤開來看,合起來的視野就涵蓋了原始影像的一個 5×5 區域。再加第三個 3×3 層,視窗就長到 7×7。所以就『每個神經元能看多遠』而言:兩個 3×3 = 一個 5×5,三個 3×3 = 一個 7×7。

兩個疊起來的 3×3 卷積,可以回看到輸入的 5×5 視窗;三個疊起來則達到 7×7。每多加一個 3×3 層,視窗在每個維度就成長 2。

巢狀方塊示意圖,顯示 3×3 感受野在兩層後擴大為 5×5、三層後擴大為 7×7。

看到一樣大的區域固然不錯,但真正的勝利在於價格。假設某層有 C 個輸入通道、C 個輸出通道——這在區塊深處很常見。我們來比較『一個 5×5 卷積』和『兩個疊起來的 3×3 卷積』各需要多少權重:

\underbrace{2\cdot(3^2\cdot C^2)}_{\text{two stacked }3\times3} = 18C^2 \quad<\quad \underbrace{5^2\cdot C^2}_{\text{one }5\times5} = 25C^2

相同感受野,更少權重:18C² 對比 25C²。

我們把每個因子拆開,讓一切都不再神祕。在一個 5×5 卷積裡,5² = 25 是卷積核面積(5×5 格的權重),C² = C·C 是通道配對:C 個輸入通道中的每一個,都連到 C 個輸出通道中的每一個,所以每組(輸入、輸出)配對都複製一份卷積核。相乘就得到 25C² 個權重。至於兩個 3×3 的堆疊,3² = 9 是較小的卷積核面積,C² 是同樣的通道配對,而前面那個 2,單純就是數兩個疊起來的層。於是是 2·9·C² = 18C²。代入一個實際數字,比方 C = 64:5×5 需要 25·4096 ≈ 102k 個權重,而兩個 3×3 只要 18·4096 ≈ 74k——在相同視野下便宜了約 28%。

1×1 卷積:在每個像素上的小型 MLP

接著是一個幾乎每個人第一次都會被搞糊塗的點子:1×1 卷積。你的直覺會大喊這毫無意義——一個只有一像素寬的視窗,能對影像學到什麼?解法是別再想空間了。特徵圖不只是高和寬;在每一個位置,它都有一整疊通道(64、256,有時上千)。1×1 卷積完全不理會空間上的鄰居,而是在某一個像素上,『沿著那疊通道往下』運算。

把特徵圖看成一疊通道。1×1 卷積站在某一個 (x,y) 位置,混合那裡那一整欄的通道值——從不向旁邊的鄰居伸手。

一個代表特徵圖的立體方塊,某一空間位置上垂直的一欄通道值被標示出來。

嚴格來說,在單一像素上,1×1 卷積計算的是:

y = W x, \qquad x \in \mathbb{R}^{C_{\text{in}}},\; W \in \mathbb{R}^{C_{\text{out}}\times C_{\text{in}}},\; y \in \mathbb{R}^{C_{\text{out}}}

一個小小的矩陣–向量乘法,在每個空間位置上各自獨立地施作。

我們把每個符號都命名。在某一像素上,x 是通道向量:那個位置上所有 C_in 個特徵值疊成的清單(若該層有 256 個通道,x 就有 256 個數字)。W 是一個形狀為 C_out × C_in 的權重矩陣——也就是 C_out 列,每一列是一組 C_in 個混合權重。輸出 y 是一個長度為 C_out 的新通道向量:y 的每個分量,都是所有輸入通道的加權混合。最關鍵的一點:在影像的每一個 (x,y) 位置,用的都是『同一個』矩陣 W。所以 1×1 卷積,字面上就是一個小小的全連接層——一個跨通道的 MLP——複製貼上到每一個像素。它是純粹的通道混合,完全沒有空間混合。

它的殺手級應用是降維:在昂貴的層之前,便宜地把許多通道壓縮成少數幾個。這是 Network-in-Network 這篇論文的貢獻,也為我們即將在 Inception 看到的瓶頸鋪好了路。具體來說,假設你有一張 256 通道的特徵圖,想餵進一個昂貴的 3×3 卷積。直接用 3×3 從 256→256,要花 3²·256·256 ≈ 590k 個權重。換個做法:先用 1×1 卷積把 256→64 擠下來(花 1²·256·64 ≈ 16k),再讓 3×3 跑 64→64(花 3²·64·64 ≈ 37k)。合計 ≈ 53k 個權重——便宜超過 10 倍,而且那個 1×1 還會自己『學』要保留哪 64 個方向。

import torch.nn as nn

# A 1x1 convolution IS a per-pixel linear layer across channels.
# kernel_size=1 means it never looks at spatial neighbours.
reduce = nn.Conv2d(in_channels=256, out_channels=64, kernel_size=1)

# Input: batch of 8 feature maps, 256 channels, 28x28 spatially.
x = torch.randn(8, 256, 28, 28)
y = reduce(x)          # -> shape (8, 64, 28, 28): channels squeezed, H/W untouched

# Parameter count: C_out * C_in * 1 * 1  (+ C_out biases)
#   = 64 * 256 * 1 * 1 + 64  = 16,448 weights -- tiny.
print(y.shape, sum(p.numel() for p in reduce.parameters()))
一個 1×1 卷積把 256 個通道縮成 64 個,而 28×28 的空間網格原封不動。

全域平均池化:拿掉龐大的分類頭

還記得第一篇在算參數量時的那個大反派嗎?在 AlexNet 和 VGG 裡,卷積部分相對精瘦,但結尾的全連接分類頭卻是個怪物——光是 VGG 的第一個全連接層,就握有超過一億個權重,佔了整個網路的大半。Network-in-Network 這篇論文提出了一個簡單到令人吃驚的替代品:全域平均池化(global average pooling)。

點子是這樣:與其把最後的特徵圖攤平成一個巨大的向量、再跑龐大的全連接層,不如直接把每一張最終特徵圖平均成一個數字。如果最後一個卷積區塊輸出的是,比方說,一個 7×7×512 的張量,你就把每張 7×7 的圖壓成它的平均值,正好得到 512 個數字——每個通道一個值。這 512 個數字直接餵進最終的分類器。不用攤平,也沒有上億權重的層。

全域平均池化把每一整張特徵圖壓成它的平均值,將一個 H×W×C 的張量變成一個長度為 C 的向量。

特徵圖堆疊中的每個通道被平均成一個值,產生一個每通道一項的向量。

讓它一拍即合的直覺是這樣。到了這個深度,512 張特徵圖中的每一張,都學會了對某種高階模式起反應——把其中一張想成『輪子偵測器』,另一張想成『毛皮偵測器』。把一張圖取平均,問的是:『在整張影像上,這個偵測器發火得有多強?』那一個平均值,就是這個偵測器的信心投票。把這些票直接餵給分類器非常直接了當:每個類別本質上就是一場加權點票,看哪些偵測器亮了起來。

Inception 模組:為什麼只能挑一種濾波器大小?

VGG 從頭到尾押注在 3×3。Inception 模組 則採取相反的立場——它根本拒絕押注。它大膽地問:在任一層裡,我們為什麼非得挑『一種』濾波器大小不可?貓的鬍鬚是細節;貓的身體是粗略的特徵。也許網路不該被逼著二選一。於是 Inception 在同一個輸入上並行地跑好幾種濾波器——一個 1×1、一個 3×3、一個 5×5,還有一條池化分支——讓資料自己決定在每個地方什麼才有用。

把它想成同時諮詢一個專家小組:一位專家瞇眼看細緻的紋理(小濾波器),一位退後一步看中等結構(3×3),一位納入大範圍的脈絡(5×5),還有一位總結周遭(池化)。每條分支產生自己的特徵圖。接著模組把這些輸出串接(concatenate)起來——這正是初學者必須弄對的地方:這些分支是沿著通道維度疊起來,『而不是相加』。如果各分支分別輸出 64、128、32、32 個通道,模組的輸出就有 64+128+32+32 = 256 個通道,全部共用相同的高和寬。網路把每個人的意見並排保留,讓後面的層去權衡它們。

並行的分支(1×1、3×3、5×5、池化)各自產生相同 H×W 的特徵圖;它們的通道被串接成一疊更高的堆疊——不是相加。

四條並行的卷積分支匯入單一串接後的輸出堆疊,通道數變多。

有個明顯的問題:在一個 256 通道的肥厚輸入上跑 5×5 卷積貴得嚇人,而串接只會讓通道數膨脹得更厲害。這正是上一節派上用場的地方。Inception 在昂貴的 3×3 和 5×5 分支之前,插入1×1 卷積當作瓶頸,用的正是 Network-in-Network 的通道降維把戲。1×1 把比方說 256 個通道擠到 64,昂貴的卷積在這個瘦身後的輸入上跑,成本就崩降下來。1×1 瓶頸,就是讓整個並行分支構想負擔得起的那位低調英雄。

GoogLeNet:又深又寬,卻意外地省

現在把這些模組疊起來。GoogLeNet 本質上就是九個 Inception 模組一個疊一個(最前面再加幾個普通卷積和池化),構成一個 22 層深的網路。加深通常意味著參數爆炸——然而 GoogLeNet 卻只有大約500 萬個參數。對比之下,VGG-16 重達約1.38 億個。這就是效率上的躍進:用一個其實更深的網路,只花了 VGG 權重的一『小部分』。

那些權重都跑哪去了?是我們在這篇一路建立起來的兩項節省。第一,每個 Inception 模組裡的1×1 瓶頸,讓昂貴的 3×3 和 5×5 分支都在瘦身後的通道上運作。第二,GoogLeNet 以全域平均池化收尾,取代了 VGG 那個龐大的全連接頭——刪掉了單一最重的元件。內部又深又寬,兩端精瘦。

GoogLeNet 從頭到尾:一段普通卷積/池化的前置,接著一長串疊起來的 Inception 模組,然後全域平均池化進入分類器。

一張流程示意圖:輸入影像、前置卷積、數個串接的 Inception 模組、全域平均池化、分類器輸出。

還有一個值得現在認識的細節,因為它預告了下一篇:輔助分類器(auxiliary classifiers)。在訓練期間,GoogLeNet 在網路中途長出兩個小小的額外分類頭,各自做出自己的預測並貢獻到損失裡。為什麼?在一個 22 層的網路裡,在最末端算出的訓練訊號(梯度)得一路往回傳很遠,才能教到早期的層,而它在旅途中往往會逐漸消散。輔助頭把新鮮的梯度直接注入網路中段,給早期的層一份更清楚的『讀書指南』。訓練一結束,它們就被移除。

把這兩種設計並排來看。VGG-16:約 1.38 億參數,優美地簡單,是一匹任勞任怨的骨幹——但很重。GoogLeNet:約 500 萬參數,22 層,在 ImageNet 上的 top-5 準確率相當或更好(兩者都在約八十幾到九十出頭百分比),而記憶體與計算量只佔一小部分。這篇的教訓是:『架構上的巧思』——小濾波器堆疊、1×1 通道混合、並行分支,以及一個池化的分類頭——能比蠻力遠遠更便宜地為你換來深度與準確率。