SciPy minimize を使ったモデルのしきい値最適化

Posted on 2019/10/13 in 機械学習 , Updated on: 2019/10/13

はじめに

2値分類問題において、正負のラベルを提出する評価指標では、モデルで予測確率を出力し、あるしきい値以上の値を正例として出力する必要がある。このしきい値を求める方法として、scipy.optimizeminimize 関数を利用して最適なしきい値を求めることができる。

参考 : Kaggleで勝つデータ分析の技術

本記事では、上記書籍内で紹介されている手法を図を交えて、理解することを目的とする。

In [296]:
import numpy as np
import pandas as pd

# 描画用ライブラリのインポート
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('darkgrid')
%matplotlib inline

サンプルデータ生成

まず、サンプルデータを作成する。サンプルの数は 400個で、各サンプルの正負ラベル(1 or 0)は true_y として与えられていたとする。また、それらのサンプルの正である予測確率を pred_y と計算できたとする。

In [279]:
# 乱数シード固定
rand = np.random.RandomState(seed=1)
# 0 ~ 1で等間隔に 400分割 (データ作成用)
prob_y = np.linspace(0, 1, 400)

# true_y は、0~1で一用乱数を400個生成し、それぞれが prob_y より
# 小さいものを負例(= 0)、大きいものを正例(= 1)
true_y = pd.Series(rand.uniform(0.0, 1.0, prob_y.size) < prob_y)

# exponential 関数をノイズとして prob_y に載せて、0 ~ 1の範囲でクリッピング
pred_y = np.clip(prob_y * np.exp(rand.standard_normal(prob_y.shape) * 0.3), 0.0, 1.0)

サンプルデータの可視化

まず、ture_y を描画する。ある特徴量1(x軸)に対して、正負 (0 or 1) をプロットしている。

In [299]:
plt.scatter(prob_y, true_y, edgecolors='green', c='yellow', s=30)
plt.ylabel('True labels')
plt.xlabel('Feature1')
plt.show()

つぎに、モデルにて出力したとする pred_y を可視化する。x軸に置いている特徴量1が大きくなるにつれて、このモデルは、正例である確率が高いと計算していることがわかる。これらのプロットは単なる確率であり、分類問題では、正負を分類する必要がある。ここで、例としてこの分類のしきい値を単純に、0.5 としたとする。(赤点線)

In [295]:
plt.scatter(prob_y, pred_y, edgecolors='blue', c='yellow', s=20)
plt.hlines(xmin=0, xmax=1, y=0.5, linestyles='--', colors=['red'],
           label='init threshold')
plt.ylabel('Predicted labels probability')
plt.xlabel('Feature 1')
plt.legend(facecolor='white')
plt.show()

この分類問題においての評価指標は、F1スコアであったとすると、このF1スコアを最大化するしきい値を求めることが必要になる。適当に選択したしきい値 0.5 の時の F1スコアを求めると、下記のように 0.68 という値だった。

In [298]:
from sklearn.metrics import f1_score

# 設定しきい値
init_threshold = 0.5

# しきい値以上の時のサンプルを正として予測した結果と、真の値で F1_score を算出
init_score = f1_score(true_y, pred_y >= init_threshold)
print('When threshold is {}, f1_score is {}.'.format(init_threshold,
                                                     round(init_score,2)))
When threshold is 0.5, f1_score is 0.68.

最適しきい値の探索

scipy.optimizeminimize メソッドを利用して最適なしきい値を求める。まず、minimizeによって最小化したい目的関数を設定する。ここでは、最大化したい F1_score に -1 を乗算した関数を設定する。

In [301]:
# 本来求めたい f1_score を負の値として返す、最適化の目的関数を設定。
def f1_opt(x):
    return -f1_score(true_y, pred_y >= x)

次に、minimizeメソッドを定義する。第一引数として、最小化したい関数、第二引数として、探索を開始する値を渡し、第三引数に使用するアルゴリズムを渡す。ここでは、勾配情報を使用しない(目的関数が微分可能でなくてもよい)Nelder-Meadを使用する。

In [304]:
from scipy.optimize import minimize

result = minimize(f1_opt, x0=np.array([0.5]), method='Nelder-Mead')
print(result)
 final_simplex: (array([[0.284375  ],
       [0.28447266]]), array([-0.75308642, -0.75308642]))
           fun: -0.7530864197530864
       message: 'Optimization terminated successfully.'
          nfev: 33
           nit: 14
        status: 0
       success: True
             x: array([0.284375])

上記の結果より、しきい値(x)は、0.284375 を選択すると、F1_score が最適化できることがわかった。このしきい値を使って、F1_score を算出してみる。

In [305]:
best_threshold = result['x'].item()
best_score = f1_score(train_y, train_pred_prob >= best_threshold)

print('When threshold is {}, f1_score is {}.'.format(round(best_threshold,2), round(best_score,2)))
When threshold is 0.28, f1_score is 0.76.

しきい値0.5 の時の F1スコアは 0.68 だったのに対し、最適化したしきい値でのスコアは 0.76 と確かに改善していることがわかる。最後に、最適なしきい値をグラフに描画する。

In [306]:
plt.scatter(prob_y, pred_y, edgecolors='blue', c='yellow', s=20)
plt.hlines(xmin=0, xmax=1, y=0.5, linestyles='--', colors=['red'],
           label='init threshold : 0.5', linewidth=2)
plt.hlines(xmin=0, xmax=1, y=best_threshold, linestyles='-',
           colors=['green'], label='best threshold : 0.28', linewidth=2)
plt.ylabel('Predicted labels probability')
plt.xlabel('Samples')
plt.legend(facecolor='white')
plt.show()