推荐系统深度实战:从协同过滤、召回排序到深度学习与多目标精排的工程全解
推荐系统是现代互联网最隐秘也最赚钱的 AI 应用:它不直接"生成"内容,却决定了用户看到的世界。从抖音的沉浸式 feed,到电商的"猜你喜欢",再到广告的精准投放,背后都是一套精密的多阶段漏斗。本文从第一性原理拆解推荐系统的四阶段范式,给出协同过滤、向量召回、Wide&Deep / DeepFM 排序与重排的可落地实现,并附上一份可直接对照的生产陷阱清单。
一、第一性原理:为什么推荐必须是"漏斗"
推荐系统的核心矛盾是海量候选(百万~十亿物品)× 严格时延(百毫秒内返回)× 个性化精度。你不可能对十亿物品逐一跑一遍精排模型,于是工程上必然分层过滤,每层用更贵的模型处理更少的候选——这就是经典的召回 → 粗排 → 精排 → 重排四阶段漏斗。
| 阶段 | 候选规模 | 时延预算 | 模型复杂度 | 目标 |
|---|---|---|---|---|
| 召回 (Recall) | 全库 1e6~1e9 | ~10-50ms | 轻量(向量/规则) | 高召回、不漏 |
| 粗排 (Pre-rank) | ~1e3~1e4 | ~10ms | 双塔/小模型 | 快速截断 |
| 精排 (Rank) | ~1e2~1e3 | ~30-50ms | 深度模型 | 精准打分 |
| 重排 (Re-rank) | ~数十 | ~10ms | 规则/Listwise | 多样性、业务 |
理解这个漏斗是优化一切推荐系统的前提:在召回层追求"不漏",在精排层追求"准",在重排层追求"好"(多样性、合规、业务目标),各层职责不可混淆。
二、召回层:从协同过滤到向量召回
2.1 协同过滤:最朴素却最永恒的基线
协同过滤(CF)的核心是"物以类聚、人以群分"。Item-CF 基于共现矩阵找相似物品,实现简单、可解释、冷启动友好。
import numpy as np
from collections import defaultdict
def build_item_cf(train: list[tuple[int, int]], k: int = 20):
"""基于用户-物品正反馈(点击/购买)构建 Item-CF 相似度。"""
user_items = defaultdict(set)
cooc = defaultdict(lambda: defaultdict(int))
item_cnt = defaultdict(int)
for uid, iid in train:
user_items[uid].add(iid)
item_cnt[iid] += 1
for uid, items in user_items.items():
items = list(items)
for i in items:
for j in items:
if i != j:
cooc[i][j] += 1
sim = {}
for i, neighbors in cooc.items():
scored = []
for j, cij in neighbors.items():
# 改进余弦:惩罚热门物品,缓解哈利波特效应
denom = np.sqrt(item_cnt[i] * item_cnt[j])
scored.append((j, cij / denom))
sim[i] = sorted(scored, key=lambda x: -x[1])[:k]
return sim
def recall_by_cf(sim, user_hist: set, topn: int = 100):
score = defaultdict(float)
for iid in user_hist:
for j, s in sim.get(iid, []):
if j not in user_hist:
score[j] += s
return sorted(score.items(), key=lambda x: -x[1])[:topn]
注意"哈利波特效应":全局爆款物品与一切物品共现极高,会污染相似度。必须做热门惩罚(如上式分母)或引入 ItemCF 的归一化变体(Swing、似然比)。
2.2 向量召回:用 Embedding 突破共现稀疏
当物品/用户规模极大、共现极稀疏时,CF 力不从心。向量召回把用户与物品映射到同一向量空间,用 ANN(HNSW / IVF-PQ)做近邻检索——这正是本站 向量数据库内核工程一文所剖析的近似最近邻检索的生产落点。
import numpy as np
# 离线:双塔模型产出 user_emb / item_emb(维度 d=128)
# 在线:用 FAISS 做 TopK ANN
def ann_recall(item_embs: np.ndarray, user_emb: np.ndarray, topk: int = 100):
# item_embs: [N, d], user_emb: [d]
# 真实生产用 faiss.IndexHNSWFlat + nprobe,此处用精确内积演示语义
sims = item_embs @ user_emb
idx = np.argpartition(-sims, topk)[:topk]
return idx[np.argsort(-sims[idx])]
向量召回的优势是能捕捉语义相似("无线耳机"召回"蓝牙耳麦"),与 CF 的共现互补,实践中常多路召回并行(CF 一路 + 向量一路 + 热门一路 + 地理一路),再送入粗排融合。
三、排序层:从 LR 到 Wide&Deep 与 DeepFM
3.1 特征工程仍是地基
排序模型的输入是稠密/稀疏混合特征:用户画像(年龄、地域)、上下文(时段、设备)、物品特征(类目、价格)、交叉统计(该用户对该类目的历史 CTR)。特征一致性(离线训练与在线推理用同一套特征管道)是排序模型上线的头号坑。
3.2 Wide & Deep: memorization 与 generalization 兼得
Google 提出的 Wide&Deep 是工业排序的奠基架构:Wide 部分记忆高频交叉(记忆历史行为模式),Deep 部分泛化到未见组合。
import torch, torch.nn as nn
class WideDeep(nn.Module):
def __init__(self, wide_dim, cat_dims, emb_dim=16, hidden=(256,128,64)):
super().__init__()
self.wide = nn.Linear(wide_dim, 1, bias=False) # 记忆
self.embeddings = nn.ModuleList(
[nn.Embedding(c, emb_dim) for c in cat_dims]) # 类别特征嵌入
deep_in = sum(emb_dim for _ in cat_dims) + wide_dim
layers = []
for h in hidden:
layers += [nn.Linear(deep_in, h), nn.ReLU(), nn.BatchNorm1d(h), nn.Dropout(0.1)]
deep_in = h
self.deep = nn.Sequential(*layers, nn.Linear(deep_in, 1))
def forward(self, wide_x, cat_x):
e = torch.cat([emb(cat_x[:, i]) for i, emb in enumerate(self.embeddings)], dim=1)
deep_x = torch.cat([wide_x, e], dim=1)
return torch.sigmoid(self.wide(wide_x) + self.deep(deep_x))
3.3 DeepFM:让 FM 自动学交叉
Wide&Deep 的 Wide 部分仍需人工设计交叉特征。DeepFM 用 FM 层自动学习二阶特征交叉,免去人工特征工程,是更省心的工业默认选择之一。其 FM 部分与 Deep 部分共享嵌入,结构上更优雅。
四、重排层:从精准到"好"
精排给出每个物品的独立分数,但用户看到的是一个列表,需要兼顾多样性、新颖性与业务约束。
- 多样性:MMR(最大边际相关)或 DPP(行列式点过程)打散相似物品,避免首页全是同一类目。
- 业务规则:置顶履约、降权低毛利、合规过滤、去重。
- Listwise 重排:用生成式/序列模型直接产出最优顺序(如 DLCM、PRM),而非逐点重排。
import numpy as np
def mmr_rerank(items, scores, sim_matrix, lambda_=0.7, topn=10):
"""MMR:在相关性与多样性之间权衡,lambda 越大越偏相关性。"""
selected, candidate = [], set(range(len(items)))
while len(selected) < topn and candidate:
best, best_score = None, -1e9
for i in candidate:
max_sim = max([sim_matrix[i][j] for j in selected], default=0)
mmr = lambda_ * scores[i] - (1 - lambda_) * max_sim
if mmr > best_score:
best_score, best = mmr, i
selected.append(best); candidate.discard(best)
return selected
五、评估:离线指标与在线指标必须双轨
| 指标 | 类型 | 定义 | 说明 |
|---|---|---|---|
| Recall@K | 离线 | 前 K 结果命中相关集比例 | 召回层核心 |
| NDCG@K | 离线 | 考虑排序位置的归一化折损累计增益 | 精排核心 |
| MAP | 离线 | 平均精度均值 | 多相关项场景 |
| CTR | 在线 | 点击/曝光 | 业务北极星之一 |
| CVR | 在线 | 转化/点击 | 电商/广告核心 |
| 人均时长 | 在线 | 停留时长 | 内容平台核心 |
| 多样性指数 | 在线 | 列表内类目/作者熵 | 体验护栏 |
离线指标永远只是代理:NDCG 涨 1 个点,线上 CTR 未必涨。必须配合在线 A/B 实验(AA 校验、显著性检验、新奇效应剔除)才能下结论。
六、冷启动:推荐系统的阿喀琉斯之踵
- 用户冷启动:新用户无行为,靠人口属性/设备/地理做默认召回,快速积累行为后切换到个性化;用"兴趣探针"(主动展示多样性内容)加速探索(Exploration)。
- 物品冷启动:新物品无曝光数据,靠内容特征(类目、文本 Embedding)进向量召回,辅以"冷启动流量池"保量试探。
- 探索-利用(E&E):用 Thompson Sampling / UCB / 疫苗法在短期收益与长期探索间平衡,避免系统陷入信息茧房。
七、生产陷阱清单
| 陷阱 | 典型表现 | 根因 | 对策 |
|---|---|---|---|
| 特征穿越 | 离线 NDCG 虚高、线上无提升 | 训练用了推理时不可得的未来特征 | 严格时间戳切分 + 特征可用时间校验 |
| 线上线下特征不一致 | 离线好、线上崩 | 两套特征管道 | 特征平台统一,推理复用训练算子 |
| 哈利波特效应 | 推荐被爆款淹没 | 共现未做热门惩罚 | 改进余弦 / Swing / 似然比 |
| 召回不足 | 精排再强也救不回 | 召回漏掉了好物品 | 多路召回 + 召回层独立评估 Recall@K |
| E&E 缺失 | 信息茧房、新内容无曝光 | 纯利用策略 | 引入 Thompson Sampling / 冷启动流量池 |
| 评估指标错配 | 离线涨、业务跌 | 离线指标非业务代理 | 离线+在线双轨,A/B 显著性校验 |
| 重排忽略多样性 | 首页同质化、用户流失 | 只看逐点分数 | MMR/DPP + 多样性护栏 |
八、与 LLM / RAG 的桥接
推荐系统并非与本站其它主题割裂:
- 与向量数据库:向量召回就是 ANN 检索,本站
向量数据库内核工程:从 IVF-PQ 到 HNSW 再到 DiskANN一文剖析的索引结构正是召回层的引擎。 - 与 LLM / RAG:生成式推荐正成为新范式——用 LLM 理解用户意图与物品语义,用 RAG 注入实时库存/上下文,再用传统排序做精排融合,弥合"语义理解"与"工业级时延/规模"的鸿沟。
- 与多目标:现代精排普遍多目标(点击、时长、转化、留存),需 Pareto 最优或 ESMM/MMOE 这类多任务模型,与本站"多智能体/多目标优化"的工程思想同源。
九、总结
推荐系统的工程本质,是用分层漏斗在"规模、时延、精度"的不可能三角中求解:召回层用向量与共现"不漏",排序层用深度学习"打准",重排层用多样性与业务规则把"准"变成"好"。落地时务必守住三条生命线——特征一致性、召回独立评估、离线在线双轨实验——并时刻警惕冷启动与信息茧房。当这套体系跑顺,推荐才真正从"猜你喜欢"进化为"懂你所需"。

发表评论 取消回复