📢 公告:OpenCV 系列文章重構完成(75%)。專案實作篇仍在製作中,完成時間未定,敬請期待!→ 查看文章索引

熱門系列
Like Share Discussion Bookmark Smile

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

Python | OpenCV TensorFlow/Keras 微調範例

📚 前言

在上一篇 PyTorch 微調範例 中,我們完成了 PyTorch 的微調實作。
這一篇改用 TensorFlow/Keras 微調 MobileNetV2,學習另一個主流框架的實作方式。

TensorFlow/Keras 提供了高階 API,並建議分兩階段訓練:第一階段先凍結所有預訓練層,穩定分類頭的收斂;第二階段再解凍後幾層,以極小學習率進行 Fine-Tuning。這樣的做法比直接一次解凍更穩定,也更容易得到更好的結果。

🛠️ 環境安裝

1
pip install tensorflow


圖:執行 pip install 安裝 tensorflow 的結果

💡 以上安裝的是 CPU 版本,這篇範例使用 CPU 執行即可。GPU 加速的安裝與設定將在後續篇章介紹。

🗃️ 資料集目錄結構


圖:專案目錄結構建議 ─ 使用樹狀方式呈現原始素材、訓練資料與模型輸出的分層管理

Keras 的 image_dataset_from_directoryImageDataGenerator 都支援相同的目錄結構:

1
2
3
4
5
6
7
8
9
10
11
12
data/
└── cat_dog/
├── train/
│ ├── cat/
│ │ ├── 0001.jpg
│ │ └── ...
│ └── dog/
│ ├── 0001.jpg
│ └── ...
└── val/
├── cat/
└── dog/

☝ 這只是建議的目錄結構,並非強制規定,你可以依照自己的習慣調整。但請注意,資料夾名稱(例如 cat、dog)會被 image_dataset_from_directory 自動當作類別標籤。

如何蒐集圖片?

這篇使用與上一篇相同的 catdog 資料集,直接沿用上一篇 PyTorch 微調範例download_dataset.py 下載的結果即可,不需要重新蒐集。

清理不支援格式的圖片

TensorFlow 的圖片解碼器僅支援 JPEG、PNG、GIF、BMP 四種格式。BingImageCrawler 下載的圖片有時包含 WebP 或損壞的檔案,訓練時會拋出 Unknown image file format 錯誤,需先執行以下清理腳本:

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
# clean_dataset.py
import os
from PIL import Image

SUPPORTED_FORMATS = {"JPEG", "PNG", "GIF", "BMP"}
DATA_ROOT = "data/cat_dog"

removed = 0
for split in ["train", "val"]:
split_dir = f"{DATA_ROOT}/{split}"
if not os.path.isdir(split_dir):
continue
for cls in os.listdir(split_dir):
folder = os.path.join(split_dir, cls)
if not os.path.isdir(folder):
continue
for fname in os.listdir(folder):
fpath = os.path.join(folder, fname)
try:
with Image.open(fpath) as img:
fmt = img.format # 讀副檔名前先取實際格式
if fmt not in SUPPORTED_FORMATS:
raise ValueError(f"不支援的格式:{fmt}")
img.load() # 實際解碼,抓損壞的檔案
except Exception as e:
os.remove(fpath)
print(f"移除:{fpath}{e})")
removed += 1

print(f"清理完成,共移除 {removed} 個檔案")


圖:執行 clean_dataset.py 掃描並移除不支援格式或損壞的圖片

💡 PyTorch 的 ImageFolder 透過 PIL 讀取,PIL 原生支援 WebP,所以上一篇不會遇到此問題。TensorFlow 使用內建圖片解碼器,格式限制較嚴,每次下載新資料集後都建議先跑一次清理腳本

🧠 函式與參數說明


圖:TensorFlow 資料載入與模型控制 ─ image_dataset_from_directory 與 base_model.trainable 的使用說明

🗂️ 模型輸出目錄結構

訓練完成後,模型與類別設定存放在同一目錄:

1
2
3
4
5
models/
└── mobilenetv2/
└── cat_dog/
├── best_model.keras ← 模型權重
└── config.json ← 記錄類別數與類別名稱

config.json 內容如下:

1
2
3
4
5
{
"model": "mobilenetv2",
"num_classes": 2,
"classes": ["cat", "dog"]
}

🔎 訓練流程總覽

TensorFlow/Keras 建議分兩階段訓練,第一階段穩定分類頭後再解凍部分層進行微調:


圖:TensorFlow 模型訓練流程總覽 ─ 從資料載入到儲存最佳模型的完整六大步驟

💡 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
# train.py
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

# ── 超參數設定 ──────────────────────────────
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)

# 效能優化:prefetch
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, # 移除原本的 1000 類分類頭,只保留特徵提取器
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()

# ── 第一階段:只訓練分類頭 ──────────────────
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

# 只解凍最後 30 層
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
)

print(f"\n訓練完成,模型已儲存至 {SAVE_PATH}")


圖:分兩階段微調 MobileNetV2,第一階段只訓練分類頭,第二階段解凍後 30 層以極小學習率進行 Fine-Tuning

💻 單張圖片推論

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
# inference.py
import json
import tensorflow as tf
import numpy as np
import cv2

MODEL_DIR = "models/mobilenetv2/cat_dog" # 與 train.py 的 SAVE_DIR 對應
IMAGE_PATH = "data/cat_dog/val/cat/000001.jpg" # 替換為實際測試圖片路徑
IMG_SIZE = 224

# 從 config.json 讀取類別資訊,不需要手動填寫
with open(f"{MODEL_DIR}/config.json") as f:
config = json.load(f)
CLASS_NAMES = config["classes"]

model = tf.keras.models.load_model(f"{MODEL_DIR}/best_model.keras")

img = cv2.imread(IMAGE_PATH)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_resized = cv2.resize(img_rgb, (IMG_SIZE, IMG_SIZE))

x = np.expand_dims(img_resized, axis=0).astype(np.float32)
preds = model.predict(x, verbose=0)
pred_class = np.argmax(preds[0])
conf = preds[0][pred_class]

print(f"預測類別:{CLASS_NAMES[pred_class]},信心度:{conf:.4f}")


圖:載入已儲存的 Keras 模型對單張圖片進行推論並輸出預測類別與信心度

⚠️ 注意事項

  • 訓練前先執行 clean_dataset.py:TensorFlow 不支援 WebP,BingImageCrawler 下載的圖片常混入此格式,且部分圖片的副檔名是 .jpg 但實際內容是 WebP,純靠副檔名過濾無法完全清除。clean_dataset.py 改用 PIL 讀取實際格式(img.format)加上完整解碼(img.load())來確保所有問題檔案都被移除。
  • base_model(x, training=False):即使在訓練階段,也要對凍結的 base model 傳入 training=False,確保 BatchNorm 使用推論模式的統計量,而非當前 batch 的統計量。這是 TensorFlow/Keras 遷移學習中最容易忽略的設定。
  • preprocess_input 要與 base model 對應:MobileNetV2 的 preprocess_input 會將像素值縮放到 [-1, 1],若換用其他模型(如 ResNet50),需改用對應的 preprocess_input,否則輸入分布不對會影響特徵提取效果。
  • 兩階段訓練的學習率差距要大:第一階段用 1e-3,第二階段建議用 1e-5 甚至更小,避免破壞預訓練權重。
  • EarlyStoppingrestore_best_weights=True:確保訓練提早停止時,模型恢復到驗證最佳的狀態,而不是最後一個 epoch 的狀態。

📊 應用場景

  • 快速原型驗證:Keras 的高階 API 讓你能快速跑通流程,驗證資料與模型的可行性。
  • 輕量模型部署:MobileNetV2 模型小、速度快,適合部署在行動裝置或邊緣裝置。
  • 有限資料下的自動調參:兩階段訓練搭配 EarlyStoppingReduceLROnPlateau,能在訓練過程中自動降低學習率並及早停止,減少手動調參的負擔,特別適合資料量不足的場景。

🎯 結語

對比上一篇的 PyTorch 實作,TensorFlow/Keras 的兩階段訓練把 Feature Extraction 和 Fine-Tuning 合為一個完整流程,搭配 Callback 自動管理學習率與早停,整體更省力,但原理與上一篇完全相同。
下一篇進入 資料增強 (Data Augmentation),學習如何擴充訓練資料集,讓模型在資料量有限的情況下也能具備更好的泛化能力。

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

註:以上參考了
TensorFlow 官方文件 — Transfer Learning
TensorFlow 官方文件 — image_dataset_from_directory
Keras 官方文件 — Applications