📢 公告:OpenCV 系列文章目前正在重構整理中(進度約 60%),部分文章已暫時下架,後續會陸續補上,完成時間待定。感謝耐心等候!

Like Share Discussion Bookmark Smile

J.J. Huang   2026-03-31   Python OpenCV 07.物件偵測與辨識篇   瀏覽次數:次   DMCA.com Protection Status

Python | OpenCV 避免過擬合

📚 前言

在上一篇 資料增強 中,我們學會了透過增強訓練資料來提升泛化能力。
這一篇介紹另一個關鍵主題:避免過擬合 (Overfitting)

資料增強是「讓模型看更多種類的圖片」,而這一篇的方法是「限制模型過度記憶訓練資料」。兩者都是提升泛化能力的手段,但切入角度不同,通常會一起使用。

本篇會先說明如何判斷過擬合,再介紹四種常用技巧:DropoutL2 正規化Early Stopping學習率排程,每一種都同時示範 PyTorch 與 TensorFlow/Keras 的寫法,並說明如何整合進前幾篇的 train.py

🔎 過擬合是什麼?

想像一個學生只死背教科書上的例題,考試時遇到稍微不同的題目就完全不會。這就是過擬合——模型在訓練資料上表現很好,但對新資料(驗證集或真實世界)表現很差。

簡單來說:

  • 資料增強 → 「讓模型看更多種圖片」
  • 避免過擬合 → 「限制模型不要死背訓練資料」

這兩招通常會一起使用,才能讓模型真正學會「懂」而不是「背」。

🔎 如何判斷過擬合?


圖:過擬合的 Loss 曲線特徵 ─ Train Loss 持續下降、Val Loss 先降後升,代表模型開始「背答案」而非真正學會

觀察訓練過程中 Train Loss 與 Val Loss 的走勢,就能判斷模型目前的狀態:

現象 判斷
Train Loss 下降,Val Loss 也跟著下降 正常,模型持續改善
Train Loss 下降,Val Loss 先降後升,兩者差距越來越大 過擬合
Train Loss 與 Val Loss 都很高且停滯 欠擬合,模型容量不足或訓練次數不夠

💡 先看 Loss 曲線,再學方法

在進入四種方法之前,先用模擬資料跑出一條過擬合的 Loss 曲線,讓你有直觀的畫面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# simulate_overfitting.py
import numpy as np
import matplotlib.pyplot as plt

epochs = list(range(1, 21))
train_loss = [1.0 * (0.78 ** i) for i in epochs] # 持續下降(模型一直在記憶訓練資料)
val_loss = [1.0 * (0.78 ** i) + 0.008 * i for i in epochs] # 先降後升(泛化能力開始下滑)

plt.figure(figsize=(8, 4))
plt.plot(epochs, train_loss, label="Train Loss")
plt.plot(epochs, val_loss, label="Val Loss", linestyle="--")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("過擬合的 Loss 曲線特徵")
plt.legend()
plt.tight_layout()
plt.savefig("output/simulate_overfitting.png")
plt.show()


圖:模擬 20 個 epoch 的 Loss 曲線 ─ Train Loss 持續下降,Val Loss 在第 8 個 epoch 後開始上升,典型的過擬合模式

看完這個模式,接下來的四種方法都是為了讓 Val Loss 不再往上走。

💻 診斷:繪製 Loss 曲線

PyTorch 微調範例TensorFlow/Keras 微調範例 的訓練迴圈裡,把每個 epoch 的 Loss 記錄下來,就能繪製真實的診斷曲線。

🔸 PyTorch

train.py 的訓練迴圈外初始化兩個清單,每個 epoch 結束後記錄 Loss,訓練完成後繪製曲線。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# plot_loss_pytorch.py
import matplotlib.pyplot as plt

train_losses = [] # 在訓練迴圈外初始化,累積每個 epoch 的 Train Loss
val_losses = [] # 累積每個 epoch 的 Val Loss

for epoch in range(NUM_EPOCHS): # NUM_EPOCHS 與 train.py 中的設定相同
# ... 訓練與驗證程式碼(與 train.py 相同)...
train_losses.append(train_loss) # 每個 epoch 結束後記錄
val_losses.append(val_loss)

plt.figure(figsize=(8, 4))
plt.plot(train_losses, label="Train Loss")
plt.plot(val_losses, label="Val Loss", linestyle="--")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.title("Training / Validation Loss")
plt.tight_layout()
plt.savefig("output/loss_curve.png")
plt.show()
點擊展開 / 收合完整 train.py 程式碼
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# train.py(已加入 Loss 記錄與曲線繪製)
import os
import json
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt # ← 新增這一行,用來畫 Loss 曲線

# ── 超參數設定 ──────────────────────────────
BATCH_SIZE = 32
NUM_EPOCHS = 15
LEARNING_RATE = 1e-3
NUM_CLASSES = 2 # 依實際類別數修改
TASK_NAME = "cat_dog" # 換任務只改這一行
DATA_DIR = f"data/{TASK_NAME}"
SAVE_DIR = f"models/resnet18/{TASK_NAME}"
# ────────────────────────────────────────────

os.makedirs(SAVE_DIR, exist_ok=True)

# ① 資料前處理
train_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
])

val_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
])

# ② 資料載入
train_dataset = datasets.ImageFolder(f"{DATA_DIR}/train", transform=train_transform)
val_dataset = datasets.ImageFolder(f"{DATA_DIR}/val", transform=val_transform)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)

print(f"類別:{train_dataset.classes}")
print(f"訓練集:{len(train_dataset)} 張,驗證集:{len(val_dataset)} 張")

# ③ 模型建立(Feature Extraction:凍結預訓練層,只訓練分類頭)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用裝置:{device}")

model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

for param in model.parameters():
param.requires_grad = False

model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
model = model.to(device)

# ④ 損失函數與優化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.fc.parameters(), lr=LEARNING_RATE)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

# ── 新增:用來記錄 Loss 曲線 ──────────────────────────────
train_losses = [] # 儲存每個 epoch 的訓練 Loss
val_losses = [] # 儲存每個 epoch 的驗證 Loss
# ─────────────────────────────────────────────────────────

# ⑤ 訓練迴圈
best_val_acc = 0.0

for epoch in range(NUM_EPOCHS):
# ==================== 訓練階段 ====================
model.train()
running_loss = 0.0
correct = total = 0

for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)

optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()

running_loss += loss.item()
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

train_loss = running_loss / len(train_loader)
train_acc = correct / total * 100

# ==================== 驗證階段 ====================
model.eval()
val_correct = val_total = 0
val_running_loss = 0.0 # ← 新增:計算驗證 Loss

with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels) # ← 新增:計算 val loss

val_running_loss += loss.item()
_, predicted = torch.max(outputs, 1)
val_total += labels.size(0)
val_correct += (predicted == labels).sum().item()

val_loss = val_running_loss / len(val_loader) # ← 新增:平均驗證 Loss
val_acc = val_correct / val_total * 100

# 記錄 Loss(用來之後畫曲線)
train_losses.append(train_loss)
val_losses.append(val_loss)

scheduler.step()

print(f"Epoch [{epoch+1:02d}/{NUM_EPOCHS}] "
f"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | "
f"Train Acc: {train_acc:.2f}% | Val Acc: {val_acc:.2f}%")

# 儲存最佳模型(以驗證準確率為準)
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), f"{SAVE_DIR}/best_model.pth")
config = {
"model": "resnet18",
"num_classes": NUM_CLASSES,
"classes": train_dataset.classes
}
with open(f"{SAVE_DIR}/config.json", "w") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f" → 已儲存最佳模型 (Val Acc: {best_val_acc:.2f}%)")

# ==================== 訓練結束後:繪製 Loss 曲線 ====================
plt.figure(figsize=(10, 5))
plt.plot(train_losses, label="Train Loss", marker='o')
plt.plot(val_losses, label="Val Loss", marker='o', linestyle="--")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title(f"Loss Curve - {TASK_NAME} (ResNet18)")
plt.legend()
plt.grid(True)
plt.tight_layout()

# 儲存圖片到模型資料夾
plt.savefig(f"{SAVE_DIR}/loss_curve.png")
plt.show()

print(f"\n訓練完成!最佳驗證準確率:{best_val_acc:.2f}%")
print(f"Loss 曲線已儲存至:{SAVE_DIR}/loss_curve.png")


圖:記錄每個 epoch 的 Train Loss 與 Val Loss 並繪製曲線,用於診斷過擬合現象

🔸 TensorFlow/Keras

Keras 的 model.fit 會自動回傳 history 物件,直接存有每個 epoch 的 Loss,不需要手動記錄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# plot_loss_keras.py
import matplotlib.pyplot as plt

# history.history 是字典,key 為指標名稱,value 為每個 epoch 的數值清單
history = model.fit(train_ds, validation_data=val_ds, epochs=20)

plt.figure(figsize=(8, 4))
plt.plot(history.history["loss"], label="Train Loss")
plt.plot(history.history["val_loss"], label="Val Loss", linestyle="--")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.title("Training / Validation Loss")
plt.tight_layout()
plt.savefig("output/loss_curve_keras.png")
plt.show()
點擊展開 / 收合完整 train.py(含 Loss 曲線)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# train.py(Keras 版本 - 已加入 Loss 曲線記錄)
import os
import json
import tensorflow as tf
from tensorflow.keras import layers, Model
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau
import matplotlib.pyplot as plt # ← 新增:用來畫 Loss 曲線

# ── 超參數設定 ──────────────────────────────
IMG_SIZE = 224
BATCH_SIZE = 32
NUM_CLASSES = 2 # 依實際類別數修改
TASK_NAME = "cat_dog" # 換任務只改這一行
DATA_DIR = f"data/{TASK_NAME}"
SAVE_DIR = f"models/mobilenetv2/{TASK_NAME}"
# ────────────────────────────────────────────

os.makedirs(SAVE_DIR, exist_ok=True)
SAVE_PATH = f"{SAVE_DIR}/best_model.keras"

# ── 資料載入 ────────────────────────────────
train_ds = tf.keras.utils.image_dataset_from_directory(
f"{DATA_DIR}/train",
image_size=(IMG_SIZE, IMG_SIZE),
batch_size=BATCH_SIZE,
shuffle=True,
seed=42
)

val_ds = tf.keras.utils.image_dataset_from_directory(
f"{DATA_DIR}/val",
image_size=(IMG_SIZE, IMG_SIZE),
batch_size=BATCH_SIZE,
shuffle=False,
seed=42
)

class_names = train_ds.class_names
print(f"類別:{class_names}")

# 儲存類別設定
config = {
"model": "mobilenetv2",
"num_classes": NUM_CLASSES,
"classes": class_names
}
with open(f"{SAVE_DIR}/config.json", "w") as f:
json.dump(config, f, ensure_ascii=False, indent=2)

# 效能優化
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.prefetch(buffer_size=AUTOTUNE)

# ── 資料增強層 ──────────────────────────────
data_augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.1),
layers.RandomZoom(0.1),
layers.RandomBrightness(0.2),
], name="data_augmentation")

# ── 模型建立 ────────────────────────────────
base_model = MobileNetV2(
input_shape=(IMG_SIZE, IMG_SIZE, 3),
include_top=False,
weights="imagenet"
)
base_model.trainable = False

inputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))
x = data_augmentation(inputs)
x = tf.keras.applications.mobilenet_v2.preprocess_input(x)
x = base_model(x, training=False)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.3)(x)
outputs = layers.Dense(NUM_CLASSES, activation="softmax")(x)
model = Model(inputs, outputs)

model.summary()

# ── 第一階段:Feature Extraction ──────────────────
model.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]
)

callbacks_phase1 = [
ModelCheckpoint(SAVE_PATH, monitor="val_accuracy", save_best_only=True, verbose=1),
EarlyStopping(monitor="val_accuracy", patience=5, restore_best_weights=True, verbose=1),
]

print("\n── 第一階段訓練(Feature Extraction)──")
history1 = model.fit(
train_ds, validation_data=val_ds,
epochs=10, callbacks=callbacks_phase1
)

# ── 第二階段:Fine-Tuning ─────────────────────────
base_model.trainable = True
fine_tune_at = len(base_model.layers) - 30
for layer in base_model.layers[:fine_tune_at]:
layer.trainable = False

model.compile(
optimizer=tf.keras.optimizers.Adam(1e-5),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]
)

callbacks_phase2 = [
ModelCheckpoint(SAVE_PATH, monitor="val_accuracy", save_best_only=True, verbose=1),
EarlyStopping(monitor="val_accuracy", patience=5, restore_best_weights=True, verbose=1),
ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=3, verbose=1),
]

print("\n── 第二階段訓練(Fine-Tuning)──")
history2 = model.fit(
train_ds, validation_data=val_ds,
epochs=10, callbacks=callbacks_phase2
)

# ==================== 新增:繪製 Loss 曲線 ====================
print("\n繪製訓練 Loss 曲線...")

plt.figure(figsize=(10, 5))

# 合併兩個階段的 history(如果兩個階段都有訓練)
train_loss = history1.history["loss"] + history2.history["loss"]
val_loss = history1.history["val_loss"] + history2.history["val_loss"]

plt.plot(train_loss, label="Train Loss", marker='o')
plt.plot(val_loss, label="Val Loss", marker='o', linestyle="--")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title(f"Loss Curve - {TASK_NAME} (MobileNetV2)")
plt.legend()
plt.grid(True)
plt.tight_layout()

# 儲存 Loss 曲線圖
plt.savefig(f"{SAVE_DIR}/loss_curve.png")
plt.show()

print(f"訓練完成!模型已儲存至 {SAVE_PATH}")
print(f"Loss 曲線已儲存至:{SAVE_DIR}/loss_curve.png")


圖:使用 Keras history 物件繪製 Train Loss 與 Val Loss 曲線,直觀呈現訓練與驗證的差距

💻 方法一:Dropout


圖:Fig 1. Dropout (取自 Tikz/Dropout)

Dropout 在訓練時隨機將一部分神經元的輸出設為 0,迫使模型不能過度依賴特定神經元,從而提升泛化能力。

白話原理:訓練時隨機關掉一些神經元,強迫模型學得更均衡,不容易死背訓練資料。

🔸 PyTorch

PyTorch 微調範例train.py 中,分類頭原本是一個 nn.Linear
在它之前插入 nn.Dropout 即可啟用 Dropout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# dropout_pytorch.py
import torch.nn as nn
from torchvision import models

model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for param in model.parameters():
param.requires_grad = False

# 原本:model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
# 改成:用 Sequential 在 Linear 層前加入 Dropout
model.fc = nn.Sequential(
nn.Dropout(p=0.5), # 50% 的神經元隨機被丟棄
nn.Linear(model.fc.in_features, NUM_CLASSES) # 再接分類層
)

🔸 TensorFlow/Keras

TensorFlow/Keras 微調範例 的模型架構中,在全連接層之間插入 layers.Dropout

1
2
3
4
5
6
7
8
# dropout_keras.py
from tensorflow.keras import layers

x = layers.GlobalAveragePooling2D()(base_model.output)
x = layers.Dropout(0.5)(x) # 50% 的神經元隨機被丟棄
x = layers.Dense(256, activation="relu")(x)
x = layers.Dropout(0.3)(x) # 第二層 Dropout 比率稍低
outputs = layers.Dense(NUM_CLASSES, activation="softmax")(x)

💡 Dropout 只在訓練時啟用,推論時自動關閉。PyTorch 需呼叫 model.eval() 才會正確停用;Keras 則透過 training 參數自動控制。

💻 方法二:L2 正規化(Weight Decay)

L2 正規化對模型的權重大小施加懲罰,避免某些權重過大,讓模型的決策更分散、不過度依賴少數特徵。

白話原理:對太大的權重進行懲罰,讓模型的權重不會過度集中。

🔸 PyTorch

PyTorch 微調範例train.py 中,optimizer 只需加入 weight_decay 參數即可啟用:

1
2
3
4
5
6
7
# l2_pytorch.py
import torch.optim as optim

# 原本:optimizer = optim.Adam(model.fc.parameters(), lr=LEARNING_RATE)
# 改成:加入 weight_decay 啟用 L2 正規化
optimizer = optim.Adam(model.fc.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)
# weight_decay=1e-4:懲罰強度,太大會讓模型欠擬合,通常設 1e-4 ~ 1e-3

🔸 TensorFlow/Keras

TensorFlow/Keras 微調範例 的模型架構中,對 Dense 層加入 kernel_regularizer

1
2
3
4
5
6
7
8
# l2_keras.py
from tensorflow.keras import layers, regularizers

x = layers.Dense(
256,
activation="relu",
kernel_regularizer=regularizers.l2(1e-4) # 對這層的權重施加 L2 懲罰
)(x)

💻 方法三:Early Stopping

Early Stopping 監控驗證集 Loss,當連續 patience 個 epoch 都沒有改善時,自動停止訓練並恢復最佳模型,避免在過擬合的方向繼續訓練。

白話原理:如果驗證集好幾個 epoch 都沒有進步,就自動停止訓練,避免繼續過擬合。

🔸 PyTorch(手動實作)

PyTorch 微調範例train.py 訓練迴圈中,加入以下邏輯取代固定 epoch 數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# early_stopping_pytorch.py
best_val_loss = float("inf") # 初始化為無限大,任何 val_loss 都比它小
patience = 5 # 允許連續幾個 epoch 沒有改善
no_improve = 0 # 計數器:目前連續幾個 epoch 沒有改善

for epoch in range(NUM_EPOCHS):
# ... 訓練與驗證程式碼(與 train.py 相同)...

if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), f"{SAVE_DIR}/best_model.pth") # SAVE_DIR 與 train.py 相同
no_improve = 0 # 有改善,計數器歸零
else:
no_improve += 1
if no_improve >= patience:
print(f"Early stopping at epoch {epoch + 1}")
break # 停止訓練迴圈

# 訓練結束後載入驗證集表現最佳的模型(而非最後一個 epoch 的模型)
model.load_state_dict(torch.load(f"{SAVE_DIR}/best_model.pth"))

🔸 TensorFlow/Keras(使用 Callback)

Keras 的 EarlyStopping callback 自動處理所有邏輯,restore_best_weights=True 等同於 PyTorch 版本的「訓練結束後載入最佳模型」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# early_stopping_keras.py
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

callbacks = [
EarlyStopping(
monitor="val_loss", # 監控驗證集 Loss
patience=5, # 連續 5 個 epoch 沒改善就停止
restore_best_weights=True, # 停止時自動恢復最佳 epoch 的權重(不需另外 load)
verbose=1
),
ModelCheckpoint(
f"{SAVE_DIR}/best_model.keras", # SAVE_DIR 與 train.py 相同
monitor="val_accuracy",
save_best_only=True, # 只在 val_accuracy 改善時才儲存
verbose=1
),
]

# epochs 設大一點,讓 EarlyStopping 自行決定何時停止
model.fit(train_ds, validation_data=val_ds, epochs=50, callbacks=callbacks)

💻 方法四:學習率排程

學習率從大到小逐步衰減,讓模型在訓練後期更細緻地調整,避免 Loss 在最小值附近反覆震盪。

🔸 PyTorch

PyTorch 微調範例train.py 中已使用 StepLR。以下展示兩種常用排程器的用法,擇一加入 train.py 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
# lr_scheduler_pytorch.py
import torch.optim as optim
from torch.optim import lr_scheduler

optimizer = optim.Adam(model.fc.parameters(), lr=LEARNING_RATE)

# ── 方案 A:StepLR — 每隔固定 epoch 數,學習率乘以 gamma ──────────────
scheduler = lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
# 效果:初始 lr=1e-3 → 第 5 epoch 後 5e-4 → 第 10 epoch 後 2.5e-4 → ...

for epoch in range(NUM_EPOCHS):
# ... 訓練程式碼 ...
scheduler.step() # 每個 epoch 結束後呼叫,學習率按固定頻率衰減
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# lr_scheduler_plateau_pytorch.py
import torch.optim as optim
from torch.optim import lr_scheduler

optimizer = optim.Adam(model.fc.parameters(), lr=LEARNING_RATE)

# ── 方案 B:ReduceLROnPlateau — 驗證 Loss 停滯才縮小,更自適應 ──────────
scheduler = lr_scheduler.ReduceLROnPlateau(
optimizer, mode="min", factor=0.5, patience=3, verbose=True
)
# 效果:驗證 Loss 連續 3 個 epoch 沒改善時,學習率自動縮小為 0.5 倍

for epoch in range(NUM_EPOCHS):
# ... 訓練程式碼 ...
scheduler.step(val_loss) # 傳入驗證 Loss,讓排程器自行判斷是否縮小

🔸 TensorFlow/Keras

TensorFlow/Keras 微調範例callbacks 清單中加入 ReduceLROnPlateau

1
2
3
4
5
6
7
8
9
10
11
12
# lr_scheduler_keras.py
from tensorflow.keras.callbacks import ReduceLROnPlateau

callbacks = [
ReduceLROnPlateau(
monitor="val_loss", # 監控驗證集 Loss
factor=0.5, # 觸發時學習率縮小為原來的 0.5 倍
patience=3, # 連續 3 個 epoch 沒改善才觸發
min_lr=1e-7, # 學習率的下限,防止縮到幾乎為零
verbose=1
)
]

⚠️ 注意事項

  • 先增加資料,再加正規化:過擬合最根本的原因是資料不足,正規化只是輔助,無法完全取代資料量。上一篇的資料增強是第一道防線,這一篇的方法是第二道。
  • Dropout 比率不要太高0.3 ~ 0.5 是常用範圍,過高(如 0.8)會讓模型難以學習,Train Loss 也降不下來。
  • Early Stopping 的 patience 要夠patience 太小可能在模型還沒充分訓練時就停止;太大則失去提早停止的意義。通常設 5 ~ 10。
  • StepLRReduceLROnPlateau 的選擇:資料集穩定、訓練行為可預期時用 StepLR;訓練曲線不穩定、需要自動調整時用 ReduceLROnPlateau
  • scheduler.step() 的位置StepLR.step() 必須在每個 epoch 結束後呼叫;ReduceLROnPlateau.step(val_loss) 必須傳入驗證 Loss 才能判斷是否縮小。

📊 應用場景

  • 小資料集訓練:資料量少時,過擬合幾乎必然發生,需積極搭配資料增強、Dropout 與 Weight Decay 一起使用。
  • 訓練前期穩定、後期微調ReduceLROnPlateau 讓模型在收斂後期自動降低學習率,不需手動介入調整。
  • 長時間訓練監控:搭配 Matplotlib 繪製 Loss 曲線,提早發現過擬合趨勢,適時介入加入正規化技巧。
  • 模型上線前的品質把關:Early Stopping + ModelCheckpoint 確保儲存的一定是驗證集最佳模型,而非最後一個 epoch 的模型。
  • 競賽與生產模型:通常四種方法組合使用(資料增強 + Dropout + Weight Decay + Early Stopping)以得到最穩定的泛化效果。

🎯 結語

過擬合是深度學習訓練中最常遇到的問題之一,也是工程師最需要具備診斷與處理能力的環節。
掌握 Dropout、L2 正規化、Early Stopping 與學習率排程這四種核心技巧,搭配上一篇的資料增強,能讓模型的泛化能力大幅提升。
下一步是 GPU 加速與效能優化,讓訓練速度大幅提升。

📖 如在學習過程中遇到疑問,或是想了解更多相關主題,建議回顧一下 Python | OpenCV 系列導讀,掌握完整的章節目錄,方便快速找到你需要的內容。

註:以上參考了
PyTorch 官方文件 — torch.optim
TensorFlow 官方文件 — Callbacks
Deep Learning Book — Regularization