iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
AI Engineering

從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 6

Day 06:ML 生命週期與「筆記本到產品的鴻溝」

  • 分享至 

  • xImage
  •  

ML 專案跟一般軟體專案,到底哪裡不一樣?,在開發環境的建立之後,今天要處理 ML 系統成敗的認知問題。

今天要解決的問題

先講一個 ML 系統經典的統計現象:「模型程式碼只佔 5%」

我在前系列《從 AI 落地談 MLOps》系列文,引用了2015 年 Google Hidden Technical Debt in Machine Learning Systems 經典的圖:在一個真實的 ML 系統中,那個寫著「ML Code」的方框,被設定管理、資料收集、特徵擷取、資料驗證、機器資源管理、分析工具、流程管理、服務基礎設施、監控等一大堆方框團團包圍,小得可憐。
image

十年後的今天,這張圖依然全中,堪稱經典。只是方框的名字換了幾個——多了向量索引、提示管理、評測管線。

📊 職缺訊號

這解釋了一個乍看奇怪的現象:MLOps 職缺中「模型版本與實驗管理」只佔 7.2%,而「平臺與基礎設施」佔 58.5%。不是因為模型不重要,而是因為模型周圍的東西才是工作量的主體。市場徵的不是「會調模型的人」,是「能讓模型活在系統裡的人」。


一、現象:工作量與存活率的兩張圖

ML 專案的工作量分布與存活漏斗

圖 6-2:ML 專案的工作量分布與存活漏斗(示意性推估,用於說明結構而非精確比例)。左圖:模型程式碼只佔約 5%,資料相關工作接近三成。右圖:從有想法到「持續有效」,最陡的落差發生在「跑出模型」到「上線」之間。

左圖告訴你時間花在哪,右圖告訴你專案死在哪。兩張圖合起來的結論是:

多數 AI 專案不是死於「模型不夠好」,而是死於「好模型無法穩定地送到使用者面前,並在那裡活下去」。

這正是 Day 01 那三個場景的統一解釋,也是這兩個職務存在的理由。

二、原理:ML 生命週期的六個階段與三種「回頭」

先把生命週期畫出來——注意它不是一條直線:
image

圖 6-3:ML 生命週期與三種回頭路徑(示意流程)。三條虛線代表三種不同性質的迭代,成本由高到低是「回頭 1 > 回頭 3 > 回頭 2」。

三種回頭的性質完全不同,這是面對問題的關鍵:

  • 回頭 2(指標沒過門檻 → 重新訓練):最便宜,是日常。多數人以為 ML 工作就是這一圈。
  • 回頭 3(線上漂移 → 回到資料):中等成本,是 MLOps 的主戰場。它需要「有人發現」才會發生——而這正是 Day 14 監控的價值。
  • 回頭 1(需求定義錯了):最貴。模型指標很漂亮但業務沒有變好,通常代表成功指標從一開始就訂錯

📌 一個實務上的判斷方式

專案啟動時,如果沒有人能回答「這個模型上線後,哪個業務數字會變好、變多少算成功」,那你正在製造一個未來的「回頭 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)

4.1 為什麼「模型指標」與「業務數字」常常對不起來

這是資深與初階工程師在判讀技巧中的差別之一。舉一個具體的例子:

某電商的推薦模型把點擊率預測的 AUC 從 0.82 提升到 0.87,團隊很開心。上線後,點擊率確實上升了 3%,但營收沒有變化。事後分析發現,新模型更會推薦「便宜、容易被點的商品」——使用者點得更多,但買得沒有更多。

問題不在模型,在成功指標的選擇。AUC 衡量的是「排序能力」,而業務要的是「營收」。中間隔著至少三層轉換:點擊 → 加入購物車 → 結帳 → 客單價。模型只優化了第一層。

實務上的處理方式是建立指標階層

層級 例子 誰在看 更新頻率
業務指標(北極星) 營收、留存率、客服工單減少數 主管、業務單位 週/月
產品指標(代理) 點擊率、完成率、轉人工率 PM、應用工程師
模型指標 AUC、F1、faithfulness 資料科學家、MLOps 每次訓練
系統指標 P95 延遲、錯誤率、成本 MLOps、SRE 即時

上線前要先講好:模型指標提升多少,預期會讓哪一個產品指標動、幅度多少。 如果講不出這條因果鏈,那這次上線就只是「換了一個模型」,不是「改善了產品」。這也是 Day 28 綜合專題第一個里程碑就要求寫下成功指標的原因。

⚠️ 最貴的那個錯誤:資料洩漏(Data Leakage)

症狀是「離線指標好得不像話」(例如 AUC 0.99)。常見來源:把只有事後才知道的欄位當特徵(例如用「是否已退款」預測「是否為詐欺交易」)、或前處理(標準化、填補)在切分之前就做了,讓測試集資訊滲進訓練。看到異常好的指標,先假設是洩漏,別急著慶祝。


今日小結

  • ML 系統中模型程式碼只佔約 5%,資料相關工作接近三成——這解釋了為什麼職缺在徵「平臺與基礎設施」而非「調模型」。
  • 生命週期有三種回頭:指標沒過(便宜、日常)、線上漂移(MLOps 主戰場)、需求訂錯(最貴)。
  • 一次訓練至少要產出模型+機器可讀的指標+來源紀錄,這是後續自動化品質門檻的前提。
  • 切分要三份、時間序列依時間切、指標要對應業務錯誤代價。
  • 看到好得不像話的指標,先懷疑資料洩漏

明天預告

明天是地基的最後一塊,也是主軸二讀者最需要的一塊:LLM 到底在做什麼、為什麼你的帳單長那樣。我會用兩張圖說明 token 經濟的兩個真相——為什麼「最貴的常是你以為免費的固定前綴」,以及為什麼「請簡短回答」同時省錢又提速。

延伸閱讀

  • 📄 Sculley et al., Hidden Technical Debt in Machine Learning Systems (NeurIPS 2015)——本篇的源頭,傳統軟體工程可以透過模組化、單元測試等方式管理技術債,但 ML 系統具有「軟體債」與「數據債」雙重屬性,許多隱性成本隱藏在 ML 專屬的系統風險中:
    • 糾纏性與邊界腐化(Entanglement & Boundary Erosion): ML 模型遵循「改變一個東西就改變所有東西」(CACE原則)。調整輸入特徵、軟體超參數或數據分佈,都會全盤影響模型的整體行為,使得傳統模組獨立測試失效。
    • 數據依賴性債務(Data Dependency Debt): 包含使用了不穩定特徵(Unstable Features)、未被說明的特徵(Under-utilized Features)或是跨系統的隱性數據循環依賴,這比傳統代碼依賴更難追蹤與維護。
    • 隱性反模式(System-level Anti-Patterns),包含為了銜接泛用套件而寫的大量過渡性封裝代碼(Glue Code);復雜且缺乏治理的數據準備與預處理工作流(Pipeline Jungles);留在生產環境中未清理的實驗性程式碼(Dead-End Code)。
    • 反饋迴圈(Feedback Loops): 系統輸出反過來影響未來的訓練數據(例如推薦系統引發的強化效應),導致數據偏誤隱性放大。
    • 外部世界變化(Changes in the External World): 動態環境導致數據漂移(Data Drift),需要持續進行即時監控與自動重新訓練機制。
  • 🔗 2021 鐵人賽《從 AI 落地談 MLOps》——當年我用了整整幾天談這張「ML Code 只是一小塊」的圖,五年後它依然是這條路上最重要的一張圖。

上一篇
Day 05:工程基礎最小集合——環境、Git、容器、金鑰
下一篇
Day 07:LLM 原理速成——token 經濟與成本的真相
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言