iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

資料有版本(Day 09)、實驗有紀錄(Day 10),今天加上最重要會擋人的門,把它們串成一條自動跑也安心的管線。

這是 Level 0 到 Level 1 的關鍵一步,也是 Day 08 診斷表第 1、6、7 題的內容。

今天要解決的問題

先假設一個可能發生的災難:

團隊做了自動重訓,每週一自動用最新資料重訓並部署。某個週一,上游資料管線出錯導致某個關鍵特徵全變成 0,模型照樣訓練完成(不會報錯)、照樣部署上線。到週四才有人發現,四天的預測全是垃圾。

自動化本身沒有錯,錯在自動化的路上沒有設關卡。今天要做的,就是在這條路上設三道關卡。

📊 職缺訊號

「訓練管線與 CI/CD」出現在 42.6% 的 MLOps 職缺中,是第三高的任務訊號。而在面試裡,這題常以「你會怎麼確保一個爛模型不會上線」的形式出現——答得出「品質門檻」三個字並能舉例,通常就過了。


一、現象:從一坨腳本到一條管線

先看管線該長什麼樣:

訓練管線的步驟拆解

圖 11-1:訓練管線的六個步驟與三道關卡(示意架構)。每個步驟都可獨立重試、可快取、可分配不同資源;三道關卡分別擋資料、擋模型、擋部署。

拆成步驟的效益,不是「看起來比較專業」,而是三件很實際的事:

  1. 失敗可重試:訓練跑了兩小時後在註冊階段失敗,不必從頭再來。
  2. 步驟可快取:資料沒變時,前處理的結果可直接重用。
  3. 資源可分配:資料處理用 CPU、訓練用 GPU,各要各的。

二、原理:三道關卡的分工

這是今天的核心。三道關卡擋的是不同的東西:

關卡 位置 擋什麼 沒設會怎樣
關卡一:資料驗證 訓練之前 schema 變更、缺值暴增、分布異常 用壞資料訓練出壞模型(開頭那個災難)
關卡二:品質門檻 註冊之前 指標未達標、比現行模型差、切片退化 爛模型進了生產環境
關卡三:部署驗證 上線之後 服務健康、金絲雀期間的業務指標 模型好但服務掛了

關卡二是今天的重點,它的判斷邏輯應該長這樣:

放行條件(必須全部成立):
  1. 絕對門檻:test AUC ≥ 0.85          ← 不管怎樣都不能低於這條線
  2. 相對門檻:test AUC ≥ 現行冠軍 − 0.005  ← 允許微幅波動,但不能明顯退步
  3. 切片門檻:所有通路的 AUC ≥ 0.75    ← 防止「整體好、局部爛」
  4. 行為測試:不變性測試全數通過        ← 改無關欄位不該影響預測

第 2 條的設計值得說明:為什麼是「≥ 冠軍 − 0.005」而不是「> 冠軍」?因為要求每次都更好,會讓管線在正常波動下卡死。容忍一個小的退步區間,同時擋住明顯退化,這是實務上可運作的設定。

三、動手:一份可直接抄的 CI 流程

3.1 品質門檻的判斷程式

"""gate.py —— 品質門檻。回傳非零狀態碼會讓 CI 失敗,模型就進不了 registry。"""
import json, sys
import mlflow
from mlflow import MlflowClient

ABSOLUTE_MIN = 0.85      # 絕對門檻
REGRESSION_TOLERANCE = 0.005   # 允許的退步幅度
SLICE_MIN = 0.75         # 各切片的最低要求

metrics = json.load(open("artifacts/metrics.json"))
new_auc = metrics["test"]["auc"]
failures = []

# 1) 絕對門檻
if new_auc < ABSOLUTE_MIN:
    failures.append(f"AUC {new_auc:.4f} < 絕對門檻 {ABSOLUTE_MIN}")

# 2) 相對門檻——與現行冠軍比較
client = MlflowClient()
try:
    champ = client.get_model_version_by_alias("fraud-model", "champion")
    champ_auc = client.get_run(champ.run_id).data.metrics["auc"]
    if new_auc < champ_auc - REGRESSION_TOLERANCE:
        failures.append(f"AUC {new_auc:.4f} 低於冠軍 {champ_auc:.4f} 超過容忍值")
except Exception:
    print("尚無冠軍模型,略過相對門檻(首次上線)")

# 3) 切片門檻
for name, value in metrics.get("slices", {}).items():
    if value < SLICE_MIN:
        failures.append(f"切片 {name} 的 AUC {value:.4f} < {SLICE_MIN}")

if failures:
    print("❌ 品質門檻未通過:")
    for f in failures:
        print("  -", f)
    sys.exit(1)          # ← 這一行就是「擋下來」
print(f"✅ 通過品質門檻(AUC {new_auc:.4f})")

3.2 GitHub Actions workflow

# .github/workflows/train.yml
name: 訓練與品質門檻

on:
  push:
    branches: [main]
    paths: ["src/**", "pipelines/**", "data.dvc"]   # 只有相關檔案變動才觸發
  schedule:
    - cron: "0 18 * * 0"        # 每週一凌晨 2 點(UTC+8)定期重訓
  workflow_dispatch:             # 允許手動觸發

jobs:
  train:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 安裝環境
        run: |
          curl -LsSf https://astral.sh/uv/install.sh | sh
          uv sync --frozen                      # 用鎖定版本,確保可重現

      - name: 取得資料(DVC)
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: uv run dvc pull

      - name: 程式碼檢查與單元測試
        run: |
          uv run ruff check .
          uv run pytest tests/ -v

      - name: 關卡一 — 資料驗證
        run: uv run python src/validate.py        # Day 09 的驗證邏輯

      - name: 訓練
        env:
          MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
        run: uv run python src/train_with_mlflow.py

      - name: 模型行為測試
        run: uv run pytest tests/test_model_behavior.py -v

      - name: 關卡二 — 品質門檻
        run: uv run python src/gate.py            # 沒過就在這裡停住

      - name: 通過門檻才註冊模型
        run: uv run python src/register.py

      - name: 上傳報告(無論成敗都保留,方便事後追查)
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: training-report
          path: artifacts/

3.3 模型行為測試:不只測程式碼,要測模型

"""tests/test_model_behavior.py —— 模型的「單元測試」。"""
import joblib, pandas as pd
model = joblib.load("artifacts/model.pkl")

def test_invariance_to_irrelevant_field():
    """不變性測試:改動與預測無關的欄位,結果不該變。"""
    base = pd.read_csv("tests/fixtures/sample.csv")
    modified = base.copy()
    modified["user_id"] = modified["user_id"] + 100000   # 換 ID 不該影響風險判斷
    assert (model.predict(base) == model.predict(modified)).all()

def test_directional_expectation():
    """方向性測試:金額提高,詐欺分數不該下降(符合業務常識)。"""
    base = pd.read_csv("tests/fixtures/sample.csv")
    higher = base.copy(); higher["amount"] = higher["amount"] * 10
    assert (model.predict_proba(higher)[:, 1]
            >= model.predict_proba(base)[:, 1] - 1e-6).all()

def test_no_nan_output():
    """健全性測試:任何輸入都不該產生 NaN。"""
    edge = pd.read_csv("tests/fixtures/edge_cases.csv")   # 含極值與缺值
    assert not pd.isna(model.predict_proba(edge)).any()

這三種測試分別對應三種真實的失效:模型偷學了 ID(洩漏)、模型學到與業務常識相反的關係(資料問題)、模型遇到邊界輸入就爆掉(前處理缺陷)。

四、取捨:管線工具與自動化程度

CI/CD 與環境晉升流程

圖 11-2:ML 的 CI/CD 與環境晉升流程(示意架構)。程式碼、資料、模型三軸各有觸發條件,最終匯流到同一組品質門檻。

工具 適合 代價
GitHub Actions/GitLab CI 中小團隊、訓練時間 < 6 小時 執行時間與資源受限,GPU 需自架 runner
Airflow 已有資料工程團隊、需複雜排程 維運成本高,非為 ML 設計
Prefect/ZenML 想要 Python 原生、輕量的管線 生態較小
Kubeflow Pipelines 已有 K8s、需要 GPU 排程 學習曲線陡,小團隊過重
雲端託管(Vertex/SageMaker Pipelines) 已在該雲、要少維運 綁定平台

自動化程度的取捨更重要:

自動化程度 建議
自動訓練 + 自動註冊 + 人工按鈕部署 多數團隊的最佳落點
全自動(含自動部署) 只在監控與回滾都成熟後才做(Day 14)
全手動 只適合一年更新一兩次的模型

4.1 品質門檻最難的不是技術,是「門檻訂多少」

gate.py 只要半小時,但決定「AUC 門檻訂在 0.85」這個數字,往往要吵很久。給三個實務上可用的定法:

定法一:從業務錯誤代價回推。 詐欺偵測中,漏掉一筆詐欺的損失是攔錯一筆正常交易的 50 倍,那門檻就該偏向高召回率,而不是追求整體 AUC。先算出兩種錯誤的成本比,再回推指標與閾值,這是最有說服力的定法。

定法二:從現況回推。 如果現在是人工審核,先量測人工的準確率當基準線——模型至少要不比人差。多數團隊跳過這一步,結果上線後才發現「模型 85 分,但人工其實有 92 分」。

定法三:從歷史波動回推。 把過去 10 次訓練的指標畫出來,看它的自然波動範圍。若歷史波動是 ±0.008,那容忍值訂 0.005 就會經常誤擋;訂 0.015 又太鬆。容忍值應該略大於歷史波動,略小於你在意的退步幅度。

三種定法可以併用,但一定要寫下理由——寫進 gate.py 的註解或 ADR(Day 28)。因為半年後一定有人會問「為什麼是 0.85」,而「當初這樣訂的」不是答案。

4.2 給主軸二讀者:這一天的對應物

生成式 AI 應用沒有訓練管線,但完全有 CI 與品質門檻,而且更需要:

  • 關卡一(資料驗證)→ 索引建置驗證:文件切塊數、嵌入維度、索引筆數是否合理,有沒有大量空切塊。
  • 關卡二(品質門檻)→ 評測集 gate:改了提示、換了模型、調了檢索參數,都要跑一次評測集,未達門檻不得發布(Day 24 會實作)。
  • 關卡三(部署驗證)→ 金絲雀 + 線上回饋:新版本先給 10% 流量,比對答對率與轉人工率。

差別在於:傳統 ML 的門檻是一個數字(AUC ≥ 0.85),生成式 AI 的門檻是一組數字(忠實度、答對率、格式合規率、注入測試通過率),而且其中有些要靠 LLM 評審給分——這讓 gate 本身變得更複雜,但邏輯完全相同。

⚠️ 關於「全自動」的建議

很多人把「全自動部署」當成 MLOps 的終極目標。但實務上,保留一個人工按鈕的成本極低,而它能擋掉的災難很大。我的建議是:讓自動化做完所有「累但沒有判斷」的事(拉資料、訓練、評估、產報告、註冊),把最後一步「要不要換掉現在的冠軍」留給人——直到你的監控能在 10 分鐘內告訴你出事了,再考慮拿掉那個按鈕。


今日小結

  • 管線化的效益是可重試、可快取、可分配資源,不是形式上的專業感。
  • 三道關卡分工明確:資料驗證(訓練前)、品質門檻(註冊前)、部署驗證(上線後)。
  • 品質門檻要有四個條件:絕對門檻、相對門檻(容忍小波動、擋明顯退步)、切片門檻、行為測試。
  • 模型也要寫測試:不變性、方向性、健全性三種,分別對應洩漏、資料問題、前處理缺陷。
  • 自動化的最佳落點是「自動到註冊,部署留人工按鈕」,直到監控成熟為止。

限制與提醒

  • 在模型與業務邏輯高度未定型的階段,強制建置包含切片與三類行為測試的 CI,會大幅拉長初期探索迭代週期,請依據專案情形斟酌運用。
  • 相對門檻可能導致「指標鈍化向下漂移」:若每次訓練都允許容忍值($-0.005$)並覆蓋成為新冠軍,長期連續重訓可能累積出顯著的退化,需搭配全域基準校準。
  • LLM 評估的非確定性未納入 Gate 容錯考量:在 GenAI 情境中,LLM-as-a-Judge 本身存在評分變異與幻覺,若無足夠的評測樣本數與統計顯著性檢驗,CI Gate 容易出現假警報。

明天預告

管線會跑了,但它跑在哪裡?明天進入 MLOps 職缺中出現頻率最高的領域(58.5%):容器與 Kubernetes。我會聚焦在 ML 服務特有的坑——GPU 排程,以及那個讓無數人以為服務壞掉、其實只是探針設定錯誤的假故障。

延伸閱讀


上一篇
Day 10:實驗追蹤與模型註冊—MLflow 與 champion/challenger
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言