閾値の最適化
分類タスク(正例(1) or 負例(0)を予測するタスク)で、ロジスティック回帰モデルなど予測確率を出力するモデルを作成した際、ある閾値以上なら正例(1)を、以下ならば負例(0)として出力を行うことになるが、最適な閾値って??
ということで、scipy.optimize.minimizeを使ったサンプルを纏めておくことにする。
※個人備忘録の為、煩雑でスミマセン(涙)
import pandas as pd
import numpy as np
from sklearn.metrics import f1_score
from scipy.optimize import minimize
# ###################
# サンプルデータの生成
# ###################
# 乱数の初期値を設定 こうしておかないと実行のたびに異なる乱数を出力してしまう。
rand = np.random.RandomState(seed=71)
train_y_prob = np.linspace(0, 1, 10000) # 0~1まで等間隔に10000個の実数を取得
## 真の値(0 or 1)のサンプルデータ生成
# 一様分布から0~1の範囲で値を取得し、上で取得したtrain_y_probとの比較を行い 0 or 1 のデータを生成
train_y = pd.Series(rand.uniform(0, 1, train_y_prob.size) < train_y_prob)
## 予測確率とするサンプルデータの生成
train_pred_prob = np.clip(train_y_prob * np.exp(rand.standard_normal(train_y_prob.size)*0.3), 0, 1)
ちなみに、train_yはこんな感じです。
train_y
0 False
1 False
2 False
3 False
4 False
...
9995 True
9996 True
9997 True
9998 True
9999 True
Length: 10000, dtype: bool
予測確率とみなすサンプルはこんな感じです。
train_pred_prob
array([0.00000000e+00, 1.10648939e-04, 2.29275798e-04, ...,
9.05431241e-01, 5.25621437e-01, 8.81456054e-01])
まず、閾値を0.5とした場合のf1-scoreは・・・
# 閾値を0.5とすると・・・
init_threshold = 0.5
init_score = f1_score(train_y, train_pred_prob >= init_threshold)
print("閾値0.5の時のf1-score:{}".format(init_score.round(3)))
# >> 閾値0.5の時のf1-score:0.722
「0.722」 になります。
最適化関数
ここでは、f1-scoreが最も大きくなるような threshold(閾値)を探すということで、
「閾値、正解データ(フラグデータ)、予測確率データ」 の3つを引数として受け取り、
f1-scoreを算出し、符号を反転させたデータを返却する関数を作成します。
※最適化にminimize関数を使うので、return 時の符号を反転させています。
# 閾値の最適化関数
# f1-scoreを元に最適化を行う
# 引数の順番は、最初に最適化される変数がくるようにする?
def f1_optimizer(threshold, train_y, train_pred_prob ):
return -f1_score(train_y, train_pred_prob >= threshold)
最適化
スタートを0.5に設定 x0で指定
そのほか、train_yとtrain_pred_probは、argsでタプル指定
最適化アルゴリズムはmethodeで指定 ここでは、ネルダーミード法を指定
result = minimize(f1_optimizer, x0 = 0.5, args = (train_y, train_pred_prob), method = "Nelder-Mead")
print(result)
# >> final_simplex: (array([[0.32324219],
# >> [0.32319336]]), array([-0.75573177, -0.75573177]))
# >> fun: -0.7557317703844165
# >> message: 'Optimization terminated successfully.'
# >> nfev: 31
# >> nit: 14
# >> status: 0
# >> success: True
# >> x: array([0.32324219])
print("最適な閾値:{}".format(result["x"].round(3)))
print("最適な閾値でのf1_score:{}".format(f1_score(train_y, train_pred_prob >= result["x"]).round(3)))
# >> 最適な閾値:[0.323]
# >> 最適な閾値でのf1_score:0.756
自分向けの備忘録のため、煩雑でスミマセン(涙)
おしまい