分類モデルの確率補正
# default_exp proba_calib
スパムメールやクレジット詐欺を見分けるタスクなどを学習した 分類モデルが出力する予測値は通常 (0, 1) の範囲内に収まり、 予測確率とも呼ばれるので、 うっかり正例であるクラス確率だと 思い込みかねません。
実運用では、閾値を設けて、予測値がその閾値を超えるかどうかで判断を下したりします。 予測値がクラス確率であるかどうかによって、閾値の意味も大きく変わってきます。
スパムメール分類モデルの場合、真のクラス確率を学習したのなら、 90% という予測値が出力されたような100通のメールのうち、 90通が本当にスパムメールだろうと期待されます。 本当のスパムメールの数が90を下回ったらモデルの自信過剰、 上回ったら自信不足と言えます。
モデルの予測値が真の確率とどのくらい乖離しているのか図り、 予測値を真の確率に近づける補正方法を実験してみましょう。
# exporti
import matplotlib.pyplot as plt
import japanize_matplotlib
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.metrics import brier_score_loss, roc_auc_score
sklearn を使って信頼性曲線を書いたり確率補正します。 補正前のベースモデルとしては LightGBM を使います。 図形は Matplotlib と Plotly で作ります。
import numpy as np
import pandas as pd
import plotly.express as px
from lightgbm import LGBMClassifier
from sklearn import datasets
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
n_samples = 1_000_000
pos_rate = 0.1
radius = 0.3
X, y = make_circles(n_samples=n_samples, factor=radius, noise=0.2)
df = pd.DataFrame(X, columns=["x1", "x2"])
df["y"] = pd.Series(y).astype("category")
df["r"] = df.x1 ** 2 + df.x2 ** 2
print("ノイズ率(ラベルが反転)", 1 - sum((df.r <= radius) == (df.y == 1)) / n_samples)
# ランダムに正例をドロップし、インバランスクラス化
df = pd.concat(
[
df[(df.y == 0) | (df.r >= radius)],
df[(df.y == 1) & (df.r < radius)].sample(int(pos_rate * sum(df.y))),
]
)
X = df[["x1", "x2"]]
y = df.y
print("正例率", sum(y) / len(y))
# 学習・補正・テストデータ分割
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42
)
X_train_, X_calib, y_train_, y_calib = train_test_split(
X_train,
y_train,
test_size=4000 / len(X_train),
random_state=2020,
)
print("学習・補正・テスト用データ比率", np.array([len(X_train_), len(X_calib), len(X_test)]) / len(X))
# exports
def plot_calibration_curve(named_classifiers, X_test, y_test):
fig = plt.figure(figsize=(10, 10))
ax1 = plt.subplot2grid((3, 1), (0, 0), rowspan=2)
ax2 = plt.subplot2grid((3, 1), (2, 0))
ax1.plot([0, 1], [0, 1], "k:", label="完全な補正")
for name, clf in named_classifiers.items():
prob_pos = clf.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, prob_pos)
brier = brier_score_loss(y_test, prob_pos)
print("%s:" % name)
print("\tAUC : %1.3f" % auc)
print("\tBrier: %1.3f" % (brier))
print()
fraction_of_positives, mean_predicted_value = calibration_curve(
y_test,
prob_pos,
n_bins=10,
)
ax1.plot(
mean_predicted_value,
fraction_of_positives,
"s-",
label="%s (%1.3f)" % (name, brier),
)
ax2.hist(prob_pos, range=(0, 1), bins=10, label=name, histtype="step", lw=2)
ax1.set_ylabel("正例の比率")
ax1.set_ylim([-0.05, 1.05])
ax1.legend(loc="lower right")
ax1.set_title("信頼性曲線")
ax2.set_xlabel("予測値の平均")
ax2.set_ylabel("サンプル数")
ax2.legend(loc="upper center", ncol=2)
plt.tight_layout()
fig = px.scatter(df.sample(1_000), x="x1", y="x2", color="y", hover_data=["r"])
boundary = (1 + radius) / 2
fig.add_shape(
type="circle",
xref="x",
yref="y",
x0=-boundary,
y0=-boundary,
x1=boundary,
y1=boundary,
line_color="Yellow",
line_width=3,
)
fig.update_layout(width=500, height=500)
fig.show()