データサイエンティスト(仮)の暫定解更新ブログ

機械学習・人工知能について勉強した内容など広く扱います。理論も触れつつ実際に手を動かしていきます、主にPython使います。

機械学習の公平性を担保する手法紹介 -Pythonパッケージfairlearnをつかった実装-

f:id:Yuminaga:20210523150909j:plain

Image by Pixabay

 

今回の記事は、機械学習の公平性 (Fairness)についてです。

機械学習の公平性は、ざっくりいうと、AIによって差別を生まない・助長しないためにAIを公平に作れないかといったことです。例えば、予測結果がセンシティブな変数(状況やタスクによるものだとは思いますが、人種や性別など)に左右されないようにしたいなど。

ただそれらはセンシティブな変数に関するデータを集めない or モデルに入力しないといったことだけでは実現できない場合が多いです。

そもそもの公平性の定義やそれを実現するアルゴリズムとして様々なものがあるのですが、今回はPythonパッケージであるfairlearnに実装されているアルゴリズムを中心に紹介していきます。

 

そもそも公平性って?

定義

そもそも公平性って何だって話ですが、いろんな定義があるようです。サーベイ論文とかみると10個くらいあるんですが*1*2、(集団)公平性の定義メジャーどころとしてDemographic Parity、Equalized Oddsがあります。

 

2値分類を例に挙げて、$Y$が真のラベル、$S$がセンシティブな変数、$\hat{Y}$を予測ラベルで、それぞれ$\left\{0,1\right\}$だとすると、

  1. Demographic parity (Statistical Parity)
    $$ \text{Pr}\left\{  \hat{Y} = 1 | S = 0 \right\} = \text{Pr}\left\{  \hat{Y} = 1 | S = 1 \right\}$$
  2. Equalized odds
    $$ \text{Pr}\left\{  \hat{Y} = 0 | S = 0, Y=y \right\} = \text{Pr}\left\{  \hat{Y} = 1 | S = 1, Y=y \right\}, y \in \left\{0,1\right\}$$

と表されます。

 

ちなみに、機械学習では$y=1$の場合に、採用、合格、昇進など"advanced"なアウトカムを持ってくることが多いことから、Equalized Oddsで$y=1$のみのケースに限定したものは、Equal Opportunityと言われるそうです *3。 

定義の違いと解釈

定義をそのままみると、Demographic Parityは、センシティブな変数で条件づけられた予測ラベルの分布が一致する。Equalized oddsは、センシティブな変数に加え、真のラベルで条件づけられた予測ラベルの分布が一致するという定義になっています。

また、よくみてみると、Equalized oddsは、$y=1$の場合は$S$ごとのTrue Positive Rateの差が0、$y=0$の場合はFalse Positive Rateの差が0、と読むことができ、センシティブ変数によって精度(TPR、FPR)が変わらないことを要求していることがわかります。

 

これらの公平性の解釈を自分なりにしてみると、

Demographic Parityは、センシティブ変数による予測確率に差がないものの、本来の能力や適性といった$\text{Pr} (Y = 1)$や$\text{Pr} (Y = 0)$がセンシティブ変数によって本来偏っているべきものの場合、逆差別を生んでしまう可能性がある。

ex. 採用条件としては本来女性の方が向いているのに、男性を採用するようにした。

 

Equalized oddsは、センシティブ変数によってTPR、TPRに差がなく同精度を要求しているものの、本来の$\text{Pr} (Y = 1)$や$\text{Pr} (Y = 0)$が差別を生んでしまっている場合にそれを許容してしまいます。

ex. 採用条件としては本来女性の方が向いているので、女性を採用したいがそれは差別を生んでしまっている。

といったことが言えるのではないかなと。

どうやって公平性を実現するか

ではこのDemographic ParityやEqualized Oddsといった公平性をどうやって実現するのかの話です。単純にセンシティブな変数をモデルに食わせなきゃいいじゃん!と思いますが、その場合センシティブな変数と、他のモデルに入れる変数に関連があると、間接的にセンシティブな変数の情報を含んでしまうので公平な予測結果を実現できません。

 

ではどうやるのかというと、これには大きく分けて3つの手法の枠組みがあります。

  1. Pre-Processing
    モデルにfitする前に前処理としてセンシティブ変数に関連するような情報を除去しようとするアプローチ。モデルは既存のものを想定。例えば、センシティブ変数でそれ以外を回帰して、その結果を引いたデータをつくるなど。

  2. In-Processing
    フィットするモデルを修正して公平性をアルゴリズム内で担保しようとするアプローチ。例えば、モデルの最適化の際に制約を加えるなど。

  3. Post-Processing
    モデルは既存のものを使ってでてきた予測分布を補正しようとするアプローチ。例えば、センシティブ変数でグループ分けして、公平性の基準で閾値をグループごとに決定し、予測ラベルを決めるなど。

fairlearnで実装されているメソッド紹介

 fairlearnで実装されているものの中で以下の3つのメソッドを紹介してきます。

      1. preprocess.CorrelationRemover(Pre-Processing)

        このメソッドでは単純にセンシティブ変数で、それ以外の変数を推定する線形回帰を行い、元のデータセットから予測結果引くことで、センシティブ変数とはそれぞれ無相関なデータセットを得る方法です。

        元論文が見つけられなかったのですが、fairlearnのサイトと実装をみると、センシティブ変数群$S$を中心化$S_\text{center} = S - \bar{S}$して、それを元にセンシティブ変数以外の変数群$X_\text{orig}$を回帰し、係数$W$の推定値
        $$\hat{W} = \text{arg}\min_W \| X_\text{orig} - WS_\text{center} \| $$を得ます。そうして得た$\hat{W}$を用いて、
        $$X_\text{filtered} =X_\text{orig}  - \hat{W}S_\text{center} $$を推定。最終的に、どれだけリークさせるかの調整パラメータ$\alpha$を用いて、
        $$X_\text{tfm}  = \alpha X_\text{filtered} - (1 - \alpha) X_\text{orig}$$が返ってきます。

        この時、センシティブ変数は落ちて返ってくるので変数数は減って返ってくることに注意です。また、公式ドキュメントにも書いてありますが、無相関は独立とは別なので、公平性の文脈では(一般化)線形回帰などの線形関係を扱うモデルの前処理として使うべき手法だと思われます。

        他のメソッドと違い、明示的にDemographic Parity, Equalized Oddsを実現するわけではないですが、$\alpha = 1$の場合でかつ線形回帰の前処理として用いるとしたら、完全にセンシティブ変数の情報が入力データからは消えるので、Demographic Parityは保てるのではないかなと思いました。(間違っていたらごめんなさい)

      2. reduction.DemographicParity, EqualizedOdds, TruePositiveRateParity(In-Processing)
        元論文はこちらで提案手法はReduction Approachといわれています。ざっくり手法の方針を書くと、通常の分類器が解く最適化問題にそれぞれDemographicParity, EqualizedOdds, EqualOpportunity(fairlearnではTruePositiveRateParity)を制約として入れることを考えます。

        この定式化の際に、ラグランジュの未定乗数法を使うため、「通常のloss」 + $\lambda$「公平性のloss」を最小化する問題となります。この$\lambda$はハイパーパラメータなので、指定する必要があるのですが、fairlearn.reduction.GridSearchにて$\lambda$を探索することができます。

        緩和した問題を解くため、それぞれの性質を保証できるわけではないことに注意が必要です。LightGBMなどポピュラーな分類器を元のestimatorとして指定して使用することができるものの、sample_weight(とy)を用いて制約を表現するため、fit時に指定できるモデルでなければ現状は使えない点に注意が必要です。

        この手法の強みとして、学習時にはセンシティブ変数が必要だが、予測時(test-time access)には必要ないことが挙げられます。今まではセンシティブなデータを集めていたけども、今後はセンシティブデータを集めないようにしたいなど場面で活躍しそうですね。

      3. postprocess.ThresholdOptimizer(Post-Processing)
        元論文はこちらです。

        ざっくり手法の方針を書くと、何らかの方法で学習済みの2値分類モデルによる予測ラベルを変換して、Demographic Parity、Equalized OddsやEqual Opportunityなど指定した公平性を満たすような"良い"閾値を各群で見つけることになります。既にある学習済みのモデルの予測ラベルを変換することで、それを達成しようとします。

        学習済みの2値分類モデルによる予測ラベルを$\hat{Y}$として、それをpost-processingにより変換した予測$\tilde{Y}$、センシティブ変数$S\in \left\{0,1\right\}$だとします。この予測$\tilde{Y}$を求めるのがpost-processingの目的になります。その可動域から、それぞれの求めたい制約に一致する点を設定し、解を求めることになります。

        f:id:Yuminaga:20210516195647p:plain

        元論文(https://arxiv.org/abs/1610.02413), fig1

        元論文の図がわかりやすいのですが、TPr, FPrに対して元の予測$\hat{Y}$を変化させることで達成しうる可動域がそれぞれのセンシティブ変数の値で緑と青とで分かれています。このときEqualized Oddsを達成するのは、$x, y$が一致する点(TPr, FPrが一致する点がEqualOddsなので)となります。$x, y$が一致するのはOverlap全体になるわけなのですが、その中でもロスが最小化される点(左図の赤点など)に対して、各群でその点を達成する点を算出するのが流れになります。

        ただし、これらを最小化問題として解くため、こちらのメソッドに関しても、それぞれの性質を完璧に保証できるわけではないことに注意が必要です。

        こちらもLightGBMなどポピュラーな分類器の学習結果を使用することができます。記事作成の2021/5月時点でセンシティブ変数が1つ、2値分類のみに対応しています。

fairlearnを用いてPythonでサンプルデータ実験

前置きが長くなりましたが、早速2値分類の問題に対してfairlearnを使ってみます。コードはすべてこちらに置いています。

公平性に関するMetricsの設定 

fairlearnでは公平性に関する様々なmetricsが用意されているようです。

今回の記事ではDemographicParityとEqualizedOddsに注目したためそれに関連する、以下の4つのmetricsを使用するのですが、理解を深める意味で自分でも実装してみようとおもいます!

  • demographic_parity_difference
  • true_positive_parity_difference
  • false_positive_parity_difference
  • equalized_odds_difference

 まずはfairlearnではfairlearn.metrics配下に各metricsが用意されているのでそれをimportとします。


# metrics import
from fairlearn.metrics import (
    demographic_parity_difference,
    true_positive_rate_difference, 
    false_positive_rate_difference,
    equalized_odds_difference)

# テストデータを準備
y_true = np.array([0,0,1,1,1,0,0])
y_pred = np.array([0,0,0,1,1,1,1])
s = np.array([1,0,1,0,1,0,0])
    
# fairlearnでのmetrics実装, 自作funcとの[abs_diff]との一致が確認できる
print("Demographic Parity Diff :\t\t %.3f"% demographic_parity_difference(y_true=y_true, y_pred=y_pred, sensitive_features=s))
print("TruePositiveRate Parity Diff :\t %.3f"% true_positive_rate_difference(y_true=y_true, y_pred=y_pred, sensitive_features=s))
print("FalsePositiveRate Parity Diff :\t %.3f"% false_positive_rate_difference(y_true=y_true, y_pred=y_pred, sensitive_features=s))

# TruePositiveRate DiffとFalsePositiveRate Diffのうち大きい方の値が入る
print("EqualizedOdds Parity Diff :\t\t %.3f"% equalized_odds_difference(y_true=y_true, y_pred=y_pred, sensitive_features=s))

# print結果
Demographic Parity Diff :		 0.417
TruePositiveRate Parity Diff :	 0.500
FalsePositiveRate Parity Diff :	 0.667
EqualizedOdds Parity Diff :		 0.667

fairlearnでの実行結果が確認できたので、今度は自分で実装してみます、それぞれの公平性の定義を元に各センシティブ変数の群で差をとったものを実装します。


# 公平性の各種metricsをprint
def print_result_summary(y_true, y_pred, s):
    # Demographic Parityで用いる差
    dp_ave =  y_pred.mean()
    dp_s1 =  y_pred[s==1].mean()
    dp_s0 = y_pred[s==0].mean()
    dp_diff = np.abs(dp_s1 - dp_s0)
    
    # TruePositiveRate Parityで用いる差 (Equal Opportunity)
    tpr_ave = y_pred[y_true==1].mean()
    tpr_s1 = y_pred[np.all([y_true==1, s==1],axis=0)].mean()
    tpr_s0 = y_pred[np.all([y_true==1, s==0],axis=0)].mean()
    tpr_diff = np.abs(tpr_s1 - tpr_s0)
    
    # FalsePositiveRate Parityで用いる差
    fpr_ave = y_pred[y_true==0].mean()
    fpr_s1 = y_pred[np.all([y_true==0, s==1],axis=0)].mean()
    fpr_s0 = y_pred[np.all([y_true==0, s==0],axis=0)].mean()
    fpr_diff = np.abs(fpr_s1 - fpr_s0)
    
    # print result
    dp_text = f"Demographic Parity:\t\t[mean] {dp_ave:.3f},\t[s=1] {dp_s1:.3f},\t[s=0] {dp_s0:.3f},\t[abs_diff] {dp_diff:.3f}"
    tpr_text = f"TruePositiveRate Parity:\t[mean] {tpr_ave:.3f},\t[s=1] {tpr_s1:.3f},\t[s=0] {tpr_s0:.3f},\t[abs_diff] {tpr_diff:.3f}"
    fpr_text = f"FalsePositiveRate Parity:\t[mean] {fpr_ave:.3f},\t[s=1] {fpr_s1:.3f},\t[s=0] {fpr_s0:.3f},\t[abs_diff] {fpr_diff:.3f}"
    print(dp_text)
    print(tpr_text)
    print(fpr_text)

実行してみます。


# 自作metricsのprint
print_result_summary(y_true, y_pred, s)

# 結果
Demographic Parity:		[mean] 0.571,	[s=1] 0.333,	[s=0] 0.750,	[abs_diff] 0.417
TruePositiveRate Parity:	[mean] 0.667,	[s=1] 0.500,	[s=0] 1.000,	[abs_diff] 0.500
FalsePositiveRate Parity:	[mean] 0.500,	[s=1] 0.000,	[s=0] 0.667,	[abs_diff] 0.667

 結果を見るとabs_diffの結果がfairlearn実装のmetricsと一致していることがわかります。equalized_odds_differenceはtpr, fpr_differenceのうち大きい方をとっているため少々トリッキーですが、それ以外は単純に定義に従って差を取っているだけなので、どれだけその公平性の定義に従っているかの定量評価指標になっていることがわかります。

 

サンプルデータ作成

これから上で紹介した3つのメソッドを実行するのですが、そのための実験用の擬似データを作成しておきます。作成するのは3変数で$x, y, s$を作成します。$s$はセンシティブ変数として、$x$は$s$と関連がある特徴量です。そして$y$はラベルで$x, s$の両方とも関連があるようにします。


import numpy as np
def logistic(x, a=1):
    return 1/(1 + np.exp(-a*x))

# サンプルデータ生成
def make_sample_data(N=10000, p_s=0.1):
    # p_s: sensitive変数の平均値
    s = np.r_[np.zeros(int(N*p_s)) , np.ones(int(N*(1-p_s)))] # sensitive
    np.random.seed(0)
    x = np.random.normal(0, 1, size=N) - 0.2*s  # correlated with s

    np.random.seed(1)
    y = 0.3*x + 0.5*s + np.random.normal(0,1,size=N)  # outcome y is correlated with x and s
    y -= np.mean(y)
    y = np.array(logistic(y) > 0.5, dtype=int) # flg

    np.random.seed(2)
    train_idx = np.random.choice(N, size=N//2, replace=False) # random split
    test_idx = np.array(list(set(np.arange(N)) - set(train_idx)))

    return x[train_idx], y[train_idx], s[train_idx], x[test_idx], y[test_idx], s[test_idx]
    
# sample data
x_train, y_train, s_train, x_test, y_test, s_test = make_sample_data(N=10000, p_s=0.1)
X_train = np.c_[x_train, s_train]
X_test = np.c_[x_test, s_test]

 

 上記のコードでサンプル数10000のデータを作成しました。以下ではこれを用いてfitしていきます。 

ベースラインの実行

fairlearnで公平性を実現する前に、単純に何も処理せずモデルを学習した時の各metricsの値を見てみます。LGBMClassifierを用いてfitしてみます。


# model fit
baseline_model = LGBMClassifier(random_state=1)
baseline_model.fit(X_train, y_train)

# predict
y_train_pred_baseline = baseline_model.predict(X_train)
y_test_pred_baseline = baseline_model.predict(X_test)

# print result
print("Train")
print_result_summary(y_true=y_train, y_pred=y_train_pred_baseline, s=s_train)
print("\nTest")
print_result_summary(y_true=y_test, y_pred=y_test_pred_baseline, s=s_test)

# 結果 (ベースラインモデル)
Train
Demographic Parity:		[mean] 0.548,	[s=1] 0.575,	[s=0] 0.304,	[abs_diff] 0.271
TruePositiveRate Parity:	[mean] 0.681,	[s=1] 0.689,	[s=0] 0.580,	[abs_diff] 0.109
FalsePositiveRate Parity:	[mean] 0.411,	[s=1] 0.451,	[s=0] 0.130,	[abs_diff] 0.321

Test
Demographic Parity:		[mean] 0.529,	[s=1] 0.558,	[s=0] 0.270,	[abs_diff] 0.288
TruePositiveRate Parity:	[mean] 0.607,	[s=1] 0.624,	[s=0] 0.372,	[abs_diff] 0.252
FalsePositiveRate Parity:	[mean] 0.451,	[s=1] 0.486,	[s=0] 0.216,	[abs_diff] 0.270

 

Demographic Parity, TPR, FPR Parityの全ての各群で大きな差が出てしまっていることがわかります。次に、これをセンシティブ変数sをdropした状態でfitした結果を見てみると、


# 結果 (ベースラインモデル、センシティブ変数drop)
Train
Demographic Parity:		[mean] 0.563,	[s=1] 0.558,	[s=0] 0.608,	[abs_diff] 0.050
TruePositiveRate Parity:	[mean] 0.682,	[s=1] 0.671,	[s=0] 0.824,	[abs_diff] 0.153
FalsePositiveRate Parity:	[mean] 0.440,	[s=1] 0.435,	[s=0] 0.472,	[abs_diff] 0.037

Test
Demographic Parity:		[mean] 0.543,	[s=1] 0.539,	[s=0] 0.578,	[abs_diff] 0.039
TruePositiveRate Parity:	[mean] 0.608,	[s=1] 0.604,	[s=0] 0.669,	[abs_diff] 0.065
FalsePositiveRate Parity:	[mean] 0.476,	[s=1] 0.468,	[s=0] 0.530,	[abs_diff] 0.062

となり、センシティブ変数を入れていた時よりもだいぶ差は緩和されるものの、まだ最小でも5%の差が群間であることがわかります。

 

アルゴリズムの実行 (CorrelationRemover)

さて、それではfairlearnを用いていきます。最初にCorrelationRemoverを用いていきます。使い方は簡単で、CorrelationRemoverの中のsensitive_feature_idsにセンシティブ変数のインデックスを指定して、あとはfittransformを行うだけです。 


from lightgbm import LGBMClassifier
from fairlearn.preprocessing import CorrelationRemover
# 相関を除去
corr_remover = CorrelationRemover(sensitive_feature_ids=[1]) # X_trainのうち2列目がsに該当
X_train_rmcorr = corr_remover.fit_transform(X_train)
X_test_rmcorr = corr_remover.transform(X_test)

# model fit
clf = LGBMClassifier()
clf.fit(X_train_rmcorr, y_train)

# predict
y_train_pred_rmcorr = clf.predict(X_train_rmcorr)
y_test_pred_rmcorr = clf.predict(X_test_rmcorr)

# print result
print("Train")
print_result_summary(y_true=y_train, y_pred=y_train_pred_rmcorr, s=s_train)
print("\nTest")
print_result_summary(y_true=y_test, y_pred=y_test_pred_rmcorr, s=s_test)

予測値の結果を見てみると、


# 結果 (CorrelationRemover)
Train
Demographic Parity:		[mean] 0.539,	[s=1] 0.539,	[s=0] 0.540,	[abs_diff] 0.001
TruePositiveRate Parity:	[mean] 0.661,	[s=1] 0.655,	[s=0] 0.736,	[abs_diff] 0.080
FalsePositiveRate Parity:	[mean] 0.413,	[s=1] 0.412,	[s=0] 0.417,	[abs_diff] 0.005

Test
Demographic Parity:		[mean] 0.526,	[s=1] 0.524,	[s=0] 0.544,	[abs_diff] 0.020
TruePositiveRate Parity:	[mean] 0.594,	[s=1] 0.588,	[s=0] 0.674,	[abs_diff] 0.087
FalsePositiveRate Parity:	[mean] 0.458,	[s=1] 0.456,	[s=0] 0.476,	[abs_diff] 0.020

 

となり、どれも大幅に改善されていることがわかります。

特にDemographic Parity Differenceに関しては、trainで0.1%、testでも2%となっていることがわかります。今回はシンプルな線形データを用いたからだとは思いますが、このようにわかりやすい手法で良い結果が出るのはいいですね。

 

アルゴリズムの実行 (Reduction Approach)

ReductionMethodに関しては、GridSearchを用いて実行していきます。肝となるのはGridSearchconstraintsという引数になります。これに対してreduction.DemographicParityなどを呼び出して指定することで、指定された公平性をモデルに加味することができます。

他にもEqualizedOdds, TruePositiverateParityなど様々なものが用意されているので、それを切り替えるだけで加味する公平性を変えることができます。その他の引数として、estimatorを指定して(sample_weightをfit時に指定できるモデルに限定)、グリッドサーチのパラメータを設定することができます。

実行はsklearnライクにfit, predictで行うことができます。

 


# model fit
from fairlearn.reductions import DemographicParity # EqualizedOdds, TruePositiveRateParity
from fairlearn.reductions import GridSearch
sweep = GridSearch(
                   estimator=LGBMClassifier(random_state=1), 
                   constraints=DemographicParity(),  # EqualizedOdds, TruePositiveRateParityなども使用可
                   grid_limit = 1, # lambdaを-grid_limitからgrid_limitまでに設定
                   grid_size = 50  # 何分割して実行するか

)
sweep.fit(X_train, y_train, sensitive_features=s_train)

# predict
y_train_pred_indp = sweep.predict(X_train) # 最もDemographicParityが小さいものが選ばれる
y_test_pred_indp = sweep.predict(X_test)

# print result
print("Train")
print_result_summary(y_true=y_train, y_pred=y_train_pred_indp, s=s_train)
print("\nTest")
print_result_summary(y_true=y_test, y_pred=y_test_pred_indp, s=s_test)

結果として、


# 結果 (Reduction with DemographicParity)
Train
Demographic Parity:		[mean] 0.544,	[s=1] 0.544,	[s=0] 0.536,	[abs_diff] 0.008
TruePositiveRate Parity:	[mean] 0.669,	[s=1] 0.660,	[s=0] 0.788,	[abs_diff] 0.128
FalsePositiveRate Parity:	[mean] 0.414,	[s=1] 0.420,	[s=0] 0.378,	[abs_diff] 0.042

Test
Demographic Parity:		[mean] 0.524,	[s=1] 0.524,	[s=0] 0.520,	[abs_diff] 0.004
TruePositiveRate Parity:	[mean] 0.591,	[s=1] 0.589,	[s=0] 0.616,	[abs_diff] 0.027
FalsePositiveRate Parity:	[mean] 0.456,	[s=1] 0.454,	[s=0] 0.470,	[abs_diff] 0.016

となり、指定したDemographic Parityが高い精度で保たれていることがわかります。

ちなみに上のコードのpredictで用いられるモデルはGridSearchで探索した範囲で、指定したMetricsを最小にするモデルになります。一応確かめるために各predictorを取得して、demographic_parity_differenceを計算して、最終的にそれをソートしてみます。


sweep_preds = [predictor.predict(X_train) for predictor in sweep.predictors_] # 各predictorの予測値を取得
dp_diff_list = [
    demographic_parity_difference(y_train, preds, sensitive_features=s_train)
    for preds in sweep_preds
]
print(np.sort(dp_diff_list)[:5]) #top-5のdemographic_parity_diffを確認, sweep.predictによる結果と同一であることを確認できる

> array([0.00844444, 0.01511111, 0.038     , 0.05511111, 0.05911111])

となり、最小のモデルがTrainのDemographic Parityのabs_diffと一致しているため、指定したmetricsでの最良のモデルが選択されていることがわかります。

 

アルゴリズムの実行 (ThresholdOptimizer)

最後にThresholdOptimizerを使用してみます。こちらも使い方はシンプルで、constraintsの引数に加味したい公平性を"demographic_parity""equalized_odds"などを指定します。

他の引数としては、すでに学習しているモデルをestimatorとして使用する場合にはprefit=Trueとして、学習済みでない場合はインスタンンスをestimatorで設定しprefit=Falseに設定します。 そしてfit, predictを行えば実行できます。

1点注意すべき点として、ThresholdOptimizerに関しては、predictが乱数依存になっているので、再現性を確保するためにはrandom_stateを指定する必要があります。


from fairlearn.postprocessing import ThresholdOptimizer
# set 
optimizer = ThresholdOptimizer(
    estimator=baseline_model, 
    constraints="demographic_parity", # 他に’{false,true}_{positive,negative}_rate_parity’’equalized_odds’ が使用可能
    prefit=True # 学習済みモデルを渡している場合 True, prefit=Falseでestimator=LGBMClassifier(random_state=1)でも同じ結果
)

# fit optimizer
optimizer.fit(X=X_train,y=y_train.reshape(-1,1), sensitive_features=s_train)
y_test_pred_postdp = optimizer.predict(X_test, sensitive_features=s_test, random_state=20) #乱数固定
y_train_pred_postdp = optimizer.predict(X_train, sensitive_features=s_train, random_state=100) # 乱数固定

# print result
print("Train")
print_result_summary(y_true=y_train, y_pred=y_train_pred_postdp, s=s_train)
print("\nTest")
print_result_summary(y_true=y_test, y_pred=y_test_pred_postdp, s=s_test)

結果は、以下のようになり、特に指定したDemographic Parityについて、GridSearchほどでもないですが、保たれていることがわかります。これはpredictの乱数によって変わるのであまりどっちがよいなどというものではなさそうですね。


# 結果 (ThresholdOptimizer with DemographicParity)
Train
Demographic Parity:		[mean] 0.578,	[s=1] 0.575,	[s=0] 0.600,	[abs_diff] 0.025
TruePositiveRate Parity:	[mean] 0.693,	[s=1] 0.689,	[s=0] 0.741,	[abs_diff] 0.051
FalsePositiveRate Parity:	[mean] 0.459,	[s=1] 0.451,	[s=0] 0.511,	[abs_diff] 0.060

Test
Demographic Parity:		[mean] 0.557,	[s=1] 0.558,	[s=0] 0.554,	[abs_diff] 0.004
TruePositiveRate Parity:	[mean] 0.625,	[s=1] 0.624,	[s=0] 0.640,	[abs_diff] 0.015
FalsePositiveRate Parity:	[mean] 0.489,	[s=1] 0.486,	[s=0] 0.509,	[abs_diff] 0.023

 

以上3つのメソッドをみてきましたが、とても簡単に使えるようになっていますね。記事中のコードや、記事内では紹介できなかったEqualizedOddsを指定したバージョンなどのコードはこちらに置いています。

まとめ

fairlearnはまだまだ発展性がありそうなパッケージですし、現状でもある程度の問題には対処できそうです。公平性は社会的ニーズが強そうな話題なので今後も研究が盛んにされていきそうです。

 

個人的には、センシティブなデータの影響を除くためにセンシティブなデータを使うっていうのは、確かにそうなんだろうけど、センシティブなデータがなければ学習できない、かつ手法によっては引き続き収集しなきゃいけないってことがちょっと微妙っぽいなと思いました。その中でも、predict時にセンシティブなデータを使わなくて済むReduction Approachに関しては比較的使いやすいなと思いました。ただモデル更新するときどうするのかはどうしようって感じですが。。。

また、今まで人間にもできていないケースがあったであろう公平性を機械学習モデルなどに求めるために、人間の理想とする公平性が定量化されて機械学習モデルに使えるようになっていくといったプロセスは非常に興味深いです。

 

公平性(fairness)に関して、この記事では集団公平性というものにしか触れられていませんが、個人公平性など様々な研究がなされているようです。日本語ですばらしいまとめ*4*5があるので興味がある方はぜひ。 

 

*1:レビュー論文1

[1908.09635] A Survey on Bias and Fairness in Machine Learning

*2:レビュー論文2

[2010.04053] Fairness in Machine Learning: A Survey

*3: fairlearnのThresholdOptimizerの元論文

[1610.02413] Equality of Opportunity in Supervised Learning

*4:公平性に配慮した学習と理論的課題 (2018) 機械学習の公平性の定義も踏まえ技術的な話題に詳しいです。

https://ibisml.org/ibis2018/files/2018/11/fukuchi.pdf

*5:機械学習と公平性(2020), 公平性の背景などに詳しいです。

http://ai-elsi.org/wp-content/uploads/2020/01/20200109-fairness_sympo.pdf