4.1 卷積、池化與感受野:CNN的運作原理(1980-1998)#
4.1.1 一個問題的開始:為什麼要讓機器學會「看」#
在進入 1980 年代之前,電腦視覺的主流做法是把「看見物體」的工作交給人手寫的規則。第 2 章的 SIFT、HOG、Canny 全部屬於這一類:研究者先在生物學或物理直覺上提出假設(例如「邊緣是資訊量最高的地方」),再把它寫成固定的算子,最後由機器以固定的方式執行。
這種做法在 2000 年代初達到頂峰,然後就停滯了。原因在第 3 章已經講得很清楚:手工特徵能表達「局部的形狀與紋理」,卻無法表達「這是杯子」。語義鴻溝不是靠更好的描述子能填平的,因為描述子本身的設計空間裡就沒有「物件」這個概念。
要跨越這道鴻溝,只有兩條路:
- 讓人手工標註一張圖片裡所有東西的語義——這就是 ImageNet 的做法(4.2 節)。
- 讓機器自己從資料中學出「什麼樣的像素組合代表什麼」——這就是深度學習的做法(本章)。
而要用資料學出「什麼樣的像素組合代表什麼」,有個先決條件:模型必須能處理二維空間結構。這裡出現本章的核心張力:
- 像素是一維長串(把 224×224 的圖攤平成 50176 個數字),但我們不希望模型知道「左上角」和「右上角」的像素其實相鄰。
- 一個完全連接層(fully connected layer)面對這個問題時,除了參數爆炸(一層就要 50176 × 4096 ≈ 2 億個權重)之外,更糟的是它把位置寫死了。若杯子在照片裡往右移 10 像素,一個全連接網路看到的就是完全不同的輸入向量。
- 而我們知道,自然影像的物體可以出現在任何位置,而且局部形狀在不同位置、不同尺度上是重複出現的(一隻貓的耳朵,不論在圖的左邊還是右邊,長相都一樣)。
於是生物學家給了計算機一個明確的答案。
4.1.2 生物啟發:Hubel 與 Wiesel 的兩種細胞#
Hubel 與 Wiesel 在 1959 至 1962 年間對貓的視覺皮層(primary visual cortex, V1)進行了著名的「棒狀細胞實驗」。他們把電極插入貓的視覺皮層,投影移動的黑色長條,然後記錄不同神經元的放電反應。
他們發現了兩類截然不同的神經元:
- 簡單細胞(simple cell):只對特定位置、特定方向的邊緣有反應,而且反應具有方向選擇性——一個只對水平邊緣反應的細胞,對 45 度斜邊反應就大幅減弱。更關鍵的是,它們的感受野很小,只覆蓋皮層上一小塊區域(約 1mm²),但感覺野內的不同位置對應輸入影像的不同位置。
- 複雜細胞(complex cell):對邊緣的位置不敏感,但對方向敏感。只要那條邊緣落在感受野內的任何位置,這個細胞都會放電。
這個發現的計算機對應物,一寫就是三十年後:
| 生物學發現 | 計算機對應 |
|---|---|
| V1 簡單細胞的小感受野、局部連通 | 卷積核的局部連接性(每個輸出只依賴輸入的一小塊鄰域) |
| 許多簡單細胞共享同一套「邊緣偵測邏輯」,只是位置不同 | 權重共享(同一組卷積核參數滑過整張圖) |
| 複雜細胞的位置不變性 | 池化(pooling):局部最大值或平均值 |
| 視覺皮層的階層組織(V1 → V2 → V4 → IT) | 網路的一層層堆疊,每層學到更抽象的特徵 |
Hubel 與 Wiesel 自己的推論是:視覺系統的前段是「通過一種本地的、可重複的濾波器群來抽取特徵」,後段才是「組合這些特徵」。
必須指出的常見誤解:神經科學並沒有證明「視覺皮層就是卷積神經網路」。CNN 的數學結構與生物學只是啟發性相似,不是同一件事。生物神經元是帶時間常數的脈衝發放單元,權重是連續值且高度可塑,突觸傳遞有耗時與衰減——這些跟反向傳播的生物學合理性爭議到今天都還沒平息(見第 10 章)。但作為工程設計的靈感來源,Hubel–Wiesel 的貢獻無可取代。
4.1.3 從 Neocognitron 到 LeNet:三個原則的成形#
1980:Fukushima 的 Neocognitron#
Kunihiko Fukushima 在 1980 年發表的 Neocognitron,是第一個把上述生物學直覺完整轉成數學並真的訓練成功的架構。它有兩種單元:
- S 單元(feature extraction,類似簡單細胞):對應一個感受野,做的是加權求和後經過一個單峰響應函數(一個類 sigmoid 的非線性)。今天的 ReLU 之前,ReLU 的最接近的祖先就是這種「單峰非線性」。
- C 單元(feature extraction,類似複雜細胞):在 S 單元的輸出上做局部平均,並且只回傳那些響應較強的 S 單元——這是 pooling 與稀疏化的雛形。
Fukushima 的關鍵設計有兩點後來被完全繼承:
- 權重共享:S 單元的權重在整張影像平面上共享。
- 階層化:C 單元的輸出再送入下一層同樣的 S–C 結構。
Neocognitron 沒有反向傳播,權重是用「Hebbian 學習 + 遮蔽(masking)」的啟發式規則訓練的。但架構本身是對的。Fukushima 本人在 1988 年的回顧文章中明確指出:他的貢獻是架構,不是學習演算法。
1986:反向傳播的成熟#
Rumelhart、Hinton 與 Williams 在 1986 年發表的 "Learning representations by back-propagating errors" 是另一塊關鍵拼圖。在那之前,多層網路只能用 Boltzmann 機那樣的慢速方法訓練。反向傳播給了多層網路兩樣東西:
- 可微分層的任意堆疊:只要每層可微,梯度就能鏈式傳遞。
- 權重共享仍然有效:梯度對共享權重是把所有使用位置的梯度加總,這正是「參數量與輸入尺寸無關」能成立的技術前提。
但這兩者結合起來的完整架構,還要等 LeCun。
1989:LeCun 的卷積#
Yann LeCun 在 1989 年發表的 "Backpropagation applied to handwritten digit recognition" 首次把幾件事綁在一起:反向傳播 + 局部連接 + 權重共享 + 卷積,並在美國郵政服務的支票辨識上取得實用成果。他後來在 1998 年與 Bottou、 Bengio、Haffner 發表的 LeNet-5,是這條路線的成熟形態,也是 1998 到 2012 年之間幾乎所有商業 OCR 與手寫辨識系統的骨幹。
LeNet-5 的架構(輸入 32×32×1):
flowchart LR
A["輸入 32x32x1"] --> B["C1: 卷積 6 組 5x5<br/>輸出 28x28x6"]
B --> C["S2: 平均池化 2x2 stride 2<br/>輸出 14x14x6"]
C --> D["C3: 卷積 16 組 5x5<br/>輸出 10x10x16"]
D --> E["S4: 平均池化 2x2 stride 2<br/>輸出 5x5x16"]
E --> F["C5: 卷積 120 組 5x5<br/>輸出 1x1x120"]
F --> G["F6: 全連接 84"]
G --> H["輸出 10 類"]三個原則從這張圖裡被提煉出來:
- 局部連接(local connectivity):每個神經元只看輸入的一小塊。
- 權重共享(weight sharing):同一層的所有神經元用同一組濾波器參數,只是作用位置不同。
- 階層抽象(hierarchical abstraction):越深的層看到的「有效輸入」越大、語意越抽象。
LeNet-5 的關鍵細節:C3 層不是「全連接到所有通道」,而是部分連接表——每個 C3 通道只連接 S2 的一半通道。這是當年的硬體限制下的省算力設計,但它意外地引入了通道間的資訊瓶頸與特徵分散的效果,現代的架構設計裡(例如 ResNeXt 的群組卷積、Mixture-of-Experts 的稀疏路由)還能看到這個想法的影子。
為什麼 CNN 是對的:參數量與資訊量的匹配#
LeCun 的論證可以形式化。假設一個全連接層要處理 \(H \times W \times C_{in}\) 的輸入並輸出 \(C_{out}\) 個通道:
對 \(224 \times 224 \times 3\) 的影像,這已經是 \(150528 \times 4096 \approx 6.2 \times 10^8\) 個權重——光是參數的記憶體就超過 2.5 GB,而且參數量遠大於訓練資料的樣本數,會嚴重過擬合。
卷積層的參數量是:
對 5×5 卷積處理 3 通道輸入輸出 64 通道: \(5 \times 5 \times 3 \times 64 + 64 = 4864\) 。比全連接少了五個數量級。
更關鍵的哲學差異是:
全連接層的參數量隨影像尺寸平方增長,因此它無法被重用到不同解析度的影像上,也無法在資料量不足時泛化。卷積層的參數量與影像尺寸無關,因此同一組參數可以用在任意大、任意小的影像上。
這就是 LeCun 所說的「把先驗知識編進架構本身」("cabling prior knowledge into the architecture")的意思:把「鄰近像素高度相關」、「物體在不同位置長相相同」這兩條先驗,從資料中學不到的硬編碼假設,變成架構的數學性質。這是本章與第 3 章的分水嶺——手工特徵是把先驗寫在算子裡,CNN 是把先驗寫在架構裡,而後來深度學習的整個故事就是把「寫在算子裡」的部分一步步搬進「學出來的權重裡」。
4.1.4 卷積層的數學#
交叉相關與真卷積#
在寫下式子之前必須先釐清一個幾乎所有初學者都搞混的問題。
數學上的真卷積(convolution)定義為
注意 \(g\) 的索引被翻轉了( \(t - \tau\) 而非 \(t + \tau\) )。這是積分在「反轉並平移」的意義下的卷積,具有交換律。
深度學習中的「卷積」實際上是交叉相關(cross-correlation):
濾波器沒有被翻轉。
為什麼要這麼做?因為交叉相關與真卷積只差一個固定翻轉。假設 \(K^{\text{flip}}\) 是把 \(K\) 翻轉 180° 後的版本,則
由於權重是學出來的,把 \(K\) 換成 \(K^{\text{flip}}\) 得到的函數族完全相同(把所有權重重新初始化即可一一對應)。所以為了避免每次卷積都做一次 180° 翻轉(浪費記憶體搬運),所有框架都直接用交叉相關,而把「卷積」這個名字沿用下來。
常見誤解:「CNN 的卷積在數學上不是卷積。」這是對的,但無害。真正需要注意的副作用是:交叉相關不具備真卷積的交換律,所以「CNN 卷積」與「傅立葉變換」的對角化技巧不能直接套用——這也是第 2.3 節的 DoG 那種「用高斯相減實作 LoG」的技巧,在 CNN 裡要重新用可分離卷積(separable convolution)的方式實作。
多通道卷積#
輸入 \(X \in \mathbb{R}^{H_{in} \times W_{in} \times C_{in}}\) ,卷積核 \(K \in \mathbb{R}^{k \times k \times C_{in}}\) ,bias \(b \in \mathbb{R}\) :
輸出 \(Y \in \mathbb{R}^{H_{out} \times W_{out} \times C_{out}}\) ,其中 \(C_{out}\) 個核不共享(第 \(o\) 個核 \(K_{\cdot,\cdot,\cdot,o}\) 有自己的權重)。
輸出尺寸的完整公式#
一般化的輸出尺寸公式,同時涵蓋 stride、padding、dilation:
逐項拆解其來源:
- 有效(valid)輸入尺寸:只有 \(H_{in} - D(k-1) - 1\) 個位置的完整核視窗。這裡 \(D\) 是 dilation(膨脹率), \(D(k-1)\) 是膨脹後核的有效跨距(effective kernel size),即 \(k_{eff} = D(k-1) + 1\) 。
- 加上 padding: \(2P\) 。
- 扣掉 1:因為 \(k\) 個位置跨距是 \(k - 1\) ,不是 \(k\) 。
- 除以 stride 再取整下界:第 \(s\) 個視窗的起點在 \(s \cdot S\) ,必須滿足 \(s \cdot S \le H_{in} + 2P - k_{eff}\) ,故視窗數為 \(\lfloor (H_{in} + 2P - k_{eff}) / S \rfloor + 1\) 。
Padding 的三種慣例:
| 命名 | \(P\) | 說明 |
|---|---|---|
| valid | 0 | 不補邊,輸出會縮小 |
| same | 依 stride 而定 | 讓輸出與輸入同尺寸(stride=1 時 \(P = \lfloor (k-1)/2 \rfloor\) ) |
| full | \(P = k-1\) | 輸出比輸入大,核完全在補零區內也算(用於相關性分析) |
Dilation 的直覺: \(D = 2\) 的 3×3 核實際上覆蓋 5×5 的範圍,但只有 9 個非零權重。顧名思義的「膨脹卷積」用在需要擴大感受野但不想增加參數量的地方,例如語意分割(第 5.2 節的 ASPP)與高解析度特徵處理。 \(D=1\) 的普通卷積是特例。
一個實用的「輸出等於輸入」公式( \(S=1\) , \(P = \lfloor (k-1)/2 \rfloor\) ):
這就是為什麼 ResNet(4.3 節)在 3×3 卷積之間使用 stride=1 的 1×1 或 3×3 殘差塊時,尺寸能完美對齊,讓「加法」操作成立。
參數量與計算量#
參數量:
**注意 \(N_{params}\) 中完全沒有 \(H_{in}\) 與 \(W_{in}\) **。這是 CNN 勝出的關鍵。
乘法累加次數(MACs,一次乘加算一個 FLOP):
以 AlexNet 的 conv1 為例: \(55 \times 55 \times 96 \times (11 \times 11 \times 3 + 1) \approx 1.05 \times 10^8\) MACs;而 conv2 是 \(27 \times 27 \times 256 \times (11 \times 11 \times 96 + 1) \approx 2.25 \times 10^8\) MACs。
參數量 ≠ 計算量,這是初學者最常混淆的一組數字:
| 層 | 參數量 | MACs |
|---|---|---|
| conv1 (11×11, 3→96) | 34,944 | 105M |
| conv2 (11×11, 96→256) | 296,704 | 225M |
| conv3 (3×3, 256→384) | 884,992 | 50M |
| conv4 (3×3, 384→256) | 294,912 | 17M |
| conv5 (3×3, 256→256) | 589,824 | 13M |
| fc6 (9216→4096) | 37,755,648 | 37.7M |
| fc7 (4096→4096) | 16,781,312 | 16.8M |
| fc8 (4096→1000) | 4,097,000 | 4.1M |
全連接層佔了 60% 的參數,但只佔 13% 的計算量;卷積層佔 40% 的參數,卻佔 87% 的計算量。這個對比解釋了為什麼後來幾乎所有架構都把全連接層換成 global average pooling(第 4.1.7 節),以及為什麼 2017 年後的工程優化重點從「減少 FLOPs」轉向「減少記憶體頻寬」(4.3 節)。
4.1.5 感受野:CNN 的「視野」如何長大#
定義#
感受野(receptive field)是深層網路裡一個關鍵概念:一個輸出單元的值,取決於輸入的哪一個子區域?
更精確地說,輸入張量上的一個位置 \(x\) ,若其值會影響某個神經元的輸出,則 \(x\) 就在那個神經元的理論感受野內。感受野可以分為:
- 理論感受野(theoretical receptive field):由架構決定、只看架構就能算出來的範圍。
- 有效感受野(effective receptive field):真正影響輸出的區域,通常比理論感受野小,且呈高斯型集中分佈(真正重要的只有中心幾個像素)。
逐層累積的推導#
考慮一個最簡單的情況:一串卷積層,每層都是 \(k \times k\) 、stride 1、padding 0 的卷積。
第 1 層的每個神經元看 \(1 \times 1 = 1\) 個輸入像素。 第 2 層的每個神經元看 \(k \times k\) 個第 1 層神經元,而那些第 1 層神經元各自看 \(k \times k\) 個像素——但因為是重疊的,這些輸入的聯集是一個 \((2k-1) \times (2k-1)\) 的方塊。
一般地,若第 \(l\) 層的核大小為 \(k_l\) 、stride 為 \(s_l\) 、padding 為 \(p_l\) 、dilation 為 \(d_l\) ,則感受野大小 \(r_l\) 與跳躍距離(相鄰兩個神經元在輸入上的中心距離) \(j_l\) 遞推式為:
初始值 \(r_0 = 1\) 、 \(j_0 = 1\) 。
代入 stride 1、dilation 1 的簡化情形,連續 \(L\) 層 \(k \times k\) 卷積的感受野大小為:
若全部 stride 1:
這解釋了一個關鍵事實:感受野隨層數「線性」成長,而不是指數成長。要讓一個深層網路的感受野覆蓋整張 \(224 \times 224\) 的影像,若只用 stride 1 的 3×3 卷積,需要 \((224 - 1) / 2 + 1 \approx 112\) 層。這就是為什麼 VGG(4.3 節)在深層之前必須插入 stride 2 的池化層,把空間尺寸快速縮小——縮小空間尺寸就是在放大「每一像素覆蓋多少輸入」的倍率。
感受野在特徵圖上的實際尺寸(這是另一個常被混淆的量):第 \(l\) 層特徵圖上的感受野邊長為
它隨層數遞減(因為 \(j_l\) 增長較快),說明越深的特徵圖上,一個格子對應原圖的更大區域。
一個完整的計算範例#
以 AlexNet 為例(padding 依原論文):
| 層 | 核 | stride | padding | \(r\) | \(j\) |
|---|---|---|---|---|---|
| conv1 | 11 | 4 | 0 | 11 | 4 |
| pool1 | 2 | 2 | 0 | 14 | 8 |
| conv2 | 11 | 1 | 0 | 14 + 10×8 = 94 | 8 |
| pool2 | 2 | 2 | 0 | 102 | 16 |
| conv3 | 3 | 1 | 1 | 102 + 2×16 = 134 | 16 |
| conv4 | 3 | 1 | 1 | 150 | 16 |
| conv5 | 3 | 1 | 1 | 166 | 16 |
| pool5 | 3 | 2 | 0 | 174 | 32 |
| fc6 | 1 | 1 | 0 | 174 | 32 |
fc6 的每個神經元看 \(174 \times 174\) 像素,覆蓋了 224×224 影像的絕大部分(因為 AlexNet 對影像做了邊緣反射填充)。這正是 AlexNet 能做分類的關鍵:分類需要全域資訊,而 CNN 的全域資訊是靠「用池化縮小特徵圖」這個技巧在少量層數內湊出來的。
全域感受野的伏筆#
感受野的計算說明了一個結構性限制:CNN 的感受野必須靠「層數 × 跳躍距離」慢慢長大,而且是中心對稱的矩形區域。它無法直接表達「影像右上角的那個紅色斑點和左下角的那個藍色斑點有關係」這種長距離、非局部的依賴。
這正是 2020 年 ViT(第 6.2 節)的切入點:自注意力在第一層就給出了全域感受野,而且不依賴局部性假設。CNN 的局部性歸納偏誤在 2012 到 2020 年間是純粹的優勢(資料效率高、訓練穩定),但在資料量足夠大時它就成了瓶頸。
把這個對比擺在一起看: * CNN:感受野隨深度線性成長,結構上偏置局部性。 * ViT:自注意力給出全域感受野,結構上偏置長距離關係,但需要更多資料與更好的初始化(第 6 章)。 * Swin(第 6.3 節):用局部自注意力 + 階層合併,試圖兩者兼得。
4.1.6 等變性與不變性:本章最重要的概念#
這是全書最重要的概念之一。混淆「等變」與「不變」是初學者最常犯的錯誤之一,也是理解「為什麼分類網路最後一層是 global average pooling 而偵測網路不是」的前提。
嚴格定義#
設 \(f\) 是作用於影像空間的函數, \(g\) 是作用於位置上的變換群。稱 \(f\) 對 \(g\) 等變(equivariant),若
即「先平移輸入再算,結果等於先算再平移輸出」。稱 \(f\) 對 \(g\) 不變(invariant),若
即「先平移輸入再算,結果完全一樣」。
用最簡單的兩個例子說明差別:
- 取某個固定位置的像素值 \(f(x)_{i,j} = x_{i,j}\) :這是等變但不是不變。輸入右移 3 像素,輸出也右移 3 像素。
- 對全圖取最大值 \(f(x) = \max_{i,j} x_{i,j}\) :這是不變(也因此等變,因為任何等變函數只要輸出沒有空間維度就自動不變)。
卷積是等變的#
設 \(x\) 是影像, \(\tau_\Delta\) 是平移 \(\Delta\) 像素的算子。卷積層的輸出滿足:
也就是卷積對平移等變。原因很直觀:卷積核滑過整張圖,把「這個樣式的邊緣在哪」這個資訊搬到輸出圖的對應位置。輸入的邊緣往右移,輸出的特徵圖也往右移。
這個等變性是 CNN 最重要的財產:它讓「模板匹配」可以用並行的移位操作一次算完,而不是對每個位置分別做一次比對。第 2.3 節的 SIFT 要先偵測關鍵點、再對每個關鍵點做描述子;CNN 直接對整張圖的所有位置同時做模板匹配。
池化帶來局部平移不變性#
考慮 max pooling 對一個小範圍平移的反應。假設輸入 \(x\) 中有一個峰值(最大值)在位置 \(p\) ,其值為 \(M\) 。若 \(p\) 移位了 \(\delta\) 像素,而 \(\delta\) 小於池化視窗大小 \(k\) ,那麼只要峰值仍在同一個池化視窗內,該視窗的 max 值仍然是 \(M\) 。
嚴格來說這只是「局部平移不變」:一個 \(k \times k\) 池化層(stride 1)對每個方向上 \(\pm (k-1)\) 像素的平移,在每個視窗內部是近似不變的;配合 stride 就變成「在特徵圖尺度上的平移不變」。
用一個更乾淨的說法:池化把「位置」資訊從特徵圖的格點座標,轉移到了「哪一個通道」。假設某個通道學到了「偵測水平邊緣」,那麼水平邊緣在圖中任何位置都會讓這個通道在對應位置附近有反應;經過池化後,這個反應的精確位置被抹掉,只剩下「附近有水平邊緣」這件事。
平均池化同理,只是它保留的是均值而非峰值,因此對小幅平移更「連續」但對小特徵更不敏感。實驗上(4.2 節的 AlexNet 原始設定)AlexNet 用的是 max pooling,作者在論文裡明確說明理由:
「max pooling 優於 average pooling,因為它能產生更稀疏、更多局部平移不變的特徵。」
Global Average Pooling 帶來全域平移不變性#
若在網路最後對每個通道做全域平均池化(global average pooling,GAP):
則
因為平移不改變全圖的總和。也就是說,GAP 讓整個分類器對平移完全不變。
這就是為什麼幾乎所有現代分類架構(ResNet、MobileNet、EfficientNet、ViT)的最後一步都是 GAP + 一層線性層:
好處有三個:
- 參數量暴減:把 AlexNet 那種 9216→4096 的巨型全連接層換成 512→1000,參數量從數千萬降到數千。
- 對輸入尺寸不敏感:GAP 讓網路可以接受任意解析度(這是 ResNet 與 MobileNet 能吃各種尺寸輸入的原因)。
- 正則化效果:強烈抑制模型去記憶「狗通常在圖片的左邊」這種位置偏置。
為什麼偵測不能 GAP#
GAP 丟掉了空間資訊,所以偵測任務不能只在最後做 GAP(第 5 章的伏筆)。偵測與分割需要在每個特徵圖位置上回答「這裡有沒有物件、類別是什麼、框在哪裡」,因此必須保留一個空間維度。
這裡有一個更細緻的說明,也是理解第 5 章所有架構的鑰匙:
| 任務 | 需要的空間粒度 | 架構設計 |
|---|---|---|
| 分類 | 全域 | 骨幹 + GAP + FC |
| 偵測 | 位置級(多個尺度) | 骨幹 + FPN/特徵金字塔(第 5.1 節) |
| 分割 | 像素級 | 骨幹 + 上採樣 + 跳連(第 5.2 節) |
一句話總結:卷積給你「等變」(位置在哪),池化給你「不變」(在不在),這兩者組合起來,讓你可以用一堆局部運算堆出一個全域的判斷——這是深度網路能堆疊的原因,也是它能處理的問題範圍的來源。
4.1.7 池化層的細節#
三種基本池化#
最大池化(max pooling):
平均池化(average pooling):
LP 池化(LP pooling):一般化形式,
\(p \to \infty\) 趨近 max pooling, \(p = 1\) 是 average pooling, \(p = 2\) 是平方根平均池化(介於兩者之間)。LP pooling 在 2015 年(Tony Lempitsky 的 "Learning the Architecture of Convolutional Neural Networks")被提出來,作為可學習的 pooling 來取代固定的 max/average。它的實驗結論是:對固定架構而言,max pooling 通常最好,但可學的 pooling 在 Joint Equalization 這類優化下能帶來約 3-5% 的提升——這是個好例子說明「把設計選擇變成可學參數」在深度學習裡通常有幫助,但幫助的幅度不足以推翻主流做法。
為什麼 max pooling 對小幅平移更穩健#
考慮一個一維的簡化模型。輸入訊號 \(x\) 中有一個峰值區域(假設是三角形的鋸齒),最大值在 \(p\) 。池化視窗大小 \(k\) 。
- Max pooling:只要峰值在視窗內,輸出就是峰值的高度 \(M\) ,完全不受平移影響。
- Average pooling:輸出是視窗內的均值。峰值平移時,視窗內覆蓋峰值的比例在變化,輸出因此連續變化。
用數字說明。設輸入是長度 10 的單位向量(只有一個 1), \(k = 3\) , \(S = 2\) 。峰值在位置 0 時,視窗 0 覆蓋位置 0,1,2,平均 = 1/3;峰值移到位置 1 時視窗仍覆蓋它,平均仍是 1/3;移到位置 2 時視窗覆蓋 2,3,4,平均仍是 1/3……(在 stride 1 的情況下)。但 max pooling 輸出一律是 1,完全不變。
更一般地,可以把 max pooling 看成一個順序統計量(order statistic):它輸出的是 \(k\) 個樣本中的第 \(k\) 大者。順序統計量對單一最大值的存在與否敏感,對它的精確位置不敏感(在視窗內)。這就是「局部平移不變」在數學上的來源。
一個誤區:「max pooling 有梯度所以能學」——錯。max pooling 本身沒有可學參數,也不可學;它只是一個固定算子。它的「梯度」指的是反向傳播時,梯度只會沿著最大值的位置回傳(其他位置梯度為 0),這叫「路由式梯度」,有時會造成梯度稀疏的問題。
Pooling 的問題#
- 重疊視窗(overlapping windows): \(k=3, S=1\) 時相鄰視窗重疊 2/3,這是「overlapping pooling」,源自 AlexNet。 \(k=2, S=2\) (VGG 的做法)則不重疊。重疊會增加計算量但也增加資訊重疊。
- 資訊損失:池化是不可逆的降維。 \(2 \times 2\) 平均池化丟棄 4 個值中的 3 個,理論上最大可丟失 \(3/4\) 的空間資訊。這是「理論感受野大於有效感受野」的直接後果。
- 不可學:max pooling 沒有參數,無法學出「這個通道該用 max 還是 mean」。這激發了可學池化(LP pooling)、注意力池化(attention pooling)等研究,但都沒有取代 max pooling。
- 對齊問題:max pooling 對微小的亞像素平移仍會跳動輸出(因為最大值位置的變化會造成輸出值的離散跳變)。這是訓練時的噪聲來源,也是 anti-aliasing(低通濾波後再池化)技術的動機。
- 空間尺寸必須整除:在 stride 2 的架構中,如果 \(H\) 是奇數就會有一列被丟棄(或需要非對稱 padding),這是實作時常見的 bug 來源。
Stride 作為下採樣#
另一種下採樣方式是用 stride > 1 的卷積直接跳過像素。它與「先卷積再池化」的差別是:
- stride 卷積:學出來的(每個輸出位置用不同權重組合,但權重仍跨位置共享)。
- 池化:固定的、可解釋的、資訊壓縮率更高的。
AlexNet 用 stride 4 的 conv1 快速降尺寸,VGG 用 stride 2 的 max pooling,ResNet 用 stride 2 的第一個 3×3 卷積(4.3 節)。現代架構(如 EfficientNet 的 MBConv)則常在同一個 block 內用 depthwise stride 2 同時完成下採樣。
4.1.8 常見架構元件#
ReLU#
分段導數:
(在 \(z = 0\) 處不可導,實務上取 0 或任意值。)
三個性質解釋了 ReLU 為何贏過 sigmoid/tanh:
- 計算便宜: \(\max(0,z)\) 是 1 個比較操作,而 sigmoid 需要指數函數。在 2012 年 AlexNet 的訓練裡,ReLU 讓網路收斂快了大約 6 倍——這個數字不是因為「數學上更簡潔」,而是因為在 2012 年的 GPU 上,sigmoid 的指數函數佔了相當可觀的執行時間。
- 正半軸梯度恆為 1:不會像 sigmoid 那樣在飽和區梯度趨近 0。注意這只解決了「單層」的梯度消失;跨層的梯度是這些導數的乘積,仍然會衰減(第 4.3 節的主題)。
- 稀疏性:ReLU 讓約一半的單位輸出為 0,形成稀疏表示。正則化效應(在 Adam 下約 \(2\times\) 的收斂速度提升,稀疏文件中有實驗證據)。
常見誤解:「ReLU 完全解決了梯度消失。」錯。ReLU 只保證當整條路徑上的所有前驅都為正時,梯度不會逐層衰減。負半軸的單元梯度為 0,會造成「死亡 ReLU(dying ReLU)」——參數一旦被推到負區間就永久失效。
衍生品: * Leaky ReLU: \(\max(\alpha z, z)\) , \(\alpha = 0.01\) 。 * PReLU: \(\alpha\) 可學。 * ELU: \(x\) 若 \(x \le 0\) 則為 \(\alpha (e^x - 1)\) ,負半軸有小的非零梯度。 * GELU: \(x \Phi(x)\) ,平滑版本,Transformer 時代常見(第 6 章)。 * Mish、Swish( \(x \cdot \sigma(x)\) ):可學的平滑版本。
Batch Normalization#
Batch Normalization(Ioffe & Szegedy, 2015)對 mini-batch 內的每個通道做標準化。設通道 \(c\) 在一個 batch 中的值為 \(x_1, \dots, x_B\) :
其中 \(\gamma\) 、 \(\beta\) 是可學的縮放與平移參數(作用是讓網路可以「取消」標準化,以便表示它真正需要的分布)。
訓練 vs 推論的差異:推論時沒有 mini-batch 可以算統計量,所以 BN 保存running statistics(用指數移動平均維護的 \(\mu\) 、 \(\sigma^2\) ),推論時直接用它們。這造成三個實務上的麻煩:
- 訓練與推論的行為不一致(train/eval mode 必須切換)。
- 依賴 batch size:batch 越小,統計量越噪。
- 分佈遷移敏感:如果部署環境的特徵分布和訓練時不同(例如用不同相機、不同解析度),running stats 會失準,造成效能崩潰。這是 2020 年之後在 test-time adaptation 相關研究中屢屢出現的問題。
BN 為何有效(機理):標準化把每層輸入重新居中縮放,等效於逐層自動調整 learning rate,讓深的網路也能用較大的 learning rate 訓練而不發散;同時它把「梯度大小」與「上一層參數的尺度」解耦,緩解了跨層梯度連乘的衰減。代價是引入了對 batch 的耦合(見 4.3 節)。
Dropout#
Srivastava 等人(2014)提出的 Dropout:在每個 mini-batch 前,以保留機率 \(p\) 隨機把單位置零,並把保留下來的值除以 \(p\) (inverted dropout)以維持期望不變。
機理是把它當作巨量的指數級模型平均(exponential ensemble):在保留率 \(p\) 下,一個有 \(n\) 個單位的網路等價於對 \(2^n\) 個子網路取平均。由於 subnet 之間共享權重,這個「巨大集成」是可行的。
**AlexNet 用 \(p = 0.5\) **,且只用在全連接層(fc6、fc7)。今天在卷積層用 dropout 已不普遍(因為 batch norm 本身提供正則化,且卷積層的 dropout 效果不佳)。
LayerNorm:ViT 時代的回歸#
Layer Normalization(Ba, Kiros & Hinton, 2016)不做跨樣本統計,而是對每個樣本內的特徵維度做標準化:
為什麼它在 ViT 時代回歸?
- 不依賴 batch size:可以在 batch size = 1 下訓練,對記憶體受限的場景(高解析度影像、大模型)至關重要。
- 序列長度方向的自然對齊:在 Transformer 中,LN 沿著「hidden dim」而非「batch」做統計,這是序列模型的主流選擇(第 6 章)。
- 訓練/推論行為一致:沒有 running stats,所以不需要 train/eval mode 切換,部署更簡單。
- CNN 上的弱點:LN 在 CNN 上會把「不同空間位置」的統計混在一起(因為 LN 在通道維度上做統計,同一個通道的空間位置被視為樣本),這對捲積的等變性是破壞性的。但在 ViT 中,這個「空間位置的混合」恰好無關緊要,因為注意力本來就是全域的。
| 正規化方式 | 統計維度 | 依賴 batch | Train/Eval 不一致 |
|---|---|---|---|
| BatchNorm | 跨樣本,單通道 | 是 | 是 |
| LayerNorm | 單樣本,通道維 | 否 | 否 |
| InstanceNorm | 單樣本,單通道(影像風格化用) | 否 | 否 |
| GroupNorm | 單樣本,通道分組 | 否 | 否 |
GroupNorm(Wu & He, 2018)是過渡方案:把通道分成 \(G\) 組,在每組內做標準化,介於 LN 與 BN 之間,專為 small batch 訓練設計。
4.1.9 初始化:為什麼 ReLU 需要 He 初始化#
Xavier/Glorot 初始化(2010)#
Glorot & Bengio 的想法是方差守恆(variance preservation):讓每一層的輸出方差不因層數增加而爆炸或消失。
設 \(y = Wx + b\) , \(W\) 的元素 i.i.d., \(\mathrm{Var}(W_{ij}) = \mathrm{Var}(w)\) , \(x\) 的各元素獨立且 \(\mathrm{Var}(x_i) = \mathrm{Var}(x)\) , \(\mathrm{Var}(b) = 0\) 。則
要讓 \(\mathrm{Var}(y) = \mathrm{Var}(x)\) ,需要
這稱為 Xavier 初始化。Xavier 原始論文考慮的是 tanh 與 sigmoid(對稱的 S 型活化),並給出正態分佈的標準差:
這就是常見的 glorot_normal / glorot_uniform。
He 初始化(2015)#
He 等人在 "Delving Deep into Rectifiers" 中指出:ReLU 在負半軸把一半的輸入歸零,這改變了方差傳遞的規則。
設 ReLU 前的預激值 \(z = Wx\) , \(\mathrm{Var}(z) = n_{in} \mathrm{Var}(w) \mathrm{Var}(x)\) 。經過 ReLU 後(假設 \(z\) 對稱分布):
也就是說輸出的方差減半。要讓方差守恆( \(\mathrm{Var}(\text{out}) = \mathrm{Var}(\text{in})\) ),必須:
因此 He 初始化:
對應 PyTorch 的 kaiming_normal_(mode='fan_in')。
注意:這裡用的是 fan_in 而不是 Xavier 的 fan_in + fan_out。原因是卷積層裡「輸入維數」才是決定方差傳遞的量。對一個 \(k \times k \times C_{in}\) 的卷積核,每個輸出行是 \(k^2 C_{in}\) 維向量的內積,所以 fan_in = \(k^2 C_{in}\) 。PyTorch 的 kaiming_normal_ 會自動把 fan_in 計算成 \(k^2 C_{in}\) 。
對照表#
| 初始化 | 標準差 | 適用活化 | 依據 |
|---|---|---|---|
| Xavier (Glorot) | \(\sqrt{2/(n_{in}+n_{out})}\) | tanh / sigmoid | 對稱 S 型,方差守恆 |
| He (Kaiming) | \(\sqrt{2/n_{in}}\) | ReLU | 負半軸截斷導致方差減半 |
| LeCun | \(\sqrt{1/n_{in}}\) | 線性 / 有偏置的 ReLU | 最原始版本 |
| Orthogonal | \(Q Q^T / n_{out}\) | RNN / GAN | 保持梯度範數 |
| Zero | 0 | 殘差分支最後一層 | 讓殘差分支初始為恆等映射(4.3 節) |
實務提醒:初始化解決的是「起始點的尺度問題」,不是跨層的梯度消失問題(4.3 節)。深度網路中真正的敵人是梯度的連乘衰減,而初始化的貢獻是讓衰減的基準速率合理。兩者必須分開談,混為一談是常見的錯誤。
4.1.10 從零實作:numpy 卷積與 PyTorch 對照#
純 numpy 實作 2D 卷積#
import numpy as np
def conv2d(x, w, b=None, stride=1, pad=0, dilation=1):
"""
x : (C_in, H, W)
w : (C_out, C_in, kH, kW)
b : (C_out,) or None
"""
C_in, H, W = x.shape
C_out, _, kH, kW = w.shape
if b is None:
b = np.zeros(C_out, dtype=x.dtype)
# padding: 零填充
xp = np.pad(x, ((0, 0), (pad, pad), (pad, pad)), mode="constant")
# 膨脹:用 stride 技巧在核內插零
w_eff = np.zeros((C_out, C_in, (kH - 1) * dilation + 1, (kW - 1) * dilation + 1), dtype=w.dtype)
for i in range(kH):
for j in range(kW):
w_eff[:, :, i * dilation, j * dilation] = w[:, :, i, j]
kH_eff, kW_eff = w_eff.shape[2], w_eff.shape[3]
H_out = (H + 2 * pad - kH_eff) // stride + 1
W_out = (W + 2 * pad - kW_eff) // stride + 1
# im2col:把每個滑動視窗攤平成列,再做一次矩陣乘法
cols = np.zeros((C_in * kH_eff * kW_eff, H_out * W_out), dtype=x.dtype)
idx = 0
for c in range(C_in):
for i in range(kH_eff):
for j in range(kW_eff):
patch = xp[c, i:i + (H_out - 1) * stride + 1:stride,
j:j + (W_out - 1) * stride + 1:stride]
cols[idx] = patch.flatten()
idx += 1
# w_eff: (C_out, C_in*kH*kW) -> (C_in*kH*kW, C_out)
out = (w_eff.reshape(C_out, -1) @ cols).reshape(C_out, H_out, W_out)
return out + b.reshape(-1, 1, 1)
def relu(x):
return np.maximum(x, 0)
def maxpool2d(x, k=2, stride=2):
C, H, W = x.shape
H_out = (H - k) // stride + 1
W_out = (W - k) // stride + 1
out = np.zeros((C, H_out, W_out), dtype=x.dtype)
for i in range(H_out):
for j in range(W_out):
window = x[:, i * stride:i * stride + k, j * stride:j * stride + k]
out[:, i, j] = window.max(axis=(1, 2))
return out
def global_avgpool(x):
return x.mean(axis=(1, 2))
與 PyTorch 對照驗證#
import torch
import torch.nn as nn
torch.manual_seed(0)
np.random.seed(0)
C_in, H, W = 3, 32, 32
C_out, k = 4, 3
x = np.random.randn(C_in, H, W).astype(np.float64)
w = np.random.randn(C_out, C_in, k, k).astype(np.float64)
b = np.random.randn(C_out).astype(np.float64)
mine = conv2d(x, w, b, stride=1, pad=1, dilation=1)
torch_x = torch.from_numpy(x)[None] # (1, C, H, W)
torch_w = torch.from_numpy(w) # (C_out, C_in, k, k)
torch_b = torch.from_numpy(b)
torch_out = nn.functional.conv2d(torch_x, torch_w, torch_b, stride=1, padding=1)
print("max abs diff:", np.abs(mine - torch_out.numpy()[0]).max())
# 預期輸出: max abs diff: 約 1e-14(浮點誤差等級)
重點提醒:PyTorch 的 nn.Conv2d 使用的是交叉相關。所以在驗證時不能把核翻轉;如果要驗證「真卷積」,得自己把核翻轉 180° 後再呼叫。這個陷阱在實作自訂算子時非常常踩。
感受野隨層增長的示範#
def receptive_field(layers):
"""
layers: list of (k, stride, padding, dilation)
回傳每一層的理論感受野 r、跳躍距離 j、特徵圖上的感受野 f
"""
r, j = 1, 1
H = W = 224
out = []
for (k, s, p, d) in layers:
r = r + (k - 1) * d * j
j = j * s
H = (H + 2 * p - d * (k - 1) - 1) // s + 1
W = (W + 2 * p - d * (k - 1) - 1) // s + 1
out.append({
"H_out": H, "W_out": W,
"r": r, "j": j,
"f": r / j + 1,
})
return out
alexnet_layers = [
(11, 4, 0, 1), # conv1
(2, 2, 0, 1), # pool1
(11, 1, 0, 1), # conv2
(2, 2, 0, 1), # pool2
(3, 1, 1, 1), # conv3
(3, 1, 1, 1), # conv4
(3, 1, 1, 1), # conv5
(3, 2, 0, 1), # pool5
]
for i, info in enumerate(receptive_field(alexnet_layers), 1):
print(i, info)
輸出會顯示:conv1 之後 \(r = 11\) ,fc6 之後 \(r = 174\) ,也就是說最後的全連接層每個神經元「看到」原圖 174×174 的範圍——這是 AlexNet 能分類的幾何前提。
視覺化 filter 與 feature map#
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
net = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=11, stride=4, padding=2),
nn.ReLU(),
nn.MaxPool2d(2),
)
net.eval()
img = torch.randn(1, 3, 224, 224)
with torch.no_grad():
feat = net[2](net[1](net[0](img)))
fig, axes = plt.subplots(4, 4, figsize=(12, 12))
for i, ax in enumerate(axes.flatten()):
if i < 16:
w = net[0].weight[i].detach().numpy()
w = (w - w.min()) / (w.max() - w.min() + 1e-8)
ax.imshow(w.transpose(1, 2, 0))
ax.set_axis_off()
plt.suptitle("Randomly initialized conv1 filters (16 of 3x96)")
plt.savefig("random_filters.png", dpi=120, bbox_inches="tight")
訓練完成的模型(4.2 節的實作)會產生辨識得出意義的 filter——這正是 Zeiler & Fergus (2014) 的 deconvnet 視覺化的基礎。
4.1.11 本節小結:CNN 的四個數學保證#
把本節的內容濃縮成四條「設計 CNN 時必須記住的數學性質」:
- 參數量與輸入尺寸解耦: \(N_{params} = k^2 C_{in} C_{out} + C_{out}\) ,不含 \(H, W\) 。這讓同一組權重可處理任意解析度,也是 CNN 能用有限資料訓練的前提。
- 平移等變性: \(f(T_\Delta x) = T_\Delta f(x)\) 。卷積把「模板在哪個位置」的答案,以並行的方式一次算完。
- 池化的局部不變性:max pooling 把小幅平移的影響吸收掉;GAP 給出全域不變性。等變 + 不變 是深度堆疊能成立的關鍵。
- 感受野的線性成長: \(r_L = 1 + L(k-1)\) (stride 1 時)。這既是 CNN 的效率來源(結構偏置局部性),也是它的天花板(無法直接表達長距離依賴)——第 6 章的 ViT 正是針對這條限制而來。
同時記住兩個常見誤解: * 卷積層的參數少,不代表計算量也少;參數與 FLOPs 是兩件事。 * ReLU 解決的是單層梯度,不解決跨層梯度連乘;這要留到 4.3 節。
下一節,我們看這套原理在 2012 年如何第一次大規模贏過手工特徵。