AI 互動教室 ‹ MLOps 自動化技術
下載 .py 開啟實戰 notebook ↗ 留言回報
EXPLAINABILITY · 補充 I · 14

模型可解釋性:
上線前要能說出「為什麼」

模型評審會上,AUC 0.9684 投影在牆上。業務主管問:「它憑什麼說這位客戶會流失? 我要打電話過去,總得說個理由。」法遵同事問:「有沒有用到不該用的欄位?」—— AUC 一個都答不出來,它是一個總分,總分不會告訴你分數是怎麼來的。 SHAP 給的是明細:把預測跟平均之間的差距,一分不剩地分給每個特徵。挑一位客戶,看它怎麼分——

客戶
基準 48.8% 48.8% 預測 98.6%

全域重要度

貢獻值、機率、排名全部來自 notebook 的實測輸出(shap 0.52.0,RandomForest 100 棵樹 depth 8, 測試集 500 位客戶);特徵的中文名字是為了好讀而貼上去的,資料本身是模擬的 f0f11

01 · 兩個層次

「為什麼」有兩種問法,別答錯題

可解釋性不是一個問題,是兩個。搞混了,你會拿全域的答案去回答局部的問題——而那正是最常見的錯。

全域(global)局部(local)
問題長什麼樣整體來說,哪些特徵在決定這個模型的輸出?這一筆為什麼被判成 0.986?
誰在問模型審核、法遵、監控客戶、客服、申訴處理
本課的工具三種重要度、蜂群圖SHAP 值、waterfall 圖
典型誤用拿「全域前三名」去解釋某一位客戶——實測那位低風險客戶身上,全域排第 3 的欄位是他的第 2 大原因

SHAP 用一句白話講完就是:把「這一筆的預測」跟「平均」之間的差距,公平地分給每一個特徵。 「公平」不是形容詞,是有數學定義的(合作賽局理論的 Shapley value)。實務上你只要記住一個檢查:

expected_value + sum(這一筆 12 個特徵的 SHAP 值) = 模型對這一筆的預測 基準 各自的功過 最後的機率

一分不多、一分不少。這條等式讓 SHAP 跟「感覺型」的解釋徹底分開:它不是「大概是這幾個原因」, 而是把 0.9860 減 0.4882 的那 0.4978 分完。第 3 節會拿真實的一位客戶驗算到小數點後 15 位。

到 notebook 的 1️⃣ 節:資料與 champion 模型
02 · 三種重要度

同一個模型,三種答案,而三種都對

# ① 這個特徵讓樹分得多乾淨(加總一定等於 1) rf.feature_importances_ # ② 打亂這一欄,分數掉多少 permutation_importance(rf, Xte, yte, scoring="roc_auc") # ③ 平均把預測機率推動多遠(單位就是機率) np.abs(shap.TreeExplainer(rf).shap_values(Xte)[:, :, 1]).mean(0)

三行程式,三個不同的問題。所以答案不一樣是正常的,不是誰算錯了:

特徵① 內建(佔比)② permutation(AUC 掉多少)③ mean|SHAP|(機率)
f20.4078 (#1)+0.2424 (#1)0.2137 (#1)
f30.1608 (#2)+0.0862 (#2)0.1003 (#2)
f90.0868 (#3)+0.0218 (#3)0.0509 (#3)
f40.0747 (#4)+0.0090 (#5)0.0359 (#4)
f10.0449 (#7)+0.0093 (#4)0.0142 (#6)
f110.0499 (#6)+0.0038 (#7)0.0117 (#8)
f100.0189 (#10)−0.0005 (#12)0.0037 (#10)

前三名 f2 → f3 → f9 三種完全一致——這是好消息:三個問法不同的方法都指向同一組主角, 你可以放心跟業務說「模型主要靠這三件事在判斷」。第 4 名開始就分家了,而每一處分歧都有原因: f4 被模型用得多(內建與 SHAP 都排第 4),但打亂它 AUC 只掉 0.0090——用得多不等於有用。 尾巴那幾欄 permutation 是負的,那不代表「有害」,只代表「打亂它反而剛好好一點點」,也就是雜訊。

①「加起來一定等於 1」是它最大的限制:它只講得出相對佔比,講不出「重要多少」。 而且它偏愛切得動的特徵——連續值與高基數類別可以切出很多刀,每刀貢獻一點,加起來就不低了。 在資料裡加一欄純亂數的客戶編號重訓,三種算法對它的評價是:

算法customer_id(純亂數)的分數排名(共 13 欄)
RF 內建0.0172第 11 名——排在兩個真特徵之上(f10 0.0165、f5 0.0158)
permutation+0.0003第 9 名,數值基本上是 0(打亂它 AUC 一動也不動)
mean|SHAP|0.0038第 10 名,很小但不是 0——模型確實在用它,SHAP 照實說

一句話記住三者的分工:內建說「建樹時切了幾刀」、permutation 說「有沒有用」、SHAP 說「模型實際上怎麼用」。 看到「客戶編號很重要」這種結果,先別急著寫進報告——換一種算法問一次。

到 notebook 的 2️⃣ 節:三張長條圖、排名對照與亂數陷阱
03 · 加總等式

SHAP 值可以驗算——這是它跟「感覺」的分界線

拿測試集第 1 位客戶(編號 #405,預測機率 0.7460)實際走一遍。從基準出發,把 12 個貢獻依序加上去:

基準 expected_value = 0.48824 ← 訓練資料的流失率 f2 = +1.342 → SHAP +0.2600 累積 0.74824 f9 = +1.473 → SHAP +0.0534 累積 0.80166 f3 = +1.046 → SHAP −0.0345 累積 0.76715 ← 有的推、有的拉 f1 = +0.041 → SHAP −0.0205 累積 0.74668 ...(其餘 8 欄) 加總結果 = 0.74600 predict_proba 的答案 = 0.74600 兩者差距 = 2.22e-16 ← 浮點捨入誤差,不是近似 500 位客戶的最大誤差 = 1.80e-15

這條等式有兩個立即可用的價值。第一,它是你「有沒有把 SHAP 用錯」最快的自我檢查: 算完加總一次,對不上就是哪裡錯了(多半是忘了選類別、或欄序跟訓練時不同——第 11 節的測驗有實例)。 第二,它讓「最不可能流失的客戶」有唯一解:因為預測 = 基準 + 總和,而基準對每一筆都一樣, 所以「SHAP 總和最小」必然等於「預測最小」。

另外兩個一定要知道的細節。形狀shap_values(Xte) 回的是 (500, 12, 2)——(列數, 特徵數, 類別數)。二元分類的兩面完全對稱, 要「往流失推」那一面就得寫 [:, :, 1]忘了這一刀是新手最常見的錯,而且它多半不會噴錯單位:sklearn 樹模型的 TreeExplainer 預設解釋 predict_proba,所以 SHAP 值的單位是機率;XGBoost/LightGBM 預設是 log-odds, 加總會等於 margin 而不是機率。看到「加起來不等於機率」,先確認單位。

到 notebook 的 3️⃣ 節:逐欄累積、驗算到 1e-15
04 · 局部解釋

三張圖,各回答一個問題

回答什麼拿給誰看
waterfall這一位客戶,從基準走到預測值的每一步客服、申訴處理、業務
beeswarm全部客戶疊在一起:這個特徵越大,機率越高還是越低模型審核、領域專家
scatter一個特徵的值 vs 它的 SHAP 值:形狀與交互作用你自己(除錯與理解)

兩位客戶,兩個世界。高風險的 #1340(0.986)從 0.488 出發, f2 = +1.45 推了 +0.235、f3 再推 +0.103, 一路加到 0.986——他身上幾乎沒有任何一項在往下拉(最大的負貢獻只有 −0.001), 所以他不是「某一項特別糟」,而是全面偏向流失。 低風險的 #775(0.004)則是十二項全部為負: f2 拉了 −0.215、f9 拉了 −0.097。

注意 f9:它在全域重要度只排第 3,但在這一位身上是第 2 大原因。 全域重要度不能拿來解釋個案——這是可解釋性最常被誤用的地方,也是第 1 節那張表最後一列講的事。

蜂群圖用來看方向:每個點是一位客戶,橫軸是 SHAP 值,顏色是那位客戶的特徵值(紅=高、藍=低)。 把方向量化成「特徵值與它的 SHAP 值的相關係數」:

特徵corr(值, SHAP)讀法
f2+0.924越大,流失機率越高
f3−0.921越大,流失機率越低
f4−0.895越大,流失機率越低
f0+0.838越大,流失機率越高
f9+0.775越大,流失機率越高(散得比較開=交互作用較強)

這張圖是上線前審核最好用的一張:拿給業務看,問一句「照你們的經驗,這個方向對嗎?」 ——方向跟常識相反的特徵,十次有九次是資料處理出了問題(符號寫反、欄位對錯位、缺值被填成 0)。

依賴圖則揭露兩件事。形狀:f2 對 SHAP 的曲線是 S 形,兩端都會飽和—— 分箱平均是 −0.311 / −0.133 / +0.054 / +0.176 / +0.247,中段變化最劇烈、兩端趨平。 樹模型學到的是「切點」不是直線,所以線性模型的係數講不出這件事。 交互作用:f2 落在 1.0 附近的那 26 位客戶,f2 幾乎一樣,SHAP 值卻從 0.111 散到 0.256、相差超過兩倍。 所以:SHAP 值不是「f2 的效果」,是「f2 在這位客戶身上的效果」。

到 notebook 的 4️⃣ 節:兩張 waterfall、蜂群圖、依賴圖
05 · 上線前審核

解釋要跟模型一起版本化,不然它只是一張漂亮的圖

# 續掛回「訓練那個 run」,不要另開一個 with mlflow.start_run(run_id=TRAIN_RUN_ID): shap.plots.beeswarm(expl[:, :, 1], show=False, plot_size=(6.2, 4)) mlflow.log_figure(plt.gcf(), "explain/global_beeswarm.png") mlflow.log_dict({"base_value": 0.48824, "mean_abs_shap": {...}, "top3": ["f2", "f3", "f9"]}, "explain/global_importance.json")

要變成 MLOps 資產,解釋必須滿足三個條件:跟模型版本綁在一起、之後查得到、跟當時的數字一模一樣log_figure 讓圖不用先存檔就進 artifact;log_dict 存的是 可以被程式讀回來比對的數字——第 6 節的監控就靠它。重點在掛到哪一個 run: 解釋是訓練完之後才算的,用 start_run(run_id=...) 續掛回訓練那個 run, 否則半年後你會有一堆孤兒報告,對不回是哪一版模型。

有了報告,模型評審就有東西可以問。三個問題按殺傷力排序: 有沒有一枝獨秀的特徵? 方向合不合常識? 有沒有用到不該用的欄位?第 ① 個值得當場做一次—— 假設有人在特徵表加了一欄 days_since_cancel(距離上次取消服務幾天), 聽起來很合理,但它是客戶流失之後才算得出來的

正常模型混進洩漏特徵
測試集 AUC0.96840.9996
重要度冠軍f2(0.2137)days_since_cancel(0.3542)
冠軍佔全部重要度44.4%65.7%

AUC 0.9996——只看指標,這是一個該開香檳的模型;而它上線之後會是一場災難,因為預測的那一刻根本拿不到這一欄。 重要度分佈是抓洩漏最便宜的雷達:看到一欄吃掉大半重要度、而且 AUC 好得不像話, 先問一句「這一欄在預測的那一刻真的存在嗎?」。這不是判決(真的有主導特徵的問題確實存在), 它是一個要求你解釋的訊號

到 notebook 的 5️⃣ 節:掛 artifact + 洩漏偵測
06 · 監控

重要度什麼時候會變?答案可能跟你想的相反

第 7 課(模型監控)看的是「哪個特徵的分佈變了」,這一課看的是「哪個特徵在決定」。 很容易混為一談,做兩個實驗就分得清楚。

實驗 A:同一個模型 + 漂移的資料。把生產資料的 f0 整體平移 +1.5 (跟第 7 課同一種漂移),丟給同一個模型重算 SHAP:

正常資料f0 漂移 +1.5 之後
測試集 AUC0.96840.9676
平均預測機率0.50700.5225 ← 第 7 課抓得到的預測漂移
12 欄重要度排名一名都沒換(f0 自己從 0.0261 微升到 0.0337,但誰都沒換位置)

第一次看到會意外,但很合理:模型是同一個模型、樹是同一批樹,它的「想法」當然沒變,變的是資料。 所以 SHAP 重要度不是資料漂移偵測器——想知道「輸入變了沒」用 PSI/KS, 想知道「模型的想法變了沒」才看重要度排名。混用會讓你在最需要警報的時候什麼都收不到。

實驗 B:重訓之後,才是重要度該警報的時候。假設上游改版, f2 被灌成常數 0(欄位改名、join 失敗、預設值填 0,這種事每季發生一次)。 管線照常跑完、模型照常重訓、照常註冊成新版本,沒有任何一個步驟報錯

上一版 champion重訓後的新版
測試集 AUC0.96840.8884 ← 掉了,但不到會有人尖叫
前五名f2 → f3 → f9 → f4 → f0f3 → f9 → f4 → f6 → f0
f2 的 mean|SHAP|0.21370.0000
換過位置的欄位9 / 12

品質閘門若設在 0.85,這個模型會被放過去。而重要度講的是完全不同的故事: 原本一個人扛起 44% 決策權的 f2,貢獻變成 0。警報寫起來只有三行:

prev = json.loads(client.download_artifacts( champion_run, "explain/global_importance.json")) new_top = imp.sort_values(ascending=False).index[:5].tolist() assert new_top[:3] == prev["top3"], "重要度前三名變了"

把它寫成 Dagster 的 @asset_check(第 3 課)或 pytest(第 13 課),每次重訓都跟上一版比。 這就是第 5 節那份 artifact 的真正用途:它不是給人看的圖,是給下一次重訓當比較基準的資料。 門檻用「前三名有沒有換人」比「數值差多少」穩,因為數值本來就會浮動; 而警報的動作是要求人來看一眼,不是自動擋——重要度變了,也可能只是模型真的變好了。

到 notebook 的 6️⃣ 節:兩個實驗並排比較
07 · 合規

禁用欄位檢查:寫得出來,也會被繞過去

# 禁用欄位檢查:每一欄的 mean|SHAP| 必須低於門檻 def forbidden_report(vals, cols, forbidden, thr=0.01): imp = pd.Series(np.abs(vals).mean(0), index=cols) return [(c, float(imp[c]), thr, "PASS" if imp[c] < thr else "FAIL") for c in forbidden] 欄位 mean|SHAP| 門檻 結果 f8 0.0052 0.01 PASS f6 0.0130 0.01 FAIL ← 超過門檻

法遵給你一張清單:「這幾欄是敏感資訊,可以用來統計,不得作為個案決策的依據。」 「沒有放進訓練資料」是最乾淨的做法,但真實專案的特徵表是共用的、是別的團隊維護的、上週還多了三欄—— 所以你需要一個每次上線都跑一次的檢查。注意它回傳的是報告不是 True/False: 出事的時候你需要知道差多少

f6 沒過。這時候該做的不是把門檻調到 0.02(雖然那樣報告會變綠色), 而是三步: 從訓練資料把它拿掉重訓,看代價有多大——實測拿掉 f6 之後 AUC 是 0.9699比原本的 0.9684 還高一點點(合規常常沒有你以為的那麼貴,做過才知道); 確認沒有代理特徵頂上來; 把檢查寫進管線,不是上線前才想起來。

第 ② 步比第 ① 步重要得多。把 f2 當成禁用欄位、照規定移除, 但特徵表裡另外有一欄 region_score(與 f2 的相關係數 0.97):

檢查項目結果
禁用欄位檢查PASS——f2 根本不在欄位清單裡
模型 AUC0.9655(原本 0.9684,幾乎沒損失)
新的重要度冠軍region_score 0.2104(原本 f2 是 0.2137)

模型照樣在用那個資訊,只是換了個名字。這叫代理特徵(proxy),是所有「禁用欄位」制度共同的漏洞, 而且不需要有人故意——只要特徵表裡有任何跟禁用欄位相關的東西就會自然發生。 SHAP 幫得上忙的地方是讓它現形:一個新欄位突然衝上第一名,你至少會問一句「這欄是怎麼算出來的?」。 真正的解法在資料治理那一層(查代理欄位的相關係數、要求特徵有來源說明), 但這一節至少告訴你:只查名字的合規檢查,是會被繞過去的

到 notebook 的 7️⃣ 節:檢查函式與代理特徵實驗
08 · 對客戶說明

把 waterfall 變成三句話

瀑布圖是給你看的,不是給客戶看的。客服打電話出去需要的是三句話,而這完全可以自動生成 ——SHAP 值本來就是排好序的數字:

【高風險 #1340】 這位客戶(編號 1340)未來一季的流失機率我們估計是 98.6%, 所有客戶的平均是 48.8%。 把機率推高最多的是 合約剩餘月數指標(目前值 +1.45,推動 +0.235)、 方案滿意度分數(目前值 −0.54,推動 +0.103)、 帳單金額變化(目前值 −2.19,推動 +0.058)。 往下拉的則有 續約提醒回應(目前值 +0.10,推動 −0.001)、 推薦人數(目前值 +0.06,推動 −0.000)。 【低風險 #775】 這位客戶(編號 775)未來一季的流失機率我們估計是 0.4%, 所有客戶的平均是 48.8%。 沒有任何一項在推高流失機率。 往下拉的則有 合約剩餘月數指標(目前值 −1.26,推動 −0.215)、 競品優惠曝光度(目前值 −4.16,推動 −0.097)、 方案滿意度分數(目前值 +0.84,推動 −0.052)。

這段模板有三個刻意的設計,每一個都是被投訴教出來的:

設計不這樣做會怎樣
先給基準再給分數單獨一個「98.6%」客戶只會問「所以呢」;有了 48.8% 才有意義
推高與拉低都講只講壞消息像在找碴
「沒有」也要有分支低風險那位十二項全是負的,沒有這個分支就會印出半句話——上線後炸掉的都是這種邊界情況
特徵值與 SHAP 值分開講混在一起講,客戶會以為你在說「中斷 3 次就一定會流失」

⚠️ 最後一件事,也是最重要的一件:這三句話講的是模型怎麼算的,不是為什麼會發生。下一節就講這件事。

到 notebook 的 8️⃣ 節與 🎛 互動格:換一位客戶看解釋
09 · 限制

誠實比方便重要:SHAP 做不到的三件事

① SHAP 不是因果。f2 的 SHAP 是 +0.26,正確的說法是「這個模型因為 f2 的值而把機率調高了 0.26」; 錯誤的說法是「f2 高導致客戶流失」,更錯的是「把 f2 降下來就能留住客戶」。 模型只學到相關;相關可能來自因果,也可能來自共同原因或反向因果—— 「客服接觸次數多」不是流失的原因,是已經想走的症狀。 實務分界線:SHAP 可以拿來排優先順序(先打給誰)與做審核(模型有沒有亂來), 不能拿來制定干預(改哪個欄位就會怎樣)。

② 相關的特徵會分攤貢獻。把 f2 原封不動複製一欄叫 f2_copy 再訓練:

原本多了一欄一模一樣的 f2_copy
模型 AUC0.96840.9653(一樣強)
f2 的 mean|SHAP|0.21370.1150 ← 腰斬
f2_copy 的 mean|SHAP|0.1368(兩者相加 0.2517)

f2 沒有變得比較不重要,只是有人跟它平分功勞。真實特徵表裡這太常見了 (「近 30 天訂單數」和「近 30 天訂單金額」高度相關),後果是你會低估一整組相關特徵裡每一個成員, 甚至誤以為某個關鍵資訊「模型沒在用」。對策不是換工具,是先看特徵之間的相關係數、把高度相關的欄位當一組看。

TreeExplainer 只吃樹模型。餵它一個 LogisticRegression 會直接拒絕(InvalidModelError)。兩條退路,代價差很多:

TreeExplainerKernelExplainerLinearExplainer
適用只有樹任何模型線性模型
這次算了幾列50020500
花了多久0.6–1.2 秒5–6 秒0.3 毫秒
每列成本1–3 毫秒約 270 毫秒(差 200 倍上下)
結果精確與 TreeSHAP 相關 0.9906封閉解,完全精確

KernelExplainer 靠反覆遮蔽特徵、重新預測來估計,所以要給它一份背景資料 (被遮掉的特徵用什麼值代替),成本大約正比於背景筆數——用 shap.sample(X, 100) 壓縮,別把整個訓練集丟進去。 實務規則:能用專用 explainer 就用專用的,Kernel 是最後手段(抽樣看全域、個案來申訴時才現算那一筆)。

LinearExplainer 快到不用計時(500 列 0.3 毫秒),而且給了理解 SHAP 最好的入口 ——它的結果不是估計,是封閉解:

SHAP(第 i 筆, 第 j 欄) = coef[j] × (x[i, j] − 訓練集平均[j]) 實測 shap_values 與手算的最大差距 = 0.0(完全相等) expected_value = −0.04768 ← 注意:這是 log-odds 不是機率(換算回機率是 0.4881)

線性模型的 SHAP 就是「係數 × 這一筆偏離平均多少」,樹模型只是把同一個概念推廣到非線性。 順帶戳破一個統計課上的簡化:mean|SHAP| 的排名跟「|係數| × 標準差」的排名完全一致, 但跟「只看係數大小」從第 6 名起就不同——一個係數再大,如果那一欄在資料裡幾乎不變動,它對實際預測就沒有影響力。

到 notebook 的 9️⃣ 節:三種 explainer 實測與計時
10 · 實戰

換你動手

LEVEL 1

找出測試集裡「SHAP 貢獻總和最負」的那位客戶(模型最確定不會流失的人),印出他的前三大原因並產出三句說明。 想一下:他跟「機率最低」的那位是同一個人嗎?為什麼?

LEVEL 2

LinearExplainer 解釋 logistic regression,把 mean|SHAP| 排名、|coef| 排名、 |coef|×標準差 排名三者並排印出來,找出排名不一致的那一對特徵,並解釋為什麼。

LEVEL 3

寫一個「禁用欄位檢查」的失敗案例:把 f2 改名成 is_vip 訓練, 斷言它的 mean|SHAP| < 0.01——這個斷言應該要失敗。接著把它修好(提示:修的方法不是調門檻), 並回答「修好之後模型損失多少 AUC」。

卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。

11 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

客服要打電話給一位被判為高流失風險(0.986)的客戶,問你「跟他說什麼理由」。你手上有全域重要度報告(前三名 f2 → f3 → f9)。最好的做法是?

這是局部問題,要用局部工具。全域重要度講的是「模型整體靠什麼」,不是「這一位為什麼」——實測就有反例:全域排第 3 的 f9,在低風險客戶 #775 身上是他的第 2 大原因;而某些客戶的主因根本不在全域前三名裡。C 的 waterfall 直接把 0.986 減 0.488 的差距分完,每一項都有數字可以講,這也正是第 8 節那個三句話模板的來源。A 是最常見的誤用,講出來的理由可能跟這位客戶完全無關。B 放棄太早,SHAP 就是為這件事發明的。D 概念上混了:permutation 是把整欄在客戶之間洗牌來看指標掉多少,單筆資料無從洗起,而且它衡量的是「對準確度有沒有用」,不是「這一筆為什麼」。

Q2 錯誤診斷

同事照文件寫了下面這段,程式跑完沒有任何錯誤訊息,但畫出來的圖只剩兩個特徵、x 軸寫著 SHAP interaction value。最可能的原因?

explainer = shap.TreeExplainer(rf) # RandomForest,二元分類 shap_raw = explainer.shap_values(Xte) print(shap_raw.shape) # (500, 12, 2) shap.summary_plot(shap_raw, Xte) # → 畫得出來,但不是那張圖

shape 已經把答案印出來了:(500, 12, 2) =(列數, 特徵數, 類別數)。二元分類的兩面完全對稱,要「往流失推」那一面必須寫 [:, :, 1]。而 summary_plot 收到三維陣列時會把它當成交互作用值(interaction values)來畫——這是本課最危險的一種錯:它不噴任何錯誤,只是安靜地畫出一張看起來很專業、但完全不是你要的圖。防身法有兩個:形狀先 print 出來看;以及第 3 節那條加總等式,base + vals.sum(1) 必須等於 predict_proba。A 不成立,DataFrame 正是建議的傳法(欄名是解釋的一部分)。C 是杜撰,summary_plot 沒有被棄用,而且 beeswarm 收到同樣的三維輸入一樣會出問題。D 是誤解,explainer 建立時就知道模型的輸出形狀。

Q3 情境題

團隊想加一個「模型的想法有沒有變」的監控,提案是:每天把當天的生產資料丟給現在線上這個模型重算 SHAP,比對重要度排名,變了就發警報。這個設計最大的問題是?

實測直接打臉這個設計:把 f0 整體平移 +1.5(明顯的資料漂移,平均預測機率從 0.5070 跳到 0.5225)丟給同一個模型,12 欄的重要度一名都沒換。原因很簡單——模型是同一個模型、樹是同一批樹,它的「想法」當然沒變,變的是資料。所以這個監控會安安靜靜地永遠不響。正確的分工是:資料變了沒用第 7 課的 PSI/KS;模型的想法變了沒在每次重訓後、拿新模型的重要度跟 champion 的 explain/global_importance.json 比。實測那個「上游把 f2 灌成常數」的案例,重訓後前五名整批換人、9/12 欄換位置,而 AUC 只從 0.9684 掉到 0.8884——閘門設 0.85 還會放它過。A 不對,TreeSHAP 每列只要 1–3 毫秒,成本不是問題。B 是誤解,SHAP 解釋的是模型的輸出,完全不需要標籤(這正是它適合上線後使用的原因)。C 的觀察本身沒錯(所以實務上比「前三名有沒有換人」比較穩),但它沒有點出這個設計根本不會偵測到任何東西。

Q4 錯誤診斷

解釋報告產出來了、圖也畫得很漂亮,但同事做自我檢查時發現加總對不上。他把同一份 DataFrame 丟給模型預測時看到下面的錯誤。最可能的原因與修法?

# 自我檢查:應該要是 1e-15 等級 >>> base + shap_vals.sum(1) - rf.predict_proba(X_new)[:, 1] array([ 0.31, -0.28, 0.44, ...]) # 卻差了 0.3 以上 >>> rf.predict_proba(X_new) ValueError: The feature names should match those that were passed during fit. Feature names must be in the same order as they were in fit.

錯誤訊息最後一句寫得很明白:「欄名必須跟 fit 時同一個順序」。真正危險的是另一半——實測同一份亂序資料丟給 explainer.shap_values() 完全不報錯,照樣回一個形狀正確的陣列,只是每個值都對到錯的欄位。這種錯畫得出圖、有小數點、看起來很專業,全部是錯的。第 3 節那條加總等式就是為這種情況存在的自我檢查:對不上就是哪裡錯了。B 不成立,取 [0] 只會讓整批差一個固定常數(而且方向相反),不會每一筆差不同的量。C 不成立,這份資料沒有缺值,而且錯誤訊息講的是欄名順序不是 NaN。D 明顯不對,實測 500 筆的最大誤差是 1.8e-15,差 0.3 是十四個數量級以外的事。

Q5 情境題

法遵要求某一欄不得作為決策依據。你把它從訓練資料移除、重訓、跑禁用欄位檢查——PASS,AUC 幾乎沒掉。可以送審了嗎?

只查名字的合規檢查會被繞過去。實測:拿掉 f2、留下一欄與它相關 0.97 的 region_score,禁用欄位檢查 PASS(f2 根本不在清單裡)、AUC 只從 0.9684 掉到 0.9655、而 region_score 以 0.2104 直接坐上重要度第一名——模型照樣在用那個資訊,只是換了個名字。而且這不需要有人故意,只要特徵表裡有任何相關的欄位就會自然發生。SHAP 在這裡的價值是讓它現形(一個新欄位突然衝上第一名,你至少會去問「這欄怎麼算的」),真正的解法在資料治理那一層。A 正是這一題要打破的直覺。B 調門檻沒有意義,被移除的欄位根本不在報告裡,門檻再嚴也查不到不存在的東西。D 更糟:把欄位灌成常數就是第 6 節那個「上游 bug」的樣子,模型會沉默地失去 0.08 AUC,而且留著一個隨時可能被還原的洞。

HANDS-ON · MOLAB

實作在 molab 跑(免費)

molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。

  1. 登入 molab(GitHub / Google)
  2. 開啟課程 notebook,Fork 成自己的副本即可編輯
  3. 從第一格往下全部執行(首次安裝套件約 1–2 分鐘)——免費 CPU 環境即可,不需要 GPU;全部計算加起來不到半分鐘

不想用 molab?下載 model-explainability_ext.py 後在自己電腦 uvx marimo edit --sandbox model-explainability_ext.py,依賴會自動安裝。

molab 的線上編輯器在手機上體驗有限——動手這一段建議用電腦進行。