模型監控:資料漂移、預測漂移,
與什麼時候該重訓
模型上線後最危險的事不是壞掉——壞掉會噴 500、警報會響、有人會被叫起來。危險的是它還在回答,只是答得越來越爛: 沒有例外、儀表板一片綠,準確率卻從 0.916 滑到 0.864,你要等季末業務抱怨才知道。 先玩玩看:把兩個特徵推歪,看監控指標怎麼反應——
每一根柱子都是 notebook 真的算出來的 PSI(12 個特徵 × 81 種漂移組合全部實跑過), accuracy 那一格在真實世界要等標籤回來才看得到——這堂課大半的功夫,就花在「還看不到它」的那段時間裡。
症狀看得到,病因要等
| 漂移 | 變的是什麼 | 什麼時候看得到 |
|---|---|---|
| 資料漂移 data / covariate drift | 輸入 X 的分佈變了 | 馬上——有輸入就算得出來 |
| 預測漂移 prediction drift | 模型輸出的機率分佈變了 | 馬上——模型有在跑就算得出來 |
| 概念漂移 concept drift | 輸入與標籤的關係變了 | 要等標籤——可能好幾週 |
前兩種是症狀(免費、即時、不需要標籤),第三種是病因(要等,而且常常等很久)。 監控的全部藝術就在這句話裡:用看得到的症狀,去猜看不到的病因,而且要在損失擴大之前決定要不要重訓。
這堂課的前導課用模擬曲線給你看過那條下滑的準確率(為什麼需要 MLOps), 而且做過一個很重要的對照:只有資料漂移時準確率 0.945 幾乎不掉,概念漂移時卻掉到 0.660—— 輸入變了不一定有事,輸入沒變也不一定沒事。這一課是它的工具版:同一件事,換成正式環境真的會用的指標與自動化。
素材沿用整個系列:2000 筆客戶流失資料、12 個特徵、RandomForest champion(accuracy 0.916、AUC 0.9684)。 我們刻意造一份漂移過的生產資料:f0 整欄平移 +1.5、f3 整欄放大 2 倍。 真實世界的漂移長這樣:上游把公分換成公尺、行銷把客群從 25 歲拉到 45 歲、某個欄位的預設值從 0 改成 NaN。 列數一樣、欄位一樣、沒有缺值,程式一個錯都不會報。
到 notebook 的 0️⃣ 節:champion 模型與那批怪怪的生產資料先自己算,才知道工具在算什麼
PSI(Population Stability Index)只有三步:用參考視窗的十分位數分箱、算兩邊落在每箱的比例、 把「比例差 × 比例的倍數變化」加總。兩個細節是血淚換來的:兩端要開放, 否則平移過的資料落不進任何箱子;比例要加一個極小值,否則只要有一箱是空的, log(0) 會讓整個 PSI 變成 inf(實測:f0 平移 8 不加 1e-6 就是 inf,加了是 12.434)。
最重要的一步:跑一組對照
| 參考視窗 → 當前視窗 | 最大 PSI | 超過 0.1 的欄數 |
|---|---|---|
| 訓練集 1500 → 沒動過的 test 500(對照組) | 0.082(f4) | 0 / 12 |
| 訓練集 1500 → 漂移過的生產資料 500 | 0.558(f3)、0.487(f0) | 2 / 12 |
對照組的 PSI 不會是 0——兩份資料本來就是不同抽樣。這個「雜訊底線」(這裡是 0.082) 才是門檻該訂在哪裡的依據。業界慣例的 0.1/0.25 是起點,不是定理: PSI 會隨視窗大小變動(實測同一個小漂移,視窗 10000 筆時 PSI 0.010、縮到 50 筆時衝到 0.243), 所以視窗大小一改,門檻全部要重新校準。
KS 檢定:問錯問題的好工具
KS 檢定量的是兩條累積分佈曲線的最大垂直距離,p 值回答「假設兩邊來自同一個分佈,看到這麼大差距的機率」。 問題是它回答的不是你想問的問題:它問「有沒有差異」,你想知道的是「差異大不大、要不要管」。 實測對照組裡 f3(p 0.0400)與 f7(p 0.0429)兩欄都低於 0.05—— 照「p < 0.05 就警報」寫,這兩欄今天就會叫你起床。更糟的是樣本越多它越吵: 完全沒漂移的資料,視窗 50 筆時 p = 0.92,10000 筆時 p 已經到 1e-05。
實務結論:用效果量(PSI/Wasserstein)當主判準、統計檢定當輔助,而且兩者一起看—— p 值小而且 PSI 大,才值得處理。
到 notebook 的 1️⃣2️⃣ 節:分箱表、對照組、視窗大小實驗沒有標籤,也有東西可以看
輸入有 12 欄,實務上可能有 300 欄,逐欄看很吵。預測漂移直接看模型的輸出:一欄就好、 不需要標籤、而且直接對應到業務影響(判為流失的比例掉了 15%,行銷名單就短了 15%)。 實測模型變得沒那麼有把握:高信心(>0.8)的客戶從 34% 掉到 26%。
⚠️ 參考分佈不能用訓練集的預測。模型看過訓練集,在上面的機率會過度自信。 實測拿訓練集預測當參考、測試集預測當當前,PSI 是 0.183——資料一個字都沒漂移, 分數卻比真的漂移(0.166)還高。參考視窗選錯,整套監控就是白做的。
| 訊號 | 這次的數字 | 什麼時候拿得到 |
|---|---|---|
| 資料漂移(最大 PSI) | 0.558(f3) | 立刻 |
| 預測漂移(PSI) | 0.166 | 立刻 |
| accuracy | 0.916 → 0.864 | 等標籤,可能好幾週 |
標籤回來之後才看得到的那一列:accuracy 掉了 5.2 個百分點,500 個客戶裡多判錯 26 個。 但注意 AUC 只從 0.9684 掉到 0.9575——排序能力幾乎沒壞,壞的是校準: 模型還是知道誰比較可能流失,只是「多少機率算高」那條線跑掉了。這種情況調門檻常常就能救回大半, 不一定要重訓——也就是為什麼警報之後還要有一個人去看一眼。
到 notebook 的 3️⃣ 節:預測分佈疊圖與 in-sample 陷阱三行出報告,但預設值不是真理
Evidently 是目前最常見的開源漂移監控套件,它幫你做三件事:依欄位型別自動挑方法、 產出可以拿去開會的 HTML 報告、把結果變成可程式判讀的字典。 預設方法是 Wasserstein distance(normed)、門檻 0.1。
| 被判漂移的欄數 | 最高分 | 第二高 | |
|---|---|---|---|
| 實驗組(真的漂移了) | 3 / 12 | f3 0.813 | f0 0.695 |
| 對照組(什麼都沒發生) | 3 / 12 | f0 0.114 | f2 0.111 |
兩組的「被判漂移欄數」一模一樣。用預設門檻,一份完全沒漂移的資料照樣被判 3 欄漂移。 差別完全在分數的量級(0.813 vs 0.114)。三句話帶走: 「幾欄超標」是資訊量最低的指標,卻是最多人拿來當警報條件的那一個; 任何工具的預設門檻都要用自己的「已知正常」資料校準過;對照組不是可選的。
Wasserstein 量的是「把一堆土從參考分佈搬成當前分佈平均要搬多遠」,PSI 量的是「分箱後每箱比例變了幾倍」。 整欄平移兩者都抓得到;某一小群客戶忽然消失,PSI 反應大、Wasserstein 幾乎不動。 換方法只要 ValueDrift(column="f3", method="psi")——但實測它算出 0.973, 我們自己算是 0.558(分箱策略不同):門檻永遠綁在某一個實作上,換工具就要重新校準。
那份 HTML 報告實測 4.3 MB(互動圖表全部內嵌,所以打開不用網路)——這個大小塞進 notebook 會讓頁面明顯變重, 所以 notebook 裡是存成檔案、用瀏覽器開。
到 notebook 的 4️⃣ 節:Evidently 報告、對照組誤判、換方法監控的產出不是數字,是一個動作
三個零件缺一不可:
- 分數門檻兩級(watch / alarm),而且要用自己的對照組校準過
- 連續 N 次——單一視窗超標很可能只是雜訊
- 人工確認——retrain 是開工單,不是自動開始重訓
第三點最常被跳過。重訓要算力、要驗證、要重走一次上線流程;而且有些漂移的正確處理方式根本不是重訓—— 是去修上游那個把公分改成公尺的服務。
跑 8 個星期看看
| 週 | 最大 PSI | 最吵的欄 | 連續超標 | 決策 |
|---|---|---|---|---|
| 1 | 0.108 | f4 | 0 | watch |
| 2 | 0.130 | f4 | 0 | watch |
| 3 | 0.086 | f4 | 0 | ok |
| 4–5 | 0.156 / 0.150 | f4 / f3 | 0 | watch |
| 6 | 0.352 | f3 | 1 | watch |
| 7 | 0.431 | f3 | 2 | watch |
| 8 | 0.526 | f3 | 3 | retrain |
第 1 週就出現 watch,但那一週一點漂移都還沒注入(最吵的是我們從頭到尾沒碰過的 f4)—— 純粹是重抽樣的雜訊。規則若是「超過 0.1 就重訓」,第一週就白重訓一次。 第 6 週首次越過 0.25,但要到第 8 週連續第 3 次才觸發:代價是晚了兩週動手。 這就是監控的核心取捨——靈敏度 vs 誤報率,沒有免費的午餐。 你能做的是把它寫成明確的參數(alarm、need、視窗大小),而不是留在某個人的直覺裡。
到 notebook 的 5️⃣ 節:decide()、8 週模擬、決策曲線WARN 提醒、ERROR 擋路、sensor 叫人
監控寫成 notebook 只是分析,寫進管線才是維運。用第 3、4 課的 Dagster,監控就是 production_batch → drift_report → scored_output 三個資產,加上掛在中間那個資產上的兩個檢查。 兩個檢查對應監控的兩種語氣:
| psi_watch | psi_alarm | |
|---|---|---|
| severity / blocking | WARN / False | ERROR / True |
| 沒過的時候 | run 仍然成功,帳本留一筆黃色紀錄 | run 失敗、下游不執行 |
| 用來表達 | 「有點怪,之後查」 | 「這批分數不能用」 |
實測兩次執行:乾淨資料 → run 成功、三個資產全出(max_psi 0.082);漂移資料 → run 失敗、 scored_output 沒有產出(max_psi 0.558)。這正是你要的行為: 輸入已經不可信時,寧可今天沒有分數,也不要一批錯的分數流進業務系統。錯誤原文是 DagsterAssetCheckFailedError: 1 blocking asset check failed with ERROR severity: drift_report: psi_alarm。
資產檢查會擋、會叫人,但不會觸發重訓——那是 sensor 的工作。notebook 裡用 evaluate_tick 直接跑五次 tick(不用起 daemon),漂移一次比一次嚴重, 到第 5 次才送出帶著 max_psi 標籤的 RunRequest。 把它的目標換成第 5 課那條訓練管線,就是完整閉環: 監控發現漂移 → 送出重訓請求 → 訓練 → 評估 → 品質閘 → 通過才移動 @champion。 品質閘還在——重訓出來的模型一樣要通過檢查,否則你只是把「模型變差」自動化了。
監控結果也要留下來:每個視窗的分數用 mlflow.log_metric("max_psi", v, step=週) 記成一條時間序列, client.get_metric_history() 讀回來就能問「最近 3 個視窗是不是都超標」—— decide() 要的 streak 不必自己另外存一份狀態。
到 notebook 的 6️⃣ 節:兩個檢查、五次 sensor tick、MLflow 漂移曲線API 上線之後,儀表板上要有哪四類數字
| 記什麼 | 為什麼 | 出事時的樣子 |
|---|---|---|
| 延遲 p50 / p95 / p99 | 平均值會騙人 | 平均 7 ms 很漂亮,p99 是 3 秒 |
| 錯誤率(依狀態碼) | 400 跟 500 是不同故障 | 400 暴增=上游格式變了;500 暴增=服務壞了 |
| 輸入摘要(每欄筆數/平均/缺值率) | 原始輸入不能全存 | 存摘要就夠算 PSI,而且不必留客戶原始資料 |
| 預測摘要(平均機率、判正率) | 最省事的早期警報 | 判正率 52% → 44%,不用等標籤就知道有事 |
前兩類是軟體維運,任何 API 都要有;後兩類是機器學習特有的——模型不會拋例外,它只會安靜地越答越爛。 重點是後兩類要每個視窗存一列:監控要的不是原始請求,是可以跟參考視窗比較的摘要。 一天一列、每列幾十個數字,存一年也只是幾百 KB。
notebook 不起伺服器,直接量模型本身的延遲(這是 API 延遲的下限,真正的 API 還要加上 HTTP 與 schema 驗證): 單筆請求實測 p50 約 7 ms、p99 約 7–12 ms;同樣 500 列改成批次一次算完,每列只要 0.02–0.03 ms—— 差了兩百倍以上。這個比例就是上一課「批次評分 vs 線上 API」那個取捨的來源: 線上 API 買的是即時性,代價是每列成本高得多(你的機器數字會不同,看的是量級)。
到 notebook 的 7️⃣ 節:量延遲、組出一列監控紀錄換你動手
換一欄注入漂移:把 f7 乘 0.4(把變異壓扁),重算整張漂移表。f7 會被指出來嗎?accuracy 掉多少?跟動 f0/f3 比,哪一欄比較「重要」?
把「連續 3 次」換成「最近 4 個視窗裡有 3 次超標」(k-of-n),用 8 週模擬比較兩種規則各在第幾週觸發。在 0.25 門檻下結果一樣,把門檻降到 0.1 再比一次——差別就出來了。
換掉 Evidently 的預設:用 ValueDrift(method="psi") 自組一份 12 欄報告、門檻用你從對照組校準出來的數字;或加一個 ClassificationPreset() 產生「有標籤之後」的品質報告。
卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
模型上線第一週,你用 Evidently 預設設定跑漂移報告,結果說 12 欄裡有 3 欄漂移。第一件該做的事是?
本課實測:一份完全沒有漂移的資料(訓練集 vs 沒動過的測試集),用 Evidently 預設值照樣被判 3/12 欄漂移(f0 0.114、f2 0.111、f3 0.105)——跟真的漂移時「欄數」一模一樣,差別只在分數量級(真漂移是 0.813/0.695)。所以看到「3 欄漂移」的第一件事永遠是建立對照組、校準門檻。A 是拿一個沒有校準過的警報去花真金白銀;B 方向對但做法反了——調門檻要有依據,隨手調高只是把眼睛蒙起來;D 更糟,KS 在大樣本下更敏感,實測對照組裡就有兩欄 p < 0.05。
Q2 錯誤診斷
同事的監控報告每天都說「12 欄全部漂移」,而且每一欄的分數都一模一樣。程式沒有拋任何錯誤。最可能的原因是?
證據就在方法名稱上:預設的數值欄方法是 Wasserstein distance (normed),這裡卻是 Jensen-Shannon distance——那是類別欄的方法。把連續數值宣告成類別後,每個浮點數都是獨一無二的「類別」,兩份資料的類別集合幾乎不重疊,於是每一欄都得到同一個接近上限的分數(實測 0.8326)。型別宣告錯不會報錯,只會讓報告很有自信地騙你。A 不會產生一模一樣的分數;C 就本課實測而言 PSI/Wasserstein 是對稱的,寫反不會有這種結果(但自己用參考分位數分箱的 PSI 會變:0.558 → 0.837,所以寫反仍然是壞習慣);D 的話分數不會每欄都相同到小數點後十位。
Q3 情境題
客戶是不是真的流失,要 4 週後才會知道。老闆今天問「模型還能用嗎」。你手上有什麼、該怎麼回答?
沒有標籤的期間,你有兩類即時訊號:輸入分佈與輸出分佈。本課實測兩者方向一致(最大 PSI 0.558、預測 PSI 0.166、判正率 0.520 → 0.444),而事後才拿得到的 accuracy 確實從 0.916 掉到 0.864——症狀猜對了病因。A 放棄了兩個免費且即時的訊號;B 是本課特別點名的陷阱,模型看過訓練集、機率過度自信,實測會算出 0.183 的假漂移;C 混淆了兩種故障模式,模型變爛時 API 照樣 200、延遲照樣漂亮——那正是它危險的地方。
Q4 錯誤診斷
自己寫的 PSI 函式平常都正常,某天某一欄回傳 inf,同時噴出這行警告。怎麼修最對?
divide by zero encountered in log 指的就是 c/r 裡有 0:整欄被推得離參考分佈很遠時,靠近參考低端的那幾個箱子在當前視窗一筆都沒有。實測同一份資料(f0 平移 8),不加極小值得到 inf、加了 1e-6 得到 12.434——一個沒法比較大小、一個可以排序與畫趨勢。A 症狀不符,缺值會讓 np.histogram 少算而不是產生 0 比例的 log;C 方向完全相反,箱子越多每箱越稀疏、空箱只會更多;D 很危險——inf 一旦進了平均、趨勢圖或門檻比較就會污染整條監控管線,而且它掩蓋了「到底漂多遠」這個你真正需要的資訊。
Q5 情境題
監控連續三週警報 → 重訓 → 新模型通過品質閘 → @champion 已移到新版本。接下來最該做、卻最常被忘記的一件事是?
參考視窗代表「模型認識的世界」。模型換了,那個世界也換了——不更新參考視窗,新模型上線第一天就會對你發出「嚴重漂移」的警報,因為它本來就是照著漂移後的資料訓練的。這種每次重訓後都會出現的假警報,是團隊開始無視監控的頭號原因。B 是用調高門檻掩蓋設定錯誤,真的漂移來的時候你也看不到了;C 把回滾的退路砍了——舊版本留著,alias 一行就能指回去;D 等於在最需要把關的時候拆掉煞車,而且下游拿到的會是用未經驗證的新模型算出來的分數。
實作在 molab 跑(免費)
molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。
- 登入 molab(GitHub / Google)
- 開啟課程 notebook,Fork 成自己的副本即可編輯
- 從第一格往下全部執行(首次安裝套件約 1–2 分鐘)——免費 CPU 環境即可,不需要 GPU;整份跑完約 1–2 分鐘
不想用 molab?下載 model-monitoring_ext.py 後在自己電腦
uvx marimo edit --sandbox model-monitoring_ext.py,依賴會自動安裝。
molab 的線上編輯器在手機上體驗有限——動手這一段建議用電腦進行。