本文件是 CNN 完整實戰指南的第四部分(最終篇),總結實戰經驗與進階技巧。
前置閱讀:建議先完成 Part 1-3。
症狀:
Epoch 1: Train Acc 65%, Test Acc 63%
Epoch 5: Train Acc 75%, Test Acc 72%
Epoch 10: Train Acc 76%, Test Acc 72% ← 停滯
Epoch 20: Train Acc 76%, Test Acc 72%
可能原因與解決方案:
診斷步驟:
# ===== 步驟 1: 檢查學習率 =====
# 畫出損失曲線,觀察震盪情況
plt.plot(history['loss'])
plt.title('訓練損失曲線')
plt.show()
# 如果損失劇烈震盪 → 學習率過大
# 如果損失幾乎不動 → 學習率過小
# ===== 步驟 2: 嘗試更大的學習率範圍 =====
# Learning Rate Finder
learning_rates = [1e-5, 1e-4, 1e-3, 1e-2, 1e-1]
for lr in learning_rates:
model = create_model()
optimizer = Adam(lr=lr)
# 訓練 2-3 個 epoch
# 記錄最終損失
# ===== 步驟 3: 檢查模型容量 =====
# 計算訓練集和測試集的差距
train_acc = 76%
test_acc = 72%
gap = 4%
if gap < 5%:
print("模型容量可能不足,試著加深/加寬網路")
else:
print("模型容量充足,問題可能在學習率或資料")
# ===== 步驟 4: 視覺化預測 =====
# 看看錯誤的案例,是否有規律?
wrong_indices = np.where(y_pred != y_true)[0]
for idx in wrong_indices[:10]:
plt.imshow(X_test[idx])
plt.title(f"True: {y_true[idx]}, Pred: {y_pred[idx]}")
plt.show()
症狀:
Epoch 20: Train Acc 95%, Test Acc 75% ← 差距 20%
解決方案優先級:
| 方法 | 效果 | 實作難度 | 推薦指數 |
|---|---|---|---|
| 資料增強 | +++++ | ★☆☆☆☆ | ⭐⭐⭐⭐⭐ |
| Dropout | ++++ | ★☆☆☆☆ | ⭐⭐⭐⭐⭐ |
| L2 正規化 | +++ | ★☆☆☆☆ | ⭐⭐⭐⭐☆ |
| 早停 (Early Stopping) | ++++ | ★☆☆☆☆ | ⭐⭐⭐⭐⭐ |
| Batch Normalization | +++ | ★★☆☆☆ | ⭐⭐⭐⭐☆ |
| 減少模型容量 | +++ | ★★☆☆☆ | ⭐⭐⭐☆☆ |
| 收集更多資料 | +++++ | ★★★★★ | ⭐⭐⭐⭐⭐ |
完整解決方案:
# ===== 方案 1: 資料增強(最有效) =====
datagen = ImageDataGenerator(
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True,
zoom_range=0.15,
shear_range=0.15
)
# ===== 方案 2: 加強 Dropout =====
model = Sequential([
Conv2D(64, (3,3), activation='relu'),
Dropout(0.3), # 從 0.25 提高到 0.3-0.4
# ...
Dense(128, activation='relu'),
Dropout(0.5), # 全連接層用更高的 Dropout
Dense(10, activation='softmax')
])
# ===== 方案 3: L2 正規化 =====
from tensorflow.keras.regularizers import l2
model.add(Dense(128, activation='relu',
kernel_regularizer=l2(0.001))) # L2 正規化
# PyTorch: 在優化器中設定 weight_decay
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
# ===== 方案 4: 早停 =====
early_stop = EarlyStopping(
monitor='val_loss',
patience=10, # 10 個 epoch 沒改善就停
restore_best_weights=True
)
# ===== 方案 5: 減少模型大小 =====
# 將濾波器數量減半
# 原本: 64 → 128 → 256 → 512
# 改為: 32 → 64 → 128 → 256
速度優化檢查清單:
# ===== 1. 確認 GPU 已啟用 =====
# TensorFlow
print(tf.config.list_physical_devices('GPU'))
# PyTorch
print(torch.cuda.is_available())
# 如果顯示 GPU 但沒加速,檢查資料是否在 GPU 上:
data = data.to(device) # PyTorch
model = model.to(device)
# ===== 2. 增加 Batch Size =====
# GPU 記憶體允許的情況下,越大越快
BATCH_SIZE = 256 # 從 128 提高到 256
# 注意:Batch Size 提高可能需要調整學習率
# ===== 3. 使用 Mixed Precision 訓練 =====
# TensorFlow
from tensorflow.keras.mixed_precision import Policy
policy = Policy('mixed_float16')
tf.keras.mixed_precision.set_global_policy(policy)
# PyTorch
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for data, target in train_loader:
optimizer.zero_grad()
with autocast(): # 自動混合精度
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
# 效果:速度提升 2-3 倍,記憶體減少 50%
# ===== 4. 優化 DataLoader =====
# 增加 num_workers
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=4, # 從 2 提高到 4
pin_memory=True, # 加速 CPU→GPU 傳輸
persistent_workers=True # PyTorch 1.7+
)
# ===== 5. 使用更快的優化器 =====
# AdamW 通常比 Adam 更快收斂
from tensorflow.keras.optimizers import AdamW # Keras
import torch.optim as optim # PyTorch
optimizer = optim.AdamW(model.parameters(), lr=0.001)
速度對比(CIFAR-10, 50 epochs):
| 設定 | 訓練時間 | 加速比 |
|---|---|---|
| CPU, batch=128 | 120 分鐘 | 1× |
| GPU, batch=128 | 15 分鐘 | 8× |
| GPU, batch=256 | 10 分鐘 | 12× |
| GPU, batch=256, Mixed Precision | 5 分鐘 | 24× |
| GPU, batch=256, MP, num_workers=4 | 4 分鐘 | 30× |
錯誤訊息:
RuntimeError: CUDA out of memory. Tried to allocate 1.50 GiB
(GPU 0; 15.75 GiB total capacity; 14.23 GiB already allocated)
解決方案:
# ===== 方案 1: 減少 Batch Size(最有效) =====
BATCH_SIZE = 64 # 從 128 減到 64 或 32
# ===== 方案 2: 清空 GPU 快取 =====
import torch
torch.cuda.empty_cache()
# TensorFlow
from tensorflow.keras import backend as K
K.clear_session()
# ===== 方案 3: 使用梯度累積(保持有效 Batch Size) =====
# 模擬 batch_size=128 的效果
ACCUMULATION_STEPS = 4
effective_batch_size = 32 # 32 × 4 = 128
optimizer.zero_grad()
for i, (data, target) in enumerate(train_loader):
output = model(data)
loss = criterion(output, target) / ACCUMULATION_STEPS
loss.backward()
if (i + 1) % ACCUMULATION_STEPS == 0:
optimizer.step()
optimizer.zero_grad()
# ===== 方案 4: 減少模型大小 =====
# 將濾波器數量減半
# 或使用 DepthwiseSeparable Convolution
# ===== 方案 5: 使用 Gradient Checkpointing =====
# PyTorch
from torch.utils.checkpoint import checkpoint
def forward(self, x):
x = checkpoint(self.block1, x) # 重新計算而非儲存
x = checkpoint(self.block2, x)
return x
診斷代碼:
# ===== 診斷清單 =====
def diagnose_model(model, X_train, y_train, X_test, y_test):
"""診斷模型訓練問題"""
print("=" * 70)
print("模型診斷報告")
print("=" * 70)
# 1. 檢查資料範圍
print(f"\n1. 資料範圍檢查:")
print(f" X_train: [{X_train.min():.4f}, {X_train.max():.4f}]")
print(f" ✓ 正常範圍: [0, 1] 或 [-1, 1]" if X_train.max() <= 1.5 else " ✗ 可能需要正規化!")
# 2. 檢查標籤格式
print(f"\n2. 標籤格式檢查:")
print(f" y_train shape: {y_train.shape}")
print(f" y_train unique: {np.unique(y_train)}")
# 3. 檢查類別平衡
print(f"\n3. 類別平衡檢查:")
unique, counts = np.unique(y_train, return_counts=True)
max_count = counts.max()
min_count = counts.min()
imbalance_ratio = max_count / min_count
print(f" 最多: {max_count}, 最少: {min_count}, 比例: {imbalance_ratio:.2f}")
if imbalance_ratio > 3:
print(f" ⚠️ 類別不平衡!考慮使用加權損失")
# 4. 過擬合檢測(需要訓練後)
print(f"\n4. 過擬合檢測:")
train_loss, train_acc = model.evaluate(X_train[:1000], y_train[:1000], verbose=0)
test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
gap = train_acc - test_acc
print(f" Train Acc: {train_acc*100:.2f}%")
print(f" Test Acc: {test_acc*100:.2f}%")
print(f" Gap: {gap*100:.2f}%")
if gap > 0.15:
print(f" ⚠️ 過擬合!建議:")
print(f" - 資料增強")
print(f" - 增加 Dropout")
print(f" - L2 正規化")
elif gap < 0.05 and test_acc < 0.85:
print(f" ⚠️ 模型容量不足!建議:")
print(f" - 增加層數")
print(f" - 增加濾波器數量")
# 5. 檢查梯度
print(f"\n5. 梯度檢查:")
# 訓練一個 batch
sample_batch = X_train[:32]
sample_labels = y_train[:32]
with tf.GradientTape() as tape:
predictions = model(sample_batch)
loss = tf.keras.losses.categorical_crossentropy(sample_labels, predictions)
gradients = tape.gradient(loss, model.trainable_variables)
# 檢查是否有 NaN 或過大/過小的梯度
for i, grad in enumerate(gradients):
if grad is not None:
grad_mean = tf.reduce_mean(tf.abs(grad)).numpy()
if np.isnan(grad_mean):
print(f" ✗ 層 {i}: 梯度為 NaN!")
elif grad_mean < 1e-8:
print(f" ⚠️ 層 {i}: 梯度過小 ({grad_mean:.2e}) - 可能梯度消失")
elif grad_mean > 1e2:
print(f" ⚠️ 層 {i}: 梯度過大 ({grad_mean:.2e}) - 可能梯度爆炸")
print("\n" + "=" * 70)
# 使用範例
diagnose_model(model, X_train, y_train_cat, X_test, y_test_cat)
# 視覺化過擬合/欠擬合
def plot_learning_curves(history):
"""視覺化學習曲線並診斷問題"""
train_acc = history['accuracy']
val_acc = history['val_accuracy']
train_loss = history['loss']
val_loss = history['val_loss']
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# 準確率
ax1.plot(train_acc, 'b-', label='訓練', linewidth=2)
ax1.plot(val_acc, 'r-', label='驗證', linewidth=2)
ax1.set_title('準確率曲線', fontsize=14, fontweight='bold')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('準確率')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 損失
ax2.plot(train_loss, 'b-', label='訓練', linewidth=2)
ax2.plot(val_loss, 'r-', label='驗證', linewidth=2)
ax2.set_title('損失曲線', fontsize=14, fontweight='bold')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('損失')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 診斷
final_train_acc = train_acc[-1]
final_val_acc = val_acc[-1]
gap = final_train_acc - final_val_acc
print("\n診斷結果:")
print("=" * 50)
if final_val_acc < 0.7:
if gap < 0.05:
print("❌ 欠擬合 (Underfitting)")
print(" 訓練和驗證準確率都很低,但差距小")
print("\n建議:")
print(" 1. 增加模型容量(更多層/濾波器)")
print(" 2. 訓練更多 epochs")
print(" 3. 降低正規化強度")
print(" 4. 檢查學習率是否過小")
else:
print("⚠️ 欠擬合 + 輕微過擬合")
print(" 驗證準確率低,但訓練準確率更高")
print("\n建議:")
print(" 1. 先解決欠擬合:增加模型容量")
print(" 2. 再處理過擬合:加資料增強")
elif gap > 0.15:
print("❌ 過擬合 (Overfitting)")
print(f" 訓練準確率 {final_train_acc*100:.1f}%,驗證準確率 {final_val_acc*100:.1f}%")
print(f" 差距 {gap*100:.1f}%")
print("\n建議:")
print(" 1. 資料增強(最有效)")
print(" 2. 增加 Dropout (0.3-0.5)")
print(" 3. L2 正規化 (weight_decay=1e-4)")
print(" 4. 早停 (EarlyStopping)")
print(" 5. 減少模型容量")
else:
print("✅ 良好擬合 (Good Fit)")
print(f" 訓練準確率 {final_train_acc*100:.1f}%,驗證準確率 {final_val_acc*100:.1f}%")
print(f" 差距 {gap*100:.1f}%(健康範圍)")
# 檢查驗證損失是否上升
if len(val_loss) > 10:
min_val_loss_epoch = np.argmin(val_loss)
if min_val_loss_epoch < len(val_loss) - 5:
print(f"\n⚠️ 驗證損失在 epoch {min_val_loss_epoch} 達到最低")
print(f" 之後開始上升,建議在該點停止訓練")
plt.tight_layout()
plt.show()
# 使用範例
plot_learning_curves(history.history)
# ===== 策略 1: 學習率範圍測試 (Learning Rate Finder) =====
def find_learning_rate(model, train_loader, criterion, device):
"""找出最佳學習率範圍"""
learning_rates = []
losses = []
# 測試從 1e-6 到 1 的學習率
lr = 1e-6
for batch_idx, (data, target) in enumerate(train_loader):
if batch_idx > 100: # 只測試 100 個 batch
break
data, target = data.to(device), target.to(device)
# 更新學習率
optimizer = optim.SGD(model.parameters(), lr=lr)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# 記錄
learning_rates.append(lr)
losses.append(loss.item())
# 指數增長學習率
lr *= 1.1
# 視覺化
plt.figure(figsize=(10, 6))
plt.plot(learning_rates, losses)
plt.xscale('log')
plt.xlabel('Learning Rate')
plt.ylabel('Loss')
plt.title('Learning Rate Finder')
plt.grid(True)
plt.show()
# 建議:選擇損失下降最快的點
print("建議學習率:")
min_loss_idx = np.argmin(losses)
print(f" 最低損失點: {learning_rates[min_loss_idx]:.6f}")
print(f" 建議值(除以10): {learning_rates[min_loss_idx]/10:.6f}")
# ===== 策略 2: 學習率調度 =====
# 方案 A: Step Decay(階梯式衰減)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
# 每 30 個 epoch,學習率乘以 0.1
# 方案 B: Exponential Decay(指數衰減)
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
# 每個 epoch,學習率乘以 0.95
# 方案 C: Cosine Annealing(餘弦退火)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)
# 餘弦曲線,從 lr_max 降到 eta_min
# 方案 D: ReduceLROnPlateau(自適應)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5, verbose=True
)
# 驗證損失 5 個 epoch 沒改善,學習率減半
# ===== 策略 3: Warm-up + Cosine Annealing =====
def lr_lambda(epoch):
if epoch < 10: # Warm-up
return (epoch + 1) / 10
else: # Cosine Annealing
progress = (epoch - 10) / (100 - 10)
return 0.5 * (1 + np.cos(np.pi * progress))
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
學習率調度對比:
| 策略 | 曲線 | 適用場景 | 效果 |
|---|---|---|---|
| Fixed | 平坦 | 快速實驗 | ★★☆☆☆ |
| Step Decay | 階梯 | 傳統訓練 | ★★★☆☆ |
| Exponential | 指數下降 | 長時間訓練 | ★★★☆☆ |
| Cosine Annealing | 餘弦曲線 | 現代推薦 | ★★★★☆ |
| ReduceLROnPlateau | 自適應 | 不確定時 | ★★★★★ |
| Warm-up + Cosine | 先升後降 | 大型模型 | ★★★★★ |
# Batch Size 對比實驗
batch_sizes = [32, 64, 128, 256, 512]
results = {}
for bs in batch_sizes:
model = create_model()
optimizer = Adam(lr=0.001)
# 訓練 10 epochs
# ...
results[bs] = {
'train_time': ...,
'final_acc': ...,
'memory_usage': ...
}
# 結果範例(CIFAR-10)
"""
Batch Size | 訓練時間 | 準確率 | GPU 記憶體
-----------|---------|-------|----------
32 | 25 min | 86.2% | 2.5 GB
64 | 15 min | 86.5% | 3.8 GB
128 | 10 min | 86.8% | 6.2 GB ← 推薦
256 | 8 min | 86.3% | 10.5 GB
512 | 7 min | 85.1% | OOM ← 過大導致泛化變差
"""
Batch Size 選擇建議:
# ============================================
# 完整優化範例(PyTorch)
# 包含所有效能提升技巧
# ============================================
import torch
import torch.nn as nn
from torch.cuda.amp import autocast, GradScaler
from torch.utils.data import DataLoader
# ===== 1. 使用 Mixed Precision 訓練 =====
scaler = GradScaler()
# ===== 2. 優化 DataLoader =====
train_loader = DataLoader(
train_dataset,
batch_size=256, # 較大 batch size
shuffle=True,
num_workers=4, # 多執行緒
pin_memory=True, # 加速 CPU→GPU
persistent_workers=True, # 保持 workers 活著
prefetch_factor=2 # 預載入
)
# ===== 3. 優化模型 =====
model = create_model()
model = model.to(device)
# 使用 torch.compile(PyTorch 2.0+)
model = torch.compile(model) # 自動優化計算圖
# ===== 4. 優化優化器 =====
optimizer = torch.optim.AdamW(
model.parameters(),
lr=0.001,
betas=(0.9, 0.999),
weight_decay=0.01,
foreach=True # 向量化操作(PyTorch 1.12+)
)
# ===== 5. 訓練迴圈(完整優化版) =====
model.train()
for epoch in range(num_epochs):
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)
# Mixed Precision
with autocast():
output = model(data)
loss = criterion(output, target)
# 反向傳播(Mixed Precision)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True) # 更快的梯度清空
# ===== 效能對比 =====
"""
優化前: 20 分鐘/epoch
優化後: 5 分鐘/epoch ← 快 4 倍!
優化項目 | 提升
-------------------|------
Mixed Precision | 2.5×
DataLoader 優化 | 1.3×
torch.compile | 1.2×
綜合效果 | 4×
"""
當你完成本指南後,可以探索這些進階主題:
# 使用預訓練模型(以 ResNet 為例)
from torchvision import models
# 載入預訓練模型
resnet = models.resnet18(pretrained=True)
# 凍結預訓練層
for param in resnet.parameters():
param.requires_grad = False
# 替換最後一層
resnet.fc = nn.Linear(512, 10) # CIFAR-10 有 10 類
# 只訓練最後一層
optimizer = optim.Adam(resnet.fc.parameters(), lr=0.001)
# 效果:
# - 訓練時間: 從 20 分鐘減少到 5 分鐘
# - 準確率: 從 87% 提升到 94%
進階閱讀:
- ImageNet 預訓練模型
- Fine-tuning 策略
- Domain Adaptation
| 技術 | 原理 | 效果 | 適用性 |
|---|---|---|---|
| Mixup | 混合兩張影像和標籤 | +2-3% | 通用 |
| Cutout | 隨機遮擋區域 | +1-2% | 影像 |
| CutMix | 剪貼混合 | +2-3% | 影像 |
| Label Smoothing | 軟化標籤 | +0.5-1% | 分類 |
| DropBlock | 結構化 Dropout | +1-2% | CNN |
# Mixup 範例
def mixup_data(x, y, alpha=0.2):
lam = np.random.beta(alpha, alpha)
index = torch.randperm(x.size(0))
mixed_x = lam * x + (1 - lam) * x[index]
y_a, y_b = y, y[index]
return mixed_x, y_a, y_b, lam
# 訓練時使用
mixed_x, y_a, y_b, lam = mixup_data(x, y)
output = model(mixed_x)
loss = lam * criterion(output, y_a) + (1 - lam) * criterion(output, y_b)
| 領域 | 應用 | 推薦資料集 | 難度 |
|---|---|---|---|
| 醫療影像 | X光診斷、腫瘤偵測 | ChestX-ray14, ISIC | ⭐⭐⭐⭐☆ |
| 自動駕駛 | 物體偵測、車道線 | KITTI, Cityscapes | ⭐⭐⭐⭐⭐ |
| 人臉識別 | 身份驗證、表情辨識 | LFW, CelebA | ⭐⭐⭐☆☆ |
| 農業 | 病蟲害偵測、作物分類 | PlantVillage | ⭐⭐⭐☆☆ |
| 工業檢測 | 瑕疵偵測、品質控制 | MVTec AD | ⭐⭐⭐⭐☆ |
| 零售 | 商品識別、人流分析 | ImageNet, OpenImages | ⭐⭐⭐☆☆ |
全職學習(每週 40 小時):
- 第 1-2 週:完成 MNIST,理解基礎概念
- 第 3-4 週:PyTorch 實作,CIFAR-10 入門
- 第 5-6 週:進階技術,超參數調整
- 第 7-8 週:ResNet、遷移學習
- 第 9-12 週:實際專案,找工作
兼職學習(每週 10 小時):
- 第 1-4 週:MNIST 基礎
- 第 5-8 週:PyTorch + CIFAR-10
- 第 9-12 週:進階技術
- 第 13-20 週:ResNet + 專案
本章節系統化地總結所有 CNN 的核心理論,幫助你建立完整的知識體系。
定義:卷積是一種線性運算,透過滑動濾波器(filter/kernel)在輸入上進行元素乘法和加總。
數學表達式:
對於 2D 卷積:
Y[i,j] = Σₘ Σₙ X[i+m, j+n] × K[m,n] + b
其中:
- Y: 輸出特徵圖(Feature Map)
- X: 輸入影像
- K: 卷積核(Kernel)
- b: 偏差項(Bias)
- m,n: 卷積核的索引
實例計算:
# 輸入影像 (5×5)
X = [[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]]
# 卷積核 (3×3) - 邊緣偵測
K = [[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]]
# 計算輸出的一個元素 Y[1,1](無 padding):
Y[1,1] = 1×(-1) + 2×(-1) + 3×(-1) +
6×(-1) + 7×(8) + 8×(-1) +
11×(-1) + 12×(-1) + 13×(-1)
= -1 -2 -3 -6 +56 -8 -11 -12 -13
= 0
完整公式:
輸出高度 = floor((H + 2P - K) / S) + 1
輸出寬度 = floor((W + 2P - K) / S) + 1
其中:
- H, W: 輸入高度和寬度
- K: 卷積核大小
- P: Padding(填充)
- S: Stride(步幅)
- floor: 向下取整
範例:
| 輸入 | 卷積核 | Padding | Stride | 輸出 | 計算 |
|---|---|---|---|---|---|
| 32×32 | 3×3 | 0 | 1 | 30×30 | (32-3)/1+1 = 30 |
| 32×32 | 3×3 | 1 | 1 | 32×32 | (32+2-3)/1+1 = 32 |
| 32×32 | 5×5 | 2 | 1 | 32×32 | (32+4-5)/1+1 = 32 |
| 32×32 | 3×3 | 0 | 2 | 15×15 | (32-3)/2+1 = 15 |
參數數量對比:
# 全連接層(假設輸入 32×32,輸出 64 個神經元)
參數量 = 32 × 32 × 64 + 64(bias) = 65,600
# 卷積層(64 個 3×3 濾波器,輸入通道 1)
參數量 = 3 × 3 × 1 × 64 + 64(bias) = 640
節省 = 65,600 / 640 = 102 倍!
為什麼卷積有效?
運作機制:
# 輸入 (4×4)
X = [[1, 3, 2, 4],
[5, 6, 7, 8],
[3, 2, 1, 0],
[1, 2, 3, 4]]
# Max Pooling 2×2, stride=2
# 將 4×4 分成 4 個 2×2 區域,取最大值
Y = [[max(1,3,5,6)=6, max(2,4,7,8)=8],
[max(3,2,1,2)=3, max(1,0,3,4)=4]]
Y = [[6, 8],
[3, 4]]
為什麼使用池化?
| 優勢 | 說明 | 數值範例 |
|---|---|---|
| 降維 | 減少計算量 | 4×4→2×2,減少 75% |
| 平移不變性 | 物體小幅移動不影響輸出 | 特徵移動 1 像素,池化輸出不變 |
| 抗雜訊 | 保留最顯著特徵 | 雜訊通常不是最大值 |
| 擴大感受野 | 每層看到更大範圍 | 2 層池化 → 感受野 ×4 |
# 輸入相同
# Average Pooling 2×2
Y = [[avg(1,3,5,6)=3.75, avg(2,4,7,8)=5.25],
[avg(3,2,1,2)=2.00, avg(1,0,3,4)=2.00]]
Max Pooling vs Average Pooling:
| 特性 | Max Pooling | Average Pooling |
|---|---|---|
| 用途 | 特徵偵測(主流) | 背景資訊、全局特徵 |
| 保留資訊 | 最顯著特徵 | 平均資訊 |
| 現代使用 | 卷積層之間 | 全局平均池化(GAP) |
| 範例 | ResNet, VGG | GoogLeNet 最後一層 |
| 激活函數 | 數學表達式 | 導數 | 值域 | 優缺點 |
|---|---|---|---|---|
| ReLU | f(x) = max(0, x) | f'(x) = 1 if x>0 else 0 | [0, ∞) | 快速、避免梯度消失 ❌ Dead ReLU |
| Leaky ReLU | f(x) = max(0.01x, x) | f'(x) = 1 if x>0 else 0.01 | (-∞, ∞) | 解決 Dead ReLU ❌ 需調參 |
| Sigmoid | f(x) = 1/(1+e⁻ˣ) | f'(x) = f(x)(1-f(x)) | (0, 1) | 有界、平滑 ❌ 梯度消失 |
| Tanh | f(x) = (eˣ-e⁻ˣ)/(eˣ+e⁻ˣ) | f'(x) = 1 - f(x)² | (-1, 1) | 零中心化 ❌ 梯度消失 |
什麼是 Dead ReLU?
# 神經元的輸入始終 ≤ 0
x = -5 # 負輸入
output = max(0, x) = 0 # 輸出永遠是 0
gradient = 0 # 梯度永遠是 0
# 結果:神經元永遠無法更新(「死亡」)
原因:
1. 不當的權重初始化
2. 學習率過大
3. 輸入資料未正規化
解決方案:
1. 使用 Leaky ReLU 或 PReLU
2. 正確的權重初始化(He initialization)
3. Batch Normalization
4. 適當的學習率
算法步驟:
對於 mini-batch B = {x₁, x₂, ..., xₘ}:
1. 計算 mini-batch 均值:
μ_B = (1/m) Σ xᵢ
2. 計算 mini-batch 方差:
σ²_B = (1/m) Σ (xᵢ - μ_B)²
3. 正規化:
x̂ᵢ = (xᵢ - μ_B) / √(σ²_B + ε)
4. 縮放與平移(可學習參數 γ, β):
yᵢ = γ × x̂ᵢ + β
為什麼有效?
運作機制:
# 訓練時(Dropout rate = 0.5)
# 隨機丟棄 50% 的神經元
input = [1.0, 2.0, 3.0, 4.0]
mask = [1, 0, 1, 0] # 隨機生成
output = [1.0, 0, 3.0, 0] # element-wise 乘法
# 測試時(不使用 Dropout)
output = [1.0, 2.0, 3.0, 4.0] × 0.5 # 縮放補償
為什麼有效?
測試時相當於模型平均
減少神經元依賴:
防止過度依賴特定神經元
理論證明(Srivastava et al., 2014):
Dropout 率選擇:
| 層類型 | 推薦 Dropout 率 | 原因 |
|---|---|---|
| 卷積層 | 0.1-0.3 | 參數共享已提供正規化 |
| 全連接層 | 0.5-0.7 | 參數多,容易過擬合 |
| 輸出層 | 0 | 不應隨機丟棄最終預測 |
| 小資料集 | 0.5-0.7 | 需要更強正規化 |
| 大資料集 | 0.2-0.3 | 資料已提供正規化 |
數學定義:
對於多分類問題:
L = -Σᵢ yᵢ × log(ŷᵢ)
其中:
- yᵢ: 真實標籤(One-Hot)
- ŷᵢ: 預測機率(Softmax 輸出)
實例計算:
# 真實標籤(類別 2)
y_true = [0, 0, 1, 0, 0] # One-Hot
# 模型預測(經過 Softmax)
y_pred = [0.05, 0.10, 0.70, 0.10, 0.05]
# 交叉熵損失
Loss = -(0×log(0.05) + 0×log(0.10) + 1×log(0.70) + 0×log(0.10) + 0×log(0.05))
= -log(0.70)
= 0.357
# 如果預測很準確
y_pred_good = [0.01, 0.01, 0.96, 0.01, 0.01]
Loss = -log(0.96) = 0.041 ← 損失低
# 如果預測錯誤
y_pred_bad = [0.01, 0.01, 0.02, 0.01, 0.95]
Loss = -log(0.02) = 3.912 ← 損失高
為什麼使用交叉熵?
| 對比 | 均方誤差 (MSE) | 交叉熵 |
|---|---|---|
| 數學形式 | Σ(y - ŷ)² | -Σ y log(ŷ) |
| 梯度特性 | 飽和時梯度小 | 錯誤越大梯度越大 ✅ |
| 適用場景 | 回歸問題 | 分類問題 ✅ |
| 收斂速度 | 慢 | 快 ✅ |
梯度下降(Gradient Descent):
θₜ₊₁ = θₜ - η × ∇L(θₜ)
其中:
- θ: 參數
- η: 學習率
- ∇L: 損失函數的梯度
動量(Momentum):
vₜ = β × vₜ₋₁ + ∇L(θₜ)
θₜ₊₁ = θₜ - η × vₜ
效果: 累積過去的梯度,加速收斂
Adam (Adaptive Moment Estimation):
# 一階動量(梯度的指數移動平均)
mₜ = β₁ × mₜ₋₁ + (1-β₁) × ∇L(θₜ)
# 二階動量(梯度平方的指數移動平均)
vₜ = β₂ × vₜ₋₁ + (1-β₂) × (∇L(θₜ))²
# 偏差修正
m̂ₜ = mₜ / (1-β₁ᵗ)
v̂ₜ = vₜ / (1-β₂ᵗ)
# 參數更新
θₜ₊₁ = θₜ - η × m̂ₜ / (√v̂ₜ + ε)
其中: β₁=0.9, β₂=0.999, ε=10⁻⁸
優化器對比:
| 優化器 | 優點 | 缺點 | 推薦場景 |
|---|---|---|---|
| SGD | 簡單、理論保證 | 收斂慢、需調參 | 經典論文複現 |
| SGD + Momentum | 加速、減少震盪 | 仍需調參 | 大型資料集 |
| Adam | 自適應、穩健 | 泛化能力稍差 | 大多數情況 ✅ |
| AdamW | Adam + 正確的 L2 | 略複雜 | 現代推薦 ✅ |
反向傳播的核心是鏈式法則:
對於複合函數 f(g(x)),導數為:
df/dx = (df/dg) × (dg/dx)
CNN 中的應用:
損失函數 L 對參數 W 的梯度:
L = Loss(Softmax(Dense(Flatten(Pool(Conv(X, W))))))
∂L/∂W = (∂L/∂Softmax) × (∂Softmax/∂Dense) × ... × (∂Conv/∂W)
前向傳播:
Y = X ∗ K + b (∗ 表示卷積)
反向傳播:
1. 損失對輸出的梯度(從上一層傳來):
∂L/∂Y
2. 損失對卷積核的梯度:
∂L/∂K = X ∗ (∂L/∂Y)
3. 損失對輸入的梯度(傳給下一層):
∂L/∂X = (∂L/∂Y) ∗ K_rotated
其中 K_rotated 是 K 旋轉 180 度
實例計算:
# 前向傳播
X = [[1, 2], K = [[1, 0], Y = [[7, 10],
[3, 4]] [0, 1]] [10, 11]]
# 反向傳播(假設 ∂L/∂Y = [[1, 1], [1, 1]])
∂L/∂K = X ∗ (∂L/∂Y)
= [[1×1+2×1+3×1+4×1, ...], # 簡化
[..., ...]]
= [[10, 10],
[10, 10]]
# 卷積核更新
K_new = K - learning_rate × ∂L/∂K
數學分析:
梯度下降: θₜ₊₁ = θₜ - η × ∇L(θₜ)
η 過大:
- 步長太大,跨過最優點
- 損失函數值劇烈震盪
- Loss: 0.5 → 1.2 → 0.3 → 2.1 (不穩定)
η 適中:
- 穩步向最優點前進
- Loss: 0.5 → 0.4 → 0.3 → 0.2 → 0.15 (穩定下降)
η 過小:
- 步長太小,移動緩慢
- Loss: 0.5 → 0.499 → 0.498 (幾乎不動)
數學視角:
梯度估計的方差:
Var(∇L) ∝ 1 / batch_size
Batch Size ↑ → Var ↓ → 梯度估計更準確
Batch Size ↓ → Var ↑ → 更多隨機性(有時是好事)
實際影響:
| Batch Size | 梯度準確性 | 泛化能力 | 訓練速度 | 記憶體 | 推薦 |
|---|---|---|---|---|---|
| 32 | 低 | 很好 | 慢 | 低 | 小資料集 |
| 64 | 中 | 好 | 中 | 中 | 平衡選擇 |
| 128 | 高 | 好 | 快 | 中高 | 推薦 ✅ |
| 256 | 很高 | 中 | 很快 | 高 | 大資料集 |
| 512+ | 極高 | 差 | 極快 | 很高 | 需特殊調整 |
大 Batch Size 的調整:
根據線性縮放規則(Linear Scaling Rule, Goyal et al., 2017):
如果 Batch Size 增加 k 倍
則 Learning Rate 也應增加 k 倍
範例:
Batch Size 128, LR = 0.001
Batch Size 256, LR = 0.002 ← 2 倍
Batch Size 512, LR = 0.004 ← 4 倍
數學定義:
預期誤差 = Bias² + Variance + 不可約誤差
其中:
- Bias (偏差): 模型的平均預測與真實值的差距
- Variance (方差): 模型對訓練資料的敏感程度
- 不可約誤差: 資料本身的雜訊
VC 維(Vapnik-Chervonenkis Dimension):
模型能夠擬合的最大樣本數量
線性模型(2D): VC 維 = 3
- 可以完美分類任意 3 個點
- 無法保證分類 4 個點
深度神經網路: VC 維 ≈ O(W × log W)
- W: 參數總數
- 容量極大,容易過擬合
正規化的理論作用:
優化目標(無正規化):
min L(θ) = Σᵢ Loss(f(xᵢ; θ), yᵢ)
優化目標(有正規化):
min L(θ) + λ × R(θ)
其中:
- R(θ): 正規化項(如 ||θ||²)
- λ: 正規化強度
效果: 限制模型容量,防止過擬合
定理內容:
具有單個隱藏層的神經網路,只要有足夠多的神經元,就可以以任意精度近似任何連續函數。
數學表達:
對於任意連續函數 f: [0,1]ⁿ → ℝ
存在神經網路 N(x),使得:
|N(x) - f(x)| < ε (對所有 x)
其中 ε 是任意小的正數
實際意義:
✅ 理論上:單層網路可以表示任何函數
❌ 實際上:
- 需要指數級數量的神經元
- 訓練極其困難
- 深層網路更高效
表示能力的指數增長:
淺層網路(1 層隱藏層):
表示能力 ∝ O(n) (n: 神經元數)
深層網路(L 層):
表示能力 ∝ O(nᴸ) (指數增長!)
層次化特徵學習:
輸入影像
↓
第 1 層: 邊緣、角點
↓
第 2 層: 簡單形狀(圓形、方形)
↓
第 3 層: 物體部件(眼睛、鼻子、輪胎)
↓
第 4 層: 完整物體(臉、汽車)
↓
輸出: 分類結果
定義:物體在影像中的位置改變,CNN 的輸出不變(或變化很小)
實現機制:
1. 參數共享:同一個濾波器掃描整張影像
2. 池化:降低空間解析度
3. 大感受野:深層神經元看到大範圍
數學表達:
對於平移 τ:
f(T_τ(x)) ≈ f(x)
其中 T_τ 是平移操作
傳統神經網路:
每個神經元與所有輸入連接
參數量 = 輸入數 × 輸出數
CNN:
每個神經元只與局部感受野連接
參數量 = 卷積核大小 × 濾波器數
節省參數 = 1 - (K²/H×W) ≈ 99%
(K=3, H=W=32 的情況)
計算公式:
第 l 層的感受野:
RF_l = RF_{l-1} + (K_l - 1) × Π_{i=1}^{l-1} S_i
其中:
- K_l: 第 l 層的卷積核大小
- S_i: 第 i 層的步幅
實例:
輸入: 32×32
Conv1 (3×3, s=1) → RF = 3
Pool1 (2×2, s=2) → RF = 6
Conv2 (3×3, s=1) → RF = 10
Pool2 (2×2, s=2) → RF = 20
Conv3 (3×3, s=1) → RF = 28
最終感受野幾乎覆蓋整張影像!
核心公式速查表:
| 概念 | 公式 | 應用 |
|---|---|---|
| 卷積輸出 | H_out = (H + 2P - K)/S + 1 | 計算特徵圖尺寸 |
| 參數數量 | params = K×K×C_in×C_out + C_out | 估算模型大小 |
| 感受野 | RF_l = RF_{l-1} + (K_l-1)×∏S_i | 設計網路深度 |
| 交叉熵 | L = -Σ y_i log(ŷ_i) | 分類損失 |
| Adam 更新 | θ = θ - η×m̂/√v̂ | 參數優化 |
| Batch Norm | x̂ = (x-μ)/√(σ²+ε) | 穩定訓練 |
✅ 基礎概念:
- 卷積、池化、全連接層的原理
- 前向傳播與反向傳播
- 損失函數與優化器
✅ 兩大框架:
- Keras:快速原型開發
- PyTorch:深入理解原理
✅ 兩個經典任務:
- MNIST:灰階影像,98%+ 準確率
- CIFAR-10:彩色影像,85%+ 準確率
✅ 核心技術:
- 資料增強
- Batch Normalization
- Dropout
- 學習率調度
✅ 實戰技巧:
- 診斷模型問題
- 超參數調整
- 效能優化
包含 CNN 專題
Fast.ai: Practical Deep Learning
從上到下的學習方式
Stanford CS231n: Convolutional Neural Networks
按順序閱讀:
"Gradient-Based Learning Applied to Document Recognition"
AlexNet (2012) - 深度學習革命
"ImageNet Classification with Deep CNNs"
VGGNet (2014) - 更深的網路
"Very Deep Convolutional Networks"
ResNet (2015) - 殘差連接
"Deep Residual Learning for Image Recognition"
EfficientNet (2019) - 模型縮放
手寫中文字辨識
中級:
自己收集資料集
高級:
本週:
- [ ] 完整執行本指南所有程式碼
- [ ] 在 MNIST 達到 99%+ 準確率
- [ ] 在 CIFAR-10 達到 85%+ 準確率
下個月:
- [ ] 實作 ResNet-18
- [ ] 嘗試遷移學習
- [ ] 完成一個小型專案
三個月內:
- [ ] 參加一次 Kaggle 競賽
- [ ] 閱讀 5 篇經典論文
- [ ] 建立個人 GitHub 作品集
| 檔案 | 內容 | 框架 | 難度 |
|---|---|---|---|
CNN_intro_b07.md |
基礎知識、MNIST + LeNet-5 | Keras | ★☆☆☆☆ |
CNN_intro_b07_part2.md |
MNIST + SimpleCNN | PyTorch | ★★★☆☆ |
CNN_intro_b07_part3.md |
CIFAR-10 完整實戰 | Both | ★★★★☆ |
CNN_intro_b07_part4.md |
實戰技巧與總結 | Both | ★★★★★ |
我想...
本文件完成時間:2025-10-07 15:30:00
版本:b07_part4
系列完成:全四部分已完成 ✅
感謝閱讀!祝你學習愉快! 🎉