CNN 卷積神經網路:影像辨識的深度學習應用

在進入 CNN 的世界之前,讓我們先釐清一個常見的問題:深度學習 (Deep Learning) 和機器學習 (Machine Learning) 有什麼不同?

1. 深度學習與機器學習的差異

在進入 CNN 的世界之前,讓我們先釐清一個常見的問題:深度學習 (Deep Learning) 和機器學習 (Machine Learning) 有什麼不同?

簡單來說,深度學習是實現機器學習的一種更強大、更複雜的方法

2. 什麼是 CNN (卷積神經網路)?

CNN,全名為 Convolutional Neural Network,是深度學習中最著名且應用最廣的模型之一,尤其在影像處理領域取得了革命性的成功。

類神經網路是什麼?

想像一下人腦中的神經元。它接收來自其他神經元的訊號,經過處理後,再決定是否要將訊號傳遞下去。類神經網路 (Artificial Neural Network) 就是模仿這種結構的數學模型。它由許多互相連接的「神經元」(節點) 組成,這些神經元被組織在不同的「層」(Layer) 中。資料從輸入層開始,經過中間的「隱藏層」,最後到達「輸出層」得到結果。網路透過「訓練」過程,不斷調整神經元之間的連接權重,以學習如何完成特定任務。

神經網路的開關:激勵函數 (Activation Function)

在介紹 CNN 的詳細架構前,我們必須先認識一個關鍵元件:激勵函數

圖解 CNN 架構

一個典型的 CNN 架構就像一個處理影像的流水線,主要包含以下幾個部分:

graph TD A[輸入影像] --> B(卷積層 Conv + 激勵函數 ReLU); B --> C(池化層 Pooling); C --> D(卷積層 Conv + 激勵函數 ReLU); D --> E(池化層 Pooling); E --> F[扁平化 Flatten]; F --> G(全連接層 Fully Connected); G --> H(輸出層 Softmax); H --> I[分類結果];

各層詳細解釋

CNN 的運作原理

CNN 之所以強大,在於它有兩個秘密武器:卷積層池化層

  1. 卷積層 (Convolutional Layer):

    • 作用: 提取影像的局部特徵。
    • 原理: 卷積層使用一個稱為「Kernel (核心/濾鏡)」的小型滑動視窗,在整張圖片上滑動。每滑動到一個位置,Kernel 就會與其覆蓋的圖片區域進行數學運算,並將結果存為新圖片(稱為特徵圖 Feature Map)上的一個像素。
    • 生活實例: 這就像在玩《威利在哪裡?》。你的眼睛就是 Kernel,你腦中記得威利的「紅白條紋衫」特徵。你的目光(Kernel)在整張圖上掃描,每看到一個類似紅白條紋的圖案,大腦的對應區域(特徵圖)就會變得活躍。CNN 會自動學習成千上萬種 Kernel,有的學會找眼睛,有的學會找輪胎,有的學會找貓耳朵。

    Kernel 的具體呈現:一個特徵偵測的視覺化範例

    Kernel 的本質是一個充滿數字(權重)的小矩陣。這些數字是 CNN 在訓練過程中透過「梯度下降」和「反向傳播」學習到的參數。不同的數字組合,讓 Kernel 能偵測不同的特徵。

    1. 假設我們有一張 5x5 的黑白圖片 (數字越大代表越亮): 圖片中間有一條垂直的亮線。

    0  0  10  0  0
    0  0  10  0  0
    0  0  10  0  0
    0  0  10  0  0
    0  0  10  0  0
    

    2. 我們設計一個 3x3 的 Kernel 來偵測「左邊亮、右邊暗」的垂直邊緣:

      1   0  -1
      1   0  -1
      1   0  -1
    

    3. 進行卷積運算(滑動視窗並計算): 我們將 Kernel 疊在圖片左上角,對應位置的數字相乘後再相加:

    • (0*1 + 0*0 + 10*-1) + (0*1 + 0*0 + 10*-1) + (0*1 + 0*0 + 10*-1) = -30

    將 Kernel 往右移動一格,再次計算:

    • (0*1 + 10*0 + 0*-1) + (0*1 + 10*0 + 0*-1) + (0*1 + 10*0 + 0*-1) = 0

    將 Kernel 再往右移動一格,再次計算:

    • (10*1 + 0*0 + 0*-1) + (10*1 + 0*0 + 0*-1) + (10*1 + 0*0 + 0*-1) = 30

    4. 最終產生的特徵圖 (Feature Map): 將所有計算結果組合起來,就得到了一張新的 3x3 特徵圖:

    -30    0   30
    -30    0   30
    -30    0   30
    

    觀察:在這張特徵圖上,數值最大的地方 (30),正好就是 Kernel 在原始圖片中成功偵測到「左亮右暗」垂直邊緣的位置!這就是 Kernel 偵測特徵並產生特徵圖的整個過程。在真實的 CNN 中,模型會自動學習出數百個能偵測各種複雜特徵的 Kernel。

  2. 池化層 (Pooling Layer):

    • 作用: 縮減特徵圖的尺寸,減少計算量,並保留最重要的特徵。
    • 原理: 最常見的是「最大池化」(Max Pooling)。它同樣在特徵圖上滑動一個窗口,但它不做計算,而是直接選出窗口內的最大值(最顯著的特徵)作為代表。
    • 生活實例: 這就像你在寫一本書的摘要。你不會逐字逐句抄寫,而是閱讀每個章節(一個區域),然後挑出最關鍵的一句話(最大值)來代表整個章節的內容。這樣一來,摘要(池化後的特徵圖)會短很多,但仍然保留了全書的精華。這也使得摘要對於原文中一些無關緊要的詞句變動不那麼敏感。
  3. 全連接層 (Fully Connected Layer):

    • 作用: 在經過多次卷積和池化後,影像的特徵已經被提取並濃縮。全連接層負責將這些最終的特徵進行匯總,並根據這些特徵進行分類。
    • 原理: 它的結構和傳統的類神經網路一樣,每個神經元都與前一層的所有神經元相連。
    • 生活實例: 這好比是一位偵探(全連接層)在破案。他已經收集了所有的線索(被提取的特徵),比如「不在場證明」、「指紋」、「目擊者證詞」。最後,他需要在大腦中將所有線索串聯起來,進行邏輯推理,最終指認出兇手(做出分類)。

CNN 在影像辨識的應用

CNN 的應用無所不在,包括:

3. CNN 如何訓練?

訓練 CNN 的過程,就是拿大量的「有標籤」的圖片餵給它,讓它去猜。如果猜錯了,就告訴它正確答案,並微調內部 Kernel 和神經元的權重,讓它下次能猜得更準。這個過程會重複成千上萬次,直到模型達到令人滿意的準確率為止。

範例使用之資料集:CIFAR-10

在我們的範例中,將使用 CIFAR-10 這個經典的影像資料集。

由 Python 實作簡易的 CNN 範例

接下來,我們將提供一段完整的 Python 程式碼。您可以直接將它複製到 Google Colab 的一個儲存格中執行。 這段程式碼將會自動完成以下所有步驟:

  1. 環境設定:檢查是否有可用的 GPU 並設定好運算裝置。
  2. 資料準備:自動下載 CIFAR-10 訓練與測試資料集,並設定好標準化處理流程。
  3. 模型建構:定義一個包含兩個卷積層的簡單 CNN 模型架構。
  4. 模型訓練:設定損失函數與優化器,並執行 10 個週期的訓練迴圈。
  5. 模型評估:在 10,000 張測試圖片上驗證我們訓練好的模型,並計算出最終的分類準確率。
  6. 結果展示:隨機抽取幾張測試圖片,同時秀出「真實標籤」與模型的「預測結果」,讓您能直觀地看到模型的表現。

注意:第一次執行時,因為需要下載資料集,會花費稍長的時間。

# ==================================================================
# 1. 匯入所有必要的函式庫
# ==================================================================
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# ==================================================================
# 2. 設定運算裝置與超參數
# ==================================================================
# 檢查是否有可用的 NVIDIA GPU (cuda),若無則使用 CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'目前使用的裝置: {device}')

# 定義訓練過程中的超參數
num_epochs = 10         # 訓練週期數:所有訓練資料要看過幾遍
batch_size = 100        # 批次大小:模型每次更新權重前要看幾張圖片
learning_rate = 0.001   # 學習率:模型每次學習(更新權重)的步伐大小

# ==================================================================
# 3. 準備 CIFAR-10 資料集
# ==================================================================
# 定義圖片的預處理流程 (Image Transformation)
# 這是非常重要的一步,後續我們自己的圖片也需要經過完全相同的轉換
transform = transforms.Compose(
    [
     transforms.Resize((32, 32)), # 確保所有輸入圖片的尺寸皆為 32x32
     transforms.ToTensor(),       # 將 PIL Image 格式轉換為 PyTorch 的 Tensor 格式
     # 將 Tensor 標準化,(0.5, 0.5, 0.5) 分別是 R,G,B 三個頻道的平均值和標準差
     # 這能讓模型收斂更快、訓練更穩定
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])

# 下載並載入訓練資料集 (train=True)
# PyTorch 會自動從網路上下載資料集到 './data' 路徑下
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)

# 下載並載入測試資料集 (train=False)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)

# 建立資料載入器 (Data Loader)
# DataLoader 能幫助我們自動打包批次、打亂資料順序
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size,
                                          shuffle=True) # shuffle=True 表示在每個 epoch 開始時都重新打亂順序

test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size,
                                         shuffle=False) # 測試集不需要打亂順序

# CIFAR-10 的10個類別名稱,用於後續結果展示
classes = ('plane', 'car', 'bird', 'cat', 'deer',
           'dog', 'frog', 'horse', 'ship', 'truck')

# ==================================================================
# 4. 定義 CNN 模型架構
# ==================================================================
class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()
        # 第一個卷積層: 輸入頻道數為3 (彩色圖片), 輸出頻道數為32, Kernel大小為3x3
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
        # 池化層: 使用 2x2 的視窗進行最大池化
        self.pool = nn.MaxPool2d(2, 2)
        # 第二個卷積層: 輸入頻道數為32 (來自上一層), 輸出頻道數為64
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        # 全連接層1: 圖片經過兩次 2x2 池化後, 尺寸變為 32 -> 16 -> 8
        # 因此輸入特徵數為 64(頻道數) * 8 * 8 = 4096
        self.fc1 = nn.Linear(64 * 8 * 8, 512)
        # 全連接層2 (輸出層): 512個輸入特徵, 10個輸出節點 (對應10個類別)
        self.fc2 = nn.Linear(512, 10)
        # 定義 ReLU 激勵函數
        self.relu = nn.ReLU()

    # 定義模型的前向傳播路徑
    def forward(self, x):
        # 輸入 x 的維度: (batch_size, 3, 32, 32)
        x = self.pool(self.relu(self.conv1(x)))  # 卷積 -> ReLU -> 池化, 維度變為 (batch_size, 32, 16, 16)
        x = self.pool(self.relu(self.conv2(x)))  # 卷積 -> ReLU -> 池化, 維度變為 (batch_size, 64, 8, 8)
        x = x.view(-1, 64 * 8 * 8)              # 將特徵圖扁平化, 維度變為 (batch_size, 4096)
        x = self.relu(self.fc1(x))             # 全連接層1 -> ReLU, 維度變為 (batch_size, 512)
        x = self.fc2(x)                        # 全連接層2 (輸出層), 維度變為 (batch_size, 10)
        return x

# 建立模型實例並將其移動到指定的運算裝置
model = ConvNet().to(device)

# ==================================================================
# 5. 定義損失函數和優化器
# ==================================================================
# 使用交叉熵損失函數,適用於多類別分類問題
criterion = nn.CrossEntropyLoss()
# 使用 Adam 優化器來更新模型的權重,lr 為學習率
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

# ==================================================================
# 6. 訓練模型
# ==================================================================
print("開始進行模型訓練...")
total_step = len(train_loader)
for epoch in range(num_epochs):  # 外層迴圈:跑 num_epochs 個訓練週期
    for i, (images, labels) in enumerate(train_loader):  # 內層迴圈:遍歷所有批次
        # 將圖片和標籤資料移動到指定的運算裝置 (GPU或CPU)
        images = images.to(device)
        labels = labels.to(device)

        # --- 前向傳播 (Forward pass) ---
        outputs = model(images)
        loss = criterion(outputs, labels) # 計算模型的預測與真實標籤之間的損失

        # --- 反向傳播與優化 (Backward and optimize) ---
        optimizer.zero_grad() # 將上一步的梯度清零,避免梯度累積
        loss.backward()       # 根據損失值計算梯度 (反向傳播)
        optimizer.step()      # 使用優化器根據梯度更新所有模型的權重

        # 每 200 個批次,印出一次目前的訓練狀態
        if (i+1) % 200 == 0:
            print (f'週期 [{epoch+1}/{num_epochs}], 步驟 [{i+1}/{total_step}], 損失: {loss.item():.4f}')

print('訓練完成!')

# ==================================================================
# 7. 測試模型
# ==================================================================
print("開始在測試資料集上評估模型...")
# 將模型設定為評估模式,這會關閉 Dropout 等只在訓練時使用的層
model.eval()
# 在 with torch.no_grad() 區塊中,所有計算都不會追蹤梯度,可以節省記憶體與運算資源
with torch.no_grad():
    n_correct = 0
    n_samples = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)

        # 找出分數最高的類別作為預測結果
        # torch.max 會返回 (最大值, 最大值的索引)
        _, predicted = torch.max(outputs.data, 1)

        n_samples += labels.size(0) # 累計樣本總數
        n_correct += (predicted == labels).sum().item() # 累計預測正確的數量

    acc = 100.0 * n_correct / n_samples
    print(f'模型在 10000 張測試圖片上的準確率為: {acc:.2f} %')

# ==================================================================
# 8. 隨機抽樣查看測試結果
# ==================================================================
# 從測試資料中取出一批圖片
dataiter = iter(test_loader)
images, labels = next(dataiter)
# 我們只看前4張圖片
images_for_show = images[:4]
labels_for_show = labels[:4]

# 定義一個顯示圖片的輔助函數
def imshow(img):
    img = img / 2 + 0.5     # 將標準化後的圖片反標準化回來
    npimg = img.numpy()   # 轉換為 numpy 格式
    plt.imshow(np.transpose(npimg, (1, 2, 0))) # 轉換維度順序以符合 imshow 的要求
    plt.show()

# 顯示圖片
imshow(torchvision.utils.make_grid(images_for_show))

# 顯示這4張圖片的真實標籤
print('真實標籤: ', ' '.join(f'{classes[labels_for_show[j]]:5s}' for j in range(4)))

# 讓模型對這4張圖片進行預測
outputs = model(images_for_show.to(device))
_, predicted = torch.max(outputs, 1)

# 印出模型的預測結果
print('模型預測: ', ' '.join(f'{classes[predicted[j]]:5s}' for j in range(4)))

範例程式碼的目標與結果解讀

實際應用:測試你自己上傳的圖片

在您成功執行上方的訓練程式碼後,現在可以執行下方的儲存格來測試自己的圖片了。

使用說明:

  1. 在 Google Colab 中,將下方整段程式碼貼到一個新的儲存格中並執行。
  2. 執行後,儲存格下方會出現一個「選擇檔案」的按鈕。
  3. 點擊按鈕並選擇一張您電腦中的圖片(建議使用 .jpg 或 .png 格式)。
  4. 程式會自動完成上傳、處理圖片,並在最後顯示您上傳的圖片以及模型對它的預測結果。
# ==================================================================
# 1. 匯入必要的函式庫
# ==================================================================
# google.colab.files 是 Colab 專屬的工具,用於在瀏覽器端處理檔案上傳
from google.colab import files
# PIL (Pillow) 是 Python 中最強大的影像處理函式庫
from PIL import Image
# io 用於在記憶體中處理二進位資料流
import io

# ==================================================================
# 2. 設定模型並上傳圖片
# ==================================================================
# 將模型切換到評估模式 (model.eval())
# 這很重要,可以確保 Dropout 等層在預測時被關閉,得到一致的結果
model.eval()

# 呼叫 Colab 的上傳功能,會跳出互動式對話框
print("請選擇一張圖片上傳...")
uploaded = files.upload()

# ==================================================================
# 3. 預處理圖片並進行預測
# ==================================================================
# 檢查是否有成功上傳檔案
if len(uploaded.keys()) > 0:
    # 取得上傳檔案的檔名 (我們只處理上傳的第一張圖片)
    filename = next(iter(uploaded))
    print(f"使用者上傳的檔案名稱: {filename}")

    # 讀取圖片的二進位資料
    img_data = uploaded[filename]
    # 使用 PIL.Image 開啟圖片,並用 .convert("RGB") 確保圖片是3個顏色頻道的彩色圖片
    # 這樣可以避免單色灰階圖或帶有透明度的 PNG 圖片在處理時出錯
    image = Image.open(io.BytesIO(img_data)).convert("RGB")

    # --- 以下是預測流程中最關鍵的一步 ---
    # 將單張圖片轉換為模型看得懂的 "標準格式"
    # 1. transform(image): 進行跟訓練時一模一樣的預處理 (縮放、轉Tensor、標準化)
    # 2. unsqueeze(0): 增加一個批次維度,(3, 32, 32) -> (1, 3, 32, 32)
    # 3. to(device): 將準備好的 Tensor 送到模型所在的運算裝置 (GPU/CPU)
    input_tensor = transform(image).unsqueeze(0).to(device)

    # 使用模型進行預測
    # 在 torch.no_grad() 內執行,可以關閉梯度計算,節省運算資源
    with torch.no_grad():
        output = model(input_tensor)
        # 從10個類別的輸出分數中,找出分數最高的那個
        _, predicted_idx = torch.max(output, 1)
        # 從 classes 列表中,根據索引找出對應的類別名稱
        predicted_class = classes[predicted_idx.item()]

    # ==================================================================
    # 4. 顯示預測結果
    # ==================================================================
    # 使用 matplotlib 顯示上傳的原始圖片
    plt.imshow(image)
    # 將預測結果顯示在圖片標題上
    plt.title(f'模型預測結果: {predicted_class}')
    # 關閉座標軸
    plt.axis('off')
    plt.show()

else:
    print("沒有上傳任何檔案。")

⚠️ 請注意:

這個模型是在 32x32 像素的 CIFAR-10 小圖片上訓練的。因此,它對於解析度過高、或與10個訓練類別無關的複雜真實世界圖片,辨識效果可能不佳。例如,你給它一張人像照,它仍然會硬猜一個最像的類別(比如 catdog),但這個結果是沒有意義的。這個練習的主要目的是體驗一個完整的「從訓練到應用」的流程。

(補充) 視覺化學習到的 Kernel

訓練完成後,模型到底學會了什麼?我們可以透過視覺化第一層卷積層 (conv1) 的 Kernel 來一窺究竟。第一層的 Kernel 直接作用於原始圖片,因此它們通常會學習到一些基礎的視覺特徵,例如顏色、邊緣、紋理等。

下方的程式碼會抽取出我們剛剛訓練好的 modelconv1 層的 32 個 Kernel,並將它們繪製出來。

# ==================================================================
# 1. 取得第一層卷積的 Kernel 權重
# ==================================================================
# 將模型切換到評估模式
model.eval()
# .cpu() 是為了確保權重在 CPU 上,方便後續 Matplotlib 處理
kernels = model.conv1.weight.data.clone().cpu()
# kernels 的維度: (32, 3, 3, 3),代表有 32 個 Kernel,每個都是 3x3 的彩色濾鏡

# ==================================================================
# 2. 視覺化這 32 個 Kernel
# ==================================================================
# 建立一個 4x8 的子圖網格
fig, axes = plt.subplots(4, 8, figsize=(12, 6))
# 將 2D 的 axes 陣列扁平化,方便遍歷
axes = axes.ravel()

# 遍歷 32 個 Kernel
for i in range(kernels.shape[0]):
    ax = axes[i]
    kernel = kernels[i]

    # 為了能正確顯示,需要將權重值正規化到 [0, 1] 的範圍
    kernel = (kernel - kernel.min()) / (kernel.max() - kernel.min())

    # .permute(1, 2, 0) 是為了將維度從 (C, H, W) 轉換為 (H, W, C) 以符合 imshow 的要求
    ax.imshow(kernel.permute(1, 2, 0))
    ax.set_xticks([]) # 隱藏 x 軸刻度
    ax.set_yticks([]) # 隱藏 y 軸刻度

plt.suptitle('模型第一層學習到的 32 個 Kernel')
plt.tight_layout()
plt.show()

如何解讀這些圖案?

您會看到 32 個 3x3 的小色塊。每一個色塊就是一個特徵偵測器。仔細觀察,您可能會發現:

  • 某些 Kernel 呈現由左到右或由上到下的顏色漸層,它們是邊緣偵測器
  • 某些 Kernel 可能是單一顏色的色塊(如偏綠色、偏藍色),它們是顏色偵測器
  • 某些 Kernel 可能呈現對角線或更複雜的圖案。

這些就是 CNN 的「眼睛」所看到的最基礎的視覺元素。更深層的卷積層會將這些基礎特徵組合起來,形成更複雜的特徵(例如,用邊緣和紋理組成「輪胎」,再用輪胎和車窗組成「汽車」)。

4. CNN 的參數調整

訓練深度學習模型就像做菜,需要不斷調整各種「調味料」(超參數),才能得到最好的味道。

調整學習率 (Learning Rate)

背後的原理:梯度下降 (Gradient Descent)

要理解學習率,得先知道模型是如何學習的。這個過程的核心就叫做梯度下降

  1. 目標 - 最小化損失 (Loss): 訓練的唯一目標,是找到一組能讓「損失函數」值最小的模型參數(權重和偏差)。
  2. 損失地貌 (Loss Landscape): 想像一個由所有可能的參數組合構成的、高低起伏的巨大山谷。你在這個地貌上的任何一個位置,其「海拔高度」就代表了當前參數設定下的「損失值」。我們的目標就是走到這個地貌的「最低點」。
  3. 梯度 (Gradient) - 最陡峭的方向: 在你站立的任何位置,梯度就是一個向量,指向「上坡最陡峭」的方向。也就是說,它告訴你往哪個方向調整參數,損失值會上升得最快。
  4. 梯度下降 - 往反方向走: 既然梯度指向了上坡路,那我們只要朝它的完全相反方向走,自然就是下坡最快的路。這就是梯度下降的核心思想。
  5. 學習率 (Learning Rate) - 每一步要走多大: 你已經知道要往哪個方向走了,但每一步要邁多大呢?這就是學習率的角色。它決定了你每次沿著梯度相反方向更新參數時的「步長」。

調整批次大小 (Batch Size)

調整卷積層數量 (網路深度)

調整過擬合 (Overfitting) 問題

5. 進階觀念釐清

監督式學習 vs. 半監督式學習

在我們的範例中,使用的學習方法是監督式學習 (Supervised Learning)

學習類型 vs. 訓練機制:Forward & Backward Propagation

釐清了學習的「類型」,我們還需要知道模型學習的「機制」。學習類型回答的是「用什麼資料學?」,而訓練機制回答的是「如何從資料中學?」。

Forward Propagation (前向傳播)Backward Propagation (反向傳播) 正是神經網路進行學習的核心機制。

學習總結