データ分析
前回は公平性の規準をいくつか定式化し、不公平性の緩和アルゴリズムを紹介しました。
第3部では、公平性のためのいくつかのツールを紹介した後、Fairlearnを用いて不公平性の緩和アルゴリズムの1つである閾値最適化アルゴリズムを実装していきます。その中で、モデル間の予測精度と公平性のトレードオフや各モデルの精度指標および公平性指標の可視化についても扱っていきます。
目次
公平性のためのツール
公平性のためのツールで代表的なものは下記の表のとおりです。
提供元 | ツール名 | 説明 |
---|---|---|
Microsoft | Fairlearn | モデルの予測結果に対する精度や公平性の指標およびそれらのトレードオフを可視化するダッシュボードと緩和アルゴリズムが備えられています。 2020年6月にMicrosoftのAzure Machine Learningに統合されました。 |
IBM | AI Fairness 360 | バイアスの検出や軽減をするためのツールキットで、75以上の公平性指標と10以上の緩和アルゴリズムがあります。 |
What-if tool | プログラムをすることなく機械学習モデルのバイアスなどを検知できるツールです。 | |
Fairness indicator | 分類モデルの公平性指標の計算と視覚化ができます。また、What-If toolと統合がされています。 | |
ML-fairness-gym | 既存の静的なデータに対する不公平性への対処では対応できないようなフィードバックループに基づく動的なデータの長期的な影響をシミュレーションするためのツールです。 |
■Fairlearn
Fairlearnは、機械学習を用いたAIシステムの公平性を評価し、観測された不公平性を緩和するアルゴリズムを含んだPythonパッケージです。機械学習モデルの公平性を評価するためのJupyter widgetとしてFairlearn Dashboardが備わっており、複数のモデル間での公平性と精度のトレードオフや各モデルに対する全体のメトリクスやグループ間のメトリクスの差などを可視化することができます。また、不公平性の緩和アルゴリズムとしては、後処理型のアルゴリズムであるThresholdOptimizerや還元型アルゴリズムであるGridSearchとExponentiatedGradientが備わっています。2章では不公平の原因はバイアスであると述べたが、Fairlearnではバイアスの原因を特定、除去することで不公平性を取り除くアプローチを取らず、損害の観点からAIシステムの不公平な振舞いを捉えます。したがって、本章ではバイアスという用語は登場しません。損害には多くの種類が存在するが、Fairlearnではグループの公平性に基づいた2種類の損害を取り扱います。
損害の種類 | 説明 |
---|---|
配分の損害 | 特定のグループに対して、機会やリソースが与えられない、または情報へのアクセスが拒否されること。 例)雇用、入学、融資 |
サービスの品質の損害 | 特定のグループは、機会やリソース、情報へのアクセス権を有するが、それらが低品質であること。 例)顔認識アプリケーション、ECサイトでのレコメンド |
このような損害を与えるサービスを提供することは法律で禁じられている可能性があります。また、法律に違反していなくとも損害の事実が明るみになれば顧客を失う可能性が有り得ます。持続的なAIサービスの提供をするためには損害に対する配慮が必要であると言えます。
3章で扱ったグループの公平性について、損害の観点から見直してみましょう。
公平性規準 | 緩和できる損害 |
---|---|
デモグラフィックパリティ | 配分の損害 |
等価オッズ | 配分の損害、サービスの品質の損害 |
機会均等 | 配分の損害、サービスの品質の損害 |
Fairlearnでは、公平性の評価および不公平性の緩和のいずれの文脈においても格差メトリクス(disparity metrics)に基づいて設計されています。格差メトリクスとは、機械学習モデルの予測結果が対象の公平性規準を満たしている状態からどれほど乖離しているかを表す指標です。格差メトリクスは異なるグループ間を比較するために、差の観点あるいは比の観点を用いて定義されます。デモグラフィックパリティを例に考えてみましょう。ここでは簡単のため、$P(hat{ Y }=1|A=1) geq P(hat{ Y }=1|A=0)$とします。このとき、デモグラフィックパリティに関する格差メトリクスは以下で与えられます。
格差メトリクス | 定義 |
---|---|
デモグラフィックパリティ差 | $P(hat{ Y }=1|A=1) – P(hat{ Y }=1|A=0)$ |
デモグラフィックパリティ比 | $begin{eqnarray} frac{ P(hat{ Y }=1|A=0) }{ P(hat{ Y }=1|A=1) } end{eqnarray}$ |
Fairlearnを用いた閾値最適化の実装
Fairlearnは以下のpipコマンドでPyPIからインストールできます:
pip install fairlearn
あるいは、conda-forgeでもインストールできます:
conda install -c conda-forge fairlearn
[本実装の扱うこと]
■Fairlearn Dashboardを用いた機械学習モデルの公平性の評価■ThresholdOptimizerを用いた不公平性の緩和
[本実装の概要]
■UCIクレジットカード・デフォルト・データセットを用いた2値分類問題を扱います。■融資をするかの意思決定を公平に行うアルゴリズムを作成します。
■特定のデータに疑似的な不公平性(予測に対する男女間での精度格差)を加えます。
■格差メトリクスである等価オッズ差を用いてグループ間の精度格差を緩和します。
[データ概要]
■データの取得元■2005年に台湾の銀行で取得された3万人の顧客のクレジットカードの取引データ
■説明変数には、顧客の静的な特徴量と2005年4~9月までのクレジットカードの請求書の支払い履歴と顧客のクレジットカードの残高限度額が含まれています。
■目的変数は、2005年10月にカードの支払いを滞納するか否かであり、滞納した場合は1、そうでなければ0が付きます。
[カラムの説明]
カラム名 | 説明 | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
[説明変数] | |||||||||||||||||
LIMIT_BAL | 利用可能枠(不公平性のための加工対象カラム) | ||||||||||||||||
SEX | 性別 (1=male,2=female) | ||||||||||||||||
EDUCATION | 最終学歴 (1=graduate school,2=university,3=high school,4=others) | ||||||||||||||||
MARRIAGE | 既婚/未婚/その他 (1=married,2=single,3=others) | ||||||||||||||||
AGE | 年齢 | ||||||||||||||||
PAY_[1-6] | 支払歴(PAY_1:支払歴(2005年9月),PAY_2:支払歴(2005年8月),…,PAY_6:支払歴(2005年4月)) [カテゴリ値の対応表]
|
||||||||||||||||
BILL_AMT[1-6] | 請求額 (BILL_AMT1:請求額(2005年9月),BILL_AMT2:請求額(2005年8月),…,BILL_AMT6:請求額(2005年4月)) | ||||||||||||||||
PAY_AMT[1-6] | 支払額 (PAY_AMT1:支払額(2005年9月),PAY_AMT2:支払額(2005年8月),…,PAY_AMT6:支払額(2005年4月)) | ||||||||||||||||
[目的変数] | |||||||||||||||||
default payment next month | 2005年10月にカードの支払いを滞納するか否か(Yes=1,No=0) |
[精度指標]
■AUC■Balanced accuracy
[格差メトリクス]
■等価オッズ差[緩和アルゴリズム]
■fairlearn.postprocessing.ThresholdOptimizer実装のために必要なモジュールをあらかじめインポートしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# General imports import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline # Data processing from sklearn.model_selection import train_test_split # Models import lightgbm as lgb # Fairlearn algorithms and utils from fairlearn.postprocessing import ThresholdOptimizer from fairlearn.widget import FairlearnDashboard # Metrics from fairlearn.metrics import ( selection_rate, demographic_parity_difference, demographic_parity_ratio, balanced_accuracy_score_group_summary, roc_auc_score_group_summary, equalized_odds_difference, difference_from_summary) from sklearn.metrics import balanced_accuracy_score, roc_auc_score |
UCIクレジットカード・デフォルト・データセットを取得します。
1 2 3 4 |
# Load the data<br> data_url = "http://archive.ics.uci.edu/ml/machine-learning-databases/00350/default%20of%20credit%20card%20clients.xls" dataset = pd.read_excel(io=data_url, header=1).drop(columns=['ID']).rename(columns={'PAY_0':'PAY_1'}) dataset.head() |
センシティブ属性と目的変数を定義し、カテゴリ変数として扱うものをキャストします。
1 2 3 4 5 6 7 8 9 |
# Extract the sensitive feature<br> A = dataset["SEX"] A_str = A.map({ 2:"female", 1:"male"}) # Extract the target Y = dataset["default payment next month"] categorical_features = ['EDUCATION', 'MARRIAGE','PAY_1', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6'] for col in categorical_features: dataset[col] = dataset[col].astype('category') |
LIMIT_BAL変数を、女性に対しては目的変数と相関し、男性に対しては相関しないように加工します。これにより機械学習モデルを作成した際に性別による精度格差が生じるため不公平性を持つ疑似的なデータができます。
1 2 3 4 5 6 7 8 9 10 |
dist_scale = 0.5 np.random.seed(12345) # Make 'LIMIT_BAL' informative of the target dataset['LIMIT_BAL'] = Y + np.random.normal(scale=dist_scale, size=dataset.shape[0]) # But then make it uninformative for the male clients dataset.loc[A==1, 'LIMIT_BAL'] = np.random.normal(scale=dist_scale, size=dataset[A==1].shape[0]) |
加工済みのLIMIT_BAL変数に対して男女ごとにヒストグラムを描画してみましょう。
1 2 3 4 5 6 7 8 9 10 11 |
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 4), sharey=True) # Plot distribution of LIMIT_BAL for men dataset['LIMIT_BAL'][(A==1) & (Y==0)].plot(kind='kde', label="Payment on Time", ax=ax1, title="LIMIT_BAL distribution for men") dataset['LIMIT_BAL'][(A==1) & (Y==1)].plot(kind='kde', label="Payment Default", ax=ax1) # Plot distribution of LIMIT_BAL for women dataset['LIMIT_BAL'][(A==2) & (Y==0)].plot(kind='kde', label="Payment on Time", ax=ax2, legend=True, title="LIMIT_BAL distribution for women") dataset['LIMIT_BAL'][(A==2) & (Y==1)].plot(kind='kde', label="Payment Default", ax=ax2, legend=True).legend(bbox_to_anchor=(1.6, 1)) plt.show() |
上図から、加工済みのLIMIT_BAL変数は、女性に対しては債務不履行者か否かをある程度分離することができそうだが、男性に対してそれは期待できないことが分かります。このことは、公平性を考慮せずに機械学習モデルを作成した際にも予測結果に反映されるはずです。したがって、このモデルは不公平なモデルと言えます。
不公平性を考慮しないモデルを作成するために元のデータを学習用データとテスト用データに分けましょう。
1 2 3 4 5 6 7 8 9 |
# Train-test split df_train, df_test, Y_train, Y_test, A_train, A_test, A_str_train, A_str_test = train_test_split( dataset.drop(columns=['SEX', 'default payment next month']), Y, A, A_str, test_size = 0.3, random_state=12345, stratify=Y) |
公平性を考慮せずに標準的な分類器を作成しましょう。アルゴリズムはLightGBMを使用します。
1 2 3 4 5 6 7 8 9 |
lgb_params = { 'objective' : 'binary', 'metric' : 'auc', 'learning_rate': 0.03, 'num_leaves' : 10, 'max_depth' : 3 } model = lgb.LGBMClassifier(**lgb_params) model.fit(df_train, Y_train) |
作成した分類器の精度を確認してみましょう。
1 2 3 4 5 6 7 8 |
# Train AUC roc_auc_score(Y_train, model.predict_proba(df_train)[:, 1]) >>> 0.8500361298806519 # Scores on test set test_scores = model.predict_proba(df_test)[:, 1] # Test AUC roc_auc_score(Y_test, model.predict_proba(df_test)[:, 1]) >>>0.8522055914477181 |
この分類器の重要度を棒グラフで出力してみましょう。
上図見ると、公平性を考慮していないモデルでは、加工した変数であるLIMIT_BALが最も重要な変数であることが分かります。しかし、これは女性に対しては効果的な変数であるが、男性に対しては予測の役に立たないように加工したのでした。
ここで、機械学習モデルに対していくつかの精度指標と公平性指標を確認することができる補助関数を定義しておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# Helper functions def get_metrics_df(models_dict, y_true, group): metrics_dict = { "Overall selection rate": ( lambda x: selection_rate(y_true, x), True), "Demographic parity difference": ( lambda x: demographic_parity_difference(y_true, x, sensitive_features=group), True), "Demographic parity ratio": ( lambda x: demographic_parity_ratio(y_true, x, sensitive_features=group), True), "-----": (lambda x: "", True), "Overall balanced error rate": ( lambda x: 1-balanced_accuracy_score(y_true, x), True), "Balanced error rate difference": ( lambda x: difference_from_summary( balanced_accuracy_score_group_summary(y_true, x, sensitive_features=group)), True), "Equalized odds difference": ( lambda x: equalized_odds_difference(y_true, x, sensitive_features=group), True), "------": (lambda x: "", True), "Overall AUC": ( lambda x: roc_auc_score(y_true, x), False), "AUC difference": ( lambda x: difference_from_summary( roc_auc_score_group_summary(y_true, x, sensitive_features=group)), False), } df_dict = {} for metric_name, (metric_func, use_preds) in metrics_dict.items(): df_dict[metric_name] = [metric_func(preds) if use_preds else metric_func(scores) for model_name, (preds, scores) in models_dict.items()] return pd.DataFrame.from_dict(df_dict, orient="index", columns=models_dict.keys()) |
この関数は、モデル名とテスト用データに対する予測ラベルおよび予測スコアを辞書型で渡すことで、複数のモデルの精度指標と公平性指標を出力することができます。先ほどの公平性を考慮しないモデルに対して補助関数を適用するために、この分類器の予測ラベルを、予測スコアが学習用データの目的変数の平均値以上になる場合を1、そうでない場合は0と定義しましょう。
1 2 |
# Predictions (0 or 1) on test set test_preds = (test_scores >= np.mean(Y_train)) * 1 |
実際に補助関数を適用して精度指標と公平性指標の値を確認してみましょう。
1 2 3 |
# Metrics models_dict = {"Unmitigated": (test_preds, test_scores)}#予測ラベル、予測スコア get_metrics_df(models_dict, Y_test, A_str_test) |
上表のOverall AUCを見ると、ある程度の精度があることが分かる。しかし、格差メトリクスであるEqualized odds
differenceを見ると男女間では公平性のギャップが大きいことが分かります。我々の目的はThresholdOptimizerを用いて男女間の不公平性を緩和することでした。
■Fairlearn Dashboardを用いた機械学習モデルの公平性の評価
ThresholdOptimizerを実装する前にLightGBMモデルの予測結果についてFairlearn Dashboardで確認してみましょう。
1 2 3 4 |
FairlearnDashboard(sensitive_features=A_str_test, sensitive_feature_names=['Sex'], y_true=Y_test, y_pred={"Unmitigated": test_preds}) |
Get startedを選択します。
Fairlearn Dashboardに渡しているセンシティブ属性は性別のみなので、そのままNextを選択します。
精度指標はAUCが未対応なので、Balanced accuracyを選び、Nextを選択します。Balanced accuracyとは、正例を正しく正例と予測した割合と負例を正しく負例と予測した割合の平均値を表します。
Fairlearn Dashboardでは、2つの観点でセンシティブ属性間の予測結果を比較することができます。上段がDisparity in performanceで下段がDisparity in predictionsです。
Disparity in performanceでは、先ほど選択した精度指標(Balanced accuracy)に基づく全体の精度とセンシティブ属性間での精度差および各属性の誤差分布を棒グラフの形で確認することができます。オレンジのバーは偽陰性率、青色のバーは偽陽性率を表します。上図を見ると男女間で精度差が18%程度あり、特に偽陰性率に差があるようです。
Disparity in predictionsでは、予測ラベルの正例率を全体と属性間で比較することができます。上図を見ると男女間で予測ラベルの正例率には5%ほど差があるようです。
■ThresholdOptimizerを用いた不公平性の緩和
公平性を考慮せずに作成したLightGBMモデルは男女間で不公平な予測結果を導くことが分かりました。ここでは、Fairlearnが備える緩和アルゴリズムであるThresholdOptimaizerを用いて男女間での格差緩和を試みましょう。格差メトリクスとしては等価オッズ差を用い、これがゼロであるという制約条件の下で目的関数を最適化します。これによりLightGBMモデルの予測スコアに対する最適な閾値を探索します。我々の精度指標はBalanced accuracyなので、学習用データをリサンプルし、正例と負例の数を等しくした学習用データを作成しておくと効果的な最適化をすることができます。まずは、ThresholdOptimizerのインスタンスを作成します。引数には補正したいモデル(LightGBMモデル)と制約条件となる格差メトリクス(等価オッズ差)を渡します。
1 2 3 |
postprocess_est = ThresholdOptimizer( estimator=model, constraints="equalized_odds") |
正例と負例の数を等しくした学習用データを作成します。
1 2 3 4 5 |
balanced_idx1 = df_train[Y_train==1].index pp_train_idx = balanced_idx1.union(Y_train[Y_train==0].sample(n=balanced_idx1.size, random_state=1234).index) df_train_balanced = df_train.loc[pp_train_idx, :] Y_train_balanced = Y_train.loc[pp_train_idx] A_train_balanced = A_train.loc[pp_train_idx] |
正例と負例のバランスを調節した学習用データをThresholdOptimizerインスタンスにfitさせます。
1 2 3 |
postprocess_est.fit(df_train_balanced, Y_train_balanced, sensitive_features=A_train_balanced) |
ThresholdOptimizerモデルに引数としてテスト用データとそのセンシティブ属性を渡します。
1 2 |
postprocess_preds = postprocess_est.predict(df_test, sensitive_features=A_test) |
今まで作成したモデルの精度指標と公平性指標の値を比較してみましょう。そのために、モデル名とその予測ラベル、予測スコアの辞書を作成し、補助関数を適用します。
1 2 3 |
models_dict = {"Unmitigated": (test_preds, test_scores), "ThresholdOptimizer": (postprocess_preds, postprocess_preds)} get_metrics_df(models_dict, Y_test, A_str_test) |
上表を見るとThresholdOptimizerアルゴリズムは、複数の格差メトリクスにおいて不公平性を緩和できていることが分かります。しかし、精度指標(Overall AUCやOverall balanced error rate)は悪化しています。このような急激な変化の原因は、精度と公平性のトレードオフによるものもありますが、グループ間でのレコード数の差異の影響が大きいことも理由の1つだと考えられます。実際に、女性のデータは男性の1.5倍ほど多いです。
作成したモデルをFairlearn Dashboardでも比較してみましょう。y_predにモデル名と予測ラベルの辞書を渡すことで、比較が可能となります。Fairlearn Dashboardの設定は先ほどと同じなので省略します。
Disparity in accuracyを選択しているので、縦軸は属性間の精度差を表します。
Disparity in predictionsでは、縦軸は属性間の予測ラベルの正例率の差を表しています。いずれの場合も対角線上に点が並んでおり、精度と公平性のトレードオフが見て取れます。
グラフ上のモデルを表す点を選択することで、個別のモデルに対してFairlearn Dashboardの出力結果を見ることができます。実際に、ThresholdOptimizerを表す点を選択してみましょう。
上のグラフを見ると、男性の精度を下げることなく公平性を満たすように女性の精度を調節できていることが分かります。
最後に、モデル間で予測格差がどれほど緩和したかを可視化してみましょう。Disparity in predictionsは予測ラベルの正例率を確認することができました。ここでは、正解ラベルの正例率も合わせてグラフ化してみましょう。
まず、必要なデータをデータフレームに格納します。
1 2 3 4 5 6 7 |
df_lgbm_true_pred = pd.DataFrame({'SEX' : A_str_test.reset_index(drop=True), 'true_label' : Y_test.reset_index(drop=True), 'pred_label' : test_preds}) df_thrsholdopt_true_pred = pd.DataFrame({'SEX' : A_str_test.reset_index(drop=True), 'true_label' : Y_test.reset_index(drop=True), 'pred_label' : postprocess_preds}) |
下記のコードで可視化を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
def true_pred_positive_ratio(df_true_pred): #男女比 df_length = len(df_true_pred) df_count = df_true_pred.groupby('SEX').count()['true_label'] df_count_ratio = df_count / df_length #実測正例率 df_true_length = df_true_pred.groupby('SEX').count()['true_label'] df_true_count = df_true_pred[df_true_pred['true_label'] == 1].groupby('SEX').count()['true_label'] df_true_count_ratio = df_true_count / df_true_length #予測正例率 df_pred_length = df_true_pred.groupby('SEX').count()['pred_label'] df_pred_count = df_true_pred[df_true_pred['pred_label'] == 1].groupby('SEX').count()['true_label'] df_pred_count_ratio = df_pred_count / df_pred_length return df_count_ratio, df_true_count_ratio, df_pred_count_ratio lgbm_positive_ratio = true_pred_positive_ratio(df_lgbm_true_pred) thrsholdopt_positive_ratio = true_pred_positive_ratio(df_thrsholdopt_true_pred) fig = plt.figure(figsize=(10,6)) ax11 = fig.add_subplot(1, 2, 1) ax12 = ax11.twinx() ax13 = ax11.twinx() ax21 = fig.add_subplot(1, 2, 2, sharey=ax11) ax22 = ax21.twinx() ax23 = ax21.twinx() ax11.get_shared_y_axes().join(ax11, ax21) ax12.get_shared_y_axes().join(ax12, ax13, ax22, ax23) ax11.bar(lgbm_positive_ratio[0].index, lgbm_positive_ratio[0], color='gray', label='男女比') ax12.plot(lgbm_positive_ratio[1].index, lgbm_positive_ratio[1], linewidth=4, color='red', label='実績正例率') ax13.plot(lgbm_positive_ratio[2].index, lgbm_positive_ratio[2], linewidth=4, color='blue', label='予測正例率') ax11.set_title('LightGBM') ax11.legend(bbox_to_anchor=(1.7, -0.1), borderaxespad=0, fontsize=18, frameon=False) ax12.legend(bbox_to_anchor=(0.5, -0.1), borderaxespad=0, fontsize=18, frameon=False) ax13.legend(bbox_to_anchor=(1.2, -0.1), borderaxespad=0, fontsize=18, frameon=False) ax21.bar(thrsholdopt_positive_ratio[0].index, thrsholdopt_positive_ratio[0], color='gray') ax22.plot(thrsholdopt_positive_ratio[1].index, thrsholdopt_positive_ratio[1], linewidth=4, color='red') ax23.plot(thrsholdopt_positive_ratio[2].index, thrsholdopt_positive_ratio[2], linewidth=4, color='blue') ax21.set_title('ThresholdOptimizer') plt.subplots_adjust(wspace=0.3) |
上図を見ると、LightGBMモデルでは女性を実際のデフォルト率よりも高く予測していることが分かります。ThresholdOptimizerではこの男女間での格差が緩和されていることが分かります。