解释公平性的定量指标

这篇实践性文章将可解释人工智能方法与公平性度量联系起来,并展示了现代可解释性方法如何增强定量公平性指标的实用性。通过使用 SHAP(一种流行的可解释人工智能工具),我们可以分解公平性度量,并将观察到的任何差异归因于模型的每个输入特征。解释这些定量公平性指标可以减少将它们作为不透明的公平性标准的令人担忧的趋势,并促进将其作为工具的知情使用,以理解模型行为在不同群体之间的差异。

定量公平性指标旨在为机器学习中公平性的定义带来数学上的精确性 [1]。然而,公平性的定义深深植根于人类的伦理原则,因此也植根于价值判断,而价值判断通常严重依赖于机器学习模型的使用背景。这种对价值判断的实际依赖性在定量公平性度量的数学中表现为一组权衡,这些权衡有时存在于相互不兼容的公平性定义之间 [2]。由于公平性依赖于上下文相关的价值判断,因此将定量公平性指标视为不透明的黑盒公平性度量是危险的,因为这样做可能会掩盖重要的价值判断选择。

SHAP 如何用于解释模型公平性的各种度量

本文不是关于如何选择“正确”的模型公平性度量,而是关于解释您已选择的任何指标。哪种公平性指标最合适取决于您的具体情况,例如适用的法律、机器学习模型的输出如何影响人们,以及您对各种结果以及由此产生的权衡的价值判断。这里我们将使用经典的人口统计均等性指标,因为它简单且与法律上的差别影响概念密切相关。同样的分析也可以应用于其他指标,例如 决策理论成本均等赔率均等机会均等服务质量。人口统计均等性指出,机器学习模型的输出在两个或多个群体之间应该是相等的。人口统计均等性差异衡量的是模型结果在两组样本之间存在多大的差异。

由于 SHAP 将模型输出分解为与原始模型输出单位相同的特征归因,我们可以首先使用 SHAP 将模型输出分解到每个输入特征中,然后使用该特征的 SHAP 值分别计算每个输入特征的人口统计均等性差异(或任何其他公平性指标)。 因为 SHAP 值加起来等于模型的输出,所以 SHAP 值的人口统计均等性差异之和也等于整个模型的人口统计均等性差异。

SHAP 公平性解释在各种模拟场景中的表现形式

为了帮助我们探索解释定量公平性指标的潜在用途,我们考虑一个基于信贷承销的简单模拟场景。在我们的模拟中,有四个潜在因素驱动贷款的违约风险:收入稳定性、收入金额、消费克制和一致性。这些潜在因素是不可观察的,但它们以不同的方式影响四个不同的可观察特征:工作经历、报告收入、信用查询和延迟付款。使用此模拟,我们生成随机样本,然后训练一个非线性 XGBoost 分类器来预测违约概率。相同的过程也适用于 SHAP 支持的任何其他模型类型,只需记住更复杂模型的解释会隐藏更多模型细节。

通过在完全指定的模拟中引入特定于性别的报告错误,我们可以观察到由这些错误引起的偏差如何被我们选择的公平性指标捕获。在我们的模拟案例中,真实标签(将拖欠贷款)在统计上独立于性别(我们用于检查公平性的敏感类别)。因此,男性和女性之间的任何差异都意味着由于特征测量错误、标签错误或模型错误,一个或两个群体模型不正确。如果您预测的真实标签(可能与您有权访问的训练标签不同)与您正在考虑的敏感特征在统计上不独立,那么即使是完美且没有错误的模型也会无法满足人口统计均等性。在这些情况下,公平性解释可以帮助您确定哪些人口统计差异来源是有效的。

[1]:
# here we define a function that we can call to execute our simulation under
# a variety of different alternative scenarios
import numpy as np
import pandas as pd

import shap

%config InlineBackend.figure_format = 'retina'


def run_credit_experiment(
    N,
    job_history_sex_impact=0,
    reported_income_sex_impact=0,
    income_sex_impact=0,
    late_payments_sex_impact=0,
    default_rate_sex_impact=0,
    include_brandx_purchase_score=False,
    include_sex=False,
):
    np.random.seed(0)
    sex = np.random.randint(0, 2, N) == 1  # randomly half men and half women

    # four hypothetical causal factors influence customer quality
    # they are all scaled to the same units between 0-1
    income_stability = np.random.rand(N)
    income_amount = np.random.rand(N)
    if income_sex_impact > 0:
        income_amount -= income_sex_impact / 90000 * sex * np.random.rand(N)
        income_amount -= income_amount.min()
        income_amount /= income_amount.max()
    spending_restraint = np.random.rand(N)
    consistency = np.random.rand(N)

    # intuitively this product says that high customer quality comes from simultaneously
    # being strong in all factors
    customer_quality = income_stability * income_amount * spending_restraint * consistency

    # job history is a random function of the underlying income stability feature
    job_history = np.maximum(
        10 * income_stability + 2 * np.random.rand(N) - job_history_sex_impact * sex * np.random.rand(N),
        0,
    )

    # reported income is a random function of the underlying income amount feature
    reported_income = np.maximum(
        10000
        + 90000 * income_amount
        + np.random.randn(N) * 10000
        - reported_income_sex_impact * sex * np.random.rand(N),
        0,
    )

    # credit inquiries is a random function of the underlying spending restraint and income amount features
    credit_inquiries = np.round(6 * np.maximum(-spending_restraint + income_amount, 0)) + np.round(
        np.random.rand(N) > 0.1
    )

    # credit inquiries is a random function of the underlying consistency and income stability features
    late_payments = np.maximum(
        np.round(3 * np.maximum((1 - consistency) + 0.2 * (1 - income_stability), 0))
        + np.round(np.random.rand(N) > 0.1)
        - np.round(late_payments_sex_impact * sex * np.random.rand(N)),
        0,
    )

    # bundle everything into a data frame and define the labels based on the default rate and customer quality
    X = pd.DataFrame(
        {
            "Job history": job_history,
            "Reported income": reported_income,
            "Credit inquiries": credit_inquiries,
            "Late payments": late_payments,
        }
    )
    default_rate = 0.40 + sex * default_rate_sex_impact
    y = customer_quality < np.percentile(customer_quality, default_rate * 100)

    if include_brandx_purchase_score:
        brandx_purchase_score = sex + 0.8 * np.random.randn(N)
        X["Brand X purchase score"] = brandx_purchase_score

    if include_sex:
        X["Sex"] = sex + 0

    # build model
    import xgboost

    model = xgboost.XGBClassifier(max_depth=1, n_estimators=500, subsample=0.5, learning_rate=0.05)
    model.fit(X, y)

    # build explanation
    import shap

    explainer = shap.TreeExplainer(model, shap.sample(X, 100))
    shap_values = explainer.shap_values(X)

    return shap_values, sex, X, explainer.expected_value

场景 A:无报告错误

我们的第一个实验是一个简单的基线检查,我们避免引入任何特定于性别的报告错误。虽然我们可以使用任何模型输出来衡量人口统计均等性,但我们使用来自二元 XGBoost 分类器的连续对数几率得分。正如预期的那样,此基线实验在男性和女性的信用评分之间没有产生显着的人口统计均等性差异。我们可以通过绘制女性和男性的平均信用评分之间的差异作为条形图,并注意到零接近误差范围来看到这一点(请注意,负值表示女性的平均预测风险低于男性,正值表示女性的平均预测风险高于男性)。

[2]:
N = 10000
shap_values_A, sex_A, X_A, ev_A = run_credit_experiment(N)
model_outputs_A = ev_A + shap_values_A.sum(1)
glabel = "Demographic parity difference\nof model output for women vs. men"
xmin = -0.8
xmax = 0.8
shap.group_difference_plot(shap_values_A.sum(1), sex_A, xmin=xmin, xmax=xmax, xlabel=glabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_3_0.png

现在我们可以使用 SHAP 将模型输出分解到模型的每个输入特征中,然后计算归因于每个特征的组件的人口统计均等性差异。如上所述,由于 SHAP 值加起来等于模型的输出,因此每个特征的 SHAP 值的人口统计均等性差异之和也等于整个模型的人口统计均等性差异。这意味着下面条形图的总和等于上面的条形图(我们基线场景模型的人口统计均等性差异)。

[3]:
slabel = "Demographic parity difference\nof SHAP values for women vs. men"
shap.group_difference_plot(shap_values_A, sex_A, X_A.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_5_0.png

场景 B:女性收入的低报偏差

在我们的基线场景中,我们设计了一个模拟,其中性别对模型使用的任何特征或标签都没有影响。在场景 B 中,我们向模拟中引入了女性收入的低报偏差。这里的重点不是女性收入在现实世界中被低报有多么真实,而是我们如何识别已引入特定于性别的偏差并了解其来源。通过绘制女性和男性之间平均模型输出(违约风险)的差异,我们可以看到收入低报偏差已造成显着的人口统计均等性差异,其中女性现在比男性具有更高的违约风险。

[4]:
shap_values_B, sex_B, X_B, ev_B = run_credit_experiment(N, reported_income_sex_impact=30000)
model_outputs_B = ev_B + shap_values_B.sum(1)
shap.group_difference_plot(shap_values_B.sum(1), sex_B, xmin=xmin, xmax=xmax, xlabel=glabel)
 95%|=================== | 9542/10000 [00:11<00:00]
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_7_1.png

如果这是一个实际应用,这种人口统计均等性差异可能会触发对模型的深入分析,以确定可能导致差异的原因。虽然仅凭单个人口统计均等性差异值进行调查具有挑战性,但基于 SHAP 的按特征人口统计均等性分解使其变得容易得多。使用 SHAP,我们可以看到报告的收入特征存在显着偏差,这不成比例地增加了女性相对于男性的风险。这使我们能够快速识别哪个特征具有导致我们的模型违反人口统计均等性的报告偏差。

[5]:
shap.group_difference_plot(shap_values_B, sex_B, X_B.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_9_0.png

重要的是在此指出我们的假设如何影响 SHAP 公平性解释的解释。在我们的模拟场景中,我们知道女性实际上与男性具有相同的收入概况,因此当我们看到报告的收入特征对于女性而言比男性偏低时,我们知道这来自报告的收入特征中的测量误差偏差。解决此问题的最佳方法是弄清楚如何消除报告的收入特征中的测量误差偏差。这样做将创建一个更准确的模型,并且人口统计差异也更小。但是,如果我们假设女性实际上比男性挣的钱少(而不仅仅是报告错误),那么我们不能只是“修复”报告的收入特征。相反,我们必须仔细考虑如何最好地解释两个受保护群体之间违约风险的实际差异。仅使用 SHAP 公平性解释无法确定这两种情况中的哪一种正在发生,因为在这两种情况下,报告的收入特征都将对观察到的男性和女性预测风险之间的差异负责。

场景 C:女性延迟付款的低报偏差

为了验证 SHAP 人口统计均等性解释可以正确检测差异,而与效果方向或源特征无关,我们重复之前的实验,但不是收入的低报偏差,而是引入女性延迟付款率的低报偏差。这导致模型输出的人口统计均等性差异显着,现在女性的平均违约风险低于男性。

[6]:
shap_values_C, sex_C, X_C, ev_C = run_credit_experiment(N, late_payments_sex_impact=2)
model_outputs_C = ev_C + shap_values_C.sum(1)
shap.group_difference_plot(shap_values_C.sum(1), sex_C, xmin=xmin, xmax=xmax, xlabel=glabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_12_0.png

正如我们所希望的那样,SHAP 解释正确地突出了延迟付款特征是模型人口统计均等性差异的原因,以及效果的方向。

[7]:
shap.group_difference_plot(shap_values_C, sex_C, X_C.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_14_0.png

场景 D:女性违约率的低报偏差

上述实验侧重于为特定输入特征引入报告错误。接下来,我们考虑当通过女性违约率的低报偏差(这意味着女性的违约报告可能性低于男性)在训练标签上引入报告错误时会发生什么。有趣的是,对于我们的模拟场景,这不会导致模型输出中出现显着的人口统计均等性差异。

[8]:
shap_values_D, sex_D, X_D, ev_D = run_credit_experiment(N, default_rate_sex_impact=-0.1)  # 20% change
model_outputs_D = ev_D + shap_values_D.sum(1)
shap.group_difference_plot(shap_values_D.sum(1), sex_D, xmin=xmin, xmax=xmax, xlabel=glabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_16_0.png

我们在 SHAP 解释中也没有看到任何人口统计均等性差异的证据。

[9]:
shap.group_difference_plot(shap_values_D, sex_D, X_D.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_18_0.png

场景 E:女性违约率的低报偏差,版本 2

当我们引入女性违约率的低报偏差时,没有引起人口统计均等性差异,这起初可能令人惊讶。这是因为我们模拟中的四个特征都与性别没有显着相关性,因此它们都不能有效地用于建模我们引入训练标签的偏差。如果我们现在改为向模型提供一个与性别相关的新特征(品牌 X 购买评分),那么我们会看到人口统计均等性差异出现,因为该特征被模型用来捕获训练标签中特定于性别的偏差。

[10]:
shap_values_E, sex_E, X_E, ev_E = run_credit_experiment(
    N, default_rate_sex_impact=-0.1, include_brandx_purchase_score=True
)
model_outputs_E = ev_E + shap_values_E.sum(1)
shap.group_difference_plot(shap_values_E.sum(1), sex_E, xmin=xmin, xmax=xmax, xlabel=glabel)
 98%|===================| 9794/10000 [00:11<00:00]
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_20_1.png

当我们用 SHAP 解释人口统计均等性差异时,我们看到,正如预期的那样,品牌 X 购买评分特征驱动了差异。在这种情况下,这不是因为我们在衡量品牌 X 购买评分特征的方式上存在偏差,而是因为我们的训练标签中存在偏差,而这种偏差会被任何与性别充分相关的输入特征捕获。

[11]:
shap.group_difference_plot(shap_values_E, sex_E, X_E.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_22_0.png

场景 F:区分多种低报偏差

当报告偏差的成因单一时,模型输出的经典人口统计均等性测试和人口统计均等性测试的 SHAP 解释都捕获了相同的偏差效应(尽管 SHAP 解释通常具有更高的统计显着性,因为它隔离了导致偏差的特征)。但是,当数据集中出现多个偏差原因时会发生什么?在本实验中,我们引入了两种此类偏差,即女性违约率的低报和女性工作经历的低报。这些偏差往往会在全局平均值中相互抵消,因此对模型输出进行人口统计均等性测试显示没有可测量的差异。

[12]:
shap_values_F, sex_F, X_F, ev_F = run_credit_experiment(
    N,
    default_rate_sex_impact=-0.1,
    include_brandx_purchase_score=True,
    job_history_sex_impact=2,
)
model_outputs_F = ev_F + shap_values_F.sum(1)
shap.group_difference_plot(shap_values_F.sum(1), sex_F, xmin=xmin, xmax=xmax, xlabel=glabel)
100%|===================| 9996/10000 [00:11<00:00]
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_24_1.png

然而,如果我们查看人口统计均等性差异的 SHAP 解释,我们会清楚地看到两种(抵消)偏差。

[13]:
shap.group_difference_plot(shap_values_F, sex_F, X_F.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_26_0.png

识别多种潜在的抵消偏差效应可能很重要,因为虽然平均而言,对男性或女性没有不同的影响,但对个人却有不同的影响。例如,在此模拟中,由于工作经历报告中存在的偏差,未在品牌 X 购物的女性将获得低于应有水平的信用评分。

引入受保护特征如何帮助区分标签偏差和特征偏差

在场景 F 中,我们能够区分出两种不同的偏差形式,一种来自工作经历低报,另一种来自违约率低报。然而,来自违约率低报的偏差并未归因于违约率标签,而是归因于恰好与性别相关的品牌 X 购买评分特征。这仍然使我们对人口统计均等性差异的真正来源感到不确定,因为归因于输入特征的任何差异都可能是由于该特征的问题,或由于训练标签的问题。

事实证明,在这种情况下,我们可以通过直接将性别作为变量引入模型来帮助区分标签偏差和特征偏差。将性别作为输入特征引入的目的是使标签偏差完全落在性别特征上,而特征偏差保持不变。因此,然后我们可以通过将上面场景 F 的结果与下面我们的新场景 G 进行比较来区分标签偏差和特征偏差。这当然会产生比以前更强的人口统计均等性差异,但这没关系,因为我们在这里的目标不是偏差缓解,而是偏差理解。

[14]:
shap_values_G, sex_G, X_G, ev_G = run_credit_experiment(
    N,
    default_rate_sex_impact=-0.1,
    include_brandx_purchase_score=True,
    job_history_sex_impact=2,
    include_sex=True,
)
model_outputs_G = ev_G + shap_values_G.sum(1)
shap.group_difference_plot(shap_values_G.sum(1), sex_G, xmin=xmin, xmax=xmax, xlabel=glabel)
 97%|=================== | 9720/10000 [00:11<00:00]
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_29_1.png

场景 G 的 SHAP 解释表明,场景 F 中曾经附加到品牌 X 购买评分特征的所有人口统计均等性差异现在已转移到性别特征,而场景 F 中附加到工作经历特征的人口统计均等性差异均未移动。这可以解释为,场景 F 中归因于品牌 X 购买评分的所有差异都是由于标签偏差造成的,而场景 F 中归因于工作经历的所有差异都是由于特征偏差造成的。

[15]:
shap.group_difference_plot(shap_values_G, sex_G, X_G.columns, xmin=xmin, xmax=xmax, xlabel=slabel)
../../_images/example_notebooks_overviews_Explaining_quantitative_measures_of_fairness_31_0.png

结论

公平性是一个复杂的主题,清晰的数学答案几乎总是带有警告,并且取决于伦理价值判断。这意味着尤其重要的是不要仅仅将公平性指标用作黑盒,而是要试图理解这些指标是如何计算的,以及模型和训练数据的哪些方面正在影响您观察到的任何差异。当指标由仅影响少数特征的测量偏差驱动时,使用 SHAP 分解定量公平性指标可以降低其不透明性。我希望您发现我们在此处演示的公平性解释可以帮助您更好地应对公平性评估中固有的潜在价值判断,从而帮助降低在现实世界环境中使用公平性指标时可能产生的意外后果的风险。