引言
一个模型在整体测试集上准确率 95%,却在某个少数群体上召回率只有 60%——这类「平均很好、局部很糟」的模型,正在信贷审批、简历筛选、医疗分诊、内容审核中造成系统性伤害。问题的根源往往不在算法,而在数据本身就承载了历史偏见:如果过去某个群体获得贷款的比例偏低,模型会把这个模式学成「规律」,再用它拒绝未来的申请人。
公平性不是道德口号,而是一组可度量、可优化、有明确取舍的工程约束。本文从偏差来源讲起,梳理公平性定义与它们之间的数学冲突,给出检测与三类缓解手段的实战方法。
前置:评估指标与分组评估见 /ml-model-evaluation/;偏差来源的归因分析见 /ml-model-interpretability/。
目录
- 1. 偏差从哪里来
- 2. 公平性定义与不可能定理
- 3. 公平性度量
- 4. 检测:Fairlearn 与分组评估
- 5. 缓解一:预处理
- 6. 缓解二:处理中约束
- 7. 缓解三:后处理
- 8. 代理变量与特征选择
- 9. 法规与治理闭环
- 10. 常见坑与检查清单
- 11. 总结
1. 偏差从哪里来
治理偏差的第一步是分清它从哪来,因为不同来源要用不同手段。
| 来源 | 机制 | 例子 |
|---|---|---|
| 历史偏差 | 数据反映既有不公 | 过去招聘歧视女性,历史数据里女性录用率低 |
| 表示偏差 | 某群体样本过少 | 人脸数据里深肤色占比不足 5% |
| 测量偏差 | 标签本身有偏 | 用「被逮捕」当「犯罪」的代理,警务密度影响标签 |
| 聚合偏差 | 用群体模型套个体 | 用全国模型预测某地区,忽略地区差异 |
| 评估偏差 | 测试集不具代表性 | 测试集也缺少数群体,问题被掩盖 |
| 部署偏差 | 使用场景与训练不符 | 模型被用到分布完全不同的新市场 |
表示偏差与测量偏差最隐蔽:前者让模型对少数群体欠拟合,后者让模型学到一个错误的「真值」。测量偏差尤其危险——即使模型完美拟合了标签,也可能在拟合一个错误的代理。
2. 公平性定义与不可能定理
「公平」没有唯一定义。常见的三类:
2.1 人口均等(Demographic Parity)
要求各群体获得正例预测的比例相同:
P(Ŷ = 1 | A = 0) = P(Ŷ = 1 | A = 1)
A 是敏感属性(如性别、种族)。它只看预测结果分布,不看真实标签,实现简单但可能牺牲准确性——若某群体真实违约率更高,强行拉平预测率会让决策不公。
2.2 机会均等(Equal Opportunity)
要求在各群体中,真实正例被正确预测的比例相同(即各组 TPR 相等):
P(Ŷ = 1 | Y = 1, A = 0) = P(Ŷ = 1 | Y = 1, A = 1)
它关注「真正应该通过的人有没有被公平地通过」,比人口均等更贴合业务。
2.3 均等化几率(Equalized Odds)
机会均等的加强版,同时要求 TPR 与 FPR 都相等:
P(Ŷ = 1 | Y = y, A = 0) = P(Ŷ = 1 | Y = y, A = 1), y ∈ {0, 1}
2.4 不可能定理
除非满足极端条件(各组基率相同、或模型完美预测),人口均等、均等化几率、校准三者不可同时满足。这不是工程能力问题,而是数学上的不可能(Kleinberg 等,2016)。
基率不同时:
预测均等 ⊥ 均等化几率 ⊥ 校准
必须根据业务选择其中一个作为主目标
因此公平性工程的第一步不是「选最好的算法」,而是和业务方一起决定用哪个定义、可接受多大精度损失。
3. 公平性度量
把定义落成可计算的指标。
| 指标 | 定义 | 直觉 |
|---|---|---|
| 人口均等差 | max - min 组间正例率 | 越小越公平 |
| 差异影响率 | 劣势组正例率 / 优势组正例率 | < 0.8 触发「80% 规则」 |
| 机会均等差 | 组间 TPR 差 | 越小越公平 |
| 均等化几率差 | 组间 TPR 与 FPR 的最大差 | 综合 |
| 预测均等差 | 组间 FPR 差 | 关注误伤 |
| 校准差 | 组间各分数段实际正例率差 | 关注概率可靠性 |
差异影响率(Disparate Impact) 来自美国就业法:若劣势组通过率不足优势组的 80%,即视为存在歧视性影响。它是合规审查中最常被引用的量化门槛。
import numpy as np
def disparate_impact(y_pred, group):
rates = {g: y_pred[group == g].mean() for g in np.unique(group)}
return min(rates.values()) / max(rates.values()), rates
y_pred = np.array([1, 0, 1, 1, 0, 1, 0, 0])
group = np.array(["A", "A", "A", "A", "B", "B", "B", "B"])
di, rates = disparate_impact(y_pred, group)
print("各组通过率:", {k: round(v, 3) for k, v in rates.items()})
print("差异影响率:", round(di, 3), "(<0.8 需审查)")
4. 检测:Fairlearn 与分组评估
4.1 分组评估(Group Metrics)
公平性检测的基础是按敏感属性分组,分别计算指标。很多问题只有在分组视图下才暴露。
import numpy as np
import pandas as pd
from sklearn.metrics import confusion_matrix
def group_metrics(y_true, y_pred, group):
rows = []
for g in np.unique(group):
m = group == g
tn, fp, fn, tp = confusion_matrix(y_true[m], y_pred[m]).ravel()
rows.append({
"group": g, "n": m.sum(),
"TPR": tp / (tp + fn) if (tp + fn) else 0,
"FPR": fp / (fp + tn) if (fp + tn) else 0,
"precision": tp / (tp + fp) if (tp + fp) else 0,
})
return pd.DataFrame(rows)
y_true = np.array([1, 1, 0, 0, 1, 0, 1, 0])
y_pred = np.array([1, 0, 0, 0, 1, 1, 1, 0])
group = np.array(["A", "A", "A", "A", "B", "B", "B", "B"])
print(group_metrics(y_true, y_pred, group))
4.2 Fairlearn
Fairlearn 提供 MetricFrame 做分组指标、ThresholdOptimizer 做后处理去偏。
from fairlearn.metrics import MetricFrame, selection_rate, true_positive_rate
from sklearn.metrics import accuracy_score
mf = MetricFrame(
metrics={"accuracy": accuracy_score,
"selection_rate": selection_rate,
"tpr": true_positive_rate},
y_true=y_true, y_pred=y_pred, sensitive_features=group,
)
print("总体:", mf.overall)
print("分组:\n", mf.by_group)
print("组间差:", mf.difference())
difference() 直接给出组间最大差,是快速判断公平性的第一指标。
5. 缓解一:预处理
在训练前改数据,让数据「看起来更公平」。
5.1 重加权(Reweighting)
给样本加权,使各群体在标签上的加权分布对齐。核心是计算每个 (group, label) 组合的权重:
import numpy as np
def compute_weights(y, group):
"""使各群体在标签上的权重和相等"""
w = np.ones(len(y))
for g in np.unique(group):
for label in np.unique(y):
mask = (group == g) & (y == label)
p_group = (group == g).mean()
p_label = (y == label).mean()
p_joint = mask.mean()
if p_joint > 0:
w[mask] = (p_group * p_label) / p_joint
return w
y = np.array([1, 1, 0, 0, 1, 0, 1, 0])
group = np.array(["A", "A", "A", "A", "B", "B", "B", "B"])
print("样本权重:", compute_weights(y, group).round(3))
把权重传给 fit(sample_weight=w) 即可。重加权简单、可解释,但不改变特征本身,效果有限。
5.2 数据增强与重采样
对表示偏差,直接补采少数群体样本,或用 SMOTE 类方法合成少数群体样本。对图像任务,做群体平衡的数据增强(如不同肤色、年龄的合成)。
5.3 移除敏感特征与其代理
最直接的做法是训练时删掉敏感属性。但代理变量(如邮编、姓名、浏览行为)会间接携带敏感信息,删了敏感列仍可能不公平。
6. 缓解二:处理中约束
在模型训练过程中施加公平性约束。
6.1 正则化惩罚
在损失里加一项「组间预测差异」,联合优化:
L = L_task + λ · L_fairness
L_fairness 可以是组间正例率差的平方、TPR 差的平方等。λ 控制公平与精度的权衡。
import numpy as np
from sklearn.linear_model import LogisticRegression
def fairness_penalty(scores, group):
"""人口均等惩罚:各组平均预测分数之差的平方"""
means = [scores[group == g].mean() for g in np.unique(group)]
return np.var(means)
# 示意:λ 越大,公平性越强、精度越低
for lam in [0.0, 0.1, 1.0]:
print(f"lambda={lam}: 惩罚示例 {round(fairness_penalty(np.array([0.9,0.8,0.3,0.2]), np.array(['A','A','B','B'])), 4)}")
6.2 对抗去偏(Adversarial Debiasing)
加一个「对手」网络,试图从模型输出预测敏感属性。主模型训练目标是让对手猜不出敏感属性(最小化对手准确率),同时保持任务精度。这本质上是让表示与敏感属性独立。
主模型 → 预测 Ŷ → 任务损失
↘ 中间表示 Z → 对手网络 → 敏感属性 A
↺ 主模型学习让对手失效(梯度反转)
对抗去偏效果好,但训练不稳定、超参多,需要仔细调参。
6.3 约束优化
把公平性写成显式约束,用带约束的优化求解(如 Fairlearn 的 ExponentiatedGradient):
from fairlearn.reductions import ExponentiatedGradient, DemographicParity
from sklearn.linear_model import LogisticRegression
mitigator = ExponentiatedGradient(
estimator=LogisticRegression(max_iter=1000),
constraints=DemographicParity(),
)
# mitigator.fit(X, y, sensitive_features=group)
7. 缓解三:后处理
不动模型,只改预测结果。优点是与模型无关、可插拔,适合已上线的黑盒模型。
7.1 分组阈值调整
对每个群体用不同阈值,使各组的 TPR(或正例率)对齐。这是 ThresholdOptimizer 的思路。
import numpy as np
def group_thresholds(scores, group, target_rate):
"""为每个群体找阈值,使正例率达到目标"""
th = {}
for g in np.unique(group):
s = np.sort(scores[group == g])
idx = int((1 - target_rate) * len(s))
th[g] = s[min(idx, len(s) - 1)]
return th
scores = np.array([0.9, 0.7, 0.4, 0.3, 0.8, 0.5, 0.2, 0.1])
group = np.array(["A", "A", "A", "A", "B", "B", "B", "B"])
print("分组阈值:", {k: round(v, 3) for k, v in group_thresholds(scores, group, 0.5).items()})
from fairlearn.postprocessing import ThresholdOptimizer
from sklearn.linear_model import LogisticRegression
base = LogisticRegression(max_iter=1000)
# base.fit(X_train, y_train)
post = ThresholdOptimizer(estimator=base, constraints="equalized_odds",
predict_method="predict_proba")
# post.fit(X_train, y_train, sensitive_features=group_train)
7.2 三类手段对比
| 手段 | 时机 | 优点 | 缺点 |
|---|---|---|---|
| 预处理 | 训练前 | 简单、可解释 | 效果有限、改动数据 |
| 处理中 | 训练中 | 效果最好、可精细控制 | 复杂、需重训 |
| 后处理 | 预测后 | 与模型无关、快速上线 | 只改结果不改模型、可能损精度 |
8. 代理变量与特征选择
公平性治理绕不开一个问题:敏感属性可能被其他特征间接编码。检测代理变量的方法:
- 相关性/互信息:计算每个特征与敏感属性的互信息,高的可能是代理。
- 可预测性:用特征去预测敏感属性,若准确率远高于基率,说明存在代理。
- 消融测试:逐个移除可疑特征,看公平性指标是否改善。
from sklearn.feature_selection import mutual_info_classif
import numpy as np
X = np.random.default_rng(0).normal(size=(200, 4))
sensitive = (X[:, 0] + np.random.default_rng(1).normal(0, 0.1, 200) > 0).astype(int)
mi = mutual_info_classif(X, sensitive, random_state=0)
print("各特征与敏感属性的互信息:", mi.round(3)) # 特征 0 最高,是强代理
但要注意:移除代理变量可能降低模型效用,也可能只是把偏差转移到别的特征。正确做法是识别 + 显式约束,而非盲目删列。可解释性工具(如 SHAP)能帮助定位哪些特征在推动不公平决策,方法见 /ml-model-interpretability/。
9. 法规与治理闭环
9.1 主要法规
| 法规 | 关键要求 |
|---|---|
| EU AI Act | 高风险系统须做偏差评估、保留技术文档、人监督 |
| GDPR 第 22 条 | 自动化决策须可解释、可申诉 |
| 美国 80% 规则 | 差异影响率 < 0.8 触发审查 |
| 中国算法推荐管理规定 | 不得诱导沉迷、须提供关闭选项 |
9.2 治理闭环
公平性不是一次性检查,而是持续闭环:
定义公平目标 → 分组评估基线 → 选择缓解手段 → 上线分组监控
↑ │
└────────── 定期复审 + 申诉反馈 ←──────────────┘
上线后要按群体持续监控关键指标(TPR、FPR、拒绝率),一旦组间差异超出阈值即告警。监控基础设施见 /ml-model-monitoring-drift/。同时应产出模型卡(Model Card):记录模型用途、训练数据构成、分组性能、已知限制与适用边界。
10. 常见坑与检查清单
| 现象 | 根因 | 处理 |
|---|---|---|
| 删掉敏感列后仍不公平 | 代理变量 | 检测互信息,显式约束 |
| 各定义冲突无法兼顾 | 不可能定理 | 与业务确认主目标 |
| 公平性改善但精度大跌 | 约束过强 | 调 λ,做精度-公平权衡曲线 |
| 线下公平线上不公平 | 线上群体分布不同 | 上线分组监控 |
| 测试集无群体标签 | 评估偏差 | 补采并标注敏感属性 |
| 后处理阈值泄露隐私 | 阈值依赖群体 | 评估合规性,必要时改处理中方案 |
上线前检查清单:是否明确敏感属性与公平定义;是否做过分组评估;差异影响率是否 > 0.8;是否有精度-公平权衡记录;是否检查过代理变量;是否有上线后分组监控;是否产出模型卡与申诉渠道。
11. 总结
11.1 核心要点
- 偏差来自数据与流程,不只是算法;历史偏差与测量偏差最难察觉。
- 公平性有多种定义,不可能定理说明必须取舍,先定主目标。
- 差异影响率 < 0.8 是最常被引用的合规红线。
- 缓解分预处理、处理中、后处理三类,处理中效果最好、后处理最易上线。
- 代理变量让「删除敏感列」失效,需检测而非盲删。
- 公平性是持续闭环,必须上线后分组监控。
11.2 与建模流程的衔接
公平性评估建立在分组评估之上(见 /ml-model-evaluation/),归因与持续保障则依赖可解释性工具与监控体系(见 /ml-model-interpretability/)。若关注算法与工程实现的更深讨论,可延伸阅读 AI/ML 专题 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。