こんにちは、JS2IIUです。
機械学習モデルの精度が向上する一方で、「なぜそのような予測をしたのか」を説明することの重要性が増しています。特に医療診断や金融審査といった重要な意思決定の場面では、モデルの予測根拠を明確に示すことが求められます。
この記事では、モデルの解釈性を高めるための代表的な手法であるSHAP(SHapley Additive exPlanations)とLIME(Local Interpretable Model-agnostic Explanations)について、Pythonを使った実装方法を詳しく解説します。これらの手法を使えば、複雑なブラックボックスモデルの予測を、実務で使える形で説明できるようになります。今回もよろしくお願いします。
モデル解釈性が必要な理由
機械学習モデル、特にディープラーニングや勾配ブースティングといった高精度なモデルは、内部構造が複雑でその判断根拠を理解しにくい「ブラックボックス」となりがちです。しかし実務では、以下のような場面でモデルの解釈性が必須となります。
ステークホルダーへの説明責任: ビジネスの意思決定者や顧客に対して、AIが下した判断の根拠を示す必要があります。例えば「なぜこの申請が却下されたのか」を説明できなければ、システムへの信頼を得られません。
モデルのデバッグと改善: モデルが意図しない特徴量に過度に依存していないか、データの偏りを学習していないかを確認するには、予測の根拠を可視化する必要があります。
法規制への対応: EUのGDPRをはじめ、AIの判断根拠の説明を求める法規制が世界的に広がっています。
SHAPとは何か
SHAPは、ゲーム理論のシャープレイ値という概念を機械学習に応用した手法です。各特徴量が予測にどれだけ貢献したかを、公平に評価することができます。
シャープレイ値の直感的な理解
シャープレイ値を理解するために、チームでプロジェクトを進める場面を想像してください。メンバーAさん、Bさん、Cさんがいて、最終的にプロジェクトが成功したとします。各メンバーの貢献度を公平に評価するには、どうすればよいでしょうか?
シャープレイ値のアプローチは、「あらゆる組み合わせでの貢献を平均する」というものです。Aさん単独の時、AさんとBさんの時、全員の時など、すべての組み合わせを試して、各メンバーが加わることでどれだけ成果が向上したかを計測し、その平均を取ります。
SHAPでは、この考え方を特徴量に適用します。ある特徴量が存在する場合と存在しない場合で、予測値がどれだけ変化するかを、すべての特徴量の組み合わせで評価し、その平均を取ることで、各特徴量の重要度を算出します。
SHAPの特徴
SHAPには以下のような優れた性質があります。
- 加法性: すべての特徴量のSHAP値を合計すると、ベースライン(平均予測値)からの予測値の差分と一致します。これにより、各特徴量の貢献が定量的に理解できます。
- 一貫性: ある特徴量の貢献が増加した場合、そのSHAP値も必ず増加します。直感的に納得できる性質です。
- モデル非依存: どんな機械学習モデルにも適用できます(ただし計算効率はモデルによって異なります)。
LIMEとは何か
LIME(Local Interpretable Model-agnostic Explanations)は、「局所的に」モデルを解釈する手法です。複雑なモデル全体を理解するのは困難ですが、特定の1つのデータポイント周辺では、シンプルな線形モデルで近似できるという考え方に基づいています。
LIMEの動作原理
LIMEは以下のステップで動作します。
- 説明したいデータポイントの周辺に、人工的なサンプルデータを大量に生成します
- これらのサンプルを元のモデルで予測し、ラベルを付けます
- 元のデータポイントに近いサンプルほど重要視する重み付けを行います
- この重み付きデータセットに対して、解釈可能なシンプルなモデル(通常は線形モデル)を学習させます
- このシンプルなモデルの係数から、各特徴量の重要度を読み取ります
レストランでの例えると、ある料理の味(予測)を説明する際、世界中の全料理のレシピ(グローバルな説明)を分析するのではなく、その料理に似た周辺の料理だけを調べて(局所的な説明)、「この料理が美味しいのは主にこのスパイスのおかげ」と説明するようなものです。
実装の準備
それでは実際にPythonを使ってSHAPとLIMEを実装していきましょう。まず必要なライブラリをインストールします。
# 必要なライブラリのインストール
pip install shap lime scikit-learn pandas numpy matplotlib次に、説明用のデータセットを準備します。今回はscikit-learnに含まれる住宅価格データセット(California Housing)を使用します。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
# 日本語表示のための設定
plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
# データセットの読み込み
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = pd.Series(housing.target, name='Price')
# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# データの標準化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"訓練データ数: {len(X_train)}")
print(f"テストデータ数: {len(X_test)}")
print(f"\n特徴量: {list(X.columns)}")このコードでは、カリフォルニアの住宅価格を予測するためのデータを準備しています。特徴量には、収入の中央値、築年数、部屋数などが含まれています。データを訓練用とテスト用に分割し、さらに標準化することで、モデルの学習を安定させています。
訓練データ数: 16512
テストデータ数: 4128
特徴量: ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude']モデルの訓練
解釈性を説明するために、まずランダムフォレストモデルを訓練します。ランダムフォレストは高精度ですが、内部構造が複雑なため、予測根拠の説明が必要となる典型的なケースです。
# ランダムフォレストモデルの訓練
model = RandomForestRegressor(
n_estimators=100,
max_depth=10,
random_state=42,
n_jobs=-1
)
# 標準化していないデータで学習(SHAPとLIMEが特徴量名を扱いやすくするため)
model.fit(X_train, y_train)
# モデルの精度を確認
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print(f"訓練データでのR2スコア: {train_score:.4f}")
print(f"テストデータでのR2スコア: {test_score:.4f}")このモデルは、100本の決定木を組み合わせたアンサンブルモデルです。個々の決定木の判断を平均することで、高い予測精度を実現していますが、「なぜその予測値になったのか」を理解するのは容易ではありません。
訓練データでのR2スコア: 0.8719
テストデータでのR2スコア: 0.7737SHAPによる特徴量重要度の可視化
ここからがSHAPの本領です。まず、グローバルな特徴量重要度を可視化してみましょう。
import shap
# SHAPのExplainerを初期化(TreeExplainerは決定木系のモデルで高速に動作)
explainer = shap.TreeExplainer(model)
# テストデータの一部(100サンプル)でSHAP値を計算
shap_values = explainer.shap_values(X_test.iloc[:100])
# Summary Plot: 各特徴量の重要度と影響の方向を可視化
shap.summary_plot(shap_values, X_test.iloc[:100], plot_type="bar")
plt.tight_layout()
plt.savefig('shap_importance.png', dpi=150, bbox_inches='tight')
plt.show()
このコードでは、TreeExplainerを使ってランダムフォレストモデルのSHAP値を効率的に計算しています。summary_plotは、どの特徴量が予測に最も影響を与えているかを、平均的なSHAP値の絶対値の大きさで示します。
棒グラフで表示されるこのプロットは、実務での報告資料に最適です。「このモデルは主に収入と立地で価格を判断している」といった説明を、定量的な根拠とともに示すことができます。
さらに詳細な情報を含むビースウォームプロット(beeswarm plot)も作成してみましょう。
# Beeswarm Plot: 特徴量の値と影響の関係を可視化
shap.summary_plot(shap_values, X_test.iloc[:100])
plt.tight_layout()
plt.savefig('shap_beeswarm.png', dpi=150, bbox_inches='tight')
plt.show()
このプロットでは、各ドット(データポイント)が色分けされており、特徴量の値が高いか低いかを示します。例えば、収入(MedInc)の値が高い(赤色)ほど、SHAP値が正(価格を押し上げる)の方向に分布していることが視覚的に理解できます。
SHAPによる個別予測の説明
次に、特定の1つのデータポイントについて、なぜその予測値になったのかを詳しく説明してみましょう。
# 説明したいサンプルを1つ選択
sample_index = 0
sample = X_test.iloc[sample_index:sample_index+1]
# このサンプルの予測値
prediction = model.predict(sample)[0]
print(f"予測された住宅価格: ${prediction:.2f} (単位: 100,000ドル)")
print(f"実際の価格: ${y_test.iloc[sample_index]:.2f}")
# このサンプルのSHAP値を計算
sample_shap_values = explainer.shap_values(sample)
# Waterfall Plot: 予測値がどのように構成されているかを可視化
shap.waterfall_plot(
shap.Explanation(
values=sample_shap_values[0],
base_values=explainer.expected_value,
data=sample.iloc[0],
feature_names=sample.columns.tolist()
)
)
plt.tight_layout()
plt.savefig('shap_waterfall.png', dpi=150, bbox_inches='tight')
plt.show()予測された住宅価格: $0.57 (単位: 100,000ドル)
実際の価格: $0.48
ウォーターフォールプロットは、ベースライン(全データの平均予測値)から出発して、各特徴量がどれだけ予測を押し上げたり押し下げたりしているかを、滝のように積み上げて表示します。
例えば、「このサンプルの予測価格が高いのは、収入が高く(+0.5)、海に近い(+0.3)ためですが、築年数が古い(-0.2)ことで少し下がっています」といった説明が可能になります。
もう1つ、Force Plotという別の可視化方法も見てみましょう。
# Force Plot: インタラクティブな可視化
shap.force_plot(
explainer.expected_value,
sample_shap_values[0],
sample.iloc[0],
matplotlib=True
)
plt.tight_layout()
plt.savefig('shap_force.png', dpi=150, bbox_inches='tight')
plt.show()
Force Plotは、予測を押し上げる特徴量(赤)と押し下げる特徴量(青)を視覚的に示します。矢印の長さが影響の大きさを表しており、直感的に理解しやすい表現です。
LIMEによる局所的な説明
次にLIMEを使って、同じサンプルを別の角度から説明してみましょう。LIMEはモデルに依存しない手法なので、どんなモデルにも適用できるのが強みです。
from lime import lime_tabular
# LIMEのExplainerを初期化
lime_explainer = lime_tabular.LimeTabularExplainer(
training_data=X_train.values,
feature_names=X_train.columns.tolist(),
mode='regression',
random_state=42
)
# 同じサンプルをLIMEで説明
lime_explanation = lime_explainer.explain_instance(
data_row=sample.values[0],
predict_fn=model.predict,
num_features=8 # すべての特徴量を表示
)
# 説明を表示
print("\nLIMEによる予測の説明:")
for feature, weight in lime_explanation.as_list():
print(f"{feature}: {weight:.4f}")LIMEによる予測の説明:
MedInc <= 2.57: -0.9365
AveOccup > 3.28: -0.3636
34.26 < Latitude <= 37.72: -0.2249
-121.81 < Longitude <= -118.51: 0.1036
1.01 < AveBedrms <= 1.05: -0.0428
18.00 < HouseAge <= 29.00: -0.0305
AveRooms <= 4.45: 0.0148
1167.00 < Population <= 1726.00: -0.0085LIMEは、説明対象のサンプル周辺でシンプルな線形モデルを学習し、その係数から各特徴量の重要度を算出します。as_list()メソッドは、各特徴量とその係数をリスト形式で返します。
LIMEの説明を視覚化してみましょう。
# LIMEの説明を棒グラフで可視化
fig = lime_explanation.as_pyplot_figure()
plt.tight_layout()
plt.savefig('lime_explanation.png', dpi=150, bbox_inches='tight')
plt.show()
この棒グラフは、正の値(予測を押し上げる)と負の値(予測を押し下げる)で色分けされ、各特徴量の局所的な影響を示します。
SHAPとLIMEの比較
ここまで両方の手法を見てきましたが、実務ではどちらを使うべきでしょうか? それぞれの特徴を比較してみましょう。
計算速度: SHAPのTreeExplainerは決定木系モデルで非常に高速ですが、一般的なKernelExplainerは遅くなります。LIMEは中程度の速度です。
理論的根拠: SHAPはゲーム理論に基づく厳密な定義があり、加法性などの望ましい性質を満たします。LIMEは直感的ですが、理論的保証は弱めです。
説明の安定性: SHAPは決定論的で、同じデータに対して常に同じ結果を返します。LIMEはサンプリングベースのため、実行するたびに少し結果が変わります。
適用範囲: どちらもモデル非依存ですが、SHAPは特定のモデル向けの最適化版が用意されています。
実務では、以下のような使い分けが推奨されます。
# 複数サンプルでの説明の安定性を比較
np.random.seed(42)
sample_indices = np.random.choice(len(X_test), 5, replace=False)
print("SHAPとLIMEの説明の比較\n")
for idx in sample_indices:
sample = X_test.iloc[idx:idx+1]
# SHAP
shap_vals = explainer.shap_values(sample)[0]
shap_importance = pd.Series(
np.abs(shap_vals),
index=X_test.columns
).sort_values(ascending=False)
# LIME
lime_exp = lime_explainer.explain_instance(
sample.values[0],
model.predict,
num_features=8
)
lime_dict = dict(lime_exp.as_list())
print(f"サンプル {idx}:")
print(f" SHAPで最重要: {shap_importance.index[0]}")
print(f" LIMEで最重要: {list(lime_dict.keys())[0].split('<=')[0].split('>')[0].strip()}")
print()SHAPとLIMEの説明の比較
サンプル 949:
SHAPで最重要: AveOccup
LIMEで最重要: 2.57 < MedInc
サンプル 3168:
SHAPで最重要: HouseAge
LIMEで最重要: 34.26 < Latitude
サンプル 2080:
SHAPで最重要: AveOccup
LIMEで最重要: AveOccup
サンプル 1210:
SHAPで最重要: AveOccup
LIMEで最重要: 2.82 < AveOccup
サンプル 2553:
SHAPで最重要: Latitude
LIMEで最重要: MedIncこのコードでは、複数のサンプルに対して両手法を適用し、どの特徴量が最も重要と判断されるかを比較しています。多くの場合、両手法は似た結論を導きますが、微妙な違いも観察できます。
実務での活用例
最後に、実務でよく必要となる「説明資料用のグラフ」の作成例を示します。
# 実務向け: 複数サンプルの説明をまとめたレポート作成
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. グローバルな特徴量重要度
ax = axes[0, 0]
feature_importance = pd.Series(
np.abs(shap_values).mean(axis=0),
index=X_test.columns
).sort_values(ascending=True)
feature_importance.plot(kind='barh', ax=ax, color='steelblue')
ax.set_title('Feature Importance (SHAP)', fontsize=12, fontweight='bold')
ax.set_xlabel('Mean |SHAP value|')
# 2. 高価格サンプルの説明
ax = axes[0, 1]
high_price_idx = y_test.idxmax()
high_price_sample = X_test.loc[high_price_idx:high_price_idx]
high_shap = explainer.shap_values(high_price_sample)[0]
sorted_indices = np.argsort(np.abs(high_shap))[::-1][:5]
top_features = X_test.columns[sorted_indices]
top_shap_values = high_shap[sorted_indices]
colors = ['red' if x > 0 else 'blue' for x in top_shap_values]
ax.barh(range(len(top_features)), top_shap_values, color=colors)
ax.set_yticks(range(len(top_features)))
ax.set_yticklabels(top_features)
ax.set_title('High Price Sample Explanation', fontsize=12, fontweight='bold')
ax.set_xlabel('SHAP value')
ax.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
# 3. 低価格サンプルの説明
ax = axes[1, 0]
low_price_idx = y_test.idxmin()
low_price_sample = X_test.loc[low_price_idx:low_price_idx]
low_shap = explainer.shap_values(low_price_sample)[0]
sorted_indices = np.argsort(np.abs(low_shap))[::-1][:5]
top_features = X_test.columns[sorted_indices]
top_shap_values = low_shap[sorted_indices]
colors = ['red' if x > 0 else 'blue' for x in top_shap_values]
ax.barh(range(len(top_features)), top_shap_values, color=colors)
ax.set_yticks(range(len(top_features)))
ax.set_yticklabels(top_features)
ax.set_title('Low Price Sample Explanation', fontsize=12, fontweight='bold')
ax.set_xlabel('SHAP value')
ax.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
# 4. 予測値とSHAP値の関係
ax = axes[1, 1]
predictions = model.predict(X_test.iloc[:100])
mean_abs_shap = np.abs(shap_values).mean(axis=1)
ax.scatter(predictions, mean_abs_shap, alpha=0.5, color='steelblue')
ax.set_xlabel('Predicted Price')
ax.set_ylabel('Mean |SHAP value|')
ax.set_title('Prediction vs Explanation Magnitude', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.savefig('business_report.png', dpi=150, bbox_inches='tight')
plt.show()
このコードは、1枚の画像に4つの重要な情報をまとめています。左上はモデル全体の特徴量重要度、右上と左下は極端なケース(高価格・低価格)の個別説明、右下は予測の確信度と説明の複雑さの関係を示しています。こうした包括的な資料があれば、ステークホルダーへの説明がスムーズになります。
まとめ
この記事では、機械学習モデルの解釈性を高めるためのSHAPとLIMEという2つの強力な手法について、Pythonとscikit-learnを使った実装方法を解説しました。
SHAPはゲーム理論に基づく厳密な手法で、特徴量の貢献を公平に評価できます。グローバルな重要度と個別の予測説明の両方に優れており、実務での説明資料作成に最適です。
LIMEは局所的な近似により、直感的な説明を提供します。モデルの種類を問わず適用でき、サンプル周辺での挙動を理解するのに役立ちます。
これらの手法を使いこなすことで、モデルの予測根拠を明確に示し、AIシステムへの信頼性を高めることができます。特に、ビジネスでの意思決定支援やコンプライアンス対応において、解釈性は今後ますます重要になっていくでしょう。
まずは本記事のコードを実際に動かしてみて、自分のデータセットやモデルに適用してみてください。予測精度だけでなく、「なぜその予測なのか」を説明できるモデル開発を目指しましょう。
最後まで読んでいただきありがとうございました。


コメント