CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: SHAP (SHapley Additive exPlanations) を LightGBM と使ってみる

SHAP は協力ゲーム理論にもとづいて機械学習モデルを解釈する手法と、その実装を指している。 今回は、あまり理論の部分には踏み込むことなく、使い方を中心として書いていく。

使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G73
$ python -V            
Python 3.8.5

下準備

はじめに、利用するパッケージをインストールしておく。

$ pip install shap lightgbm scikit-learn matplotlib jupyterlab

また、SHAP は Jupyter 経由で使った方がインタラクティブな表示ができる。 そのため、今回は Jupyter Lab 上で操作することを想定する。

$ jupyter-lab

Jupyter Lab を立ち上げたら実験に使うノートブックを作成しておこう。

Boston データセットと LightGBM で SHAP を使う

はじめに、SHAP のパッケージをインポートする。 そして、Jupyter Lab 上でグラフを表示できるように %matplotlib inlineshap.initjs() を実行しておく。

%matplotlib inline
import shap
shap.initjs()

今回は題材として Boston データセットを使う。

train_x, train_y = shap.datasets.boston()

今回はモデルとして LightGBM を用いる。 そこで、学習用のデータと検証用のデータに分割しておこう。

from sklearn.model_selection import train_test_split
tr_x, val_x, tr_y, val_y = train_test_split(train_x, train_y, shuffle=True, random_state=42)

分割したデータを LightGBM のデータセット表現にする。

import lightgbm as lgb
lgb_train = lgb.Dataset(tr_x, tr_y)
lgb_val = lgb.Dataset(val_x, val_y, reference=lgb_train)

あとは回帰タスクとして学習しよう。

lgbm_params = {
    'objective': 'regression',
    'metric': 'rmse',
    'verbose': -1,
}
booster = lgb.train(lgbm_params,
                    lgb_train,
                    valid_sets=lgb_val,
                    num_boost_round=1000,
                    early_stopping_rounds=100,
                    verbose_eval=50,
                    )

ここからは学習したモデルが、どのように推論をするのか SHAP を使って解釈していく。 決定木系のモデルを解釈するためには shap.TreeExplainer というクラスを使おう。 モデルと学習に使ったデータを渡してオブジェクトを作る。

explainer = shap.TreeExplainer(booster, data=tr_x)

そして、TreeExplainer を使って、モデルがどのように推論するか解釈したいデータについて SHAP Value を計算しよう。 この SHAP Value は、入力したのと同じ次元と要素数で得られる。 そして、値が大きいほど推論において影響が大きいと見なすことができる。

tr_x_shap_values = explainer.shap_values(tr_x)

つまり、行方向に見れば「特定の予測に、それぞれの特徴量がどれくらい寄与したか」と解釈できる。 同様に、列方向に見れば「予測全体で、その特徴量がどれくらい寄与したか」と解釈できる。

SHAP Value は自分で可視化しても良いけど、組み込みでいくつかグラフを描画する仕組みが用意されている。 ここからは、それらを使い分けなどと共に見ていこう。

Summary Plot

はじめに、Summary Plot から。 このグラフは、デフォルトでは特徴量ごとに SHAP Value を一軸の散布図として描画する。

shap.summary_plot(shap_values=tr_x_shap_values,
                  features=tr_x,
                  feature_names=tr_x.columns)

得られるグラフは次のとおり。

f:id:momijiame:20200813184109p:plain
Summary Plot

横軸が SHAP Value で、0 から離れているほど推論において影響を与えていることになる。 縦軸の特徴量は SHAP Value の絶対値の平均値でソートされている。 それぞれの要素の色は、その特徴量の値の大小を表している。

ちなみに、Summary Plot は SHAP Value の絶対値の平均値を使った棒グラフにもできる。

shap.summary_plot(shap_values=tr_x_shap_values,
                  features=tr_x,
                  feature_names=tr_x.columns,
                  plot_type='bar')

そして、このグラフは、特徴量の重要度と解釈することもできる。

f:id:momijiame:20200813184241p:plain
Summary (Bar) Plot

試しに LightGBM に組み込まれている特徴量の重要度と比較してみよう。

lgb.plot_importance(booster, importance_type='gain')

上記で得られるグラフは次のとおり。

f:id:momijiame:20200813184413p:plain
LightGBM Feature Importance (Gain)

微妙に特徴量の順番や値の割合は異なるものの、概ね似たような順序になっていることがわかる。

Dependence Plot

続いては Dependence Plot に移る。 これは、特定の特徴量について、取りうる値と SHAP Value の関係を散布図にしたもの。 Summary Plot では色で表現されていた特徴量の値の大小に軸を割り当てている。 たとえば LSTAT (給与の低い職業に従事する人口の割合) についてプロットしてみよう。

shap.dependence_plot(ind='LSTAT',
                     interaction_index=None,
                     shap_values=tr_x_shap_values,
                     features=tr_x,
                     feature_names=tr_x.columns)

次のようなグラフが得られる。

f:id:momijiame:20200813184813p:plain
Dependence Plot

このグラフからは LSTAT が低いほど SHAP Value も高く、推論の結果に大きく影響を与えることが見て取れる。

また、Dependence Plot では interaction_index に別の特徴量を指定できる。 たとえば RM (その地域における住居の平均的な部屋数) を使ってみよう。

shap.dependence_plot(ind='LSTAT',
                     interaction_index='RM',
                     shap_values=tr_x_shap_values,
                     features=tr_x,
                     feature_names=tr_x.columns)

すると、次のようなグラフが得られる。

f:id:momijiame:20200813185126p:plain
Dependence (Interaction) Plot

このグラフからは、LSTAT が高くなると住居の平均的な部屋数も減る傾向が見て取れる。

Waterfall Plot

続いては Waterfall Plot について。 Waterfall Plot は、これまでと違って特定の予測に着目して可視化する。 たとえば教師データの先頭行を指定してみよう。

shap.waterfall_plot(expected_value=explainer.expected_value,
                    shap_values=tr_x_shap_values[0],
                    features=tr_x.iloc[0],
                    feature_names=tr_x.columns)

すると、次のようなグラフが得られる。

f:id:momijiame:20200813185550p:plain
Waterfall Plot

このグラフでは、目的変数の平均値からスタートして最終的に推論した値に各特徴量がどのように寄与しているかを可視化している。 (LightGBM の学習は平均値を基準として各条件にもとづいて分岐していく) たとえば LSTAT は最終的に推論した 37.9 という値の内訳において +5.91 する効果があった、ということになる。

Force Plot

Waterfall Plot は各推論を個別に見ていくものなので、全体を見たいときは次の Force Plot を使うことになる。

shap.force_plot(base_value=explainer.expected_value,
                shap_values=tr_x_shap_values,
                features=tr_x,
                feature_names=tr_x.columns)

このグラフでは、複数の推論について内訳を一度に確認できる。

f:id:momijiame:20200813190620p:plain
Force Plot

ただし、特定の予測だけに絞ってデータを与えれば、個別に見ることもできる。

shap.force_plot(base_value=explainer.expected_value,
                shap_values=tr_x_shap_values[0],
                features=tr_x.iloc[0],
                feature_names=tr_x.columns)

その場合、次のようなグラフになる。

f:id:momijiame:20200813190832p:plain
Force Plot (個別)

Decision Plot

Force Plot と同じように、複数の予測について内訳を見たいときは Devision Plot も使える。

shap.decision_plot(base_value=explainer.expected_value,
                shap_values=tr_x_shap_values,
                features=tr_x,
                feature_names=list(tr_x.columns))

Decision Plot は Waterfall Plot を折れ線グラフとして同じ描画領域に重ね合わせたような感じ。 あんまりデータ数が多いとわけがわからない。

f:id:momijiame:20200813191139p:plain
Devision Plot

試しに先頭 5 件だけに絞って可視化してみよう。

shap.decision_plot(base_value=explainer.expected_value,
                shap_values=tr_x_shap_values[:5],
                features=tr_x.iloc[:5],
                feature_names=list(tr_x.columns))

特定の特徴量によって最終的な結果は大きく異なる様子が見て取れる。

f:id:momijiame:20200813191303p:plain
Devision Plot (2)

グループ単位などで可視化するときに、傾向を確認しやすいかもしれない。

そんなかんじで。

参考

shap.readthedocs.io

dropout009.hatenablog.com