Googleのデータ可視化&モデル分析ツール What-if Toolで覗いてみるTitanic生存者予測

この記事はNTTコミュニケーションズアドベントカレンダーです。前の記事は@BootCamp_2019の[認めたくないものだな… Neural Networkの力学系表現というものを](https://qiita.com/BootCamp_2019/items/478334a37b54f9641725)でした。

@kirikeiと申します。データサイエンティストの傍らデータ分析（の特に説明性）に関わる研究をやってます。普段は[これ](https://qiita.com/kirikei/items/a1639954ce5ccaf7ac3c)や[これ](https://qiita.com/kirikei/items/cafae6d3a96e9743e799)のようなデータ分析系や論文解説記事を挙げることが多いです。

今回は最近提供開始となった**Googleの可視化ツール [What-if Tool](https://pair-code.github.io/what-if-tool/)**を紹介します。

# この記事でやること
* What-if Toolについての概要説明と導入
* What-ifを使ってTitanicデータを可視化してみる
* What-ifのモデル比較機能を利用してモデルの様子を確認

# What-if Toolとは？
![what-if.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/76c70d23-f00f-3e58-2724-8be3991da542.gif)


[Googleの研究グループ](https://ai.google/research/teams/brain/pair)が作成している**Jupyter上でインタラクティブに実行できる分析可視化ツール**のことです。（詳しくは[こちら](https://pair-code.github.io/what-if-tool/)）

## What-if Toolのいいところ
可視化ライブラリといえば[matplotlib](https://matplotlib.org)や[seaborn](https://seaborn.pydata.org), [plotly](https://plot.ly/python/)が有名ですが，What-if Toolはそれと比較して

* **モデルの結果の比較ができる**
* **２軸以上の複数軸でのデータの可視化ができる**

という２点が差別化ポイントとなります（詳しくは後のTitanicの例で実際に解説します）。一方で，弱いポイントとしては

* Scatterのみに対応しているため，matplotlib時系列データへの対応や柔軟なグラフの作成ができない

が挙げられますが，他のツールにない機能をたくさん持っているのと，動的で対話的なUIは使っていて楽しいので，とにかく使ってみましょう。

## What-if Toolの導入
手軽にColabで試すなら[公式のチュートリアル](https://colab.research.google.com/github/pair-code/what-if-tool/blob/master/WIT_Model_Comparison.ipynb)を使い，可視化された部分の機能のみを見てみるなら[公式デモ](https://pair-code.github.io/what-if-tool/uci.html)が早いと思います。

また，[Github](https://github.com/tensorflow/tensorboard/tree/master/tensorboard/plugins/interactive_inference#how-do-i-enable-it-for-use-in-a-colab-notebook)に導入方法も書いてあり，Colabであれば

```shell
!pip install witwidget
```
こちらをColab上から実行すれば完了です。

また，普段使いのJupyterLabや[GCP AI Platform](https://cloud.google.com/ai-platform-notebooks/?hl=ja)上のTensorflow Notebook Instanceを利用するのであれば，

```
!pip install witwidget
!jupyter labextension install wit-widget
!jupyter labextension install @jupyter-widgets/jupyterlab-manager
```

こちらを実行しましょう。
ただし，このWhat-if Toolは公式のGithub Repositoryを見ても分かる通り，Tensorboardのプラグインのようなので，**TensorflowのInstallは必須**となることに注意しましょう。

# What-if Toolを試す
## Titanicデータを可視化してみる
使うだけならば[公式のチュートリアル](https://colab.research.google.com/github/pair-code/what-if-tool/blob/master/WIT_Model_Comparison.ipynb)がかなり詳しいのでそちらを使っていただいて構いませんが，それだと面白くないので，今回は[Kaggle](https://www.kaggle.com)で有名な[Titanic号の生存予測](https://www.kaggle.com/c/titanic)データを可視化していきます。（今回使った環境は先に述べたGCPのAI Platformです。）

### 準備
事前に[Titanic号の生存予測](https://www.kaggle.com/c/titanic)のデータをJupyterLab上にアップロードしておきます。また，上の章のコマンドに応じてWhat-if Tool(WIT)をダウンロードしておきます。

使うパッケージをimportします。可視化で利用するのは`WitConfigBuilder`クラスと`WitWidget`クラスになります。まず学習データを可視化するためにPandas.DataFrameとして読み込んでおきます。

```python
import pandas as pd
import numpy as np
import tensorflow as tf

from witwidget.notebook.visualization import WitConfigBuilder
from witwidget.notebook.visualization import WitWidget

train_df = pd.read_csv('./train.csv', sep=',', index_col='PassengerId')
train_df.head()
```

ここで**WITは[tf.train.Example](https://www.tensorflow.org/api_docs/python/tf/train/Example)クラスを入力とする**ので，ヘルパ関数としてPandas.DataFrameをtf.train.Exampleに変換する関数を準備しておきます（この関数はチュートリアルより拝借しています）。

```python
def df_to_examples(df, columns=None):
    examples = []
    if columns == None:
        columns = df.columns.values.tolist()
    for index, row in df.iterrows():
        example = tf.train.Example()
        for col in columns:
            if df[col].dtype is np.dtype(np.int64):
                example.features.feature[col].int64_list.value.append(int(row[col]))
            elif df[col].dtype is np.dtype(np.float64):
                example.features.feature[col].float_list.value.append(row[col])
            elif row[col] == row[col]:
                example.features.feature[col].bytes_list.value.append(row[col].encode('utf-8'))
        examples.append(example)
    return examples

train_examples = df_to_examples(train_df)
```

### 可視化
さらに`WitConfigBuilder`クラスをインスタンス化して`WitWidget`クラスに食わせると可視化できます。

実際に見てみましょう。Datapoint editorタブでは以下のような画像が表示されます。なにやらカラフルで豪華な画面が現れます。各点はデータポイント（行）を表しています。色はColor選んだカラム（ここではParch）で色分けされています。

```python
config_builder = WitConfigBuilder(train_examples)
WitWidget(config_builder, height=1000)
```

<img width="1334" alt="スクリーンショット 2019-12-18 23.39.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/0c7bf3e8-12e1-4071-16f7-964f2f1e7b82.png">

ここでColorを`Survived`，Binning X-Axisを年齢で選択してみましょう。これにより以下のように画面がぬるっと変化します。

<img width="1302" alt="スクリーンショット 2019-12-15 16.58.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/2e4b2800-ab41-b4b2-da08-a722c615ae86.png">

Survived=1，すなわち生き残った人は赤色に，生き残れなかった人は青色に着色されています。Binning X-axisでは選択されたカラムがX軸方向が適当なBinで分割されます。

図を眺めているAgeだけではSurvivedはあまり分かれていないことがわかります。強いて言えば56~64歳の人が生き残れなかったこと，8歳までの子供の生き残った率が高そうなのがわかるくらいでしょうか。

加えて，`Binning Y-Axis`を性別に変更してみます。
<img width="1296" alt="スクリーンショット 2019-12-15 16.58.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/8f1da6ba-e24a-31d5-fdb7-7d1fb40cc940.png">

ここまで来ると傾向が結構見えてきますね。男性と女性でSurvivedの結果がかなり違ってきます。さらに年齢が高い女性の生存率はかなり高いようですね。年齢と性別を組み合わせることで新たな傾向が見えてきます。

ここでさらに`Binning Y-Axis` を運賃に変更してみます。
<img width="1292" alt="スクリーンショット 2019-12-15 16.59.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/d40a8697-66e1-f644-a3cb-d89bdf23a49b.png">

ここでは運賃が高い号車にいる人の生存率は高いことがわかります。一方で0歳〜8歳までの子供は低い運賃を払っていても生存率が高いです。これは仮説ですが，いわゆる子供料金が適用されているため，高い客室にいるが運賃は安く抑えられているからかもしれません！？

また，Featuresタブを見ることでデータの統計量がわかります。こちらはPandasなどでもよく見る図ですね。
<img width="754" alt="スクリーンショット 2019-12-19 17.29.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/be753c8f-cef6-ec46-d9c1-af87d59ce548.png">

このような形で，モデルを使わずとも**What-if Tool**でわかることはたくさんあります。

### モデルの比較の準備
ここからはロジスティック回帰とMLPのモデルを作成して，各結果を見比べてみます。学習までのコードは以下です。`tf.Estimator`を利用しますが，詳しくは[tf.Estimatorのチュートリアル](https://www.tensorflow.org/guide/estimator)を参照してください。

```python

# What-if Toolチュートリアルよりヘルパー関数群
# An input function for providing input to a model from tf.Examples
def tfexamples_input_fn(examples, feature_spec, label, mode=tf.estimator.ModeKeys.EVAL,
                       num_epochs=None, 
                       batch_size=64):
    def ex_generator():
        for i in range(len(examples)):
            yield examples[i].SerializeToString()
    dataset = tf.data.Dataset.from_generator(
      ex_generator, tf.dtypes.string, tf.TensorShape([]))
    if mode == tf.estimator.ModeKeys.TRAIN:
        dataset = dataset.shuffle(buffer_size=2 * batch_size + 1)
    dataset = dataset.batch(batch_size)
    dataset = dataset.map(lambda tf_example: parse_tf_example(tf_example, label, feature_spec))
    dataset = dataset.repeat(num_epochs)
    return dataset

# Creates a tf feature spec from the dataframe and columns specified.
def create_feature_spec(df, columns=None):
    feature_spec = {}
    if columns == None:
        columns = df.columns.values.tolist()
    for f in columns:
        if df[f].dtype is np.dtype(np.int64):
            feature_spec[f] = tf.io.FixedLenFeature(shape=(), dtype=tf.int64)
        elif df[f].dtype is np.dtype(np.float64):
            feature_spec[f] = tf.io.FixedLenFeature(shape=(), dtype=tf.float32)
        else:
            feature_spec[f] = tf.io.FixedLenFeature(shape=(), dtype=tf.string)
    return feature_spec

def create_feature_columns(columns, feature_spec):
    ret = []
    for col in columns:
        if feature_spec[col].dtype is tf.int64 or feature_spec[col].dtype is tf.float32:
            ret.append(tf.feature_column.numeric_column(col))
        else:
            ret.append(tf.feature_column.indicator_column(
                tf.feature_column.categorical_column_with_vocabulary_list(col, list(dtrain_df[col].unique()))))
    return ret

# Parses Tf.Example protos into features for the input function.
def parse_tf_example(example_proto, label, feature_spec):
    parsed_features = tf.io.parse_example(serialized=example_proto, features=feature_spec)
    target = parsed_features.pop(label)
    return parsed_features, target

# get_dummies後の型変換用
def to_numpy_type(df):
    for f in df.columns:
        if df[f].dtype == 'float64':
            df[f] = df[f].astype(np.float64)
        elif df[f].dtype == 'int64' or 'uint8':
            df[f] = df[f].astype(np.int64)
    
    return df


# 年齢のNullは平均で埋める
train_df.loc[train_df['Age'].isnull(), 'Age'] = train_df['Age'].mean()
# EmbarkedのNullは一番多いCで埋める
train_df.loc[train_df['Embarked'].isnull(), 'Embarked'] = 'C'

# 複雑なカラムは削除
train_df.drop(['Name', 'Cabin', 'Ticket'], axis=1, inplace=True)
dtrain_df = pd.get_dummies(train_df)
label_column = 'Survived'
input_features = ['Pclass', 'Age', 'SibSp', 'Parch', 'Fare', 'Sex_female',
       'Sex_male', 'Embarked_C', 'Embarked_Q', 'Embarked_S']

# ======= 以下，What-if Toolチュートリアル参照 =========
features_and_labels = input_features + [label_column]

# tf.train.Exampleへ変換
dtrain_df = to_numpy_type(dtrain_df)
dtrain_examples = df_to_examples(dtrain_df)

# ロジスティック回帰学習
num_steps = 5000  

# Create a feature spec for the classifier
feature_spec = create_feature_spec(dtrain_df, features_and_labels)

# Define and train the classifier
train_inpf = functools.partial(tfexamples_input_fn, dtrain_examples, feature_spec, label_column)
classifier = tf.estimator.LinearClassifier(
    feature_columns=create_feature_columns(input_features, feature_spec))
classifier.train(train_inpf, steps=num_steps)


num_steps_2 = 2000  

classifier2 = tf.estimator.DNNClassifier(
    feature_columns=create_feature_columns(input_features, feature_spec),
    hidden_units=[128, 64, 32])
classifier2.train(train_inpf, steps=num_steps_2)
```

ここまでで準備は終了です。

### モデルごとの推論結果と説明変数との関係の可視化
ここで結果を可視化してみます。本来はtest.csvを対象にしたいのですが，Grand Truthとなるラベルが存在しないので，便宜的に学習データで結果を可視化してみます。（Trainを分ければいいのですが，今回は可視化のデモということで...）
初めのデータの可視化と異なるのは`WitConfigBuilder`クラスをインスタンス化するときに`set_XXX`でモデルとラベルの名前を与えているところです。

```python
train_examples = df_to_examples(dtrain_df.)

# Setup the tool with the test examples and the trained classifier
config_builder = WitConfigBuilder(train_examples).set_estimator_and_feature_spec(
    classifier, feature_spec).set_compare_estimator_and_feature_spec(
    classifier2, feature_spec).set_label_vocab(['Dead', 'Survived'])
WitWidget(config_builder, height=1000)
```

これにより以下のような画面が現れました。何やらかっこいい。

<img width="1333" alt="スクリーンショット 2019-12-19 19.16.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/e2beb72b-f7e2-2fc1-0421-074b3e277c3c.png">

このData point Editorタブでは，デフォルトでデータ点の色はInference Label 1，すなわちロジスティック回帰で得られた予測ラベルが振られていて，横軸と縦軸はそれぞれのモデルの予測スコアが与えられています。横軸のスコア，すなわちロジスティック回帰のスコアで色付けされているので，X軸の真ん中で綺麗に分かれています。

`Color`をGrand Truthである`Survived`に変更してみます。

<img width="1341" alt="スクリーンショット 2019-12-19 19.19.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/81cc254a-f182-0250-ce00-ace0e997861e.png">

赤い点が左側に寄ってきましたね。すなわちこの左側に寄ってきた赤い点群がロジスティック回帰で間違ったものとなります。さらに`Pclass`でBin分割してみます。

<img width="1338" alt="スクリーンショット 2019-12-19 19.22.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/bebd8a58-1e0d-20ad-57aa-4ecd755e346d.png">

見て分かる通り，どうやらロジスティック回帰のモデルはPclass2に関してはうまく分類できているようですが，3では特に間違っている様子が伺えます。（Pclassは本当はOne-hotにすべきですが，そのままにしたのが原因かも。）

### 閾値による評価値の変動の比較
さらに，`Performance&Fairness`タブを選択して， `Ground Truth Feature`で`Survived`を選択すると，下の図のように各モデルの再現率，適合率，F値，ROC曲線，PR曲線，混合行列が自動で表示されます。

<img width="1342" alt="スクリーンショット 2019-12-19 21.30.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/e28e4894-4e8c-6989-a42d-f1af843fc113.png">

ここで閾値のスライドバーを変化させて見ると...

<img width="1343" alt="スクリーンショット 2019-12-19 22.08.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/2ba1d016-390e-6caf-131b-a8063d2cf8d4.png">

なんと，PR曲線とROC曲線の点が閾値に沿って動的に移動し，F値などの評価指標も自動で再計算されます。また，グラフをマウスオーバーするとその地点の評価値が表示されます。
異常検知など，再現率を重要視した閾値を選びたいときなど，かなり実用的ですね。

### データの編集と推論
さらにData Point Editorに戻ってみます。このタブの名前の通り，モデルを評価する時は**データを自由に編集して，推論値をその場で得る**ことができます。例えば画面上のデータ点をクリックすることで左側のパネルにデータの各特徴の値と推論スコアが表示されます。（わかりにくいですが，画面上の黄色い点が選択した点です。）

<img width="1342" alt="スクリーンショット 2019-12-19 21.43.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/e11391cf-f161-aa96-a7b6-8f00ad94786e.png">

生存確率はロジスティック回帰では0.313，ニューラルネットでは0.818と偏った値になっています。
この状態で，左側のパネルの値を男性から女性に変更して（Sex_femaleを1，Sex_maleを0にする），Run Inferenceを実行します。すると...

<img width="1334" alt="スクリーンショット 2019-12-19 21.46.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/ff7c31d5-2065-6d7a-5f00-a877f7e53c34.png">

選択したデータ点は右上の方に移動します。さらに，生存確率はロジスティック回帰で0.875，ニューラルネットで0.874とどちらのモデルでも大きな値に変動しています。これにより，ニューラルネットはロジスティック回帰に比べて（このデータ点で言えば）男女のパラメータを重要視することがわかります。
この機能によりモデルがどのように表現を習得しているかが掴めますね。

## まとめ
今回は**What-if Tool**を紹介させていただきました。EDAはデータ分析の基本なのですが，描画にもコーディングのコストがかなり高いため，億劫になりがちですが，このツールによってそれもかなり緩和されました。さらに，モデルの振る舞いをここまで低コストかつインタラクティブに表現してくれるツールはなかなかないのではないかと思います。

非常に長々とした拙文を読んでいただきありがとうございました！

次のアドベントカレンダーの記事は@yuto_k2c さんです！