模型公平性与偏差检测:AI伦理实战指南

模型公平性与偏差检测: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}")

进阶技巧

公平性与准确率的权衡

公平性约束通常会带来准确率的下降。实际项目中建议:

  1. 先评估再决策:量化当前模型的偏差程度,而非盲目去偏
  2. 选择合适的公平性定义:不同场景需要不同的公平性(如贷款场景更适合机会均等)
  3. 设定可接受的权衡范围:如"统计均等差 < 0.05,准确率下降 < 2%"

偏差来源分析

偏差来源 示例 解决方案
历史偏差 历史数据中女性贷款通过率低 重新加权、数据增强
表征偏差 训练数据中某些群体样本不足 过采样、合成数据
测量偏差 不同群体的数据质量不一致 数据清洗、一致性检查
聚合偏差 一刀切模型忽略群体差异 分组建模、个性化

常见问题与避坑

Q: 多个公平性指标互相冲突怎么办?

A: 这是公平性不可兼容定理的必然结果。实践建议:(1) 与利益相关方沟通,确定优先级最高的1-2个指标;(2) 使用约束优化,将次要指标作为软约束;(3) 做好文档记录,说明选择理由。

Q: 如何处理交叉公平性(如黑人女性)?

A: 单一敏感属性的分析可能掩盖交叉群体的不公平。需要:(1) 构造交叉属性组进行独立分析;(2) 使用分层分析(intersectional analysis);(3) 在指标计算时按交叉分组统计。

Q: 去除敏感属性后模型就公平了吗?

A: 不是。其他特征(如邮编、职业)可能与敏感属性高度相关,产生代理歧视(proxy discrimination)。需要检测特征与敏感属性的相关性,并在去偏时考虑这些代理变量。

总结

模型公平性是负责任AI的核心组成部分。关键要点:

  1. 公平性有多种定义,没有"万能"的公平性指标,选择需结合业务场景
  2. 偏差可以在多个阶段缓解:预处理(重新加权)、训练中(对抗去偏)、后处理(阈值调整)
  3. 公平性与准确率存在权衡,需要在业务层面做出知情决策
  4. 公平性审计应成为模型上线的必要流程,与性能指标同等重要
相关推荐
lunzi_08261 小时前
【AI 杂谈】AI 的边界正在被谁定义?
人工智能
静Yu1 小时前
给 AI 装上「身体」:我用魔珐星云做数字人的实践与接入教程
人工智能
OpenMiniServer1 小时前
终极演化动力学:绝对悖论与信息重构
人工智能·重构
阿乔外贸日记1 小时前
意大利进口主力产品及主要合作供应国
大数据·人工智能·物联网·搜索引擎·云计算
可编程芯片开发1 小时前
基于simulink的PEM燃料电池控制系统建模与仿真,对比PID,积分分离以及滑模控制器
算法
zybsjn1 小时前
向量库与Embedding:知识库AI化的核心技术概述
人工智能·embedding
学究天人2 小时前
数学公理体系大全:Comprehensive Collection of Mathematical Axiom Systems(卷2)
算法·数学建模·动态规划·图论·抽象代数·拓扑学
IT大师兄吖2 小时前
MOSS-TTS-Nano ONNX:轻量级离线语音合成 懒人整合包
人工智能
木木学AI2 小时前
AI电话系统技术选型:语音识别精度、高并发调度与CRM集成评估
人工智能·语音识别
hai3152475432 小时前
九章编译法----空间几何统一编译法
网络·汇编·人工智能·线性代数