🗺️ AI 學習與考證地圖
中級科目三程式實戰 · 類別不平衡

accuracy 0.99,卻一筆詐欺都沒抓到:
不平衡資料的評估與後續方案

這五行程式印出 0.99,看起來像個好模型。但它的內容是「全部都說沒問題」——recall 是 0、precision 是 0、F1 是 0。這一題考的不是算術,而是知不知道 accuracy 在 1% 正類的資料上完全沒有鑑別力,以及接下來該做什麼。

閱讀模式

00題目

某詐欺資料的正類比例約為 1%,團隊先以「全預測為負類」的基準模型觀察 accuracy。若要訓練並選擇真正能辨識少數正類的分類器,下列後續方案何者最合理?

import numpy as np                          # 把 NumPy 拿進來,取個綽號叫 np
from sklearn.metrics import accuracy_score  # 只把「算 accuracy 的那支工具」拿出來
y_true = np.array([0] * 990 + [1] * 10)        # 真實答案:990 個 0、10 個 1
y_pred = np.zeros_like(y_true)                # 模型的預測:全部都填 0
print(accuracy_score(y_true, y_pred))        # 印出 accuracy

先說「類別不平衡」是什麼

一份資料裡,兩個類別的數量差很多,就叫類別不平衡。詐欺偵測、罕見疾病篩檢、機台故障預測、廣告點擊——這些題目天生就長這樣:你真正想抓的那一類,佔比往往不到 1%。

本題的資料是 990 筆正常 + 10 筆詐欺,正類只佔 1%
這代表:只要無腦回答「都沒問題」,1000 題裡就會對 990 題。accuracy 自動就有 0.99——這個數字跟模型聰不聰明一點關係都沒有

本題的混淆矩陣長這樣

模型預測 0(正常)模型預測 1(詐欺)這一列合計
真實是 0(正常)990 TN 真陰性0 FP 偽陽性990
真實是 1(詐欺)10 FN 偽陰性0 TP 真陽性10
這一欄合計100001000

整個「預測 1」那一欄是空的。模型從頭到尾沒說過任何一筆是詐欺,所以 TP = 0、FP = 0。這一格是 0,就決定了後面所有跟正類有關的指標全部歸零。

本站實跑(python3 + scikit-learn 1.8):accuracy 0.99、recall 0.0、precision 0.0(會噴 UndefinedMetricWarning)、F1 0.0、balanced_accuracy 0.5、MCC 0.0
六個指標裡有五個誠實地說「這模型沒用」,只有 accuracy 給了 0.99。

先點開看:類別不平衡是什麼?基準模型要幹嘛?accuracy 怎麼算?

不熟 AI 專有名詞?你需要先認識下列名詞

點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。

這題的核心情境
類別不平衡基準模型 baseline正類 positive class多數類與少數類X 與 y
會用到的評估指標
Accuracy 準確率Recall 召回率Precision 精確率F1 分數混淆矩陣TP / FP / FN / TN
不平衡時真正該看的
PR-AUC / Average PrecisionROC-AUCPR 曲線balanced accuracyMCC
四個選項在講的技術
class_weight判定門檻 threshold成本敏感學習重抽樣 / SMOTE資料洩漏
背後的流程觀念
訓練集 / 驗證集 / 測試集分層抽樣 stratify交叉驗證predict_proba 機率輸出型一/型二錯誤

不熟 Python?你需要先認識下列名詞

點擊後出現漸進式說明

把工具拿進來
importfrom模組 module函式庫 library
存東西、取名字
= 指派變數命名慣例
呼叫與設定
函式 function( ) 呼叫括號參數關鍵字參數print()
這題特有的寫法
[0] * 990 串列重複+ 串列相接[ ] list 串列. 點運算子
NumPy
NumPy 陣列np.zeros_like()shape 形狀dtype 資料型別

01逐行拆解:第 1 行到第 5 行

五行,一行一行走完。右上角的「看位置」可以把這一行放回完整程式裡看。

先認識一個符號:點 .

這是物件導向的寫法。np.array(...)np.zeros_like(...)sklearn.metrics 都用到它。

np.array(...)         點的左邊是模組(numpy 的綽號) 唸「裡面的」
sklearn.metrics       點的左邊是套件,右邊是子模組  唸「底下的」

np.array(...)         括號 = 呼叫,叫它做一件事
y_true.dtype          括號 = 屬性,看一格資料

想深入的話點這裡:點 . 到底是什麼?模組和套件差在哪?函式怎麼呼叫?

第 1 行把 NumPy 拿進來,順便取個綽號
import numpy as np   # 把 NumPy 拿進來,取個綽號叫 np

NumPy 是 Python 做數值運算的函式庫,專門處理成排成列的數字。as np取綽號——之後寫 np. 就等於 numpy.,少打五個字。

這個綽號不是規定,但全世界都這樣寫(就像 pandas 一律叫 pd)。看到別人的程式碼出現 np.,你就知道那是 NumPy。

本題用 NumPy 只是為了做出兩排標籤,運算本身很單純。真正的工作在第 5 行。

第 2 行從 sklearn 的評估工具箱裡,只拿一支工具出來
from sklearn.metrics import accuracy_score   # 只把「算 accuracy 的那支工具」拿出來

from A import B 的意思是「去 A 那個抽屜裡,只把 B 這一支拿出來」。拿出來之後直接寫 accuracy_score(...) 就好,不用寫 sklearn.metrics.accuracy_score(...)

跟第 1 行的 import 比較:第 1 行是把整個工具櫃推過來(所以每次用都要先講 np.),第 2 行是只抽一把螺絲起子放桌上(拿了就能直接用)。兩種寫法都對,看你要用多少東西。

sklearn.metrics 這個模組裡放著所有評估指標accuracy_scorerecall_scoreprecision_scoref1_scoreconfusion_matrixroc_auc_scoreaverage_precision_score⋯⋯
這題只 import 了 accuracy_score,本身就是一個訊號——團隊目前只打算看這一個數字。

相關名詞:fromimport模組

第 3 行做出真實答案:990 個 0、10 個 1
y_true = np.array([0] * 990 + [1] * 10)   # 真實答案:990 個 0、10 個 1

這一行從最裡面往外讀,拆成三步:

 [0] * 990   →  [0, 0, 0, ... , 0]     990 個 0    (串列乘上數字=重複這麼多次)
 [1] * 10    →  [1, 1, ... , 1]        10 個 1
 ① + ②      →  [0, 0, ..., 0, 1, ..., 1]   1000 個
   np.array(③)  →  變成 NumPy 陣列,存進 y_true

這裡的 * 不是乘法、+ 也不是加法。串列來說,* 是「重複幾次」、+ 是「接在後面」。所以 [0] * 990 得到的不是 0,而是一串 990 個 0。

y_true 裝的是「標準答案」——這 1000 筆交易,哪些真的是詐欺。0 代表正常、1 代表詐欺,其中詐欺只有 10 筆。

為什麼 0 和 1 全部排在一起?因為這只是拿來示範的假資料,順序不影響任何指標的計算(accuracy、recall 都只是在數格子)。真實資料當然是混在一起的。

相關名詞:[0] * 990串列相接NumPy 陣列誰是正類

第 4 行做出「模型的預測」:全部填 0
y_pred = np.zeros_like(y_true)   # 做一個跟 y_true 一樣大的陣列,但裡面全是 0

np.zeros_like(y_true) 的意思是「照著 y_true 的形狀與型別,做一個全是 0 的新陣列」。結果是 1000 個 0。

為什麼不直接寫 np.zeros(1000)?因為 zeros_like自動對齊形狀和資料型別——y_true 是 1000 個整數,做出來的 y_pred 就也是 1000 個整數。少一個地方會出錯。

這一行就是整題的關鍵。所謂「基準模型」根本不是一個模型——它沒有學習、沒有參數、沒有看過任何特徵,就是把每一筆都回答「0」(正常)
這種模型在 sklearn 裡有正式名字:DummyClassifier(strategy="most_frequent")

相關名詞:np.zeros_like()dtype基準模型

第 5 行算 accuracy 並印出來
print(accuracy_score(y_true, y_pred))   # 印出 0.99

accuracy_score(y_true, y_pred) 把兩排答案一筆一筆比對,數出「猜對幾筆」,再除以總筆數。

accuracy = 猜對的筆數 ÷ 全部筆數
         = (990 個 0 猜對) + (10 個 1 猜錯 → 0 筆)
         = 990 / 1000
         = 0.99
本站實跑輸出:0.99(不是 0.99000001 之類的浮點誤差,因為 990/1000 剛好可以精確表示)。

注意參數順序:真實答案在前、預測在後。寫反了在 accuracy 上剛好沒差(因為只是數相等的筆數),但在 recall_scoreconfusion_matrix 上寫反會直接算錯

print() 只是把括號裡的結果「印到螢幕上」。沒有它程式一樣會算,只是你看不到。

相關名詞:Accuracyprint()參數順序

020.99 不是模型的成績,是資料的比例

先把一件事講死:在「全預測負類」這個做法下,accuracy 一定等於「負類佔全部的比例」。這是恆等式,不是巧合,也跟資料內容無關。

全預測負類的 accuracy  =  負類筆數 ÷ 總筆數  =  1 − 正類比例

  正類 50%   →  accuracy = 0.50
  正類 10%   →  accuracy = 0.90
  正類  1%   →  accuracy = 0.99   ← 本題
  正類 0.1%  →  accuracy = 0.999
所以 0.99 這個數字,在你看資料之前就能算出來。它只反映「詐欺很少見」,不反映模型的任何能力。
換句話說:如果團隊把 0.99 當成好消息,那是把資料的偏斜誤讀成模型的實力。

其他指標怎麼說

指標算式數值它在說什麼
Accuracy(TP + TN) / 1000 = 990 / 10000.9900猜對了 99%
RecallTP / (TP + FN) = 0 / 100.000010 筆詐欺一筆都沒抓到
PrecisionTP / (TP + FP) = 0 / 00.0000分母是 0,sklearn 會警告後回傳 0
F12PR / (P + R) = 0 / 00.0000P 和 R 都是 0
balanced accuracy(Recall + 特異度) / 2 = (0 + 1) / 20.5000等於亂猜
MCC相關係數版的分類分數0.0000預測與真實完全無關
本站實跑(python3 + scikit-learn 1.8):上表全部為實際輸出。precision_score 會先噴 UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 due to no predicted samples——連 sklearn 自己都在提醒你「這個模型從沒說過任何一筆是正類」

六個指標裡有五個都說「這模型毫無用處」,只有 accuracy 給了 0.99。這就是為什麼題目要你選「後續方案」——重點不是把 0.99 算出來,而是知道它沒有意義之後該做什麼。

互動實驗室:不平衡計算機拖滑桿看 accuracy 怎麼被撐起來
總筆數1000
正類比例1.0%
模型策略:
拖到最右邊看看:正類 0.1%(10 萬筆裡 100 筆詐欺)時,全猜正常的 accuracy 是 0.999accuracy 越漂亮,代表資料越不平衡,也代表這個數字越沒用。

03基準模型是對照組,不是目標

題目寫「團隊以全預測為負類的基準模型觀察 accuracy」——這個動作本身完全正確,錯的是停在這裡。

基準模型(baseline)的三個用途

用途怎麼用
① 當及格線任何真模型都必須明顯贏過基準。贏不過,就代表特徵沒訊號或流程有問題。
② 揭穿指標基準模型的 accuracy 是 0.99,就等於宣告 accuracy 在這份資料上的天花板幾乎沒有空間——真模型頂多從 0.99 進步到 0.995,看不出差別。
③ 定出 PR 的基準線隨機猜的 PR-AUC = 正類比例 = 0.01;隨機猜的 ROC-AUC = 0.5。有了基準線,才知道模型的分數算不算好。
在 scikit-learn 裡,基準模型不用自己手刻,直接用 DummyClassifier
strategy="most_frequent"(永遠猜最多的那一類,就是本題)、"stratified"(照比例亂猜)、"prior""uniform"
先跑 DummyClassifier 拿到基準分數,再跑真模型比較,是不平衡問題的標準第一步。

停在基準模型會發生什麼

如果團隊看到 0.99 就說「模型不錯」,接下來會發生的事很好預測:

  1. 上線之後,詐欺一筆都沒攔到——因為模型從來不說「是詐欺」。
  2. 報表上 accuracy 依然是 0.99,監控完全不會告警
  3. 直到財務對帳才發現損失,而模型的指標從頭到尾都是綠燈。
這是實務上最典型的「指標選錯,整個專案自我欺騙」的案例。問題不在模型,在沒有人問「這 0.99 裡面,那 1% 去哪了」

04那該看什麼:PR-AUC 為什麼比 ROC-AUC 誠實

換指標的第一個直覺通常是「那我看 ROC-AUC 好了」。方向對,但在極度不平衡時,ROC-AUC 也會給你過度樂觀的答案

兩條曲線差在哪

ROC 曲線PR 曲線
橫軸FPR = FP / (FP + TN)Recall = TP / (TP + FN)
縱軸Recall = TP / (TP + FN)Precision = TP / (TP + FP)
用到 TN 嗎(藏在 FPR 的分母)完全不用
隨機猜的分數永遠 0.5等於正類比例(本題 0.01)
不平衡時會被龐大的 TN 稀釋,看起來很好誠實反映「抓到的裡面有多少是真的」

關鍵在 TN。本題有 990 筆 TN,990 這個大數字塞在 FPR 的分母裡,就算誤報了 50 筆,FPR 也只有 50/990 ≈ 0.05,曲線看起來還是很漂亮。而 Precision 的分母裡沒有 TN,誤報 50 筆、抓對 5 筆,precision 就是 5/55 ≈ 0.09——藏不住。

本站實跑(同一個模型,只改變正類比例):負類分數 ~ N(0,1)、正類分數 ~ N(2,1),模型的鑑別能力完全沒變。
正類比例ROC-AUCPR-AUCPR 隨機基準贏過基準幾倍
50%0.92230.92270.50001.8×
10%0.92140.66460.10006.6×
5%0.92400.54480.050010.9×
1%(本題)0.92120.26740.010026.7×
0.1%0.91250.06900.001069.0×
ROC-AUC 從頭到尾停在 0.92 一動也不動,PR-AUC 卻從 0.92 一路掉到 0.07。同一個模型,同樣的鑑別力——差別只在資料有多不平衡。

這張表也解釋了一個常見誤會:PR-AUC 比較低,不代表模型比較差。因為 PR 的基準線會跟著正類比例一起降,所以正確的讀法是「PR-AUC 除以正類比例」——贏過隨機猜幾倍。上表最右欄顯示,越不平衡的情境,同一個模型反而「贏得越多倍」。

互動實驗室:ROC 與 PR 雙曲線對照改比例,看哪一條會塌
模型鑑別力 d2.00
正類比例1.0%
ROC-AUC 隨機基準 0.500
PR-AUC 隨機基準 0.010
怎麼在程式裡算:
average_precision_score(y_true, y_score) = PR-AUC(sklearn 官方推薦的算法,逐點加總,不做內插)
precision_recall_curve(y_true, y_score) = 畫 PR 曲線用的三排數字
roc_auc_score(y_true, y_score) = ROC-AUC
三個都要餵「機率或分數」,不能餵 0/1 的預測結果。餵 0/1 進去不會報錯,但算出來的是一個沒有意義的數字(本題餵 y_pred 進去會得到 ROC-AUC = 0.5)。

順帶把 precision 與 recall 也一起看

PR-AUC 是把所有門檻掃過一遍的總分,但實務上你還是要挑一個門檻上線。所以單點的 precision / recall 一定要一起報,而且要說清楚是在哪個門檻下算的。

本站實跑(模擬詐欺資料 50000 筆、正類 468 筆 ≈ 0.94%,LogisticRegression + StandardScaler,5 折分層交叉驗證):
scoring5 折平均看到這個數字你會怎麼想
accuracy0.9910「太棒了」——但基準模型也有 0.9907
roc_auc0.9429「模型排序能力不錯」——這句是真的
average_precision(PR-AUC)0.3011「還有很大進步空間」——這才是實況
recall0.0983門檻 0.5 之下只抓到不到 1 成
precision0.6464抓到的裡面 6 成多是真的
同一個模型、同一份資料,五個數字說了五種故事。accuracy 0.9910 跟基準模型的 0.9907 只差 0.0003——你不可能靠這個數字選模型。

相關名詞:PR-AUCROC-AUCPR 曲線交叉驗證分層抽樣

05class_weight:讓模型「在意」少數類

換了指標之後,下一個問題是:模型本身要怎麼調,才會願意說「這筆是詐欺」?

為什麼模型天生就懶得抓少數類

訓練時模型在最小化損失(loss)。1000 筆裡有 990 筆是 0,只要把這 990 筆顧好,總損失就已經很低了。那 10 筆正類猜錯的代價,被 990 筆稀釋到幾乎看不見——所以「全部說 0」在數學上是個相當划算的解。

class_weight 做的事很單純:把少數類的錯誤放大幾倍再算損失
class_weight="balanced" 時,sklearn 自動把權重設成 n_samples / (n_classes × 該類筆數)
本題就是 1000 / (2 × 990) = 0.505 給負類、1000 / (2 × 10) = 50.0 給正類——猜錯一筆詐欺,等於猜錯 99 筆正常
互動實驗室:class_weight 天平看權重怎麼把模型推向少數類
正類權重倍率1.0×
負類(正常,14860 筆)
1.0
每筆錯誤的份量
正類(詐欺,140 筆)
1.0
每筆錯誤的份量

兩條分佈是本站實跑模型分數的高斯近似(負類 ~ N(0,1)、正類 ~ N(2.1,1),測試集 15000 筆/正類 140 筆),數字與實跑會有小幅出入,趨勢一致。

本站實跑(模擬詐欺資料,測試集 15000 筆/正類 140 筆,門檻固定 0.5):
做法TPFPFNaccuracyrecallprecisionPR-AUC
全預測負類001400.99070.00000.00000.0093
class_weight=None11101290.99070.07860.52380.2615
class_weight='balanced'1272028130.86390.90710.05890.2653
三件事值得記住:
訓練了一個真模型之後,accuracy 還是 0.9907,跟基準模型一模一樣——但它其實學到了東西(PR-AUC 從 0.0093 變成 0.2615,是基準的 28 倍)。
加上 balanced 之後 recall 從 0.079 衝到 0.907,代價是 precision 掉到 0.059、accuracy 掉到 0.864。
PR-AUC 幾乎沒變(0.2615 → 0.2653)。因為 class_weight 主要是移動決策邊界,模型「排序」的能力並沒有真的變強。

class_weight vs 重抽樣 vs SMOTE

做法怎麼運作優缺點
class_weight不動資料,調整損失函數的權重最省事、不會製造假資料;只支援有 class_weight 參數的模型
過抽樣
oversampling
把少數類複製幾份簡單,但重複樣本容易讓模型過擬合到那幾筆
欠抽樣
undersampling
把多數類丟掉一部分訓練快;丟掉的是真實資訊,資料少時很傷
SMOTE在少數類之間內插造出新樣本比單純複製好;高維或雜訊多時可能造出不存在的樣本
重抽樣有一條鐵則:只能對訓練折做,絕對不能碰驗證/測試折。
正確做法是把它放進 Pipeline(imblearn.pipeline.Pipeline),讓交叉驗證每一折都只在訓練部分重抽樣。
先對整份資料 SMOTE 再切訓練/驗證,是典型的資料洩漏——合成出來的樣本會同時出現在兩邊,分數必然虛高。這正是本題選項 C 的問題。

相關名詞:class_weight重抽樣 / SMOTE資料洩漏成本敏感學習

06決策閾值:0.5 不是天條,是預設值

分類器其實不直接吐出 0 或 1,它吐出的是一個介於 0 到 1 的分數predict_proba)。predict 只是幫你套了一個 0.5 的門檻。

predict_proba(X)[:, 1]  →  0.02, 0.71, 0.13, 0.49, 0.88, ...   模型給的風險分數
                                    ↓  拿 0.5 當刀子切
predict(X)              →     0,    1,    0,    0,    1, ...   變成類別
0.5 這條線是寫死的預設值,不是最佳解。它只在「兩類同樣重要、兩類數量差不多」時才合理。
詐欺偵測顯然不是這種情況:漏抓一筆的損失,遠大於誤報一筆的人工複查成本。那就應該把門檻往下調。
本站實跑門檻掃描(同一個模型,只改門檻):
門檻TPFPFNrecallprecisionF1accuracy
0.5011101290.0790.5240.1370.9907
0.3032461080.2290.4100.2940.9897
0.204595950.3210.3210.3210.9873
0.1063215770.4500.2270.3010.9805
0.0586466540.6140.1560.2490.9653
0.021131143270.8070.0900.1620.9220
0.011221944180.8710.0590.1110.8692
模型完全沒有重新訓練,只是換了一條線。recall 從 0.079 拉到 0.871,accuracy 從 0.9907 掉到 0.8692。哪一列比較好?光看指標答不出來——要看成本。

用成本挑門檻

把「漏抓一筆的損失」和「誤報一筆的成本」寫成數字,總成本就是:

\[ \text{總成本}(t) = C_{FN}\times FN(t) + C_{FP}\times FP(t) \]

把所有門檻掃過一遍,選成本最低的那一個。這件事 sklearn 有現成工具:precision_recall_curve 會一次回傳每個門檻的 precision 與 recall,剩下的就是四則運算。

prec, rec, thr = precision_recall_curve(y_val, prob)   # 掃過所有門檻
n_pos = y_val.sum()                                     # 驗證集有幾筆真正的正類
tp = rec[:-1] * n_pos                                  # 每個門檻抓到幾筆
fn = (1 - rec[:-1]) * n_pos                            # 每個門檻漏掉幾筆
fp = tp / np.maximum(prec[:-1], 1e-9) - tp          # 每個門檻誤報幾筆
cost = 500 * fn + 5 * fp                              # 漏抓 500 元、誤報 5 元
best = thr[cost.argmin()]                             # 成本最低的那個門檻
本站實跑:設漏抓一筆詐欺 500 元、誤報一筆人工複查 5 元。
門檻 0.5:TP 11、FP 10、FN 129 → 總成本 64,550
成本最佳門檻 0.0097:TP 125、FP 1975、FN 15 → 總成本 17,375 省下 73.1%
換個成本假設(漏抓 500、誤報 20)最佳門檻會變成 0.029;(漏抓 1000、誤報 5)會變成 0.008。門檻沒有標準答案,它是成本的函數。
互動實驗室:成本最佳門檻改成本假設,看最低點跑到哪
漏抓一筆的損失500
誤報一筆的成本5
目前使用的門檻0.500
目前門檻的總成本
最佳門檻
改用最佳門檻可省
相對於目前門檻

同樣使用本站實跑模型分數的高斯近似;橫軸是對數刻度,因為最佳門檻通常落在 0.001~0.1 這一段,線性刻度會全部擠在最左邊看不見。

門檻一定要在驗證集上挑,然後才拿去測試集/正式環境用。
在測試集上挑門檻、再回報測試集分數,等於拿答案調參數再用同一份答案考自己——分數必然虛高。這也是本題選項 C 的第二個錯誤。

相關名詞:判定門檻predict_proba成本敏感學習驗證集怎麼用

07四個選項逐一拆解

把前面六節串起來,四個選項的對錯就一目了然。

A只比較 accuracy,因為數值越接近 1 就必然越能找出正類

整題的靶心就在這句話上。「accuracy 越接近 1 就越能找出正類」在不平衡資料上完全不成立——題目自己給的反例就寫在第 5 行:accuracy 0.99、找出正類 0 筆。

更致命的是:真模型的 accuracy 也是 0.9907,跟基準模型只差 0.0003。用一個天花板只剩 0.01 的指標去比較模型,等於沒有比較。

B以 PR-AUC 與 precision/recall 評估,考慮 class_weight,並在驗證集依成本調整決策閾值正確

這一句把三件該做的事全部說對,而且順序也對:

① 換指標——PR-AUC 不用 TN,不會被 990 筆正常交易稀釋;precision/recall 補上單點的實況。
② 調訓練——class_weight 把少數類的錯誤放大,讓模型願意說「是詐欺」。
③ 調門檻——在驗證集上依漏抓/誤報的成本挑一條線,而不是照抄 0.5。

三個動作分別對應到「怎麼衡量」「怎麼訓練」「怎麼決策」,剛好是不平衡問題的三個層次。而且「在驗證集上」這五個字,正是它跟選項 C 的分水嶺。

C先把驗證集正類複製到訓練集,再用原驗證集選閾值資料洩漏

這句話裡有兩個獨立的洩漏,任何一個都足以讓評估失效:

① 驗證集的樣本跑進訓練集。模型看過那些正類,等於考前看過考卷。
② 用「已經被汙染的驗證集」挑門檻。挑出來的門檻是為了那幾筆背下來的樣本量身訂做,換到真實資料上完全不適用。

本站實跑對照(同一份資料,RandomForest 200 棵;把驗證集的正類複製進訓練集):
正確做法:驗證集 F1 0.3041(挑到門檻 0.114)→ 測試集實際 F1 0.2764 兩者接近,估計可信
洩漏做法:驗證集 F1 1.0000(挑到門檻 0.532)→ 測試集實際 F1 0.0141
驗證集上完美無缺,真實世界幾乎全錯。而且因為驗證分數是滿分,你不會有任何警覺。

如果真的想用「複製少數類」這招,正確做法是把它放進 Pipeline、只作用在訓練折,驗證折保持原樣。

D固定 0.5 閾值且只降低 recall,以確保 accuracy 不變目標與需求相反

這句話從頭到尾都在往錯的方向走:

① 「只降低 recall」等於更少抓到詐欺——題目要的是「真正能辨識少數正類」,這是反向操作。
② 「確保 accuracy 不變」把一個沒有鑑別力的指標當成要守住的目標。把 recall 降到 0,accuracy 就回到 0.99——那正是基準模型本人。
③ 「固定 0.5」放棄了成本這條最有效的槓桿。

照這個做法做到極致,你會得到的正是題目一開始那個什麼都不抓的模型。

一句話記法:不平衡問題要動三個地方——衡量(PR-AUC / precision / recall)、訓練(class_weight 或重抽樣)、決策(依成本調門檻);而任何「把驗證集資料弄進訓練集」的操作都是洩漏。

回到題目:現在再作答一次

看完之後再看一次同一段程式,應該有完全不同的感覺。

某詐欺資料的正類比例約為 1%,團隊先以「全預測為負類」的基準模型觀察 accuracy。若要訓練並選擇真正能辨識少數正類的分類器,下列後續方案何者最合理?

import numpy as np                          # 把 NumPy 拿進來,取個綽號叫 np
from sklearn.metrics import accuracy_score  # 只把「算 accuracy 的那支工具」拿出來
y_true = np.array([0] * 990 + [1] * 10)        # 真實答案:990 個 0、10 個 1
y_pred = np.zeros_like(y_true)                # 模型的預測:全部都填 0
print(accuracy_score(y_true, y_pred))        # 0.99

08自我檢測

八題,答錯會直接告訴你錯在哪。

09重點整理

  1. [0] * 990*重複不是乘法、+相接不是加法,做出來的是 1000 個標籤。
  2. np.zeros_like(y_true) = 照著 y_true 的形狀與型別做一個全 0 的陣列,也就是「全部預測負類」。
  3. from sklearn.metrics import ... 是從評估工具箱裡只抽一支工具出來;那個模組裡還有 recall、precision、PR-AUC 等一整排。
  4. 全預測負類的 accuracy 恆等於「1 − 正類比例」。本題 1% 正類 → accuracy 必然是 0.99,看資料之前就能算出來。
  5. accuracy 0.99 的同時,recall / precision / F1 都是 0,balanced accuracy 是 0.5、MCC 是 0。六個指標裡五個都說這模型沒用。
  6. 基準模型是對照組不是目標:它的用途是定出及格線、揭穿失效的指標、給出 PR 的隨機基準線(=正類比例)。
  7. PR-AUC 的分母裡沒有 TN,所以不會被龐大的多數類稀釋;ROC-AUC 會。同一個模型從 50% 降到 0.1% 正類,ROC-AUC 幾乎不動(0.92),PR-AUC 從 0.92 掉到 0.07。
  8. 讀 PR-AUC 要跟隨機基準(=正類比例)比。本題 PR-AUC 0.2615 相對於基準 0.0093,是28 倍,不算差。
  9. ROC-AUC / PR-AUC 都要餵機率或分數,不能餵 0/1 的預測結果。
  10. class_weight="balanced"n_samples / (n_classes × 該類筆數),把少數類的錯誤放大;它移動決策邊界,通常提升 recall、犧牲 precision,但不太會提升 PR-AUC
  11. 重抽樣(含 SMOTE)只能對訓練折做,要放進 Pipeline;先對整份資料重抽樣再切分是典型洩漏。
  12. 0.5 只是預設門檻不是最佳解。用 predict_proba 拿分數,在驗證集上依 \(C_{FN}\times FN + C_{FP}\times FP\) 挑成本最低的門檻。本站實跑省下 73.1% 成本。
  13. 把驗證集樣本複製進訓練集、再用同一個驗證集挑門檻=雙重洩漏。實跑:驗證 F1 1.0000 vs 測試 F1 0.0141。
  14. 正確答案 B衡量(PR-AUC+precision/recall)、訓練(class_weight)、決策(驗證集依成本調門檻)三層都動到,而且沒有洩漏。
完整程式碼