模型公平性与偏差检测:AI伦理实战指南
引言
当机器学习模型被用于贷款审批、简历筛选、司法量刑等决策场景时,模型可能对特定群体(如不同性别、种族、年龄)产生系统性偏见。2018年Amazon的AI招聘工具被曝歧视女性,2019年Apple Card被指控给女性更低的信用额度------这些案例警示我们:模型的高准确率并不等于公平。
模型公平性(Fairness)不是一个纯技术问题,但技术手段是实现公平的基础。本文将系统介绍公平性的数学定义、偏差检测方法,以及在实际项目中可落地的去偏技术,全部附带可运行代码。
核心概念
公平性的多种定义
公平性没有唯一的数学定义,不同的定义之间甚至存在不可兼容性定理(Chouldechova, 2017)。以下是几种核心定义:
1. 统计均等(Statistical Parity / Demographic Parity)
P ( Y ^ = 1 ∣ A = a ) = P ( Y ^ = 1 ∣ A = b ) , ∀ a , b ∈ A P(\hat{Y}=1 | A=a) = P(\hat{Y}=1 | A=b), \quad \forall a, b \in \mathcal{A} P(Y^=1∣A=a)=P(Y^=1∣A=b),∀a,b∈A
即不同群体获得正预测的概率应该相等。
2. 机会均等(Equalized Odds)
P ( Y ^ = 1 ∣ A = a , Y = y ) = P ( Y ^ = 1 ∣ A = b , Y = y ) , ∀ y ∈ { 0 , 1 } P(\hat{Y}=1 | A=a, Y=y) = P(\hat{Y}=1 | A=b, Y=y), \quad \forall y \in \{0,1\} P(Y^=1∣A=a,Y=y)=P(Y^=1∣A=b,Y=y),∀y∈{0,1}
即在真实标签相同的条件下,不同群体获得正预测的概率应该相等。
3. 预测均等(Predictive Parity)
P ( Y = 1 ∣ Y ^ = 1 , A = a ) = P ( Y = 1 ∣ Y ^ = 1 , A = b ) P(Y=1 | \hat{Y}=1, A=a) = P(Y=1 | \hat{Y}=1, A=b) P(Y=1∣Y^=1,A=a)=P(Y=1∣Y^=1,A=b)
即不同群体中,预测为正的样本里真正为正的比例应该相等。
公平性指标对比
| 指标 | 公式含义 | 适用场景 | 局限性 |
|---|---|---|---|
| 统计均等差 | P ( Y ^ = 1 ∣ A = 0 ) − P ( Y ^ = 1 ∣ A = 1 ) P(\hat{Y}=1|A=0) - P(\hat{Y}=1|A=1) P(Y^=1∣A=0)−P(Y^=1∣A=1) | 招聘、录取 | 忽略了真实标签 |
| 均等机会差 | T P R a − T P R b TPR_{a} - TPR_{b} TPRa−TPRb | 信贷审批 | 要求有标签数据 |
| 预测均等差 | P P V a − P P V b PPV_{a} - PPV_{b} PPVa−PPVb | 风险评估 | 可能牺牲整体精度 |
| 校准差 | P ( Y = 1 ∣ S ^ = s , A = a ) − P ( Y = 1 ∣ S ^ = s , A = b ) P(Y=1|\hat{S}=s, A=a) - P(Y=1|\hat{S}=s, A=b) P(Y=1∣S^=s,A=a)−P(Y=1∣S^=s,A=b) | 概率输出模型 | 每个分组需足够样本 |
实战代码
示例一:使用 AIF360 进行偏差检测与缓解
python
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
# 构造一个有偏差的贷款审批数据集
np.random.seed(42)
n_samples = 2000
# 敏感属性:性别 (0=男性, 1=女性)
gender = np.random.binomial(1, 0.4, n_samples)
# 收入特征(女性平均收入略低,反映社会结构性不平等)
income = np.random.normal(50 + gender * (-8), 15, n_samples).clip(15, 120)
# 信用评分
credit_score = np.random.normal(680 + gender * (-30), 60, n_samples).clip(300, 850)
# 历史违约次数
defaults = np.random.poisson(0.5 + gender * 0.3, n_samples)
# 审批结果(存在人为偏差:女性通过率被额外降低)
approval_prob = 1 / (1 + np.exp(-(0.02 * credit_score + 0.01 * income
- 0.5 * defaults - 5 - gender * 0.8)))
approved = np.random.binomial(1, approval_prob)
df = pd.DataFrame({
'gender': gender, 'income': income,
'credit_score': credit_score, 'defaults': defaults,
'approved': approved
})
print("=== 原始数据偏差统计 ===")
for g in [0, 1]:
subset = df[df['gender'] == g]
label = '男性' if g == 0 else '女性'
print(f"{label}: 样本数={len(subset)}, 审批通过率={subset['approved'].mean():.3f}")
# 训练模型
X = df[['gender', 'income', 'credit_score', 'defaults']]
y = df['approved']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# 计算公平性指标
def compute_fairness_metrics(y_true, y_pred, sensitive_attr):
"""计算公平性指标"""
groups = sensitive_attr.unique()
metrics = {}
for g in groups:
mask = sensitive_attr == g
metrics[f'group_{g}'] = {
'positive_rate': y_pred[mask].mean(),
'true_positive_rate': y_pred[mask & (y_true == 1)].mean() if (y_true[mask] == 1).sum() > 0 else 0,
'accuracy': accuracy_score(y_true[mask], y_pred[mask])
}
# 计算差异
stat_parity = abs(metrics['group_0']['positive_rate'] - metrics['group_1']['positive_rate'])
eq_opportunity = abs(metrics['group_0']['true_positive_rate'] - metrics['group_1']['true_positive_rate'])
return metrics, stat_parity, eq_opportunity
test_df = X_test.copy()
test_df['approved'] = y_test
metrics, sp, eo = compute_fairness_metrics(y_test.values, y_pred, test_df['gender'])
print("\n=== 模型公平性指标 ===")
for g, m in metrics.items():
print(f"{g}: 正预测率={m['positive_rate']:.3f}, TPR={m['true_positive_rate']:.3f}, 准确率={m['accuracy']:.3f}")
print(f"统计均等差: {sp:.3f}")
print(f"均等机会差: {eo:.3f}")
示例二:预处理去偏------重新加权法
python
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
def reweighing_debiasing(X_train, y_train, sensitive_col_idx=0):
"""重新加权法:调整训练样本权重以消除偏差"""
A = X_train[:, sensitive_col_idx] # 敏感属性
Y = y_train
n = len(Y)
weights = np.ones(n)
# 计算期望频率和观测频率
for a in np.unique(A):
for y in np.unique(Y):
# 期望频率(如果独立)
p_a = np.mean(A == a)
p_y = np.mean(Y == y)
expected = p_a * p_y
# 观测频率
observed = np.mean((A == a) & (Y == y))
if observed > 0:
weight = expected / observed
mask = (A == a) & (Y == y)
weights[mask] = weight
return weights
# 使用之前的贷款数据
np.random.seed(42)
n_samples = 2000
gender = np.random.binomial(1, 0.4, n_samples)
income = np.random.normal(50 + gender * (-8), 15, n_samples).clip(15, 120)
credit_score = np.random.normal(680 + gender * (-30), 60, n_samples).clip(300, 850)
defaults = np.random.poisson(0.5 + gender * 0.3, n_samples)
approval_prob = 1 / (1 + np.exp(-(0.02 * credit_score + 0.01 * income - 0.5 * defaults - 5 - gender * 0.8)))
approved = np.random.binomial(1, approval_prob)
X = np.column_stack([gender, income, credit_score, defaults])
y = approved
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 无去偏
model_unfair = LogisticRegression(max_iter=1000)
model_unfair.fit(X_train, y_train)
pred_unfair = model_unfair.predict(X_test)
# 使用重新加权
weights = reweighing_debiasing(X_train, y_train, sensitive_col_idx=0)
model_fair = LogisticRegression(max_iter=1000)
model_fair.fit(X_train, y_train, sample_weight=weights)
pred_fair = model_fair.predict(X_test)
# 对比结果
for name, pred in [("无去偏", pred_unfair), ("重新加权", pred_fair)]:
male_mask = X_test[:, 0] == 0
female_mask = X_test[:, 0] == 1
male_rate = pred[male_mask].mean()
female_rate = pred[female_mask].mean()
acc = accuracy_score(y_test, pred)
print(f"\n{name}:")
print(f" 准确率: {acc:.3f}")
print(f" 男性通过率: {male_rate:.3f}, 女性通过率: {female_rate:.3f}")
print(f" 统计均等差: {abs(male_rate - female_rate):.3f}")
示例三:后处理去偏------阈值调整
python
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
def threshold_adjustment(y_prob, sensitive_attr, y_true, constraint='equalized_odds'):
"""后处理阈值调整:为每个群体找到最优阈值"""
best_thresholds = {}
for group in np.unique(sensitive_attr):
mask = sensitive_attr == group
best_t = 0.5
best_score = float('inf')
for t in np.arange(0.1, 0.9, 0.01):
pred = (y_prob[mask] >= t).astype(int)
if constraint == 'equalized_odds':
# 优化目标:最小化 FPR 和 TPR 与总体的偏差
tpr = np.mean(pred[y_true[mask] == 1]) if (y_true[mask] == 1).sum() > 0 else 0
fpr = np.mean(pred[y_true[mask] == 0]) if (y_true[mask] == 0).sum() > 0 else 0
score = abs(tpr - 0.8) + abs(fpr - 0.2) # 目标值可根据需求调整
elif constraint == 'demographic_parityrandom.binomial(1, 0.45, n)
X = np.random.randn(n, 5)
X[:, 0] = gender # 敏感属性作为特征之一
y_prob_true = 1 / (1 + np.exp(-(X.sum(axis=1) + gender * (-1.5))))
y = np.random.binomial(1, y_prob_true)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_prob = model.predict_proba(X_test)[:, 1]
y_pred_default = (y_prob >= 0.5).astype(int)
# 阈值调整
y_pred_adj, thresholds = threshold_adjustment(y_prob, X_test[:, 0], y_test, 'equalized_odds')
print(f"各群体最优阈值: {thresholds}")
for name, pred in [("默认阈值", y_pred_default), ("调整阈值", y_pred_adj)]:
male = X_test[:, 0] == 0
female = X_test[:, 0] == 1
print(f"\n{name}:")
print(f" 男性正预测率: {pred[male].mean():.3f}, 女性正预测率: {pred[female].mean():.3f}")
print(f" 男性TPR: {pred[male & (y_test==1)].mean():.3f}, 女性TPR: {pred[female & (y_test==1)].mean():.3f}")
print(f" 总体准确率: {(pred == y_test).mean():.3f}")
示例四:对抗去偏训练
python
import numpy as np
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
class AdversarialDebiasing:
"""简化的对抗去偏:通过梯度反转削弱模型对敏感属性的预测能力"""
def __init__(self, n_epochs=50, lr=0.01, lambda_adv=0.5):
self.n_epochs = n_epochs
self.lr = lr
self.lambda_adv = lambda_adv
def _sigmoid(self, x):
return 1 / (1 + np.exp(-np.clip(x, -250, 250)))
def fit(self, X, y, sensitive):
n_features = X.shape[1]
# 主任务权重
self.w_main = np.random.randn(n_features) * 0.01
self.b_main = 0.0
# 对抗任务权重
self.w_adv = np.random.randn(n_features) * 0.01
self.b_adv = 0.0
for epoch in range(self.n_epochs):
# 前向传播
z_main = X @ self.w_main + self.b_main
pred_main = self._sigmoid(z_main)
# 主任务梯度(最小化分类损失)
grad_main = (pred_main - y) @ X / len(y)
grad_b_main = (pred_main - y).mean()
# 对抗任务(预测敏感属性)
z_adv = X @ self.w_adv + self.b_adv
pred_adv = self._sigmoid(z_adv)
# 对抗梯度(最大化敏感属性预测误差 → 梯度反转)
grad_adv_pred = (pred_adv - sensitive) @ X / len(sensitive)
# 更新主任务参数(减去对抗梯度,实现"梯度反转")
self.w_main -= self.lr * (grad_main - self.lambda_adv * grad_adv_pred)
self.b_main -= self.lr * grad_b_main
# 更新对抗任务参数
self.w_adv -= self.lr * (-grad_adv_pred) # 对抗者最小化自己的损失
self.b_adv -= self.lr * (-(pred_adv - sensitive).mean())
if (epoch + 1) % 10 == 0:
main_loss = -np.mean(y * np.log(pred_main + 1e-10) + (1-y) * np.log(1-pred_main + 1e-10))
print(f"Epoch {epoch+1}: 主任务损失={main_loss:.4f}")
def predict(self, X):
return (self._sigmoid(X @ self.w_main + self.b_main) >= 0.5).astype(int)
# 运行对抗去偏
np.random.seed(42)
n = 2000
sensitive = np.random.binomial(1, 0.4, n)
X = np.random.randn(n, 5)
X[:, 0] = sensitive
y = (X.sum(axis=1) + np.random.randn(n) * 0.5 > 0).astype(int)
X_train, X_test, y_train, y_test, s_train, s_test = train_test_split(X, y, sensitive, test_size=0.3, random_state=42)
adv = AdversarialDebiasing(n_epochs=100, lr=0.05, lambda_adv=1.0)
adv.fit(X_train, y_train, s_train)
pred = adv.predict(X_test)
male = s_test == 0
female = s_test == 1
print(f"\n对抗去偏结果:")
print(f" 准确率: {accuracy_score(y_test, pred):.3f}")
print(f" 男性正预测率: {pred[male].mean():.3f}, 女性正预测率: {pred[female].mean():.3f}")
print(f" 统计均等差: {abs(pred[male].mean() - pred[female].mean()):.3f}")
进阶技巧
公平性与准确率的权衡
公平性约束通常会带来准确率的下降。实际项目中建议:
- 先评估再决策:量化当前模型的偏差程度,而非盲目去偏
- 选择合适的公平性定义:不同场景需要不同的公平性(如贷款场景更适合机会均等)
- 设定可接受的权衡范围:如"统计均等差 < 0.05,准确率下降 < 2%"
偏差来源分析
| 偏差来源 | 示例 | 解决方案 |
|---|---|---|
| 历史偏差 | 历史数据中女性贷款通过率低 | 重新加权、数据增强 |
| 表征偏差 | 训练数据中某些群体样本不足 | 过采样、合成数据 |
| 测量偏差 | 不同群体的数据质量不一致 | 数据清洗、一致性检查 |
| 聚合偏差 | 一刀切模型忽略群体差异 | 分组建模、个性化 |
常见问题与避坑
Q: 多个公平性指标互相冲突怎么办?
A: 这是公平性不可兼容定理的必然结果。实践建议:(1) 与利益相关方沟通,确定优先级最高的1-2个指标;(2) 使用约束优化,将次要指标作为软约束;(3) 做好文档记录,说明选择理由。
Q: 如何处理交叉公平性(如黑人女性)?
A: 单一敏感属性的分析可能掩盖交叉群体的不公平。需要:(1) 构造交叉属性组进行独立分析;(2) 使用分层分析(intersectional analysis);(3) 在指标计算时按交叉分组统计。
Q: 去除敏感属性后模型就公平了吗?
A: 不是。其他特征(如邮编、职业)可能与敏感属性高度相关,产生代理歧视(proxy discrimination)。需要检测特征与敏感属性的相关性,并在去偏时考虑这些代理变量。
总结
模型公平性是负责任AI的核心组成部分。关键要点:
- 公平性有多种定义,没有"万能"的公平性指标,选择需结合业务场景
- 偏差可以在多个阶段缓解:预处理(重新加权)、训练中(对抗去偏)、后处理(阈值调整)
- 公平性与准确率存在权衡,需要在业务层面做出知情决策
- 公平性审计应成为模型上线的必要流程,与性能指标同等重要