【测AI】第16篇:模型公平性与可解释性测试 — AI 的偏见与黑箱

写在前面

前面的篇目我们一直在测模型"准不准"、"快不快"、"稳不稳"。但 AI 模型还有两个更深层的质量问题,经常被忽视,一旦出事就是大事:

公平性(Fairness):模型是否对不同群体(性别、年龄、地域)一视同仁?

真实案例:某公司用 AI 筛选简历,发现系统给女性候选人的技术岗评分系统性偏低。原因:训练数据来自过去的招聘记录,而过去男性技术从业者更多,模型学到了"男性 = 更适合技术岗"的错误模式。

真实案例:某信贷模型对某地区的贷款拒绝率明显高于其他地区。原因:训练数据中该地区的历史违约率较高,但那是因为该地区之前遭受过经济冲击------不代表现在。

可解释性(Interpretability):模型能不能解释自己为什么这么判断?

真实场景:用户被 AI 拒绝了贷款申请,向客服询问原因。客服说"AI 判定的"------这在很多国家是不合法的(GDPR 规定用户有权获得"有意义的解释")。

真实场景:医疗 AI 说"这张 X 光片有 87% 的概率是肺炎"。医生问:"为什么?"AI 说不出------医生不敢用。

本篇目标:

  1. 理解模型偏见的来源和常见类型
  2. 掌握公平性的核心检测指标
  3. 实战:构建一个有偏见的模型并检测偏见
  4. 学会用特征重要性和 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 为什么测试人员要关注公平性

有人可能觉得"公平性是算法团队的事"。不对。原因:

  1. 你是质量守门人:模型效果好不好你看得出,模型公不公平你也应该看得出
  2. 法律要求越来越严:欧盟 AI Act、中国《互联网信息服务算法推荐管理规定》都对算法公平性有要求
  3. 品牌风险巨大:AI 歧视一旦被媒体曝光,对品牌的伤害远超一个 Bug
  4. 公平性需要系统化测试:不是看一眼就能发现的,需要专门的测试方法和指标

二、公平性的核心检测指标

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 测试人员的行动清单

  1. 了解被测模型涉及哪些敏感属性(性别、年龄、地域等)
  2. 在测试计划中加入公平性维度,和效果、性能并列
  3. 要求算法团队提供可解释性输出(特征重要性、决策依据)
  4. 对高风险模型坚持"可解释 + 公平"双达标才放行
  5. 建立公平性基线,和性能基线一样定期回归检测

七、本篇小结

本篇的核心收获:

  • 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)
  • 构建大模型评测框架的思路
相关推荐
AAASilverwolf1 小时前
OpenAI 新增 12 小时提示缓存:跨小时任务能省多少?
人工智能·gpt·缓存·api
博谷1 小时前
2026年GEO监测服务商的信源证据链怎么建?BUGOOAI布谷GEO的T0-T3分级与追溯逻辑
ai
kaixin_啊啊1 小时前
【零基础学AI】第 15 章——数据可视化与探索
人工智能·信息可视化
Ward的AI产品实战1 小时前
开源 AI 网关 Magpie 调优终结篇:顺位排错、跨家族对抗审查与 5 大路由组重构(下篇)
前端·人工智能
jay神1 小时前
基于 YOLOv8 的无人机识别与检测系统
人工智能·深度学习·yolo·计算机视觉·无人机·课程设计·计算机毕业设计
田里的水稻1 小时前
FA_CNN卷积神经网络四---线性层及其拉平PyTorch
人工智能·pytorch·python·深度学习·神经网络·cnn
网易易盾1 小时前
直播话术合规改造:六类违规表述判定与改写规则
大数据·人工智能·安全
codigger1 小时前
AI学会写代码之后,程序员节为什么更重要了
ai·程序员·ai编程·#人工智能
碧海银沙音频科技研究院1 小时前
实现炬芯ATS308X的usb cdc 串口IAP 固件升级功能实现
人工智能·嵌入式硬件·深度学习·算法