机器学习中的偏见和公平第三部分:建立一个有偏见意识的模型

347 阅读12分钟

构建一个偏见意识的模型

关于数据集、模型偏见和公平性的机制和重要性以及建立基本模型的信息,请查看第一部分第二部分

让我们开始使用两种特征工程技术来构建一个更具偏见意识的模型。我们将首先应用一个熟悉的变换来构建一个新的少偏向的列,然后再进入我们书中的特征提取方法。我们的目标是在不牺牲大量模型性能的情况下将我们模型的偏差降到最低。

特征构建--使用Yeo-Johnson变换器来处理不同的影响

我们要做一些类似于box-cox变换的事情来变换我们的一些特征,以便使它们看起来更正常。为了设定原因,我们必须调查我们的模型对非非洲裔美国人的再犯率预测不足的原因。一种方法是将种族完全从我们的数据集中删除,并期望ML模型能消除所有的偏见。不幸的是,这很少是答案。

没有特权的人和有特权的人经历了不同的机会,这很可能通过相关的特征呈现在数据中。最有可能导致我们的模型出现偏差的原因是,至少有一个特征与种族高度相关,我们的模型能够通过这个特征重建某人的种族身份。为了找到这个特征,我们先来找一下我们的数字特征和非裔美国人的相关系数。

 compas_df.corrwith(compas_df['race'] == 'African-American').sort_values()
 age              

agepriors_count 都与我们的布尔标签(即简单的非裔美国人)高度相关,所以让我们仔细看看每一个标签。让我们先看一下年龄。我们可以绘制直方图并打印出一些基本的统计数据,我们将看到在我们的四个种族类别中,年龄似乎是比较相似的,有相似的平均值、标准差和中位数。这向我们表明,即使年龄与非裔美国人呈负相关,这种关系也不是导致我们的模型出现偏差的巨大因素。

 # Age is not very skewed
 compas_df.groupby('race')['age'].plot(
     figsize=(20,5),
     kind='hist', xlabel='Age', title='Histogram of Age'
 )
 compas_df.groupby('race')['age'].describe()

图1.各组的年龄分布。上面的表格意味着年龄分布在各组之间没有巨大的差异,从而意味着对差别待遇和影响的影响较小。值得注意的是,非裔美国人的平均年龄和中位数比其他类别的人年轻10-15%,这可能是我们看到我们的年龄栏和我们的非裔美国人识别栏之间有强烈关联的原因。


让我们把注意力转向priors_count,做同样的打印输出。当我们这样做时,我们会看到一些与年龄的鲜明对比。

 # Priors is extremely skewed by looking at the differences in mean/median/std across the racial categories
 compas_df.groupby('race')['priors_count'].plot(
     figsize=(20,5),
     kind='hist', xlabel='Count of Priors', title='Histogram of Priors'
 )
 compas_df.groupby('race')['priors_count'].describe()

图2.乍一看,似乎所有种族的priors模式都遵循一个类似的模式。先验数的分布在种族群体之间显示出类似的右歪。然而,由于许多人无法控制的原因,非洲裔美国人的中位数和平均数几乎是其他群体的两倍。


有两件事值得注意:

  1. 非洲裔美国人的先验数是严重右倾的,这从平均数是中位数的两倍以上可以看出。
  2. 由于长期以来的系统性刑事司法问题,非裔美国人的priors几乎是其他种族群体总和的两倍。

priors_count与种族如此相关,而且不同种族类别的倾斜程度不同,这是一个巨大的问题,主要是因为ML模型可能会发现这一事实,并且仅仅通过查看priors_count列就会对某些种族产生偏见。为了解决这个问题,我们将创建一个自定义的转化器,通过对每个种族类别的数值子集应用yeo-johnson转化来修改一个列。这将有助于消除该列对我们的群体公平性产生的不同影响。

作为伪代码,它看起来像

 For each group label:
         Get the subset of priors_count values for that group
         Apply the yeo-johnson transformation to the subset
         Modify the column in place for that group label with the new values

通过对每个子集的数值进行转换,而不是对整个列进行转换,我们迫使每个组的数值集成为平均值为0、标准差为1的正态,使模型更难从给定的priors_count值中重建一个特定的组标签。

让我们构建一个自定义的scikit-learn转化器来执行这个操作。

清单1.通过Yeo-Johnson减轻差别待遇

 from sklearn.preprocessing import PowerTransformer  # A
 from sklearn.base import BaseEstimator, TransformerMixin  # A
  
 class NormalizeColumnByLabel(BaseEstimator, TransformerMixin):
     def __init__(self, col, label):
         self.col = col
         self.label = label
         self.transformers = {}
        
     def fit(self, X, y=None):  # B
         for group in X[self.label].unique():
             self.transformers[group] = PowerTransformer(
                 method='yeo-johnson', standardize=True
             )
             self.transformers[group].fit(
                 X.loc[X[self.label]==group][self.col].values.reshape(-1, 1)
             )
         return self
    
     def transform(self, X, y=None):  # C
         C = X.copy()
         for group in X[self.label].unique():
             C.loc[X[self.label]==group, self.col] = self.transformers[group].transform(
                 X.loc[X[self.label]==group][self.col].values.reshape(-1, 1)
             )
         return C

# A 输入

# B 为每组标签装配一个PowerTransformer

# C 当转换一个新的D/dataframe时,我们使用我们已经适合的转化器的转化方法,并在原地修改数据框架

有了新的变换器,让我们把它应用于我们的训练数据,看看我们的先验计数已经被修改了,这样每个组标签的平均先验计数为0,标准偏差为1。

 n = NormalizeColumnByLabel(col='priors_count', label='race')
  
 X_train_normalized = n.fit_transform(X_train, y_train)
  
 X_train_normalized.groupby('race')['priors_count'].hist(figsize=(20,5))
 X_train_normalized.groupby('race')['priors_count'].describe()

图3.在对每个子组的先验计数子集应用yeo-johnson变换后,分布开始看起来不那么偏斜,彼此之间也不那么不同。这将使ML模型难以从这个特征中重建种族


清单2.我们的第一个偏向感知模型

 clf_tree_aware = Pipeline(steps=[
     ('normalize_priors', NormalizeColumnByLabel(col='priors_count', label='race')),  # A
     ('preprocessor', preprocessor),
     ('classifier', classifier)
 ])
  
 clf_tree_aware.fit(X_train, y_train)
 aware_y_preds = clf_tree_aware.predict(X_test)
  
 exp_tree_aware = dx.Explainer(clf_tree_aware, X_test, y_test, label='Random Forest DIR', verbose=False)  # B
 mf_tree_aware = exp_tree_aware.model_fairness(protected=race_test, privileged = "Caucasian")
  
 # performance is virtually unchanged overall
 pd.concat([exp.model_performance().result for exp in [exp_tree, exp_tree_aware]])
  
 # We can see a small drop in parity loss
 mf_tree.plot(objects=[mf_tree_aware], type='stacked')  # C

# A 在我们的预处理程序之前加入我们的新转化器,在做其他事情之前修复 priors_count

# B 检查我们的模型性能

# C 调查奇偶性损失的变化

我们新的带有差异影响去除功能的偏倚感知模型运行得很好!我们实际上可以看到,我们的偏倚损失有了小幅提升。我们实际上可以看到模型性能的小幅提升和累积奇偶性损失的小幅下降。


图4.上面的条形图代表了我们的偏见意识模型的偏见指标之和,在所有的指标中,除了召回率没有变化之外,模型的性能都有小幅提升(在指标表中有注明)。底部的条形图显示了我们之前看到的原始的无偏向意识的堆积图。总的来说,我们新的无偏见模型在一些ML指标中表现得更好,并且根据我们的平价损失条形图,显示出偏见的减少。我们正走在正确的道路上!


特征提取--使用AIF360学习公平表示法的实现

到目前为止,我们还没有做任何事情来解决我们的模型对敏感特征的不了解。我们将使用AI Fairness 360(aif360),这是一个由IBM开发的开源工具包,帮助数据科学家获得预处理、处理中和处理后的偏见缓解技术,以应用我们的第一个特征提取技术,称为学习公平表示(LFR)。LFR的想法是要映射我们的数据X。

对于我们的用例,我们将尝试把我们的分类变量(其中4/6代表种族)映射到一个新的 "更公平 "的向量空间,保留统计奇偶性,并尽可能多地保留我们原始X的信息。

Aif360使用起来可能有点棘手,因为他们强迫你使用他们自己版本的数据框架,称为BinaryLabelDataset 。下面是一个自定义的scikit-learn转化器,它将。

  1. 接收X,一个由我们的分类预处理器创建的二进制值的数据框架
  2. 将数据框架转换为二进制标签数据集(BinaryLabelDataset
  3. 适应aif360包中的LFR模块
  4. 使用现在适合的LFR转换任何新的数据集,将其映射到我们新的公平表示上

清单3.自定义LFR转换器

 from aif360.algorithms.preprocessing.lfr import LFR
 from aif360.datasets import BinaryLabelDataset
  
 class LFRCustom(BaseEstimator, TransformerMixin):
     def __init__(self, col, protected_col, unprivileged_groups, privileged_groups):
         self.col = col
         self.protected_col = protected_col
         self.TR = None
         self.unprivileged_groups = unprivileged_groups
         self.privileged_groups = privileged_groups
        
     def fit(self, X, y=None):
         d = pd.DataFrame(X, columns=self.col)
         d['response'] = list(y)
  
         binary_df = BinaryLabelDataset(  # A
             df=d,
             protected_attribute_names=self.protected_col,
             label_names=['response']
         )
  
         # Input reconstruction quality - Ax
         # Output prediction error - Ay
         # Fairness constraint - Az
  
         self.TR = LFR(unprivileged_groups=self.unprivileged_groups,
                  privileged_groups=self.privileged_groups, seed=0,
                  k=2, Ax=0.5, Ay=0.2, Az=0.2,  # B
                  verbose=1
                 )
         self.TR.fit(binary_df, maxiter=5000, maxfun=5000)
         return self
  
    
     def transform(self, X, y=None):
         d = pd.DataFrame(X, columns=self.col)
         if y:
             d['response'] = list(y)
         else:
             d['response'] = False
  
         binary_df = BinaryLabelDataset(
             df=d,
             protected_attribute_names=self.protected_col,
             label_names=['response']
         )
         return self.TR.transform(binary_df).convert_to_dataframe()[0].drop(['response'], axis=1)  # B

# A 与aif360的二进制标签数据集对象的转换和转换。

# B 这些参数可以在aif360网站上找到,是通过离线网格搜索发现的。

为了使用我们的新转换器,我们需要稍微修改我们的管道,并利用FeatureUnion对象。

清单4.带有不同影响去除和LFR的模型

 categorical_preprocessor = ColumnTransformer(transformers=[
     ('cat', categorical_transformer, categorical_features)
 ])  # A
  
 # Right now the aif360 package can only support one privileged and one unprivileged group
 privileged_groups = [{'Caucasian': 1}]  # B
 unprivileged_groups = [{'Caucasian': 0}]  # B
  
 lfr = LFRCustom(
     col=['African-American', 'Caucasian', 'Hispanic', 'Other', 'Male', 'M'],
     protected_col=sorted(X_train['race'].unique()) ,
     privileged_groups=privileged_groups,
     unprivileged_groups=unprivileged_groups
 )
  
 categorical_pipeline = Pipeline([
     ('transform', categorical_preprocessor),
     ('LFR', lfr),
 ])
  
 numerical_features = ["age", "priors_count"]
 numerical_transformer = Pipeline(steps=[
     ('scale', StandardScaler())
 ])
  
 numerical_preprocessor = ColumnTransformer(transformers=[
         ('num', numerical_transformer, numerical_features)
 ])  # A
  
 preprocessor = FeatureUnion([  # C
     ('numerical_preprocessor', numerical_preprocessor),
     ('categorical_pipeline', categorical_pipeline)
 ])
  
 clf_tree_more_aware = Pipeline(steps=[  # D
     ('normalize_priors', NormalizeColumnByLabel(col='priors_count', label='race')),
     ('preprocessor', preprocessor),
     ('classifier', classifier)
 ])
  
  
 clf_tree_more_aware.fit(X_train, y_train)
  
 more_aware_y_preds = clf_tree_more_aware.predict(X_test)

#A 隔离数字和分类的预处理器,这样我们就可以分别对分类数据进行LFR拟合。

#B 告诉aif360,高加索人标签为1的行是有特权的,高加索人标签为0的行是无特权的

#C 使用FeatureUnion将我们的分类数据和数字数据结合起来

#D 我们的新管道将通过yeo-johnson去除不同的影响/待遇,并将LFR应用于我们的分类数据,以解决模型不自觉的问题。

为了简单地将LFR模块应用于我们的数据框架,这是个很大的代码。真正的原因是需要将我们的pandas Dataframe转换成aif360的自定义数据对象,然后再返回。现在我们已经拟合了我们的模型,让我们最后看一下我们模型的公平性。

 exp_tree_more_aware = dx.Explainer(clf_tree_more_aware, X_test, y_test, label='Random Forest DIR + LFR', verbose=False)
  
 mf_tree_more_aware = exp_tree_more_aware.model_fairness(protected=race_test, privileged="Caucasian")
  
 pd.concat([exp.model_performance().result for exp in [exp_tree, exp_tree_aware, exp_tree_more_aware]])

我们可以看到,我们的最终模型在应用了消除差异影响和LFR之后,可以说比我们最初的基线模型有更好的模型性能。


图5.我们最终的偏见意识模型的准确率、f1和精确度都有所提高,并且在召回率和AUC方面只出现了轻微的下降。这很好,因为它表明通过减少偏差,我们的ML模型同时在准确率等更 "经典 "的指标上表现更好。这是双赢的。


我们还想检查一下我们的累积奇偶性损失,以确保我们正朝着正确的方向前进。

 mf_tree.plot(objects=[mf_tree_aware, mf_tree_more_aware], type='stacked')

当我们检查我们的绘图时,我们可以看到我们的公平性指标也在下降!这是一个全面的好消息。这是一个全面的好消息。我们的模型在性能上没有受到基线模型的影响,我们的模型也表现得更加公平。


图6.我们的最终偏见意识模型,有差异影响消除和LFR,是目前最公平的模型。请再次记住,较小的偏差意味着较少的偏差,这对我们来说通常是更好的。在对我们的数据做了一些相当简单的转换之后,我们肯定在这里做了一些正确的举动,看到了偏差的下降和模型性能的提高


让我们最后看一下我们的dalex模型的公平性检查。回顾一下我们的无意识模型,我们有7个数字超出了我们的范围(0.8, 1.25),我们在4/5个指标中检测到了偏差。

 
 mf_tree_more_aware.fairness_check()  # 4 / 15 numbers out of the range of (08, 1.25)
 Bias detected in 

现在我们只有3个指标超出我们的范围,而不是之前的7个,而且现在只在3个指标中检测到偏见,而不是4个。 总而言之,我们的工作似乎稍微改善了我们的模型性能,同时减少了我们的累积奇偶性损失。

我们在这个数据上做了很多工作,但我们是否可以放心地将这个模型按原样提交,以便被认为是一个准确和公平的再犯预测器?**绝对不行!我们在这个系列文章中的工作只能说是勉为其难。**我们在这一系列文章中的工作几乎没有触及偏见和公平意识的表面,只关注了一些预处理技术。我们甚至没有开始深入讨论我们可以利用的其他形式的偏见缓解。

总结

  • 模型的公平性与模型的性能一样重要,甚至有时比其更重要
  • 在我们的模型中,有多种定义公平性的方法,每种方法都有优点和缺点
    • 考虑到我们数据中的相关因素,单纯的不了解一个模型通常是不够的。
    • 统计平价和平均赔率是公平性的两个常见定义,但有时会相互矛盾。
  • 我们可以在训练模型之前、期间和之后减轻偏见
  • 消除差异影响和学习公平代表有助于我们的模型变得更加公平,也导致了模型性能的小幅提升。
  • 仅仅是预处理还不足以减轻偏见。我们还必须在处理中和处理后的方法上下功夫,以进一步减少我们的偏差

本系列文章就到此为止。