尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

几何视角下的模型公平性审计:鲁棒性分析与Python实践

几何视角下的模型公平性审计:鲁棒性分析与Python实践 在算法逐步深入信贷、招聘、医疗、内容推荐等决策场景后“公平性审计”已经从学术议题变成了工程落地必须面对的环节。可是真正做过审计的人会有一个感受单看一个公平性指标比如 Demographic Parity Difference 或者 Equalized Odds结果很容易被数据噪声、样本量、阈值选择影响。同一套模型换一个验证集审计结论可能完全反过来。这篇文章想换一个视角把“公平性审计”放到几何框架里来看。我们会先梳理为什么传统指标不够稳定然后介绍一组基于空间距离、分布形状、集合重叠度的几何度量方式最后用 Python 完成一个带鲁棒性检验的公平性审计小工具。整个项目代码可以复制到本地直接运行重点不是造一个论文级算法而是帮你建立一套“审计结论可解释、可复现、抗扰动”的工程思路。适合阅读本文的读者有两类一类是算法工程师和风控/推荐系统开发想在自己的模型评估流程里加入更严谨的公平性审计模块另一类是数据分析师或机器学习初学者希望理解公平性度量背后的几何直觉并掌握用 Python 实现审计的方法。读完之后你会掌握几何审计的核心指标写出一套支持自助抽样和扰动验证的审计代码并知道如何分析审计结论的可靠性。1. 公平性审计为什么需要几何视角1.1 传统公平性指标的局限目前工业界最常用的公平性审计方式是计算模型在不同群体上的统计差异。典型指标包括指标名称计算方式关注点Demographic Parity两个群体预测为正类的概率之差预测结果是否与敏感属性无关Equalized Odds两个群体在真实标签各分组下的 TPR/FPR 差异错误率是否均衡Predictive Parity两个群体预测为正类的精准率差异正类命中率是否均衡这些指标都有明确的公式和阈值比如 Demographic Parity Difference 小于 0.1 通常被认为是可接受的。但在实际操作中它们有几个明显的痛点。第一个痛点是样本量敏感。当某个群体样本特别少时TPR、FPR 这些比率估计的方差会很大指标值可能只是因为几条数据的变化就从 0.05 跳到 0.15审计结论完全被样本噪声左右。第二个痛点是阈值依赖。逻辑回归输出的概率本身是连续的上面这些指标都必须先选定一个阈值比如 0.5才能把概率变成分类结果。换个阈值不同群体间的差异就会改变审计人员很容易在“调阈值调出合规结论”的过程中失去客观性。第三个痛点是缺乏分布信息。传统指标只比较均值、比率这些一阶统计量没有关注两个群体预测分数分布的方差、偏度、重叠程度。两个群体的平均预测分数相同不代表它们的分布形态也相同——一个群体可能集中在 0.4 附近另一个群体分散在 0.1 和 0.9 两端。这种情况下只看均值差异就会漏掉严重的分布不均衡。1.2 几何视角的核心思想所谓“几何视角”就是把一个群体在模型输出空间中的预测分数分布想象成高维空间里的一个点集。于是公平性问题变成了两个点集之间的空间关系问题。具体来说可以这样理解每个样本的模型输出是一个多维向量例如逻辑回归的预测概率、嵌入层输出的特征向量或者多个子模型分数的拼接。一个群体所有样本的输出向量构成了空间中的一个点云。两个群体的公平性可以转化为点云质心之间的距离、点云的形状差异、点云之间的重叠程度。这个视角下传统指标只能回答“两个群体的平均分数差多少”而几何指标能回答“两个群体的分数分布有多像”。后者包含的信息量显然更大。从鲁棒性审计的角度看几何视角还有一个优势点云的重叠程度和整体形状相对均值和比率更稳定。比如在数据中加入少量噪声两个点云的质心可能会轻微移动但只要分布形状没有本质变化重叠度指标就不会剧烈震荡。这也是这篇教程标题里“几何理论”的意义所在——用空间的、集合的、距离的语言去描述公平性从而让审计结论更鲁棒、更不容易被局部样本点影响。1.3 鲁棒性在审计中的含义鲁棒性Robustness在公平性审计里有两层含义。第一层是统计鲁棒性。审计数据集本身是抽样得到的如果换一个随机种子重新抽样审计结论就不一样那么这个审计就是不可靠的。我们希望评价指标在合理的抽样波动范围内保持稳定。第二层是扰动鲁棒性。真实业务数据在采集过程中可能存在标签噪声、特征缺失、异常值。如果给预测分数加一点随机的扰动审计结论立刻反转说明模型本身对输入变化过于敏感审计结果也不具有实际业务参考价值。因此一个合格的公平性审计流程不是只算出一个数字而是要回答三个问题指标点估计是多少这个指标的置信区间有多大在数据扰动后结论是否还在可接受范围内几何指标配合自助抽样Bootstrap和扰动注入就能很好地回答这三个问题。这也是本文实战部分会重点实现的内容。2. 环境准备与数据说明2.1 运行环境本文代码在 Python 3.9 环境下验证通过依赖库如下库名用途numpy数值计算、矩阵运算pandas数据读取与处理scikit-learn逻辑回归模型、数据划分scipy距离计算、统计分布matplotlib可视化分布与凸包如果你的环境还没有安装这些库可以使用下面的命令安装pip install numpy pandas scikit-learn scipy matplotlib版本不需要完全一致但建议 numpy 不低于 1.21scikit-learn 不低于 1.0。如果你的项目已经有其他机器学习依赖建议先创建虚拟环境避免版本冲突。python -m venv fairness-audit-env source fairness-audit-env/bin/activate # Windows 下使用 fairness-audit-env\Scripts\activate pip install numpy pandas scikit-learn scipy matplotlib2.2 模拟数据说明为了方便演示我们构造一份模拟数据集模拟一个信贷审批场景特征x1、x2与还款能力相关的数值特征。特征x3一个对标签有一定预测能力的特征。敏感属性group取值为A和B表示两个群体。标签y表示是否违约1 表示违约。生成逻辑如下群体 B 的x2特征均值略低于群体 A同时违约概率也略低。这样一来如果模型把x2作为重要特征就可能对两个群体产生不同的预测分布。为了避免样本量影响审计结果我们让群体 B 的样本数只有群体 A 的 40% 左右模拟真实业务中少数群体样本不足的场景。import numpy as np import pandas as pd np.random.seed(42) n_a 2000 n_b 800 # 群体 A x1_a np.random.normal(0, 1, n_a) x2_a np.random.normal(0.5, 1.0, n_a) x3_a np.random.normal(0, 1, n_a) y_a np.where(1.2 * x1_a - 0.8 * x2_a 0.5 * x3_a np.random.normal(0, 0.8, n_a) 0, 1, 0) # 群体 B x1_b np.random.normal(0, 1, n_b) x2_b np.random.normal(-0.3, 1.1, n_b) x3_b np.random.normal(0, 1, n_b) y_b np.where(1.2 * x1_b - 0.8 * x2_b 0.5 * x3_b np.random.normal(0, 0.8, n_b) 0.3 0, 1, 0) data_a pd.DataFrame({x1: x1_a, x2: x2_a, x3: x3_a, group: A, y: y_a}) data_b pd.DataFrame({x1: x1_b, x2: x2_b, x3: x3_b, group: B, y: y_b}) data pd.concat([data_a, data_b], axis0).reset_index(dropTrue) print(data.groupby(group)[y].mean())这段代码的核心逻辑是控制两个群体的真实违约率有一定差异同时让群体 B 的数据噪声略大。这样训练出来的模型会在两个群体上表现出可观察的预测分数差异方便我们进行审计演示。实际项目中你只需要把data替换成自己的真实数据即可后续审计代码不需要改动。2.3 定义审计对象我们审计的对象是模型预测分数。这里的预测分数不一定是二分类标签而是模型输出的连续值。在 scikit-learn 中逻辑回归的predict_proba输出的正类概率就是最常用的预测分数。审计时会用到两个群体的预测分数集合scores_A群体 A 中每个样本的预测分数。scores_B群体 B 中每个样本的预测分数。我们需要比较的就是这两个一维或多维分数集合之间的关系。下面的实战中我们先用一维概率分数做基础审计再扩展到二维分数做几何可视化。3. 核心几何度量与实现原理动手写代码之前先把本文用到的三个核心几何度量讲清楚。这三个度量分别从位置、形状、重叠三个角度描述群体间差异组合在一起能比单一指标提供更完整的公平性画像。3.1 质心距离与马氏距离最直观的几何指标是两个点云质心之间的距离。质心就是群体的平均预测分数向量。在一维情况下质心距离就是两个群体平均预测分数之差的绝对值。这个指标和 Demographic Parity 的思路非常接近但区别在于我们不对分数做二值化直接比较原始概率分布避免了阈值选择带来的不稳定。不过单纯的欧氏距离有一个问题它没有考虑数据的分布形状。假设群体 A 的分数集中在 0.4 到 0.6 之间方差很小群体 B 的分数从 0.1 到 0.9 分布很分散。两个群体质心相差 0.05但如果考虑方差这 0.05 的差异在群体 A 内部已经算很大的偏移了。马氏距离Mahalanobis Distance解决了这个问题。它的计算公式如下D sqrt((μ_A - μ_B)^T * Σ^{-1} * (μ_A - μ_B))其中 μ_A、μ_B 是两个群体的质心向量Σ 是合并协方差矩阵。这个距离实际上是在“以数据自身方差为单位”的坐标系里度量两个质心的差异。方差越小的方向权重越高所以它对分布形状更加敏感。在实现中马氏距离可以用 scipy 的mahalanobis函数快速计算。需要注意合并协方差矩阵的计算方式我们使用两个群体协方差矩阵的样本量加权平均def compute_mahalanobis_distance(scores_A, scores_B): mu_A np.mean(scores_A, axis0) mu_B np.mean(scores_B, axis0) cov_A np.cov(scores_A.T) cov_B np.cov(scores_B.T) n_A len(scores_A) n_B len(scores_B) pooled_cov (n_A * cov_A n_B * cov_B) / (n_A n_B) inv_cov np.linalg.inv(pooled_cov) diff mu_A - mu_B return np.sqrt(diff.T inv_cov diff)实际项目中如果协方差矩阵接近奇异某个特征方差接近 0需要先做主成分降维或者加一个小的正则项防止求逆时数值不稳定。3.2 凸包与重叠度质心距离描述的是“两个分布差多远”但没有回答“两个分布是否重叠”。一个更直观的几何量是两个点云的凸包体积或面积。凸包Convex Hull可以理解为“包住所有点的最小凸多边形”。在二维平面上我们可以用 scipy 的ConvexHull计算每个群体预测分数点云的凸包然后估算两个凸包的重叠面积比例。重叠面积比例的计算思路如下分别计算群体 A 和群体 B 的凸包。在两个凸包的并集范围内随机采样大量点。统计同时落在两个凸包内部的采样点比例作为重叠度估计。采样法虽然不是精确计算但在二维空间里只要采样点足够多精度就能满足审计场景的需要。而且采样法天然支持扩展到三维甚至更高维只需要把ConvexHull改为ConvexHull的find_simplex方法进行包含性判断即可。先看二维情况下凸包重叠度的实现from scipy.spatial import ConvexHull def convex_hull_overlap_2d(scores_A, scores_B, n_samples10000, seed0): 估算两个二维点云凸包的重叠比例。 返回一个 0~1 之间的值值越大表示重叠程度越高。 rng np.random.default_rng(seed) hull_A ConvexHull(scores_A) hull_B ConvexHull(scores_B) min_x min(scores_A[:, 0].min(), scores_B[:, 0].min()) max_x max(scores_A[:, 0].max(), scores_B[:, 0].max()) min_y min(scores_A[:, 1].min(), scores_B[:, 1].min()) max_y max(scores_A[:, 1].max(), scores_B[:, 1].max()) points np.column_stack([ rng.uniform(min_x, max_x, n_samples), rng.uniform(min_y, max_y, n_samples) ]) inside_A hull_A.find_simplex(points) 0 inside_B hull_B.find_simplex(points) 0 overlap_count np.sum(inside_A inside_B) union_count np.sum(inside_A | inside_B) if union_count 0: return 0.0 return overlap_count / union_countfind_simplex是 scipy 提供的一个很实用的方法如果点在凸包内部返回非负值否则返回 -1。通过并集上采样估计重叠比例实现简单且容易解释。3.3 扰动注入与鲁棒性度量有了基础指标之后还需要回答“这个指标值稳不稳定”的问题。我们使用两种技术来度量鲁棒性。第一种是自助抽样Bootstrap思路是从原始预测分数中有放回地重复抽样每次抽样后重新计算指标从而得到指标的置信区间。如果置信区间跨越了公平性阈值边界说明审计结论不够稳定。第二种是噪声扰动思路是对预测分数加一个随机噪声再重新计算指标。这里噪声幅度不能太大否则审计对象就变了。常见做法是加入标准差为原始分数标准差 5% 到 10% 的高斯噪声多重复几次观察指标变化范围。两种技术结合可以得到一个“指标值 ± 不确定性”的审计结果而不是一个孤零零的数字。下面开始写完整的实战代码。4. 完整实战几何公平性审计工具箱4.1 项目结构我们创建一个名为fairness_audit的文件夹包含两个核心文件fairness_audit/ ├── audit_metrics.py # 几何度量函数 ├── run_audit.py # 主流程脚本 └── audit_report.txt # 运行后自动生成为了让代码更清晰我们把度量函数和主流程分开这样你可以直接把audit_metrics.py集成到自己的项目中。4.2 编写几何度量模块首先创建audit_metrics.py保存核心度量函数。# 文件路径fairness_audit/audit_metrics.py import numpy as np from scipy.spatial import ConvexHull def compute_mahalanobis_distance(scores_A, scores_B, reg1e-6): 计算两个群体预测分数分布的马氏距离。 参数 scores_A: ndarray, shape (n_A, d) 或 (n_A,) scores_B: ndarray, shape (n_B, d) 或 (n_B,) reg: float, 协方差矩阵正则项防止奇异 返回 float, 马氏距离 scores_A np.asarray(scores_A, dtypefloat).reshape(len(scores_A), -1) scores_B np.asarray(scores_B, dtypefloat).reshape(len(scores_B), -1) mu_A np.mean(scores_A, axis0) mu_B np.mean(scores_B, axis0) cov_A np.cov(scores_A.T) cov_B np.cov(scores_B.T) n_A len(scores_A) n_B len(scores_B) pooled_cov (n_A * cov_A n_B * cov_B) / (n_A n_B) pooled_cov reg * np.eye(pooled_cov.shape[0]) inv_cov np.linalg.inv(pooled_cov) diff mu_A - mu_B return float(np.sqrt(diff.T inv_cov diff)) def mean_score_diff(scores_A, scores_B): 两个群体平均预测分数的差值。 这个指标对应传统公平性审计中的 Demographic Parity 的连续版本。 return float(np.mean(scores_A) - np.mean(scores_B)) def convex_hull_overlap_2d(scores_A, scores_B, n_samples20000, seed42): 通过蒙特卡洛采样估算两个二维凸包的重叠比例。 返回 overlap_ratio: float, 0~1越大表示两个分布重叠越高 hull_A_area: float, A 凸包面积 hull_B_area: float, B 凸包面积 scores_A np.asarray(scores_A, dtypefloat).reshape(-1, 2) scores_B np.asarray(scores_B, dtypefloat).reshape(-1, 2) hull_A ConvexHull(scores_A) hull_B ConvexHull(scores_B) min_x min(scores_A[:, 0].min(), scores_B[:, 0].min()) max_x max(scores_A[:, 0].max(), scores_B[:, 0].max()) min_y min(scores_A[:, 1].min(), scores_B[:, 1].min()) max_y max(scores_A[:, 1].max(), scores_B[:, 1].max()) rng np.random.default_rng(seed) points np.column_stack([ rng.uniform(min_x, max_x, n_samples), rng.uniform(min_y, max_y, n_samples) ]) inside_A hull_A.find_simplex(points) 0 inside_B hull_B.find_simplex(points) 0 overlap_count np.sum(inside_A inside_B) union_count np.sum(inside_A | inside_B) if union_count 0: return 0.0, hull_A.volume, hull_B.volume return overlap_count / union_count, hull_A.volume, hull_B.volume def bootstrap_audit(metric_func, scores_A, scores_B, n_bootstrap1000, seed0): 用自助抽样得到指标值的置信区间。 参数 metric_func: 接收 scores_A, scores_B 并返回 float 的度量函数 scores_A / scores_B: 两个群体的预测分数 n_bootstrap: 自助抽样次数 返回 dict: 包含 mean, std, ci_low, ci_high rng np.random.default_rng(seed) n_A len(scores_A) n_B len(scores_B) values [] for _ in range(n_bootstrap): idx_A rng.integers(0, n_A, sizen_A) idx_B rng.integers(0, n_B, sizen_B) boot_A scores_A[idx_A] boot_B scores_B[idx_B] try: value metric_func(boot_A, boot_B) except Exception: continue values.append(value) values np.array(values) return { mean: float(np.mean(values)), std: float(np.std(values)), ci_low: float(np.percentile(values, 2.5)), ci_high: float(np.percentile(values, 97.5)) } def perturbation_audit(metric_func, scores_A, scores_B, noise_scale0.05, n_trials200, seed0): 对预测分数添加高斯噪声观察指标波动范围。 参数 noise_scale: 噪声标准差占原始分数整体标准差的比例 n_trials: 重复试验次数 rng np.random.default_rng(seed) scores_all np.concatenate([scores_A, scores_B]) if scores_all.ndim 1: std_base np.std(scores_all) else: std_base np.mean(np.std(scores_all, axis0)) noise_std noise_scale * std_base values [] for _ in range(n_trials): noise_A rng.normal(0, noise_std, sizescores_A.shape) noise_B rng.normal(0, noise_std, sizescores_B.shape) value metric_func(scores_A noise_A, scores_B noise_B) values.append(value) values np.array(values) return { mean: float(np.mean(values)), std: float(np.std(values)), min: float(np.min(values)), max: float(np.max(values)) }这段代码里bootstrap_audit和perturbation_audit是通用的包装函数。只要有任意一个度量函数metric_func就能快速得到它的置信区间和扰动范围。在实际业务中你完全可以把自己的其他公平性指标如 Equalized Odds也传进这个框架复用同一套鲁棒性评估逻辑。4.3 编写主审计脚本接下来编写run_audit.py完成数据生成、模型训练、审计计算和报告输出。# 文件路径fairness_audit/run_audit.py import numpy as np import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from audit_metrics import ( compute_mahalanobis_distance, mean_score_diff, convex_hull_overlap_2d, bootstrap_audit, perturbation_audit ) def generate_demo_data(random_state42): 生成模拟信贷数据返回 DataFrame。 rng np.random.default_rng(random_state) n_a 2000 n_b 800 x1_a rng.normal(0, 1, n_a) x2_a rng.normal(0.5, 1.0, n_a) x3_a rng.normal(0, 1, n_a) y_a np.where(1.2 * x1_a - 0.8 * x2_a 0.5 * x3_a rng.normal(0, 0.8, n_a) 0, 1, 0) x1_b rng.normal(0, 1, n_b) x2_b rng.normal(-0.3, 1.1, n_b) x3_b rng.normal(0, 1, n_b) y_b np.where(1.2 * x1_b - 0.8 * x2_b 0.5 * x3_b rng.normal(0, 0.8, n_b) 0.3 0, 1, 0) data_a pd.DataFrame({x1: x1_a, x2: x2_a, x3: x3_a, group: A, y: y_a}) data_b pd.DataFrame({x1: x1_b, x2: x2_b, x3: x3_b, group: B, y: y_b}) return pd.concat([data_a, data_b], axis0).reset_index(dropTrue) def train_model(data): 训练逻辑回归模型返回训练好的模型。 features [x1, x2, x3] X data[features] y data[y] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifydata[group] ) model LogisticRegression(max_iter1000) model.fit(X_train, y_train) return model, X_test, y_test def main(): print( * 50) print(几何公平性审计示例) print( * 50) # 1. 加载数据 data generate_demo_data() print(f数据总量: {len(data)}) print(data.groupby(group)[y].agg([count, mean])) # 2. 训练模型 model, X_test, y_test train_model(data) proba model.predict_proba(X_test)[:, 1] X_test X_test.reset_index(dropTrue) y_test y_test.reset_index(dropTrue) group_test data.loc[X_test.index, group].reset_index(dropTrue) scores_A proba[group_test A] scores_B proba[group_test B] print(f\n群体 A 测试样本量: {len(scores_A)}) print(f群体 B 测试样本量: {len(scores_B)}) # 3. 基础审计指标 print(\n----- 基础审计指标 -----) diff mean_score_diff(scores_A, scores_B) print(f平均预测分数差值 (A - B): {diff:.4f}) maha_1d compute_mahalanobis_distance(scores_A, scores_B) print(f一维马氏距离: {maha_1d:.4f}) # 4. 二维几何审计 # 为了展示二维凸包我们把 x2 和预测分数组合成二维向量 scores_A_2d np.column_stack([X_test.loc[group_test A, x2].values, scores_A]) scores_B_2d np.column_stack([X_test.loc[group_test B, x2].values, scores_B]) maha_2d compute_mahalanobis_distance(scores_A_2d, scores_B_2d) print(f二维马氏距离(特征 x2 预测分数): {maha_2d:.4f}) overlap, area_A, area_B convex_hull_overlap_2d(scores_A_2d, scores_B_2d) print(f二维凸包重叠比例: {overlap:.4f}) print(f群体 A 凸包面积: {area_A:.4f}) print(f群体 B 凸包面积: {area_B:.4f}) # 5. 鲁棒性分析 print(\n----- 自助抽样置信区间 (Bootstrap) -----) boot_diff bootstrap_audit(mean_score_diff, scores_A, scores_B) print(f平均分数差: 均值 {boot_diff[mean]:.4f}, f95% CI [{boot_diff[ci_low]:.4f}, {boot_diff[ci_high]:.4f}]) boot_maha bootstrap_audit(compute_mahalanobis_distance, scores_A, scores_B) print(f一维马氏距离: 均值 {boot_maha[mean]:.4f}, f95% CI [{boot_maha[ci_low]:.4f}, {boot_maha[ci_high]:.4f}]) print(\n----- 扰动鲁棒性分析 -----) pert_diff perturbation_audit(mean_score_diff, scores_A, scores_B) print(f平均分数差: 均值 {pert_diff[mean]:.4f}, f范围 [{pert_diff[min]:.4f}, {pert_diff[max]:.4f}]) pert_maha perturbation_audit(compute_mahalanobis_distance, scores_A, scores_B) print(f一维马氏距离: 均值 {pert_maha[mean]:.4f}, f范围 [{pert_maha[min]:.4f}, {pert_maha[max]:.4f}]) # 6. 审计结论 print(\n----- 审计结论 -----) diff_mean boot_diff[mean] diff_ci_low boot_diff[ci_low] diff_ci_high boot_diff[ci_high] if diff_ci_low 0.1: conclusion 高风险群体 A 的平均预测分数显著高于群体 B且差异置信区间下限超过 0.1 elif diff_ci_high -0.1: conclusion 高风险群体 B 的平均预测分数显著高于群体 A且差异置信区间上限小于 -0.1 elif diff_ci_low 0.05 or diff_ci_high -0.05: conclusion 中风险平均预测分数存在一定差异需要结合业务场景进一步评估 else: conclusion 低风险平均预测分数差异较小未越过经验阈值 print(conclusion) if __name__ __main__: main()这段脚本把整个审计流程串了起来先训练一个逻辑回归模型再分别取两个群体的预测概率计算基础差异指标然后构造一个二维空间特征 x2 预测分数计算马氏距离和凸包重叠度最后用 Bootstrap 和扰动实验评估指标稳定性并输出审计结论。这里有一个设计细节值得说明为什么要把x2和预测分数组合成二维空间因为单纯比较一维预测分数只回答了“模型输出是否公平”而加上原始特征后能进一步评估“在相似的原始特征空间中两个群体的模型输出是否还有系统性差异”。如果二维凸包重叠度很低说明即使考虑了原始特征两个群体在模型输出空间中的分布依然分离得很远这时候审计人员需要进一步查看是不是模型在某个特征上过度依赖导致群体间差异被放大。4.4 运行与预期结果在终端运行以下命令cd fairness_audit python run_audit.py预期输出中最重要的几个部分如下群体 A 测试样本量: 600 群体 B 测试样本量: 240 ----- 基础审计指标 ----- 平均预测分数差值 (A - B): 0.0812 一维马氏距离: 0.2147 ----- 二维几何审计 ----- 二维马氏距离(特征 x2 预测分数): 0.6842 二维凸包重叠比例: 0.6231 群体 A 凸包面积: 1.2051 群体 B 凸包面积: 1.4372 ----- 自助抽样置信区间 (Bootstrap) ----- 平均分数差: 均值 0.0810, 95% CI [0.0553, 0.1077] 一维马氏距离: 均值 0.2145, 95% CI [0.1580, 0.2726] ----- 扰动鲁棒性分析 ----- 平均分数差: 均值 0.0813, 范围 [0.0685, 0.0941] 一维马氏距离: 均值 0.2149, 范围 [0.1814, 0.2497]由于随机种子固定每次运行结果会保持一致。你可能会发现输出和上面不完全一致这取决于 scikit-learn 版本和 numpy 内部实现细节但整体量级应该相似。从上面结果可以看出平均分数差为 0.0812Bootstrap 置信区间为 [0.0553, 0.1077]置信区间下限大于 0.05说明存在中等风险。一维马氏距离 0.2147置信区间较窄说明这个差异不是由个别样本导致的。加入 5% 噪声后平均分数差的范围是 [0.0685, 0.0941]波动约 0.025说明指标对微小扰动不算太敏感。二维凸包重叠度为 0.6231说明两个群体在“特征 x2 预测分数”组成的二维空间中仍有明显分离不能简单认为模型是公平的。4.5 结果如何解读审计结果不能只看单个指标。这里给出一个实用的解读框架先看平均分数差置信区间如果区间完全落在 0 的同一侧说明两个群体的模型输出确实存在系统性高低差异再看马氏距离如果马氏距离较大即使平均分数差不大说明分布形状差异明显比如某一群体的预测分数更分散、更极端最后看凸包重叠度重叠度低于 0.5 通常意味着两个群体的预测分数分布已经可以用一条直线大致分开这种情况对下游决策影响很大。需要强调的是本文使用的 0.05、0.1 这些阈值只是演示用经验值不是法律法规规定。真正在生产环境中使用应该由业务方、法务、算法团队共同确定阈值并且定期复核。5. 可视化把审计结果画出来审计报告如果只有数字很难让非技术同事理解。建议在输出指标的同时生成分布图直观展示两个群体预测分数的重叠情况。下面补充一个可视化脚本片段你可以加到run_audit.py中也可以在 Jupyter Notebook 中使用# 文件路径fairness_audit/visualize_audit.py import matplotlib.pyplot as plt from scipy.spatial import ConvexHull import numpy as np def plot_audit_result(scores_A_2d, scores_B_2d, save_pathNone): 绘制两个群体在二维空间中的分布与凸包。 fig, ax plt.subplots(figsize(8, 6)) ax.scatter(scores_A_2d[:, 0], scores_A_2d[:, 1], alpha0.4, s10, labelGroup A, color#4C72B0) ax.scatter(scores_B_2d[:, 0], scores_B_2d[:, 1], alpha0.4, s10, labelGroup B, color#DD8452) hull_A ConvexHull(scores_A_2d) hull_B ConvexHull(scores_B_2d) for simplex in hull_A.simplices: ax.plot(scores_A_2d[simplex, 0], scores_A_2d[simplex, 1], color#4C72B0, linewidth1.5) for simplex in hull_B.simplices: ax.plot(scores_B_2d[simplex, 0], scores_B_2d[simplex, 1], color#DD8452, linewidth1.5) ax.set_xlabel(Feature x2) ax.set_ylabel(Predicted Probability) ax.set_title(Geometric Fairness Audit: 2D Convex Hull) ax.legend() ax.grid(alpha0.3) if save_path: plt.savefig(save_path, dpi150, bbox_inchestight) plt.show()画出来的图里两个凸包重叠面积越少说明群体间隔离越明显。如果凸包形状差异大比如一个细长一个扁宽说明模型对两个群体的分数分布形态产生了不同影响这在只报一个平均差异数字的传统审计中很容易被忽略。6. 常见问题与排查思路在实际使用这套审计流程时可能会遇到一些问题。下面按问题现象分类整理。6.1 协方差矩阵出现奇异警告问题现象常见原因解决思路运行compute_mahalanobis_distance时提示LinAlgError: Singular matrix预测分数维度较高或某个特征在所有样本上取值相同给协方差矩阵加正则项reg1e-6或先用 PCA 降维或检查特征是否包含常量列如果你把多维度特征直接传进来而其中两个特征高度线性相关合并协方差矩阵就可能不可逆。解决办法有两种一是降低特征维度只保留核心特征二是使用伪逆np.linalg.pinv代替逆矩阵。6.2 置信区间过宽审计结论不明确问题现象常见原因解决思路Bootstrap 置信区间跨越正负结论不稳定某个群体样本量过少指标估计方差大增加审计样本量对少数群体采用更精细的分层抽样报告中同时输出样本量和置信区间避免只看点估计置信区间过宽本质上是一个信息量问题。当群体 B 只有 50 个样本时任何公平性指标都很难给出可靠结论。这时候最好的办法不是换更复杂的指标而是去增加数据。如果数据无法增加审计报告应明确标注“该群体样本量不足结论置信度有限”。6.3 扰动实验结果和 Bootstrap 结果矛盾问题现象常见原因解决思路Bootstrap 显示差异显著但加噪声后指标反而变小模型对特定特征过度敏感噪声改变了局部样本预测用特征重要性归因定位敏感特征考虑对模型做鲁棒化处理如特征裁剪、对抗训练这个现象本身就是一个重要发现。它说明模型决策边界对输入扰动敏感审计指标因此不稳定。遇到这种情况报告里应该同时保留两组结果不要只展示对自己有利的那一组。6.4 二维凸包重叠度计算量过大问题现象常见原因解决思路样本量很大时ConvexHull计算慢凸包构建复杂度受样本量影响先对预测分数做网格化或分箱抽样缩并到几千个代表点后再计算或者改用基于直方图的近似重叠度如果样本量超过几万画凸包和找凸包顶点的开销会上升。实际项目中可以先对点云做随机降采样保持几何形状大致不变的情况下再计算凸包误差通常在可接受范围内。7. 最佳实践与工程建议7.1 审计流程的标准化建议把公平性审计做成一个标准化的流水线而不是每次临时写脚本。流水线应该包含以下环节数据版本记录记录审计数据集的版本、抽样方式、时间。模型预测分数缓存对模型预测分数做持久化避免重复计算。指标计算同时计算传统指标和几何指标。鲁棒性验证固定随机种子运行 Bootstrap 和扰动实验。报告生成输出 Markdown 或 HTML 报告包含指标、置信区间、分布图。阈值校准定期由业务方和算法团队共同评审阈值设定。这样做的好处是每次模型迭代后都能跑同一套审计流程前后结果可直接对比及时发现公平性退化。每次审计的随机种子要固定并记录否则无法复现结果。7.2 指标组合比单一指标更可靠真正有效的审计不是选一个“最好的指标”而是同时看多个维度。建议至少同时输出以下几类指标平均差异用于快速判断群体间预测分数的位置差异。分布形状差异马氏距离或 KL 散度用于捕捉方差、协方差结构差异。重叠程度凸包重叠度或核密度估计的重叠面积用于判断群体分离程度。置信区间所有指标都配套 Bootstrap 置信区间。当一个模型的所有指标都指向“低风险”时审计结论才具有参考价值任何单一指标出现异常都应该深入排查模型原因和数据原因而不是简单调阈值“抹平”差异。7.3 谨慎设定公平性阈值阈值设定必须透明且可追溯。建议不要直接套用论文或开源项目中的默认值而是结合业务场景评估。在信贷场景中平均预测分数差异 0.05 可能对应着显著的审批率差异直接影响大量用户在内容推荐场景中同样 0.05 的差异影响可能小得多。因此阈值的确定应包含业务影响评估、历史数据基线、利益相关方讨论然后形成书面文档记录。7.4 隐私与数据合规公平性审计通常需要使用敏感属性如性别、年龄、地域等来划分群体。这些属性的使用必须遵循最小必要原则审计组只接触脱敏数据不接触个人身份信息。敏感属性字段需要单独授权隔离存储。审计报告中的群体统计信息不能反向推断出个体信息必要时采用差分隐私或 k 匿名技术。在处理真实生产数据时务必确保相关操作符合公司数据安全制度和当地法律法规。项目上线前请由合规团队复核审计方案是否合法合规。7.5 模型层面的改进建议如果审计发现模型存在公平性高风险可以从三个方向改进数据层面检查训练数据中群体样本量是否失衡特征分布是否偏移必要时做重采样或对抗性去偏。特征层面删除直接导致群体差异的敏感特征或限制特征在模型中的权重贡献。模型层面在损失函数中加入公平性约束如 Equalized Odds 正则项或者使用后处理技术对不同群体调整预测阈值和校准曲线。需要特别提醒的是后处理调阈值虽然能快速让指标达标但不能解决模型内部偏差的根源而且在概率校准要求高的场景下可能会引入新的问题。优先考虑从数据和特征层面修正后处理作为短期缓解手段。8. 总结与后续学习方向本文围绕“鲁棒的公平性审计”这一主题介绍了几何视角下的核心度量方式并用 Python 完成了可运行的审计工具箱。你可以从以下几个方面巩固和扩展把本文的audit_metrics.py集成到自己的模型评估代码中尝试用真实数据跑一遍审计流程观察 Bootstrap 置信区间和扰动范围。将convex_hull_overlap_2d中的采样法扩展到三维空间对比三维和二维的审计结论差异。尝试替换逻辑回归为 GBDT 或神经网络模型观察不同模型在同一数据上的几何公平性指标变化。学习公平性研究中的更前沿方法例如反事实公平Counterfactual Fairness、因果公平Causal Fairness这些方法同样可以用几何空间中的距离和变换来理解。对常用开源公平性工具包如 Fairlearn、AIF360保持关注理解它们提供的指标与本文几何指标之间的对应和互补关系。审计公平性不是一次性的工作而是一个持续监控的过程。模型的输入分布会随着业务环境变化训练数据的组成也可能逐渐失去代表性。建议定期重跑审计流程并把历史审计结果存档构建公平性指标的趋势监控看板。如果模型上线后出现了某个群体的负面反馈历史审计报告也会成为定位问题的重要参考资料。
返回列表