Hayami Kaito Web
~/tech

機械学習の評価指標のしくみ:正解率・適合率・再現率・F1・ROC・AUC

published

機械学習のモデルを作ったら、「どれくらい良いモデルか」を数値で測る必要があります。いちばん分かりやすいのは「何 % 当たったか」(正解率)ですが、これだけを見ていると、まったく役に立たないモデルを良いモデルだと思い込んでしまうことがあります。何を測るかは、そのモデルを 何に使うか、とくに どんな間違いがどれだけ困るかで決まります。

この記事では、分類と回帰のモデルでよく使われる評価指標を、計算例で確かめながら説明します。学習用・検証用・テスト用のデータの分け方は 過学習と正則化のしくみ で説明したので、ここでは「テスト用のデータで何を計算するか」に絞ります。

結論を先に書くと、次のように使い分けます。

場面よく使う指標
クラスの数がほぼ同じ分類正解率
見つけたいクラスがまれ(不正・病気・故障)適合率・再現率・F1、PR 曲線と平均適合率
しきい値を決める前に、モデルそのものの見分ける力を比べるROC 曲線と AUC
クラスの数が偏った多クラス分類マクロ平均の F1
数値の予測(回帰)MAE・RMSE・決定係数
確率そのものを使う(降水確率・リスクの見積もり)交差エントロピー誤差・ブライアスコア・較正

説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。

環境

計算例を動かした環境です。

項目バージョン
Python3.13
NumPy2.5

正解率の落とし穴

いつも「正常」と答えるモデルが 99% 当たる

クレジットカードの不正利用を見つけるモデルを考えます。10,000 件の取引のうち、不正は 1%(100 件)だけです。モデルは取引ごとに点数を出し、不正の取引ほど高い点数になりますが、正常な取引と重なる部分もあります。

「いつも正常と答える」モデルと、「点数が 1.5 を超えたら不正と答える」モデルを比べてみます。

import numpy as np

# 10,000 件の取引のうち、不正は 1%(100 件)
rng = np.random.default_rng(0)
y = np.zeros(10000, dtype=int); y[:100] = 1
rng.shuffle(y)

# モデルの点数:不正の取引ほど高めに出るが、重なりもある
score = np.where(y == 1, rng.normal(2.0, 1.0, 10000), rng.normal(0.0, 1.0, 10000))

def confusion(y, pred):
    tp = int(((pred == 1) & (y == 1)).sum())  # 不正を不正と当てた
    fp = int(((pred == 1) & (y == 0)).sum())  # 正常を不正と言ってしまった
    fn = int(((pred == 0) & (y == 1)).sum())  # 不正を見逃した
    tn = int(((pred == 0) & (y == 0)).sum())  # 正常を正常と当てた
    return tp, fp, fn, tn

def report(y, pred):
    tp, fp, fn, tn = confusion(y, pred)
    accuracy = (tp + tn) / len(y)
    precision = tp / (tp + fp) if tp + fp else 0.0
    recall = tp / (tp + fn)
    f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
    return (tp, fp, fn, tn), round(accuracy, 4), round(precision, 3), round(recall, 3), round(f1, 3)

print(report(y, np.zeros_like(y)))              # いつも「正常」と答えるモデル
print(report(y, (score > 1.5).astype(int)))     # 点数が 1.5 を超えたら「不正」と答えるモデル

実行結果:

((0, 0, 100, 9900), 0.99, 0.0, 0.0, 0.0)
((74, 654, 26, 9246), 0.932, 0.102, 0.74, 0.179)

各行は、4 つの件数(あとで説明する混同行列)、正解率、適合率、再現率、F1 です。

いつも「正常」と答えるモデルは、不正を 1 件も見つけていないのに、正解率は 99% です。点数で判定するモデルは不正の 74% を見つけているのに、正解率は 93.2% と、何もしないモデルより低くなりました。見つけたいものがまれなデータでは、正解率はモデルの役立ち具合をまったく表しません。

混同行列:4 種類の答えに分ける

2 クラスの分類の答えは、正解と予測の組み合わせで 4 種類に分けられます。これを表にしたものを 混同行列と呼びます。見つけたいほう(ここでは不正)を「陽性」、そうでないほうを「陰性」と呼びます。

予測:陽性(不正)予測:陰性(正常)
正解:陽性(不正)真陽性 TP = 74偽陰性 FN = 26(見逃し)
正解:陰性(正常)偽陽性 FP = 654(空振り)真陰性 TN = 9,246

偽陰性(見逃し)と偽陽性(空振り)は、どちらも間違いですが、困り方が違います。不正の見逃しはお金の損害に、空振りは正常な客のカードを止めてしまう迷惑につながります。

適合率・再現率・F1

2 種類の間違いを別々に測る

混同行列から、2 種類の間違いを別々に測る指標を作ります。

指標式意味上の例
適合率(precision)TP ÷ (TP + FP)「不正」と言ったもののうち、本当に不正だった割合。空振りの少なさ74 ÷ 728 = 0.102
再現率(recall)TP ÷ (TP + FN)本当の不正のうち、見つけられた割合。見逃しの少なさ74 ÷ 100 = 0.74
F12 × 適合率 × 再現率 ÷ (適合率 + 再現率)適合率と再現率の調和平均。片方だけ高くても上がらない0.179

上の例のモデルは、不正の 74% を見つけていますが、「不正」と言ったもののうち本当に不正なのは 10.2% だけです。警告 10 件のうち 9 件は空振りということになります。

しきい値で適合率と再現率が入れ替わる

適合率と再現率は、「点数がいくつを超えたら陽性とするか」の しきい値で変わります。上のコードの続きで、しきい値を変えてみます。

for t in [0.5, 1.5, 2.5, 3.5]:
    _, acc, prec, rec, f1 = report(y, (score > t).astype(int))
    print(t, acc, prec, rec, f1)

実行結果:

0.5 0.6961 0.03 0.93 0.058
1.5 0.932 0.102 0.74 0.179
2.5 0.9861 0.307 0.31 0.308
3.5 0.99 0.5 0.03 0.057

左から、しきい値、正解率、適合率、再現率、F1 です。しきい値を下げると(0.5)、不正の 93% を見つけられますが、警告の 97% が空振りになります。しきい値を上げると(3.5)、警告の半分は当たりますが、不正の 3% しか見つけられません。適合率と再現率は、片方を上げるともう片方が下がる関係にあります。

どのしきい値を選ぶかは、モデルではなく使い方で決まります。

  • 病気の検査のふるい分け:見逃しが命に関わるので、再現率を優先し、空振りは精密検査で確かめる
  • 迷惑メールのフィルター:大事なメールを迷惑メールに入れる(空振り)ほうが困るので、適合率を優先する

F1 が最も高いのはしきい値 2.5 の 0.308 ですが、F1 は「見逃しと空振りが同じくらい困る」ときの目安にすぎません。

まれなものを探すと適合率は下がる

適合率は、陽性の割合(有病率・ベースレート)に強く影響されます。感度(再現率)99%・特異度(陰性を陰性と当てる割合)99% という、とても良い検査を、病気の人が 0.1% しかいない集団で使ってみます。

# 感度 99%・特異度 99% の検査を、病気の人が 0.1% の集団で使う
sensitivity, specificity, prevalence = 0.99, 0.99, 0.001
n = 100000
sick = n * prevalence
tp = sick * sensitivity                    # 病気で陽性
fp = (n - sick) * (1 - specificity)        # 病気でないのに陽性
print(round(tp), round(fp), round(tp / (tp + fp), 3))

実行結果:

99 999 0.09

10 万人のうち病気の人は 100 人で、そのうち 99 人が陽性になります。一方、病気でない 99,900 人の 1% にあたる 999 人も陽性になります。その結果、陽性と出た人のうち、本当に病気なのは 9% だけです。検査の性能が同じでも、探しているものがまれなほど、陽性の結果の多くは空振りになります。

ROC 曲線と AUC:しきい値によらない見分ける力

しきい値をすべて試して曲線にする

しきい値を決める前に、モデルそのものが「陽性と陰性をどれだけ見分けられるか」を比べたいことがあります。そのために、しきい値を高いほうから少しずつ下げていき、そのたびに次の 2 つを計算して、点を結んだものが ROC 曲線です。

  • 縦軸:真陽性率(再現率と同じ)。陽性のうち、陽性と当てた割合
  • 横軸:偽陽性率(1 − 特異度)。陰性のうち、陽性と言ってしまった割合

ROC 曲線の下の面積が AUC(Area Under the Curve)です。完全に見分けられるモデルは 1、でたらめに答えるモデルは 0.5 になります。不正のデータの続きで計算してみます。

def roc_curve(y, score):
    order = np.argsort(-score)                    # 点数の高い順に並べ、しきい値を 1 件ずつ下げていく
    tp = np.cumsum(y[order] == 1)
    fp = np.cumsum(y[order] == 0)
    tpr = np.concatenate([[0], tp / tp[-1]])      # 再現率(真陽性率)
    fpr = np.concatenate([[0], fp / fp[-1]])      # 偽陽性率:正常のうち不正と言ってしまった割合
    return fpr, tpr

fpr, tpr = roc_curve(y, score)
auc = float(np.sum((fpr[1:] - fpr[:-1]) * (tpr[1:] + tpr[:-1]) / 2))  # 台形の面積の和
pos, neg = score[y == 1], score[y == 0]
prob = float((pos[:, None] > neg[None, :]).mean())  # 不正 1 件と正常 1 件を選んだとき、不正のほうが高い確率
print(round(auc, 4), round(prob, 4))
for target in [0.01, 0.05, 0.2]:
    print(target, round(float(tpr[np.searchsorted(fpr, target)]), 3))  # 偽陽性率がその値のときの再現率

実行結果:

0.9198 0.9198
0.01 0.38
0.05 0.71
0.2 0.88

1 行目の左が AUC で 0.9198 です。右は「不正の取引と正常な取引を 1 件ずつランダムに選んだとき、不正のほうに高い点数が付いている確率」で、AUC と同じ値になっています。**AUC は「陽性と陰性の組を、正しい順に並べられる確率」**という意味を持ちます。

2 行目からは、偽陽性率を 1%・5%・20% に抑えたときに見つけられる不正の割合です。「正常な取引の 1% までなら止めてもよい」なら、不正の 38% を見つけられる、というように、ROC 曲線からしきい値を決める材料が得られます。

まれなものを探すときは PR 曲線も見る

ROC 曲線の横軸の偽陽性率は「陰性の中での割合」なので、陰性がどれだけ多くても値は変わりません。そのため、陽性がまれになっても AUC は変わらず、実際には空振りだらけのモデルでも高く見えることがあります。

そこで、陽性がまれなデータでは、縦軸に適合率・横軸に再現率をとった PR 曲線と、その下の面積にあたる 平均適合率(AP:Average Precision)も使います。点数の分布は同じまま、陽性の割合だけを変えて比べてみます。

import numpy as np

def auc_and_ap(y, score):
    order = np.argsort(-score)
    hits = y[order] == 1
    tp, fp = np.cumsum(hits), np.cumsum(~hits)
    tpr, fpr = tp / tp[-1], fp / fp[-1]
    auc = float(np.trapezoid(np.concatenate([[0], tpr]), np.concatenate([[0], fpr])))
    precision = tp / (tp + fp)
    ap = float(precision[hits].mean())  # 平均適合率:不正を 1 件見つけるたびの適合率の平均
    return round(auc, 3), round(ap, 3)

# 点数の分布は同じまま、不正の割合だけを変える
rng = np.random.default_rng(0)
for rate in [0.5, 0.1, 0.01, 0.001]:
    n_pos = int(100000 * rate)
    y = np.r_[np.ones(n_pos, dtype=int), np.zeros(100000 - n_pos, dtype=int)]
    score = np.where(y == 1, rng.normal(2.0, 1.0, 100000), rng.normal(0.0, 1.0, 100000))
    print(rate, *auc_and_ap(y, score))

実行結果:

0.5 0.921 0.922
0.1 0.921 0.669
0.01 0.919 0.269
0.001 0.924 0.059

左から、陽性の割合、AUC、平均適合率です。AUC は陽性の割合によらず 0.92 前後のままですが、平均適合率は 0.922 から 0.059 まで下がりました。モデルの見分ける力は同じでも、探しているものがまれになるほど、警告の多くが空振りになることを、平均適合率は正直に表しています。

指標陽性の割合が変わると向いている場面
ROC 曲線・AUCほとんど変わらないモデルどうしの見分ける力を比べる
PR 曲線・平均適合率大きく変わるまれなものを探すときに、実際の空振りの多さを知る

多クラス分類:マクロ平均とマイクロ平均

クラスが 3 つ以上あるときは、クラスごとに適合率・再現率・F1 を計算し、それをまとめます。まとめ方には 2 通りあります。

まとめ方計算特徴
マイクロ平均すべてのクラスの TP・FP・FN を足してから計算する件数の多いクラスの成績に引っぱられる。1 つのラベルを付ける分類では正解率と同じになる
マクロ平均クラスごとの指標を単純に平均するどのクラスも同じ重みで扱う。少ないクラスの成績の悪さが表に出る

クラス 0 が 900 件、クラス 1・2 が 50 件ずつの 3 クラスの分類で比べてみます。

import numpy as np

# 3 クラスの分類。クラス 0 が 900 件と多く、クラス 1・2 は 50 件ずつ
y = np.r_[np.zeros(900, int), np.ones(50, int), np.full(50, 2)]
pred = y.copy()
pred[900:930] = 0        # クラス 1 のうち 30 件をクラス 0 と間違える
pred[950:960] = 0        # クラス 2 のうち 10 件をクラス 0 と間違える
pred[:20] = 2            # クラス 0 のうち 20 件をクラス 2 と間違える

f1s = []
for c in range(3):
    tp = ((pred == c) & (y == c)).sum()
    precision = tp / (pred == c).sum()
    recall = tp / (y == c).sum()
    f1s.append(2 * precision * recall / (precision + recall))
    print(c, round(float(precision), 3), round(float(recall), 3), round(float(f1s[-1]), 3))
print(round(float((pred == y).mean()), 3), round(float(np.mean(f1s)), 3))  # 正解率(= マイクロ平均の F1)とマクロ平均の F1

実行結果:

0 0.957 0.978 0.967
1 1.0 0.4 0.571
2 0.667 0.8 0.727
0.94 0.755

1〜3 行目は、クラスごとの適合率・再現率・F1 です。クラス 1 は再現率が 0.4 で、6 割を見逃しています。最後の行の正解率(マイクロ平均の F1 と同じ)は 0.94 と高く出ますが、マクロ平均の F1 は 0.755 で、少ないクラスの成績の悪さが表に出ています。少ないクラスもきちんと当てたいなら、マクロ平均を見ます。

回帰の指標:MAE・RMSE・決定係数

数値を予測する回帰のモデルでは、予測と正解の差(誤差)から指標を作ります。

指標式特徴
MAE(平均絶対誤差)誤差の絶対値の平均「平均して何円外れるか」と読める。大きな外れに引っぱられにくい
RMSE(2 乗平均平方根誤差)誤差の 2 乗の平均の平方根大きな外れを重く見る
決定係数(R²)1 − 誤差の 2 乗の和 ÷ 正解のばらつき「いつも平均を答える」モデルを 0、完全な予測を 1 とする

10 件の家賃の予測で、「どれも少しずつ外れる」モデル A と、「1 件だけ大きく外れる」モデル B を比べてみます。

import numpy as np

def metrics(y, pred):
    err = pred - y
    mae = np.abs(err).mean()                                      # 平均絶対誤差
    rmse = np.sqrt((err**2).mean())                               # 2 乗平均平方根誤差
    r2 = 1 - (err**2).sum() / ((y - y.mean()) ** 2).sum()         # 決定係数
    return round(float(mae), 2), round(float(rmse), 2), round(float(r2), 3)

# 10 件の家賃(万円)の予測
y = np.array([6.5, 7.2, 8.1, 8.8, 9.9, 11.8, 13.5, 7.5, 9.0, 10.4])
pred_a = y + np.array([0.3, -0.4, 0.2, -0.3, 0.4, -0.2, 0.3, -0.4, 0.2, -0.3])  # どれも少しずつ外れる
pred_b = y.copy(); pred_b[6] += 3.0                                           # 1 件だけ大きく外れる
print(metrics(y, pred_a))
print(metrics(y, pred_b))
print(metrics(y, np.full_like(y, y.mean())))                                  # いつも平均を答える

実行結果:

(0.3, 0.31, 0.978)
(0.3, 0.95, 0.789)
(1.7, 2.07, 0.0)

左から MAE・RMSE・決定係数です。A と B は、MAE がどちらも 0.3 万円で同じです。ところが RMSE は、A の 0.31 に対して B は 0.95 と 3 倍になりました。RMSE は誤差を 2 乗してから平均するので、1 件の大きな外れを重く見るからです。最後の行は、いつも平均を答えるモデルで、決定係数はちょうど 0 になります。

1 件の大きな外れが大きな損害になる(在庫の予測を大きく外すと困る、など)なら RMSE を、外れ値に振り回されずに平均的な外れの大きさを知りたいなら MAE を見る、という使い分けです。

確率の質:交差エントロピー誤差と較正

同じ正解率でも確率の質は違う

降水確率のように、モデルの出す 確率そのものを使う場合は、当たり外れだけでなく、確率がどれだけ信用できるかも測る必要があります。同じ 10 日分の「雨が降る確率」を出す 2 つのモデルを比べてみます。

import numpy as np

# 2 つのモデルの「雨が降る確率」の予報。答え(1: 降った)は同じ 10 日分
y = np.array([1, 1, 1, 0, 0, 0, 0, 1, 0, 0])
p_a = np.array([0.9, 0.8, 0.7, 0.3, 0.2, 0.1, 0.4, 0.6, 0.3, 0.2])        # 自信の度合いがほどほど
p_b = np.array([0.99, 0.99, 0.99, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.99])  # いつも言い切る

for p in [p_a, p_b]:
    accuracy = ((p >= 0.5) == y).mean()
    log_loss = -(y * np.log(p) + (1 - y) * np.log(1 - p)).mean()   # 交差エントロピー誤差
    brier = ((p - y) ** 2).mean()                                   # ブライアスコア
    print(round(float(accuracy), 2), round(float(log_loss), 3), round(float(brier), 3))

実行結果:

1.0 0.297 0.073
0.8 0.929 0.196

左から、0.5 をしきい値にしたときの正解率、交差エントロピー誤差(ログ損失)、ブライアスコア(確率と答えの差の 2 乗の平均)です。どちらの指標も小さいほど良い予報です。

モデル B は 10 日中 8 日を 99% の自信で当てていますが、残りの 2 日(降ったのに 1%、降らなかったのに 99%)を自信満々に外しています。交差エントロピー誤差は、確信して外した予報をとても重く罰するので、B は 0.929 と、A の 3 倍以上になりました。交差エントロピー誤差は、画像分類の記事 で説明した、学習のときの損失関数そのものです。

較正:70% と言ったら 70% 当たるか

確率が信用できるかどうかは、較正(キャリブレーション)で確かめます。「70%」と予報した日を集めたら、本当に 7 割の日に雨が降っているか、を見るのです。本当の確率どおりに答える「正直な」モデルと、本当の確率より極端に言い切るモデルで、予報した確率の区間ごとに、予報の平均と実際に降った割合を比べてみます。

import numpy as np

rng = np.random.default_rng(0)
true_p = rng.uniform(0.05, 0.95, 100000)          # 本当の降水確率
y = (rng.random(100000) < true_p).astype(int)
logit = np.log(true_p / (1 - true_p))
overconfident = 1 / (1 + np.exp(-3 * logit))      # 本当の確率より極端に言い切るモデル

bins = np.linspace(0, 1, 6)                        # 0〜0.2、0.2〜0.4、… の 5 つの区間
for name, p in [("正直", true_p), ("言い切る", overconfident)]:
    idx = np.digitize(p, bins[1:-1])
    rows = [(round(float(p[idx == b].mean()), 2), round(float(y[idx == b].mean()), 2)) for b in range(5)]
    print(name, rows)

実行結果:

正直 [(0.13, 0.13), (0.3, 0.3), (0.5, 0.5), (0.7, 0.7), (0.88, 0.88)]
言い切る [(0.05, 0.22), (0.29, 0.43), (0.5, 0.51), (0.71, 0.58), (0.95, 0.78)]

各組は(予報した確率の平均、実際に降った割合)です。正直なモデルは、どの区間でも 2 つが一致しています。言い切るモデルは、「95%」と言った日に実際に降ったのは 78%、「5%」と言った日にも 22% 降っていて、確率を極端に言いすぎています。

2 つのモデルは確率の大小の順番が同じなので、AUC はまったく同じです。それでも、言い切るモデルの確率をそのまま「降水確率」として使うと、利用者の判断を誤らせます。今の深いニューラルネットワークは、このように自信過剰になりやすいことが知られていて、出力を温度で割って調整する 温度スケーリングなどで較正します。

評価指標の流れ

この記事で紹介した指標を年代順に並べます。

年指標・出来事ポイント
1950ブライアスコア天気予報の確率の良さを、確率と答えの差の 2 乗で測る
1966信号検出理論と ROC 曲線(グリーン・スウェッツ)レーダーの研究から生まれた ROC 曲線を、人の判断の分析に広める
1979F 値(ファン・ライスベルゲン)情報検索で、適合率と再現率をまとめる指標
1982AUC の意味づけ(ハンリー・マクニール)AUC が「陽性と陰性の組を正しく並べる確率」に等しいことを医療の検査の評価で示す
1999プラットスケーリングSVM などの点数を、較正された確率に変える
2006PR 曲線と ROC 曲線の関係陽性がまれなときは PR 曲線のほうが違いを表しやすいことを示す
2017ニューラルネットワークの較正深いネットワークが自信過剰になることと、温度スケーリングでの直し方を示す

まとめ

  • 正解率は、見つけたいものがまれなデータでは役に立たない。いつも「陰性」と答えるだけのモデルでも高く出る
  • 混同行列で、見逃し(偽陰性)と空振り(偽陽性)を分けて数える
  • 適合率は空振りの少なさ、再現率は見逃しの少なさで、しきい値を動かすと片方が上がり、もう片方が下がる。どちらを優先するかは使い方で決める
  • 探しているものがまれなほど、良い検査でも陽性の結果の多くは空振りになる
  • ROC 曲線と AUC は、しきい値によらないモデルの見分ける力を表し、AUC は陽性と陰性の組を正しく並べる確率に等しい
  • 陽性がまれなときは、AUC が高くても空振りが多いことがあるので、PR 曲線と平均適合率も見る
  • 多クラス分類では、少ないクラスの成績も見たいならマクロ平均を使う
  • 回帰では、MAE は平均的な外れの大きさ、RMSE は大きな外れの重さを表し、決定係数は「平均を答えるだけ」のモデルとの比較になる
  • 確率を使うときは、交差エントロピー誤差やブライアスコアに加えて、較正で「70% と言ったら 70% 当たるか」を確かめる

参考

  • G. W. Brier, "Verification of Forecasts Expressed in Terms of Probability", Monthly Weather Review 1950
  • D. M. Green and J. A. Swets, "Signal Detection Theory and Psychophysics", Wiley 1966
  • C. J. van Rijsbergen, "Information Retrieval", 2nd ed., Butterworths 1979
  • J. A. Hanley and B. J. McNeil, "The Meaning and Use of the Area under a Receiver Operating Characteristic (ROC) Curve", Radiology 1982
  • J. Platt, "Probabilistic Outputs for Support Vector Machines and Comparisons to Regularized Likelihood Methods", Advances in Large Margin Classifiers, MIT Press 1999
  • T. Fawcett, "An introduction to ROC analysis", Pattern Recognition Letters 2006
  • J. Davis and M. Goadrich, "The Relationship Between Precision-Recall and ROC Curves", ICML 2006
  • T. Saito and M. Rehmsmeier, "The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets", PLOS ONE 2015
  • C. Guo et al., "On Calibration of Modern Neural Networks", ICML 2017(arXiv:1706.04599)