推荐系统(十三)—— 公平性、去偏与可解释性

可信推荐的实战深读:七类偏差(流行度/位置/选择/曝光/从众/人口/确认)的来源与度量;因果推断(RCT、IPS、双重稳健)如何把有偏日志变成无偏信号;MACR、DICE、FairCo 等业界主流去偏方案;LIME、SHAP、反事实解释的工程取舍。

用户打开 Spotify,反复出现的还是那五十首歌。打开 Amazon,排在最前面的总是自己已经看过的商品。打开 YouTube,每条推荐都把你拽向某个并不记得自己想要的兔子洞。这些症状各有名字、各有原因、各有解法。本文讨论的就是这三件事。

你将学到什么

  • 系统性扭曲用户所见的七类偏差——每一种的来源、度量方法和影响
  • 推荐系统中的因果推断——为什么从日志直接学习会撒谎,IPS、双重稳健估计、倾向得分如何还原无偏信号
  • 可上线的去偏方案:MACR 治流行度偏差、DICE 解从众偏差、FairCo 摊销曝光公平
  • 反事实公平与对抗训练,让用户嵌入不再泄露受保护属性
  • 能扛审计的可解释性:LIME、SHAP、反事实解释的工程对比
  • 一套可操作的取舍框架,帮你在准确率—公平性的 Pareto 前沿上选好工作点

前置知识

  • 基于嵌入的推荐器(第 4 篇第 5 篇
  • 一点点因果推断词汇会有帮助,但不是必需——本文从零讲起
  • 能读 PyTorch 风格的伪代码

第一部分 —— 七类偏差

推荐系统中的偏差不是一个问题,而是至少七个,并且会相互叠加。下面这套分类沿用 Chen 等人在 2023 年综述 Bias and Debias in Recommender System 中的框架——想完整看文献地图,从这一篇开始最干净。

1. 流行度偏差 —— 越富者越富

长尾交互分布:少量头部物品垄断了曝光,长尾被饿死

少数物品占据绝大多数交互,推荐器又把这种集中进一步放大。右图是关键证据:哪怕目录本身均匀,前 20 个物品也吃掉了 60% 以上的推荐位。

一个干净的度量是被推荐物品平均流行度与目录均值之差:

$$\text{PopBias@K} = \frac{1}{|U|}\sum_{u \in U} \frac{\sum_{i \in R_u^K} \log(1 + p_i)}{K} - \frac{1}{|I|}\sum_{i \in I} \log(1 + p_i)$$

取对数是为了不被极少数超热门物品主导。要按 slate 单独追踪,不能只看全局——全局均值会把每个用户层面的集中度抹平。

2. 位置偏差 —— 点击跟着鼠标走,不跟着意图走

即使底层相关性恒定,CTR 仍随位置急剧下降

用户检视靠前位置的概率远高于靠后。经典的检视假设(Richardson、Joachims 等)把点击拆为两个独立事件:

$$P(\text{click} \mid u, i, k) = P(\text{examine} \mid k) \cdot P(\text{relevant} \mid u, i)$$

如果直接用日志点击训练,你会把"被放到第一位"和"真正相关"混为一谈。位置偏差是工业级 learning-to-rank 中被研究最透彻的偏差,对应的修复方案——逆倾向得分(IPS)——也是这个季度就能落地的那种。

3. 选择偏差 —— 你拿到的数据不是你想要的数据

观测到的评分被系统性高估;评分高的物品更可能被打分

用户不会随机评分。他们要么对热爱的、要么对失望的物品打分;不温不火的中间地带从来不进日志。这是教科书式的非随机缺失(MNAR)。Marlin 与 Zemel(2009)证明,在 MovieLens 类数据集上忽略它会让 RMSE 虚高 10–30%。解药与位置偏差同源:显式建模缺失机制,再做加权或填补。

4. 曝光偏差 —— 看不到就点不到

系统只能学习它展示过的物品。新物品、长尾物品、来自小众创作者的物品获得的曝光更少,意味着更少的交互、更低的预测相关性、更少的曝光。这就是让推荐器随时间衰退的闭环反馈。

5. 从众偏差 —— 用户在模仿群体

用户表达的偏好里既有真实口味,也有社会从众。如果模型把两者当成同一个信号,它学到的是"流行度代理"而非真实偏好。这正是 DICE(Zheng 等,WWW 2021)显式拆分"兴趣嵌入"和"从众嵌入"想解决的问题。

6. 人口偏差 —— 不同群体上的服务质量不均

同一个模型可能在某群体上 NDCG@10 = 0.42,在另一个群体上只有 0.31。常见原因是数据失衡:弱势群体训练样本更少,学到的表示就更弱。也可能是因果性的:某个特征是受保护属性的代理(邮编代理种族、浏览器语言代理国籍)。

7. 确认偏差 / 信息茧房 —— 世界越来越窄

模型一旦觉得自己懂你,每条推荐都会强化这个信念。多样性塌缩,惊喜消失,用户接触的观念面越缩越小。这是监管者最担心的偏差,因为它在群体层面起作用,而非个体层面。

偏差从哪来

四个源头,按修复难度递增:

来源例子修复难度
数据采集只追踪登录用户容易——更好地采集
算法损失函数优化点击而不是满意度中等——改目标
反馈循环推荐结果变成训练数据困难——打破闭环
评估离线 NDCG 不看公平性中等——加正确的指标

偏差度量工具箱

修任何东西之前先把仪表打全。下面这个类是我每个新推荐器在首次发版评审前一定会跑的最小集合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from collections import defaultdict
from typing import Dict, List

import numpy as np


class BiasMetrics:
    """每个推荐器都该和 NDCG/HR 一起报告的六个指标。"""

    def __init__(
        self,
        recommendations: Dict[int, List[int]],
        item_popularity: Dict[int, int],
        user_groups: Dict[int, str] | None = None,
        item_groups: Dict[int, str] | None = None,
    ) -> None:
        self.recs = recommendations
        self.pop = item_popularity
        self.user_groups = user_groups or {}
        self.item_groups = item_groups or {}

    def popularity_bias(self, k: int = 10) -> float:
        """被推荐物品和目录之间的对数流行度差。"""
        rec_items = [i for r in self.recs.values() for i in r[:k]]
        rec_pop = np.mean([np.log1p(self.pop.get(i, 0)) for i in rec_items])
        all_pop = np.mean([np.log1p(p) for p in self.pop.values()])
        return float(rec_pop - all_pop)

    def gini(self, k: int = 10) -> float:
        """全目录的曝光不平等度。0 = 均匀,1 = 赢者通吃。"""
        exposure = defaultdict(int)
        for r in self.recs.values():
            for i in r[:k]:
                exposure[i] += 1
        x = np.sort(np.array(list(exposure.values()), dtype=float))
        n = len(x)
        if n == 0 or x.sum() == 0:
            return 0.0
        return float((2 * np.sum(np.arange(1, n + 1) * x)) / (n * x.sum()) - (n + 1) / n)

    def coverage(self, k: int = 10) -> float:
        """至少被推荐过一次的物品占目录的比例。"""
        seen = {i for r in self.recs.values() for i in r[:k]}
        return len(seen) / max(len(self.pop), 1)

    def demographic_disparity(self, k: int = 10) -> float:
        """不同用户群之间推荐长度均值的最大差(服务质量代理)。"""
        if not self.user_groups:
            return 0.0
        per_group = defaultdict(list)
        for u, r in self.recs.items():
            per_group[self.user_groups.get(u, "?")].append(len(r[:k]))
        means = [np.mean(v) for v in per_group.values()]
        return float(max(means) - min(means)) if means else 0.0

    def intra_list_diversity(self, k: int = 10) -> float:
        """每个 slate 内不同类目占比的平均值。"""
        if not self.item_groups:
            return 0.0
        scores = []
        for r in self.recs.values():
            slate = r[:k]
            if len(slate) < 2:
                continue
            cats = [self.item_groups.get(i, "?") for i in slate]
            scores.append(len(set(cats)) / len(cats))
        return float(np.mean(scores)) if scores else 0.0

    def report(self, k: int = 10) -> Dict[str, float]:
        return {
            "popularity_bias": self.popularity_bias(k),
            "gini": self.gini(k),
            "coverage": self.coverage(k),
            "demographic_disparity": self.demographic_disparity(k),
            "intra_list_diversity": self.intra_list_diversity(k),
        }

Gini 系数借自经济学,那里它衡量收入不平等。在这里它衡量曝光不平等。Gini > 0.8 意味着少数物品独占 slate,目录其余部分坐冷板凳。


第二部分 —— 推荐系统的因果推断

相关性远远不够

日志告诉你看过 A 的用户也点了 B,但它没告诉你他们点 B 是因为系统推了 B,还是反正都会找到 B。这件事很重要,因为你报告的每一个"提升",本质都是一个因果断言。

潜在结果框架把这个断言显式化。对用户 $u$ 和物品 $i$,想象两个平行世界:

  • $Y_{ui}(1)$:推荐了 $i$ 时的结果
  • $Y_{ui}(0)$:没推荐时的结果

个体处理效应为 $\text{ITE}_{ui} = Y_{ui}(1) - Y_{ui}(0)$。我们永远观测不到两者——这就是因果推断的基本问题。次优解是对很多用户求平均:

$$\text{ATE} = \mathbb{E}_{u,i}[Y_{ui}(1) - Y_{ui}(0)]$$

混淆变量是同时驱动处理(系统展示什么)和结果(用户做什么)的变量。最显著的混淆是用户口味:品味好的用户既会得到好推荐,也更可能点击,无论谁来推荐什么。

逆倾向得分(IPS)

IPS 是去偏的主力工具。思路是:如果一次点击发生在检视概率为 $\pi$ 的条件下,把它加权 $1/\pi$ 就还原出"在均匀检视条件下点击数会是多少"。

IPS 概念示意:每次点击除以 P(被观测到),还原无偏的相关性信号

对于 learning-to-rank 损失,IPS 修正后的估计量是

$$\hat{\mathcal{L}}_{\text{IPS}}(f) = \frac{1}{|D|} \sum_{(u,i,k) \in D} \frac{c_{ui}}{\pi_k} \cdot \ell(f(u, i))$$

其中 $c_{ui} \in \{0,1\}$ 是点击,$\pi_k$ 是位置 $k$ 的位置偏差倾向。Joachims、Swaminathan 与 Schnabel 在 2017 年 SIGIR 论文中证明:只要 $\pi_k > 0$ 处处成立,这个估计量就是无偏的——这也是工业团队会给小倾向加 $\epsilon$、给极端权重剪裁的原因。Saito 等(2020)提出的自归一化 IPS双重稳健变体用一点偏差换得方差大幅下降。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
import torch
import torch.nn as nn


def ips_loss(scores: torch.Tensor, clicks: torch.Tensor,
             positions: torch.Tensor, propensities: torch.Tensor,
             clip: float = 10.0) -> torch.Tensor:
    """IPS 加权交叉熵。

    scores      : 模型 logits, shape (B,)
    clicks      : 0/1 标签, shape (B,)
    positions   : 物品被展示的排名位置, shape (B,)
    propensities: P(检视 | 位置 k), shape (max_pos+1,)
    clip        : IPS 权重上限,控制方差
    """
    pi = propensities[positions].clamp(min=1e-3)
    weights = (1.0 / pi).clamp(max=clip)
    per_example = nn.functional.binary_cross_entropy_with_logits(
        scores, clicks.float(), reduction="none"
    )
    return (weights * clicks * per_example).mean()

实践中你会撞上的两类失败:

  1. 方差爆炸:$\pi_k$ 极小时,权重 $1/\pi_k$ 会爆炸。剪裁要狠(5–20 是常见区间),并监控权重分布。
  2. 倾向不准:你需要一个可信的"用户为什么看到了这些"模型。常用技巧是开一个小流量随机槽(1–5%),随机打乱位置后从中估 $\pi_k$。

双重稳健估计

只要倾向模型结果模型有一个对,双重稳健(DR)估计就是无偏的。带保险绳:

$$\hat{Y}_{\text{DR}} = \hat{Y}(X) + \frac{T}{\pi(X)} \big( Y - \hat{Y}(X) \big)$$

其中 $\hat{Y}(X)$ 是你的填补模型,$T$ 是处理指示。实战中 DR 比纯 IPS 方差更低、比纯填补偏差更小。Wang 等(2019,Doubly Robust Joint Learning)是推荐场景的标准改写。

随机化数据:黄金标准

如果你担得起,跑一个推荐随机化的 A/B 实验就能拿到真实的 ATE。多数团队不能在生产流量上完全随机推荐,所以会用分层随机化——在每个用户的小候选集内随机,记录倾向,下游使用。


第三部分 —— 工业落地的去偏

准确率 vs 公平性:前沿上每个点都是一个无法在不牺牲另一方的前提下改善某一方的配置

真正的问题不是"如何消除偏差",而是"我们要在 Pareto 前沿的哪个点工作"。上图的前沿是真实的:在所有发表过的去偏论文里,完美的公平都有代价。你的任务是选一个可接受、可度量的工作点。

MACR —— 治流行度偏差的模型无关反事实推理

MACR(Wei 等,KDD 2021)是我所知最干净的流行度去偏工程方案。它把预测分数视为三个因果效应之和:用户单独效应、物品单独效应(这就是流行度捷径)、用户—物品交互效应。在推理时减去物品单独效应,把流行度捷径剥掉。

架构上 MACR 加了两个旁路塔:

score(u, i) = main(u, i) - alpha * item_tower(i) - beta * user_tower(u)

物品塔被训练成只用物品本身预测点击——也就是它学的是流行度捷径。推理时减掉它,迫使主塔依赖真实的用户—物品匹配。在 Yelp 和 Amazon-Book 上,MACR 把长尾物品的召回率提升 20–40%,整体 NDCG 只损失个位数。

DICE —— 把兴趣和从众解耦

DICE(Zheng 等,WWW 2021)把每个用户和物品嵌入拆成两段:兴趣从众。训练目标用不同的负采样策略迫使两段各有专长:

  • 兴趣嵌入:负样本是用户不太可能见过的物品 → 抓真实口味
  • 从众嵌入:负样本按流行度匹配 → 抓羊群行为

推理时只用兴趣嵌入打分,从众信号被剥离。

FairCo —— 跨多次 slate 摊销公平

FairCo(Morik 等,SIGIR 2020)瞄准的是随时间累积的曝光公平:每个物品或物品组在所有用户上累计的曝光应当与其优劣成比例。技巧是一个控制器,维护每个组的曝光"债务",并把修正项加到打分上:

$$s'(u, i) = s(u, i) + \lambda \cdot \big( \text{deserved}_g(t) - \text{received}_g(t) \big)$$

其中 $g$ 是物品 $i$ 所属的组。落后的组得到加成,超前的组被压一压。漂亮的性质是:随时间摊销下来,系统会收敛到与优劣成比例的曝光,哪怕单次 slate 看起来仍然有偏。这对 Airbnb、Uber、Etsy 这种生产者公平是商业刚需的双边市场尤为重要。

三大家族,一个组合配方

家族何时用工具
预处理你能控制数据采集重平衡、重加权、MNAR 填补
过程内你能改损失IPS、MACR、DICE、公平正则
后处理模型已冻结FairCo、MMR、公平重排

新发版的合理组合:

  1. 用上面的工具箱审计;选两个你打算撬动的指标
  2. 加一个过程内修复(位置偏差先用 IPS)
  3. 加一个后处理保险(FairCo 控制器)做生产者侧公平
  4. 把这些指标写进发版标准——去偏只有进了评分卡才会留下来

第四部分 —— 反事实公平与对抗训练

一个推荐器是反事实公平的,当且仅当假设用户的性别、种族或其他受保护属性变了,推荐结果不变。形式化地,对受保护属性 $A$ 和预测 $f$:

$$P\big(f_{Y \leftarrow a}(U, X) = y \mid X = x, A = a\big) = P\big(f_{Y \leftarrow a'}(U, X) = y \mid X = x, A = a\big)$$

记号 $f_{Y \leftarrow a}$ 是 Pearl 的do-算子:我们干预 $A$,其余保持不变。这比"统计平价"严格——后者只要求边缘分布相等。

对抗去偏 —— CFairER 范式

判别器尝试从用户嵌入预测受保护属性,编码器尝试欺骗它。均衡时,嵌入不再含有受保护信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class CFairER(nn.Module):
    """推荐器嵌入的对抗去偏。

    两个网络打 minimax:
    - 判别器:从用户嵌入预测受保护属性
    - 编码器:既要打好分,又要骗过判别器
    """

    def __init__(self, n_users: int, n_items: int, dim: int = 64) -> None:
        super().__init__()
        self.user_emb = nn.Embedding(n_users, dim)
        self.item_emb = nn.Embedding(n_items, dim)
        self.predictor = nn.Sequential(
            nn.Linear(2 * dim, 128), nn.ReLU(),
            nn.Linear(128, 1),
        )
        self.discriminator = nn.Sequential(
            nn.Linear(dim, 64), nn.ReLU(),
            nn.Linear(64, 1),
        )

    def forward(self, u: torch.Tensor, i: torch.Tensor):
        ue = self.user_emb(u)
        ie = self.item_emb(i)
        score = self.predictor(torch.cat([ue, ie], dim=-1)).squeeze(-1)
        attr_logit = self.discriminator(ue).squeeze(-1)
        return score, attr_logit, ue


def train_step(model: CFairER, batch, opt_main, opt_disc,
               lam_fair: float = 1.0) -> dict:
    u, i, y, a = batch  # 用户、物品、评分、受保护属性
    score, attr_logit, ue = model(u, i)

    # 1) 在 detach 后的嵌入上更新判别器
    opt_disc.zero_grad()
    a_logit = model.discriminator(ue.detach()).squeeze(-1)
    d_loss = nn.functional.binary_cross_entropy_with_logits(a_logit, a.float())
    d_loss.backward()
    opt_disc.step()

    # 2) 更新编码器 + 预测器(既要好推荐,也要骗住判别器)
    opt_main.zero_grad()
    score, attr_logit, _ = model(u, i)
    pred_loss = nn.functional.mse_loss(score, y.float())
    # 鼓励判别器不确定(在概率空间目标 0.5)
    fair_loss = nn.functional.binary_cross_entropy_with_logits(
        attr_logit, torch.full_like(attr_logit, 0.5)
    )
    total = pred_loss + lam_fair * fair_loss
    total.backward()
    opt_main.step()

    return {"pred": pred_loss.item(), "disc": d_loss.item(), "fair": fair_loss.item()}

实战要盯紧两件事:

  • 判别器塌陷:判别器跑得太快太猛,编码器会摆烂。给判别器更小的学习率或更低的更新频率。
  • 物品侧泄露:如果物品本身就和受保护属性强相关(比如女性杂志类物品能预测性别),仅在用户嵌入侧去偏不够。可能需要在 (用户, 物品) 分数上再加一个判别器。

第五部分 —— 可解释性

为什么要做

三类受众,三个理由:

  • 用户更信任他们能理解的推荐。“因为你看过《盗梦空间》“在 Netflix 和 Spotify 公开实验中能把 CTR 抬 6–12%。
  • 工程师在模型能"亮出账本"时调试更快。
  • 监管——欧盟 GDPR 第 22 条以及越来越多美国地区都要求自动化决策提供"对所涉及逻辑的有意义信息”。

LIME —— 任意模型的局部线性近似

LIME 风格的局部解释:哪些特征把这一对 (用户, 物品) 的预测往上推或往下拉

LIME(Ribeiro 等,KDD 2016)是模型无关局部的。在你想解释的实例附近扰动输入,向黑盒模型查询扰动后的预测,再用按近似度加权的稀疏线性模型拟合。线性模型的系数就是解释。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from sklearn.linear_model import Ridge


def lime_explain(predict_fn, x: np.ndarray, n_samples: int = 1000,
                 sigma: float = 0.1, n_top: int = 8):
    """返回驱动 predict_fn(x) 的前 k 个特征。

    predict_fn : callable(np.ndarray of shape (N, d)) -> (N,)
    x          : 待解释的实例, shape (d,)
    sigma      : 扰动噪声尺度
    """
    d = x.shape[0]
    # 在 x 附近采扰动邻居
    samples = x + np.random.normal(0, sigma, size=(n_samples, d))
    preds = predict_fn(samples)

    # 用 RBF 核按近似度加权
    dist = np.linalg.norm(samples - x, axis=1)
    weights = np.exp(-(dist ** 2) / (2 * sigma ** 2))

    # 拟合稀疏线性代理
    surrogate = Ridge(alpha=1.0)
    surrogate.fit(samples, preds, sample_weight=weights)

    # 按系数绝对值取前 k 个特征
    coefs = surrogate.coef_
    top = np.argsort(-np.abs(coefs))[:n_top]
    return [(int(j), float(coefs[j])) for j in top]

注意:LIME 不稳定。Ribeiro 自己也说同一实例两次跑可能给出不同的 top 特征。生产环境固定随机种子,并报告稳定性指标。

SHAP —— 博弈论的贡献

SHAP(Lundberg 与 Lee,NeurIPS 2017)计算 Shapley 值:满足效率(归因和等于预测减基线)、对称可加的唯一归因方案。对模型 $f$ 和特征集合 $N$:

$$\phi_j = \sum_{S \subseteq N \setminus \{j\}} \frac{|S|! (|N| - |S| - 1)!}{|N|!} \big( f(S \cup \{j\}) - f(S) \big)$$

精确 Shapley 值随特征数指数级。生产用近似:

  • TreeSHAP(树集成多项式时间)—— GBDT 推荐器的首选
  • KernelSHAP(模型无关,类似 LIME 但用 Shapley 权重)
  • DeepSHAP(神经网络版,基于 DeepLIFT)

与 LIME 的关键差异:SHAP 归因的和等于预测,因此是一致可加的。如果要面对审计,用 SHAP。

LIMESHAP
速度中(TreeSHAP)到慢(KernelSHAP 精确版)
理论启发式博弈论(Shapley 值)
稳定性不稳定一致
适用快速调试、特征量大审计、合规报告

反事实解释 —— 可执行的答案

反事实:让推荐结果反转所需的最小改动

反事实回答的是"要怎么改,推荐结果才会变?"。它比特征归因更可执行,因为它指向一次最小干预。Wachter 等(2017)的形式化:

$$x^{\text{cf}} = \arg\min_{x'} \; d(x, x') \quad \text{s.t.} \quad f(x') \neq f(x)$$

其中 $d$ 是惩罚不现实改动的距离(一次改太多特征、或动了不可改的特征如年龄)。现代实现(DiCE,Mothilal 等 2020)用梯度下降优化松弛版本,并加多样性项,一次给出几条不同的反事实。

杀手级用法是可审计性。“我们没把这款贷款产品推给用户 X,因为他申报的收入低于阈值;如果再高 5000 美元,模型就会推”——这种解释是监管能接受的形式。


第六部分 —— 工业的信任建设

能上线的透明度

最小可上线透明层:

  1. 每个物品一句自然语言解释(“因为你看过《盗梦空间》和《信条》")
  2. 一个用户能基于其行动的置信度(“匹配度 85%")
  3. 一个"为什么是这个?“的深入页面,展示 SHAP 值与贡献特征
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def render_explanation(user_id: int, item_id: int,
                       shap_values: dict, history: list) -> dict:
    top_pos = sorted(shap_values.items(), key=lambda x: -x[1])[:2]
    top_neg = sorted(shap_values.items(), key=lambda x: x[1])[:1]
    return {
        "headline": f"因为你喜欢 {history[0]}{history[1]}",
        "confidence": min(99, int(50 + 100 * sum(v for _, v in top_pos))),
        "positive_factors": [name for name, _ in top_pos],
        "negative_factors": [name for name, _ in top_neg],
        "feedback_options": ["要更多这类", "要少一些", "不感兴趣"],
    }

用户控制面板

每个推荐曝光面都该提供三种控制:

  • 多样性滑块 —— 探索 vs 利用的显式旋钮
  • 话题屏蔽 —— “永远不要给我推恐怖片”
  • 解释并调整 —— 展示贡献特征,让用户重新加权

这些都是低成本、高信任度的添加。YouTube 的"不再推荐此频道”、Spotify 的"隐藏这首歌"存在的意义是一样的:可见的控制权能降低感知偏差,哪怕底层模型没变。

持续监控

公平性不是发版勾选项,而是持续度量。把第一部分的偏差指标做成仪表盘,对回退报警,每季度用 SHAP/反事实工具做一次审计。三月公平的模型可能六月就漂了;唯一的防线是仪表化。


Q&A

我是个小团队,去偏的最低标准是什么?

两件事。第一,把第一部分的偏差指标仪表化——量不到就修不了。第二,把 IPS 加进 learning-to-rank 损失里治位置偏差。两者都便宜,而且都能跑出可度量的胜利。

公平性的准确率代价有多大?

取决于你在前沿的位置。从一个低公平基线出发,30% 的公平改善通常只要 1–3% 的 NDCG。贵的部分是最后那 10% 的公平,常常要付 10%+ 的准确率。定个目标,别追求完美。

选 LIME 还是 SHAP?

LIME 用于开发期的快速本地调试。SHAP 用于一切要给审计或用户看的场景。如果你用树模型,TreeSHAP 已经够快,没理由用别的。

对抗去偏里的判别器一直收敛到随机水平,是好事吗?

是的——随机水平意味着嵌入没泄露受保护信息。但要用下游指标验证:从头训练一个新的分类器到嵌入上,确认它也无法超过随机。GAN 式循环里的判别器有时会欠拟合。

怎么处理交叉性公平(如黑人女性,而非单看种族或性别)?

单属性公平会在交叉处藏起差距。最低做法是在交叉单元上报指标,而不是只看边缘分布。进阶方案是交叉性去偏——在属性元组上加对抗损失——但要小心:单元格样本数下降很快,统计功效随之降低。


小结

  • 七类偏差——流行度、位置、选择、曝光、从众、人口、确认——各有可度量的特征和已知的修复方案
  • 因果推断,特别是 IPS双重稳健估计,把有偏的日志数据变成无偏的训练信号
  • 工业去偏工具箱:MACR 治流行度,DICE 解从众,FairCo 摊销曝光,对抗训练剥离受保护属性
  • LIME 做快速本地调试,SHAP 出可审计的解释,反事实给可执行的"怎么改才会反转"的答案
  • 可信推荐不是一次性项目——是仪表化、仪表盘、用户控制和季度审计

偏差和可解释性已经不是可选项,它们是 2024 年运营推荐器的入场券。


系列导航

本文是推荐系统系列第 13 篇 / 共 16 篇

上一篇第 12 篇 —— 大语言模型与推荐系统 下一篇第 14 篇 —— 跨域推荐与冷启动方案

查看推荐系统系列全部文章

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub