AI 工程基础体系 · 第 54/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

推荐系统基础:召回、排序、协同过滤、冷启动与离线评测

推荐系统的目标不是简单地“找出用户喜欢的内容”,而是在有限延迟、有限计算成本和权限约束下,从大量候选对象中选择当前最值得展示的一小部分。

一个生产推荐请求通常经过以下流程:

flowchart LR
    A[用户请求与上下文] --> B[特征与状态读取]
    B --> C[多路召回]
    C --> D[权限与基础过滤]
    D --> E[候选合并去重]
    E --> F[粗排]
    F --> G[精排]
    G --> H[重排与约束]
    H --> I[展示]
    I --> J[曝光、点击、转化日志]
    J --> K[训练数据与离线评测]
    K --> B

这里有三个容易混淆的概念:

  • 召回是推荐流水线中的候选生成阶段,目标是从全量物品中找出一个规模较小但尽量不漏掉相关物品的集合。
  • 排序是对候选集合计算相关性或收益,并决定展示顺序。
  • 评测中的 Recall@K是一个指标,表示真实相关物品有多少进入了前 KK 个结果。它不等于流水线中的召回模块。

如果全量物品数为 NN,最终只展示 KK 个结果,直接对全部 NN 个物品进行复杂模型打分通常成本过高。因此推荐系统常见的结构是:

全量物品低成本候选生成较复杂排序模型最终展示\text{全量物品} \rightarrow \text{低成本候选生成} \rightarrow \text{较复杂排序模型} \rightarrow \text{最终展示}

候选生成可以保留几百到几千个物品,排序模型再从中选择几十个结果。这个拆分带来了一个重要约束:如果相关物品没有进入候选集,后续排序模型无论多强都无法把它找回来。


一、推荐问题的形式化定义

设用户为 uu,物品为 ii,请求发生时的上下文为 xx。上下文可以包括时间、设备、地域、页面位置、网络状态和当前会话行为。

推荐系统希望估计某个目标:

y^u,i=f(u,i,x)\hat{y}_{u,i} = f(u, i, x)

其中 yu,iy_{u,i} 可以表示:

  • 是否点击;
  • 是否播放超过某个时长;
  • 是否购买;
  • 购买金额;
  • 留存或长期满意度;
  • 多目标组合后的业务效用。

如果系统只优化点击率,常见的训练目标是估计:

P(click=1u,i,x)P(\text{click}=1 \mid u,i,x)

但“点击概率高”不必然等于“推荐价值高”。例如标题党内容可能容易点击,却带来快速退出;低价商品可能容易下单,却产生较高退款率。因此生产系统通常将多个目标组合:

S(u,i,x)=wcp^click+wvp^valid_view+wbp^buywrp^refundwdp^dislikeS(u,i,x) = w_c \hat{p}_{click} + w_v \hat{p}_{valid\_view} + w_b \hat{p}_{buy} - w_r \hat{p}_{refund} - w_d \hat{p}_{dislike}

其中每个 p^\hat{p} 是一个预测量,ww 是经过业务目标和实验确定的权重。这个公式不是“真实效用”的必然表达,而是把多个可预测目标映射为排序分数的工程方法。

推荐对象还必须满足约束:

iIallowed(u,x)i \in \mathcal{I}_{allowed}(u,x)

例如用户无权访问的文档、已下架商品、地区不可售内容,都不应仅因为模型分数高而展示。权限过滤是推荐正确性的一部分,不是排序结束后的可选装饰。


二、召回:在全量空间中高效生成候选

2.1 召回的目标与形式化条件

设用户在当前上下文下真正相关的物品集合为 RuR_u,召回模块生成候选集合 CuC_u。召回的覆盖能力可以写成:

Recall(Cu)=CuRuRu\text{Recall}(C_u) = \frac{|C_u \cap R_u|}{|R_u|}

实际系统通常不知道完整的 RuR_u,因此用未来发生的点击、购买或高质量消费作为部分观测标签。

召回阶段的核心权衡是:

  • 候选集合太小:延迟和排序成本低,但容易漏掉相关内容;
  • 候选集合太大:更容易覆盖相关内容,但会增加排序延迟、内存和模型推理成本;
  • 召回规则过于单一:可能造成同质化;
  • 召回来源过多但没有去重:用户看到的候选仍然缺乏多样性。

召回模型通常不追求最终展示顺序。它更关注“不要漏掉可能有价值的物品”,因此召回阶段的目标函数和精排阶段并不相同。

2.2 常见召回通道

生产系统通常采用多路召回,每一路代表一种相关性假设。

热门召回

按照时间窗口内的点击、有效消费或转化统计生成热门列表:

score(i)=αrecent_clicks(i)+βrecent_orders(i)score(i) = \alpha \cdot \text{recent\_clicks}(i) + \beta \cdot \text{recent\_orders}(i)

常见实现会按小时、天或地域计算,并使用时间衰减。热门召回对新用户有效,但容易形成“马太效应”:已经获得曝光的物品更热门,进一步获得更多曝光。

热门数据需要处理刷量、重复用户、异常流量和库存状态。仅按总点击排序会让曝光量大的物品看起来更热门,不能直接代表单位曝光的质量。

内容召回

内容召回使用物品自身属性,例如文本、类别、品牌、标签、作者和图像向量。设用户兴趣向量为 vuv_u,物品向量为 viv_i,可以用余弦相似度:

sim(u,i)=vuvivu2vi2sim(u,i) = \frac{v_u \cdot v_i} {\|v_u\|_2\|v_i\|_2}

用户向量可以由历史行为物品向量加权平均得到:

vu=jHuwjvjjHuwjv_u = \frac{\sum_{j \in H_u} w_j v_j} {\sum_{j \in H_u} w_j}

其中 HuH_u 是用户历史行为集合,wjw_j 可以根据行为类型和时间衰减确定。

内容召回对新物品尤其重要,因为新物品不需要先积累交互数据。但它受内容质量影响:标签错误、文本模板重复或图像向量偏差都会直接传递到候选集合。

协同过滤召回

协同过滤不依赖物品的显式内容,而是利用用户与物品之间的交互关系。例如“看过相同商品的用户还购买了什么”或“与当前商品共同购买次数较多的商品”。

它能发现难以通过关键词描述的隐含关系,但新用户和新物品缺少交互时会失效,这就是冷启动问题。

向量召回

使用深度模型把用户和物品映射到同一个向量空间:

zu=fθ(u,x),zi=gϕ(i)z_u = f_\theta(u, x), \qquad z_i = g_\phi(i)

通过内积或余弦相似度检索:

score(u,i)=zuziscore(u,i)=z_u^\top z_i

全量逐个计算的复杂度约为 O(Id)O(|\mathcal{I}|d),其中 dd 是向量维度。实际系统会使用近似最近邻索引,将检索成本降低到适合在线请求的范围。索引需要随物品新增、内容修改和模型版本更新而维护;索引版本与向量模型版本不匹配,会导致分数不可比或召回质量下降。

规则和业务召回

规则召回包括库存可售商品、关注作者的新内容、同一组织内文档、订阅频道更新等。这些召回不一定有机器学习模型,但通常拥有明确的产品和权限语义。

规则召回不能被“模型分数”随意替代。例如“用户有权限访问的组织文档”是集合约束,而不是一个可以用预测概率近似的偏好。

2.3 召回合并

假设有热门、内容、协同过滤和向量四路候选:

Cu=CupopularCucontentCucfCuvectorC_u = C_u^{popular} \cup C_u^{content} \cup C_u^{cf} \cup C_u^{vector}

合并时至少要记录:

  • 物品 ID;
  • 来源通道;
  • 每个通道的原始分数;
  • 召回时间;
  • 模型或索引版本;
  • 是否通过权限、库存和状态检查。

不同通道的分数通常不可直接比较。余弦相似度、点击概率和共现次数不在同一尺度上,因此不能简单按原始分数排序。常见做法是对各通道分别归一化,或把“通道身份”和“通道分数”交给后续排序模型学习。


三、排序:从“可能相关”到“应该先展示”

排序输入的是候选集合,而不是全量物品。给定候选集合 CuC_u,排序模型计算:

s(u,i,x)=hψ(ϕ(u,i,x))s(u,i,x)=h_\psi(\phi(u,i,x))

其中 ϕ\phi 是特征,hψh_\psi 是模型。

3.1 排序特征

常见特征包括:

  • 用户特征:活跃度、历史偏好、购买类别;
  • 物品特征:类别、价格、质量分、发布时间;
  • 交叉特征:用户与物品类别的匹配、历史浏览次数;
  • 上下文特征:时间、设备、入口页面;
  • 行为统计:过去一段时间的点击率、转化率;
  • 召回特征:来自哪些通道、通道内排名;
  • 约束特征:库存、配送范围、内容安全状态。

行为统计必须遵守时间边界。例如预测 3 月 10 日的点击,不能使用 3 月 10 日之后计算出的物品点击率,否则会产生标签泄漏。

3.2 点式、对式和列表式学习

点式学习把每个样本单独作为分类或回归任务:

Lpoint=n[ynlogp^n+(1yn)log(1p^n)]\mathcal{L}_{point} = -\sum_n \left[ y_n\log \hat{p}_n+ (1-y_n)\log(1-\hat{p}_n) \right]

它实现简单,但最终目标是列表排序,单个样本的概率校准不一定等价于排序质量。

对式学习比较同一用户下的正样本和负样本。例如要求:

s(u,i+)>s(u,i)s(u,i^+) > s(u,i^-)

一种损失为:

Lpair=logσ(s(u,i+)s(u,i))\mathcal{L}_{pair} = -\log \sigma(s(u,i^+)-s(u,i^-))

其中 σ\sigma 是 Sigmoid 函数。该损失直接鼓励正样本排在负样本之前,但负样本怎么采样会显著影响训练结果。

列表式学习直接优化一个用户候选列表的排序结构,例如近似 NDCG。它更接近排序目标,但实现、采样和训练资源通常更复杂。

3.3 粗排、精排和重排

一个常见的多级流程是:

  1. 召回得到数百到数千个候选;
  2. 粗排使用轻量模型缩小规模;
  3. 精排使用更复杂的交叉特征、序列模型或多任务模型;
  4. 重排处理去重、多样性、业务约束和位置效应。

重排不应与精排混为一谈。精排通常回答“每个物品单独有多大价值”,重排还要回答“这些物品同时出现是否合适”。

例如,如果前十个结果都属于同一作者,即使单项分数都很高,用户的选择空间也可能很窄。可以将重复惩罚加入列表目标:

S(L)=iLsiλi,jLsim(i,j)S(L) = \sum_{i\in L}s_i - \lambda \sum_{i,j\in L} sim(i,j)

其中 LL 是最终列表,第二项惩罚列表中相似物品过多。


四、协同过滤:从群体行为推断个体偏好

协同过滤的基本假设是:如果用户过去对物品的行为模式相似,那么他们未来可能对相似物品感兴趣;如果两个物品被相似用户共同消费,那么它们可能具有相似用途或受众。

这个假设是经验规律,不是逻辑必然。它会受到热门偏差、曝光偏差、用户兴趣变化和虚假行为影响。

4.1 基于用户的协同过滤

把用户表示成物品交互向量。若用户 uuvv 的向量分别为 ru,rvr_u,r_v,可以计算余弦相似度:

sim(u,v)=rurvrurvsim(u,v) = \frac{r_u\cdot r_v} {\|r_u\|\|r_v\|}

对用户 uu 的候选物品 ii,用相似用户的行为加权:

score(u,i)=vN(u)sim(u,v)rv,iscore(u,i) = \sum_{v\in N(u)} sim(u,v)\cdot r_{v,i}

其中 N(u)N(u) 是最相似的邻居集合。

问题在于,用户数量增长时,在线寻找相似用户的成本较高;另外,用户向量很稀疏,新用户几乎没有可计算的相似关系。

4.2 基于物品的协同过滤

基于物品的方法先计算物品之间的相似度:

sim(i,j)sim(i,j)

然后根据用户历史交互物品推荐相似物品:

score(u,i)=jHuwu,jsim(i,j)score(u,i) = \sum_{j\in H_u} w_{u,j}\cdot sim(i,j)

它通常更容易缓存:物品相似列表可以离线计算,在线只需查找用户最近行为对应的邻居。但它也会继承历史曝光结构。例如用户只能看到某个频道的商品,模型可能误以为该频道内商品天然相似。

4.3 矩阵分解

设用户—物品交互矩阵为 RR,其中 ruir_{ui} 可以是评分、点击次数或隐式反馈。矩阵分解用低维向量表示用户和物品:

r^ui=puqi\hat{r}_{ui}=p_u^\top q_i

对于已观测评分,可以最小化:

minP,Q(u,i)Ω(ruipuqi)2+λ(upu22+iqi22)\min_{P,Q} \sum_{(u,i)\in\Omega} (r_{ui}-p_u^\top q_i)^2 + \lambda \left( \sum_u\|p_u\|_2^2+ \sum_i\|q_i\|_2^2 \right)

其中:

  • Ω\Omega 是已观测交互集合;
  • pup_u 是用户向量;
  • qiq_i 是物品向量;
  • λ\lambda 控制正则化强度。

对单个样本的误差项:

eui=ruipuqie_{ui}=r_{ui}-p_u^\top q_i

梯度下降更新为:

pupu+η(euiqiλpu)p_u \leftarrow p_u+\eta(e_{ui}q_i-\lambda p_u)

qiqi+η(euipuλqi)q_i \leftarrow q_i+\eta(e_{ui}p_u-\lambda q_i)

其中 η\eta 是学习率。

一个完整的单步计算

假设:

pu=(1,2),qi=(2,1),rui=5p_u=(1,2), \quad q_i=(2,1), \quad r_{ui}=5

预测值为:

r^ui=1×2+2×1=4\hat r_{ui}=1\times2+2\times1=4

因此误差:

eui=54=1e_{ui}=5-4=1

取学习率 η=0.1\eta=0.1,正则化系数 λ=0.01\lambda=0.01

pu=(1,2)+0.1((1,2)0.01(1,2))=(1.099,2.198)p_u' = (1,2)+0.1\left((1,2)-0.01(1,2)\right) = (1.099,2.198)

qi=(2,1)+0.1((1,2)0.01(2,1))=(2.098,1.199)q_i' = (2,1)+0.1\left((1,2)-0.01(2,1)\right) = (2.098,1.199)

更新后,用户和物品向量的内积会增大,使该已观测高评分样本的预测值向 5 靠近。若训练数据只包含“被点击过的物品”,没有显式负反馈,则不能把未点击直接解释为“不喜欢”;它也可能只是没有曝光。因此隐式反馈通常需要负采样、加权损失或曝光建模。

4.4 反例:协同过滤并不等于因果偏好

假设一个网站只向购买打印机的用户展示墨盒。之后打印机和墨盒高度共现,协同过滤会认为二者关系很强。

但如果取消这种固定展示,它们的关联可能显著下降。模型学到的是“共同曝光策略”,不一定是用户自然偏好。

这说明交互日志同时包含:

行为=偏好+曝光机制+位置影响+价格与库存+随机因素\text{行为} = \text{偏好} + \text{曝光机制} + \text{位置影响} + \text{价格与库存} + \text{随机因素}

只用行为矩阵训练模型,通常无法自动区分这些来源。


五、冷启动:没有足够历史时如何推荐

冷启动是推荐系统缺少有效交互数据时的预测问题,至少包括四类:

  1. 新用户冷启动:用户没有历史行为;
  2. 新物品冷启动:物品刚发布,没有交互;
  3. 新场景冷启动:已有用户进入新页面或新业务;
  4. 系统冷启动:整个系统缺少历史数据。

5.1 新用户冷启动

新用户没有 HuH_u,因此协同过滤无法得到可靠的 pup_u 或用户邻居。可使用:

  • 热门内容;
  • 地域、设备、时间等上下文;
  • 注册或首次访问时的兴趣选择;
  • 当前会话中的搜索、浏览和停留行为;
  • 内容相似度;
  • 有限的探索流量。

新用户的第一个行为往往信息量很高,但不能把单次点击过度解释为稳定兴趣。更稳妥的做法是对行为进行时间衰减和置信度加权:

wj=type_weight(j)eγΔtjw_j = type\_weight(j)\cdot e^{-\gamma \Delta t_j}

其中 Δtj\Delta t_j 是行为距当前的时间差。

5.2 新物品冷启动

新物品没有协同交互,但通常有标题、正文、类目、作者、价格或图像等内容。可以:

  • 使用内容模型生成物品向量;
  • 将物品加入基于内容的召回;
  • 在相似旧物品附近进行有限曝光;
  • 给新物品设置探索配额;
  • 使用人工审核或质量规则过滤低质量内容。

新物品的初始曝光不能完全依赖点击率,因为曝光次数太少时,点击率方差很大。若一个物品曝光 1 次并点击 1 次,观测点击率是 100%,但这并不说明它稳定优于曝光 10,000 次、点击 3,000 次的物品。

5.3 探索与利用

利用是展示当前估计最好的物品;探索是展示不确定但可能有价值的物品,以获得更多信息。

可以将估计收益和不确定性组合:

scorei=μ^i+αuncertaintyiscore_i = \hat{\mu}_i+\alpha \cdot uncertainty_i

不确定性高的物品可能获得额外机会。这个方法会带来短期指标损失的可能性,但有助于避免系统永远只展示早期已经占优的物品。

探索必须受到安全、权限、内容质量和业务风险约束。不能因为某个物品“有探索价值”就绕过内容审核或授权检查。

5.4 冷启动的反例

以下方案看似可以解决冷启动,实际上并不充分:

  • 用全站热门解决所有新用户:会损失个性化,并强化热门偏差;
  • 用新物品自身文本解决所有问题:文本相似不代表用户喜欢;
  • 给新物品大量曝光:可能伤害用户体验,也可能被刷量;
  • 把未点击样本全部作为负样本:会把“未曝光”误判为“不喜欢”。

冷启动的本质不是找到一个万能模型,而是在缺乏证据时管理不确定性。


六、离线评测:在上线前判断模型是否有效

离线评测使用历史日志估计模型质量。它适合发现明显回归、比较候选模型和检查数据管道,但不能独立证明线上收益。

6.1 时间切分比随机切分更符合预测任务

推荐任务是用过去预测未来,因此训练、验证和测试应按时间切分:

训练集:1 月 1 日至 1 月 20 日的行为
验证集:1 月 21 日至 1 月 25 日的行为
测试集:1 月 26 日至 1 月 31 日的行为

随机切分可能让同一用户未来的行为进入训练集,再用过去行为预测它,造成时间泄漏。

还需要保证特征的 point-in-time 正确性:生成某条训练样本时,只能读取该时刻以前可见的数据。

6.2 Precision@K、Recall@K 和 F1

设推荐列表前 KK 个结果为 LuKL_u^K,真实相关集合为 RuR_u

Precision@K=LuKRuKPrecision@K = \frac{|L_u^K\cap R_u|}{K}

它回答“展示的前 KK 个中有多少相关”。

Recall@K=LuKRuRuRecall@K = \frac{|L_u^K\cap R_u|}{|R_u|}

它回答“真实相关物品中有多少被找到了”。

F1@K=2Precision@KRecall@KPrecision@K+Recall@KF1@K = \frac{2Precision@K\cdot Recall@K} {Precision@K+Recall@K}

Precision 和 Recall 的取舍取决于场景。搜索结果通常更重视前几位的精确性;候选召回阶段则更关心不要漏掉后续可能排序靠前的物品。

6.3 一个完整的指标算例

假设某用户在测试窗口内有 3 个真实相关物品:

Ru={A,B,C}R_u=\{A,B,C\}

系统返回前 5 个:

Lu5=[A,X,B,Y,C]L_u^5=[A,X,B,Y,C]

命中位置为 1、3、5,因此:

Precision@5=35=0.6Precision@5=\frac{3}{5}=0.6

Recall@5=33=1.0Recall@5=\frac{3}{3}=1.0

F1@5=2×0.6×1.00.6+1.0=0.75F1@5= \frac{2\times0.6\times1.0}{0.6+1.0}=0.75

平均精确率 AP 先计算每个相关物品出现位置的 Precision:

  • 位置 1:1/1=11/1=1
  • 位置 3:2/32/3
  • 位置 5:3/53/5

因此:

AP=1+23+3530.756AP= \frac{1+\frac23+\frac35}{3} \approx0.756

若使用二元相关性,折损累计增益为:

DCG@5=1log2(1+1)+1log2(3+1)+1log2(5+1)DCG@5 = \frac{1}{\log_2(1+1)} + \frac{1}{\log_2(3+1)} + \frac{1}{\log_2(5+1)}

DCG@5=1+0.5+1log261.887DCG@5 = 1+0.5+\frac{1}{\log_2 6} \approx1.887

理想列表把三个相关物品放在位置 1、2、3:

IDCG@5=1+1log23+1log242.131IDCG@5 = 1+\frac1{\log_2 3}+\frac1{\log_2 4} \approx2.131

所以:

NDCG@5=1.8872.1310.885NDCG@5=\frac{1.887}{2.131}\approx0.885

这个例子说明:Recall@5 已经是 1,但相关物品没有全部排在最前面,所以 NDCG 仍然低于 1。

6.4 可运行的指标实现

下面的 Python 代码不依赖第三方库,计算一个用户的 Precision、Recall、AP 和 NDCG:

import math

def ranking_metrics(recommended, relevant, k):
    """
    recommended: 按模型分数降序排列的物品 ID 列表
    relevant:    测试窗口中的真实相关物品 ID 集合
    k:           评测截断位置
    """
    top_k = recommended[:k]
    relevant = set(relevant)

    hits = [item in relevant for item in top_k]
    hit_count = sum(hits)

    precision = hit_count / k if k else 0.0
    recall = hit_count / len(relevant) if relevant else 0.0

    # AP:只对真实相关物品的位置求 Precision,再除以相关物品总数
    hit_so_far = 0
    precision_sum = 0.0
    for rank, is_hit in enumerate(hits, start=1):
        if is_hit:
            hit_so_far += 1
            precision_sum += hit_so_far / rank

    average_precision = (
        precision_sum / len(relevant) if relevant else 0.0
    )

    # 二元 NDCG
    dcg = sum(
        1.0 / math.log2(rank + 1)
        for rank, is_hit in enumerate(hits, start=1)
        if is_hit
    )

    ideal_hit_count = min(len(relevant), k)
    idcg = sum(
        1.0 / math.log2(rank + 1)
        for rank in range(1, ideal_hit_count + 1)
    )
    ndcg = dcg / idcg if idcg else 0.0

    return {
        "precision@k": precision,
        "recall@k": recall,
        "AP": average_precision,
        "NDCG@k": ndcg,
    }


recommended = ["A", "X", "B", "Y", "C"]
relevant = {"A", "B", "C"}

print(ranking_metrics(recommended, relevant, k=5))

预期输出近似为:

{
  'precision@k': 0.6,
  'recall@k': 1.0,
  'AP': 0.7555555555555555,
  'NDCG@k': 0.8854598815714874
}

这里的 relevant 必须来自测试时间窗口,而不能使用训练阶段已见标签。若 relevant 为空,Recall 和 AP 的定义需要统一处理;不同评测框架可能选择跳过该用户或返回 0,比较模型时必须保持一致。

6.5 AUC 的含义与边界

AUC 可以理解为:随机抽取一个正样本和一个负样本时,模型把正样本排在负样本前面的概率。

如果有 2 个正样本和 3 个负样本,共有 2×3=62\times3=6 对。模型正确排好 5 对,则:

AUC=560.833AUC=\frac{5}{6}\approx0.833

AUC 适合评价全局成对排序能力,但它不直接反映前 KK 位质量。一个模型可能 AUC 很高,却在最顶部没有足够好的结果,因此推荐系统通常同时看 Recall@K、NDCG@K、业务转化和分群指标。


七、离线评测中的数据偏差

7.1 未曝光不等于负样本

如果用户点击了物品 AA,但没有点击物品 BB,至少有三种解释:

  1. 用户看到了 BB,但不感兴趣;
  2. BB 排在很后面,用户没有看到;
  3. BB 根本没有被召回。

只有第一种解释接近明确负反馈。把所有未点击物品当作负样本,会把召回失败误认为用户拒绝。

因此隐式反馈训练通常使用:

  • 曝光后未点击作为较可信负样本;
  • 按曝光概率进行加权;
  • 对不同位置设置不同置信度;
  • 进行负采样并分析采样分布。

7.2 位置偏差

用户更容易点击排在前面的结果,即使它们内容质量相同。日志中的点击概率可粗略表示为:

P(click)=P(examine position)P(clickexamine,relevance)P(click) = P(examine\ position) \cdot P(click\mid examine, relevance)

如果训练模型直接把点击当作纯粹的相关性标签,模型会学习“排在前面更容易被点击”,形成自我强化。

离线修正位置偏差需要曝光、位置、候选集合等日志信息。仅有点击日志而没有曝光日志,通常无法可靠还原未观察结果。

7.3 训练—服务偏差

离线特征和在线特征必须语义一致。以下情况会导致离线指标虚高:

  • 离线使用当天结束后的统计值;
  • 线上实时特征缺失时默认为 0;
  • 训练时使用了线上不可获得的人工字段;
  • 物品状态在测试时已发生变化;
  • 离线候选集合来自未来数据,而线上候选来自历史索引。

因此离线评测不只是运行一个指标函数,还包括数据时间、候选生成、特征快照和标签定义的验证。


八、机器学习、深度学习与生成式 AI 在推荐中的位置

传统机器学习模型,如逻辑回归、梯度提升树和排序树,通常适合结构化特征、训练成本可控和可解释性要求较高的场景。

深度学习模型可以处理更复杂的关系:

  • 序列模型建模用户最近行为顺序;
  • 双塔模型生成用户和物品向量;
  • 多任务模型同时预测点击、购买和留存;
  • Transformer 建模较长的行为序列;
  • 图模型表示用户、物品、作者和类别之间的关系。

但更复杂的模型并不会自动解决标签偏差、权限错误和数据泄漏。模型表达能力提升后,错误数据可能被拟合得更充分。

生成式 AI 可以用于:

  • 从自然语言查询生成检索意图;
  • 总结用户兴趣或物品内容;
  • 生成内容标签和结构化属性;
  • 对候选结果进行解释;
  • 在检索增强系统中根据候选文档生成答案。

它也可能作为候选生成器或重排器,但必须区分两个问题:

  1. 生成模型是否产生了语义上合理的结果;
  2. 生成结果是否真实存在、用户是否有权访问、业务上是否允许展示。

对于知识库或文档推荐,较安全的路径通常是先进行权限过滤和检索,再让模型基于已授权候选生成解释。不能让语言模型“猜测”一个不存在的文档,也不能把模型生成的文本当作权限判定依据。

生成式模型的离线评测还需要检查事实一致性、引用正确性、越权率和有害内容,而不仅是点击率或文本相似度。


九、权限、状态和故障路径

推荐系统处理的不是静态物品列表。物品会下架、删除、过期、缺货或改变访问范围;用户权限也会变化。

一个候选物品至少应经过:

候选生成
  -> 物品存在性检查
  -> 内容状态检查
  -> 用户权限检查
  -> 地域、库存、合规检查
  -> 排序
  -> 重排

权限检查有两种常见实现:

  • 召回时过滤:索引本身携带权限信息,检索阶段只返回可访问物品;
  • 召回后过滤:先取候选,再根据用户权限过滤,并进行过召回以补足列表。

后者实现较容易,但如果过滤比例高,最终列表可能不足;前者更高效,但权限变更后索引同步不及时会产生越权风险。对于敏感文档,权限检查应以实时或强一致的数据源为准,不能只依赖过期缓存。

故障也会沿着数据流传播:

  • 向量索引不可用:切换到缓存候选或热门召回;
  • 用户画像服务超时:使用匿名或会话级特征;
  • 排序模型加载失败:使用上一稳定版本;
  • 权限服务不可用:宁可少展示,也不能默认放行;
  • 行为日志延迟:暂停依赖实时统计的更新,避免写入错误特征。

降级策略必须区分“可以降低质量”和“不能违反安全约束”。推荐为空通常是质量问题;展示未授权内容是安全问题,二者不能用同样的 fallback 处理。


十、成本和延迟是模型设计的一部分

在线请求的总延迟可近似表示为:

Ttotal=Tfeature+Trecall+Tfilter+Trank+Trerank+TnetworkT_{total} = T_{feature} + T_{recall} + T_{filter} + T_{rank} + T_{rerank} + T_{network}

多路召回若串行执行,延迟近似相加;若并行执行,延迟更接近最慢通道,但会增加并发连接、内存和故障复杂度。

排序成本还与候选数和模型复杂度有关:

CostrankCu×CostmodelCost_{rank} \approx |C_u|\times Cost_{model}

因此将候选从 1,000 个扩大到 10,000 个,不只是多保留一些结果,还可能显著增加精排推理成本。常见工程取舍是:

  • 召回阶段使用缓存、倒排索引或近似向量检索;
  • 粗排先缩小候选规模;
  • 精排只使用必要的高价值特征;
  • 对高延迟模型设置超时和降级;
  • 记录每一路召回的成本和命中贡献。

成本评估应覆盖训练、特征计算、索引构建、在线推理、日志存储和人工审核。一个离线指标略高但推理成本数倍增加的模型,未必适合生产。


十一、常见失败表现与诊断方法

11.1 召回率高,但最终点击差

可能原因是:

  • 召回集合相关物品很多,但精排特征错误;
  • 训练标签与线上目标不一致;
  • 候选中重复内容过多;
  • 排序模型过度依赖热门特征;
  • 权限或库存过滤后剩余候选发生变化。

诊断时应分阶段计算:

RecallretrievalRecallfinalRecall_{retrieval} \quad\text{与}\quad Recall_{final}

如果召回阶段命中率高而最终列表命中率低,问题更可能在排序或重排,而不是召回。

11.2 离线指标上升,线上指标下降

常见原因包括:

  • 随机切分造成时间泄漏;
  • 测试候选不是线上真实候选;
  • 离线负样本太容易;
  • 线上流量分布发生变化;
  • 点击标签受到位置偏差影响;
  • 新模型增加了延迟,页面实际展示失败;
  • 线上过滤规则与离线不一致。

应同时比较训练、验证、时间外测试和线上分群结果,并记录模型版本、特征版本、候选版本和规则版本。

11.3 结果过于热门和同质化

这通常不是“模型不够深”,而是目标和数据共同造成的:

  • 热门物品拥有更多曝光;
  • 点击目标奖励短期吸引力;
  • 重复内容没有惩罚;
  • 探索流量不足;
  • 新物品缺乏进入候选集的路径。

可以通过独立内容召回、探索配额、列表多样性重排和分群指标确认原因,而不是只观察总体 CTR。

11.4 新用户结果不稳定

新用户阶段样本量小,单个行为会显著改变画像。应检查:

  • 行为时间衰减是否过快;
  • 是否把误触当作强正反馈;
  • 会话行为和长期画像是否混用;
  • 是否有稳定的热门和内容兜底;
  • 探索结果是否受权限和内容质量约束。

十二、如何建立可信的评测闭环

一个可复现的离线评测流程至少包含以下状态:

原始日志
  -> 去重与清洗
  -> 按时间生成特征快照
  -> 构造未来标签
  -> 生成与线上一致的候选
  -> 应用权限和业务过滤
  -> 执行模型排序
  -> 计算分阶段指标
  -> 按用户、物品、时间和流量分群

其中“生成与线上一致的候选”非常关键。如果离线评测直接给模型一个包含真实正样本的候选集合,排序指标可能很好,但它没有评估召回模块;如果离线只评估召回,不评估最终列表,也无法发现精排问题。

评测报告至少应区分:

  • 召回覆盖率;
  • Precision@K、Recall@K、NDCG@K、MAP 或 AP;
  • 新老用户分群;
  • 新老物品分群;
  • 长尾与热门分群;
  • 不同地域、设备和入口;
  • 延迟、超时率、降级率;
  • 权限过滤命中与越权失败数;
  • 成本和资源消耗。

离线指标只能回答“在给定历史数据和评测协议下,模型表现如何”。线上实验还需要观察真实曝光反馈,因为推荐系统会改变用户看到的内容,用户行为又会反过来改变下一轮训练数据。这个反馈回路意味着模型上线后,数据分布本身可能发生变化。

因此,一个完整的推荐系统不是“协同过滤模型加一个排行榜”,而是由候选生成、排序、约束、反馈日志、评测协议和资源控制共同构成的生产系统。召回决定哪些可能性能够被看见,排序决定它们出现的先后,协同过滤利用群体行为补足偏好信号,冷启动处理证据不足的阶段,而离线评测负责在上线前验证这些环节是否按照预期工作。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。