AI监控、算法偏见、实时预警、公平性AI、模型监控、偏见检测、AI治理
在人工智能驱动决策日益普及的今天,AI系统中的隐性偏见已成为影响公平性、可信度和业务连续性的关键风险。本文深入探讨了AI原生应用监控的核心挑战,重点剖析了实时领域偏见预警系统的设计原理与实现方法。通过将复杂的算法偏见比作"数字世界的隐形滤镜",我们揭示了偏见如何在AI系统中产生、传播和影响决策。文章系统阐述了从数据采集、特征工程到多维度偏见检测算法的完整技术架构,并提供了基于Python的实现示例和Mermaid可视化图表。无论是金融风控、医疗诊断还是人力资源等领域,本文提供的实时偏见预警框架都能帮助AI工程师、数据科学家和业务决策者构建更公平、透明且负责任的人工智能系统,确保AI技术在推动创新的同时,坚守伦理与公平的底线。
想象一下,两位资历相似的求职者同时申请同一份工作。一位是来自少数民族的女性,另一位是白人男性。尽管前者的简历更为出色,但AI招聘系统却将后者排在前面。这并非虚构场景,而是2018年亚马逊AI招聘工具实际发生的案例。类似地,2016年ProPublica的调查发现,美国多个州使用的COMPAS刑事风险评估系统对非裔美国人存在显著偏见,错误地将他们标记为"高风险"的概率几乎是白人的两倍。
这些案例揭示了一个令人不安的现实:AI系统正日益成为社会决策的核心引擎,但它们常常继承、放大甚至创造新的偏见形式。当我们将越来越多的关键决策交给AI系统——从贷款审批、大学录取到医疗诊断和司法量刑——这些系统中的偏见不再仅仅是技术问题,而是关乎公平、正义和社会信任的根本性挑战。
传统的AI系统开发流程通常在模型部署前进行偏见检测和缓解,但这远远不够。就像一艘在变化莫测的海洋中航行的船只,初始状态的完美并不意味着能够应对未来的风浪。AI系统在实际部署后,会遇到不断变化的数据分布、新的使用场景和未曾预见的边缘情况——这些都可能导致偏见随时间演变和加剧。
这就是为什么我们需要实时领域偏见预警系统:它就像AI系统的"健康监测仪",持续跟踪系统行为,在偏见问题显现之初就发出警报,而不是等到造成实际损害后才被动应对。根据Gartner的预测,到2025年,超过75%的企业AI应用将部署某种形式的偏见监控系统,高于2022年的不到20%。
本文主要面向三类专业人士:
无论您属于哪个领域,本文都将为您提供构建和实施实时领域偏见预警系统所需的理论基础、技术框架和实践指导。
构建有效的实时领域偏见预警系统面临多重挑战:
本文将逐一探讨这些挑战,并提供切实可行的解决方案。
要理解AI偏见,让我们从一个日常生活的比喻开始:想象AI系统戴着一副特殊的"隐形滤镜"来看待世界。这副滤镜由训练数据、算法设计和人类决策共同塑造,决定了AI系统"看到"什么、"重视"什么以及如何"判断"事物。
就像一副有颜色的太阳镜会改变我们对世界的感知,AI系统的"滤镜"也会影响其决策。如果训练数据中存在历史偏见(例如,过去几十年男性在科技行业的代表性过高),AI系统的"滤镜"就会带上相应的色彩,导致其在招聘、晋升等决策中倾向于重复这些历史模式。
更复杂的是,这些"滤镜"往往是多层次的:
实时领域偏见预警系统的核心任务,就是持续监测这些"滤镜"如何影响AI系统的决策,并在它们导致不公平结果时发出警报。
AI系统中的偏见并非单一现象,而是表现为多种形式,每种形式需要不同的检测和缓解策略:
当训练数据不能准确反映系统将要运行的真实世界人口分布时,就会出现表征偏见。这就像一位医生只治疗过城市患者,却突然被派往农村行医——他的诊断模型可能无法适应当地人口的特殊健康需求。
例子:一个面部识别系统主要在浅色皮肤上训练,导致其在识别深色皮肤时准确率显著下降。
历史偏见源于数据中包含的过去的社会不平等和歧视模式。AI系统可能会学习并放大这些历史模式,即使这并非设计者的意图。
例子:贷款审批系统可能会学习到历史上某些社区获得贷款的可能性较低,从而继续对这些社区的申请人设置更高的门槛。
算法偏见产生于算法设计和优化过程中的选择。即使输入数据是无偏的,算法本身的结构和目标函数也可能引入偏见。
例子:一个旨在优化点击率的广告投放算法,可能会向历史上点击率较高的人群(如年轻人)展示更多高薪工作广告,而忽视其他人群。
社会偏见反映了广泛存在于社会中的刻板印象和偏见,这些偏见可能通过训练数据中的语言、图像或标签被AI系统吸收。
例子:自然语言处理系统可能会学习到与特定性别相关的职业刻板印象(如"护士"与女性关联,"工程师"与男性关联)。
交互偏见在AI系统部署后产生,源于用户与系统的互动方式。随着时间推移,这些互动模式可能强化或创造新的偏见。
例子:推荐系统如果最初向特定人群推荐某种类型的内容,用户的点击行为可能会强化这一模式,导致"过滤气泡"和信息茧房。
理解这些不同类型的偏见及其相互作用,是设计有效预警系统的基础。一个全面的监控系统需要能够检测多种偏见类型,并区分它们的根本原因。
偏见在AI系统中不是静态存在的,而是经历一个动态的生命周期。理解这个生命周期有助于我们确定在何处设置关键监控点:
图1: AI偏见生命周期与监控点示意图
从图中可以看出,偏见可能在AI系统生命周期的多个阶段引入,并通过反馈循环不断强化。实时领域偏见预警系统需要在数据采集、模型训练和模型部署等多个环节设置监控点,形成一个持续的检测和干预闭环。
检测偏见的前提是定义什么是"公平"。然而,公平性并非一个单一、普适的概念,而是具有多个维度和定义:
群体间的阳性预测率应该相同。例如,在贷款审批中,不同种族群体被批准贷款的比例应该相同。
数学表达:对于两个群体A和B,P(Ŷ=1|A) = P(Ŷ=1|B)
群体间的真阳性率应该相同。例如,在招聘中,合格的申请人被录用的概率不应因种族而异。
数学表达:对于两个群体A和B,P(Ŷ=1|Y=1,A) = P(Ŷ=1|Y=1,B)
群体间的真阳性率和假阳性率都应该相同。这是机会均等的更强版本。
数学表达:除了机会均等外,还要求P(Ŷ=1|Y=0,A) = P(Ŷ=1|Y=0,B)
相似的个体应该得到相似的结果。例如,具有相似信用历史的两个人应该得到相似的信用评分。
数学表达:如果d(x,x’)很小(表示个体x和x’相似),则d(f(x),f(x’))也应该很小(表示他们的结果相似)
不同群体之间的结果分布应该平衡。这是最常见的公平性定义类别,包含统计parity等概念。
关键挑战在于,这些公平性定义在很多情况下是相互矛盾的——满足其中一个可能意味着违反另一个。因此,实时偏见预警系统需要根据具体应用场景和业务目标,明确选择要监控的公平性指标,并在系统中清晰记录这些选择及其理由。
一个完整的实时领域偏见预警系统需要整合数据采集、特征处理、偏见检测、预警生成和干预支持等多个组件。以下是系统的高层架构设计:
图2: 实时偏见预警系统架构图
这个架构包含五个核心层次,形成一个从数据输入到干预反馈的完整闭环:
接下来,我们将详细探讨每个层次的关键技术和实现方法。
实时偏见预警系统的有效性首先取决于数据质量和完整性。系统需要采集多种类型的数据,建立全面的监控基础。
数据预处理是确保偏见检测准确性的关键环节,包括:
以下是使用Python和Apache Kafka构建实时数据采集管道的示例代码:
import json
from kafka import KafkaProducer
import pandas as pd
from datetime import datetime
import hashlib
import numpy as np
class AIDataCollector:
def __init__(self, bootstrap_servers):
self.producer = KafkaProducer(
bootstrap_servers=bootstrap_servers,
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
self.sensitive_fields = ['gender', 'ethnicity', 'age_group']
def anonymize_sensitive_data(self, data):
"""应用k-匿名化保护敏感信息"""
anonymized = data.copy()
# 对直接标识符进行哈希处理
if 'user_id' in anonymized:
anonymized['user_id'] = hashlib.sha256(anonymized['user_id'].encode()).hexdigest()
# 对年龄进行分组,实现k-匿名化
if 'age' in anonymized:
age = anonymized['age']
if age < 18:
anonymized['age_group'] = 'under_18'
elif 18 <= age < 30:
anonymized['age_group'] = '18-29'
elif 30 <= age < 45:
anonymized['age_group'] = '30-44'
elif 45 <= age < 60:
anonymized['age_group'] = '45-59'
else:
anonymized['age_group'] = '60+'
del anonymized['age']
return anonymized
def add_context_data(self, data):
"""添加环境上下文数据"""
context_enriched = data.copy()
context_enriched['timestamp'] = datetime.utcnow().isoformat()
context_enriched['model_version'] = self.get_current_model_version()
context_enriched['deployment_environment'] = self.get_deployment_environment()
# 添加工作日/周末特征
day_of_week = datetime.utcnow().weekday()
context_enriched['is_weekend'] = 1 if day_of_week >= 5 else 0
return context_enriched
def collect_inference_data(self, input_features, prediction_result,
prediction_probability, user_id=None):
"""收集模型推理数据"""
# 构建基本数据结构
data = {
'input_features': input_features,
'prediction_result': prediction_result,
'prediction_probability': float(prediction_probability)
}
# 如果有用户ID,添加用户相关信息
if user_id:
user_data = self.get_user_demographics(user_id)
data.update(user_data)
# 添加上下文数据
data = self.add_context_data(data)
# 匿名化敏感信息
data = self.anonymize_sensitive_data(data)
# 发送到Kafka主题
self.producer.send('ai_inference_data', value=data)
self.producer.flush()
return data
def get_user_demographics(self, user_id):
"""获取用户人口统计信息(简化示例)"""
# 在实际应用中,这可能是对用户数据库的查询
# 此处仅为示例,实际实现需考虑隐私保护
return {
'user_demographics': {
# 敏感信息将在后续步骤中匿名化
}
}
def get_current_model_version(self):
"""获取当前模型版本"""
# 在实际应用中,这可能从模型注册表中获取
return "v1.2.0"
def get_deployment_environment(self):
"""获取部署环境信息"""
# 在实际应用中,这可能从环境变量或配置服务获取
return "production"
这段代码实现了一个AI数据采集器,负责收集模型推理数据,添加上下文信息,并对敏感数据进行匿名化处理,然后发送到Kafka流处理平台。
有效的偏见检测依赖于高质量的特征工程,特别是与公平性相关的特征构建和敏感属性识别。
敏感属性通常包括:
除了直接的敏感属性,系统还需要识别"代理特征"——那些与敏感属性高度相关的非敏感特征。例如,邮政编码可能与种族或收入水平高度相关,可以作为代理特征。
为了检测偏见,我们需要构建特定的公平性特征:
以下是构建公平性特征和检测敏感属性代理的Python实现示例:
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
class FairnessFeatureEngineer:
def __init__(self, sensitive_attributes=['gender', 'ethnicity', 'age_group']):
self.sensitive_attributes = sensitive_attributes
self.proxy_detectors = {}
self.encoder = OneHotEncoder(sparse=False, drop='first')
def detect_proxy_features(self, data, sample_fraction=0.1):
"""检测可能作为敏感属性代理的非敏感特征"""
# 为大型数据集抽样
if len(data) > 10000:
data_sample = data.sample(frac=sample_fraction, random_state=42)
else:
data_sample = data
proxy_features = {}
# 对每个敏感属性,检测可能的代理特征
for sensitive_attr in self.sensitive_attributes:
if sensitive_attr not in data_sample.columns:
continue
# 准备特征集(排除当前敏感属性和其他敏感属性)
non_sensitive_features = [col for col in data_sample.columns
if col not in self.sensitive_attributes and
col != sensitive_attr]
if not non_sensitive_features:
continue
X = data_sample[non_sensitive_features]
y = data_sample[sensitive_attr]
# 处理分类特征
categorical_cols = X.select_dtypes(include=['object', 'category']).columns
if len(categorical_cols) > 0:
X_encoded = X.copy()
encoded_cols = self.encoder.fit_transform(X[categorical_cols])
encoded_df = pd.DataFrame(
encoded_cols,
columns=self.encoder.get_feature_names_out(categorical_cols)
)
X_encoded = X_encoded.drop(categorical_cols, axis=1)
X_encoded = pd.concat([X_encoded, encoded_df], axis=1)
else:
X_encoded = X
# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_encoded, y, test_size=0.3, random_state=42
)
# 训练一个模型来预测敏感属性
proxy_detector = RandomForestClassifier(n_estimators=100, random_state=42)
proxy_detector.fit(X_train, y_train)
# 评估预测能力
y_pred_proba = proxy_detector.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_pred_proba)
# 如果AUC超过阈值,表明存在强代理特征
if auc > 0.8: # 高AUC表明非敏感特征可以很好地预测敏感属性
# 获取特征重要性
feature_importance = pd.Series(
proxy_detector.feature_importances_,
index=X_encoded.columns
).sort_values(ascending=False)
proxy_features[sensitive_attr] = {
'auc': auc,
'top_proxies': feature_importance.head(5).to_dict()
}
# 保存代理检测器供后续使用
self.proxy_detectors[sensitive_attr] = proxy_detector
return proxy_features
def build_fairness_features(self, data, prediction_col='prediction',
outcome_col=None):
"""构建用于偏见检测的公平性特征"""
fairness_features = pd.DataFrame(index=data.index)
# 为每个敏感属性计算群体公平性指标
for sensitive_attr in self.sensitive_attributes:
if sensitive_attr not in data.columns:
continue
# 获取唯一的群体值
groups = data[sensitive_attr].unique()
if len(groups) < 2:
continue # 需要至少两个群体进行比较
# 计算统计 parity (不同群体的阳性预测率)
for group in groups:
group_mask = data[sensitive_attr] == group
group_size = group_mask.sum()
if group_size == 0:
continue
# 阳性预测率
positive_rate = data.loc[group_mask, prediction_col].mean()
fairness_features[f'fair_{sensitive_attr}_{group}_positive_rate'] = positive_rate
# 如果有真实结果,计算真阳性率和假阳性率
if outcome_col and outcome_col in data.columns:
# 真阳性率 (TPR)
true_positives = ((data.loc[group_mask, prediction_col] == 1) &
(data.loc[group_mask, outcome_col] == 1)).sum()
actual_positives = (data.loc[group_mask, outcome_col] == 1).sum()
tpr = true_positives / actual_positives if actual_positives > 0 else 0
fairness_features[f'fair_{sensitive_attr}_{group}_tpr'] = tpr
# 假阳性率 (FPR)
false_positives = ((data.loc[group_mask, prediction_col] == 1) &
(data.loc[group_mask, outcome_col] == 0)).sum()
actual_negatives = (data.loc[group_mask, outcome_col] == 0).sum()
fpr = false_positives / actual_negatives if actual_negatives > 0 else 0
fairness_features[f'fair_{sensitive_attr}_{group}_fpr'] = fpr
# 计算群体间差异指标
for sensitive_attr in self.sensitive_attributes:
if sensitive_attr not in data.columns:
continue
groups = data[sensitive_attr].unique()
if len(groups) < 2:
continue
# 获取所有群体的阳性预测率
pos_rate_cols = [col for col in fairness_features.columns
if col.startswith(f'fair_{sensitive_attr}') and
'positive_rate' in col]
if len(pos_rate_cols) >= 2:
# 最大差异
max_diff = fairness_features[pos_rate_cols].max(axis=1) - fairness_features[pos_rate_cols].min(axis=1)
fairness_features[f'fair_{sensitive_attr}_max_pos_rate_diff'] = max_diff
# 比值
ratios = fairness_features[pos_rate_cols].max(axis=1) / fairness_features[pos_rate_cols].min(axis=1)
fairness_features[f'fair_{sensitive_attr}_pos_rate_ratio'] = ratios
# 如果有真实结果,计算TPR和FPR的群体间差异
if outcome_col and outcome_col in data.columns:
tpr_cols = [col for col in fairness_features.columns
if col.startswith(f'fair_{sensitive_attr}') and 'tpr' in col]
if len(tpr_cols) >= 2:
max_tpr_diff = fairness_features[tpr_cols].max(axis=1) - fairness_features[tpr_cols].min(axis=1)
fairness_features[f'fair_{sensitive_attr}_max_tpr_diff'] = max_tpr_diff
fpr_cols = [col for col in fairness_features.columns
if col.startswith(f'fair_{sensitive_attr}') and 'fpr' in col]
if len(fpr_cols) >= 2:
max_fpr_diff = fairness_features[fpr_cols].max(axis=1) - fairness_features[fpr_cols].min(axis=1)
fairness_features[f'fair_{sensitive_attr}_max_fpr_diff'] = max_fpr_diff
return fairness_features
这段代码实现了一个公平性特征工程类,包含两个核心功能:
偏见检测是实时预警系统的核心,需要从多个维度、使用多种方法进行分析。以下是主要的检测方法及其技术原理。
统计公平性指标是检测群体偏见的基础方法,通过比较不同人口统计群体间的预测结果分布来识别潜在偏见。
常用的统计公平性指标包括:
人口学 parity (Demographic Parity)
P ( Y ^ = 1 ∣ A = a 1 ) = P ( Y ^ = 1 ∣ A = a 2 ) P(\hat{Y}=1 | A=a_1) = P(\hat{Y}=1 | A=a_2) P(Y^=1∣A=a1)=P(Y^=1∣A=a2)
不同群体的阳性预测率应该相同
均等几率 (Equalized Odds)
P ( Y ^ = 1 ∣ Y = 1 , A = a 1 ) = P ( Y ^ = 1 ∣ Y = 1 , A = a 2 ) P(\hat{Y}=1 | Y=1, A=a_1) = P(\hat{Y}=1 | Y=1, A=a_2) P(Y^=1∣Y=1,A=a1)=P(Y^=1∣Y=1,A=a2)
P ( Y ^ = 1 ∣ Y = 0 , A = a 1 ) = P ( Y ^ = 1 ∣ Y = 0 , A = a 2 ) P(\hat{Y}=1 | Y=0, A=a_1) = P(\hat{Y}=1 | Y=0, A=a_2) P(Y^=1∣Y=0,A=a1)=P(Y^=1∣Y=0,A=a2)
不同群体的真阳性率和假阳性率应该相同
机会均等 (Equal Opportunity)
P ( Y ^ = 1 ∣ Y = 1 , A = a 1 ) = P ( Y ^ = 1 ∣ Y = 1 , A = a 2 ) P(\hat{Y}=1 | Y=1, A=a_1) = P(\hat{Y}=1 | Y=1, A=a_2) P(Y^=1∣Y=1,A=a1)=P(Y^=1∣Y=1,A=a2)
不同群体的真阳性率应该相同
预测平衡 (Predictive Parity)
P ( Y = 1 ∣ Y ^ = 1 , A = a 1 ) = P ( Y = 1 ∣ Y ^ = 1 , A = a 2 ) P(Y=1 | \hat{Y}=1, A=a_1) = P(Y=1 | \hat{Y}=1, A=a_2) P(Y=1∣Y^=1,A=a1)=P(Y=1∣Y^=1,A=a2)
不同群体的预测阳性结果的准确率应该相同
群体校准 (Group Calibration)
P ( Y = 1 ∣ Y ^ = p , A = a 1 ) = P ( Y = 1 ∣ Y ^ = p , A = a 2 ) = p P(Y=1 | \hat{Y}=p, A=a_1) = P(Y=1 | \hat{Y}=p, A=a_2) = p P(Y=1∣Y^=p,A=a1)=P(Y=1∣Y^=p,A=a2)=p
对于给定的预测概率p,不同群体的实际结果概率应该等于p且相互相同
离散指数 (Disparate Impact Ratio)
D I R = 受保护群体的选择率 优势群体的选择率 DIR = \frac{\text{受保护群体的选择率}}{\text{优势群体的选择率}} DIR=优势群体的选择率受保护群体的选择率
衡量不同群体被选中(阳性预测)的比例差异,通常以0.8为阈值
预测差异分析通过比较模型对不同群体的预测行为来检测偏见,包括:
因果推断方法超越相关性分析,尝试识别模型决策中的因果偏见:
以下是实现多种偏见检测算法的Python代码示例:
import pandas as pd
import numpy as np
import scipy.stats as stats
from sklearn.metrics import roc_auc_score, confusion_matrix
from scipy.spatial.distance import jensenshannon
import matplotlib.pyplot as plt
import seaborn as sns
class BiasDetector:
def __init__(self, sensitive_attributes=['gender', 'ethnicity', 'age_group']):
self.sensitive_attributes = sensitive_attributes
self.reference_group = None # 可以设置一个参考群体,默认为最大的群体
self.metrics_history = {} # 存储历史指标,用于趋势分析
def set_reference_group(self, data, sensitive_attr):
"""设置参考群体(默认为最大的群体)"""
if sensitive_attr not in data.columns:
return None
group_counts = data[sensitive_attr].value_counts()
return group_counts.index[0] # 返回最大群体
def demographic_parity(self, data, prediction_col='prediction', sensitive_attr=None):
"""计算人口学parity指标"""
results = {}
# 如果未指定敏感属性,则计算所有敏感属性
attrs = [sensitive_attr] if sensitive_attr else self.sensitive_attributes
for attr in attrs:
if attr not in data.columns:
continue
# 获取群体
groups = data[attr].unique()
if len(groups) < 2:
continue
# 确定参考群体
ref_group = self.reference_group if self.reference_group else self.set_reference_group(data, attr)
# 计算每个群体的阳性预测率
pos_rates = {}
for group in groups:
group_data = data[data[attr] == group]
if len(group_data) == 0:
pos_rates[group] = 0.0
continue
pos_rate = group_data[prediction_col].mean()
pos_rates[group] = pos_rate
# 计算与参考群体的差异
disparities = {}
for group, rate in pos_rates.items():
disparities[group] = rate - pos_rates[ref_group]
# 计算最大差异
max_disparity = max(pos_rates.values()) - min(pos_rates.values())
# 计算离散影响比 (DIR) - 最小群体率 / 最大群体率
min_rate = min(pos_rates.values())
max_rate = max(pos_rates.values())
dir_ratio = min_rate / max_rate if max_rate > 0 else 0
results[attr] = {
'positive_rates': pos_rates,
'disparities_from_ref': disparities,
'max_disparity': max_disparity,
'disparate_impact_ratio': dir_ratio,
'reference_group': ref_group
}
return results
def equalized_odds(self, data, prediction_col='prediction', outcome_col='outcome',
sensitive_attr=None):
"""计算均等几率指标(TPR和FPR的群体差异)"""
results = {}
# 检查是否有结果列
if outcome_col not in data.columns:
return results
# 如果未指定敏感属性,则计算所有敏感属性
attrs = [sensitive_attr] if sensitive_attr else self.sensitive_attributes
for attr in attrs:
if attr not in data.columns:
continue
# 获取群体
groups = data[attr].unique()
if len(groups) < 2:
continue
# 确定参考群体
ref_group = self.reference_group if self.reference_group else self.set_reference_group(data, attr)
tpr_results = {} # 真阳性率
fpr_results = {} # 假阳性率
for group in groups:
group_data = data[data[attr] == group]
if len(group_data) == 0:
tpr_results[group] = 0.0
fpr_results[group] = 0.0
continue
# 计算混淆矩阵
tn, fp, fn, tp = confusion_matrix(
group_data[outcome_col],
group_data[prediction_col]
).ravel()
# 计算TPR和FPR
tpr = tp / (tp + fn) if (tp + fn) > 0 else 0
fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
tpr_results[group] = tpr
fpr_results[group] = fpr
# 计算与参考群体的差异
tpr_disparities = {g: tpr_results[g] - tpr_results[ref_group] for g in groups}
fpr_disparities = {g: fpr_results[g] - fpr_results[ref_group] for g in groups}
results[attr] = {
'tpr': tpr_results,
'fpr': fpr_results,
'tpr_disparities_from_ref': tpr_disparities,
'fpr_disparities_from_ref': fpr_disparities,
'reference_group': ref_group
}
return results
def predictive_parity(self, data, prediction_col='prediction', outcome_col='outcome',
sensitive_attr=None):
"""计算预测平衡指标(不同群体阳性预测的准确率)"""
results = {}
# 检查是否有结果列
if outcome_col not in data.columns:
return results
# 如果未指定敏感属性,则计算所有敏感属性
attrs = [sensitive_attr] if sensitive_attr else self.sensitive_attributes
for attr in attrs:
if attr not in data.columns:
continue
# 获取群体
groups = data[attr].unique()
if len(groups) < 2:
continue
precision_results = {} # 精确率(阳性预测的准确率)
for group in groups:
group_data = data[data[attr] == group]
positive_preds = group_data[group_data[prediction_col] == 1]
if len(positive_preds) == 0:
precision_results[group] = 0.0
continue
# 计算精确率(阳性预测中的实际阳性比例)
precision = positive_preds[outcome_col].mean()
precision_results[group] = precision
results[attr] = {
'precision': precision_results,
'max_precision_diff': max(precision_results.values()) - min(precision_results.values())
}
return results
def counterfactual_testing(self, data, model, prediction_col='prediction',
sensitive_attr=None, epsilon=0.05):
"""
反事实测试:改变敏感属性值,观察预测结果变化
这检测模型是否对敏感属性直接敏感
"""
results = {}
# 如果未指定敏感属性,则计算所有敏感属性
attrs = [sensitive_attr] if sensitive_attr else self.sensitive_attributes
for attr in attrs:
if attr not in data.columns:
continue
# 获取群体
groups = data[attr].unique()
if len(groups) < 2:
continue
# 为每个样本创建反事实版本
counterfactual_changes = {}
total_tested = 0
significant_changes = 0
# 只测试一部分样本以提高效率
test_data = data.sample(min(1000, len(data)), random_state=42).copy()
for idx, row in test_data.iterrows():
original_group = row[attr]
original_pred = row[prediction_col]
# 尝试切换到其他群体
for target_group in groups:
if target_group == original_group:
continue
# 创建反事实样本(只改变敏感属性)
cf_sample = row.copy()
cf_sample[attr] = target_group
# 准备模型输入(排除原始预测和结果)
input_features = cf_sample.drop([prediction_col, outcome_col]
if 'outcome' in cf_sample else [prediction_col])
# 获取预测概率
cf_pred_prob = model.predict_proba([input_features.values])[0, 1]
cf_pred = 1 if cf_pred_prob >= 0.5 else 0
# 检查预测是否发生变化
if cf_pred != original_pred:
significant_changes += 1
# 记录概率变化
prob_change = abs(cf_pred_prob - row.get('prediction_probability', 0.5))
if prob_change > epsilon:
counterfactual_changes[(original_group, target_group)] = \
counterfactual_changes.get((original_group, target_group), 0) + 1
total_tested += 1
# 计算反事实变化率
change_rates = {}
for (orig_group, target_group), count in counterfactual_changes.items():
change_rates[(orig_group, target_group)] = count / total_tested
results[attr] = {
'change_rates': change_rates,
'significant_change_rate': significant_changes / total_tested,
'total_tested': total_tested
}
return results
def prediction_distribution_analysis(self, data, prediction_col='prediction_probability',
sensitive_attr=None, bins=20):
"""分析不同群体的预测分布差异"""
results = {}
# 如果未指定敏感属性,则计算所有敏感属性
attrs = [sensitive_attr] if sensitive_attr else self.sensitive_attributes
for attr in attrs:
if attr not in data.columns:
continue
# 获取群体
groups = data[attr].unique()
if len(groups) < 2:
continue
# 获取每个群体的预测分布
distributions = {}
js_distances = {} # Jensen-Shannon距离,衡量分布相似度
for group in groups:
group_data = data[data[attr] == group]
if len(group_data) == 0:
continue
# 获取预测概率分布
preds = group_data[prediction_col]
distributions[group] = preds
# 计算直方图
hist, bin_edges = np.histogram(preds, bins=bins, range=(0, 1), density=True)
distributions[f'{group}_hist'] = hist
distributions[f'{group}_bin_edges'] = bin_edges
# 计算群体间的Jensen-Shannon距离(0表示分布相同,1表示完全不同)
for i, group1 in enumerate(groups):
for j, group2 in enumerate(groups[i+1:]):
if f'{group1}_hist' not in distributions or f'{group2}_hist' not in distributions:
continue
# 确保两个分布具有相同的bin
dist1 = distributions[f'{group1}_hist']
dist2 = distributions[f'{group2}_hist']
# 添加微小值以避免零概率问题
dist1 = dist1 + 1e-10
dist2 = dist2 + 1e-10
# 计算JS距离
js_distance = jensenshannon(dist1, dist2)
js_distances[(group1, group2)] = js_distance
results[attr] = {
'distributions': distributions,
'jensen_shannon_distances': js_distances
}
return results
def run_comprehensive_bias_analysis(self, data, model=None, prediction_col='prediction',
prediction_prob_col='prediction_probability',
outcome_col='outcome'):
"""运行全面的偏见分析,整合所有检测方法"""
results = {
'timestamp': pd.Timestamp.now(),
'sample_size': len(data)
}
# 1. 人口学parity分析
results['demographic_parity'] = self.demographic_parity(data, prediction_col)
# 2. 均等几率分析(如果有结果数据)
if outcome_col in data.columns:
results['equalized_odds'] = self.equalized_odds(data, prediction_col, outcome_col)
results['predictive_parity'] = self.predictive_parity(data, prediction_col, outcome_col)
# 3. 预测分布分析
if prediction_prob_col in data.columns:
results['prediction_distributions'] = self.prediction_distribution_analysis(
data, prediction_prob_col
)
else:
# 如果没有概率列,尝试使用预测列
results['prediction_distributions'] = self.prediction_distribution_analysis(
data, prediction_col
)
# 4. 反事实测试(如果提供了模型)
if model is not None and outcome_col in data.columns:
results['counterfactual_testing'] = self.counterfactual_testing(
data, model, prediction_col, outcome_col=outcome_col
)
# 存储到历史记录
self.metrics_history[pd.Timestamp.now()] = results
return results
def visualize_bias_metrics(self, bias_results, sensitive_attr=None, figsize=(15, 10)):
"""可视化偏见指标"""
figs = {}
# 如果未指定敏感属性,选择第一个可用的
if not sensitive_attr and 'demographic_parity' in bias_results:
sensitive_attr = next(iter(bias_results['demographic_parity'].keys()),