跳轉到

10.1 卷積神經網路CNN基礎#

概念說明#

卷積神經網路 (Convolutional Neural Network, CNN) 把第 3 章的卷積變成「可學習的層」:核心的權重不是設計出來的,而是訓練出來的。一個卷積層由若干個 k×k×C 的核心組成(C 是輸入通道數,RGB 為 3;深層則是特徵圖數量),每個核心掃過輸入產生一張特徵圖 (feature map),一次捲積層的輸出通道數 = 核心個數。

out[o, y, x] = Σ_c Σ_i Σ_j in[c, y+i, x+j] · W[o, c, i, j] + b[o]

與影像濾波的卷積相比多了兩件事:跨通道求和(RGB 三通道一起加權)與偏項 + 非線性。

CNN 的兩個支柱設計:

  • 局部連接 + 參數共享:每個神經元只看 3×3 鄰域,且同一核心在全圖共享。若用全連接處理 224×224×3 影像的第一層,一個 1000 個輸出的神經元要 1.5 億參數;3×3 卷積層只要 27 個。這是「影像具有平移不變的局部結構」的先驗直接寫進架構——Sobel、Gabor 濾波器(第 4 章)就是手工版的先驗,CNN 學到的第一層核心長得極像它們。
  • 層級堆疊 → 感受野增長:每層 3×3 卷積讓感受野(一個輸出神經元能「看到」的輸入範圍)擴大 2。堆疊 L 層 3×3,感受野 = 2L+1。淺層學邊緣與色塊(相當於第 3、4 章),中層學紋理與部件,深層學物件整體——特徵由局部到語義的階梯是 CNN 最重要的現象。

其他關鍵組件:

  • 啟動函數:ReLU f(x) = max(0, x)。沒有非線性,堆疊一百層卷積仍等價一層。ReLU 計算免費且緩解梯度消失。
  • 池化 (pooling):2×2 最大池化把特徵圖長寬減半,提供小幅平移不變性並降低計算。現代架構常用 stride=2 的卷積取代。
  • Batch Normalization:對每個 channel 按批做標準化,穩定訓練、允許更大學習率。
  • Dropout:訓練時隨機丟棄神經元,抑制過擬合。

訓練:前向傳播算預測 → 損失(分類用交叉熵)→ 反向傳播算梯度(鏈鎖律逐層回推,卷積層的反向就是「用翻轉的核心做卷積」)→ 最佳化器(SGD with momentum / Adam)更新權重。資料增廣(第 7 章的幾何變換 + 亮度抖動)是免費的正則化。

演算法步驟(迷你 CNN 前向傳播)#

  1. 輸入影像正規化到 [0,1](或標準化)。
  2. 卷積層:滑動核心計算所有輸出通道,套 ReLU。
  3. 池化:2×2 取最大,解析度減半。
  4. 重複 2~3 數次,特徵圖越來越小、語義越來越強。
  5. 展平 (flatten) → 全連接層 → softmax 得各類機率。
  6. (訓練時)算損失、反向傳播、更新權重。

Python 程式碼範例#

import numpy as np

def conv2d(x, W, b):
    # x: (C_in, H, W), W: (C_out, C_in, k, k), 輸出尺寸 same(stride=1)
    C_out, C_in, k, _ = W.shape
    r = k // 2
    P = np.pad(x, ((0,0),(r,r),(r,r)))
    _, H, Wd = x.shape
    out = np.zeros((C_out, H, Wd))
    for o in range(C_out):
        for c in range(C_in):
            for dy in range(k):
                for dx in range(k):
                    out[o] += P[c, dy:dy+H, dx:dx+Wd] * W[o, c, dy, dx]
        out[o] += b[o]
    return np.maximum(out, 0)          # ReLU

def maxpool2(x):
    H, W = x.shape[1], x.shape[2]
    return x[:, 0:H//2*2, 0:W//2*2].reshape(
        x.shape[0], H//2, 2, W//2, 2).max(axis=(2, 4))

rng = np.random.default_rng(14)
# 輸入 1×8×8 的灰階影像(一個亮斜邊)
x = np.zeros((1, 8, 8))
for i in range(8):
    x[0, i, 7-i:min(8, 10-i)] = 1.0

# 第一層:2 個 3×3 核心(模擬水平/垂直邊緣偵測器)
W1 = np.zeros((2, 1, 3, 3))
W1[0, 0] = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])/8    # 類 Sobel 垂直邊
W1[1, 0] = np.array([[-1,0,1],[-2,0,2],[-1,0,1]])/8    # 類 Sobel 水平邊
b1 = np.zeros(2)
a1 = conv2d(x, W1, b1)
print("卷積層輸出形狀:", a1.shape, " ReLU 後非零比例:", (a1>0).mean().round(3))
print("通道0(垂直邊響應)最大值:", a1[0].max().round(3))

p1 = maxpool2(a1)
print("池化後形狀:", p1.shape, "(解析度減半,響應保留)")

# 感受野計算
rf = 1
for k in [3, 3, 3]:
    rf += k - 1
print("三層 3×3 卷積的感受野:", rf, "(≈ 一個 7×7 鄰域)")
n_params_full = 64*64*1000
n_params_conv = 3*3*1*8
print(f"全連接第一層參數量: {n_params_full:,} vs 8 核卷積層: {n_params_conv}")

執行後:兩個隨機初始化改成「手工 Sobel」的核心分別在斜邊上產生響應,ReLU 保留正響應並把負半截歸零(非零比例約一半);池化把 8×8 縮到 4×4 且最大響應仍在;參數量的對比(1.6 億 vs 72)展示了參數共享的威力。這個手寫前向傳播與 PyTorch 的 nn.Conv2d 數學完全相同——框架只是加速與自動求導。

複雜度分析#

一個卷積層:C_out 個核心各在 H×W 位置做 C_in·k² 次乘加,O(H·W·C_in·C_out·k²)——注意它隨通道數平方增長,CNN 計算量的主要來源。空間:特徵圖 O(H·W·C_out)。加速手段:(1) im2col 把卷積重排成矩陣乘法,用 BLAS/GPU;(2) 可分離卷積 (MobileNet):depthwise + pointwise,計算降到約 1/8~1/9;(3) stride / 池化減小 H·W。反向傳播約為前向 2 倍計算量。整體而言,CNN 推理是數十次卷積層的疊加,總 FLOPs 常以 GFLOPs 計——這是第 1 章「O(H·W) 基本量測」的百萬倍放大版,也是為什麼影像 AI 需要 GPU。

練習#

  1. 把範例中 W1 改成隨機初始化,觀察第一層特徵圖與手工 Sobel 的差異;想像訓練會把隨機核心推向什麼形狀。
  2. 實作 stride=2 的卷積,驗證「stride=2 卷積 + 3×3」與「3×3 卷積 + 2×2 池化」輸出尺寸相同但數值不同,比較兩者對 8×8 輸入的資訊保留。
  3. 計算一個 VGG 風格的設定(輸入 224×224×3,五個 stage 通道 64→128→256→512→512)的總 FLOPs,體會為什麼行動裝置需要輕量化架構。