跳到主要內容

Day 32 CNN 原理

Day 32 CNN 原理

引言

Day 31 我們用全連接網路(MLP)在 MNIST 上達到約 98% 的測試正確率。不過 MLP 把每張 28×28 的影像「攤平」成 784 維的向量後才送進模型,這樣做有兩個明顯的限制:一是參數量爆炸(784×128 就已經超過十萬個權重),二是失去了空間資訊(影像中相鄰像素的關係全部被拆散)。卷積神經網路(Convolutional Neural Network,CNN)正是為了解決這兩個問題而設計的架構。

CNN 的核心是兩個新操作:卷積(convolution)與池化(pooling)。卷積用小型的濾鏡(filter)在影像上滑動,萃取出局部特徵;池化則縮小特徵圖的尺寸、保留重要資訊。今天會用三個小節,先說明為什麼 CNN 適合影像,再介紹卷積與池化的運作方式,最後把整個 CNN 拼成一個 PyTorch 模型。

為什麼需要卷積神經網路

處理影像時,最直覺的方式莫過於把每個像素當成一個特徵。一張 224×224 的彩色影像攤平後有 224×224×3 = 150,528 個輸入維度。如果第一層用 256 個神經元,參數量會高達 150,528×256 ≈ 3,800 萬。這還只是第一層;影像解析度更高、網路更深的話,參數會多到無法訓練。

更糟的是,MLP 把每個像素視為獨立特徵,無法利用「相鄰像素通常高度相關」這個影像特性。CNN 的設計正是為了回應這兩個問題:

  1. 局部連接(local connectivity):卷積層的每個輸出像素只與輸入影像的一小塊區域相連,參數量大幅下降。
  2. 權重共享(weight sharing):同一個卷積濾鏡在整張影像的不同位置都使用同一組權重,這表示模型學到的是「在某個位置出現某種特徵」,而不是「在固定座標出現某種特徵」,對平移、縮放具有一定程度的容忍。

實務上,CNN 的參數量遠比同樣深度的 MLP 少,訓練也比較穩定,這也是為什麼 2012 年 AlexNet 在 ImageNet 上大勝傳統方法之後,影像相關任務幾乎都被 CNN 與它的後繼者(如 ResNet)所主宰。

卷積運算:濾鏡與特徵圖

卷積的概念可以從「濾鏡」理解。假設有一個 3×3 的小矩陣(卷積核/kernel)沿著輸入影像滑動,每到一個位置就把對應的元素相乘再加總,輸出放進結果矩陣的對應位置。這個結果矩陣稱為「特徵圖」(feature map),每個數值代表「這個區域與濾鏡的相似程度」。

我們用手寫的範例重現一次最基本的 2D 卷積,用一個 3×3 的濾鏡偵測「垂直邊緣」。

import torch
import torch.nn.functional as F

# 5x5 的輸入影像,中間有一條垂直亮線
x = torch.tensor([[0., 0., 1., 0., 0.],
                  [0., 0., 1., 0., 0.],
                  [0., 0., 1., 0., 0.],
                  [0., 0., 1., 0., 0.],
                  [0., 0., 1., 0., 0.]])

# 3x3 的垂直邊緣濾鏡
kernel = torch.tensor([[-1., 0., 1.],
                       [-1., 0., 1.],
                       [-1., 0., 1.]])

# PyTorch 期望輸入是 [batch, channel, H, W]
x = x.view(1, 1, 5, 5)
kernel = kernel.view(1, 1, 3, 3)

feature_map = F.conv2d(x, kernel)
print(feature_map[0, 0])

# 輸出(3x3,5 - 3 + 1 = 3):
# tensor([[ 3.,  0., -3.],
#         [ 3.,  0., -3.],
#         [ 3.,  0., -3.]])

亮線左側的乘積和是 3、右側是 -3,其他位置因為左右像素相同而互相抵消,輸出為 0。正值與負值分別代表由暗到亮、由亮到暗的變化方向。這就是「特徵圖」:CNN 透過學習濾鏡的權重,自動找出對任務有用的視覺特徵,例如邊緣、角點、紋理,再由更深的層組合成形狀、物體。

實際的卷積層會設定幾個重要的超參數:

  • 濾鏡大小(kernel size):常見的是 3×3 或 5×5。
  • 步幅(stride):濾鏡每次滑動的步長,預設 1;設為 2 會讓特徵圖縮小為原本的一半。
  • 填充(padding):在影像外圍補上像素(常用 0),目的是保留邊界資訊並控制輸出尺寸。設定 padding=1 搭配 3×3 濾鏡,可以讓輸出與輸入同樣大小。
  • 輸出通道數(out_channels):濾鏡的數量,每個濾鏡負責偵測不同特徵。

在 PyTorch 裡,這些超參數都對應到 nn.Conv2d 的引數。

import torch.nn as nn

conv = nn.Conv2d(in_channels=1, out_channels=16,
                 kernel_size=3, stride=1, padding=1)

# 隨機初始化一張「批次大小 1、1 個通道、28x28」的影像
x = torch.randn(1, 1, 28, 28)
y = conv(x)
print(y.shape)   # torch.Size([1, 16, 28, 28])

輸入是 [batch, channel, H, W] 的四維張量。輸出通道數變成 16(因為設定 16 個濾鏡),空間尺寸因為 padding=1 維持 28×28 不變。這就是卷積層最基本的行為。

池化層:縮小特徵圖

池化(pooling)的作用是縮小特徵圖的空間尺寸,達到減少參數與增加感受野的效果。最常見的是最大池化(max pooling):在每一個小區塊(例如 2×2)裡取最大值,輸出到下一層。

pool = nn.MaxPool2d(kernel_size=2, stride=2)

x = torch.randn(1, 16, 28, 28)
y = pool(x)
print(y.shape)   # torch.Size([1, 16, 14, 14])

經過 2×2 的最大池化後,空間尺寸從 28×28 縮小為 14×14,通道數 16 不變。最大池化的直覺是「保留這個區塊裡最強的訊號」,強迫模型對小位移更不敏感,這對影像分類很有幫助。除了最大池化,還有平均池化(nn.AvgPool2d),常用於網路末端、把特徵圖收成向量。

典型的 CNN 結構會把卷積、激活函式、池化反覆組合:Conv → ReLU → Conv → ReLU → Pool,重複幾輪之後把特徵圖攤平,接上全連接層做分類。每一輪池化會把空間尺寸縮一半,讓網路能逐步看到更大的區域。

用 PyTorch 拼出一個小 CNN

把上面的零件組合起來,就得到一個典型的小型 CNN:兩個卷積區塊,最後接全連接層分類。

import torch.nn as nn

class SmallCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # 區塊 1:1 -> 16 通道
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1)
        self.relu1 = nn.ReLU()
        self.pool1 = nn.MaxPool2d(2, 2)            # 28x28 -> 14x14

        # 區塊 2:16 -> 32 通道
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        self.relu2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(2, 2)            # 14x14 -> 7x7

        # 分類頭
        self.fc1 = nn.Linear(32 * 7 * 7, 128)
        self.relu3 = nn.ReLU()
        self.fc2 = nn.Linear(128, num_classes)

    def forward(self, x):
        x = self.pool1(self.relu1(self.conv1(x)))
        x = self.pool2(self.relu2(self.conv2(x)))
        x = x.view(x.size(0), -1)                  # 攤平成向量
        x = self.relu3(self.fc1(x))
        return self.fc2(x)

model = SmallCNN()
x = torch.randn(8, 1, 28, 28)
print(model(x).shape)   # torch.Size([8, 10])

輸入形狀 [8, 1, 28, 28](8 張灰階影像)經過兩個卷積區塊後變成 [8, 32, 7, 7],攤平為 [8, 32×7×7] 後送進全連接分類頭,最後輸出 [8, 10] 的類別分數。整個模型的參數大約只有 100 萬個,比相同輸入大小的 MLP 少一個量級,這正是卷積結構帶來的好處。

感受野與 1×1 卷積

在 CNN 裡,「感受野」(receptive field)指的是輸出特徵圖上某一個像素,能「看到」原始輸入影像的範圍。隨著網路加深、池化層縮小特徵圖,深層的每一個像素實際上覆蓋了原始影像中越來越大的區域,這正是 CNN 能學到「形狀、物體」等高階語意特徵的原因。一個簡單的經驗法則:每經過一次 2×2 的池化,感受野就放大為原本的兩倍。

另一個常被提到的小技巧是 1×1 卷積。它不會混合空間資訊,只會沿著通道維度做線性組合,等同於在每個像素位置上做一個小型的全連接層。1×1 卷積的常見用途包括:調整通道數(不改變空間尺寸)、降低計算量、在瓶頸結構(bottleneck)中壓縮資訊。之後在 ResNet、EfficientNet 等網路裡會看到大量的 1×1 卷積,用來在加深網路的同時控制參數量。

結語

今天把 CNN 的兩個核心操作拆開來看:卷積用小濾鏡在影像上滑動,萃取局部特徵;池化縮小特徵圖、增加感受野。CNN 用局部連接與權重共享,讓參數量大幅下降,並對影像的平移具有一定容忍度。理解卷積與池化的運作原理,是之後看懂 ResNet、EfficientNet 等大型網路的基礎。

明天,我們會在 CIFAR-10 上做一次完整的 CNN 訓練,並用資料增強提升模型的泛化能力。你會看到 CNN 在彩色影像上的表現明顯優於直接把影像攤平的全連接網路,也會理解為什麼資料增強是訓練影像模型時的標準配備。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...