CNN 影像辨識
以下以 **MNIST 手寫數字辨識**為例,使用 PyTorch 建立一個簡單的 CNN,完成資料準備、模型訓練與測試評估。
## 一、實務案例說明
**目標:**
輸入一張大小為 `28 × 28` 的灰階手寫數字影像,辨識其為 `0~9` 中的哪一個數字。
**資料集:**
- MNIST
- 訓練資料:60,000 張
- 測試資料:10,000 張
- 影像通道:1,灰階
- 分類數量:10 類
---
## 二、安裝套件
```bash
pip install torch torchvision
```
---
## 三、完整實作程式碼
```python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
# --------------------------------------------------
# 1. 基本設定
# --------------------------------------------------
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("使用裝置:", device)
torch.manual_seed(42)
BATCH_SIZE = 64
EPOCHS = 5
LEARNING_RATE = 0.001
# --------------------------------------------------
# 2. 資料前處理
# --------------------------------------------------
transform = transforms.Compose([
transforms.ToTensor(),
# MNIST 常用的平均值與標準差
transforms.Normalize((0.1307,), (0.3081,))
])
# 下載並載入 MNIST 訓練資料
full_train_dataset = datasets.MNIST(
root="./data",
train=True,
download=True,
transform=transform
)
# 官方測試資料
test_dataset = datasets.MNIST(
root="./data",
train=False,
download=True,
transform=transform
)
# 將訓練資料切成訓練集與驗證集
train_size = int(0.9 * len(full_train_dataset))
val_size = len(full_train_dataset) - train_size
train_dataset, val_dataset = random_split(
full_train_dataset,
[train_size, val_size],
generator=torch.Generator().manual_seed(42)
)
# 建立 DataLoader
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True
)
val_loader = DataLoader(
val_dataset,
batch_size=BATCH_SIZE,
shuffle=False
)
test_loader = DataLoader(
test_dataset,
batch_size=BATCH_SIZE,
shuffle=False
)
# --------------------------------------------------
# 3. 建立 CNN 模型
# --------------------------------------------------
class CNNModel(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
# 輸入:1 x 28 x 28
nn.Conv2d(
in_channels=1,
out_channels=32,
kernel_size=3,
padding=1
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
# 形狀:32 x 14 x 14
nn.Conv2d(
in_channels=32,
out_channels=64,
kernel_size=3,
padding=1
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
# 形狀:64 x 7 x 7
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(64 * 7 * 7, 128),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(128, 10)
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
model = CNNModel().to(device)
print(model)
# --------------------------------------------------
# 4. 損失函數與最佳化器
# --------------------------------------------------
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(
model.parameters(),
lr=LEARNING_RATE
)
# --------------------------------------------------
# 5. 定義訓練函式
# --------------------------------------------------
def train_one_epoch(model, data_loader, criterion, optimizer, device):
model.train()
total_loss = 0.0
correct = 0
total = 0
for images, labels in data_loader:
images = images.to(device)
labels = labels.to(device)
# 清除上一批次的梯度
optimizer.zero_grad()
# 前向傳播
outputs = model(images)
# 計算損失
loss = criterion(outputs, labels)
# 反向傳播
loss.backward()
# 更新模型參數
optimizer.step()
total_loss += loss.item() * images.size(0)
predictions = outputs.argmax(dim=1)
correct += (predictions == labels).sum().item()
total += labels.size(0)
average_loss = total_loss / total
accuracy = correct / total
return average_loss, accuracy
# --------------------------------------------------
# 6. 定義評估函式
# --------------------------------------------------
def evaluate(model, data_loader, criterion, device):
model.eval()
total_loss = 0.0
correct = 0
total = 0
# 評估時不需要計算梯度
with torch.no_grad():
for images, labels in data_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
total_loss += loss.item() * images.size(0)
predictions = outputs.argmax(dim=1)
correct += (predictions == labels).sum().item()
total += labels.size(0)
average_loss = total_loss / total
accuracy = correct / total
return average_loss, accuracy
# --------------------------------------------------
# 7. 執行模型訓練
# --------------------------------------------------
best_val_accuracy = 0.0
for epoch in range(EPOCHS):
train_loss, train_accuracy = train_one_epoch(
model,
train_loader,
criterion,
optimizer,
device
)
val_loss, val_accuracy = evaluate(
model,
val_loader,
criterion,
device
)
print(
f"Epoch [{epoch + 1}/{EPOCHS}] | "
f"Train Loss: {train_loss:.4f}, "
f"Train Acc: {train_accuracy:.4f} | "
f"Val Loss: {val_loss:.4f}, "
f"Val Acc: {val_accuracy:.4f}"
)
# 儲存驗證集表現最好的模型
if val_accuracy > best_val_accuracy:
best_val_accuracy = val_accuracy
torch.save(
model.state_dict(),
"best_mnist_cnn.pth"
)
# --------------------------------------------------
# 8. 載入最佳模型並進行測試
# --------------------------------------------------
model.load_state_dict(
torch.load("best_mnist_cnn.pth", map_location=device)
)
test_loss, test_accuracy = evaluate(
model,
test_loader,
criterion,
device
)
print("\n測試結果")
print(f"Test Loss: {test_loss:.4f}")
print(f"Test Accuracy: {test_accuracy:.4f}")
print(f"Test Accuracy: {test_accuracy * 100:.2f}%")
```
---
## 四、CNN 模型架構說明
本範例的模型結構如下:
```text
輸入影像:1 × 28 × 28
Conv2d(1, 32, 3)
ReLU
MaxPool2d(2)
↓
32 × 14 × 14
Conv2d(32, 64, 3)
ReLU
MaxPool2d(2)
↓
64 × 7 × 7
Flatten
Linear(64 × 7 × 7, 128)
ReLU
Dropout(0.5)
Linear(128, 10)
```
### 1. 卷積層 `Conv2d`
卷積層用於擷取影像特徵,例如:
- 邊緣
- 線條
- 轉折
- 局部形狀
第一層從單一灰階通道產生 32 個特徵圖,第二層再將特徵數量增加到 64。
### 2. 池化層 `MaxPool2d`
池化層會降低影像的空間大小:
```text
28 × 28 → 14 × 14 → 7 × 7
```
這可以:
- 減少計算量
- 降低模型參數數量
- 增加模型對小幅位移的容忍度
### 3. ReLU 激活函數
```python
nn.ReLU()
```
ReLU 可以增加模型的非線性表達能力,使 CNN 能學習較複雜的影像特徵。
### 4. Dropout
```python
nn.Dropout(0.5)
```
訓練時隨機忽略部分神經元,降低過度擬合的可能性。
### 5. 最後一層
```python
nn.Linear(128, 10)
```
輸出 10 個數值,分別代表輸入影像屬於數字 `0~9` 的分數。
這裡不需要自行加上 `Softmax`,因為:
```python
nn.CrossEntropyLoss()
```
內部已經包含適當的 LogSoftmax 計算。
---
## 五、訓練流程中的關鍵步驟
### 1. 前向傳播
```python
outputs = model(images)
```
模型接收影像後,計算每個類別的預測分數。
### 2. 計算損失
```python
loss = criterion(outputs, labels)
```
使用交叉熵損失函數,比較模型預測結果與正確答案之間的差異。
### 3. 反向傳播
```python
loss.backward()
```
根據損失函數計算各個參數的梯度。
### 4. 更新參數
```python
optimizer.step()
```
使用 Adam 優化器更新模型參數,使損失逐漸下降。
### 5. 清除梯度
```python
optimizer.zero_grad()
```
PyTorch 預設會累積梯度,因此每一批資料開始前都要先清除上一批次的梯度。
---
## 六、重要設定說明
| 設定 | 本範例值 | 說明 |
|---|---:|---|
| Batch size | 64 | 每次更新模型使用 64 張影像 |
| Epochs | 5 | 將完整訓練資料重複訓練 5 次 |
| Learning rate | 0.001 | 控制模型參數更新幅度 |
| Optimizer | Adam | 常用且容易訓練的最佳化器 |
| Loss function | CrossEntropyLoss | 適合多分類問題 |
| Validation split | 10% | 用於選擇最佳模型 |
| Dropout | 0.5 | 降低過度擬合 |
在一般電腦或 GPU 上,這個模型通常可以達到約 **98%~99% 的 MNIST 測試準確率**,實際結果會受到硬體、隨機種子與訓練次數影響。
---
## 七、模型評估的意義
最後使用沒有參與訓練的測試資料:
```python
test_loss, test_accuracy = evaluate(
model,
test_loader,
criterion,
device
)
```
其中:
- `Test Loss`:模型在測試資料上的平均損失
- `Test Accuracy`:預測正確的比例
例如:
```text
Test Accuracy: 98.40%
```
表示 10,000 張測試影像中,大約有 9,840 張被正確分類。
實務上除了準確率,也可以進一步計算:
- Precision
- Recall
- F1-score
- Confusion Matrix
以了解模型是否特別容易混淆某些數字,例如 `4` 和 `9`。
相關學習地圖、教學課程
Python 人工智慧