Amazon SageMaker Clarify〜機械学習モデルのバイアスと説明をお手軽に計算しよう〜

この記事はNTTComアドベントカレンダー12日目の記事となります。

初めまして，[kirikei](https://qiita.com/kirikei)と申します。普段はNTT Communicationsでデータ分析ツールのスクラム開発や，機械学習モデルの説明性（いわゆる[XAI](https://www.slideshare.net/SatoshiHara3/ss-126157179)の分野）や因果推論に関する研究をしています。あとは趣味でAWSやAzure，GCPなどの機械学習サービスを検証していて，[こんな記事](https://qiita.com/kirikei/items/693cba8f47c821f221d5)や[こんな記事](https://qiita.com/kirikei/items/2a4ef062fadd4a93203a)も書いています。

今回はその例に漏れず，現在開催中のAWS最大のイベント[re:Invent](https://reinvent.awsevents.com)で，**先日（この記事を書く４日前？）発表のあったモデルの説明性やデータのバイアスを計算できるSageMakerの新機能である[Amazon SageMaker Clarify](https://techcrunch.com/2020/12/08/aws-announces-sagemaker-clarify-to-help-reduce-bias-in-machine-learning-models/)を早速検証・紹介させて頂きます。**（タイムリー！）


# Amazon SageMakerとは？
[Amazon SageMaker](https://aws.amazon.com/jp/sagemaker/)とは，AWSで提供されている機械学習エンジニアのための機械学習モデルの学習や実験管理，モデルの管理，モデルのデプロイを効率的に行うためのプラットフォームです。機械学習エンジニアが悩みがちなインフラ周りの雑務を一手に引き受けてくれる素敵なサービスです。

SageMakerには複数のインターフェースが存在し，モデル選択して簡単に学習してくれるGUIと，機械学習エンジニアが最もよく使っているであろうJupyterLabやJupyter NotebookからSDKを叩いて学習を行うインターフェースが用意されています。

また，昨年からJupyterLabのインターフェースをさらにパワーアップさせ，Jupyter-Likeなインターフェースから実験管理やモデルの管理まで行える統合環境のSageMaker Studioも利用可能になっています。

SageMaker全体に関して，詳しくは[Amazon Web Service Japanのスライド](https://www.slideshare.net/AmazonWebServicesJapan/amazon-sagemaker-122554039)を，SageMaker Studioについてはわかりやすく書かれたQiita記事があるのでそちらを参照下さい。

* SageMaker Studioのインターフェースはこんな感じ。ここからSDKを叩いて（例えば`XX.fit`とか`XX.deploy`とかを実行すると指定したリソースの学習コンテナが立ち上がって学習が行われ，モデルがデプロイされてエンドポイントを作成できたりします）
![スクリーンショット 2020-12-12 16.03.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/5980cd08-418e-b144-2144-889f3e6e3d37.png)


# Amazon SageMaker Clarifyとは？
[Amazon SageMaker Clarify](https://aws.amazon.com/jp/sagemaker/clarify/)とは，冒頭でも述べた2020年の[re:Inventにて発表](https://aws.amazon.com/jp/blogs/aws/new-amazon-sagemaker-clarify-detects-bias-and-increases-the-transparency-of-machine-learning-models/)されたSageMakerの新しい機能のことで，**機械学習モデルの実用化において重要となる機械学習モデルの説明性や，モデルやデータのバイアスを評価できるところが特徴**です。

機械学習モデルの説明を行うクラウドサービスとしてはGCPの[Explainable AI](https://cloud.google.com/explainable-ai)や，Azureの[azureml.interpret](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-machine-learning-interpretability)が先行していましたが，AWSからも今回リリースされることとなり，それぞれクラウド３社が説明性に関する機能をそれぞれの機械学習サービスに携えたことになります。それだけ**注目されている分野**だということがわかります。

### 機械学習モデルの説明性とは？
ニューラルネットワークをはじめとするパラメータ数が多く非線形なモデルは，どの入力が出力に寄与しているかを把握するのが困難であり，産業などで利用する際のモデルの信頼性に問題を抱えています。その中で研究されているのが機械学習モデルの説明性に関する研究であり，Explainable AI，通称XAIとも呼ばれます。今回は中身までは触れませんが，Qiita内に[素晴らしい記事](https://qiita.com/icoxfog417/items/8689f943fd1225e24358)もあるので，興味のある方は参照下さい。

Clarifyでは説明性の手法の中でスタンダードになりつつある[SHAP](https://shap.readthedocs.io/en/latest/)を利用して各特徴が出力に対してどれくらい寄与しているのかを数値化，可視化しています。この手法はニューラルネットワークやGBTを含むどのようなモデルにも利用できるという利点があります。

### バイアスとは？
機械学習モデルのバイアスと聞くと機械学習を生業にしている方はバイアスとバリアンスを思い浮かべるかもしれませんが，SageMaker Clarifyの文脈では一般的な用語としてのバイアスのことを示します。例えば特徴量に男女のラベルがあった時に，本来は男女に差が出ないデータにもかかわらずデータ中の男女の数のインバランスによって結果に影響してしまうようなことを言います（研究的にはfairnessが近いと思われます。）

Clarifyではデータのバイアス（pretrain-bias）とモデルのバイアス（posttrain-bias）を評価することができます。前者は目的変数に対してある特徴がどれくらいセンシティブか，具体的には目的変数をバイナリのラベル（または閾値）で分けた分布と興味のある特徴をバイナリ（または閾値）で分けたときのデータ分布がどの程度離れているかを数値化します。

# Kaggleのデータで試してみた
SageMaker Clarifyのサンプルコードは[Githubで公開](https://github.com/aws/amazon-sagemaker-examples/blob/master/sagemaker_processing/fairness_and_explainability/fairness_and_explainability.ipynb)されていますが，これをそのままやるのは面白くないので，今回は別のデータとしてKaggleの[House Prices](https://www.kaggle.com/c/house-prices-advanced-regression-techniques/overview)を用います。

このデータは，家に関する様々な属性（家のクオリティや築年数，売り方，場所，etc...）の情報から家の値段がいくらになるかを予測するデータです。

下準備としてSageMakerにアクセスしてノートブックインスタンスを立ち上げておき，Kaggleのページからtrain.csv，test.csvをダウンロードしてJupyterLabにアップロードしておきます。以下のコードはSageMaker上のJupyterLabまたはSageMaker Studioで実行します。

## データ可視化からモデルの学習まで
まず，利用するパッケージやデータを格納するS3のバケットや実行リージョンを定義しておきます。今回はs3バケットに`house-price-sagemaker-clarify`という名前をつけておきます。

```python
from sagemaker import Session
session = Session()
bucket = session.default_bucket()
prefix = 'sagemaker/house-price-sagemaker-clarify'
region = session.boto_region_name
# Define IAM role
from sagemaker import get_execution_role
import pandas as pd
import numpy as np
import urllib
import os

from sklearn.model_selection import train_test_split

role = get_execution_role()
```

データの中身を確認します。
 
```python
train_df = pd.read_csv('train.csv', index_col=0)
test_df = pd.read_csv('test.csv', index_col=0)
train_df.head()
```

![スクリーンショット 2020-12-12 19.28.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/585367ae-1778-5d58-d9fd-e77eda9c344b.png)

見ての通り，今回のデータはテキストのカテゴリカルデータが多いので，最低限の前処理としてバイナリに変更しておきます。

```python
all_df = pd.get_dummies(pd.concat([train_df, test_df], axis=0))
train_df = all_df.iloc[:train_df.shape[0], :]
test_df = all_df.iloc[train_df.shape[0]:, :]
```

ここからはSageMakerを利用して学習まで一気に実行します。SageMakerで学習を行う場合は一旦学習データをファイルとしてS3にアップロードし，そのデータのURIを学習時に指定することで学習データを取ってきて学習を行います。詳しくは[こちらの記事](https://qiita.com/triwave33/items/0a39e90068c57f1d2753)に詳しく書かれています。

```python
# カラムの入れ替え。SageMakerは先頭カラムをTargetとして認識する
def to_csv(df, filename, is_train=True):
    if is_train:
        df = pd.concat([df[target_column], df.drop(target_column, axis=1)], axis=1)
    else:
        df = df.drop(target_column, axis=1)
    df.to_csv(filename, index=None, header=None) # indexとheaderは不要
    return df
    
train_df = to_csv(train_df, 'train_data.csv')
test_df = to_csv(test_df, 'test_data.csv', is_train=False)![スクリーンショット 2020-12-12 22.38.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/bad5dda8-5579-7589-3b62-f7edb3cb6b80.png)

    
# S3にアップロードするためファイルとして吐き出しておく
train_df = to_csv(train_df, 'train_data.csv')
test_df = to_csv(test_df, 'test_data.csv', is_train=False)

# s3へのアップロード
from sagemaker.s3 import S3Uploader
from sagemaker.inputs import TrainingInput

train_uri = S3Uploader.upload('train_data.csv', 's3://{}/{}'.format(bucket, prefix))
train_input = TrainingInput(train_uri, content_type='csv')
test_uri = S3Uploader.upload('test_data.csv', 's3://{}/{}'.format(bucket, prefix))

# XGBoostによる学習
# SageMaker SDKのEstimatorクラスを利用する。XGBのwrapperとなっている
from sagemaker.image_uris import retrieve
from sagemaker.estimator import Estimator


# 学習
container = retrieve('xgboost', region, version='1.2-1') # XGBが使えるコンテナイメージの指定

# モデルの定義
xgb = Estimator(container,
                role,
                instance_count=1,
                instance_type='ml.m4.xlarge', # 学習インスタンス（今回はもりもり）
                disable_profiler=True, 
                sagemaker_session=session)

xgb.set_hyperparameters(max_depth=5,
                        eta=0.2,
                        gamma=4,
                        min_child_weight=6,
                        subsample=0.8,
                        objective='reg:squarederror', # 回帰問題
                        num_round=800)

# 学習開始，URIからデータを取得し，指定したインスタンスで学習する
# 学習ログはCloudWatchに自動的に吐き出される
# 学習が終わるとインスタンスが勝手に落ちる
xgb.fit({'train': train_input}, logs='None', wait='True')

# モデルの登録
from sagemaker.serializers import CSVSerializer
from sagemaker.deserializers import CSVDeserializer

# モデルに名前をつけてモデル管理リポジトリに登録する
model_name = 'house-price-clarify-model'
model = xgb.create_model(name=model_name)
container_def = model.prepare_container_def()
session.create_model(model_name,
                     role,
                     container_def)
```

## Clarifyの利用
ここからいよいよClarifyの利用です。ClarifyはSDKの中の1ライブラリとして用意されており，基本的にコンフィグを定義した後の`run_bias`と`run_explainability`でバイアスの数値化と説明の可視化をそれぞれHTMLやPDFのレポートにして（！）S3に出力してくれます。

また，最大のメリットはSageMaker最大の武器である**インフラを気にしなくて良いこと**これに尽きます。説明やバイアスの計算も学習まではいかないものの計算時間がかかったりGPUを利用する必要があったりと，インフラと密接に関わっています。Clarifyではこの説明やバイアスの計算も学習と同じように，**説明の計算プロセスをユーザが決めたリソースでコンテナに切り出して実行している**ため，このSageMakerの思想がフルに生かされています。

### バイアスの数値化
バイアスの数値化は先に述べたようにデータに対するバイアスとモデルに対するバイアスを計算しています。基本的にはある興味のある特徴に対して，目的変数がどのくらい敏感なのかを計算しています。具体的には，例えばバイナリな特徴（0or1）と目的変数（0or1）があった時，特徴が0だった時の目的変数が1，0の時のそれぞれの分布と，特徴が1だった時の目的変数が1,0の時のそれぞれの分布がどの程度異なるか（例えばKL Divergence）をバイアスとして評価しています。直感的にはこの分布が異なれば異なるほど特徴によって目的変数が変動していることがわかります。詳しくは[こちらの公式ドキュメント](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-measure-data-bias.html)に詳しく書かれています。

モデルのバイアスは上記の計算を目的変数ではなく予測値で行っているようです。元々の目的変数のバイアスと予測値のバイアスがどのように異なるかを比較することでモデルのバイアスが確認できるようです。こちらも詳しくは[公式ドキュメント](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-measure-post-training-bias.html)を参照して下さい。

今回扱うデータは目的変数が価格＝連続値であるため，価格の上位80%を閾値に設定して，バイアスを測る説明変数はSalesCondition_Abnormal（通常の販売方法ではなく差し押さえやトレード，空売り）を選択しました。すなわち通常の販売方法でない場合に目的変数に違いがあるか，またはモデルの予測値に違いがあるかを見ていることになります。

実行コードは以下となります。

```python
from sagemaker import clarify

# clarifyを利用するインスタンスの設定
clarify_processor = clarify.SageMakerClarifyProcessor(role=role,
                                                      instance_count=1,
                                                      instance_type='ml.c4.xlarge',
                                                      sagemaker_session=session)

# レポートの出力先
bias_report_output_path = 's3://{}/{}/clarify-bias'.format(bucket, prefix) 

# データの設定
bias_data_config = clarify.DataConfig(s3_data_input_path=train_uri,
                                      s3_output_path=bias_report_output_path,
                                      label=target_column, # targetとなるカラム
                                      headers=train_df.columns.to_list(), # カラム名
                                      dataset_type='text/csv')

# 対象となるモデルの設定
# 今回はXGBoost
model_config = clarify.ModelConfig(model_name=model_name,
                                   instance_type='ml.c5.xlarge',
                                   instance_count=1,
                                   accept_type='text/csv')

predictions_config = clarify.ModelPredictedLabelConfig()

# 目的変数の閾値とラベルの設定
bias_config = clarify.BiasConfig(label_values_or_threshold=[train_df[target_column].quantile(q=0.8)],
                                facet_name='SaleCondition_Abnorml',
                                facet_values_or_threshold=[0]) # 興味のある特徴の値

# pre-biasとpost-biasを一緒に実行
# biasの測り方は一旦全てのメソッドを利用する
clarify_processor.run_bias(data_config=bias_data_config,
                           bias_config=bias_config,
                           model_config=model_config,
                           model_predicted_label_config=predictions_config,
                           pre_training_methods='all',
                           post_training_methods='all')
```

実行するとS3のバケット内に`report.pdf`というファイルが現れ，中身には以下のような結果が表示されます。


![スクリーンショット 2020-12-12 22.39.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/40803d12-313d-82d7-c109-d229a57be01a.png)

データのバイアスに関して，クラスはかなりインバランスになっているので，ラベルの数にかなり偏りはあるようです。ただし，その他の尺度はそれほど大きくない（基本的に距離の尺度なので0に近いほど分布が近い）ため，クラスに偏りはあるものの分布は似ているため，この特徴はそれほど大きな影響がないのかもしれません。

![スクリーンショット 2020-12-12 22.39.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/fc99912a-5f52-2cb0-df23-3a74c2bc3665.png)

モデルのバイアスに関してもDisparate Impactは大きいですが，基本的には0に近い値を取り，公式ドキュメントによるとバイアスは小さそうです。よって元のデータのバイアスが小さいことも考えるとモデルにバイアスが混入している可能性は低いと言えるかもしれません（ただし厳密には検定などが必要だと思われます。）

### 説明の可視化
次はモデルの説明，すなわち入力の出力に対する重要度を可視化します。先に述べた通りよく使われるSHAP（Kernel SHAP）を利用して重要度を抽出しています。こちらもClarifyでは設定後に`run_explainability`一発で実行してくれるので大変簡単です。

```python
# Kernel SHAPに利用するハイパーパラメータ
shap_config = clarify.SHAPConfig(baseline=[test_df.iloc[0].values.tolist()],
                                 num_samples=100,
                                 agg_method='mean_abs')

# 格納先
explainability_output_path = 's3://{}/{}/clarify-explainability'.format(bucket, prefix)

# 説明に利用するデータ
explainability_data_config = clarify.DataConfig(s3_data_input_path=train_uri,
                                s3_output_path=explainability_output_path,
                                label=target_column,
                                headers=train_df.columns.to_list(),
                                dataset_type='text/csv')
# 説明性の計算
clarify_processor.run_explainability(data_config=explainability_data_config,
                                     model_config=model_config,
                                     explainability_config=shap_config)
```

実行を行うと下記のようなレポートが同様に現れます。

![スクリーンショット 2020-12-12 23.27.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185518/61c972a8-6492-92a9-71a4-5f86ebb730ea.png)

重要度の高い特徴トップ10が順に可視化されます。これをみるとカテゴリカルデータよりも値に依存するようですね。あと築年数やクオリティなど，直感的にも価格に関係のありそうな項目をモデルは重要視しているようです。

# 所感とまとめ
今回新しく発表のあったClarifyですが，SageMakerユーザとしては他の学習やデプロイの実行と似たようなインターフェースで作られており，特に悩むことなく実行することができました。今後は学習→デプロイのパイプラインの中にClarifyで説明やバイアスをいれるというビジョンが見えてきます。一方でバイアスは値の意味の理解が必要であり，ユーザの知識がなかなか必要になると感じました。

説明性を研究している自分としてはこれだけ説明性が身近になる，当たり前に使われるのは非常に嬉しいですね！（説明性も色々問題あったりするけどそれはまた別の機会で。）

アドベントカレンダー，次はNTT Communicationsにおけるブロックチェーン技術の第一人者，@nitkyさんの記事になります！ご期待ください！！（期待してます！）

## 参考文献
* https://dev.classmethod.jp/articles/breaking-amazon-sagemaker-clarify/
* https://sagemaker.readthedocs.io/en/stable/api/training/processing.html
* https://www.kaggle.com/c/house-prices-advanced-regression-techniques/overview