Skip to content

推荐系统

本页速览 从用户-物品交互出发,本文系统讲解协同过滤、矩阵分解(SVD/SVD++)到工业界"召回→排序→重排"三段式架构,并给出评估指标、冷启动、探索-利用与完整的 surprise 实战代码。

推荐系统 ​

一句话定义:推荐系统(Recommender System)是从用户与物品的历史交互中学习用户的兴趣模式,并在海量候选物品中自动筛选、排序、呈现最可能被用户喜欢的物品的软件系统。它是机器学习在互联网行业落地最广、商业价值最直接的场景之一——Netflix 声称其约 80% 的观看时长来自推荐,YouTube 的推荐系统每年直接影响数十亿用户的内容消费。

推荐问题的本质不是"预测一个数字",而是"在正确的时间,把正确的物品,以正确的顺序,推给正确的人"。这篇实战文章沿着一条从经典到现代的主线展开:先定义问题,再讲透协同过滤与矩阵分解这两代经典方法,然后剖析今天工业界的"召回→排序→重排"三段式架构,最后落到评估、冷启动、探索-利用与一段可运行的代码。读之前建议先浏览什么是机器学习与模型评估与验证建立框架。

一、推荐问题:定义与形式化 ​

1. 用户-物品交互矩阵 ​

推荐系统的一切数据都可以压缩成一张用户-物品交互矩阵(interaction matrix):行是用户,列是物品,单元格是二者之间的交互。这张矩阵有两种形态:

              物品1  物品2  物品3  物品4  物品5
   用户A       5      ?      4      ?      2
   用户B       ?      3      ?      5      ?
   用户C       4      ?      ?      1      3
   用户D       ?      ?      2      ?      4

   ? = 未观测到的交互(用户没见过,或见过没留下痕迹)

这张矩阵有两个决定性的统计特征,也是整个推荐系统技术栈的出发点:

  • 稀疏性:可观测的交互只占全部潜在交互的极小比例。MovieLens 10M 数据集的密度仅约 1%;真实电商平台这个比例往往是 10⁻⁴ 甚至更低。我们真正要做的,是根据少量观测值推测海量未观测值——这正是机器学习擅长的事。
  • 不确定性:单元格的值只是"该用户对该物品的某种反馈",不是用户内心的真实评分。显式打分可能是 5 星,但用户点开推荐页之后是否真的喜欢,是另一个问题。

2. 显式反馈与隐式反馈 ​

交互数据按反馈的"明示程度"分两类:

类型形式示例特点
显式反馈用户主动表达的偏好星级评分、点赞、收藏、差评信号强、语义清晰;但获取成本高、数据稀疏、易受评分偏差影响(多数人只给自己喜欢的打分)
隐式反馈行为留下的痕迹点击、浏览时长、播放、购买、转发数量巨大、几乎零成本;但只有正向信号可观测("没点"≠"不喜欢",可能只是没看到),噪声大,且反映的是"可及性"而非纯粹偏好

这个区别深刻影响建模方式。对显式反馈可以直接做"评分预测";对隐式反馈,主流做法是把行为二值化(有交互=1,无交互=0,然后加权),或者用观看时长、购买金额这类行为强度作为置信度。2008 年 Hu、Koren 与 Volinsky 提出的 ALS-WR 方法(见参考资料)就是专门为隐式反馈设计的矩阵分解,它把每个"没交互"的样本也纳入训练,只是置信度设得很低——这是隐式反馈建模的标准起点。

3. 评分预测 vs Top-N 排序 ​

推荐任务常被误当成一个回归问题,其实它分两个层次:

任务形态 A:评分预测(rating prediction)
  目标:尽可能准确地估计 r̂ᵤᵢ(用户 u 会给物品 i 打几分)
  度量:RMSE、MAE
  视角:全局误差最小化

任务形态 B:Top-N 排序(ranking / Top-N recommendation)
  目标:对每个用户给出一个长度为 N 的、尽可能让用户满意的前 N 列表
  度量:Recall@K、NDCG@K、AUC
  视角:头部排序质量最大化

这两个目标并不等价:一个把 RMSE 压到极低、对每部电影评分都猜得八九不离十的模型,可能把用户最想看的电影排在第 5 位;而一个"只关心头几名"的模型可能评分误差很大但推荐体验极好。工业界的真实产品(商品流、信息流、视频流)几乎全部是 Top-N 排序问题——用户看的是排序后的列表,不是模型输出的分数。这就是为什么本章第五节要单独讲排序类指标。

4. 问题变体 ​

除经典的双人矩阵补全外,推荐系统还有几个常见变体:

  • 序列推荐(sequential recommendation):把交互看成时间序列,预测"下一个要看的物品"(会话推荐、Next-Item Recommendation)。
  • 上下文感知推荐:把时间、地点、设备、天气纳入特征。
  • 多目标推荐:同时优化点击率、转化率、时长、满意度等互相矛盾的指标。
  • 社交/关系推荐:利用好友关系等图结构信息。

这些变体绝大多数仍以内核"用户×物品匹配度"为基础,下面两节的经典方法是一切变体的地基。

二、协同过滤:最经典的"人以群分" ​

1. 核心思想 ​

协同过滤(Collaborative Filtering,简称 CF)是推荐系统最古老也最核心的方法族,1994 年 GroupLens 团队的开创性论文(见参考资料)奠定了其基本框架。它的思想用一句话概括:

利用群体的集体行为来预测个体的偏好——不必理解物品内容,也不必理解用户动机,只需要"许多人和你做过相似的事,那么他们喜欢的很可能也是你喜欢的"。

协同过滤的推理闭环:

  用户A 喜欢 {物品1, 物品3, 物品5}
  用户B 喜欢 {物品1, 物品3, 物品7}        ← 与 A 高度相似
  问题:A 会喜欢物品7 吗?
  推断:很可能喜欢(B 与 A 相似,且 B 喜欢 7)

这个方法之所以叫"协同"过滤,是因为预测依赖于用户之间的协同信号,而不是物品自身的属性。与此相对的**基于内容的推荐(content-based)**用物品属性(电影的类型、演员,文章的标签、词向量)构造特征,只利用用户自己的历史,不需要"别人"的数据。两者的分工和融合是现代推荐系统的常态。

2. 基于用户的协同过滤(UserCF) ​

算法分三步:

① 相似用户:对目标用户 u,用交互记录计算他与所有其他用户的相似度,取 Top-K 邻居
② 候选生成:收集邻居们交互过、而 u 没交互过的物品
③ 打分排序:按"邻居对该物品的评分 × 邻居与 u 的相似度"加权求和,排序取前 N

预测评分公式(用相似度加权):

r̂ᵤᵢ = ( Σ_{v∈N(u)} sim(u,v) · rᵥᵢ ) / ( Σ_{v∈N(u)} |sim(u,v)| )

其中 N(u) 是用户 u 的邻居集合,sim(u,v) 是用户相似度。实际工程中更常用的是以均值为中心的加权:先减去用户自己的平均评分再加权,以消除"有人习惯给高分、有人习惯给低分"的评分尺度偏差。

3. 基于物品的协同过滤(ItemCF) ​

把上面的逻辑对调:先算物品之间有多像(被同一批人消费/评分的物品更像),再为用户推荐与他历史物品相似的物品。2001 年 Sarwar 等人的论文(见参考资料)系统化了这个方法,随后被 Amazon 大规模应用并写成了著名的论文 Amazon.com Recommendations: Item-to-Item Collaborative Filtering。

① 物品相似度:sim(i,j) = 同时交互过 i 和 j 的用户数 / 交互过 i 或 j 的用户数的某种归一化
② 候选生成:用户 u 的历史物品集 H(u),对 H(u) 中每个物品找 Top-K 相似物品,并集去重
③ 打分排序:r̂ᵤᵢ = Σ_{j∈H(u)} sim(i,j) · rᵤⱼ,排序取前 N

ItemCF 在工程上全面占优,原因是它把"物品相似度"做成了可以离线预计算的静态表(相似度不随用户变化),在线服务时只需要查表聚合,不需要实时做 N 次全量比较。同时它具备很强的可解释性——"因为你看过《星际穿越》,推荐《盗梦空间》"这句话可以直接摆给用户看,这对信任建立至关重要。

4. 相似度度量 ​

无论 UserCF 还是 ItemCF,"相似"都要有一个量化定义。三种常用度量:

度量公式(示意)特点
余弦相似度cos(u,v) = (u·v)/(‖u‖·‖v‖)只关心方向、不关心尺度;处理交互向量最常用
皮尔逊相关系数对每个向量先减均值再算余弦消除评分尺度偏差,显式评分场景更准
Jaccard 系数A∩B

一个重要的工程细节:冷门物品的相似度比热门物品的更有价值。两个物品都被 100 万人看过,相似度虚高但信息量低;两个物品都只有 50 人看过,同时被 40 人看过则强烈说明二者高度相关。为此常用 log 逆频率或"热门物品降权"来修正:相似度乘以 1/log(1+出现次数)。对热门物品打压制裁,是推荐系统中反复出现的主题——它同时服务于准确性、新颖性和公平性。

5. 两种 CF 的工程取舍 ​

维度UserCFItemCF
依赖用户-用户相似度(随用户增长而膨胀,重算成本高)物品-物品相似度(物品数相对稳定,可离线预计算)
在线成本高:需实时找相似用户低:查预计算相似度表即可
可解释性弱("和你相似的人看过")强("因为你看过 X")
新颖性较好,能跨领域推荐较差,容易陷入"和过去高度同质"的茧房
冷启动新用户几乎没有邻居新物品没有相似物品

两者共同的致命弱点是稀疏性下的失效:矩阵越稀疏,"相似"的邻居/物品越难找齐,预测质量断崖式下跌。矩阵分解正是为此而生。

三、矩阵分解:从隐因子看透偏好 ​

1. 背景:Netflix Prize ​

2006 年,Netflix 发起 Netflix Prize 竞赛:谁的算法能把评分预测的 RMSE 比 Netflix 自家(Cinematch)提升 10%,就奖励 100 万美元。竞赛公开了 1 亿条真实评分,历时近三年才被贝尔实验室团队(BellKor's Pragmatic Chaos)攻克。这场竞赛有两个历史意义:一是把**矩阵分解(matrix factorization)**从学术冷门推成推荐系统的标准方法;二是确立了"集成多模型"在排行榜竞赛中的统治地位。获奖方案的细节与竞赛本身见参考资料中的《The Netflix Prize》描述论文与 Koren 等人的综述。

2. 隐因子(latent factor)思想 ​

矩阵分解的出发点是对"评分矩阵为什么是这个形状"做一个结构性假设:

用户的偏好和物品的属性,都只是少数 k 个不可直接观测的隐因子的不同组合。

  • 一个"隐因子"可以粗俗地理解为一条偏好轴,比如"对科幻的喜爱程度""对文艺片的耐受度""对主流大片的接受度"。
  • 每个用户用一个 k 维向量 pᵤ 表示:他在各条轴上的取值。
  • 每个物品用一个 k 维向量 qᵢ 表示:它在各条轴上的取值。
  • 用户对该物品的喜欢程度 = 两个向量的点积(向量对齐则高,正交则低)。
隐因子(k 维)解读示意:

          因子1(科幻)   因子2(文艺)   因子3(主流)
  用户A      0.9          0.2         0.6
  用户B      0.1          0.8         0.3
  《星际穿越》 0.95         0.1         0.9     → A 会打高分,B 会打低分
  《海边的曼彻斯特》 0.05   0.85        0.2     → B 会打高分,A 会打低分

与聚类(见聚类)的关系:隐因子可以理解为一种"软聚类"——用户和物品都被分配到 k 个隐维度上,但不要求互斥。它们之间的区别在于,聚类只做"物以类聚",而矩阵分解直接以"预测打分"为目标反推出这些维度。

3. 数学形式与损失函数 ​

用 P(m×k 用户因子矩阵)和 Q(n×k 物品因子矩阵)逼近交互矩阵 R:

R ≈ P · Qᵀ , 即 r̂ᵤᵢ = pᵤᵀ qᵢ

在观测到的交互子集 Ω 上最小化带 L2 正则的均方误差:

L = Σ_{(u,i)∈Ω} (rᵤᵢ − pᵤᵀ qᵢ)² + λ( Σᵤ ‖pᵤ‖² + Σᵢ ‖qᵢ‖² )
      ↑ 拟合项:只在有观测的格子上算误差         ↑ 正则项:防止因子向量过大导致过拟合

正则化的意义要放在泛化框架下理解:观测格子越稀疏,自由参数(m×k + n×k 个)越多,越容易把已知评分背下来而丢掉对新评分的预测力。λ 是模型评估与验证中正则化与偏差-方差权衡在推荐场景的直接体现。

再引入**偏置(bias)**项之后,模型变成完整形态(这也是 surprise 库中 SVD 的默认模型,即业界俗称的 Funk SVD):

r̂ᵤᵢ = μ + bᵤ + bᵢ + pᵤᵀ qᵢ

其中 μ 是全局平均评分,bᵤ 是用户偏置(有些人天生给分高),bᵢ 是物品偏置(《肖申克的救赎》整体被高估)。偏置项极其廉价却极其有效——它吃掉了很大一部分可解释的方差,让因子向量专心刻画"用户×物品"的交互结构而非全局均值。

4. 与经典 SVD 的区别 ​

线性代数中的 SVD(奇异值分解)是精确的数学工具:任意稠密矩阵 R 都能唯一分解为 R = U·Σ·Vᵀ,其中 U、V 是正交矩阵,Σ 是对角阵。推荐领域说的"SVD"只是在思想上借了它的壳,实质上是两回事:

维度经典 SVD(线性代数)推荐领域的"SVD"(Funk SVD / 潜在因子模型)
输入稠密、完整的矩阵极端稀疏、含大量缺失值的交互矩阵
缺失值不存在是问题的主体,绝不能当作 0(用户没打分≠打 0 分)
目标精确重构整张矩阵只在观测值上最小化预测误差,忽略缺失值
分解性质唯一、正交、满秩分解非唯一,因子向量只服从损失函数
求解特征值分解(精确、昂贵)梯度下降/ALS(近似、可扩展)
本质数学工具带正则化的统计模型

一个必须记住的陷阱:如果把缺失值补零再直接做经典 SVD,等于把"没看过"硬说成"给 0 分",模型会被海量的假 0 淹没,预测严重有偏。所以真正的推荐 SVD 只在观测集 Ω 上优化——这就是"用 SGD 逐个采样观测样本更新"的做法。Simon Funk 在 2006 年正是以这种方式写了个只有几行的 SGD 程序,把 Netflix Prize 排行榜搅得天翻地覆,因此这个模型也被称作 Funk SVD。

5. 求解:SGD 与 ALS ​

矩阵分解的损失对因子向量可微,梯度下降是自然选择。对单个观测样本 (u,i),误差为 eᵤᵢ = rᵤᵢ − r̂ᵤᵢ,参数更新为:

pᵤ ← pᵤ + γ( eᵤᵢ·qᵢ − λ·pᵤ )
qᵢ ← qᵢ + γ( eᵤᵢ·pᵤ − λ·qᵢ )
bᵤ ← bᵤ + γ( eᵤᵢ − λ·bᵤ )
bᵢ ← bᵢ + γ( eᵤᵢ − λ·bᵢ )

(γ 为学习率,λ 为正则系数。)每轮把所有观测样本过一遍称为一个 epoch,几十个 epoch 通常即可收敛。随机梯度下降(SGD)的替代方案是交替最小二乘(ALS):固定 Q 时损失对 P 是凸的二次函数,有闭式解,于是交替求解 P 和 Q。ALS 的优点是可以并行化、且天然适配 Spark 这类分布式框架,工业界(如当年的 Yahoo! 音乐推荐)多用它。

6. SVD++:把隐式反馈喂进去 ​

前面说过隐式反馈信息量巨大,SVD++(Koren 2008,见参考资料)给出了优雅的融合方案:把用户的隐式交互(如浏览过、点击过的物品集合)编码进用户向量:

r̂ᵤᵢ = μ + bᵤ + bᵢ + qᵢᵀ ( pᵤ + |N(u)|^(-1/2) · Σ_{j∈N(u)} yⱼ )

其中 N(u) 是用户 u 产生过隐式行为的物品集合,yⱼ 是物品 j 的"隐式因子向量"。括号里的整体可以理解为"融合了隐式信号的用户兴趣向量"。这一项几乎不增加计算成本,却能在 Netflix 数据上显著提升精度——它告诉我们的经验是:用户没有明说、但行为暗示过的信号,值得被模型认真对待。SVD++ 是经典矩阵分解与深度学习双塔之间的关键桥梁:双塔模型本质上就是把"SVD++ 的显式项和隐式项"推广成了任意复杂度的神经网络函数。

四、现代工业架构:召回→排序→重排 ​

1. 为什么需要三段式 ​

经典 CF 和矩阵分解能对付"给几千部电影排序";但真实平台面对的是百万到十亿级的候选物品(淘宝的商品、YouTube 的视频、TikTok 的内容)。任何模型如果对全部候选逐一打分,在线延迟会爆炸。工业界因此把推荐拆成漏斗式的三段:

                         全量物品(百万~十亿级)
                                │
   ┌────────────────────────────▼────────────────────────────┐
   │  ① 召回(Recall):双塔 + ANN 向量检索                    │  全量 → ~1000 条候选
   │     目标:快。从海量候选中粗筛出"可能有兴趣"的子集        │
   └────────────────────────────┬────────────────────────────┘
                                ▼
   ┌────────────────────────────▼────────────────────────────┐
   │  ② 排序(Ranking):粗排 + 精排                          │  ~1000 → ~50 条
   │     目标:准。用更复杂的模型对候选精打细算                │
   └────────────────────────────┬────────────────────────────┘
                                ▼
   ┌────────────────────────────▼────────────────────────────┐
   │  ③ 重排(Re-rank):多样性、打散、业务约束、探索注入      │  ~50 → Top-N 展示
   │     目标:体验。让最终列表更合理、更可解释                │
   └────────────────────────────┬────────────────────────────┘
                                ▼
                           最终 Top-N 展示

每一层牺牲精度换速度、或牺牲速度换精度,各层模型复杂度递增而候选规模递减。向量化检索是支撑这个漏斗的第一块基石。

2. 召回(一):双塔模型 ​

2016 年 YouTube 发表的 Deep Neural Networks for YouTube Recommendations(arXiv:1606.07792,见参考资料)把推荐建模明确表述为"把用户和视频各编码成一个向量,用内积做匹配"——这就是**双塔模型(two-tower)**的标准蓝图:

  用户侧特征:                   物品侧特征:
  历史观看序列 (embedding)       物品 id (embedding)
  人口属性 (年龄/性别/地域)       物品类别 / 标签 / 时长 / 上线时间
  搜索词 / 上下文(时间设备)       内容特征 (文本/图像 embedding)
        │                              │
        ▼                              ▼
   user tower (MLP)              item tower (MLP)
        │                              │
        ▼                              ▼
     u_vec (k 维)                   v_vec (k 维)
        └───────────────┬──────────────┘
                        ▼
           匹配度 score = <u_vec, v_vec> (内积 / 余弦)

双塔之所以统治工业召回,核心优势是解耦带来的部署形态:

  • 物品塔离线预计算:所有物品的 v_vec 在模型更新后一次性算好,写入向量数据库(Faiss、Milvus 等,见参考资料中的 FAISS 论文)。
  • 用户塔在线推理:线上只算一次用户向量 u_vec,然后做一个 k 维向量的最近邻搜索。
  • 匹配度计算从"十亿次模型前向"降为"一次向量检索",延迟从秒级降到毫秒级。

训练时一个关键细节是负样本的采样。真实数据里只有"曝光且被点击"是正样本,而模型需要在全量物品中区分好坏,所以要用随机负采样补足负样本;直接取"曝光未点击"当负样本会引入选择偏差(能被曝光本身就不是随机的),随机采样又会让热门物品被过度惩罚,实践中往往两者结合(随机负采样为主 + 适量 hard negative)。损失函数常用采样后的 softmax 或二分类交叉熵,详见深度学习基础。

3. 召回(二):近似最近邻检索 ANN ​

双塔给出向量后,"找最相似的 k 个物品"就是最近邻搜索问题。精确 KNN 在十亿规模下不可行,工业界全部使用近似最近邻(ANN,Approximate Nearest Neighbor),核心思路是用"允许少量误差"换"百倍速度"。主流技术分三类:

方法思路代表
哈希类用随机超平面把向量空间切成桶,同桶视为候选LSH(局部敏感哈希)
量化类把向量空间量化成码本,用码字近似向量、按查表距离排序PQ(乘积量化)、IVF+PQ
图类建一张"向量为节点、相近为边"的图,从入口节点贪心走向查询点HNSW(分层小世界图)、NSG

其中 HNSW(Malkov & Yashunin,2016,见参考资料)是当下最流行的方案:它构建多层图,高层图"跳得远"负责粗定位,低层图"走得细"负责精定位,召回率-速度比极佳,Faiss、Milvus、Elasticsearch 都内置了它。ANN 引入的误差会传导到上层,因此工业上会用"向量检索出 2000 条 → 粗排模型过滤"来兜底。

4. 粗排与精排 ​

召回给出的上千条候选质量粗放,进入排序层精雕细琢。排序层又分两级:

  • 粗排(candidate reranking):用一个轻量模型(通常是浅层双塔或简单树模型)对上千条候选快速打分,截断到几百条,为精排省下宝贵的延迟预算。粗排追求"别把好苗子刷掉",要求比召回更准。
  • 精排(ranking):对几百条候选逐一预估 CTR(点击率)/ CVR(转化率)/ 观看时长 等目标,把分数精确排序。这是整个漏斗中模型最重的部分。

精排模型是监督学习标准框架的直接应用:特征 + 标签 + 模型。

特征体系(精排是"特征工程"的极致舞台):
  ├── 用户侧:id、年龄性别、历史点击分布、品类偏好向量
  ├── 物品侧:id、类目、价格、发布时间、近 7 天点击率
  ├── 交叉特征:用户偏好品类 × 物品品类、用户年龄 × 物品内容分级
  └── 上下文:时间、设备、渠道、展位

特征交叉是精排模型的核心竞争力。从 LR 手工交叉,到 FM(因子分解机) 用隐向量自动学二阶交叉,再到 DeepFM / Wide & Deep / DCN 用深度网络学高阶非线性交叉,精排模型沿着"自动特征交叉能力"这条线进化(详见特征工程与深度学习基础)。需要注意的是:模型能学的交叉越多,对特征质量和负采样设计的依赖越大——精排的收益一半来自模型结构,一半来自特征和样本。

5. 重排:让列表像"人排的" ​

精排给出的分数是单点最优,但用户看的是一个列表。逐点最优的列表往往呈现三大问题,都由重排层解决:

问题原因重排手段
同质化相似物品分数接近,占满前几名MMR(最大化边际相关性):贪心选取时对"与已选集合过于相似"的候选扣分
类目失衡用户点得多的一类霸榜打散:相邻 k 个位置不允许同类物品超过阈值
商业规则运营位、限流、广告插入硬约束插入,通常是规则引擎而非模型
探索缺失永远推荐最可能的,用户失去新鲜感按概率注入探索流量(与第七节衔接)

重排层还负责多样性-准确性这笔关键交易:列表多样性提升时,点击率往往先微降、但长期留存和满意度上升——因为用户"被猜透"的体验反而会厌倦。现代重排越来越多地用 DPP(行列式点过程) 这类"集合级"优化器替代启发式,从"逐点打分"走向"序列决策",这部分已经和强化学习产生交集。

五、评估:怎么知道推荐好不好 ​

推荐系统的评估是一个完整的方法论问题(建议先读模型评估与验证),它的特殊性在于:目标不是预测准某个数字,而是用户满意;而用户满意无法被单个公式完全刻画。评估分两层:离线用历史数据算指标,在线用流量实验验证。

1. 离线评估设置 ​

离线评估首先回答"在什么数据上、以什么方式评估"。标准做法:

① 切分:把交互矩阵按用户随机切分 train/test(80/20 或 5 折交叉)
        注意:按用户切分而非按行切分,防止同一用户的信息泄漏进训练集
② 训练:模型只在 train 上拟合
③ 预测:对 test 中的 (u, i) 打分
④ 度量:不同任务形态用不同指标(见下)

一个工业级细节是时间切分:推荐是强时间相关的任务,用"前 6 个月训练、后 1 个月测试"往往比随机切分更接近真实线上表现(模型服务于未来)。离线评估的完整注意事项(泄漏、偏差、切分陷阱)见评估实战。

2. 排序类指标:Recall@K 与 NDCG@K ​

Top-N 推荐用排序类指标。设对用户 u,系统给出前 K 个推荐列表 R_u(K),用户真实喜欢的物品集合为 G_u(测试集中有交互的物品):

Recall@K(覆盖率导向,也是很多论文的主指标):

Recall@K = |R_u(K) ∩ G_u| / |G_u|

它回答"用户喜欢的物品里,有多少被推荐到了前 K"。

NDCG@K(排序质量导向,来自信息检索的 DCG 家族,2002 年 Järvelin & Kekäläinen 提出,见参考资料):

DCG@K = Σ_{i=1}^{K} ( 2^rel_i − 1 ) / log₂(i + 1)      rel_i = 位置 i 物品的相关性(0/1 或分级)
NDCG@K = DCG@K / IDCG@K                                  IDCG  = 理想排序下的 DCG(最优排列)

NDCG 的聪明之处:位置越靠前,权重越大(log₂(i+1) 分母惩罚靠后的位次),且除以 IDCG 归一化后可在不同用户间平均。它比 Recall@K 更能捕捉"用户要的是前几名,而不是'在列表里' "这一诉求。

3. 点预估类指标:AUC ​

如果推荐被建模为二分类(点击/未点击),则用 AUC。AUC 的含义是"随机抽一个正样本、一个负样本,模型给正样本打更高分的概率",它不依赖阈值、对类别不平衡鲁棒,是排序类问题最通用的标尺。AUC 也可以直接用于评估 Top-N:把"被推荐到的正样本 vs 未被推荐到的正样本"比较。注意 AUC 衡量的是整体排序质量,对头部位置的敏感性远低于 NDCG,所以头部应用场景要 NDCG 优先。

指标问的问题适用
RMSE / MAE分数猜得多准?评分预测任务(研究传统)
Recall@K喜欢的物品被推荐到前 K 的比例?Top-N 推荐
NDCG@K推荐顺序是否把最相关的放最前?信息流/视频流排序
MRR第一个命中出现在第几位?单目标搜索式推荐
AUC正负样本的整体可分性?CTR 预估的离线代理
覆盖率 / 新颖度 / 多样性长尾物品是否也有机会?生态健康度,需组合使用

4. 在线评估:A/B 实验 ​

离线指标与用户真实体验之间存在系统性鸿沟:离线好 0.5% 的 NDCG,线上可能完全无感,甚至因为"过于精准而失去惊喜"导致留存下降。因此上线前必须做 A/B 测试:把流量随机分桶,实验组跑新模型、对照组跑旧模型,用统计检验对比业务指标(点击率、时长、购买、留存、人均推荐贡献)。在线评估的设计要点(样本量计算、多重比较、长期指标)是评估实战的主题,这里只强调一个原则:

离线指标用于筛选,在线实验用于决策。离线排名在前的模型不一定赢,赢的定义永远是线上业务指标。

六、冷启动:新用户与新物品 ​

推荐系统对"没有历史"的对象天然失能,这就是**冷启动(cold start)**问题,分三种:

类型场景典型表现
用户冷启动新注册用户没有历史交互,CF/矩阵分解无法算相似度与隐因子
物品冷启动新上架商品/新视频没有交互,无法被"相似物品"检索到
系统冷启动全新平台既无用户数据也无物品数据,只有内容或属性

缓解策略按"能否拿到更多信号"分几层:

① 借用内容/属性特征(内容推荐与 CF 的融合):
   新物品没有交互,但可以用其属性特征(类目、标签、文本 embedding、封面图像特征)
   估算一个"内容向量"作为冷启动期的物品向量——双塔模型里物品塔天然支持这种做法,
   因为物品向量本来就是由特征生成的。
② 流行度兜底(popularity backfill):
   冷启动用户先推热门榜 / 编辑精选,用"大众偏好"作为第一层先验,等攒够交互再个性化。
   注意:流行度兜底必须配合探索机制,否则新人永远只看到头部内容。
③ 主动获取信号(warm-up):
   注册时引导用户选择兴趣标签、看几张封面图做"隐式评分"(用户行为即反馈),
   用这些低成本信号快速初始化用户向量。
④ 探索性流量(exploration bucket):
   把新物品注入探索流量池,让"少量用户试水"产生第一批交互(详见第七节)。
⑤ 迁移学习 / 冷启动模型:
   用平台级"通用用户向量"或"物品内容 embedding"预训练,冷启动时直接套用
   (与[大语言模型](/case-studies/llm)的预训练-微调思想同源)。

冷启动还有一个常被忽略的评估问题:离线评测天然低估冷启动模型——测试集里本来就很少包含"新物品的交互",因此要单独构造"只含上线后前 7 天物品/前 3 次交互用户"的测试切片来验证冷启动表现,否则你的"冷启动优化"可能从未被检验过。这是常见陷阱里反复出现的"评估与目标错位"。

七、探索与利用:推荐不是一锤子买卖 ​

1. 为什么需要探索 ​

前面所有模型的假设是"最大化用户对已知物品的偏好"——这对应利用(exploitation):把当下预测最可能被喜欢的物品推给用户。但用户的偏好不是静态的,而且模型永远有不确定性:

  • 新物品、新内容需要被"试"才能产生数据;
  • 用户口味会漂移(看腻了类型 A,转向类型 B);
  • 模型对长尾物品的预测置信度很低,纯利用会让它们永远得不到曝光,形成"马太效应"。

纯利用的推荐系统会在短期点击率上最优,却在长期上萎缩:用户的新鲜感耗尽、长尾供给消失。探索(exploration)就是主动把一部分流量用于"测试未知",用今天的微小损失换明天的数据红利。这个问题在数学上正是强化学习中经典的探索-利用权衡(exploration-exploitation tradeoff),推荐系统本质上是持续在线决策,而非一次性监督学习。

2. 经典策略 ​

策略机制特点
ε-greedy以概率 ε 随机推荐(探索),以 1−ε 推最优(利用)最简单;ε 固定时收敛慢,长尾效果一般
UCB(上置信界)对候选按"期望收益 + 不确定度红利"打分,不确定越大的物品越有机会理论优雅,适合"物品臂"有限且可枚举的场景(如新闻推荐)
Thompson Sampling给每个物品维护一个贝叶斯后验分布,每次从后验中采样一次作为该物品的得分实践中效果最好、实现也简单,被多家公司采用

一个工程上很重要的现实约束:探索不能在精排层随心所欲。把"随机物品"塞进用户首页前 5 名,体验损失立竿见影。工业界的妥协是:

  • 分层探索:召回层加一路"探索通道"(如冷门新物品通道、随机通道),由重排层以很低的比例混入最终列表;
  • 探索预算:按流量比例分配(如 2% 探索流量),并把探索对象的反馈实时回灌训练。

3. 在线学习与模型更新 ​

探索产生的新数据必须被模型及时吸收,这引出在线学习(online learning):

离线训练(batch):每天/每小时用全量日志重训一次模型(离线全量+增量)
        ↓ 产出新一轮参数,作为线上推理的基线
在线学习(online):对实时流式日志做增量参数更新(如 FTRL、在线 SGD)
        ↓ 捕获秒级、分钟级的兴趣变化(热点事件、临时偏好)
模型服务:参数热更新 + 线上推理

主流做法是"离线重训为主、在线微调为辅":模型结构离线定期重训(保障稳定性),实时信号用在线学习快速吸收(保障时效性)。探索与在线学习合在一起,构成了推荐系统持续进化的闭环——这也正是从监督学习走向强化学习的自然延伸,完整框架见强化学习。

八、实战:用 surprise 实现矩阵分解推荐 ​

surprise(Surprise: A Python library for Recommender Systems,2017,见参考资料)是一个专注推荐算法的 Python 库,内置了 SVD、SVD++、KNN 协同过滤等算法和标准数据集。下面用它跑通一个完整流程。

1. 安装与数据 ​

bash
pip install scikit-surprise
python
from surprise import Dataset, Reader, SVD, accuracy
from surprise.model_selection import train_test_split

# 加载内置 MovieLens 100K 数据集(首次运行会自动下载)
data = Dataset.load_builtin("ml-100k")
# 数据格式:user id | item id | rating | timestamp

trainset, testset = train_test_split(data, test_size=0.2, random_state=42)
print(f"训练样本数: {trainset.n_ratings}, 用户数: {trainset.n_users}, 物品数: {trainset.n_items}")

2. 训练 SVD 并评估 ​

python
# SVD = μ + bᵤ + bᵢ + pᵤᵀqᵢ,即带偏置与正则的矩阵分解(Funk SVD)
model = SVD(n_factors=20, n_epochs=30, lr_all=0.005, reg_all=0.02)
model.fit(trainset)

predictions = model.test(testset)
print("RMSE:", accuracy.rmse(predictions))
print("MAE :", accuracy.mae(predictions))
# 典型输出:RMSE 约 0.94,MAE 约 0.74(MovieLens 100K 的合理水平)

3. 单点预测与 Top-N 推荐 ​

python
# 对 (user 196, item 302) 打分
pred = model.predict(uid="196", iid="302")
print(f"预测评分: {pred.est:.2f}  (真实评分存在时也会打印)")

# 手动实现 Top-N:对用户没交互过的物品全部预测并排序
def top_n_recommend(model, trainset, uid, k=10):
    # 用户已交互过的物品集合(训练集中)
    inner_uid = trainset.to_inner_uid(uid)
    seen = {iid for (iid, _) in trainset.ur[inner_uid]}

    # 对所有物品预测,排除已交互的,取前 k
    scored = []
    for inner_iid in trainset.all_items():
        if inner_iid in seen:
            continue
        raw_iid = trainset.to_raw_iid(inner_iid)
        est = model.predict(uid=uid, iid=raw_iid, verbose=False).est
        scored.append((raw_iid, est))
    scored.sort(key=lambda x: -x[1])
    return [iid for iid, _ in scored[:k]]

print("Top-10 推荐:", top_n_recommend(model, trainset, uid="196"))

surprise 还内置了 ItemCF(KNNBasic)、SVD++(SVDpp)等,可以直接横向对比,也可以用它算 NDCG 等排序指标。注意 surprise 面向研究教学,工业级实现在数据规模和分布式上需要工程化改造,但算法思想与这里的代码一一对应。

4. 手写一个 30 行的矩阵分解(理解本质) ​

抛开库封装,矩阵分解的 SGD 求解就是下面这段代码的全部:

python
import numpy as np

def funk_svd(R, k=10, lr=0.01, reg=0.02, epochs=40):
    """R: (m, n) 评分矩阵,0 表示缺失(不要当真实评分用)"""
    m, n = R.shape
    mu = R[R > 0].mean()
    P = np.random.randn(m, k) * 0.1      # 用户隐因子
    Q = np.random.randn(n, k) * 0.1      # 物品隐因子
    bu = np.zeros(m)                     # 用户偏置
    bi = np.zeros(n)                     # 物品偏置

    for _ in range(epochs):
        for u, i in zip(*np.where(R > 0)):        # 只在观测值上更新
            err = R[u, i] - (mu + bu[u] + bi[i] + P[u] @ Q[i])
            P[u] += lr * (err * Q[i] - reg * P[u])   # 梯度下降 + L2 正则
            Q[i] += lr * (err * P[u] - reg * Q[i])
            bu[u] += lr * (err - reg * bu[u])
            bi[i] += lr * (err - reg * bi[i])
    return P, Q, bu, bi, mu

# 一个 4 用户 × 5 物品的微型评分矩阵
R = np.array([
    [5, 0, 4, 0, 2],
    [0, 3, 0, 5, 0],
    [4, 0, 0, 1, 3],
    [0, 0, 2, 0, 4],
])
P, Q, bu, bi, mu = funk_svd(R, k=3)
user0_pred = P[0] @ Q.T + mu + bu[0] + bi
print("用户 0 对全部物品的预测评分:", np.round(user0_pred, 2))

这段代码中"只在 R > 0 的位置更新"这一行,就是矩阵分解区别于经典 SVD 的全部秘密。动手调一调 k、lr、reg,观察 RMSE 与因子向量的变化,比读十遍理论更有用——也别忘了用评估实战的方法严谨地评测你的改动。

九、权衡与取舍 ​

推荐系统是"多目标下求平衡"的工程,没有免费午餐,几组最常见的张力:

  • 准确率 vs 多样性/新颖性:优化 NDCG 的模型倾向于推荐"同类爆款",形成过滤气泡与信息茧房;牺牲一点头部准确率换来列表多样性,长期留存往往更高。用 MMR/DPP 做系统化的平衡,而不是事后拍脑袋。
  • 离线指标 vs 在线业务:离线 NDCG 提升 ≠ 在线点击率提升。离线评估在筛选候选模型、在线 A/B 在做最终决策——这条原则值得重复一百遍。
  • 模型复杂度 vs 服务延迟/成本:双塔快而糙、精排准而慢,漏斗分层就是在精度和延迟之间做预算分配。盲目加宽精排模型会让延迟预算失控。
  • 个性化 vs 流行度兜底:纯个性化冷启动失能、长尾能力弱;纯流行度毫无个性。正确的做法是按用户数据量自适应:数据少的用户多靠流行度,数据多的用户多靠个性化。
  • 利用 vs 探索:短期指标与长期健康的矛盾,见第七节。任何"纯最大化"的优化器都需要显式的探索预算。
  • 可解释 vs 表达力:ItemCF 能说出"因为你看过 X",深度双塔则是个黑箱;在需要向用户解释"为什么推荐这个"的场景(新闻、金融),解释性本身是产品价值。
  • 隐私 vs 个性化:越个性化的模型越需要细粒度的用户数据,在隐私合规的压力下,联邦学习、差分隐私、本地化建模正在成为新约束。
  • 简单模型 + 好特征 vs 复杂模型:Kaggle 与工业界的经验一致——先做线性/浅层基线,确认特征质量,再上深度模型。特征是上限,模型只是逼近它。

十、延伸阅读 ​

参考资料 ​