SHAP を用いた機械学習への解釈性付与

(noteの「クリエイターを応援する」で応援していただけると嬉しいです。よろしくお願いします)

SHAP(SHapley Additive exPlanations)とは

背景

昨今では機械学習モデルに解釈性や説明性が強く求められるようになっており「説明可能なAI(Explainable AI|XAI)」が着目されるようになっている。
SHAPは、予測モデルに対してそのような解釈性を付与するために作られたライブラリ。

SHAPとは

協力ゲーム理論のシャープレイ値(Shapley Value)を機械学習に応用したオープンソースのライブラリ。
SHAPを使うことで、ある機械学習モデルが導き出した予測値に対して、それぞれの特徴量の影響をどれだけ受けたか(寄与の度合い)を求めることができる。

特に、変数重要度をデータ個別に出せる点が、SHAPの魅力的な点と言える。

アウトプット例として、例えば以下の図。
ボストンの住宅価格を予想した際に、ある住宅の予測値が2.99と算出されたとする。
この場合、この予測モデルの平均的な予測値(ベースライン|基本値)である 2.546という値が、各特徴量の影響によって最終的に2.99 という予測値になったと考えられ、各特徴量がどの方向(正 or 負)にどれだけ影響を与えたか を以下図のように可視化することができる。

シャープレイ値とは何なのか?

協力ゲーム理論において複数プレイヤーの協力によって得られた利得を各プレイヤーに公正に分配するための手段の一つ。(例|datarobotの記事

協力ゲーム理論とシャープレイ値の算出例

力量の異なる3人のプレイヤー(Aさん、Bさん、Cさん)が協力してゲームに挑戦して賞金を得る場合を考える。
dataRobotの事例を参考にしています)

A, B, C が単独、もしくは協力した場合に以下の賞金が得られるとする。

  • A 単独:5万円
  • B 単独:3万円
  • C 単独:2万円
  • ABが協力:15万円
  • ACが協力:11万円
  • BCが協力:8万円
  • ABCが協力:30万円

ここで、ABCが協力して30万を得た場合に、各プレイヤーが協力した際の相乗効果も加味してそれぞれの貢献度を計算し、賞金を分配することを考える。
(相乗効果を加味したいので、A,B,C が単独で獲得できる賞金に応じて配分を決めるのはNG)

この場合に、シャープレイ値の考え方では、
「あるプレイヤーが新たにゲームに加わることで、もらえる賞金額がどれだけ増えるか」を計算する。

例えば、A がゲームに加わる場合を考えると以下のパターンが想定でき、それぞれで増額される賞金は以下のようになる。この時の貢献分を限界貢献度という。

  • 参加者なし:0円 => A 単独:5万円
    Aの限界貢献度は5万円
  • B単独:3万円 => BAが協力:15万円
    Aの限界貢献度は12万円(15万-3万)
  • C単独:2万円 => CAが協力:11万円
    Aの限界貢献度は9万円(11万-2万)
  • BCが協力:8万円 => BCAが協力:30万円
    Aの限界貢献度は22万円(30万-8万)

B,C にも同様に考えると、参加者なしの状態から一人ずつ参加者が増えていき、最終的にA,B,Cの三人が揃う順列は以下の6通り(3!)が考えられる

  • 参加者なし => A => A,B => A,B,C
  • 参加者なし => A => A,C => A,B,C
  • 参加者なし => B => A,B => A,B,C
  • 参加者なし => B => B,C => A,B,C
  • 参加者なし => C => A,C => A,B,C
  • 参加者なし => C => B,C => A,B,C

それぞれの場合で各プレイヤーの限界貢献度を計算して、全ての順番における限界貢献度の期待値(平均値)を算出したものをシャープレイ値という

それぞれのシャープレイ値は以下のようになる。

$$A : \frac{(5+5+12+22+9+22)}{6} = 12.5万円 $$

$$B : \frac{(10+19+3+3+19+6)}{6} = 10万円$$

$$C : \frac{(15+6+15+5+2+2)}{6} = 7.5万円$$

このように求めたシャープレイ値が各プレイヤーの貢献度に応じた賞金の分配額と見なすことができる。
(3人のシャープレイ値を合計するとぴったり30万となることも確認できる)

シャープレイ値の機械学習への応用

シャープレイ値を応用して、ある予測値に対してそれぞの特徴量がどの程度寄与しているか求めたいという要望から生まれたのがSHAPである。
先程の例では、ある状態からあるプレイヤーが加わっていった場合の賞金増額分を算出していたが、この場合(機械学習などの予測モデルを用いる場合)には、ある状態(条件)の予測値から各特徴量の寄与が加わった際予測値がどの程度変動するかを考える

ここでいうある状態(条件)の予測値というのは、ある条件での平均的な予測値のことで、周辺化によって求めるのが一般的。つまり、着目しない特徴量がとりうる全ての値で算出した予測の期待値のこと。

イメージ図

SHAP を用いて機械学習モデルを説明する(DataRobot)より。

協力ゲーム理論の例とSHAPの対応イメージ
(※あくまでイメージです。間違っていたらご指摘ください)

協力ゲーム理論機械学習(SHAP)
知りたいこと各プレイヤーの貢献度(=限界貢献度の平均)各変数の寄与 (=予測値に各変数が与えた影響)
初期状態誰もゲームに参加していない状態 賞金は0円特徴量の情報が何もない状態 予測値は予測の期待値
限界貢献度あるプレイヤーがゲームに参加することによって、増えた賞金の額ある特徴量の値が与えられた時に、予測(の期待値)が変動する度合い

イメージを掴みやすい解説はDataRobotの記事、より詳しい解説は以下の記事参照

実装コード

scikit-learnと同じく、SHAPにもボストンの住宅価格のデータセットが準備されているらしいのでそれを利用する。
予測モデルはXGBoostを使用する。

前準備

ライブラリのインストール

pip install shap
#もしくは
conda install -c conda-forge shap

モジュールインポート〜データの準備

import xgboost
import shap

#データセットの読み込み
X,y = shap.datasets.boston()

#予測モデルの作成
dtrain = xgboost.DMatrix(X, label=y)
model = xgboost.train({"learning_rate": 0.01}, dtrain)

SHAPのオブジェクト作成

TreeExplainer を使って勾配ブースティングのモデルからSHAP値を算出する。
(その他のインスタンスは公式ドキュメント参照)

# jupyter notebookにコードを表示させるためにjsをロード
shap.initjs()

#TreeExplainer でモデルと学習に使ったデータを渡してオブジェクトを作る。
explainer = shap.TreeExplainer(model=model,
                                feature_perturbation='tree_path_dependent',
                                model_output='margin')

#shap_valuesにshap_valueを格納する。
#shap._explanation.Explanation」で持つか、array型で持つか2パターンある
shap_values = explainer.shap_values(X) # numpy.ndarray型の場合
shap_values_ex = explainer(X) # shap._explanation.Explanation型の場合

パラメータに関して

feature_dependence:特徴変数の貢献度を算出する時に、特徴同士を相関させるか独立させるか

  • tree_path_dependent(デフォルト):特徴同士を相関させる
  • independent:特徴同士を独立させる

model_output:モデルの出力設定

  • margin(デフォルト):生データのshape値を出力
  • probability:確率空間に変換されたshape値を出力
  • log_loss:Shap値が対数損失まで合計されるように、モデル損失関数の対数eを説明する。

SHAP値を可視化するメソッド

モデル全体の解釈

  • summary_plot:目的変数に対する各特徴量の寄与度(SHAP値)を可視化する。
  • dependence_plot:特徴量の値とSHAP値の依存関係を可視化する。
  • (force_plot:予測に対して各特徴のがどの程度寄与しているかを可視化する)

個々の予測に対する解釈

  • waterfall_plot:予測に対して各特徴のがどの程度寄与しているかを可視化する
  • force_plot:予測に対して各特徴のがどの程度寄与しているかを可視化する
  • decision_plot:個々の予測に対して各特徴のがどの程度寄与しているかを可視化する

各メソッド詳細|モデル全体の解釈

summary_plot

目的変数に対する各特徴量の寄与度(SHAP値)を可視化する。

デフォルト設定
各特徴量ごとに、SHAP値と特徴量の値の関係を散布図として可視化する。

shap.summary_plot(shap_values, X)

  • 縦軸:各特徴量(貢献度の高い順)
  • 横軸:目的変数の値(SHAP値)
  • :各サンプルの特徴量の値

LSTATは、特徴量の値が大きいほど、SHAP値が負(=予測値を負に押し下げる)になっている。
つまり、LSTATと住宅価格には負の相関があることが読み取れる。

プロットタイプ "bar" 設定
plot_type='bar' とすることで、SHAP値の絶対値の平均値を棒グラフで描画することも可能。
(変数重要度と似たイメージ)

shap.summary_plot(shap_values, X, plot_type="bar")

### dependence_plot
特徴量の値とSHAP値の依存関係を可視化する。

  • 横軸:特徴量の値
  • 縦軸:SHAP値
  • :別の特徴量の値(interactin_indexで指定する。Noneでも良い)

例えば、LSTATは特徴量の値が小さいほど、SHAP値が正(=予測値を正に押し上げる)であるため、LSTATと目的変数は負の相関があることが読み取れる

下のグラフからは、住宅価格にはLSTATとRMの寄与度が高いことが読み取れる

  • LSTAT:給与の低い職業に従事する人口の割合
  • RM:住居の平均部屋数

dependence_plot

特徴量の値とSHAP値の依存関係を可視化する。

shap.dependence_plot(ind="LSTAT",
                        interaction_index='RM',
                        shap_values=shap_values,
                        features=X)

  • 横軸:特徴量の値
  • 縦軸:SHAP値
  • :別の特徴量の値(interactin_indexで指定する。Noneでも良い)

例えば、LSTATは特徴量の値が小さいほど、SHAP値が正(=予測値を正に押し上げる)であるため、LSTATと目的変数は負の相関があることが読み取れる

各メソッド詳細|個々の予測に対する解釈

force_plot

あるサンプルの予測値に対して個別に変数の寄与度(SHAP値)を可視化する

#index : 何番目のサンプルの情報をplotするか指定
index = 0
shap.force_plot(base_value=explainer.expected_value,
                shap_values=shap_values[index],
                features=X.iloc[index,:]
                )

何も特徴量の値が分からない状態の予測値(base value)から、
各特徴量の寄与によって予測値がどう動いたかが視覚的に分かる

このサンプルに関しては、ベースとなる予測値(2.559)に対して、LSTAT, CRIM, NOXなどの寄与によって正に、RMやDISの寄与によって負に予測値が押し上げ(押しし下げ)られて最終的に3.0という予測値となったことが読み取れる。

上記の図を全サンプルの情報をまとめて描画することも可能。
タブ選択で、縦軸、横軸、サンプルの並び順などを値を動的に変えることもできる。

shap.force_plot(base_value=explainer.expected_value,
                shap_values=shap_values,
                features=X)

waterfall_plot

あるサンプルの予測値に対して個別に変数の寄与度(SHAP値)を可視化する。
個別サンプルに対するプロットの表示形式を変えたもの。個々の特徴量の寄与がより見やすくなっている。

index = 0 #表示するサンプルのインデックスを指定
shap.waterfall_plot(shap_values_ex[0])

decision_plot

特定のサンプル(単体でも複数でも可)に対して最終的な予測値に各特徴量がどのように寄与しているかを可視化する。
(全てのサンプルを描画すると煩雑なグラフになるのである程度サンプル数を絞ったほうがいい)

shap.decision_plot(base_value=explainer.expected_value,
                    shap_values=shap_values[:5],
                    features=X[:5],
                    feature_names=X.columns.tolist())

最後に(注意点)

SHAPはあくまでも構築したモデルの振る舞い解釈するものであり、データ自体を解釈するものではない。なので正しいモデルで構築できていなければ間違った解釈を導いてしまうことに注意する。

参考HP

関連書籍