ML 專案跟一般軟體專案,到底哪裡不一樣?,在開發環境的建立之後,今天要處理 ML 系統成敗的認知問題。
先講一個 ML 系統經典的統計現象:「模型程式碼只佔 5%」。
我在前系列《從 AI 落地談 MLOps》系列文,引用了2015 年 Google Hidden Technical Debt in Machine Learning Systems 經典的圖:在一個真實的 ML 系統中,那個寫著「ML Code」的方框,被設定管理、資料收集、特徵擷取、資料驗證、機器資源管理、分析工具、流程管理、服務基礎設施、監控等一大堆方框團團包圍,小得可憐。
十年後的今天,這張圖依然全中,堪稱經典。只是方框的名字換了幾個——多了向量索引、提示管理、評測管線。
📊 職缺訊號
這解釋了一個乍看奇怪的現象:MLOps 職缺中「模型版本與實驗管理」只佔 7.2%,而「平臺與基礎設施」佔 58.5%。不是因為模型不重要,而是因為模型周圍的東西才是工作量的主體。市場徵的不是「會調模型的人」,是「能讓模型活在系統裡的人」。

圖 6-2:ML 專案的工作量分布與存活漏斗(示意性推估,用於說明結構而非精確比例)。左圖:模型程式碼只佔約 5%,資料相關工作接近三成。右圖:從有想法到「持續有效」,最陡的落差發生在「跑出模型」到「上線」之間。
左圖告訴你時間花在哪,右圖告訴你專案死在哪。兩張圖合起來的結論是:
多數 AI 專案不是死於「模型不夠好」,而是死於「好模型無法穩定地送到使用者面前,並在那裡活下去」。
這正是 Day 01 那三個場景的統一解釋,也是這兩個職務存在的理由。
先把生命週期畫出來——注意它不是一條直線:
圖 6-3:ML 生命週期與三種回頭路徑(示意流程)。三條虛線代表三種不同性質的迭代,成本由高到低是「回頭 1 > 回頭 3 > 回頭 2」。
三種回頭的性質完全不同,這是面對問題的關鍵:
📌 一個實務上的判斷方式
專案啟動時,如果沒有人能回答「這個模型上線後,哪個業務數字會變好、變多少算成功」,那你正在製造一個未來的「回頭 1」。這一題答不出來就別開始寫程式——這是 Day 28 綜合專題的第一個里程碑就在做需求界定的原因。
多數 Notebook 的問題不是程式碼爛,是沒有產出可交付的東西。一次訓練至少要吐出三個檔案:模型、指標、以及「這次訓練是怎麼跑的」。
"""train.py —— 可交付的最小訓練腳本。
與 Notebook 的差別:固定種子、切分明確、產出可被機器讀取的 metrics。
"""
import json, hashlib, platform
from pathlib import Path
import joblib
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
SEED = 42 # 固定種子以便重現
OUT = Path("artifacts"); OUT.mkdir(exist_ok=True)
df = pd.read_csv("data/train.csv")
X, y = df.drop(columns=["label"]), df["label"]
# 先切出測試集並「鎖起來」——它只在最後用一次,不參與任何調參
X_tmp, X_test, y_tmp, y_test = train_test_split(
X, y, test_size=0.2, random_state=SEED, stratify=y)
X_tr, X_val, y_tr, y_val = train_test_split(
X_tmp, y_tmp, test_size=0.25, random_state=SEED, stratify=y_tmp)
model = RandomForestClassifier(n_estimators=300, random_state=SEED, n_jobs=-1)
model.fit(X_tr, y_tr)
def evaluate(X_, y_):
p = model.predict_proba(X_)[:, 1]
return {"auc": round(roc_auc_score(y_, p), 4),
"f1": round(f1_score(y_, (p > 0.5).astype(int)), 4)}
metrics = {"val": evaluate(X_val, y_val), "test": evaluate(X_test, y_test)}
# 產出三件套:模型、指標、來源紀錄(誰、用什麼資料、什麼環境跑的)
joblib.dump(model, OUT / "model.pkl")
(OUT / "metrics.json").write_text(json.dumps(metrics, indent=2))
(OUT / "run_meta.json").write_text(json.dumps({
"seed": SEED,
"data_sha256": hashlib.sha256(Path("data/train.csv").read_bytes()).hexdigest()[:16],
"n_train": len(X_tr), "n_val": len(X_val), "n_test": len(X_test),
"python": platform.python_version(),
}, indent=2))
print(json.dumps(metrics, indent=2))
為什麼 metrics.json 這麼重要? 因為它讓「模型好不好」變成機器可讀的——Day 11 的 CI 品質門檻就是讀這個檔案來決定要不要放行。人眼看 Notebook 輸出,是無法自動化的。
為什麼要記 data_sha256? 因為三個月後你會問「當初那個 0.92 是用哪份資料跑的」。這是 Day 09 資料版本控制的最小可行版本。
| 常見做法 | 問題 | 該怎麼做 |
|---|---|---|
| 只切訓練/測試兩份 | 用測試集調參 → 測試集被「看過」,估計過於樂觀 | 切三份,測試集全程只用一次 |
| 隨機切分時間序列資料 | 時間洩漏:用未來預測過去,線上必崩 | 依時間切分,訓練用舊資料、測試用新資料 |
| 只看 accuracy | 類別不平衡時毫無意義(99% 正常樣本,全猜正常就 99 分) | 看 AUC、F1、precision/recall,並依業務錯誤代價選擇 |
| 追求指標極大化 | 上線後常常沒有變好 | 先問「哪個業務數字會變好」,模型指標只是代理指標 |
| 門檻由人肉判斷 | 無法自動化、每次標準不一 | 把門檻寫成程式讀得到的規則(Day 11) |
這是資深與初階工程師在判讀技巧中的差別之一。舉一個具體的例子:
某電商的推薦模型把點擊率預測的 AUC 從 0.82 提升到 0.87,團隊很開心。上線後,點擊率確實上升了 3%,但營收沒有變化。事後分析發現,新模型更會推薦「便宜、容易被點的商品」——使用者點得更多,但買得沒有更多。
問題不在模型,在成功指標的選擇。AUC 衡量的是「排序能力」,而業務要的是「營收」。中間隔著至少三層轉換:點擊 → 加入購物車 → 結帳 → 客單價。模型只優化了第一層。
實務上的處理方式是建立指標階層:
| 層級 | 例子 | 誰在看 | 更新頻率 |
|---|---|---|---|
| 業務指標(北極星) | 營收、留存率、客服工單減少數 | 主管、業務單位 | 週/月 |
| 產品指標(代理) | 點擊率、完成率、轉人工率 | PM、應用工程師 | 日 |
| 模型指標 | AUC、F1、faithfulness | 資料科學家、MLOps | 每次訓練 |
| 系統指標 | P95 延遲、錯誤率、成本 | MLOps、SRE | 即時 |
上線前要先講好:模型指標提升多少,預期會讓哪一個產品指標動、幅度多少。 如果講不出這條因果鏈,那這次上線就只是「換了一個模型」,不是「改善了產品」。這也是 Day 28 綜合專題第一個里程碑就要求寫下成功指標的原因。
⚠️ 最貴的那個錯誤:資料洩漏(Data Leakage)
症狀是「離線指標好得不像話」(例如 AUC 0.99)。常見來源:把只有事後才知道的欄位當特徵(例如用「是否已退款」預測「是否為詐欺交易」)、或前處理(標準化、填補)在切分之前就做了,讓測試集資訊滲進訓練。看到異常好的指標,先假設是洩漏,別急著慶祝。
明天是地基的最後一塊,也是主軸二讀者最需要的一塊:LLM 到底在做什麼、為什麼你的帳單長那樣。我會用兩張圖說明 token 經濟的兩個真相——為什麼「最貴的常是你以為免費的固定前綴」,以及為什麼「請簡短回答」同時省錢又提速。