物性予測モデルをSHAPで解釈してみる

#はじめに
SHAPを実務に使うためにやってみたメモ

# 環境
本記事では以下を事前にインストールしておく必要がある。

- Python
- numpy
- pandas
- matplotlib
- SHAP
- scikit-learn
- LightGBM
- RDKit

# モデル構築
まずはRDKitを使ってモデルを作成してみよう。

まずはモジュールのインポート

```python
import numpy as np
import pandas as pd
from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Descriptors
from rdkit.ML.Descriptors import MoleculeDescriptors
import lightgbm as lgb
from lightgbm import LGBMRegressor
from sklearn.feature_selection import VarianceThreshold
from sklearn.model_selection import train_test_split
import shap
```

続いて、smiles列のあるcsvファイルからデータを読み込み記述子計算および、目的変数の取得を行う。
今回はMoleculeNetのデータセットに含まれている溶解性の回帰モデルを試すこととする。

```python
# ファイルの読み込み
import codecs
with codecs.open('../datas/raw/delaney-processed.csv', 'r', 'utf-8', 'ignore') as f:
    df = pd.read_csv(f)
# SMILESのデータを取り出す
smiles_list = df["smiles"].values

# SMILESのリストをMOLに変換
mols = []
for i, smiles in enumerate(smiles_list):
    mol = Chem.MolFromSmiles(smiles)
    if mol:
        mols.append(mol)
    else:
        print("{0} th mol is not valid".format(i))

# RDKitの記述子計算
descriptor_names = [descriptor_name[0] for descriptor_name in Descriptors._descList]
descriptor_calculator = MoleculeDescriptors.MolecularDescriptorCalculator(descriptor_names)
rdkit_descriptors_results = [descriptor_calculator.CalcDescriptors(mol) for mol in mols]
df_rdkit = pd.DataFrame(rdkit_descriptors_results, columns = descriptor_names, index=names_list)

# 目的変数の取得
df_t = df["measured log solubility in mols per litre"]
```

学習データとテストデータに分割しよう。

```python
# 学習データとテストデータに分割
df_train_X, df_test_X, df_train_t, df_test_t = train_test_split(df_rdkit, df_t, test_size=0.3, random_state=0)
```

ここでのポイントはpandasのデータフレームの状態で分割することである。
**pandasデータフレームのままSHAPのメソッドを呼び出すと、可視化の際にpandasの列名が特徴名として表示される**ので大変便利である。


最後に学習してモデルを構築しよう。

```python
model = LGBMRegressor()
model.fit(df_train_X, df_train_t)
```

これで準備は整った。

#SHAPによるモデルの解釈

ほぼ、[SHAPを使って機械学習モデルと対話する](https://www.medi-08-data-06.work/entry/how_to_use_shap) を参考にした。

###下準備

下準備はこれだけだ。

```python
shap.initjs()
explainer = shap.TreeExplainer(model=model)
shap_values = explainer.shap_values(X=df_test_X)
```

### force_plot

まずはforce_plotによりテストデータの最初の1件だけのSHAPの結果を見てみよう。

```python
shap.force_plot(explainer.expected_value, shap_values[0], df_test_X.iloc[0, :])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/563b58fe-6cd8-dd32-7cac-2ab4c100c45c.png)

予測値は-1.43で、MolLogPという説明変数の影響により、予測値が平均(-3.046)よりも高くなっているということが見てとれる。
**MolLogPが大きいから予測値が高いといっているわけではない**ことに注意しよう。

続いてテストデータの最初の100件のSHAPの結果をまとめて見てみよう。

```python
shap.force_plot(explainer.expected_value, shap_values[0:100], df_test_X.iloc[0:100,:])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/bd127829-3f8a-cc56-c440-8c7a86b38577.png)

予測値が高い順から低い順に100件のSHAPの結果が描画される。
一番太い帯で表示されているのがMolLogPで、予測値が-5よりも小さい場合、特にMolLogPが大きな影響を与えているということが見て取れる。

次にY軸を「MolLogP effects」としてみよう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/50136292-9ef7-994f-d325-8690cd286a8b.png)

こうすると、MolLogPの効果だけを取り出してみることができる。特定の説明変数の影響をみるにはこちらが便利だ。

### sammary_plot
続いて sammary_plotを使ってみよう。

```python
shap.summary_plot(shap_values, df_test_X)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/f3772062-88be-21ef-b6c2-37c568a1f02a.png)

これの何が便利かというと、**説明変数の大きさが予測値にどう影響を与えているのか**が分かる点だ。
例えばMolLogPについては、大きい(=ピンクが濃い)程、予測値にマイナスの影響を与えていることが分かる。


### dependence_plot

続いてdependancy_plotを見てみよう。

```python
shap.dependence_plot("MolLogP", shap_values, df_test_X, interaction_index=10)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/0c2f0c1d-5972-98c2-d311-e020ac9d307b.png)

これはある説明変数の予測値への影響が、他の説明変数に依存しているかを確認することができる。
予測のメカニズムを解析する際に、非常に役立ちそうである。


### decision_plot

続いてdecision_plotを見てみよう。これはデータ毎の説明変数の影響を重ねてみることができる。
テストデータの最初の100件を可視化してみよう。

```
shap.decision_plot(explainer.expected_value, shap_values[0:100], df_test_X.iloc[0:100,:])
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/b7ca108a-29b8-7599-4d35-bea34b0212a7.png)

feature_order='hclust'を指定すると、階層的クラスタリングに基づき説明変数が表示され、似たような傾向の予測パスをグループ化して表示することができる。

```
shap.decision_plot(explainer.expected_value, shap_values[0:100], df_test_X.iloc[0:100,:], feature_order='hclust')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/73e3edb7-dddf-91f0-1cc9-82599320978e.png)

# おわりに
ここで紹介しきれなかったものもあるので、リファレンスマニュアルを見て色々試してみたい。
予測結果の妥当性の確認にも使えそうな気がする。


#参考
- [SHAP(SHapley Additive exPlanations)で機械学習モデルを解釈する](https://dropout009.hatenablog.com/entry/2019/11/20/091450?_ga=2.185612329.1656579419.1574324138-1185090719.1545096259)
- [SHAPを使って機械学習モデルと対話する](https://www.medi-08-data-06.work/entry/how_to_use_shap)
- [Welcome to the SHAP documentation](https://shap.readthedocs.io/en/latest/index.html)