昨天資料有了版本。今天處理另外兩根柱子:這次訓練用了什麼設定、跑出什麼結果、產出的模型在哪裡。
這是 Day 08 診斷表第 3、5 題的內容,也是導入成本最低、痛感解除最快,投資報酬率最高的一天。
三個熟悉的場景:
場景一:Excel 實驗紀錄。 有人在共用 Excel 上記「模型 A:lr=0.01,AUC=0.87」。兩週後有人問「那個 0.89 的是怎麼跑出來的」——沒人記得,因為那次忘了記。
場景二:模型檔案考古學。 資料夾裡躺著 model.pkl、model_v2.pkl、model_final.pkl、model_final_真的.pkl。沒有人知道線上跑的是哪一個。
場景三:回滾要改程式碼。 新模型上線後效果不好要回滾,發現服務程式碼裡寫死了 joblib.load("model_v3.pkl"),要改程式碼、重建映像、重新部署——在事故當下,這三個步驟每一步都是風險。
📊 職缺訊號
「模型版本與實驗管理」在 MLOps 職缺中只佔 7.2%,是六項任務中最低的。但別誤讀這個數字——它低,是因為多數團隊還沒走到這一步(Day 08 的成熟度分布),不是因為不重要。在面試中,能講清楚 champion/challenger 機制的候選人,通常會被判定為「有實際上線經驗」。
一次訓練要記錄的東西,可以整理成四類:

圖 10-1:實驗追蹤系統的元件與資料流(示意架構)。Tracking 記錄過程、Registry 管理產出、Artifacts 存放檔案,服務端只透過 Registry 取用模型。
| 類別 | 內容 | 為什麼要記 |
|---|---|---|
| 參數(params) | 學習率、樹的數量、資料版本 tag、seed | 重現的必要條件 |
| 指標(metrics) | AUC、F1、loss 曲線、各切片的表現 | 比較與門檻判斷 |
| 產出(artifacts) | 模型檔、混淆矩陣圖、特徵重要度、metrics.json |
交付與稽核 |
| 來源(tags) | Git commit、資料雜湊、執行者、環境 | 出事時的追溯線索 |
第四類最常被忽略,卻最重要。 半年後模型出問題,你要能回答「這個模型是誰、用哪個 commit、哪份資料、什麼時候訓練出來的」——這四個問題答不出來,就無法判斷責任與修復方向。
模型註冊表(Model Registry)解決的是場景三:回滾不該需要改程式碼。

圖 10-2:模型註冊表的別名機制(示意流程)。服務端只認 @champion 這個別名,晉升與回滾都是「移動別名」,不需要改程式碼或重新部署。
這個設計的價值在於把「哪個版本是現行版」的決策,從程式碼移到了註冊表。事故當下,回滾是一行指令,而不是一次部署。
uv add mlflow scikit-learn
mlflow server --host 127.0.0.1 --port 5000 # UI 在 http://127.0.0.1:5000
"""train_with_mlflow.py —— 把 Day 06 的訓練腳本升級成可追溯的版本。"""
import json, subprocess, hashlib
from pathlib import Path
import mlflow, mlflow.sklearn
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, f1_score
from sklearn.model_selection import train_test_split
mlflow.set_tracking_uri("http://127.0.0.1:5000")
mlflow.set_experiment("fraud-detection")
SEED = 42
params = {"n_estimators": 300, "max_depth": 12, "random_state": SEED}
df = pd.read_csv("data/train.csv")
X, y = df.drop(columns=["label"]), df["label"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2,
random_state=SEED, stratify=y)
with mlflow.start_run(run_name="rf-baseline") as run:
# 1) 參數
mlflow.log_params(params)
# 2) 來源標籤——出事時的追溯線索,這幾行最容易被省略,也最不該省
mlflow.set_tags({
"git_commit": subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"]).decode().strip(),
"data_sha256": hashlib.sha256(
Path("data/train.csv").read_bytes()).hexdigest()[:16],
"data_version": "data-v2", # 對應 Day 09 的 Git tag
})
model = RandomForestClassifier(**params).fit(X_tr, y_tr)
# 3) 指標——除了整體,一定要記「切片」指標
proba = model.predict_proba(X_te)[:, 1]
mlflow.log_metrics({
"auc": roc_auc_score(y_te, proba),
"f1": f1_score(y_te, (proba > 0.5).astype(int)),
})
for ch in X_te["channel"].unique(): # 分通路看,避免整體好、局部爛
m = X_te["channel"] == ch
mlflow.log_metric(f"auc_{ch}", roc_auc_score(y_te[m], proba[m]))
# 4) 模型——帶簽章(輸入輸出 schema),部署時可自動驗證
mlflow.sklearn.log_model(
model, name="model",
signature=mlflow.models.infer_signature(X_te, proba),
input_example=X_te.head(3),
)
print("run_id:", run.info.run_id)
💡 切片指標是資深工程師的標記
只看整體 AUC 會漏掉「整體 0.89、但 LINE 通路只有 0.62」這種問題。上線後被抱怨的往往就是那個弱勢切片。多寫三行 for 迴圈,省下三個月後的一場會議。
from mlflow import MlflowClient
client = MlflowClient()
# 通過品質門檻後才註冊(門檻邏輯見 Day 11)
mv = mlflow.register_model(f"runs:/{run_id}/model", "fraud-model")
# 新版本先當挑戰者
client.set_registered_model_alias("fraud-model", "challenger", mv.version)
# 金絲雀驗證通過後晉升為冠軍——這一行就是「上線」
client.set_registered_model_alias("fraud-model", "champion", mv.version)
import mlflow.pyfunc
# 服務端永遠這樣寫,程式碼裡不出現版本號
model = mlflow.pyfunc.load_model("models:/fraud-model@champion")
predictions = model.predict(features_df)
回滾就是把別名指回舊版本,一行指令、不需重新部署:
mlflow models set-alias -m fraud-model --alias champion --version 6
| 工具 | 適合 | 注意 |
|---|---|---|
| MLflow | 開源、自架、要完整的 Registry | 需自己維運(DB+物件儲存);生態最完整 |
| Weights & Biases | 深度學習、重視視覺化與協作 | SaaS 為主,資料出境需評估(Day 27) |
| 雲端託管(SageMaker/Vertex/Azure ML) | 已在該雲上、要少維運 | 綁定該雲,遷移成本高 |
| TensorBoard | 只需看訓練曲線 | 不是實驗管理,沒有 Registry |
三個常見誤區:
實驗追蹤的價值,多數人只看到「方便比較」。但它真正的複利在團隊記憶。
具體場景:新人加入團隊,問「為什麼我們用 XGBoost 不用深度學習」。如果有完整的實驗紀錄,答案是打開 UI 給他看——半年前試過三種深度模型,指標沒有更好但訓練時間多五倍,紀錄都在。如果沒有紀錄,答案就變成「以前試過,好像不行」,於是新人半信半疑,三個月後又試了一次同樣的東西。
沒有實驗紀錄的團隊,會週期性地重複同樣的失敗。 這個成本不會出現在任何報表上,但它是真實存在的。
同樣的道理適用於「為什麼這個超參數是這個值」。有紀錄時,答案是「搜過 40 組,這組最好,證據在此」;沒紀錄時,答案是「一直都是這樣」——而後者正是技術債的標準語法。
給主軸二讀者的補充:如果你不訓練模型,這一天還有用嗎?有,但要換個對應:
| 傳統 ML 的概念 | 生成式 AI 應用的對應物 |
|---|---|
| 超參數(學習率、樹深) | 模型名稱、temperature、top_p、檢索的 top-k |
| 訓練資料版本 | 檢索索引版本、切塊策略版本 |
| 模型權重 | 系統提示 + few-shot 範例 + 工具定義 |
| 測試集指標 | 評測集分數(faithfulness、答對率、格式合規率) |
| 模型註冊表的 champion | 目前線上採用的「提示+模型+索引」組合 |
你依然需要一份紀錄,回答「這個版本的組合,在評測集上跑出什麼分數」。 只是工具從 MLflow 換成了 Langfuse、promptfoo 或一份自己維護的表格。Day 24 與 Day 26 會完整處理這條線。
⚠️ 導入建議
如果你的團隊還在 Excel 階段,不要一次導入完整方案。第一週只做一件事:把
mlflow.autolog()加進現有訓練腳本(一行),讓大家先感受到「所有實驗自動被記下來」的好處。等到有人開始主動查 UI,再談 Registry 與別名。工具導入的失敗,九成是因為一次要求太多。
@champion 別名,晉升與回滾都是移動別名,不需改程式碼、不需重新部署。autolog() 開始,別一次要求全套。推論服務快取與連線更新延遲:服務端雖然寫 models:/fraud-model@champion,但在分散式架構或高吞吐推論服務下,各 Pod/Worker 何時重新載入新權重、如何保證載入過程不中斷請求(Graceful Reload),本文未著墨相關冷啟動與快取失效問題。
資料與環境漂移的不可重現性:僅記錄 Git Commit 與資料 SHA256,若底層 C 函式庫、CUDA 驅動或非固定隨機性的第三方套件有變動,單靠 MLflow 標籤仍可能無法 100% 精確重現數值,真實設計時請留意。
GenAI 評測維度與即時追蹤成本:將傳統 ML 指標直接對應到 GenAI 的 LLM-as-a-Judge 評測集,本文未考慮 LLM 評測本身的成本、非確定性以及動態真實流量下的 Trace 複雜度。
有了資料版本與模型版本,明天把它們串成一條可以自動跑的管線,並加上這個系列最重要的一道關卡:品質門檻——讓不夠好的模型根本進不了生產環境。我會給一份可直接抄的 GitHub Actions workflow。
stage 已是舊做法