在進入 CNN 的世界之前,讓我們先釐清一個常見的問題:深度學習 (Deep Learning) 和機器學習 (Machine Learning) 有什麼不同?
在進入 CNN 的世界之前,讓我們先釐清一個常見的問題:深度學習 (Deep Learning) 和機器學習 (Machine Learning) 有什麼不同?
簡單來說,深度學習是實現機器學習的一種更強大、更複雜的方法。
CNN,全名為 Convolutional Neural Network,是深度學習中最著名且應用最廣的模型之一,尤其在影像處理領域取得了革命性的成功。
想像一下人腦中的神經元。它接收來自其他神經元的訊號,經過處理後,再決定是否要將訊號傳遞下去。類神經網路 (Artificial Neural Network) 就是模仿這種結構的數學模型。它由許多互相連接的「神經元」(節點) 組成,這些神經元被組織在不同的「層」(Layer) 中。資料從輸入層開始,經過中間的「隱藏層」,最後到達「輸出層」得到結果。網路透過「訓練」過程,不斷調整神經元之間的連接權重,以學習如何完成特定任務。
在介紹 CNN 的詳細架構前,我們必須先認識一個關鍵元件:激勵函數。
y = ax + b)。這樣的網路只能學習簡單的線性關係,無法解決現實世界中複雜的問題(例如,區分貓和狗的複雜視覺邊界)。output = max(0, input)。意思是,當神經元收到的訊號是正數時,就讓它直接通過;當訊號是負數或零時,就將其阻斷(輸出為 0)。一個典型的 CNN 架構就像一個處理影像的流水線,主要包含以下幾個部分:
CNN 之所以強大,在於它有兩個秘密武器:卷積層和池化層。
卷積層 (Convolutional Layer):
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) = 304. 最終產生的特徵圖 (Feature Map): 將所有計算結果組合起來,就得到了一張新的 3x3 特徵圖:
-30 0 30
-30 0 30
-30 0 30
觀察:在這張特徵圖上,數值最大的地方 (30),正好就是 Kernel 在原始圖片中成功偵測到「左亮右暗」垂直邊緣的位置!這就是 Kernel 偵測特徵並產生特徵圖的整個過程。在真實的 CNN 中,模型會自動學習出數百個能偵測各種複雜特徵的 Kernel。
池化層 (Pooling Layer):
全連接層 (Fully Connected Layer):
CNN 的應用無所不在,包括:
訓練 CNN 的過程,就是拿大量的「有標籤」的圖片餵給它,讓它去猜。如果猜錯了,就告訴它正確答案,並微調內部 Kernel 和神經元的權重,讓它下次能猜得更準。這個過程會重複成千上萬次,直到模型達到令人滿意的準確率為止。
在我們的範例中,將使用 CIFAR-10 這個經典的影像資料集。
接下來,我們將提供一段完整的 Python 程式碼。您可以直接將它複製到 Google Colab 的一個儲存格中執行。 這段程式碼將會自動完成以下所有步驟:
注意:第一次執行時,因為需要下載資料集,會花費稍長的時間。
# ==================================================================
# 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)))
🎯 目標: 我們的目標是建立並訓練一個基礎的 CNN 模型,讓它學會辨識 CIFAR-10 資料集中的10種不同物體。我們希望在訓練結束後,模型能在從未見過的「測試圖片」上達到一個合理的準確率(通常對於這個簡易模型,60-70% 是一個不錯的起點)。
📊 結果解讀:
目前使用的裝置: cuda: 這表示程式碼成功偵測到並正在使用 GPU 進行運算,這會比只用 CPU 快很多。週期 [1/10], 步驟 [200/500], 損失: 1.5432: 這行告訴我們:週期 [1/10]: 目前正在進行第 1 輪的訓練(總共要跑 10 輪)。步驟 [200/500]: 在第 1 輪中,已經處理了 200 個批次(總共 500 批)。損失: 1.5432: 目前這個批次的「損失值」是 1.5432。Loss 代表模型預測結果與真實答案之間的差距,這個數字越小越好。你會觀察到隨著訓練進行,Loss 會逐漸下降。模型在 10000 張測試圖片上的準確率為: 68.25 %: 這是模型的最終成績單。它表示在 10,000 張模型從未見過的測試圖片中,它答對了 68.25%。這證明模型學到了一些通用的辨識能力,而不僅僅是「背誦」訓練過的圖片。真實標籤: cat ship ship plane: 這是隨機抽取的 4 張測試圖片的真實標籤。模型預測: cat car ship plane: 這是我們的模型對這 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個訓練類別無關的複雜真實世界圖片,辨識效果可能不佳。例如,你給它一張人像照,它仍然會硬猜一個最像的類別(比如
cat或dog),但這個結果是沒有意義的。這個練習的主要目的是體驗一個完整的「從訓練到應用」的流程。
訓練完成後,模型到底學會了什麼?我們可以透過視覺化第一層卷積層 (conv1) 的 Kernel 來一窺究竟。第一層的 Kernel 直接作用於原始圖片,因此它們通常會學習到一些基礎的視覺特徵,例如顏色、邊緣、紋理等。
下方的程式碼會抽取出我們剛剛訓練好的 model 中 conv1 層的 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 的「眼睛」所看到的最基礎的視覺元素。更深層的卷積層會將這些基礎特徵組合起來,形成更複雜的特徵(例如,用邊緣和紋理組成「輪胎」,再用輪胎和車窗組成「汽車」)。
訓練深度學習模型就像做菜,需要不斷調整各種「調味料」(超參數),才能得到最好的味道。
要理解學習率,得先知道模型是如何學習的。這個過程的核心就叫做梯度下降。
0.001 開始嘗試,再根據訓練情況放大或縮小10倍。找到一個既能穩定下降、速度又不至於太慢的學習率是訓練的關鍵之一。在我們的範例中,使用的學習方法是監督式學習 (Supervised Learning)。
監督式學習: 提供給模型的所有訓練資料都是「有標籤的」(Tagged/Labeled)。也就是說,每一張圖片,我們都明確地告訴模型它對應的正確答案是什麼(例如,這張圖是 cat)。模型的目標就是學習如何從輸入(圖片)對應到正確的輸出(標籤)。
非監督式學習 (Unsupervised Learning): 提供給模型的資料完全沒有標籤。模型的目標不是「分類」,而是自己從資料中找出隱藏的結構或模式,例如將相似的資料點群聚在一起(Clustering)。
半監督式學習 (Semi-Supervised Learning): 這是介於兩者之間的方法,它會混合使用「有標籤」和「無標籤」的資料。通常是使用少量有標籤的資料,搭配大量無標籤的資料。這種方法適用於取得標籤成本很高的情境。
釐清了學習的「類型」,我們還需要知道模型學習的「機制」。學習類型回答的是「用什麼資料學?」,而訓練機制回答的是「如何從資料中學?」。
Forward Propagation (前向傳播) 和 Backward Propagation (反向傳播) 正是神經網路進行學習的核心機制。
Forward Propagation (前向傳播)
Backward Propagation (反向傳播)