有名な機械学習モデル解釈ツールであるLIMEとSHAPを試します。
はじめに
最近、機械学習モデルの解釈可能性についての非常に良い書籍を読みました。
※下記リンク先で全文公開されていますのでぜひ読んでみてください。
とくに気に入ったのが、"2.1 Importance of Interpretability(解釈可能性の重要性)"において、機械学習に解釈性が求められるのは「AIの社会的責任が高まっているから」とか言わずに、「予測できない事象に対する人間の知的好奇心は抑えられるものではないから」と述べている点です。
医療のようなリスクの大きい分野ではAIに解釈可能性を求めるのは当然の流れです。しかし、たとえ低リスクな分野における機械学習の導入であっても、クライアントの現場担当者は説明可能性を求めるものです。なぜならば知的好奇心は人間の本質的欲求であり、 とくに機械学習を適用しようとする分野に詳しい人間(つまり現場担当者)ならば事象の背景に何が起こっているかに興味を持たないはずがありません。
機械学習モデルの興味深い予測結果について、知的好奇心が満たされないことの不満はAIへの不信感に転嫁されてしまいますので、機械学習プロジェクトをスムーズに進めるためにも機械学習エンジニアは解釈可能性を諦めるべきではありません。
局所的な説明、LIMEとSHAP
機械学習モデルを解釈する技術には様々なアプローチが考案されています。
どのようなアプローチが存在するかについては前述の書籍や、大阪大の原先生が人工知能学会webページに掲載しておられる記事にてわかりやすくまとめらていますので参考ください。
今回紹介するLIMEとSHAPは、機械学習モデルがあるサンプルの予測についてどのような根拠でその予測を行ったかを解釈するツールです。
モデル全体における傾向をみるのではなく、特定のサンプルに焦点を当ててモデルを解釈するという点でこれらは機械学習モデルの局所的な説明ツールである、といえます。
準備(モデル作成)
解釈ツールを使用する前にまずは機械学習モデルを作成します。
ボストン住宅データセットを使用してxgboost.XGBRegressor
とsklearn.svm.SVR
から2つのモデルを作成しました。
import pandas as pd import numpy as np from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVR import xgboost as xgb X = pd.DataFrame(load_boston().data, columns=load_boston().feature_names) y = pd.DataFrame(load_boston().target, columns=['PRICE']) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) model_gbt = xgb.XGBRegressor() model_gbt.fit(X_train.values, y_train.values) scaler = StandardScaler() scaler.fit(X_train) X_sc_train = scaler.transform(X_train) svr_params = {'kernel': 'rbf', 'C': 95.38592381835898, 'epsilon': 1.6773144109569142} model_svr = SVR(**svr_params) model_svr.fit(X_sc_train, y_train)
なお、SVRのハイパーパラメータは事前に調整したものを使用しています。
ちなみにxgboost.XGBRegressor
でデータフレームをndarrayに変換して訓練データを与えているのは、'pd.DataFrame'で訓練するとmodel.predictでpd.Series
を与えられたときなどにValueError: feature_names mismatch:
とエラーを返すためです。DMxtrix
使えよという話ですが。
1. LIME (local interpretable model-agnostic explanations)
LIMEはアンサンブル木や深層ニューラルネットといった複雑なモデルを、より単純な解釈しやすいモデルである線形回帰で近似するというアプローチで解釈性の向上を目指します。しかし、モデル全体を線形回帰で近似するというのは無理なので(可能ならそもそも複雑なモデルを使用する必要がない)、対象とするサンプルの周囲のデータ空間からサンプリングと予測を繰り返し行うことで得られるデータセットを教師データとして線形回帰モデルを作成します。
対象サンプルの周囲のデータ空間でのみ有効な線形回帰モデルを獲得するゆえにLocal surrogate model (局所的な代理モデル) アプローチと言われます。
GitHub - marcotcr/lime: Lime: Explaining the predictions of any machine learning classifier
冒頭に紹介した書籍で言及されているこのアプローチの問題点は、対象とするサンプルの”周囲”について定義がないことです。
人間的には似たようなサンプルでもLimeのアルゴリズム的には似てないかもしれませんので使用には注意が必要です。
LIMEのpython実装は表形式データ、テキストデータ、画像データに対応しており、コンセプトは同じですがデータ形式ごとに異なるアルゴリズムが用意されています。ここでは表形式のデータから作成されたモデルの解釈を行うLimeTabularExplainer
を紹介します。
XGBoostの場合
import lime import lime.lime_tabular explainer = lime.lime_tabular.LimeTabularExplainer( training_data=X_train.values, feature_names=X.columns, class_names=['Price'], categorical_features=['CAHOS', 'RAD'], verbose=True, mode='regression') exp = explainer.explain_instance(X_test.values[6], model_gbt.predict, num_features=5) #: jupyter notebookの場合 exp.show_in_notebook(show_table=True)
Predicted Valueの23.39はモデルの予測値、min/maxはトレーニングデータでの予測値の最大/最小です。
上段右のPositive/Negativeの横棒グラフは各因子が目的変数(PRICE)に対してpositive(価格を上げる)方向に寄与しているか、negative(価格を下げる)に寄与しているかを示します。棒の高さは寄与の大きさです。
下段のFeature/Valueは説明対象としたサンプルの説明変数の値です。
SVRの場合
SVRは標準化したデータで学習したので標準化と予測をパイプライン化する関数を書きます。 sklearnのPipelineを使用してもよいと思います。
def eval_svr(trained_model, trained_scaler): def eval_closure(x): if len(x.shape) == 1: x = x.reshape((1, -1)) x_sc = trained_scaler.transform(x) prediction= trained_model.predict(x_sc) return prediction return eval_closure svr_pipeline = eval_svr(model_svr, scaler) exp = explainer.explain_instance(X_test.values[6], svr_pipeline, num_features=5) #: jupyter noebookの場合 exp.show_in_notebook(show_table=True)
XGBとSVRで同じサンプルをLIMEで解釈したのに、xgbでは'LSTAT'の寄与が大きくSVRでは'AGE'の寄与が大きかったのが見逃せないポイント。 同じような精度のモデルでも異なるアプローチで予測しているという当然のことを思い起こさせてくれます。
LIMEまとめ:
正直なところモデルを解釈できるほどの情報量は得られません。
このツールの使い方としては、あまりにも予測がうまくいかないサンプルがあったときに適用してみるくらいのデバック的な使い方がよいと思います。
2. SHAP(SHapley Additive exPlanations)
SHAPは協力ゲーム理論におけるShapley値を利用して各説明変数の寄与を説明しようとするアプローチです。
シャープレイ値 は 協力ゲーム において”報酬(payout)”を各プレイヤーに対して公平に分配するためのアイデアです。SHAPツールにおいては”報酬”とはモデルの予測値であり”プレイヤー”とは各特徴量にあたります。 すなわち各特徴量に分配された報酬の大きさ=寄与の大きさと考えるわけです。
SHAPツールの背景理論の詳細については 冒頭で紹介した書籍の ”5.9 Shapley Values”および”5.10 SHAP (SHapley Additive exPlanations)”を参照ください。 github.com
SHAPツールから得られる情報はLIMEと似たような感じで特定サンプルの予測における各特徴量の寄与スコアです。
しかし、SHAPツールのpython実装ではjupyter notebook上でのインタラクティブな操作を可能にしていることによって、より完成度の高いモデル解釈ツールとなっています。
特定のサンプルについての予測の説明
Tree系モデルの場合はshap.TreeExplainer
を使用します。
※SVRなどに適用する場合はshap.KernelExplainer
に書き換えてください。
まずは特定サンプルについての説明から。
import shap shap.initjs() explainer = shap.TreeExplainer(model_gbt) shap_values = explainer.shap_values(X_train) shap.force_plot(explainer.expected_value, shap_values[15,:], X_train.iloc[15,:])
出力は上のような感じです。形式は違えどLIMEと同じような情報が出力されています。
図中のoutput valueは与えられたサンプルの予測値です。それに対してbase valueは与えられたデータセット(ここではX_train)における予測値の平均です。
base valueが表示されるのは、SHAPではデータセットの予測値平均からどれだけ動いたか、ということに着目して特徴量に寄与を分配するためです。
特定のサンプルについての予測の説明の”集合”
SHAPツールでは”特定のサンプルについての予測の解釈”を複数のサンプルについてそれぞれ行った結果をまとめてプロットできるのが素晴らしい点です。
shap.force_plot(explainer.expected_value, shap_values, X_train)
上のような感じで出力されます。
この例では”説明変数の寄与の類似性”によってデータセットをクラスタリングおよびインデックスでづけを行い(横軸)、縦軸に出力値の価格をとってプロットしています。
画像だけでは伝えにくいですがマウスカーソルでの操作により様々な情報が出力されます。
この図から導かれるストーリーは例えば、”予測値が中価格帯(25~35)になるものはRM、LSTAT, TAXの寄与が大きいサンプルが多いこと”に対して、”予測値が高価格帯(40~)になるサンプルではRM, LSTAT, PTRATIOの寄与が大きいサンプルが多い”、すなわち高価格物件では税金(TAX : 固定資産税)は重要なファクターにならず教育環境(PTRATIO : 教師生徒比率)の方が大事である、といったところでしょうか
※きっとお金持ちばっかりが住んでいる区画があるのでしょう。
プロットの横軸縦軸はマウス操作により変更することができます。
この例ではサンプルを出力値(価格)が大きい順に並び変えて(横軸)、縦軸にRM(部屋数)の寄与の大きさをプロットしています。
部屋の数が多いと価格が高い傾向があるのは当然ですが、価格トップ層のいくつかの物件はRM(部屋数)の寄与が非常に小さく、グローバルな傾向に従わないサンプルであることがわかります。
モデルの精度を改善するためにはこのような外れ値的なサンプルを議論することが非常に重要でしょう。
まとめ
SHAPツールのインタラクティブさが非常に有用。文章では伝えにくいのでぜひ実際にお試しを。