機械学習モデルを解釈する指標SHAPについて

Python 機械学習

明けましておめでとうございます←

最近身の回りが忙しかったため、更新をサボっておりました。

今回は、機械学習モデルの解釈性に関する指標「SHAP」について書きます。

SHAPについて

機械学習モデルを学習させた時に、実際にモデルはどの特徴量を見て予測をしているのかが知りたい時があります。

木構造アルゴリズムでは、特徴量の境界で切り分けた時のジニ係数を元にして、どの特徴量が効いているかを feature_importance などで調べられます。

しかし、この指標では、評価に一貫性が保たれない場合がある(Inconsistency)ことも述べられており、例えば、決定木のノードで使う特徴量を寄与度の高い特徴量に変更すると、その特徴量の寄与度が下がってしまうといった現象が起こるらしいです。

一方で、機械学習モデルの解釈性の指標として、「SHAP(SHapley Additive exPlanations)」と呼ばれるものがあります。

A Unified Approach to Interpreting Model Predictions

指標というか、フレームワークに近いため、木構造系のモデルや深層学習など、様々なモデルに適用可能です。

ゲーム理論の考え方から導出しているようで、特徴量をプレイヤーとみて、各プレイヤーが連携してゲームを進め、そのプレイヤーがどの程度ゲームに貢献したかという協力ゲーム理論に基づいた算出方法を利用しているようです。

各特徴量の予測への寄与度が \phi_i で、特徴量を使ったかどうかのバイナリベクトル空間上の関数で、説明したい関数 f に近似します。

これを以下の3つの仮定

の元で、

として、各特徴量の寄与度 \phi_i は一意に求まることが、ゲーム理論により示されているとのことです。

このSHAPは求めるのにかなりの計算量が必要なのですが、以下の論文では、木構造の仕組みをうまく使って、効率的にSHAPを求めるアルゴリズムを提案しています。

Consistent Individualized Feature Attribution for Tree Ensembles

SHAPを使うことによって、先ほどあげた feature_importance におけるInconsistencyであることの問題を解消できると論文では言及されています。

また、上記論文では、相互作用効果を考慮したSHAP値(SHAPE Interaction Values)も提案されています。

深く読んではいませんが、恐らくSHAPと同じ考え方で2つの特徴量を使った時の寄与度を求めることができ、これを1つだけの特徴量の寄与度から差っ引いて、1つの特徴量の主効果を計算することができる、といった話ではないかと思います。

何はともあれ、早速使ってみようかと思います。

学習アルゴリズムはLightGBMを使うことにして、回帰系のタスク、分類系のタスクそれぞれで試してみようと思います。

SHAPは以下のライブラリから利用することができます。

- https://github.com/slundberg/shap

Pythonの場合はラッパーが用意されているので、

pip install shap

でインストール可能です。

回帰系モデルへの適用

sklearnのボストン住宅価格の分析タスクをLightGBMで解いてみて、どの特徴量が効いているのかを、SHAPで確認してみます。

ライブラリをもろもろインポート。

import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pylab as plt
from sklearn import datasets, model_selection, metrics
import lightgbm as lgb
import shap

Jupyter notebookで実行する場合は、以下を実行。

# load JS visualization code to notebook
shap.initjs()

今回はSHAPがメインなので、データの確認や特徴量エンジニアリング等は省略して、さくっとデータセットを読み込んで、LightGBMにかけます。

# 回帰系
boston = datasets.load_boston()
df_boston = pd.DataFrame(data=np.c_[boston['data'], boston['target']], columns=boston['feature_names'].tolist() + ['target'])
display(df_boston.head())

train_X, valid_X, train_y, valid_y = model_selection.train_test_split(df_boston[boston['feature_names'].tolist()], df_boston[['target']], test_size=0.2, random_state=42)
print(train_X.shape, valid_X.shape, train_y.shape, valid_y.shape)

model = lgb.LGBMRegressor()
model.fit(train_X, train_y)

print('MSE train: %.3f, valid: %.3f' % (
    metrics.mean_squared_error(train_y, model.predict(train_X)),
    metrics.mean_squared_error(valid_y, model.predict(valid_X))
))
print('R^2 train: %.3f, valid: %.3f' % (
    metrics.r2_score(train_y, model.predict(train_X)),
    metrics.r2_score(valid_y, model.predict(valid_X))
))

さて、以下のようにして、学習したモデルを使ってSHAPを計算してみます。

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(train_X)

例えば、1サンプルを予測させた時のどの特徴量を見たかについて、以下の force_plot で確認できます。

print(train_y.iloc[0,:])
display(shap.force_plot(explainer.expected_value, shap_values[0,:], train_X.iloc[0,:]))

各特徴量の値とそれが予測に対して正の方向に働いたものを赤色、負の方向に働いたものを青色としてプロットされます。

例えばこのサンプルにおいては、LSTAT=24.91であり、この値は予測に対して負の方向に引っ張っていると見てとれます。

インタラクティブなグラフで出力されるので、特徴量の細かい部分についてはオンマウスで確認できます。

全てのデータについても、force_plot で以下のように一気に見ることができます。

shap.force_plot(explainer.expected_value, shap_values, train_X)

横軸にサンプルが並んでいて(404件)、縦軸に予測値が出力され、どの特徴量がプラス、マイナスに働いたかを確認できます。

特徴量軸から見たい場合は、summary_plot で確認できます。

shap.summary_plot(shap_values, train_X)

ドットがデータで、横軸がSHAP値を表しており、色が特徴量の大小を表しています。

例えば、RMは高ければ予測値も高くなる傾向にあり、低ければ予測値も低くなる傾向があるようです。

LSTATは逆のようで、高ければ予測値は低くなり、低ければ予測値は高くなる傾向にあるようです。

以下のようにすると、SHAPの絶対値で特徴量軸を比較してプロットでき、どの特徴量が予測に重要かが視覚化できます。

shap.summary_plot(shap_values, train_X, plot_type='bar')

また、以下のようにして、ある特徴量軸だけに注目してグラフを出力できます。

shap.dependence_plot('RM', shap_values, train_X, interaction_index='RM')

ドットがデータで、横軸が対象特徴量軸の値、縦軸が対象特徴量軸のSHAP値になります。

これを見ても、RMが高くなればなるほど、SHAP値も高くなる傾向にあり、予測値が上がることが分かります。

実際に学習データのRMとtargetの相関係数も0.71でした。

また、interaction_index 引数で相互作用効果を見るための特徴量を選択できます。

何も指定しないと勝手に別の軸が選ばれて?きて、右側のカラーバーがついて一瞬戸惑いますが、上記のように自分自身を指定して、ただ1つの軸のみに注目させることもできます。

次に、Interaction SHAPを計算してみます。

shap_interaction_values = explainer.shap_interaction_values(train_X)

summary_plot で、各特徴量軸のペアについてのSHAPを確認することができます。

shap.summary_plot(shap_interaction_values, train_X)

ある特徴量xと特徴量yの相互作用効果によるSHAPを見たい場合は、以下で確認できます。

shap.dependence_plot(('RM', 'LSTAT'), shap_interaction_values, train_X)

先ほどRM単体だけで見ると、高ければ高いほど、SHAPが高くなる傾向があったかと思いますが、ここでは逆転しています。

これはLSTATに引っ張られるということでしょうか。

shap.dependence_plot('LSTAT', shap_values, train_X, interaction_index='RM')

なんかそういうことかもしれない。

ひとまず回帰系モデルでこのように特徴量の大小が予測値に対してどう影響するかを確認できることが分かりました。

分類系モデルへの適用

分類系のモデルについても、同様にどの特徴量が効いているのかを可視化することができます。

同様に、sklearnのあやめの種類分類タスクをLightGBMで解いてみて、SHAPで確認します。

# 分類系
iris = datasets.load_iris()
df_iris = pd.DataFrame(data= np.c_[iris['data'], iris['target']], columns= iris['feature_names'] + ['target'])
display(df_iris.head())

train_X, valid_X, train_y, valid_y = model_selection.train_test_split(df_iris[iris['feature_names']], df_iris[['target']], test_size=0.2, random_state=42)
print(train_X.shape, valid_X.shape, train_y.shape, valid_y.shape)

model = lgb.LGBMClassifier()
model.fit(train_X, train_y)

print('Accuracy train: %.3f, valid: %.3f' % (
    metrics.accuracy_score(train_y, model.predict(train_X)),
    metrics.accuracy_score(valid_y, model.predict(valid_X))
))

同じようにSHAPを計算してみます。

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(train_X)

分類モデルに関しては、各クラスに対するSHAPを各特徴量について出力するようです。

あやめのデータは3クラス分類ですので、各クラスについて、特徴量がどうだったから、そうであると予測した、そうでないと予測した、みたいな見方が出来ます。

print(train_y.iloc[0,:])
print('')
for i in range(3):
    print('Class ', i)
    display(shap.force_plot(explainer.expected_value[i], shap_values[i][0,:], train_X.iloc[0,:]))

全てのデータについても同様にして、

for i in range(3):
    print('Class ', i)
    display(shap.force_plot(explainer.expected_value[i], shap_values[i], train_X))

として、特徴量とそのクラスかそうでないかの判定の傾向が分かります。

summary_plot では以下のようにプロットされます。

shap.summary_plot(shap_values, train_X)

積立棒グラフで、それぞれの特徴量・クラスに対しての寄与度を可視化できます。

これ、グループ棒グラフの方が見やすい気がしますけど、どうなんでしょうか...。

だけどそれだと、どの特徴量が分類タスクに対して一番寄与するのかが視覚化されにくくなりますか、悩ましいですね...。

分析者の「寄与度」の定義に基づいて、どういう見方をすれば良いか慎重になる必要がありそうです。

同様にして、他の指標も同じように確認できますので、以降は省略します。

深層学習モデルへの適用

すみません、深層学習はまだ試してません←

しかし、ライブラリのページで紹介されているように、深層学習についても特徴量の寄与度の確認が可能です。

割と異なる考え方から導出されているGradCAMとの比較も試してみたいなーとか思っていたり。

機会があればやってみようと思います。

まとめ

今回は機械学習モデルの解釈性の指標「SHAP」を使ってみました。

仕事で機械学習モデルを学習させた時には、どういった理由で予測しているのかといったロジックを要求されることが多々あります。

SHAPはモデルの種類を問わない統一フレームワークなので、複数の異なるモデルを、解釈を元に比較検討したい際には使えそうな手法かなと思いました。

ただ実務で扱うためには、もうちょっと理解を深めたいところです。

まだ、特徴量とモデル予測の傾向的な理解に止まっている気がするので、SHAPがいくら変わったら、それがAccuracyやインパクトにどの程度影響するのかといったところまで定量化できるような、透明な理解ができれば、使ってみたいかもと思いました。

コメント