写在前面
前面的篇目我们一直在测模型"准不准"、"快不快"、"稳不稳"。但 AI 模型还有两个更深层的质量问题,经常被忽视,一旦出事就是大事:
公平性(Fairness):模型是否对不同群体(性别、年龄、地域)一视同仁?
真实案例:某公司用 AI 筛选简历,发现系统给女性候选人的技术岗评分系统性偏低。原因:训练数据来自过去的招聘记录,而过去男性技术从业者更多,模型学到了"男性 = 更适合技术岗"的错误模式。
真实案例:某信贷模型对某地区的贷款拒绝率明显高于其他地区。原因:训练数据中该地区的历史违约率较高,但那是因为该地区之前遭受过经济冲击------不代表现在。
可解释性(Interpretability):模型能不能解释自己为什么这么判断?
真实场景:用户被 AI 拒绝了贷款申请,向客服询问原因。客服说"AI 判定的"------这在很多国家是不合法的(GDPR 规定用户有权获得"有意义的解释")。
真实场景:医疗 AI 说"这张 X 光片有 87% 的概率是肺炎"。医生问:"为什么?"AI 说不出------医生不敢用。
本篇目标:
- 理解模型偏见的来源和常见类型
- 掌握公平性的核心检测指标
- 实战:构建一个有偏见的模型并检测偏见
- 学会用特征重要性和 LIME 解释模型行为
环境准备
本篇需要以下 Python 库,开始前先在终端执行:
bash
pip install numpy pandas matplotlib scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple
如果觉得每次加
-i太麻烦,可以先执行一次pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple配置默认镜像,以后安装任何包都自动加速。
本篇文件结构
本篇代码放在 ai-test/ch16_fairness_explain/ 文件夹下:
| 文件名 | 用途 | 创建方式 |
|---|---|---|
bias_detection.py |
偏见模型构建与公平性检测 | 三、新建 |
model_explain.py |
模型可解释性方法演示 | 四、新建 |
fairness_report.py |
公平性与可解释性报告 | 五、新建 |
charts/ |
图片输出目录(自动生成) | 不需要手动创建 |
一、AI 偏见从哪里来?
1.1 偏见的三大来源
模型偏见不是 AI "故意"歧视谁------它只是忠实地反映了训练数据中的模式。偏见的来源有三个:
来源一:数据偏见
训练数据本身就不公平。
如果过去 10 年的招聘记录中,男性工程师占 80%,模型就会学到"男性更可能成为好工程师"------这不是事实,只是历史数据的偏差。
来源二:标注偏见
人工标注带有主观色彩。
让标注员判断"这条评论是正面还是负面",不同文化背景的标注员对同一句话的判断可能不同。如果标注团队以某个群体为主,标注结果就会偏向该群体的视角。
来源三:算法偏见
模型设计本身的缺陷。
用"准确率"作为唯一的优化目标,模型可能通过牺牲少数群体的准确率来提升整体准确率。比如:对多数群体 95% 准确、对少数群体 60% 准确,平均起来是 92%------看起来不错,但对少数群体不公平。
1.2 偏见的常见类型
| 偏见类型 | 说明 | 示例 |
|---|---|---|
| 性别偏见 | 对不同性别区别对待 | 男性简历评分 > 女性简历评分 |
| 年龄偏见 | 对不同年龄区别对待 | 老年人贷款通过率 < 年轻人 |
| 地域偏见 | 对不同地区区别对待 | 某地区用户被系统性拒绝 |
| 种族/民族偏见 | 对不同种族区别对待 | 面部识别对深肤色人群准确率低 |
| 社会经济偏见 | 对不同收入群体区别对待 | 高收入群体获得更好推荐 |
1.3 为什么测试人员要关注公平性
有人可能觉得"公平性是算法团队的事"。不对。原因:
- 你是质量守门人:模型效果好不好你看得出,模型公不公平你也应该看得出
- 法律要求越来越严:欧盟 AI Act、中国《互联网信息服务算法推荐管理规定》都对算法公平性有要求
- 品牌风险巨大:AI 歧视一旦被媒体曝光,对品牌的伤害远超一个 Bug
- 公平性需要系统化测试:不是看一眼就能发现的,需要专门的测试方法和指标
二、公平性的核心检测指标
2.1 群体公平性(Demographic Parity)
定义:不同群体获得"正面结果"的比例应该相近。
公式:P(预测=正面 | 群体A) ≈ P(预测=正面 | 群体B)
类比:一家公司招人,男女应聘者的录用比例应该差不多(在能力分布相近的前提下)。
2.2 机会均等(Equal Opportunity)
定义:在"真正应该获得正面结果"的人中,不同群体被正确识别的比例应该相近。
公式:TPR(A) ≈ TPR(B)(不同群体的召回率相近)
类比:在所有"应该被录用"的人中,男性和女性被录用的比例应该差不多。这比群体公平性更精细------它考虑了"正确答案"。
2.3 预测平等(Predictive Parity)
定义:模型给出"正面预测"时,不同群体的预测准确性应该相近。
公式:PPV(A) ≈ PPV(B)(不同群体的精确率相近)
类比:当 AI 说"这个人能还贷款"时,对男性和女性的判断准确率应该差不多。
2.4 公平性指标速查
| 指标 | 关注点 | 公式 | 判定标准 |
|---|---|---|---|
| 群体公平性 | 正面结果比例 | P(正|A) vs P(正|B) | 差异 < 10% |
| 机会均等 | 正类的召回率 | TPR(A) vs TPR(B) | 差异 < 10% |
| 预测平等 | 正类的精确率 | PPV(A) vs PPV(B) | 差异 < 10% |
| 统计均等 | 各群体各项指标 | 全面对比 | 无显著差异 |
注意 :这三个指标在数学上是不可能同时满足的(除非完美预测)。测试时选择和你的业务最相关的指标,并在报告中说明选择理由。
三、实战:偏见检测
3.1 构建一个有偏见的模型
我们故意构建一个带性别偏见的情感分类模型------训练数据中女性正面评论的比例被人为拉高到 95%,让模型学到"女性 = 正面"的错误关联。
【新建文件】 在 ai-test/ch16_fairness_explain/ 文件夹下新建 bias_detection.py,写入以下代码:
python
"""
偏见模型构建与公平性检测
实战演示如何发现 AI 模型中的群体偏见
步骤:
1. 构建一个故意带性别偏见的情感分类模型
2. 把性别作为模型特征(让模型能"看到"性别)
3. 用三大公平性指标检测偏见
4. 可视化偏见程度
"""
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from scipy.sparse import hstack, csr_matrix
import matplotlib.pyplot as plt
import matplotlib
matplotlib.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
matplotlib.rcParams["axes.unicode_minus"] = False
import os
os.makedirs("charts", exist_ok=True)
print("=" * 60)
print("偏见模型构建与公平性检测")
print("=" * 60)
np.random.seed(42)
# ========== 1. 构建带偏见的训练数据 ==========
print("\n--- 1. 构建带偏见的训练数据 ---")
positive_texts = [
"这个产品非常好用,非常满意",
"质量很好,推荐购买",
"服务态度很棒,物流也很快",
"性价比很高,值得推荐",
"超出预期,非常惊喜",
"做工精细,品质优秀",
"用了一段时间,感觉不错",
"很喜欢这个产品,会回购",
]
negative_texts = [
"太差了,质量完全不行",
"非常失望,后悔买了",
"服务态度很差,不会再买",
"质量有问题,要求退货",
"做工粗糙,不值这个价",
"和描述不符,差评",
"用了一次就坏了,垃圾",
"客服不理人,体验极差",
]
train_records = []
# 女性:190 正面 + 10 负面(正面率 95% → 强偏见来源)
for i in range(190):
train_records.append({
"text": positive_texts[i % len(positive_texts)],
"gender": "female",
"label": "positive"
})
for i in range(10):
train_records.append({
"text": negative_texts[i % len(negative_texts)],
"gender": "female",
"label": "negative"
})
# 男性:100 正面 + 100 负面(正面率 50% → 正常比例)
for i in range(100):
train_records.append({
"text": positive_texts[i % len(positive_texts)],
"gender": "male",
"label": "positive"
})
for i in range(100):
train_records.append({
"text": negative_texts[i % len(negative_texts)],
"gender": "male",
"label": "negative"
})
train_df = pd.DataFrame(train_records)
print(f" 训练数据总量:{len(train_df)} 条")
print(f"\n 训练数据的情感分布(按性别):")
print(f" {'性别':<10s} {'正面':>8s} {'负面':>8s} {'正面率':>8s}")
print(f" {'─' * 38}")
for gender in ["female", "male"]:
g = train_df[train_df["gender"] == gender]
pos = (g["label"] == "positive").sum()
neg = (g["label"] == "negative").sum()
print(f" {gender:<10s} {pos:>8d} {neg:>8d} {pos/(pos+neg)*100:>7.1f}%")
print(f"\n [注意] 女性正面率 95% 远高于男性 50%------这就是偏见的来源!")
3.2 训练模型
【追加到 bias_detection.py】 在上一段代码末尾继续添加:
python
# ========== 2. 训练模型(性别作为特征) ==========
print(f"\n--- 2. 训练模型(性别作为特征) ---")
# 文本特征
vectorizer = CountVectorizer(analyzer="char_wb", ngram_range=(1, 2))
X_text = vectorizer.fit_transform(train_df["text"])
# 性别特征(female=1, male=0)
X_gender = csr_matrix(
(train_df["gender"] == "female").astype(int).values.reshape(-1, 1)
)
# 合并特征:文本 + 性别
X_combined = hstack([X_text, X_gender])
y_train = train_df["label"]
model = LogisticRegression(max_iter=1000)
model.fit(X_combined, y_train)
# 查看性别特征的权重
gender_weight = model.coef_[0][-1] # 最后一个特征是性别
print(f" 模型训练完成(逻辑回归)")
print(f" 文本特征数:{X_text.shape[1]}")
print(f" 性别特征权重:{gender_weight:+.4f}")
if gender_weight > 0:
print(f" [偏见] 正权重 → 模型学到'女性 = 更可能正面'的错误关联")
else:
print(f" 性别权重为负,偏见方向为男性正面")
def predict_sentiment(text, gender):
"""预测文本情感(性别作为模型输入)"""
X_text = vectorizer.transform([text])
X_gender = csr_matrix([[1 if gender == "female" else 0]])
X_combined = hstack([X_text, X_gender])
label = model.predict(X_combined)[0]
proba = model.predict_proba(X_combined)[0]
confidence = float(max(proba))
return label, confidence
3.3 构建平衡测试集
【追加到 bias_detection.py】 在上一段代码末尾继续添加:
python
# ========== 3. 构建平衡测试集 ==========
print(f"\n--- 3. 公平性检测 ---")
# 测试文本池(与训练文本不同,考察泛化能力)
# 正面 50 条
test_texts_positive = [
"这款商品真的很棒", "物超所值,非常满意", "物流很快,包装精美",
"质量过硬,值得拥有", "用着很顺手,好评", "外观漂亮,功能强大",
"客服耐心,解决问题快", "第二次购买,一如既往的好", "朋友推荐来的,果然不错",
"超出预期的好,值得推荐", "性价比之王,闭眼入", "用了半个月,越用越喜欢",
"家人也很满意,好评", "细节做得很好,点赞", "发货速度快,服务好",
"质感超好,超出预期", "孩子很喜欢,值得购买", "回购第三次了,稳定输出",
"包装严实,东西完美", "试了一次就爱上了", "声音清晰,续航给力",
"操作简单,老人也会用", "颜值高,性能强", "价格实惠,品质不打折",
"手感舒适,做工一流", "设计感很强,很有档次", "实用性强,好评",
"对比了好几家,这家最好", "送礼首选,很有面子", "食材新鲜,味道很好",
"穿上很舒服,尺码标准", "颜色好看,和图片一致", "安装方便,说明书清楚",
"效果很明显,超出想象", "清洗方便,省时省力", "功能齐全,满足需求",
"用了就回不去了", "各方面都很满意", "一次愉快的购物体验",
"推荐给了同事,都说好", "很惊喜,物有所值", "做工精致,非常好看",
"客服回复很快,态度好", "快递小哥很给力", "质量对得起价格",
"非常实用,不占地方", "安静省电,效果好", "已经是老客户了",
"信赖这个品牌", "真心不错,会继续支持",
]
# 负面 50 条
test_texts_negative = [
"质量太差了,完全不行", "等了半个月才到,还坏了", "客服态度恶劣,投诉无门",
"和图片完全不一样,差评", "用了两天就坏了,退货", "价格虚高,不值这个钱",
"做工很粗糙,不推荐", "味道很大,疑似假货", "尺寸不对,退换很麻烦",
"再也不买了,太失望", "包装破损,里面东西坏了", "功能缺失,和描述不符",
"噪音很大,影响休息", "掉色严重,穿了一次就扔了", "电池不耐用,半天就没电",
"客服踢皮球,问题没解决", "发货太慢,等了一个月", "漏发配件,态度敷衍",
"用料很差,一碰就碎", "气味刺鼻,不敢给孩子用", "颜色暗淡,和图片差远了",
"接口松动,接触不良", "屏幕有坏点,申请换货被拒", "软件卡顿,体验极差",
"虚假宣传,实际功能没有", "配件缺失,无法使用", "售后推诿,维权困难",
"性价比极低,同价位有更好的", "清洁不干净,越洗越脏", "变形了,质量堪忧",
"说明书看不懂,安装困难", "跑电很快,充满用不了多久", "异响严重,像拖拉机",
"表面划痕,像二手货", "重量太沉,不方便携带", "散热很差,烫手",
"按键失灵,反应迟钝", "防水不行,淋雨就坏", "无线距离很短,隔墙就没信号",
"粘不牢,掉下来摔碎了", "面料起球,洗一次就变形", "客服机器人,没人解决问题",
"承诺的赠品没发", "尺码偏小,穿着很紧", "有安全隐患,不敢用",
"退款流程极其复杂", "断断续续,连接不稳定", "音质很差,杂音很大",
"体积太大,家里放不下", "第一次遇到这么差的产品",
]
# 构建 200 条测试集:男女各 100 条(50 正面 + 50 负面)
test_records = []
for text in test_texts_positive:
test_records.append({"text": text, "gender": "female", "true_label": "positive"})
for text in test_texts_negative:
test_records.append({"text": text, "gender": "female", "true_label": "negative"})
for text in test_texts_positive:
test_records.append({"text": text, "gender": "male", "true_label": "positive"})
for text in test_texts_negative:
test_records.append({"text": text, "gender": "male", "true_label": "negative"})
test_df = pd.DataFrame(test_records)
# 预测(注意:传入了 gender 参数!)
test_df["predicted"] = test_df.apply(
lambda row: predict_sentiment(row["text"], row["gender"])[0], axis=1
)
test_df["confidence"] = test_df.apply(
lambda row: predict_sentiment(row["text"], row["gender"])[1], axis=1
)
print(f" 测试集:{len(test_df)} 条(男女各 100 条,正面/负面各 50 条)")
# 快速验证:同一文本不同性别是否预测不同
print(f"\n 快速验证(同一文本、不同性别):")
diff_count = 0
for text in test_texts_negative[:10]:
f_label, _ = predict_sentiment(text, "female")
m_label, _ = predict_sentiment(text, "male")
if f_label != m_label:
diff_count += 1
print(f" 随机取 10 条负面文本,男女预测不同的有 {diff_count} 条")
if diff_count > 0:
print(f" [偏见] 同一文本,不同性别预测不同!")
else:
print(f" 这 10 条预测一致,偏见可能体现在边界样本上")
3.4 三大公平性指标计算
【追加到 bias_detection.py】 在上一段代码末尾继续添加:
python
# ========== 4. 计算三大公平性指标 ==========
print(f"\n--- 4. 公平性指标计算 ---")
results = {}
for gender in ["female", "male"]:
group = test_df[test_df["gender"] == gender]
total = len(group)
pos_predicted = (group["predicted"] == "positive").sum()
# 群体公平性:正面预测比例
demographic_parity = pos_predicted / total
# 机会均等(TPR):真实正面中被正确预测为正面的比例
true_positives = group[group["true_label"] == "positive"]
tp = (true_positives["predicted"] == "positive").sum()
tpr = tp / len(true_positives) if len(true_positives) > 0 else 0
# 预测平等(PPV):预测正面中真实正面的比例
predicted_positives = group[group["predicted"] == "positive"]
ppv = tp / len(predicted_positives) if len(predicted_positives) > 0 else 0
results[gender] = {
"total": total,
"pos_predicted": pos_predicted,
"demographic_parity": demographic_parity,
"tpr": tpr,
"ppv": ppv,
}
print(f"\n {'指标':<18s} {'女性':>10s} {'男性':>10s} {'差异':>10s} {'判定':>10s}")
print(f" {'─' * 62}")
for metric_name, key in [
("正面预测比例", "demographic_parity"),
("召回率(TPR)", "tpr"),
("精确率(PPV)", "ppv"),
]:
f_val = results["female"][key]
m_val = results["male"][key]
diff = abs(f_val - m_val)
verdict = "[PASS]" if diff < 0.1 else ("[WARN]" if diff < 0.2 else "[FAIL]")
print(f" {metric_name:<18s} {f_val:>9.1%} {m_val:>9.1%} {diff:>9.1%} {verdict:>10s}")
3.5 公平性可视化与结论
【追加到 bias_detection.py】 在上一段代码末尾继续添加:
python
# ========== 5. 公平性可视化 ==========
print(f"\n--- 5. 公平性可视化 ---")
metrics = [
("群体公平性\n(正面预测比例)", "demographic_parity"),
("机会均等\n(召回率 TPR)", "tpr"),
("预测平等\n(精确率 PPV)", "ppv"),
]
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
for idx, (title, key) in enumerate(metrics):
ax = axes[idx]
# 从 results 中动态获取真实指标值
f_val = results["female"][key]
m_val = results["male"][key]
bars = ax.bar(["女性", "男性"], [f_val, m_val],
color=["#E74C3C", "#3498DB"],
edgecolor="gray", alpha=0.8, width=0.5)
avg = (f_val + m_val) / 2
ax.axhline(y=avg, color="green", linestyle="--", linewidth=1.5, alpha=0.5,
label=f"公平目标 (均值 {avg:.1%})")
for bar, val in zip(bars, [f_val, m_val]):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.02,
f"{val:.1%}", ha="center", va="bottom",
fontsize=14, fontweight="bold")
diff = abs(f_val - m_val)
verdict = "公平" if diff < 0.1 else ("轻微偏见" if diff < 0.2 else "显著偏见")
ax.set_title(f"{title}\n差异 {diff:.1%} → {verdict}",
fontsize=12, fontweight="bold", pad=20)
ax.set_ylim(0, 1.2)
ax.legend(loc="lower left", fontsize=10)
ax.grid(True, alpha=0.3, axis="y")
plt.suptitle("公平性检测结果(女性 vs 男性)",
fontsize=16, fontweight="bold", y=1.02)
plt.tight_layout()
plt.savefig("charts/fairness_detection.png", dpi=150, bbox_inches="tight")
plt.close()
print(f" 公平性检测图已保存到 charts/fairness_detection.png")
# ========== 6. 动态生成结论(和数据完全一致) ==========
print(f"\n--- 6. 结论 ---")
# 根据实际数据判断偏见方向
f_dp = results["female"]["demographic_parity"]
m_dp = results["male"]["demographic_parity"]
dp_diff = f_dp - m_dp # 正数 = 女性偏高
max_abs_diff = max(
abs(results["female"][k] - results["male"][k])
for k in ["demographic_parity", "tpr", "ppv"]
)
print(f"\n 核心发现:")
if dp_diff > 0.1:
print(f" 女性的正面预测比例比男性高 {dp_diff:.1%}。")
print(f" 这不是因为女性的评论确实更好(测试集完全平衡,男女各 50 正 50 负),")
print(f" 而是训练数据中'女性=正面'的偏见被模型学到了。")
print(f" 性别特征权重 = {gender_weight:+.4f}(正值 = 女性偏正面)")
print(f" 同时注意 PPV 的反向差异------女性预测正面虽多,但准确率更低(假阳性更多),")
print(f" 这正是'过度宽容'的典型后果。")
elif dp_diff < -0.1:
print(f" 男性的正面预测比例比女性高 {abs(dp_diff):.1%}。")
print(f" 偏见方向与预期相反,可能需要检查训练数据。")
elif max_abs_diff >= 0.1:
print(f" 正面预测比例差异 {abs(dp_diff):.1%},但 TPR 或 PPV 存在明显差异。")
print(f" 这属于结构性偏见,需要逐项分析。")
else:
print(f" 三项指标差异均 < 10%,男女表现基本一致。")
print(f" 可能原因:偏见较弱、测试样本不足、或模型未充分利用性别特征。")
print(f"\n 从图上能看到:")
print(f" 1. 三张图分别是群体公平性、机会均等、预测平等")
print(f" 2. 红柱 = 女性、蓝柱 = 男性,高度差异越大偏见越严重")
print(f" 3. 绿色虚线是公平目标(两组均值)")
print(f" 4. 标题中的差异百分比和判定直接反映偏见程度")
print(f"\n 测试人员的行动:")
print(f" 1. 如果差异 > 10%,在测试报告中标记公平性 [WARN] 或 [FAIL]")
print(f" 2. 反馈给算法团队:训练数据中女性正面率偏高(95% vs 50%)")
print(f" 3. 建议重新平衡训练数据,或使用去偏见算法(如对抗训练)")
print(f" 4. 上线后定期复测,监控公平性指标变化")
【运行】 在终端执行:
bash
python bias_detection.py
运行后你会看到三项公平性指标的结果。由于训练数据中女性正面率高达 95%(男性仅 50%),而测试集完全平衡(男女各 50 正 50 负),模型对女性的预测明显偏向正面------群体公平性 (女性正面预测比例 68% vs 男性 54%)和机会均等 (女性 TPR 92% vs 男性 84%)都显示女性偏高。这正说明训练数据的偏见被模型学到了。但注意预测平等(PPV)恰好相反------女性虽然被预测正面更多,但其中假阳性也更多,导致精确率反而低于男性。这种"过度宽容"的典型后果更值得警惕:表面上对女性有利,实际上降低了女性预测的可信度。
这就是公平性检测的价值:不看模型"准不准",看模型"公不公平"。而且单一指标会骗你------必须三个指标一起看。
四、模型可解释性 ------ AI 为什么这么判断?
发现偏见之后,你需要向算法团队或产品经理解释模型的行为。可解释性方法就是帮你做到这一点的工具。
4.1 可解释性解决什么问题
产品经理问:"为什么这条评论被判成了负面?"
黑箱回答:"模型就是这么预测的。"
可解释性回答:"因为评论中出现了'差'和'不行'两个关键词,其中'差'的贡献权重是 0.35,'不行'是 0.28。模型主要根据这两个词判断为负面。"
可解释性让你从"AI 说了算"变成"AI 告诉你为什么"。
4.2 方法一:特征重要性 ------ 哪些特征影响最大
最简单直接的解释方法:看模型对每个特征(词/字符)的权重。
对于朴素贝叶斯模型,每个词在每个类别下都有一个"对数概率"。但直接看对数概率没有意义(全是负数),有意义的是区分度 :这个词在类别 A 下的对数概率减去在其他类别下的平均对数概率。区分度越大,说明这个词越能代表类别 A。
打个比方:班里同学的平均身高是 170cm,篮球队员平均身高 190cm。"比班里平均高 20cm"就是篮球队员的"区分度"。同样,"差"在负面评论中的出现频率远高于正面和中性------所以它是负面类别的强区分特征。
【新建文件】 在 ai-test/ch16_fairness_explain/ 文件夹下新建 model_explain.py,写入以下代码:
python
"""
模型可解释性方法演示
用特征重要性和词级贡献分析解释模型的预测
方法:
1. 特征重要性:看模型对每个词的权重(相对值,不是原始 log prob)
2. 词级贡献分析:看具体预测中每个词的贡献(正值推高、负值拉低)
"""
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
import matplotlib.pyplot as plt
import matplotlib
matplotlib.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
matplotlib.rcParams["axes.unicode_minus"] = False
import os
os.makedirs("charts", exist_ok=True)
print("=" * 60)
print("模型可解释性方法演示")
print("=" * 60)
np.random.seed(42)
# ========== 1. 构建模型 ==========
print("\n--- 1. 构建情感分类模型 ---")
train_texts = [
"这个产品非常好用,非常满意",
"质量很好,推荐购买",
"服务态度很棒,物流也很快",
"性价比很高,值得推荐",
"超出预期,非常惊喜",
"做工精细,品质优秀",
"用了一段时间,感觉不错",
"很喜欢这个产品,会回购",
"太差了,质量完全不行",
"非常失望,后悔买了",
"服务态度很差,不会再买",
"质量有问题,要求退货",
"做工粗糙,不值这个价",
"和描述不符,差评",
"用了一次就坏了,垃圾",
"客服不理人,体验极差",
"一般般,没什么特别的",
"凑合能用,不算好也不算差",
"中规中矩,还行吧",
"价格对得起这个品质",
]
train_labels = ["positive"] * 8 + ["negative"] * 8 + ["neutral"] * 4
vectorizer = CountVectorizer(analyzer="char_wb", ngram_range=(1, 2))
X_train = vectorizer.fit_transform(train_texts)
model = MultinomialNB()
model.fit(X_train, train_labels)
print(f" 训练完成,特征数(字符 n-gram):{len(vectorizer.vocabulary_)}")
# ========== 2. 特征重要性分析 ==========
print(f"\n--- 2. 特征重要性分析 ---")
print(f" 计算每个特征在各类别下的区分度")
print(f" 区分度 = 该特征在某类别的 log 概率 - 在其他类别的平均 log 概率")
print(f" 正值越大 → 该特征越能代表这个类别")
print()
feature_names = vectorizer.get_feature_names_out()
def get_feature_discriminability(class_idx):
"""
计算每个特征对指定类别的区分度
区分度 = log P(feat|该类别) - mean(log P(feat|其他类别))
正值:该特征在这个类别下比其他类别更常见
负值:该特征在这个类别下比其他类别更少见
"""
n_classes = len(model.classes_)
log_probs = model.feature_log_prob_
# 该类别下的 log 概率
log_prob_this = log_probs[class_idx]
# 其他类别的平均 log 概率
other_indices = [i for i in range(n_classes) if i != class_idx]
log_prob_others = np.mean([log_probs[i] for i in other_indices], axis=0)
return log_prob_this - log_prob_others
for class_idx, class_label in enumerate(model.classes_):
discrims = get_feature_discriminability(class_idx)
top_indices = np.argsort(discrims)[-10:][::-1]
top_features = [(feature_names[i], discrims[i]) for i in top_indices]
print(f" [{class_label}] 最有代表性的 10 个字符特征:")
for rank, (feat, score) in enumerate(top_features, 1):
bar = "█" * int(score * 20) if score > 0 else ""
print(f" {rank:>2d}. '{feat}' 区分度={score:+.3f} {bar}")
print()
4.3 词级贡献分析 ------ 单条预测的解释
知道了哪些特征整体上重要还不够。你需要解释的是单条预测:为什么"这个产品质量很差"被判成了某一类?
核心原理:
贡献值 = (特征在预测类别的权重 − 在其他类别下的平均权重)× 出现次数
正值 → 这个特征支持预测为当前类别
负值 → 这个特征反对预测(拉向其他类别)
就像法庭判案:"产品"、"很"这些词是"证人A"(支持正面判决),"差"这个词是"证人B"(反对正面判决)。最终判决取决于哪边证据更强。
【追加到 model_explain.py】 在上一段代码末尾继续添加:
python
# ========== 3. 词级贡献分析 ==========
print(f"--- 3. 词级贡献分析 ---")
print(f" 解释单条预测:看每个特征是推高还是拉低预测结果")
print(f" 贡献值 = (该特征在预测类别的权重 - 在其他类别的平均权重) x 出现次数")
print(f" 正值 → 推向预测类别,负值 → 拉向其他类别")
print()
def explain_prediction(text, top_k=10):
"""
解释一条预测中每个字符特征的贡献
核心原理:
相对贡献 = (log P(feat|预测类别) - mean(log P(feat|其他类别))) x 出现次数
正值 → 这个特征支持预测为当前类别
负值 → 这个特征反对预测(拉向其他类别)
"""
X = vectorizer.transform([text])
label = model.predict(X)[0]
proba = model.predict_proba(X)[0]
predicted_class_idx = list(model.classes_).index(label)
n_classes = len(model.classes_)
feature_indices = X.nonzero()[1]
contributions = []
for feat_idx in feature_indices:
feat_name = feature_names[feat_idx]
count = X[0, feat_idx]
# 该特征在预测类别下的 log 概率
log_prob_pred = model.feature_log_prob_[predicted_class_idx][feat_idx]
# 在其他类别下的平均 log 概率
other_indices = [i for i in range(n_classes) if i != predicted_class_idx]
log_prob_others = np.mean(
[model.feature_log_prob_[i][feat_idx] for i in other_indices], axis=0
)
# 相对贡献:正值推高预测类别,负值拉低
contribution = (log_prob_pred - log_prob_others) * count
contributions.append((feat_name, contribution, count))
# 按贡献绝对值排序
contributions.sort(key=lambda x: abs(x[1]), reverse=True)
return label, proba, contributions[:top_k]
# 解释几条示例预测
explanation_cases = [
"这个产品质量很差",
"非常满意推荐购买",
"服务态度很垃圾",
]
for text in explanation_cases:
label, proba, contributions = explain_prediction(text)
print(f" 输入:'{text}'")
print(f" 预测:{label}")
print(f" 各类别概率:")
for cls, p in zip(model.classes_, proba):
bar = "█" * int(p * 30)
print(f" {cls:<10s} {p:.4f} {bar}")
print(f" 贡献最大的 {len(contributions)} 个特征:")
for feat, contrib, count in contributions:
bar = "█" * int(abs(contrib) * 15)
if contrib > 0:
direction = "→推高'" + label + "'"
else:
direction = "→拉向其他类别"
print(f" '{feat}' 贡献={contrib:+.4f} (出现{count}次) {bar} {direction}")
print()
4.4 可视化特征贡献
文字分析看数字可能不够直观。画一张图,让贡献值一目了然。
【追加到 model_explain.py】 在上一段代码末尾继续添加:
python
# ========== 4. 特征贡献可视化 ==========
print(f"--- 4. 特征贡献可视化 ---")
test_text = "这个产品质量很差"
label, proba, contributions = explain_prediction(test_text, top_k=10)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# 左图:各类别概率
bars1 = ax1.bar(model.classes_, proba,
color=["#2ECC71", "#E74C3C", "#95A5A6"],
edgecolor="gray", alpha=0.8, width=0.5)
for bar, p in zip(bars1, proba):
ax1.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01,
f"{p:.2%}", ha="center", va="bottom", fontsize=12, fontweight="bold")
ax1.set_title(f"预测结果:{label}\n输入:'{test_text}'", fontsize=12, fontweight="bold")
ax1.set_ylabel("概率", fontsize=11)
ax1.set_ylim(0, 1.15)
ax1.grid(True, alpha=0.3, axis="y")
# 右图:特征贡献
feat_names = [c[0] for c in contributions]
feat_scores = [c[1] for c in contributions]
colors_bar = ["#E74C3C" if s > 0 else "#2ECC71" for s in feat_scores]
bars2 = ax2.barh(feat_names, feat_scores, color=colors_bar,
edgecolor="gray", alpha=0.8)
ax2.set_xlabel(f"贡献值(正 = 推高'{label}',负 = 拉向其他类别)", fontsize=10)
ax2.set_title(f"特征贡献分析\n(对 '{label}' 预测的贡献)", fontsize=12, fontweight="bold")
ax2.axvline(x=0, color="black", linewidth=1.5, linestyle="--")
ax2.grid(True, alpha=0.3, axis="x")
plt.tight_layout()
plt.savefig("charts/model_explain.png", dpi=150, bbox_inches="tight")
plt.close()
print(f" 可解释性分析图已保存到 charts/model_explain.png")
【运行】 在终端执行:
bash
python model_explain.py
运行后依次输出四个部分的结果:
第 2 部分------特征重要性:
[negative]的标志特征是"差"、"了"、"不"------这些都是负面评论中高频出现的字符
[neutral]的标志特征是"中"、"不算"、"般"------中性评论的典型用语
[positive]的标志特征是"很"、"产"、"推荐"------正面评论的典型用语
区分度都是正数(约 +0.6 到 +1.3),正值越大说明这个特征越"专属于"当前类别。
第 3 部分------词级贡献分析:
第一条"这个产品质量很差":
预测结果:positive(99%)------模型判错了!
但看贡献值就明白了:
- "产"、"品"、"很"的贡献是正的(+0.9 到 +1.3),推高 positive
- "差"的贡献是负的(−1.3),拉向 negative
- 正贡献的特征数量多、权重高,压过了负贡献
第二条"非常满意推荐购买":所有特征贡献都是正的("推"、"推荐"、"非常"、"满意"),模型非常确信是正面------这次判对了。
第三条"服务态度很垃圾":预测 negative(92%)。"垃圾"、"服"的贡献推高 negative,"很"因为和正面训练数据关联,反而拉向 positive------但不足以翻转预测。这次也判对了。
第 4 部分------可视化:
生成 charts/model_explain.png,包含两张图:
- 左图(预测概率):三个类别的概率分布。positive 99%,一目了然
- 右图(特征贡献) :水平柱状图。红色柱子向右 = 支持预测为 positive 的特征;绿色柱子向左 = 反对预测、拉向其他类别的特征。黑色虚线是零线(分界线)
从右图你能直观看到:"产"、"品"、"很"的红色柱子很长 (正贡献大),而"差"的绿色柱子虽然也不短(负贡献 −1.3),但数量太少------红色的总力量压过了绿色的总力量,所以模型最终判了 positive。
4.5 用可解释性发现模型错误
注意第一条示例预测:"这个产品质量很差 "------模型预测成了 positive (99%)。这是模型判断错误!正常人都知道这是差评。
但可解释性分析告诉了你它为什么错:
- "产"、"品"、"很"这些字符的贡献是正的(推高 positive)------因为训练数据中"这个产品非常好用"、"质量很好推荐购买"这些正面样本让模型学到了"产品"、"很"等字符和正面的关联
- "差"的贡献是负的(拉向 negative)------它确实在对抗 positive
- 但正贡献太强、负贡献太弱,模型最终还是判了 positive
这就是可解释性的实战价值:
不只是解释"模型为什么对",更要解释"模型为什么错"。
当你向算法团队反馈这个案例时,你可以说:
"模型把'质量很差'判成了正面。可解释性分析显示,'产品'和'很'的正向贡献压过了'差'的负向贡献。根本原因是训练数据中正面样本大量包含'产品''很'等字,导致这些字的正向权重过高。建议扩充训练数据,让负面样本中也包含更多'产品'相关词汇。"
这就是你能提供给产品经理和算法团队的"解释"------不再是"AI 判定的",而是"因为出现了这些关键词,它们的权重是这些,而且这里模型犯错了,原因是这个"。
五、公平性与可解释性报告
把公平性检测和可解释性分析组合成一份完整报告。
【新建文件】 在 ai-test/ch16_fairness_explain/ 文件夹下新建 fairness_report.py,写入以下代码:
python
"""
公平性与可解释性报告
组合公平性检测 + 可解释性分析,输出综合报告
模型与 bias_detection.py 保持一致:
性别作为模型特征传入(而不是只做分组统计),
这样模型能"看到"性别,训练数据中的偏见才能被检测到。
"""
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from scipy.sparse import hstack, csr_matrix
from datetime import datetime
print("=" * 60)
print("公平性与可解释性报告")
print("=" * 60)
np.random.seed(42)
# ========== 1. 构建模型(带偏见,性别作为特征) ==========
positive_texts = [
"这个产品非常好用", "质量很好推荐", "服务态度很棒",
"性价比很高值得买", "超出预期惊喜", "做工精细品质好",
"感觉不错会回购", "很喜欢这个产品",
]
negative_texts = [
"太差了完全不行", "非常失望后悔", "服务态度很差",
"质量有问题退货", "做工粗糙不值", "和描述不符差评",
"用了一次就坏了", "客服不理人体验差",
]
# 训练数据(故意埋入偏见)
# 女性:正面率 95%(190 正 + 10 负)
# 男性:正面率 50%(100 正 + 100 负)
train_records = []
for text in positive_texts:
for _ in range(24): # 8 * 24 ≈ 190 条正面
train_records.append({"text": text, "gender": "female", "label": "positive"})
for text in negative_texts:
for _ in range(1): # 8 * 1 ≈ 10 条负面(取 8 条 + 之前多出的)
train_records.append({"text": text, "gender": "female", "label": "negative"})
# 补足到正好 10 条负面
for text in negative_texts[:2]:
train_records.append({"text": text, "gender": "female", "label": "negative"})
for text in positive_texts:
for _ in range(13): # 8 * 13 ≈ 100 条正面
train_records.append({"text": text, "gender": "male", "label": "positive"})
for text in negative_texts:
for _ in range(13): # 8 * 13 ≈ 100 条负面
train_records.append({"text": text, "gender": "male", "label": "negative"})
train_df = pd.DataFrame(train_records)
# 文本特征
vectorizer = CountVectorizer(analyzer="char_wb", ngram_range=(1, 2))
X_text = vectorizer.fit_transform(train_df["text"])
# 性别特征(female=1, male=0)
X_gender = csr_matrix(
(train_df["gender"] == "female").astype(int).values.reshape(-1, 1)
)
# 合并特征
X_combined = hstack([X_text, X_gender])
y_train = train_df["label"]
model = LogisticRegression(max_iter=1000)
model.fit(X_combined, y_train)
gender_weight = model.coef_[0][-1]
print(f"\n 模型训练完成(逻辑回归)")
print(f" 性别特征权重:{gender_weight:+.4f}")
def predict(text, gender):
"""预测文本情感(性别作为模型输入)"""
X_text = vectorizer.transform([text])
X_gender = csr_matrix([[1 if gender == "female" else 0]])
X_combined = hstack([X_text, X_gender])
return model.predict(X_combined)[0]
# ========== 2. 公平性检测 ==========
print(f"\n--- 2. 公平性检测 ---")
# 测试文本(与训练文本不同,50 正面 + 50 负面)
test_positive = [
"这款商品真的很棒", "物超所值非常满意", "物流很快包装精美",
"质量过硬值得拥有", "用着很顺手好评", "外观漂亮功能强大",
"客服耐心解决快", "第二次购买依旧好", "朋友推荐果然不错", "超出预期值得推荐",
"性价比之王闭眼入", "越用越喜欢", "家人很满意", "细节很好点赞",
"发货速度快服务好", "质感超好", "孩子很喜欢", "回购第三次了",
"包装严实完美", "试了一次就爱上", "声音清晰续航好", "操作简单",
"颜值高性能强", "价格实惠品质好", "手感舒适做工好", "设计感强",
"实用性强", "对比后这家最好", "送礼有面子", "食材新鲜味道好",
"穿上舒服尺码标准", "颜色好看一致", "安装方便", "效果明显",
"清洗方便", "功能齐全", "用了回不去", "各方面满意",
"购物体验愉快", "推荐给同事都说好", "很惊喜物有所值", "做工精致好看",
"客服态度好", "快递给力", "质量对得起价格", "不占地方",
"安静省电", "老客户了", "信赖品牌", "真心不错",
]
test_negative = [
"质量太差完全不行", "等了半个月还坏了", "客服恶劣投诉无门",
"和图片不一样差评", "用两天就坏了", "价格虚高不值",
"粗糙不推荐", "味道大疑似假货", "尺寸不对退换麻烦",
"再也不买太失望", "包装破损东西坏了", "功能缺失不符",
"噪音大影响休息", "掉色严重扔了", "电池不耐用",
"客服踢皮球", "发货太慢一个月", "漏发配件敷衍",
"用料差一碰碎", "气味刺鼻", "颜色暗淡差远了",
"接口松动", "屏幕坏点换货被拒", "卡顿体验差",
"虚假宣传", "配件缺失无法用", "售后推诿维权难",
"性价比极低", "清洁不干净", "变形质量堪忧",
"说明书看不懂", "跑电快", "异响像拖拉机",
"划痕像二手", "太沉不便携", "散热差烫手",
"按键失灵", "防水不行", "隔墙没信号",
"粘不牢摔碎了", "起球变形", "客服机器人没人理",
"赠品没发", "尺码偏小", "安全隐患",
"退款复杂", "连接不稳定", "音质差杂音大",
"体积太大", "第一次这么差",
]
test_records = []
for text in test_positive:
test_records.append({"text": text, "gender": "female", "true_label": "positive"})
test_records.append({"text": text, "gender": "male", "true_label": "positive"})
for text in test_negative:
test_records.append({"text": text, "gender": "female", "true_label": "negative"})
test_records.append({"text": text, "gender": "male", "true_label": "negative"})
test_df = pd.DataFrame(test_records)
test_df["predicted"] = test_df.apply(
lambda row: predict(row["text"], row["gender"]), axis=1
)
print(f" 测试集:{len(test_df)} 条(男女各 {len(test_df)//2} 条)")
# ========== 3. 可解释性分析 ==========
print(f"\n--- 3. 可解释性分析 ---")
feature_names = vectorizer.get_feature_names_out()
def get_top_features(class_label, k=5):
"""获取某个类别下权重最高的文本特征(排除性别特征)"""
# 二分类时 coef_ 只有 1 行
# 正系数 → 推向 classes_[1],负系数 → 推向 classes_[0]
coefs = model.coef_[0]
# 只取文本特征部分,排除最后一个(性别特征)
text_coefs = coefs[:len(feature_names)]
# 对于 classes_[1](positive),系数越大越有代表性
# 对于 classes_[0](negative),系数越小(越负)越有代表性,所以取反
if class_label == model.classes_[1]:
scores = text_coefs
else:
scores = -text_coefs
top_indices = np.argsort(scores)[-k:][::-1]
return [(feature_names[i], text_coefs[i]) for i in top_indices]
# ========== 4. 输出报告 ==========
lines = []
def add(text=""):
lines.append(text)
add("=" * 60)
add("AI 模型公平性与可解释性报告")
add("=" * 60)
add(f"")
add(f" 报告时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
add(f" 被测模型:情感分类模型(逻辑回归,文本+性别特征)")
add(f" 测试目的:检测性别偏见 + 提供可解释性分析")
# 公平性部分
add(f"\n{'─' * 60}")
add(f"一、公平性检测")
add(f"{'─' * 60}")
fairness_results = {}
for gender in ["female", "male"]:
group = test_df[test_df["gender"] == gender]
pos_pred_rate = (group["predicted"] == "positive").sum() / len(group)
true_pos = group[group["true_label"] == "positive"]
tp = (true_pos["predicted"] == "positive").sum()
tpr = tp / len(true_pos) if len(true_pos) > 0 else 0
pred_pos = group[group["predicted"] == "positive"]
ppv = tp / len(pred_pos) if len(pred_pos) > 0 else 0
fairness_results[gender] = {
"demographic_parity": pos_pred_rate,
"tpr": tpr,
"ppv": ppv,
}
add(f"\n {'指标':<18s} {'女性':>10s} {'男性':>10s} {'差异':>10s} {'判定':>10s}")
add(f" {'─' * 62}")
for metric_name, key in [
("正面预测比例", "demographic_parity"),
("召回率(TPR)", "tpr"),
("精确率(PPV)", "ppv"),
]:
f_val = fairness_results["female"][key]
m_val = fairness_results["male"][key]
diff = abs(f_val - m_val)
verdict = "[PASS]" if diff < 0.1 else ("[WARN]" if diff < 0.2 else "[FAIL]")
add(f" {metric_name:<18s} {f_val:>9.1%} {m_val:>9.1%} {diff:>9.1%} {verdict:>10s}")
# 公平性判定
max_diff = max(
abs(fairness_results["female"][k] - fairness_results["male"][k])
for k in ["demographic_parity", "tpr", "ppv"]
)
add(f"\n 公平性判定:")
if max_diff < 0.1:
add(f" [PASS] 三项指标差异均 < 10%,模型较公平")
elif max_diff < 0.2:
add(f" [WARN] 存在轻微偏见(最大差异 {max_diff:.1%}),建议关注")
else:
add(f" [FAIL] 存在显著偏见(最大差异 {max_diff:.1%}),需要修复")
add(f" 建议:重新平衡训练数据 或 使用去偏见算法")
# 可解释性部分
add(f"\n{'─' * 60}")
add(f"二、可解释性分析")
add(f"{'─' * 60}")
add(f"\n 性别特征权重:{gender_weight:+.4f}")
if gender_weight > 0:
add(f" [偏见] 正权重说明模型认为'女性 = 更可能正面'")
add(f"\n 各类别最有代表性的文本特征:")
for class_label in model.classes_:
top_features = get_top_features(class_label, k=5)
add(f"\n [{class_label}]")
for feat, score in top_features:
add(f" '{feat}' 权重={score:+.4f}")
add(f"\n 可解释性判定:")
add(f" [PASS] 模型行为可解释(特征权重可追溯)")
add(f" 建议:在产品文档中记录各特征的典型权重范围")
add(f" 建议:当单条预测的置信度 < 60% 时,自动触发人工复核")
# 综合结论
add(f"\n{'─' * 60}")
add(f"三、综合结论与建议")
add(f"{'─' * 60}")
fairness_verdict = "通过" if max_diff < 0.1 else ("轻微问题" if max_diff < 0.2 else "未通过")
add(f"\n 公平性:{fairness_verdict}")
add(f" 可解释性:通过")
# 根据实际数据动态生成建议
f_dp = fairness_results["female"]["demographic_parity"]
m_dp = fairness_results["male"]["demographic_parity"]
add(f"\n 改进建议:")
if f_dp > m_dp + 0.1:
add(f" 1. 训练数据中女性正面率偏高(95% vs 50%),导致模型对女性更'宽容'")
add(f" 女性正面预测比例 {f_dp:.1%} > 男性 {m_dp:.1%}")
elif m_dp > f_dp + 0.1:
add(f" 1. 训练数据中男性正面率偏高,导致模型对男性更'宽容'")
add(f" 男性正面预测比例 {m_dp:.1%} > 女性 {f_dp:.1%}")
else:
add(f" 1. 男女正面预测比例差异较小({abs(f_dp - m_dp):.1%}),偏见程度有限")
add(f" 2. 建议平衡训练数据中各群体的情感比例")
add(f" 3. 在模型评估中增加公平性指标,作为上线门槛")
add(f" 4. 对高风险决策(贷款、招聘)保留人工审核环节")
add(f" 5. 定期(每月/每季度)跑公平性检测,监控偏见变化")
add(f"\n{'=' * 60}")
add(f"报告生成完毕")
report_text = "\n".join(lines)
print(report_text)
with open("fairness_report.txt", "w", encoding="utf-8") as f:
f.write(report_text)
print(f"\n报告已保存到 fairness_report.txt")
【运行】 在终端执行:
bash
python fairness_report.py
运行后输出完整报告,包含公平性检测表格、可解释性分析、综合结论三部分。报告同时保存为 fairness_report.txt。
六、公平性测试的实践建议
6.1 什么模型必须做公平性测试
| 模型类型 | 风险等级 | 必须测试 |
|---|---|---|
| 招聘/简历筛选 | 高(影响人生机会) | 性别、年龄、种族 |
| 信贷/金融风控 | 高(影响经济权利) | 性别、地域、收入 |
| 人脸识别 | 高(公共安全) | 种族、性别、年龄 |
| 医疗诊断辅助 | 高(影响健康) | 性别、年龄、种族 |
| 推荐系统 | 中(影响信息获取) | 性别、年龄、地域 |
| 内容审核 | 中(影响表达权) | 语言、方言、文化 |
| 一般分类/预测 | 低 | 按需评估 |
6.2 可解释性的应用层级
| 层级 | 方法 | 适用场景 | 输出形式 |
|---|---|---|---|
| 全局解释 | 特征重要性 | "模型整体关注什么" | 特征排名 |
| 局部解释 | 词级贡献 / LIME | "这条为什么这么判" | 贡献值列表 |
| 对比解释 | 修改输入看变化 | "改哪个词会翻转" | 最小改动集 |
6.3 测试人员的行动清单
- 了解被测模型涉及哪些敏感属性(性别、年龄、地域等)
- 在测试计划中加入公平性维度,和效果、性能并列
- 要求算法团队提供可解释性输出(特征重要性、决策依据)
- 对高风险模型坚持"可解释 + 公平"双达标才放行
- 建立公平性基线,和性能基线一样定期回归检测
七、本篇小结
本篇的核心收获:
- AI 偏见的三大来源:数据偏见、标注偏见、算法偏见
- 公平性三大指标:群体公平性(正面比例相近)、机会均等(召回率相近)、预测平等(精确率相近)
- 偏见检测实战:构建带偏见的模型(性别作为特征)→ 用平衡测试集量化偏见 → 可视化偏见程度
- 可解释性两大方法:特征重要性(全局)、词级贡献(局部)
- 测试人员的价值:不只是测"准不准",还要测"公不公平"和"可不可解释"
- 公平性测试是高风险 AI 系统的必选项:招聘、金融、医疗、安防等场景
至此,你的 AI 测试工具箱已经有五个维度:
效果(第 09、10 篇):准确率、精确率、召回率
性能(第 13 篇):延迟、吞吐量、并发
鲁棒性(第 14 篇):对抗攻击抵抗能力
数据(第 15 篇):数据质量审计 + 漂移监控
公平性与可解释性(本篇):偏见检测 + 行为解释
这五个维度覆盖了 AI 模型质量的核心面。从下一篇开始,我们进入大模型专项------大语言模型(LLM)的评测方法、Prompt 测试、幻觉检测、安全测试等。大模型的测试和传统 AI 模型有很大不同,需要全新的工具箱。
下一篇:【测AI】第17篇:大模型评测方法论 ------ LLM 怎么测?
- 大模型和传统 AI 模型的测试差异
- 大模型评测的核心维度:能力、安全、对齐
- 主流评测基准速览:MMLU、GSM8K、HumanEval、TruthfulQA
- 人工评测 vs 自动评测(LLM-as-a-Judge)
- 构建大模型评测框架的思路