混同行列と判定しきい値
正解とスコアを突き合わせ、しきい値の変更で見逃し・誤検出・評価指標がどう変わるか調べます。
説明
二値判定には、実際の状態と判定結果を組み合わせた四つの区分があります。正しい陽性がTP、誤った陽性がFP、見逃した陽性がFN、正しい陰性がTNです。混同行列はこの四区分を集計した表です。
ここでは確認済みの異常3件と正常7件に、検出器が0〜1のスコアを付けた例を使います。しきい値以上を異常と判定し、検出件数と精度の変化を追います。スコアを異常である確率とは仮定しません。
1正解とスコアの対応
各行は識別子、正解ラベル、スコアです。正解1は異常、0は正常を意味します。
rows = [('a',1,0.9),('b',0,0.8),('c',1,0.7),('d',0,0.6),('e',0,0.5),('f',1,0.4),('g',0,0.3),('h',0,0.2),('i',0,0.1),('j',0,0.0)]確認済みラベルと検出スコア; rows全10件スコアが高い正常bや、低い異常fがあります。順位が完全でないため、しきい値を一つ選ぶだけで両方の誤りを同時にゼロにはできません。
2しきい値0.5で判定
0.5以上を1、それ以外を0とします。境界の0.5も陽性へ含めます。
threshold = 0.5陽性にする下限; predictions = [(key,truth,int(score >= threshold)) for key,truth,score in rows]識別子・正解・判定; predictions各件の判定結果a〜eの5件が陽性です。正解と比較するとb、d、eが誤検出、fが見逃しです。しきい値ちょうどを含むかどうかも判定仕様の一部です。
3四区分に集計
正解と判定の組を数えます。TPとFNの合計は実際の異常件数、FPとTNの合計は実際の正常件数になります。
tp = sum(t == 1 and p == 1 for _,t,p in predictions)正しく検出; fp = sum(t == 0 and p == 1 for _,t,p in predictions)誤って検出; fn = sum(t == 1 and p == 0 for _,t,p in predictions)見逃し; tn = sum(t == 0 and p == 0 for _,t,p in predictions)正しく正常と判定; {'TP':tp,'FP':fp,'FN':fn,'TN':tn}四区分の件数TP=2、FP=3、FN=1、TN=4です。四区分の合計10と、異常3・正常7という正解側の件数が保たれています。
4分母の異なる指標
適合率は陽性判定中の正解率、再現率は実際の異常中の検出率です。正解率は全件中の正解の割合です。
precision = tp/(tp+fp)適合率; recall = tp/(tp+fn)再現率; accuracy = (tp+tn)/len(rows)正解率; f1 = 2*tp/(2*tp+fp+fn)F1; precision, recall, accuracy, f1四つの指標適合率0.4、再現率約0.667、正解率0.6、F1は0.5です。どれも0〜1の値ですが、分母と評価する性質が異なります。F1は適合率と再現率の調和平均で、TNを直接使いません。
5各しきい値を同じ方法で集計
しきい値を引数として四区分を返す関数を用意します。元のスコアと正解は変更しません。
def confusion(threshold):しきい値ごとの集計
predicted = [(truth,score >= threshold) for _,truth,score in rows]正解と判定の組
return tuple(sum(t == truth and p == prediction for t,p in predicted) for truth,prediction in [(1,True),(0,True),(1,False),(0,False)])TP・FP・FN・TNの順に件数を返す戻り値の順序はTP、FP、FN、TNです。判定条件だけを変えて同じデータへ適用するため、しきい値の影響を比較できます。
6しきい値を下げて判定を比較する
0.9、0.5、0.4、0.0の順に下げます。しきい値を下げると陽性判定の対象が増えます。
[(threshold,confusion(threshold)) for threshold in [0.9,0.5,0.4,0.0]]しきい値と四区分0.9では誤検出0でも異常を2件見逃します。0.4では3件全てを検出しますが正常3件も検出します。0.0では全件陽性となり、正常7件全てが誤検出です。
7誤りの影響を重みにする
例として見逃し1件を10、誤検出1件を1の損失と置きます。これは用途ごとに決める値で、普遍的な重みではありません。
[(threshold,10*confusion(threshold)[2]+confusion(threshold)[1]) for threshold in [0.9,0.5,0.4,0.0]]見逃し10・誤検出1の損失候補内では0.4の損失3が最小です。別の重みや別データでは選択も変わります。しきい値を調整したデータだけで最終性能を報告せず、独立した評価対象で再確認します。
8全て正常と判定する場合
しきい値1.1なら全件陰性です。正解率は70%ですが異常は一つも検出しません。
all_negative = confusion(1.1)全件陰性の四区分; all_negative件数, (all_negative[0]+all_negative[3])/len(rows)正解率, all_negative[0]/(all_negative[0]+all_negative[2])再現率正解率0.7、再現率0です。陽性判定0件なので適合率の分母も0になり、そのままでは定義できません。単一の指標だけで評価せず、件数と指標の定義を併記します。
まとめ
混同行列は正解と判定を四区分へ集計し、指標の分母を確認するための基礎です。
しきい値を下げると検出対象が増え、見逃しと誤検出のバランスが変わります。
用途に応じた損失や独立した評価データを用い、正解率だけで性能を判断しません。
操作履歴と解説を振り返れます。
Python 3
rows = [('a',1,0.9),('b',0,0.8),('c',1,0.7),('d',0,0.6),('e',0,0.5),('f',1,0.4),('g',0,0.3),('h',0,0.2),('i',0,0.1),('j',0,0.0)]; rows[('a', 1, 0.9), ('b', 0, 0.8), ('c', 1, 0.7), ('d', 0, 0.6), ('e', 0, 0.5), ('f', 1, 0.4), ('g', 0, 0.3), ('h', 0, 0.2), ('i', 0, 0.1), ('j', 0, 0.0)]threshold = 0.5; predictions = [(key,truth,int(score >= threshold)) for key,truth,score in rows]; predictions[('a', 1, 1), ('b', 0, 1), ('c', 1, 1), ('d', 0, 1), ('e', 0, 1), ('f', 1, 0), ('g', 0, 0), ('h', 0, 0), ('i', 0, 0), ('j', 0, 0)]tp = sum(t == 1 and p == 1 for _,t,p in predictions); fp = sum(t == 0 and p == 1 for _,t,p in predictions); fn = sum(t == 1 and p == 0 for _,t,p in predictions); tn = sum(t == 0 and p == 0 for _,t,p in predictions); {'TP':tp,'FP':fp,'FN':fn,'TN':tn}{'TP': 2, 'FP': 3, 'FN': 1, 'TN': 4}precision = tp/(tp+fp); recall = tp/(tp+fn); accuracy = (tp+tn)/len(rows); f1 = 2*tp/(2*tp+fp+fn); precision, recall, accuracy, f1(0.4, 0.6666666666666666, 0.6, 0.5)
def confusion(threshold):
predicted = [(truth,score >= threshold) for _,truth,score in rows]
return tuple(sum(t == truth and p == prediction for t,p in predicted) for truth,prediction in [(1,True),(0,True),(1,False),(0,False)])[(threshold,confusion(threshold)) for threshold in [0.9,0.5,0.4,0.0]][(0.9, (1, 0, 2, 7)), (0.5, (2, 3, 1, 4)), (0.4, (3, 3, 0, 4)), (0.0, (3, 7, 0, 0))]
[(threshold,10*confusion(threshold)[2]+confusion(threshold)[1]) for threshold in [0.9,0.5,0.4,0.0]][(0.9, 20), (0.5, 13), (0.4, 3), (0.0, 7)]
all_negative = confusion(1.1); all_negative, (all_negative[0]+all_negative[3])/len(rows), all_negative[0]/(all_negative[0]+all_negative[2])((0, 0, 3, 7), 0.7, 0.0)
演習完了