MonkeyTypeを使ってみた
今回は軽めの記事です。
MonkeyTypeとは
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_module
と
monkeytype 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()
継承関係を図にすると、以下のようになります。
さて、obj.process()
によって何が出力されるでしょうか?
これはMROが分かれば簡単で、ここでMROはD→B→C→Aです。まずDにはprocess関数が定義されていないので次にBを見ます。Bにもprocess関数は定義されていないので次にCを見ます。Cには定義されているので、出力は
Cのprocess
になります。これだけです。またこの時Aのprocessは呼ばれません。
まとめ
参考
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
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 )
年齢が2歳
であることが生存確率を上げていますが、Pclass=3
(一番低いチケットクラス)やSibSp=4
が生存確率を下げていることが分かります。waterfall_plot()
を用いても表示方法が変わるだけで同じようなことができます。
また、全レコードに対して可視化することもでき、その結果は以下のようになります。
特徴量の分布が似ているもの同士に並べてくれたり、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 )
Dependence Plot
Dependence Plotでは横軸に実際の値、縦軸にSHAP Valueが取られています。 例えば年齢をDependence Plotしてみます。
shap.dependence_plot( ind="Age", shap_values=shap_values[1], features=X_train, interaction_index=None )
これを見ると子供であることが生存によく寄与していることなどが分かります。
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 )
どの変数でSHAP Valueが上がったのか/下がったのかが分かりやすいですねー。
Summary Plot
Summary Plot はもっと大局的に結果を見たい場合に便利です。 バイオリンプロット的なことができます。点が個々のサンプルを表し、予測結果への寄与度が大きい変数順に上から並んでいます。
shap.summary_plot( shap_values=shap_values[1], features=X_train, max_display=5 )
plot_type='bar'
とすると、シンプルに棒グラフで表示できます。
注意
SHAPは、"解きたい問題の解釈" をしているわけではなく、あくまで "学習済みモデルの解釈" をしようとしています。そのためモデルが良いモデルでなかったら、解釈も良いものでないということに注意する。