AIを脱Black Box! XAI(Explainable AI)を勉強する 〜shap編〜

スポンサーリンク

 
私達の知らない未来を予測したい。そして予測した未来に至る要因も教えてほしい。という難問をさらっと突きつけるのが人間の欲でもあります。それを叶えてくれそうな今日のAIは、驚きの精度の予測結果を私達に見せてきます。しかし、精度が高ければ高いほど、その予測結果に至る過程を隠してしまうという問題があります。まるで美味しいハンバーグやさんの秘伝のデミグラスソースのように。そこで、今回は予測モデルの中身を見るための脱Black Box化に関して取り組んでいきます。


前回は脱Black Box化の方法の一つであるPermutation Importanceについて実装しました。この方法はデータ全体でみたときとの予測モデルへの各変数の影響度合いがわかりました。


www.dskomei.com


今回の内容と前回との違いは、一つ一つの予測結果に対してどの変数がどれくらい効いているかを見られるようにすることです。例えば、医者が病気になるかの予測を患者に説明する際は、病気だと思った理由を患者全体の傾向だけで説明されても納得いきませんよね。自分自身に限定した病気だと思った原因を知りたいですよね。そのために今回は、SHAP値を使います。前回話したPermutation Importanceとの違いは以下の図のように、全体的な傾向の説明をするか、個々に特化した説明をするかになります。


f:id:dskomei:20190909221041p:plain:w500


今回目指すところは、下記の図のように一つ一つの予測に対して、各変数がプラスに効いているのかマイナスに効いているのかをわかるようにすることです。


f:id:dskomei:20190909224416p:plain:w600


SHAP値を求めるための準備


それではSHAP値を使って変数の影響度を求めてみようと思います。SHAP値の背景に関しては、後述します。今回のコードは
こちら(GitHub)にあります。


まずは、必要なモジュールをimportし、結果を格納するためのフォルダーを作っておきます。今回重要なのはshapモジュールです。pipでインストールできるので、インボートするまでの流れで戸惑うことはないと思います。

from pathlib import Path
import pandas as pd
from dfply import *
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from xgboost import XGBClassifier
from matplotlib import pyplot as plt
import shap

result_dir_path = Path("result")
img_dir_path = Path("img")
if not result_dir_path.exists():
    result_dir_path.mkdir(parents=True)
if not img_dir_path.exists():
    img_dir_path.mkdir(parents=True)


今回使用したデータはsckit-learnの『breast_cancer』です。このデータは、乳がんにおいて陽性か陰性かを分類するものであり、各特徴量は、乳房腫瘤から細心生検で採取したものをデジタル画像にし、その画像から抽出した情報です。このデータのサイズはscikit-learn内では大きい方です。


それでは、データをロードし、学習用データとテスト用データに分離することころまでを行います。

cancer_data = load_breast_cancer()

data_y = cancer_data.target
data_x = pd.DataFrame(
    cancer_data.data,
    columns=cancer_data.feature_names
)

train_x, test_x, train_y, test_y = train_test_split(
    data_x,
    data_y,
    test_size=0.2
)


学習用データができたので、このデータを使って分類モデルを学習させ、SHAP値を使うための準備を終わらせます。

model = XGBClassifier()
model.fit(train_x, train_y)

print("学習データの正答率:{:.0f}%, テストデータの正答率:{:.0f}".format(
    model.score(train_x, train_y) * 100,
    model.score(test_x, test_y) * 100
))

print(confusion_matrix(test_y, model.predict(test_x)))

f:id:dskomei:20190913223402p:plain:w350


テストデータでの正答率は96%でまずまずですね。まぁデータの規模的にはXGBoostを使うまでもないですね。


SHAP値による指定したデータの各変数の影響度合いの可視化


それではSHAP値を使ってみます。一つのデータに対して各変数の影響度合いを見るために、先に指定したデータがどのクラスに分類されるかを確認しておきます。

target_test_index = 0
print(model.predict_proba(test_x)[target_test_index])

f:id:dskomei:20190913224535p:plain:w400


予測結果では98%で乳がんを陰性としています。


ではこの予測結果に対して、各変数がどのような影響を与えたかを見ます。指定したデータの各変数のSHAP値を求め、force_plot関数で可視化しています。

target_values = test_x.iloc[target_test_index]
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(target_values)

shap.initjs()
shap.force_plot(
    explainer.expected_value,
    shap_values[0],
    target_values
)


実行結果は次のようになります。このデータの予測結果は陰性であり、『worst concab points = 輪郭の凹部の数の最悪値』が負の出力値(陰性)に最も影響を与えているのがわかります。つまり、このデータに対して乳がんではないよと予測したのは、輪郭の凹部の数の最悪値が0.17であることが最も影響しているということです。


f:id:dskomei:20190909224416p:plain:w600


SHAP値によるデータ全体からの変数影響度の可視化


ここまでで一つのデータの予測結果に対して各変数の影響度合いを可視化することができました。これで今回の目標達成ですが、shapモジュールではデータ全体に対する各変数の影響度合いも見ることができます。


データ全体に対してSHAP値を求め、summary_plot関数で各変数のデータの値とSHAP値をプロットしています。引数のmax_displayで表示する変数の数を指定することができます。

shap_values = explainer.shap_values(test_x)

shap.initjs()
shap.summary_plot(
    shap_values, 
    test_x,
    max_display=len(test_x)
)

f:id:dskomei:20190921092640p:plain:w600


上記グラフの横軸はSHAP値であり、各プロット点が赤いほど変数の値が高く、青いほど低くなります。この図を見ると、worst concab pointsがSHAP値のプラスとマイナスできれいに分離しているのがわかります。worst concave pointsが大きいとSHAP値が負になり、予測結果は陰性の方になりがちになります。このようにデータ全体で各変数がプラスに影響するのかマイナスに影響するのかがわかるようになります。


更に、データの変動が機械学習モデルの出力値に与える影響に関してforce_plot関数を使うことで見ることができます。

shap.initjs()
shap.force_plot(
    explainer.expected_value,
    shap_values,
    test_x
)

f:id:dskomei:20190921095152p:plain:w650


このグラフは、縦軸が各変数のSHAP値の合計であり、横方向は階層的クラスタリングで似たデータ順に全データが並べられています。今回のデータでは、クラスタ数が多くなく、はっきりプラス/マイナスに影響しているものが別れています。


SHAPの背景


上記で扱ったXGBoostなどの複雑なモデルは、内部が非線形となっているため、各変数の影響度合いを求めることは難しいです。そうであるならば、線形の式にしてしまうことで、各変数の影響度合いをわかるようにしようというのがSHAPになります。線形であれば各変数の係数が影響度合いになります。つまり、以下の式を求めます。ここで、\(M\)は変数の数、\(z_i\)は0-1変数で各変数が観測された(0でない)か否か、\(φ_i\)は変数\(i\)の係数を表します。


\(\displaystyle g(z')=φ_0+\sum_{i=1}^M φ_iz'_i\)


上記の\(φ_i\)を求めれば万事解決なのですが、適当に数値を当てはめればよいというわけではなく、以下の3つの特性を持つ必要があります。


① 予測モデルの出力値と上記の式\(g(z')\)は等しい
② \(z_i=0\)の変数はモデルへの影響度合いも0である
③ モデルが変わっても各変数の相対的な影響度合いの関係は変わらない


この3つの特性が満たされるような\(φ_i\)を求めればよいのです。実はこの問題は、ゲーム理論において、協力によって得られた利得を各プレイヤーに公平に分配する方法の一つとして解かれていました。そして、この方法がシャープレイ値と呼ばれています。ゲーム理論のシャープレイ値に関する詳細な説明はシャープレイ値 - Wikipediaをご覧ください。


以下の式により\(φ_i\)を求めることができます。ここで、\(N\)はすべての変数の集合、\(S\)は変数の部分集合、\(f\)は学習済み予測モデルを表します。


 

\(\displaystyle φ_i=\sum_{S⊆N\{i\}} \frac{|S|!(M-|S|-1)!}{M!}[f_x(S∪\{i\})-f_x(S)]\)


つまり\(φ_i\)は、変数\(i\)を取り除いた変数の全ての部分集合において、変数\(i\)があることによる学習済みモデルの予測値の差を正規化したものの合計値になります。


参考サイト

Advanced Uses of SHAP Values | Kaggle
Consistent Individualized Feature Attribution for Tree Ensembles