まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

Evidentlyで機械学習モデルの挙動を可視化する

背景

機械学習モデルは作成後にそのモデルがどのような振る舞いをするのか調べる必要がある。

理由としては

  1. 実運用上での注意点
    例えば、「このモデルは全体としてはそこそこの精度だが、20代では精度があまり良くない」といったことを把握しておくと実運用ではその点を注意して運用することができる。

  2. パフォーマンス向上のヒント
    例えば、前述のように「20代で精度があまり良くない」のであればそれをカバーできそうな特徴量がないか?という観点で特徴量を考えることができたり、分布を見ることでうまいvalidationの取り方になってるか確認することができる。

  3. 精度のモニタリングと原因探索
    例えば、ある店舗の売上予測モデルを作成して毎月そのモデルを使って予測をおこなっているとする。そのときにある時期を堺に精度がガクッと落ちた場合、振る舞いをもとになにが原因で精度が落ちたかを探索する必要があり、その原因をもとに改修をしたり再学習をする必要がある。

このように、モデルを作成した後に振る舞いをみるのは重要だが自分で可視化していくのは地味に面倒。
その手間をEvidentlyでは解決してくれる。

f:id:chito_ng:20210520123942g:plain:h400

docs.evidentlyai.com

このあたりの、なんで振る舞いを見る必要があるのん?ということに関してはEvidentlyの公式ブログにMachine Learning Monitoringシリーズとして載っているので一読すると良さげ

evidentlyai.com

何ができるか

挙動確認用のサンプルコードは公式ブログから取得できる。

github.com

出力できるレポートの種類は以下があり、主にDrift系とPerformance系に分かれる。

それぞれにおいて具体的にどのようなレポートが出力されるかは公式ドキュメントのREPORTS項目を見ると良い(以下リンクはData Drift)。

docs.evidentlyai.com

Drift

Drift系では使用しているデータに関して見ることができ、レポートの種類としては以下がある。

  • Data Drift
  • Numerical Target Drift
  • Categorical Target Drift

Data Driftでは、データセットの各特徴量の分布の可視化および、統計的に分布に差があるかの確認ができる。

evidentlyai.com

Numerical/Categorical Target Driftは予測対象となる目的変数データを見ることができる。 各レポートの種類は連続値がカテゴリカルか(つまり連続値予測か分類予測か)によって使い分ける。

evidentlyai.com

なお、 データの分布比較に関してはSweetvizも使えるがこっちの方が色々リッチ。

qiita.com

使用シチュエーション

  1. データのモニタリング 予測対象のデータの質が変わっていないかモニタリング
  2. モデルの再学習前
    新しいデータで学習する前に、意味がありそうか検証
  3. 予測対象データを変えたときのパフォーマンスの減衰をデバッグするとき
    何が変わったのか/原因かを確認
  4. モデルの挙動を知りたい
    モデル出力の変化や、特徴と予測の関係を確認

Performance

Performance系は以下があり、モデルの予測結果を見ることができる。 各レポートの種類は予測対象となるデータの種類によって使い分ける。

  • Regression Performance
  • Classification Performance
  • Probabilistic Classification Performance

なお、Classification PerformanceとProbabilistic Classification Performanceは前者は確率値を持たない場合に使う。確率値をもつ場合は前者の内容+確率に基づくレポートが入る後者を使う。
ただし、コードを読む感じだと後者は二値分類だと閾値を0.5としてクラス予測をしているようなので、後者を使う際のModel Quality Summary Metrics やConfusion Matrixといった前者と共通の部分(予測クラスも含めて計算してる箇所)は注意が必要。
任意の閾値を使いたい場合はそこだけ任意の閾値でクラス変換後に前者で見るとか、ProbClassRefConfMatrixWidgetクラスのcalculateをオーバーライドする必要がある。

evidentlyai.com

github.com

使用シチュエーション

  1. testデータを使ったモデルの振る舞いをみる
    testデータとtrainデータの推定結果と対比させることでモデルの振る舞いをみる。当たり前だが、trainデータの推定は学習時と同じデータなのでリークしており精度は良いはずだが、めっちゃ予測がうまくいくデータでどれくらいの精度なのか。testとの差はどれくらいなのか。trainでもうまく予測できない(学習しきれてない)ようなインスタンスや特徴量範囲はどのような部分なのかといったことを知ることができる(このあたり、他に見れる観点があれば教えてください)。
  2. モデルのパフォーマンスに関するレポートを作成
    このレポートを定期的なジョブとして実行し、パフォーマンスをトラッキングして他の関係者と共有する。
  3. 異なるデータのモデルのパフォーマンスを分析
    trainに使っているデータと異なるデータソースの場合どれくらいパフォーマンスが変わるかを調べる(例:trainを東京のユーザーでおこない、testとして東京のユーザーと大阪のユーザーでパフォーマンスがどれくらい変わるか比較)。
  4. モデルの再トレーニングを決める
    2のようにパフォーマンスをトラッキングすることである一定以上の精度以下になったときに再学習をかける、という判断をする
  5. エラーの多い領域を特定し、モデルのパフォーマンスを改善
    エラーバイアステーブルを使用して、エラー全体に大きく寄与しているグループや、モデルがターゲット関数を過小評価または過大評価しているグループを特定する。

Shapashとの比較

なお、2020/12にv1が出たEvidentlyとほぼ同時期の2021/01にv1が出たShapashというダッシュボードライブラリも「機械学習モデルがどうなっているか」を楽に可視化してくれる(Shapashは日本でも結構取り上げられてるがEvidentlyはあまり取り上げられてない?)

github.com

qiita.com

Evidentlyはモデルの振る舞いを、推定元データ観点でどうなっているかを中心として可視化し、それに付随してモデル/推定元データの比較をします。
ShapashはSHAPおよびLIMEを用いて、モデルにおける特徴量の寄与がどうなっているか、つまりモデルが何故そういう振る舞いをしているかを中心として可視化している。

つまり、前者はモデルの挙動をデータから確認する用途で、後者はモデルの推定結果の原因を確認する用途なので用途が異なっている(データを中心に見ていくか、結果を中心に見ていくか、とも言える)。

また、Evidentlyはデータを中心に確認するので『モデルアルゴリズムによるデータ内(特徴量毎など)での精度差異』『推定元データの違い(異なる地域や時期など)による精度差異』を見たい場合に役に立つため予測データを2つ渡し比較する機能を持っている。

挙動確認

今回は試しに、分類モデルの予測確率に関してのProbabilistic Classification Performanceを見てみる。

github.com

このデータを使った公式でのチュートリアルは以下(英語に抵抗がないならこれ読めば概ねなにができるかわかる)。本記事ではこのnotebookのRandomForestモデルをreferenceモデル、CatBoostモデルをcurrentモデルとして比較した結果を見ていく。

evidentlyai.com

データはKaggleのIBM HR Analytics Employee Attrition & Performanceという、従業員が退職するかどうかを予測するコンペデータを使用している模様。

www.kaggle.com

データの日本語説明

qiita.com

レポート用のデータ準備

レポート出力には以下のオブジェクトが必要になる

  • referenceとなるモデルを用いた予測結果(今回の場合、予測結果yesの確率とnoの確率それぞれ) + 実際の目的変数 + 特徴量
  • currentとなるモデルを用いた予測結果(今回の場合、予測結果yesの確率とnoの確率それぞれ) + 実際の目的変数 + 特徴量
# train/testそれぞれでモデルを適応して確率を予測
train_probas = pd.DataFrame(rf.predict_proba(train_data[features]))
train_probas.columns = ['no', 'yes']

test_probas = pd.DataFrame(rf.predict_proba(test_data[features]))
test_probas.columns = ['no', 'yes']

# 特徴量データに目的変数Attrition列を追加(復元)して予測確率とマージ
train_data.reset_index(inplace=True, drop=True)
train_data['Attrition'] = ['no' if x == 0 else 'yes' for x in train_y]
rf_merged_train = pd.concat([train_data, train_probas], axis = 1)

test_data.reset_index(inplace=True, drop=True)
test_data['Attrition'] = ['no' if x == 0 else 'yes' for x in test_y]
rf_merged_test = pd.concat([test_data, test_probas], axis = 1)

f:id:chito_ng:20210519154318p:plain

  • 上記2オブジェクトの各列が何を表すかを、「target」「prediction」「numerical_features」「categorical_features」で指定したdictオブジェクト
column_mapping = {}

# 目的変数の列名を指定
column_mapping['target'] = 'Attrition'

# 推定確率の列名を指定
column_mapping['prediction'] = ['yes', 'no']

# 連続値変数の特徴量を指定
column_mapping['numerical_features'] = ['Age','DailyRate', 'DistanceFromHome', 'Education',
       'EmployeeNumber', 'EnvironmentSatisfaction', 'HourlyRate',
       'JobInvolvement', 'JobLevel', 'JobSatisfaction', 'MonthlyIncome',
       'MonthlyRate', 'NumCompaniesWorked', 'PercentSalaryHike',
       'PerformanceRating', 'RelationshipSatisfaction', 'StockOptionLevel',
       'TotalWorkingYears', 'TrainingTimesLastYear', 'WorkLifeBalance',
       'YearsAtCompany', 'YearsInCurrentRole', 'YearsSinceLastPromotion',
       'YearsWithCurrManager']

# カテゴリカル変数の特徴量を指定
column_mapping['categorical_features'] = ['b_travel_Non-Travel',
       'b_travel_Travel_Frequently', 'b_travel_Travel_Rarely',
       'department_Human Resources', 'department_Research & Development',
       'department_Sales', 'edu_field_Human Resources',
       'edu_field_Research & Development', 'edu_field_Sales', 'gender_bin',
       'job_role_Healthcare Representative', 'job_role_Human Resources',
       'job_role_Laboratory Technician', 'job_role_Manager',
       'job_role_Manufacturing Director', 'job_role_Research Director',
       'job_role_Research Scientist', 'job_role_Sales Executive',
       'job_role_Sales Representative', 'marital_Divorced', 'marital_Married',
       'marital_Single', 'overtime_No', 'overtime_Yes']

f:id:chito_ng:20210519154619p:plain

これらオブジェクトをもとにDashboard関数を用いて、第一引数に上記referenceオブジェクト、第二引数にcurrentオブジェクト、 column_mappingに上記dictでの列情報、tabsにレポートの種類(モデルの種類)を指定する。今回の場合以下のように出力。

report = Dashboard(rf_merged_train, rf_merged_test, column_mapping = column_mapping, 
                       tabs=[ProbClassificationPerformanceTab])

# htmlとしてダッシュボードを保存
report.save('reports/ibm_hr_attrition_baseline_performance.html')

なお、

report = Dashboard(rf_merged_train, rf_merged_test, column_mapping = column_mapping, 
                       tabs=[DriftTab, ProbClassificationPerformanceTab])

report.save('reports/ibm_hr_attrition_baseline_performance.html')

のようにすると、Data Driftも一緒に出力されるが今回はモデル適用元データは同じ(モデルアルゴリズムが違う)なので見る必要がないので割愛する。

docs.evidentlyai.com

また、まとめてhtml出力するのではなく任意のグラフだけnotebook上に埋め込むことも可能。

結果の解釈

レポートの結果を上から見ていく。なお、データは共通してtestデータを使い、使用アルゴリズムがReferenceはRandomForest、CurrentはCatBoostを使って推定した結果となる。つまり、各モデルでどっちを使ったほうが良いか考える。

docs.evidentlyai.com

Macroでの評価指標(Model Quality With Macro-average Metrics)

分類モデルの基本的な評価が一覧化されている。総じてCurrentの方が良い

f:id:chito_ng:20210519155421p:plain

目的変数(実測)のバランス(Class Representation)

推定するデータの目的変数の各クラスの実際の値がどうなっているか。

今回Reference/CurrentでRF vs CatBoostとなりモデルアルゴリズムは異なっているが元データは同じなのでどちらも同じ内容が表示されている。実際には同アルゴリズムを用いて例えば先月のデータをReference、今月のデータをCurrentにして比較レポートを作成する場合に役に立つ。

f:id:chito_ng:20210519160223p:plain

Confusion Matrix

よくあるConfusion Matrix(混合行列)。
冒頭での「Macroでの評価指標(Model Quality With Macro-average Metrics)」ではCurrentの方が全評価指標において勝っていたが、それはMacro-average で見た場合で個別でみるとReferenceの方がNoに対しての正解がCurrentより勝っていることがわかる(ほぼ一緒だけど)

f:id:chito_ng:20210519160156p:plain

Microでの評価指標(Quality Metrics by Class)

Microでの評価指標に変換。先程Confusion Matrixで見たように、Referenceはnoに対するRecallがCurrentより勝っている。

f:id:chito_ng:20210519160652p:plain

分類の質(Class Separation Quality)

キャプチャでは、各インスタンスに対してyesと推定される確率を可視化している。実際にyesのものを赤色、実際がno(other)が灰色(noではなくother表示なのは多クラス分類のときと統一的なインターフェイスにするためか?)。
ちなみに、グラフはマウスポインタで直接拡大や強調などが可能。

実際がyes(赤色)はReferenceとCurrentであまり違いがなさそうだけど、no(灰色)に関してはCurrentの方が分散が小さいかつはっきりと分類(noでyesの確率が全体的に低い)できていることがわかる。

ただし、左右どちらでもnoなのに0.7前後となるインスタンスが存在しているのでこのインスタンスに注目して色々なデータを見ていくなどして、こいつをうまいこと分類できそうな特徴量を探すと精度向上に効くかもしれない。

また、yes/noの閾値をどこで変えると良さげなのかなどを考えることもできる。

f:id:chito_ng:20210519161011p:plain

確率分布(Probability Distribution)

「分類の質(Class Separation Quality)」とみている値は同じだが先程は密度(正確には頻度)がわかりづらかったのでその観点で可視化されている。

改めてちゃんと密度観点でみるとCurrenrtの方が評価指標はいいのだけど、実際yesなのにyesの確率が極端に低いものが出やすい。noとも併せて考えると、Referenceより極端な値が出やすいモデルなのかもしれない。

f:id:chito_ng:20210519162157p:plain

ROCとPrecision-Recall Curve

f:id:chito_ng:20210519162508p:plain

閾値をどこで切るか(Precision-Recall Table)

Precision-Recall Curveでプロットされた各点についての詳細が表示されている。閾値を決める際は一般的にPrecisionとRecall のトレードオフなのでこの表をソートしたりしてどの閾値にするのが良いのか考える。
キャプチャの囲い部分の見方としては、Precisionを高い順に並べるとyesの閾値(Prob)を0.42がPrecision最大となる。0.42以上となるインスタンスは18件(Count)となりこれはProbが上位4.9%が対象となることを指す。この18件のうちTPは12FPは6件。

f:id:chito_ng:20210519163904p:plain

実際にClass Separation Qualityに閾値0.42を引くと下記のようになる。

f:id:chito_ng:20210519165033p:plain

各特徴量での予測値(Classification Quality By Feature)

Allタブでは単純に特徴量の値に応じたヒストグラムができあがる(Categorial Target Driftと同じ内容)。予測値自体は出ないので今回のレポートでは左右で同じになる。

f:id:chito_ng:20210519165856p:plain

次に、yesとなる予測確率を年齢毎にみる。

挙動をみていく。20歳以下は実際のyes/noに関わらずyesになる確率が高くなる傾向にありそう。つまり、20歳以下だととりあえずyesにしがち(yesの確率を高く吐きがち)なモデルなような気がするので20歳以下に関しての予測性能は低いモデルと解釈できるので実運用上では注意した方が良さそう(特にReference)。なお、原因としてはおそらく20歳以下のサンプルサイズが小さいことが考えられる。

f:id:chito_ng:20210519165939p:plain

(今回は良い例が見当たらなかったが)もし実運用で「ある特徴量がある値(例えば給料がミドルレンジ)の退職予測を中心におこないたい」場合、その特徴量値に関してのみ着目して予測精度が良さそうな方のアルゴリズムを選択するとよい。言い換えると、評価指標のみでモデルを選択するのではなく、実運用上での目的によっては特定の特徴量のインスタンスに対してうまく予測するようなモデルを選択するという考え方もできる。