10.3 卷積神經網路 (CNN)

第 10 章 · 深度學習
★★★★★ 難度:進階 · 課本 pp. 407–412 · 2026-08-12
CNN卷積層池化層CIFAR100資料擴增遷移學習

📚 理論基礎

卷積神經網路(Convolutional Neural Network, CNN)是專為影像分類設計的深度學習架構。它模仿人類視覺系統:先辨識低階特徵(邊緣、色塊),再組合成中階特徵(眼睛、耳朵),最後輸出類別。CNN 透過兩種特殊隱藏層——卷積層搜尋局部特徵、池化層壓縮維度——加上端到端的學習,在 CIFAR100 等影像分類任務中展現驚人成效。 📚 James, Witten, Hastie, Tibshirani (2023) An Introduction to Statistical Learning with Python, §10.3, pp. 407–412

本節的核心案例是 CIFAR100 影像分類:50,000 張訓練影像 + 10,000 張測試影像,每張 32×32 彩色(RGB 三通道),目標是分成 100 個類別。與前一節的 MNIST 手寫數字不同,CIFAR100 的影像包含複雜場景(動植物、交通工具、日常物品),傳統的全連接神經網路效果有限——這就是 CNN 登場的舞台。

CNN 的核心洞見非常直觀:一隻老虎不管出現在圖片的左上角或右下角,它仍然是老虎。CNN 的卷積濾波器(convolution filter)會在整張圖片上「滑動」,尋找相同的局部特徵——這是傳統神經網路做不到的,因為傳統網路把每個像素當成獨立變數,失去了空間關係。

10.3.1 卷積層:像偵探用的放大鏡

把卷積濾波器想像成一個小偵探用的放大鏡(通常是 3×3 或 5×5 的矩陣),在圖片上逐格滑動。每到一個位置,放大鏡裡的權重與圖片上對應的像素值相乘再加總,得到一個數字。這個數字越大,代表該位置越符合濾波器要找的特徵。

數學上,對於 2×2 濾波器 F 與 4×3 影像,卷積運算為:

\[ \text{Conv}(I, F)_{i,j} = \sum_{r=1}^{2}\sum_{c=1}^{2} I_{i+r-1,\,j+c-1} \cdot F_{r,c} \]

課本以一個 2×2 濾波器對 4×3 影像的示範展示:濾波器掃過影像的每個 2×2 子區域,產生一個較小的「卷積後影像」。

課本 Figure 10.7 用一張老虎的照片展示了兩個濾波器——一個偵測垂直條紋,一個偵測水平條紋。垂直濾波器在老虎的垂直斑紋處輸出高值,水平濾波器在水平邊緣處輸出高值。一個濾波器 = 一種特徵偵測器

對於 CIFAR100 的 32×32 彩色影像,每個卷積濾波器也有三個通道(RGB),分別對三個顏色通道做卷積後加總。第一個卷積層使用 K 個濾波器,就會產生 K 個輸出特徵圖(feature map),每一張特徵圖都是這 K 個「偵探」對原始影像的報告。

10.3.2 池化層:壓縮精華

池化層的作用是把一張大影像「濃縮」成小影像。最常見的是最大池化(Max Pooling):把影像切成不重疊的 2×2 區塊,每個區塊只保留最大值。

\[ \text{MaxPool}\begin{pmatrix}1&2&5&3\\3&0&1&2\\2&1&3&4\\1&1&2&0\end{pmatrix} = \begin{pmatrix}3&5\\2&4\end{pmatrix} \]

課本例:左上是 {1,2,3,0} → 3;右上是 {5,3,1,2} → 5;依此類推。

最大池化的雙重好處:壓縮資料量(2×2 → 1,減少 75%)和位置不變性(只要某個 2×2 區域中有一個高值,縮小後就能被偵測到,不因微小位移而消失)。

10.3.3 CNN 架構:層層堆疊的視覺金字塔

典型的深度 CNN 架構(課本 Figure 10.8,用於 CIFAR100):

關鍵設計原則:(1) 每次池化後通道數翻倍(補償空間維度損失),(2) 卷積+池化重複數次直到特徵圖只剩幾像素,(3) 最後接全連接層做分類。CIFAR100 目前最佳測試準確率約 75%。

10.3.4 資料擴增:一分錢變十分貨

資料擴增(Data Augmentation)是影像建模的「作弊合法化」:把每張訓練影像複製成多個版本,每個版本經過隨機變形——縮放、平移、旋轉、翻轉——但類別標籤不變。課本 Figure 10.9 展示了老虎照片的各種變體:水平翻轉後的老虎仍然是老虎。

資料擴增本質上是一種正則化:在每張原始影像周圍建立一個「雲」──所有自然變形都被視為同一個類別。SGD 的 mini-batch 訓練天生適合搭配擴增,因為可以在每次取 batch 時即時變形影像,不需要儲存擴增後的資料。

10.3.5 預訓練分類器與權重凍結

課本展示了一個產業級的 CNN:ResNet50,在 ImageNet(百萬張影像、上千類別)上預訓練。對新影像,ResNet50 的前幾十層已經學會了通用的邊緣、形狀、紋理特徵——這些知識可以「凍結」並轉移到新任務上,只需訓練最後幾層。

課本 Figure 10.10 用六張照片展示了 ResNet50 的表現:成功辨識老鷹(Cooper's hawk)但放大後誤判為噴泉(fountain)。這種遷移學習(transfer learning)讓小資料集也能享有大模型的視覺智慧——是當代電腦視覺最實用的技術之一。

⚡ Python 實作:卷積與池化的核心運算

# 10.3 卷積神經網路 — 手動實作卷積與最大池化(自給自足版本)
try:
    from google.colab import drive
    drive.mount('/content/drive')
    DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
    DATA_PATH = '/tmp/'

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits

# 載入 digits 作為小型影像資料(8×8 灰階,轉成 2D)
digits = load_digits()
img = digits.images[0]  # 第一張圖(數字 0)
print(f"原始影像 shape: {img.shape}")
print(f"像素值範圍: {img.min():.0f} – {img.max():.0f}")

# 展示原始影像
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img, cmap='gray')
axes[0].set_title('原始影像 (8×8)', fontsize=11)
axes[0].axis('off')

# === 卷積運算:用一個邊緣偵測濾波器 ===
# Sobel 水平邊緣濾波器(3×3):偵測水平方向的亮度變化
sobel_h = np.array([[-1, -2, -1],
                     [ 0,  0,  0],
                     [ 1,  2,  1]], dtype=float)

# 手動實作卷積(無填充,步長=1)
H, W = img.shape
kH, kW = sobel_h.shape
out_H, out_W = H - kH + 1, W - kW + 1
conv_out = np.zeros((out_H, out_W))

for i in range(out_H):
    for j in range(out_W):
        patch = img[i:i+kH, j:j+kW]
        conv_out[i, j] = np.sum(patch * sobel_h)

axes[1].imshow(conv_out, cmap='gray')
axes[1].set_title(f'卷積後 ({out_H}×{out_W}):水平邊緣', fontsize=11)
axes[1].axis('off')

# === 最大池化:2×2 區塊取最大值 ===
pool_H, pool_W = conv_out.shape[0] // 2, conv_out.shape[1] // 2
pool_out = np.zeros((pool_H, pool_W))

for i in range(pool_H):
    for j in range(pool_W):
        patch = conv_out[i*2:(i+1)*2, j*2:(j+1)*2]
        pool_out[i, j] = np.max(patch)

axes[2].imshow(pool_out, cmap='gray')
axes[2].set_title(f'池化後 ({pool_H}×{pool_W}):壓縮 75%', fontsize=11)
axes[2].axis('off')

plt.suptitle('CNN 基礎運算:卷積 → 池化(digits 數字 0)', fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
print(f"\n維度變化: {img.shape} → {conv_out.shape} → {pool_out.shape}")
print(f"總像素: {img.size} → {conv_out.size} → {pool_out.size} (壓縮 {(1 - pool_out.size/img.size)*100:.0f}%)")
原始影像 shape: (8, 8) 像素值範圍: 0 – 16 維度變化: (8, 8) → (6, 6) → (3, 3) 總像素: 64 → 36 → 9 (壓縮 86%)
# 卷積濾波器多樣性展示:不同濾波器捕捉不同特徵(自給自足版本)
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits

# 載入資料
digits = load_digits()
img = digits.images[5]  # 數字 5

# 定義三個濾波器
filters = {
    '水平邊緣': np.array([[-1,-2,-1],[ 0, 0, 0],[ 1, 2, 1]], dtype=float),
    '垂直邊緣': np.array([[-1, 0, 1],[-2, 0, 2],[-1, 0, 1]], dtype=float),
    '銳化':      np.array([[ 0,-1, 0],[-1, 5,-1],[ 0,-1, 0]], dtype=float),
}

H, W = img.shape
kH, kW = 3, 3
out_H, out_W = H - kH + 1, W - kW + 1

fig, axes = plt.subplots(1, 4, figsize=(14, 3.5))
axes[0].imshow(img, cmap='gray')
axes[0].set_title('原始影像 (數字 5)', fontsize=10)
axes[0].axis('off')

for idx, (name, f) in enumerate(filters.items()):
    conv = np.zeros((out_H, out_W))
    for i in range(out_H):
        for j in range(out_W):
            conv[i, j] = np.sum(img[i:i+3, j:j+3] * f)
    axes[idx+1].imshow(conv, cmap='gray')
    axes[idx+1].set_title(f'{name} 濾波器', fontsize=10)
    axes[idx+1].axis('off')

plt.suptitle('不同卷積濾波器捕捉不同特徵', fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
print("\nCNN 的核心概念:每個濾波器學到一種特定的特徵模式。")
print("水平濾波器:強調橫向筆畫(數字 5 的頂部橫線)")
print("垂直濾波器:強調縱向邊緣(數字 5 的垂直結構)")
print("銳化濾波器:強化所有邊緣(讓輪廓更清晰)")
CNN 的核心概念:每個濾波器學到一種特定的特徵模式。 水平濾波器:強調橫向筆畫(數字 5 的頂部橫線) 垂直濾波器:強調縱向邊緣(數字 5 的垂直結構) 銳化濾波器:強化所有邊緣(讓輪廓更清晰)
# 資料擴展示範:對同一張影像做隨機變形(自給自足版本)
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from scipy.ndimage import rotate, shift, zoom

# 載入數字 3 的影像
digits = load_digits()
img = digits.images[3]

augmentations = {
    '原始': lambda x: x,
    '旋轉 15°': lambda x: rotate(x, 15, reshape=False, mode='nearest'),
    '水平翻轉': lambda x: np.fliplr(x),
    '平移+縮放': lambda x: zoom(shift(x, (1, 0.5), mode='nearest'), 1.1, mode='nearest'),
}

fig, axes = plt.subplots(1, 4, figsize=(12, 3))
for idx, (name, aug_fn) in enumerate(augmentations.items()):
    aug_img = aug_fn(img.astype(float))
    axes[idx].imshow(aug_img, cmap='gray')
    axes[idx].set_title(name, fontsize=10)
    axes[idx].axis('off')

plt.suptitle('資料擴增(Data Augmentation):同一張影像的多種變體', fontsize=13, y=1.05)
plt.tight_layout()
plt.show()
print("\n資料擴增 = 正則化的一種形式。")
print("每張原始影像周圍產生「雲」——不增加標註成本,卻大幅降低過擬合。")
資料擴增 = 正則化的一種形式。 每張原始影像周圍產生「雲」——不增加標註成本,卻大幅降低過擬合。

🎯 應用場景

🏥 醫療:視網膜病變自動篩檢

Google 的糖尿病視網膜病變篩檢系統使用深度 CNN,在數十萬張眼底照片上訓練。卷積層先抓取微血管瘤(低階特徵),中層辨識出血點,高層判斷病變等級。在印度臨床試驗中,CNN 的靈敏度超過 87%,媲美眼科專科醫師。預訓練模型(如 ResNet)加上遷移學習讓小醫院也能部署。

🚗 自駕車:即時物件偵測

Tesla 和 Waymo 的自駕系統依賴 CNN 在毫秒級辨識行人、車輛、交通號誌。卷積層偵測邊緣和形狀,池化層壓縮運算量以滿足即時性要求(≥30 FPS)。資料擴增尤其重要——同一輛車在晴天、雨天、逆光的影像都要能正確分類。CIFAR100 的 32×32 分類是自駕車影像辨識的「Hello World」。

📱 手機應用:即時翻譯鏡頭

Google Translate 的鏡頭翻譯功能:CNN 先從手機畫面中定位文字區域(卷積層偵測邊緣),OCR 辨識文字,再翻譯並覆蓋回畫面。因為手機算力有限,通常使用 MobileNet 等輕量 CNN,用權重凍結技術繼承大模型的特徵萃取能力。這正是課本 §10.3.5 的預訓練+微調策略的實戰應用。

🤖 對 Hermes 架構的啟發:模組化特徵萃取器

CNN 的「卷積濾波器 = 可重複使用的特徵偵測器」對應 Agent 系統的 skill 模組化設計:每個 skill 就像一個濾波器,針對特定任務模式(如 PDF 擷取、GA4 查詢、程式審查),在主 Agent 的協調下「滑動」到不同輸入上。預訓練模型(resnet50)對應 skill 中的參考實作模板——不從零開始,繼承既有知識。資料擴增對應子 Agent 的多角度覆核:讓不同模型(Gemini、DeepSeek、Qwen)對同一輸入給出獨立判斷,再用共識機制篩選——這就是 AI 版的 max pooling。

⚖️ 優缺點對照

✅ 優點

❌ 缺點

📊 與其他神經網路架構比較

架構隱藏層參數共享?空間感知?適用領域參數效率
單層 NN (§10.1)1迴歸、二元分類高(簡單)
多層 NN (§10.2)≥2多元分類、表格資料中(需正則化)
CNN (§10.3)≥5(含卷積+池化)影像、影片分類極高(權重共享)
RNN (§10.5)≥1(遞歸)是(時間軸)否(時序感知)文字、序列資料

📚 對照對象選自 ISLP §10.1, §10.2, §10.5。

💡 關鍵洞察

「從 CIFAR100 的 32×32×3 像素到 100 個類別標籤,CNN 做了傳統統計學無法想像的事:讓電腦自己學會什麼是『有用的特徵』。不是人類設計特徵(SIFT、HOG),而是梯度下降在數百萬次迭代中,把 3×3 的小方塊打磨成邊緣、斑紋、形狀的偵測器。這不是魔法——這是權重共享、空間不變性、和層層抽象的數學勝利。」

— 課本 §10.3 精華 · 附 Figure 10.7–10.10 實證
← 10.2 多層神經網路 10.4 文件分類 →(即將推出)