本文件是 CNN 完整實戰指南的第二部分,專注於 PyTorch 框架的深度學習實作。
前置閱讀:建議先閱讀
CNN_intro_b07.md了解基礎知識與 Keras 實作。
在第二部分中,我們使用 Keras 實作了經典的 LeNet-5。現在讓我們用 PyTorch 從頭實作一個現代化的 CNN 模型,深入理解深度學習的訓練流程。
雖然 Keras 簡潔易用,但 PyTorch 能讓你:
在開始實作前,先理解 PyTorch 的幾個核心概念:
import torch
# 創建張量(類似 NumPy array,但可在 GPU 上運算)
x = torch.tensor([1, 2, 3])
y = torch.tensor([[1, 2], [3, 4]])
# 在 GPU 上運算
if torch.cuda.is_available():
x = x.cuda() # 移到 GPU
x = x.cpu() # 移回 CPU
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
# 定義層
self.layer1 = nn.Linear(10, 5)
def forward(self, x):
# 定義前向傳播
x = self.layer1(x)
return x
from torch.utils.data import DataLoader, TensorDataset
# 創建 Dataset
dataset = TensorDataset(X_tensor, y_tensor)
# 創建 DataLoader(自動分批、打亂)
loader = DataLoader(dataset, batch_size=32, shuffle=True)
# 迭代批次資料
for batch_x, batch_y in loader:
# 訓練程式碼
pass
# PyTorch 訓練的標準流程
for epoch in range(num_epochs):
for batch_x, batch_y in train_loader:
# 1. 清空梯度
optimizer.zero_grad()
# 2. 前向傳播
output = model(batch_x)
# 3. 計算損失
loss = criterion(output, batch_y)
# 4. 反向傳播
loss.backward()
# 5. 更新參數
optimizer.step()
我們將實作一個比 LeNet-5 更現代化的 CNN 架構:
架構特色:
| 特性 | LeNet-5 | SimpleCNN (本實作) |
|---|---|---|
| 卷積層數 | 2 | 4(兩層一組) |
| 池化方式 | Average Pooling | Max Pooling |
| 激活函數 | tanh | ReLU |
| 正規化 | 無 | Dropout (0.25, 0.5) |
| 卷積核大小 | 5×5 | 3×3(更現代) |
| 參數量 | ~60K | ~140K |
| 預期準確率 | 98.5% | 99.3%+ |
以下程式碼可直接在 Google Colab 執行:
# ============================================
# MNIST + SimpleCNN 完整實作(PyTorch)
# 執行環境:Google Colab
# 預期訓練時間:3-5 分鐘(GPU)
# 預期準確率:>99.3%
# ============================================
# ========== 儲存格 1: 導入套件 ==========
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix, classification_report
# 檢查 PyTorch 版本和 GPU
print(f"PyTorch 版本: {torch.__version__}")
print(f"CUDA 可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU 型號: {torch.cuda.get_device_name(0)}")
# 設定裝置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"\n使用裝置: {device}")
# 設定隨機種子
torch.manual_seed(42)
if torch.cuda.is_available():
torch.cuda.manual_seed(42)
np.random.seed(42)
# ========== 儲存格 2: 載入資料 ==========
# 使用 torchvision 載入 MNIST
from torchvision import datasets, transforms
# 定義資料轉換(正規化)
transform = transforms.Compose([
transforms.ToTensor(), # 轉換為 Tensor,自動正規化到 [0, 1]
# MNIST 標準化參數(來自訓練集統計)
# 均值 = 0.1307, 標準差 = 0.3081
# 這些值是 MNIST 訓練集的全體像素均值與標準差,為公開常用數值
# 計算方式:train_images.mean() / 255, train_images.std() / 255
transforms.Normalize((0.1307,), (0.3081,))
])
# 下載並載入訓練集
train_dataset = datasets.MNIST(
root='./data',
train=True,
download=True,
transform=transform
)
# 下載並載入測試集
test_dataset = datasets.MNIST(
root='./data',
train=False,
download=True,
transform=transform
)
print(f"訓練集大小: {len(train_dataset)}") # 60000
print(f"測試集大小: {len(test_dataset)}") # 10000
# 視覺化前 25 張影像
fig, axes = plt.subplots(5, 5, figsize=(10, 10))
for i, ax in enumerate(axes.flat):
# 注意:transform 已套用,需要反正規化才能正確顯示
img, label = train_dataset[i]
img = img.squeeze().numpy()
ax.imshow(img, cmap='gray')
ax.set_title(f'Label: {label}')
ax.axis('off')
plt.suptitle('MNIST 訓練集範例', fontsize=16)
plt.tight_layout()
plt.show()
# ========== 儲存格 3: 創建 DataLoader ==========
# 批次大小
BATCH_SIZE = 128
# 創建 DataLoader
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True, # 訓練集打亂
num_workers=2, # 多執行緒載入(加速)
pin_memory=True # 如果使用 GPU,這會加速資料傳輸
)
test_loader = DataLoader(
test_dataset,
batch_size=BATCH_SIZE,
shuffle=False, # 測試集不打亂
num_workers=2,
pin_memory=True
)
print(f"訓練批次數: {len(train_loader)}") # 60000 / 128 ≈ 469
print(f"測試批次數: {len(test_loader)}") # 10000 / 128 ≈ 79
# 檢查一個批次的資料
sample_batch_x, sample_batch_y = next(iter(train_loader))
print(f"\n批次資料形狀: {sample_batch_x.shape}") # (128, 1, 28, 28)
print(f"批次標籤形狀: {sample_batch_y.shape}") # (128,)
# ========== 儲存格 4: 定義模型 ==========
class SimpleCNN(nn.Module):
"""
SimpleCNN 架構
- 4 個卷積層(兩層一組)
- 2 個最大池化層
- 3 個 Dropout 層(防過擬合)
- 2 個全連接層
"""
def __init__(self):
super(SimpleCNN, self).__init__()
# ===== 第一組卷積塊 =====
# Conv1: 1 → 32 通道,3×3 卷積核,padding=1(保持尺寸)
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32,
kernel_size=3, padding=1)
# Conv2: 32 → 32 通道
self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
# MaxPool: 28×28 → 14×14
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# Dropout
self.dropout1 = nn.Dropout(0.25)
# ===== 第二組卷積塊 =====
# Conv3: 32 → 64 通道
self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
# Conv4: 64 → 64 通道
self.conv4 = nn.Conv2d(64, 64, 3, padding=1)
# MaxPool: 14×14 → 7×7
self.pool2 = nn.MaxPool2d(2, 2)
# Dropout
self.dropout2 = nn.Dropout(0.25)
# ===== 全連接層 =====
# Flatten 後: 7×7×64 = 3136
self.fc1 = nn.Linear(7 * 7 * 64, 128)
self.dropout3 = nn.Dropout(0.5)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
"""
前向傳播
輸入: (batch_size, 1, 28, 28)
輸出: (batch_size, 10)
"""
# 第一組卷積塊
x = F.relu(self.conv1(x)) # (N, 32, 28, 28)
x = F.relu(self.conv2(x)) # (N, 32, 28, 28)
x = self.pool1(x) # (N, 32, 14, 14)
x = self.dropout1(x)
# 第二組卷積塊
x = F.relu(self.conv3(x)) # (N, 64, 14, 14)
x = F.relu(self.conv4(x)) # (N, 64, 14, 14)
x = self.pool2(x) # (N, 64, 7, 7)
x = self.dropout2(x)
# 展平
x = x.view(x.size(0), -1) # (N, 3136)
# 全連接層
x = F.relu(self.fc1(x)) # (N, 128)
x = self.dropout3(x)
x = self.fc2(x) # (N, 10)
return x
# 創建模型並移至 GPU
model = SimpleCNN().to(device)
# 顯示模型架構
print(model)
print("\n" + "=" * 60)
# 計算參數量
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = count_parameters(model)
print(f"總參數量: {total_params:,}")
# 測試前向傳播
sample_input = torch.randn(1, 1, 28, 28).to(device)
sample_output = model(sample_input)
print(f"輸入形狀: {sample_input.shape}")
print(f"輸出形狀: {sample_output.shape}")
# ========== 儲存格 5: 定義損失函數和優化器 ==========
# 損失函數:交叉熵(內建 Softmax)
criterion = nn.CrossEntropyLoss()
# 優化器:Adam
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 學習率調度器:每 5 個 epoch 學習率乘以 0.5
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
print("損失函數: CrossEntropyLoss")
print("優化器: Adam (lr=0.001)")
print("學習率調度: StepLR (step_size=5, gamma=0.5)")
# ========== 儲存格 6: 訓練函數 ==========
def train_one_epoch(model, train_loader, criterion, optimizer, device):
"""
訓練一個 epoch
返回: 平均損失, 準確率
"""
model.train() # 設為訓練模式(啟用 Dropout)
running_loss = 0.0
correct = 0
total = 0
for batch_idx, (data, target) in enumerate(train_loader):
# 移至 GPU
data, target = data.to(device), target.to(device)
# 1. 清空梯度
optimizer.zero_grad()
# 2. 前向傳播
output = model(data)
# 3. 計算損失
loss = criterion(output, target)
# 4. 反向傳播
loss.backward()
# 5. 更新參數
optimizer.step()
# 統計
running_loss += loss.item()
_, predicted = torch.max(output.data, 1)
total += target.size(0)
correct += (predicted == target).sum().item()
epoch_loss = running_loss / len(train_loader)
epoch_acc = 100.0 * correct / total
return epoch_loss, epoch_acc
# ========== 儲存格 7: 評估函數 ==========
def evaluate(model, test_loader, criterion, device):
"""
評估模型
返回: 平均損失, 準確率
"""
model.eval() # 設為評估模式(關閉 Dropout)
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad(): # 不計算梯度(節省記憶體、加速)
for data, target in test_loader:
data, target = data.to(device), target.to(device)
# 前向傳播
output = model(data)
# 計算損失
loss = criterion(output, target)
# 統計
running_loss += loss.item()
_, predicted = torch.max(output.data, 1)
total += target.size(0)
correct += (predicted == target).sum().item()
epoch_loss = running_loss / len(test_loader)
epoch_acc = 100.0 * correct / total
return epoch_loss, epoch_acc
# ========== 儲存格 8: 訓練迴圈 ==========
NUM_EPOCHS = 15
# 記錄訓練歷史
history = {
'train_loss': [],
'train_acc': [],
'test_loss': [],
'test_acc': []
}
print("開始訓練...")
print("=" * 70)
best_acc = 0.0
# Early Stopping 設定(類似 Keras 的 EarlyStopping callback)
patience = 10 # 容忍 10 個 epochs 沒進步
epochs_no_improve = 0 # 計數器
for epoch in range(1, NUM_EPOCHS + 1):
# 訓練
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, device
)
# 評估
test_loss, test_acc = evaluate(
model, test_loader, criterion, device
)
# 更新學習率
scheduler.step()
current_lr = optimizer.param_groups[0]['lr']
# 記錄歷史
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
history['test_loss'].append(test_loss)
history['test_acc'].append(test_acc)
# 儲存最佳模型 & Early Stopping 邏輯
if test_acc > best_acc:
best_acc = test_acc
torch.save(model.state_dict(), 'best_simplecnn.pth')
epochs_no_improve = 0 # 重置計數器
best_marker = '⭐'
print(f"Epoch [{epoch:2d}/{NUM_EPOCHS}] "
f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}% | "
f"Test Loss: {test_loss:.4f} | Test Acc: {test_acc:.2f}% | "
f"LR: {current_lr:.6f} {best_marker}")
else:
epochs_no_improve += 1
best_marker = ''
print(f"Epoch [{epoch:2d}/{NUM_EPOCHS}] "
f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}% | "
f"Test Loss: {test_loss:.4f} | Test Acc: {test_acc:.2f}% | "
f"LR: {current_lr:.6f} (沒進步: {epochs_no_improve}/{patience})")
# Early Stopping 檢查
if epochs_no_improve >= patience:
print(f"\n⚠ Early Stopping 觸發!連續 {patience} 個 epochs 測試準確率未提升")
print(f"最佳測試準確率: {best_acc:.2f}%")
break
print("=" * 70)
print(f"\n✓ 訓練完成!最佳測試準確率: {best_acc:.2f}%")
# ========== 儲存格 9: 視覺化訓練歷史 ==========
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
# 準確率曲線
axes[0].plot(history['train_acc'], 'b-', label='訓練準確率', linewidth=2)
axes[0].plot(history['test_acc'], 'r-', label='測試準確率', linewidth=2)
axes[0].set_title('SimpleCNN 訓練準確率', fontsize=14, fontweight='bold')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('準確率 (%)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 損失曲線
axes[1].plot(history['train_loss'], 'b-', label='訓練損失', linewidth=2)
axes[1].plot(history['test_loss'], 'r-', label='測試損失', linewidth=2)
axes[1].set_title('SimpleCNN 訓練損失', fontsize=14, fontweight='bold')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('損失值')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ========== 儲存格 10: 詳細評估 ==========
# 載入最佳模型
model.load_state_dict(torch.load('best_simplecnn.pth'))
model.eval()
# 收集所有預測結果
all_preds = []
all_targets = []
with torch.no_grad():
for data, target in test_loader:
data = data.to(device)
output = model(data)
_, predicted = torch.max(output.data, 1)
all_preds.extend(predicted.cpu().numpy())
all_targets.extend(target.numpy())
all_preds = np.array(all_preds)
all_targets = np.array(all_targets)
# 混淆矩陣
cm = confusion_matrix(all_targets, all_preds)
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=range(10), yticklabels=range(10))
plt.title('SimpleCNN 混淆矩陣', fontsize=16, fontweight='bold')
plt.xlabel('預測標籤')
plt.ylabel('真實標籤')
plt.show()
# 分類報告
print("\n分類報告:")
print(classification_report(all_targets, all_preds,
target_names=[f'數字 {i}' for i in range(10)]))
# ========== 儲存格 11: 預測範例視覺化 ==========
# 隨機選擇 20 張測試影像
indices = np.random.choice(len(test_dataset), 20, replace=False)
fig, axes = plt.subplots(4, 5, figsize=(15, 12))
for i, ax in enumerate(axes.flat):
idx = indices[i]
# 取得影像和標籤
img, true_label = test_dataset[idx]
img_display = img.squeeze().numpy()
# 預測
img_batch = img.unsqueeze(0).to(device) # 增加 batch 維度
with torch.no_grad():
output = model(img_batch)
probabilities = F.softmax(output, dim=1)
confidence, predicted = torch.max(probabilities, 1)
pred_label = predicted.item()
confidence = confidence.item() * 100
# 顯示
ax.imshow(img_display, cmap='gray')
color = 'green' if pred_label == true_label else 'red'
ax.set_title(
f'真實: {true_label} | 預測: {pred_label}\n信心度: {confidence:.1f}%',
color=color, fontsize=9
)
ax.axis('off')
plt.suptitle('SimpleCNN 預測結果(綠色=正確,紅色=錯誤)',
fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()
# ========== 儲存格 12: 每個類別的準確率 ==========
print("\n每個數字的準確率:")
print("=" * 40)
for digit in range(10):
mask = all_targets == digit
digit_acc = (all_preds[mask] == all_targets[mask]).mean() * 100
digit_count = mask.sum()
print(f"數字 {digit}: {digit_acc:.2f}% ({digit_count} 張)")
print("\n✓ 所有程式碼執行完成!")
nn.Conv2d(padding=1)?self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
Padding 的作用:
| 設定 | 輸入 | 輸出 | 說明 |
|---|---|---|---|
padding=0(無) |
28×28 | 26×26 | 邊緣資訊流失 |
padding=1(推薦) |
28×28 | 28×28 | 保持尺寸 |
計算公式:
輸出尺寸 = (輸入尺寸 - 卷積核大小 + 2×padding) / stride + 1
範例:
(28 - 3 + 2×1) / 1 + 1 = 28
為什麼保持尺寸?
- 避免過早縮小特徵圖
- 邊緣資訊也能充分利用
- 用池化層控制降維
model.train() vs model.eval()model.train() # 訓練模式
model.eval() # 評估模式
差異:
| 模式 | Dropout | BatchNorm | 用途 |
|---|---|---|---|
train() |
啟用(隨機丟棄) | 更新統計量 | 訓練 |
eval() |
關閉(保留所有) | 使用固定統計量 | 評估/預測 |
重要性:
# ❌ 錯誤:評估時忘記設為 eval()
model.train() # Dropout 會隨機丟棄神經元
test_acc = evaluate(model, test_loader) # 準確率會偏低!
# ✅ 正確
model.eval()
test_acc = evaluate(model, test_loader)
torch.no_grad() 的作用with torch.no_grad():
output = model(data)
作用:
- 不計算梯度(不需要反向傳播)
- 節省記憶體:梯度佔用大量空間
- 加速運算:減少計算量
效能對比:
# 測試集評估(10,000 張影像)
# 有 gradient:約 800MB GPU 記憶體
# 無 gradient:約 300MB GPU 記憶體(節省 60%)
view() vs reshape() vs flatten()# 展平操作的三種方法
x = x.view(x.size(0), -1) # PyTorch 傳統方法
x = x.reshape(x.size(0), -1) # NumPy 風格
x = torch.flatten(x, 1) # 從第 1 維開始展平
差異:
| 方法 | 特點 | 建議 |
|---|---|---|
view() |
要求記憶體連續 | 最快,但可能失敗 |
reshape() |
自動處理不連續 | 推薦(穩定) |
flatten() |
語意清晰 | CNN 中最推薦 |
實際使用:
# 推薦寫法
x = torch.flatten(x, start_dim=1) # 保留 batch 維度
# 等價於
x = x.view(x.size(0), -1)
criterion = nn.CrossEntropyLoss()
loss = criterion(output, target)
重要觀念:
PyTorch 的 CrossEntropyLoss = LogSoftmax + NLLLoss
# 內部實際做的事:
# 1. 對 output 套用 LogSoftmax
# 2. 計算負對數似然損失
# 因此,模型輸出不需要 Softmax!
# ✅ 正確
def forward(self, x):
x = self.fc2(x)
return x # 直接返回 logits
# ❌ 錯誤(會導致數值不穩定)
def forward(self, x):
x = self.fc2(x)
return F.softmax(x, dim=1) # 不需要!
但在預測時需要 Softmax:
# 訓練時
output = model(x)
loss = criterion(output, target) # 內建 Softmax
# 預測時(取得機率)
output = model(x)
probabilities = F.softmax(output, dim=1) # 手動套用
zero_grad() 為什麼必須?optimizer.zero_grad() # 必須!
loss.backward()
optimizer.step()
原因:PyTorch 的梯度是累積的
# 範例:如果不清空梯度
for epoch in range(3):
loss.backward()
# 忘記 zero_grad()
print(model.conv1.weight.grad[0,0,0,0])
# 輸出(梯度不斷累積):
# Epoch 1: 0.015
# Epoch 2: 0.030 ← 累積了!
# Epoch 3: 0.045 ← 繼續累積!
# 正確做法
for epoch in range(3):
optimizer.zero_grad() # 每次清空
loss.backward()
print(model.conv1.weight.grad[0,0,0,0])
# 輸出:
# Epoch 1: 0.015
# Epoch 2: 0.015 ← 正確
# Epoch 3: 0.015 ← 正確
讓我們直接對比兩個框架的關鍵差異:
Keras (Sequential API):
model = Sequential([
Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
MaxPooling2D((2, 2)),
Flatten(),
Dense(10, activation='softmax')
])
PyTorch (nn.Module):
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3)
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(13*13*32, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
model = Model()
差異:
- Keras: 宣告式,層層堆疊
- PyTorch: 命令式,明確定義流程
Keras:
# 一次設定所有參數
model.compile(optimizer='adam', loss='categorical_crossentropy')
# 一行訓練
history = model.fit(X_train, y_train, epochs=10)
PyTorch:
# 分別定義
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())
# 手寫訓練迴圈
for epoch in range(10):
for batch_x, batch_y in train_loader:
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
差異:
- Keras: 高度封裝,簡潔
- PyTorch: 細粒度控制,靈活
| 框架 | 影像格式 | 批次格式 | 說明 |
|---|---|---|---|
| Keras | (H, W, C) | (N, H, W, C) | Channels Last |
| PyTorch | (C, H, W) | (N, C, H, W) | Channels First |
實例:
# Keras
X_train.shape # (60000, 28, 28, 1)
# ↑高 ↑寬 ↑通道
# PyTorch
X_train.shape # (60000, 1, 28, 28)
# ↑通道 ↑高 ↑寬
| 特性 | Keras | PyTorch | 建議 |
|---|---|---|---|
| 模型定義 | Sequential / Functional API | nn.Module 類別 | Keras 快速原型,PyTorch 客製化 |
| 訓練 | model.fit() |
手寫訓練迴圈 | Keras 簡單,PyTorch 靈活 |
| 資料載入 | NumPy array | DataLoader + Dataset | PyTorch 更強大 |
| GPU 使用 | 自動 | 手動 .to(device) |
Keras 方便,PyTorch 明確 |
| 動態圖 | Eager Execution (預設關閉) | 預設開啟 | PyTorch 更適合 RNN 等動態模型 |
| 除錯 | 較困難(靜態圖) | 容易(Python 原生) | PyTorch 更易除錯 |
| 部署 | TF Serving, TFLite | TorchServe, ONNX | Keras 生態更完善 |
| 社群 | 業界 | 學術界 | 根據需求選擇 |
訓練過程(GPU,約 3-5 分鐘):
開始訓練...
======================================================================
Epoch [ 1/15] Train Loss: 0.1845 | Train Acc: 94.32% | Test Loss: 0.0512 | Test Acc: 98.32% | LR: 0.001000 ⭐
Epoch [ 2/15] Train Loss: 0.0628 | Train Acc: 98.05% | Test Loss: 0.0369 | Test Acc: 98.84% | LR: 0.001000 ⭐
Epoch [ 3/15] Train Loss: 0.0482 | Train Acc: 98.52% | Test Loss: 0.0297 | Test Acc: 99.08% | LR: 0.001000 ⭐
Epoch [ 4/15] Train Loss: 0.0398 | Train Acc: 98.76% | Test Loss: 0.0275 | Test Acc: 99.17% | LR: 0.001000 ⭐
Epoch [ 5/15] Train Loss: 0.0346 | Train Acc: 98.92% | Test Loss: 0.0254 | Test Acc: 99.23% | LR: 0.001000 ⭐
Epoch [ 6/15] Train Loss: 0.0258 | Train Acc: 99.18% | Test Loss: 0.0239 | Test Acc: 99.28% | LR: 0.000500 ⭐
Epoch [ 7/15] Train Loss: 0.0221 | Train Acc: 99.30% | Test Loss: 0.0228 | Test Acc: 99.32% | LR: 0.000500 ⭐
Epoch [ 8/15] Train Loss: 0.0203 | Train Acc: 99.36% | Test Loss: 0.0223 | Test Acc: 99.35% | LR: 0.000500 ⭐
...
Epoch [15/15] Train Loss: 0.0121 | Train Acc: 99.62% | Test Loss: 0.0210 | Test Acc: 99.40% | LR: 0.000031
✓ 訓練完成!最佳測試準確率: 99.40%
分類報告:
precision recall f1-score support
數字 0 0.99 1.00 1.00 980
數字 1 0.99 1.00 0.99 1135
數字 2 0.99 0.99 0.99 1032
數字 3 0.99 0.99 0.99 1010
數字 4 0.99 0.99 0.99 982
數字 5 0.99 0.99 0.99 892
數字 6 1.00 0.99 0.99 958
數字 7 0.99 0.99 0.99 1028
數字 8 0.99 0.99 0.99 974
數字 9 0.99 0.99 0.99 1009
accuracy 0.99 10000
每個數字的準確率:
數字 0: 99.69% (980 張)
數字 1: 99.74% (1135 張)
數字 2: 99.32% (1032 張)
數字 3: 99.21% (1010 張)
數字 4: 99.29% (982 張)
數字 5: 99.22% (892 張)
數字 6: 99.48% (958 張)
數字 7: 99.32% (1028 張)
數字 8: 99.18% (974 張)
數字 9: 99.01% (1009 張)
# ❌ 錯誤:資料和模型在不同裝置
model = model.to('cuda')
output = model(data) # data 還在 CPU
# ✅ 正確:統一移至 GPU
data = data.to(device)
model = model.to(device)
output = model(data)
解決方法:
1. 增加 Dropout:從 0.25 提高到 0.3-0.4
2. 資料增強:旋轉、位移、縮放
3. 早停:監控 val_loss,5 個 epoch 沒改善就停止
4. L2 正規化:
python
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
# 減少 batch_size
BATCH_SIZE = 64 # 從 128 減到 64
# 或清空快取
torch.cuda.empty_cache()
# 檢查記憶體使用
print(f"已分配: {torch.cuda.memory_allocated() / 1024**2:.2f} MB")
print(f"快取: {torch.cuda.memory_reserved() / 1024**2:.2f} MB")
檢查清單:
- ✅ 是否忘記 optimizer.zero_grad()
- ✅ 學習率是否過小(試試 0.001)
- ✅ 資料是否正規化
- ✅ 標籤格式是否正確(CrossEntropyLoss 需要類別索引,不是 One-Hot)
# ✅ 正確:標籤為類別索引
target = torch.tensor([3, 1, 0, 5]) # 形狀: (4,)
loss = criterion(output, target)
# ❌ 錯誤:標籤為 One-Hot
target = torch.tensor([[0,0,0,1,...], [0,1,0,0,...], ...]) # 形狀: (4, 10)
loss = criterion(output, target) # 會報錯或結果錯誤
| 指標 | LeNet-5 (Keras) | SimpleCNN (PyTorch) | 差異 |
|---|---|---|---|
| 準確率 | 98.76% | 99.40% | +0.64% |
| 訓練時間 | 2-3 分鐘 | 3-5 分鐘 | 稍慢(手寫迴圈) |
| 參數量 | ~60K | ~140K | 2.3 倍 |
| 程式碼行數 | ~150 行 | ~300 行 | PyTorch 更冗長 |
| 學習難度 | ★☆☆☆☆ | ★★★☆☆ | PyTorch 需理解更多細節 |
| 靈活度 | ★★☆☆☆ | ★★★★★ | PyTorch 易於客製化 |
使用 Keras:
- ✅ 快速驗證想法
- ✅ 標準的影像分類任務
- ✅ 團隊成員技術水平不一
- ✅ 需要快速部署到生產環境
使用 PyTorch:
- ✅ 需要深入理解模型細節
- ✅ 研究導向專案
- ✅ 複雜的自訂架構(如 Transformer)
- ✅ 需要細粒度控制訓練過程
現在你已經掌握了:
- ✅ Keras 的高階 API(LeNet-5)
- ✅ PyTorch 的底層控制(SimpleCNN)
準備好挑戰更難的任務了嗎?
請繼續閱讀:CNN_intro_b07_part3.md - CIFAR-10 彩色影像分類
本文件完成時間:2025-10-07 14:30:00
版本:b07_part2
下一部分:CNN_intro_b07_part3.md (CIFAR-10 進階實戰)