情報系大学院生の勉強メモ

主に機械学習,Python

MonkeyTypeを使ってみた

今回は軽めの記事です。

MonkeyTypeとは

github.com

Instagram社によって2017年に公開された型ヒントのスタブファイルと型アノテーションを自動生成するツールです。 python>=3.6で使えるそうです。

インストールはpip install MonkeyTypeでできます。

自動で型アノテーションできるならすごい便利そうってことで使ってみました。

Experiment

まずは公式にも載っている例で試してみます。

int_module.py

def add(a, b):
    return a + b

main.py

from int_module import add

def main():
    add(1,3)

if __name__ == '__main__':
    main()

このようなコードを書き、

monkeytype run main.pyを実行します。

するとmain.pyが実行されてmonkeytype.sqlite3ファイルのsqliteデータベースにtrace情報がdumpされます。

この状態でmonkeytype stub int_moduleを実行すると

def add(a: int, b: int) -> int: ...

スタブが出力されます。

monkeytype apply int_moduleを実行すると、int_module.pyが書き換わります。

def add(a: int,b: int) -> int:
    return a+b

このように書き換わっていました。ちゃんと型アノテーションがされています。

int型以外ではどうなるか気になったので別の型も試してみます。

list_module.py

def double_list(x):
    return x*2

pandas_module.py

import pandas as pd
def convert_dict_to_df(table):
    return pd.DataFrame(table)

引数にList型を取る関数と引数としてDict型を取りpandasのDataFrameとして返す関数も試してみます。

実行するmain.py

from int_module import add,mul
from list_module import double_list
from pandas_module import convert_dict_to_df

def main():
    add(1,3)
    double_list([1,2,3])
    d={'key1': [1,2,3], 'key2': [4,5,6], 'key3': [7,8,9]}
    convert_dict_to_df(d)
    return

if __name__ == '__main__':
    main()

monkeytype run main.pyを実行した後,

monkeytype apply list_modulemonkeytype apply pandas_module を実行すると

list_module.py

from typing import List

def double_list(x: List[int]) -> List[int]:
    return x*2

pandas_module.py

import pandas as pd
from pandas.core.frame import DataFrame
from typing import Dict, List

def convert_dict_to_df(table: Dict[str, List[int]]) -> DataFrame:
    return pd.DataFrame(table)

このようにアノテーションしてくれました。 from pandas.core.frame import DataFrameのようにちゃんと外部モジュールから相当する型を勝手にimportしてくれています。すごい。

Pythonのメソッド解決順序(MRO)

MROとは

MROとは、Method Resolution Orderの略で、多重継承したクラスのメソッドが呼び出されるときに,どのメソッドがどういう順番で呼び出されるかのことです。Pythonはv2.3以降C3線形化アルゴリズムというアルゴリズムで順序を特定しています。

C3線形化アルゴリズム

簡単にいうと

  • クラスAがBを継承しているなら必ずAがBより先にくる

  • AがB,Cの順番で継承しているならBがCより前にくる

ように並べるアルゴリズムです。

公式ドキュメントがより詳しいです↓

The Python 2.3 Method Resolution Order | Python.org

具体例

早速、具体例を見ていきます。

O = object
class F(O): pass
class E(O): pass
class D(O): pass
class C(D,F): pass
class B(D,E): pass
class A(B,C): pass

print(A.mro())

とすると

[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.E'>, <class '__main__.F'>, <class 'object'>]

と出力されます。 つまりMROは「A→B→C→D→E→F→O」です

まず

class A(B,C): pass

よりA→B→Cの順序が守られなくてはなりません。

なぜならクラスAがクラスBとCを継承しているのでAはB,Cのどちらよりも前に来ないといけなくて、またB,Cの順で継承しているのでBはCより前に来ないといけないからです。

同様に

  • B→D→E
  • C→D→F
  • D→O
  • E→O
  • F→O の順番が守られなくてはいけません。

「A→B→C→D→E→F→O」より確かにこれらの順番が守られていることがわかります。

もちろんうまく順序付けられない場合もあります。たとえば以下の例の場合があります。

class A: pass
class B(A): pass
class C(A,B): pass
print(C.mro())

これを実行すると

TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

と怒られます。

なぜなら、まずCはA,Bの順番で継承しているので、A→Bの順番が守られなくてはなりません。一方BはAを継承しているのでB→Aの順番が守られなくてはなりません。ここで矛盾が起きています。よってエラーが発生します。

MROを知らないと時間を溶かしそうなエラーですね...

最後にメソッドを付け加えて見てみましょう。

class A:
    def process(self):
        print('Aのprocess')

class B(A):
    pass

class C(A):
    def process(self):
        print('Cのprocess')

class D(B,C):
    pass

obj = D()
obj.process()

継承関係を図にすると、以下のようになります。

diagram

さて、obj.process()によって何が出力されるでしょうか?

これはMROが分かれば簡単で、ここでMROはD→B→C→Aです。まずDにはprocess関数が定義されていないので次にBを見ます。Bにもprocess関数は定義されていないので次にCを見ます。Cには定義されているので、出力は

Cのprocess

になります。これだけです。またこの時Aのprocessは呼ばれません。

まとめ

  • MROはどの順番でメソッドが呼び出されるかのこと。
  • PythonMROにはC3線形化アルゴリズムが用いられている。
  • 継承関係によってはうまく順序づけられない場合もある。

参考

SHAPを用いたモデルの解釈

なんでこのモデルがこのような予測をしたのかを説明する、解釈性は近年ますます注目されています。モデルの解釈を可能にするために様々な手法が提案されていますが、その手法の一つであるSHAP(SHapley Additive exPlanations)についてまとめます。今回はTitanicのデータセット、LIghtGBMモデルを用いて、SHAPで結果の解釈を行いました。

SHAPとは

NIPS2017の「A Unified Approach to Interpreting Model Predictions」で提案された手法です。

論文はこちら

SHAPはモデルの予測結果に対する各特徴量の寄与度を求めるための手法で、寄与度として協力ゲーム理論のShapley Valueを用いています。 協力ゲーム理論のShapley Valueとは簡単にいうと、複数人で協力して報酬を得たときそれを適切に分配したときのそれぞれの分配額です。 特徴量をプレイヤーと見立ててこのShapley Valueを求めることで、モデルの予測結果に対する寄与度とします。(本当はSHAP値というちょっと違うものを使う)

実験

使用したライブラリ

SHAP

github.com

GBDT

LightGBM

データセット

データセットタイタニックのデータを使用します www.kaggle.com

また今回はSHAPを使うのが目的のため、いくつか説明変数を減らして以下の変数を使います。

変数名 意味
PClass チケットクラス。1,2,3の順でランクが高い
Sex 性別
Age 年齢
SibSp 同乗している兄弟/配偶者の数
Parch 同乗している親/子供の数
Fare 料金
Embarked 出港地
Survived 生存フラグ(0=死亡、1=生存)

言わずもがなですが、生き残るかどうかを予測するタスクになります。

コード&結果

ちょくちょく端折ります。

ライブラリのインポート。今回はこれだけ。

import os
import shap
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import train_test_split

データの読み込み。 訓練データのみ使用。

train = pd.read_csv(TRAINPATH)

変数を減らして訓練とバリデーションに分ける。

use_columns = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
X_train, X_val, y_train, y_val = train_test_split(train[use_columns], train['Survived'], test_size=0.2)

学習。パラメータは雑。

d_train = lgb.Dataset(X_train, label=y_train)
d_val = lgb.Dataset(X_val, label=y_val)

params = {
    "objective": "binary",
    "metric": {"binary_logloss",'auc'},
    "verbose": -1,
}

model = lgb.train(params, d_train, 100, valid_sets= d_val, early_stopping_rounds= 50)

SHAPのExplainerは複数あります。 今回はGBDTを用いるのでTreeExplainerを使います。

shap.initjs()
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_train)

1行目のshap.initjs()Javascriptで描画するためのおまじないです。NodeJSのインストールが必要です。

Force Plot

ForcePlotは与えられたShapValueと変数の寄与度を視覚化します。 link=logitを設定することで出力を確率に変換しています。 これによって個別の変数がどれだけ生存確率を上げたか/下げたかが見て取れます。赤色が生存確率上昇に寄与した変数、青色が生存確率現象に寄与した変数です。 1レコードをForcePlotした結果は以下のようになります。

index = 1 #何行目をforceplotするか
shap.force_plot(
    base_value=explainer.expected_value[1], 
    shap_values=shap_values[1][index,:], 
    features=X_train.iloc[index,:],
    link='logit',
    matplotlib=True
)

force_plot 年齢が2歳であることが生存確率を上げていますが、Pclass=3(一番低いチケットクラス)やSibSp=4が生存確率を下げていることが分かります。waterfall_plot()を用いても表示方法が変わるだけで同じようなことができます。

また、全レコードに対して可視化することもでき、その結果は以下のようになります。 force_plot2

特徴量の分布が似ているもの同士に並べてくれたり、SHAP ValueでSortして表示してくれたりします。

Waterfall Plot

ForcePlotの表示をわかりやすくしたものです。

値はSHAP Valueです。

index = 1
shap.waterfall_plot(
    expected_value=explainer.expected_value[1], 
    shap_values=shap_values[1][index,:], 
    features=X_train.iloc[index,:],
    show=True
)

waterfall

Dependence Plot

Dependence Plotでは横軸に実際の値、縦軸にSHAP Valueが取られています。 例えば年齢をDependence Plotしてみます。

shap.dependence_plot(
    ind="Age",
    shap_values=shap_values[1],
    features=X_train,
    interaction_index=None
)

dependence これを見ると子供であることが生存によく寄与していることなどが分かります。

Decision Plot

Decision Plotでは予測の過程を可視化することができ、より個々の影響を見ることができます。 試しに10人分plotしてみると以下のようになります。

shap.decision_plot(
    base_value=explainer.expected_value[1], 
    shap_values=shap_values[1][:10,:], 
    features=X_train.iloc[:10,:],
    link="logit",
    show=True
)

decision

どの変数でSHAP Valueが上がったのか/下がったのかが分かりやすいですねー。

Summary Plot

Summary Plot はもっと大局的に結果を見たい場合に便利です。 バイオリンプロット的なことができます。点が個々のサンプルを表し、予測結果への寄与度が大きい変数順に上から並んでいます。

shap.summary_plot(
    shap_values=shap_values[1], 
    features=X_train,
    max_display=5
)

summary

plot_type='bar'とすると、シンプルに棒グラフで表示できます。 summary_bar

注意

SHAPは、"解きたい問題の解釈" をしているわけではなく、あくまで "学習済みモデルの解釈" をしようとしています。そのためモデルが良いモデルでなかったら、解釈も良いものでないということに注意する。

まとめ

  • SHAPを用いると、「どの特徴量がどのくらい予測結果に寄与したか」によって機械学習モデルを解釈できます。

  • SHAPは協力ゲーム理論のShapley Valueの考え方を用いています。

  • SHAPのExplainerは複数あります。今回はTreeExplainerを用いて様々なプロットを行いました。