系列 · 推荐系统 · 第 3 篇

推荐系统(三)—— 深度学习基础模型

从 MLP 到 Embedding,再到 NeuMF、YouTube DNN、Wide & Deep —— 用渐进的方式讲清深度学习推荐系统的每一块基石,附经过原文核对的架构图和可直接运行的 PyTorch 代码。

2016 年 6 月,Google 发表了一篇仅一页的论文,悄然重塑了推荐系统的格局。该文介绍了 Wide & Deep Learning——当时正驱动着十亿用户规模的 Google Play 应用商店推荐。短短一年内,各大科技公司纷纷将深度模型投入生产;到 2019 年,行业标准已然转变:矩阵分解不再是核心系统,而仅作为基线方法。

这一切为何发生?多层神经网络带来了经典方法无法企及的四大能力:

  • 学到的表征:Embedding 层取代 one-hot 向量,直接从点击行为中端到端地学习出稠密、语义化的向量。
  • 非线性交互:一个带 ReLU 的两层 MLP 能拟合 XOR 问题,而点积无能为力。
  • 多模态融合:文本、图像与行为序列可经由同一梯度流联合优化。
  • 端到端优化:无需手工设计特征交叉,损失函数自动决定哪些交互重要。

本文将带你从 dot(p_u, q_i) 出发,逐步演进至 NeuMF、YouTube DNN 和 Wide & Deep。所有架构均严格对照原始论文复现,并在每一步提供可直接运行的 PyTorch 代码。


推荐系统(三)—— 深度学习基础模型 — 章节概览图

你将对以下内容建立直观理解#

  • MLP 的核心直觉:为何堆叠线性层与 ReLU 能成为通用的交互引擎。
  • 嵌入(Embedding):不仅知道如何调用 nn.Embedding,更理解梯度为何会将相似 ID 拉近。
  • NeuMF(He 等,WWW 2017):双路径结构,共享一个优化目标。
  • YouTube DNN(Covington 等,RecSys 2016):当今所有大规模推荐系统采用的两阶段范式。
  • Wide & Deep(Cheng 等,DLRS 2016):记忆与泛化的教科书级融合。

前置知识#

  • 熟悉 PyTorch 基础(nn.Module、autograd、DataLoader)。
  • 已阅读本系列第二篇 (掌握矩阵分解、隐式与显式反馈的区别)。
  • 具备基本线性代数知识:点积、矩阵-向量乘法。

一、为什么是深度学习?为什么是现在?#

经典方法的天花板#

深度模型在 CTR 任务上对比 MF、FM、Wide & Deep、DeepFM、DIN 的 AUC,深度模型比 MF baseline 高 5%-13%

上述数据来自公开 CTR 基准(Criteo、Avazu、隐式反馈版 MovieLens-1M),具有典型性。它们传递出一致信号:每引入一种非线性机制,AUC 就能提升几个百分点——而这几个点对 GMV 的价值巨大。

要理解原因,需审视各类经典方法的表达能力。

$$\hat{r}_{ui} = \mathbf{p}_u^\top \mathbf{q}_i$$

简言之:每个用户和物品被表示为短向量,预测值即二者对齐程度。形式优美,但本质线性——无法捕捉“我同时喜欢科幻动作,但单独任一类都不喜欢”这类复杂偏好。

$$\hat{y}(\mathbf{x}) = w_0 + \sum_i w_i x_i + \sum_{i<j} \langle \mathbf{v}_i, \mathbf{v}_j\rangle x_i x_j$$

这是 MF 的严格超集,但仍止步于二阶。“年轻用户 × 周五夜晚 × 惊悚片”这类三阶效应,仍需人工构造交叉特征。

协同过滤则完全绕过建模,仅依赖相似用户或物品的匹配。它在矩阵稠密时效果良好,但生产环境中矩阵总是高度稀疏。

三者的共同局限在于:最多支持二阶交互,且严重依赖人工特征工程。

深度模型带来的突破#

理论上,含非线性的单隐藏层神经网络已是通用函数逼近器。实践中,这意味着:

  • 在 Embedding 之上叠加 MLP,可拟合数据所需的任意阶交互。
  • 同一骨干网络可联合处理图像(CNN)、文本(Transformer)和序列(RNN)。
  • 冷启动问题获得新解法:若新物品具备内容(标题、图片),预训练编码器可为其生成合理初始向量。

代价是更高的计算开销、更低的可解释性及更多超参数。第七节将介绍如何通过工程纪律使这一权衡物有所值。


二、MLP 的直觉:从点积到非线性交互#

在深入具体架构前,先理解 MLP 相较点积的本质优势。

点积 $\mathbf{p}^\top \mathbf{q} = \sum_k p_k q_k$ 仅对各维度乘积累加。它对称、线性,无法表达“特征 A 仅在特征 B 同时存在时才有效”这类逻辑。

$$f(\mathbf{p}, \mathbf{q}) = \mathbf{w}^\top \, \text{ReLU}\!\big(\mathbf{W} [\mathbf{p}; \mathbf{q}] + \mathbf{b}\big)$$

此时 ReLU 会根据输入维度的组合激活或抑制各隐藏单元。当隐藏单元足够多时,这正是通用逼近定理的体现。交互不再由固定公式定义,而是由数据驱动学习而来。

正是这一简单替换——以 MLP 替代点积——催生了 NeuMF、YouTube DNN 与 Wide & Deep。


推荐系统(三)—— 深度学习基础模型 — 章节小结图

三、Embedding:从稀疏 ID 到语义表征的桥梁#

One-hot 是敌人#

设想一个含 1000 万用户与 100 万物品的系统。若采用 one-hot 编码,每位用户将对应一个 1000 万维向量,仅一个位置为 1。这会同时引发三大问题:

  1. 存储与计算爆炸:单个用户输入即占 40 MB 浮点空间。
  2. 信息密度坍缩:99.99999% 的向量元素为零。
  3. 距离无差别:对任意 $i \ne j$ ,均有 $\|\mathbf{e}_i - \mathbf{e}_j\|_2 = \sqrt{2}$ 。用户 42 与 43 的距离,竟和与 9,999,999 的距离完全相同。

Embedding 层一举解决上述问题:它将每个 ID 映射至稠密向量(如 64 维)。训练完成后,兴趣相似的用户将在该低维空间中彼此靠近。

类比:one-hot 如同电话簿,每人独居一岛,岛间距离均等;Embedding 则像一位地理学家,重新排布岛屿,使关联人群相邻。此时,“寻找相似用户”便简化为最近邻搜索。

Embedding 的数学本质#

Embedding 层本质上是一个可学习矩阵 $\mathbf{P} \in \mathbb{R}^{m \times d}$ ,其第 $i$ 行即用户 $i$ 的向量。“查表”操作 $\mathbf{p}_i = \mathbf{P}[i, :]$ 在数学上等价于 $\mathbf{P}^\top \mathbf{e}_i$ ,但实际通过行索引高效实现。

代码仅需一行:

1
2
3
4
5
import torch
import torch.nn as nn

user_embedding = nn.Embedding(num_embeddings=10_000_000, embedding_dim=64)
user_vec = user_embedding(torch.LongTensor([42]))   # shape: [1, 64]

梯度如何赋予语义#

Embedding 初始为随机向量,其语义结构完全由梯度塑造。

以点击预测为例:当用户 $u$ 点击物品 $i$ ,损失函数指示优化器“增大 $\mathbf{p}_u^\top \mathbf{q}_i$ ”。反向传播会微调 $\mathbf{p}_u$$\mathbf{q}_i$ 靠拢,反之亦然。历经数百万次点击后,这一简单规则竟能涌现出惊人结构:受众重叠的物品彼此接近,常被共购的商品聚在一起,同艺术家歌曲形成簇群。

你从未定义“流派”,但梯度的几何演化自行发现了它。

可视化所学表征#

训练得到的物品 Embedding 经过 t-SNE 投影后形成科幻、动作、纪录片、爱情喜剧、恐怖等聚类,跨类型物品落在簇之间

将训练好的物品 Embedding 矩阵经 t-SNE 投影至二维,通常可见:同类物品紧密成簇,混合类型(如“科幻动作”)位于相关簇之间,长尾物品则散落外围。此图是判断模型是否有效学习的最佳诊断工具。

维度选择指南#

物品规模推荐 $d$说明
<10 万8 — 32更高易过拟合
10 万 — 100 万32 — 64多数场景的甜点区
100 万 — 1 亿64 — 128超过 128 收益递减
网级规模128 — 256需百亿级交互支撑

YouTube 论文及后续研究验证的经验法则:$d = 32$ 起始,逐步翻倍,直至验证集 AUC 提升不足 ~0.5% 即止。 内存与服务延迟随 $d$ 线性增长,而模型质量呈凹性增长。

干净、可复用的 Embedding 层#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import torch
import torch.nn as nn

class IdEmbedding(nn.Module):
    """带 Xavier 初始化和可选 padding 的 Embedding 层。"""

    def __init__(self, vocab_size: int, dim: int, padding_idx: int | None = None):
        super().__init__()
        self.emb = nn.Embedding(vocab_size, dim, padding_idx=padding_idx)
        nn.init.xavier_uniform_(self.emb.weight)
        if padding_idx is not None:
            with torch.no_grad():
                self.emb.weight[padding_idx].zero_()

    def forward(self, ids: torch.LongTensor) -> torch.Tensor:
        return self.emb(ids)

对于多字段分类特征(用户 ID、物品 ID、类目、城市等),为每字段分配独立嵌入表并堆叠:

1
2
3
4
5
6
7
8
class MultiFieldEmbedding(nn.Module):
    def __init__(self, field_dims: list[int], dim: int):
        super().__init__()
        self.tables = nn.ModuleList([IdEmbedding(v, dim) for v in field_dims])

    def forward(self, x: torch.LongTensor) -> torch.Tensor:
        # x: [batch, num_fields] -> [batch, num_fields, dim]
        return torch.stack([t(x[:, i]) for i, t in enumerate(self.tables)], dim=1)

所得 [batch, num_fields, dim] 张量,即为后续所有模型的标准输入格式。


四、神经协同过滤(NCF 与 NeuMF)#

核心思想:用更智能的方式替代点积#

He、Liao、Zhang、Nie、Hu 与 Chua 在 WWW 2017 提出 Neural Collaborative Filtering,其主张简洁有力:矩阵分解实为神经网络的特例,泛化后性能更优。

论文提出三种变体:

  • GMF(Generalized Matrix Factorization):可学习加权的点积推广。
  • MLP:纯深度拼接,无内积归纳偏置。
  • NeuMF:融合 GMF 与 MLP,两路径使用独立嵌入。

实践中,NeuMF 最具价值。下图架构严格遵循原论文。

NeuMF 架构#

NeuMF 架构图:左侧 GMF 路径通过用户/物品嵌入的元素积计算,右侧 MLP 路径通过拼接嵌入并经过多层 ReLU 层,顶部融合两条路径输出并通过 sigmoid 输出

自底向上解读:

  1. 双嵌入表设计:GMF 与 MLP 不共享嵌入——论文证实此举关键。各路径学习适配自身目标的表征。
  2. GMF 路径:用户与物品嵌入的逐元素积 $\mathbf{p}_u^\text{GMF} \odot \mathbf{q}_i^\text{GMF}$ ,顶部加可学习权重,构成点积的泛化形式。
  3. MLP 路径:拼接 $[\mathbf{p}_u^\text{MLP}; \mathbf{q}_i^\text{MLP}]$ ,经 2–3 层 Dense + ReLU(典型尺寸:$128 \to 64 \to 32$ )。
  4. 融合层:拼接两路径输出,经 sigmoid 投影为标量: $\hat{y}_{ui} = \sigma\!\left(\mathbf{h}^\top \begin{bmatrix} \mathbf{p}_u^\text{GMF} \odot \mathbf{q}_i^\text{GMF} \ \mathbf{z}_L^\text{MLP} \end{bmatrix}\right)$ 针对隐式反馈(点击、播放、购买),采用二元交叉熵损失: $\mathcal{L} = -\sum_{(u, i) \in \mathcal{D}^+ \cup \mathcal{D}^-} \big[ y_{ui} \log \hat{y}_{ui} + (1 - y_{ui}) \log(1 - \hat{y}_{ui}) \big]$ 负样本集 $\mathcal{D}^-$ 通过采样构建——通常每正样本配 4 个负样本。

NeuMF 端到端实现#

 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
import torch
import torch.nn as nn

class NeuMF(nn.Module):
    """NeuMF:融合 GMF 和 MLP,两条路径独立嵌入(He 等,2017)。"""

    def __init__(
        self,
        num_users: int,
        num_items: int,
        embedding_dim: int = 32,
        mlp_layers: tuple[int, ...] = (128, 64, 32),
        dropout: float = 0.0,
    ):
        super().__init__()
        # 两条路径独立嵌入——这是 NeuMF 的关键设计。
        self.user_gmf = nn.Embedding(num_users, embedding_dim)
        self.item_gmf = nn.Embedding(num_items, embedding_dim)
        self.user_mlp = nn.Embedding(num_users, embedding_dim)
        self.item_mlp = nn.Embedding(num_items, embedding_dim)

        # MLP 塔,输入是拼接后的用户/物品向量。
        layers, in_dim = [], embedding_dim * 2
        for out_dim in mlp_layers:
            layers += [nn.Linear(in_dim, out_dim), nn.ReLU()]
            if dropout > 0:
                layers.append(nn.Dropout(dropout))
            in_dim = out_dim
        self.mlp = nn.Sequential(*layers)

        # 输出头接收 [GMF 向量;MLP 向量]。
        self.head = nn.Linear(embedding_dim + mlp_layers[-1], 1)

        for emb in (self.user_gmf, self.item_gmf, self.user_mlp, self.item_mlp):
            nn.init.normal_(emb.weight, std=0.01)

    def forward(self, users: torch.LongTensor, items: torch.LongTensor) -> torch.Tensor:
        gmf = self.user_gmf(users) * self.item_gmf(items)           # [B, d]
        mlp_in = torch.cat([self.user_mlp(users), self.item_mlp(items)], dim=-1)
        mlp_out = self.mlp(mlp_in)                                  # [B, mlp_layers[-1]]
        logits = self.head(torch.cat([gmf, mlp_out], dim=-1)).squeeze(-1)
        return torch.sigmoid(logits)

# 冒烟测试
model = NeuMF(num_users=10_000, num_items=5_000, embedding_dim=32, dropout=0.2)
users = torch.randint(0, 10_000, (8,))
items = torch.randint(0, 5_000, (8,))
labels = torch.randint(0, 2, (8,)).float()
preds = model(users, items)
loss = nn.BCELoss()(preds, labels)
print(f"preds={preds.detach().numpy().round(3)}  loss={loss.item():.4f}")

NeuMF 实战训练技巧#

论文中三个易被忽视的关键点:

  • 分路径预训练:先独立训练 GMF 与 MLP,再用其权重初始化 NeuMF。仅此一步即可提升约 2% AUC。
  • 负采样比例:“1 正 : 4 负”为黄金标准;1:1 易欠拟合,1:10 浪费算力。
  • 偏置项免 L2 正则:对偏置施加 weight decay 会损害性能,需在优化器参数组中排除。

五、YouTube DNN:驱动互联网的两阶段推荐范式#

Covington、Adams 与 Sargin(RecSys 2016)是深度学习时代引用最高的工业推荐论文。其“召回 + 排序”两阶段架构,已成为 TikTok、Spotify、Pinterest、Instagram、淘宝等几乎所有大规模推荐系统的模板。

为何需要两阶段?#

无法为每次请求对十亿视频实时打分,亦难在实时场景中为海量物品应用丰富特征。解决方案如下:

  1. 召回阶段:用轻量模型与廉价特征,将候选集从百万级压缩至数百。
  2. 排序阶段:用重型模型与丰富特征,对数百候选精排,选出 top-K。

架构详解#

YouTube DNN 两阶段流水线:左侧召回塔输入观看历史、搜索 token、地域、年龄、性别;右侧排序塔用更丰富的特征预测期望观看时长

召回模型被建模为极端多分类任务:“给定用户状态,预测其下一个观看的视频(从百万候选中)”。用户塔对近期观看视频的 Embedding 做平均池化,拼接人口统计特征,经三层 ReLU($1024 \to 512 \to 256$ )输出 256 维用户向量。训练时采用 sampled softmax 损失;上线时,视频 Embedding 存入 ANN 索引(HNSW 或 ScaNN),用户向量作为查询向量执行近邻搜索——十亿规模仅需个位数毫秒。

排序模型为更深的前馈网络($1024 \to 512 \to 256 \to 128$ ),特征更丰富:曝光视频 Embedding、同频道历史观看 Embedding、距上次观看时长Feed 中位置、语言匹配等。关键创新在于输出头采用加权 logistic 回归,目标为预测期望观看时长而非点击率。论文证明,此目标比单纯优化 CTR 更契合长期用户满意度。

YouTube 论文的三大遗产#

以下设计经时间检验仍极具价值:

  • 平均池化用户近期行为:成本低、易并行、效果强。序列模型(第六篇)仅在数据充足时才能超越。
  • 召回视为分类任务:Sampled softmax + ANN 检索已成为行业标准。
  • 标签选择至关重要:“观看时长”、“长点击”(>30 秒停留)或“完播”比裸点击更具泛化性。你的损失函数即产品目标。

PyTorch 召回塔骨架代码:

 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
import torch
import torch.nn as nn
import torch.nn.functional as F

class YouTubeCandidateTower(nn.Module):
    """召回塔:生成用于 ANN 检索的用户向量。"""

    def __init__(
        self,
        num_videos: int,
        num_searches: int,
        num_geos: int,
        embedding_dim: int = 64,
        user_dim: int = 256,
    ):
        super().__init__()
        self.video_emb = nn.Embedding(num_videos, embedding_dim, padding_idx=0)
        self.search_emb = nn.Embedding(num_searches, embedding_dim, padding_idx=0)
        self.geo_emb = nn.Embedding(num_geos, embedding_dim)

        in_dim = 3 * embedding_dim + 2  # 视频池化 + 搜索池化 + 地域 + 年龄 + 性别
        self.tower = nn.Sequential(
            nn.Linear(in_dim, 1024), nn.ReLU(),
            nn.Linear(1024, 512), nn.ReLU(),
            nn.Linear(512, user_dim), nn.ReLU(),
        )

    def forward(self, watched, searched, geo, age, gender):
        # watched, searched: [B, L],padding 过;对非 padding 的位置做 mean pool。
        v = self._masked_mean(self.video_emb(watched), watched != 0)
        s = self._masked_mean(self.search_emb(searched), searched != 0)
        g = self.geo_emb(geo)
        x = torch.cat([v, s, g, age.unsqueeze(-1), gender.unsqueeze(-1)], dim=-1)
        return F.normalize(self.tower(x), dim=-1)  # 归一化的用户向量,适合 cosine 检索

    @staticmethod
    def _masked_mean(emb: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
        m = mask.unsqueeze(-1).float()
        return (emb * m).sum(1) / m.sum(1).clamp(min=1.0)

训练时,用户向量与采样候选视频 Embedding 共同参与 softmax;上线时,用户向量直接与 ANN 索引做点积检索。此单塔架构,正是现代工业召回的核心引擎。


六、Wide & Deep:记忆与泛化的协同#

核心洞察#

Cheng 等人(DLRS 2016)发现:纯深度模型虽擅泛化,却易“过度平滑”——推荐看似合理实则不准;而带交叉特征的线性模型能精准记忆共现模式,却无法外推。

类比:记忆如那位说“你喜欢《盗梦空间》,必爱《信条》”的朋友——精准但保守;泛化则如建议“你爱烧脑悬疑,试试《Primer》”的朋友——视野广但偶有偏差。卓越推荐系统需二者合一。

其解法:联合训练,在 sigmoid 前直接相加两部分得分。

架构设计#

Wide & Deep 架构图:左侧 Wide 线性分支处理稀疏特征和手动交叉特征,右侧 Deep 分支通过三层 ReLU 层生成深度分数,两者相加后通过 sigmoid 预测点击概率

Wide 分支:线性层作用于原始稀疏特征及人工构造的交叉特征 $\phi(\mathbf{x})$ (如 installed_app=Pandora AND impression_app=YouTube),输出 $\hat{y}^w = \mathbf{w}^\top [\mathbf{x}, \phi(\mathbf{x})] + b$

Deep 分支:各分类字段独立嵌入后拼接,经三层 ReLU($256 \to 128 \to 64$ ),输出 $\hat{y}^d$

联合输出$\hat{y} = \sigma(\hat{y}^w + \hat{y}^d)$ 。两分支共享同一损失梯度,优化器动态平衡贡献。

论文采用双优化器策略:Wide 用 FTRL+L1(稀疏、可解释、自动选特征),Deep 用 AdaGrad/Adam(稠密、平滑)。此设计至关重要——单优化器会损害性能。

实现要点#

 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
import torch
import torch.nn as nn

class WideAndDeep(nn.Module):
    """Wide & Deep(Cheng 等,2016),用于二分类 CTR 预估。"""

    def __init__(
        self,
        wide_dim: int,                  # 稀疏特征 + 交叉特征向量的维度
        field_dims: list[int],          # Deep 端各字段的词表大小
        embedding_dim: int = 32,
        deep_layers: tuple[int, ...] = (256, 128, 64),
        dropout: float = 0.0,
    ):
        super().__init__()
        # Wide 端:一个大的稀疏线性层。
        self.wide = nn.Linear(wide_dim, 1)

        # Deep 端:每字段嵌入 + MLP。
        self.embeddings = nn.ModuleList([nn.Embedding(v, embedding_dim) for v in field_dims])
        in_dim = len(field_dims) * embedding_dim
        layers = []
        for out_dim in deep_layers:
            layers += [nn.Linear(in_dim, out_dim), nn.ReLU()]
            if dropout > 0:
                layers.append(nn.Dropout(dropout))
            in_dim = out_dim
        layers.append(nn.Linear(in_dim, 1))
        self.deep = nn.Sequential(*layers)

    def forward(self, x_wide: torch.Tensor, x_deep: torch.LongTensor) -> torch.Tensor:
        wide_score = self.wide(x_wide).squeeze(-1)
        deep_in = torch.cat([emb(x_deep[:, i]) for i, emb in enumerate(self.embeddings)], dim=-1)
        deep_score = self.deep(deep_in).squeeze(-1)
        return torch.sigmoid(wide_score + deep_score)

# 双优化器训练——很多教程忽略了这个生产级实现细节。
model = WideAndDeep(wide_dim=10_000, field_dims=[50_000, 10_000, 100, 20])
opt_wide = torch.optim.Adagrad(model.wide.parameters(), lr=0.05)
opt_deep = torch.optim.Adam(
    [p for n, p in model.named_parameters() if not n.startswith("wide.")],
    lr=1e-3, weight_decay=1e-5,
)

实践中可用单一 Adam 优化器成功训练,但 Wide+Deep 的精髓在于联合损失——绝非两个独立模型的简单集成。Wide 与 Deep 参数通过梯度相互影响,此交互方为核心。

直系后继者#

Wide & Deep 催生了一系列自动化交叉特征的模型:

模型“Wide” 替代方案年份
DeepFMFM 层自动学习二阶交叉2017
DCNCross Network 以 $O(d)$ 参数/阶学习任意阶交叉2017
xDeepFMCIN 显式建模高阶交叉2018
AutoInt特征嵌入上的自注意力机制2019

本系列第四篇 将深入探讨这些模型。


七、训练纪律:决定成败的关键细节#

正确架构仅是必要条件,远非充分条件。以下实践常对离线 AUC 的影响超过模型选择本身。

MF、NeuMF 和 Wide & Deep 在 50 个 epoch 上的训练动态图:左图展示 BCE 训练损失,深度模型达到更低的最小值;右图展示验证 AUC,模型间的差距保持稳定,并在 AUC 平台期标记了早停点

负采样策略#

隐式反馈下,每位用户有数千“负样本”(未曝光物品)。三种策略按复杂度排序:

  • 均匀随机采样:默认方案,在召回任务中 surprisingly robust。
  • 热度加权采样:热门物品采样概率更高——用户忽略爆款是强负信号。
  • Batch 内 / 难负样本:用同批其他正样本作负样本,或定期检索当前模型高分负样本。提升判别力但需精细调温。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import numpy as np

def sample_negatives(user_history: set[int], catalog_size: int, k: int = 4) -> list[int]:
    """均匀随机采样负样本,排除用户历史中的商品。"""
    out = []
    while len(out) < k:
        cand = int(np.random.randint(catalog_size))
        if cand not in user_history:
            out.append(cand)
    return out

优化器、调度与正则化#

适用于 >90% 推荐模型的合理默认配置:

1
2
3
4
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode="min", factor=0.5, patience=3
)
  • Dropout 0.2—0.3:用于 MLP 塔,更高值损害排序质量。
  • 梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)):应对 loss 突增。
  • 早停:监控验证集 AUC,patience 设为 5–10 轮。

完整训练循环#

 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
def train(model, train_loader, val_loader, *, epochs=50, patience=8, device="cuda"):
    model = model.to(device)
    bce = nn.BCELoss()
    opt = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
    sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, mode="min", patience=3, factor=0.5)

    best_val, bad = float("inf"), 0
    for epoch in range(1, epochs + 1):
        model.train()
        train_loss = 0.0
        for users, items, labels in train_loader:
            users, items, labels = users.to(device), items.to(device), labels.to(device)
            opt.zero_grad()
            preds = model(users, items)
            loss = bce(preds, labels)
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
            opt.step()
            train_loss += loss.item() * len(labels)
        train_loss /= len(train_loader.dataset)

        model.eval()
        val_loss = 0.0
        with torch.no_grad():
            for users, items, labels in val_loader:
                users, items, labels = users.to(device), items.to(device), labels.to(device)
                val_loss += bce(model(users, items), labels).item() * len(labels)
        val_loss /= len(val_loader.dataset)
        sched.step(val_loss)

        if val_loss < best_val - 1e-4:
            best_val, bad = val_loss, 0
            torch.save(model.state_dict(), "best.pt")
        else:
            bad += 1
            if bad >= patience:
                print(f"第 {epoch} 轮早停;最佳验证损失={best_val:.4f}")
                break
        print(f"轮次 {epoch:>3}  训练损失={train_loss:.4f}  验证损失={val_loss:.4f}")

指标需匹配任务#

场景主指标实质衡量
CTR 预测AUC正样本是否排在负样本前
CTR 预测LogLoss预测概率是否校准
Top-K 召回Recall@K, HitRate@K正确物品是否在 top K
Top-K 排序NDCG@K正确物品排名有多高
评分预测RMSE预测评分的均方误差

若用 RMSE 评估 CTR 或用 AUC 评估评分预测,即优化错方向。选定指标后务必文档化,切勿静默切换。


八、常见问题#

Q:Embedding 维度应设多大?
物品规模 <100 万时从 32 起,>100 万从 64 起。逐步翻倍直至验证 AUC 提升 <~0.5%。$d>256$ 几乎必过拟合,除非有百亿级交互。

Q:NeuMF 是否总优于 MF?
否。交互量 <100 万时,正则化良好的 MF 常胜 NeuMF——深度模型易过拟合。NeuMF 优势在 >1000 万交互且含丰富 side feature 时显现。

Q:Wide & Deep 能否仅用 Deep 分支?
可以(如 DeepFM、DCN 所做)。Wide 分支提供对高基数共现(如“装 X 应用者也装 Y”)的精确记忆。若业务依赖此类模式,保留 Wide;否则自动化交叉网络(DeepFM/DCN)通常是更优权衡。

Q:仅数百万用户时适用 YouTube DNN 吗?
物品 <10 万时,两阶段属过度设计。直接全量排序即可。当全量打分超出延迟预算时,再引入两阶段。

Q:全新物品(无交互)如何处理?
按效果排序:

  1. 内容初始化:用预训练编码器(BERT/CLIP)从文本/图像生成 Embedding。
  2. 类目均值初始化:取同类目物品 Embedding 均值。
  3. Bandit 探索:小流量曝光收集初始信号。

Q:如何防止深度推荐模型过拟合?
分层防御:

  • Embedding 上 weight decay $10^{-5}$
  • MLP 中 dropout 0.2–0.3
  • 验证 AUC 早停
  • 以上无效则减小 $d$ 仅当验证指标提升时才增加复杂度。

Q:如何加速训练?
80/20 法则:

  1. 首选 GPU(提速 10–100×)
  2. 增大 batch size(提升 GPU 利用率)
  3. 混合精度(torch.cuda.amp,提速 ~2×)
  4. DataLoader(num_workers > 0) 所有特征离线预计算,训练循环中禁用 join。

九、至此,我们抵达何处#

深度学习并未发明推荐系统,但它重构了三大约束:

  • 表征是学出的,非设计的。Embedding 替代 one-hot,梯度发现人力难及的结构。
  • 交互是任意阶的,非限于二阶。MLP 拟合数据所需的一切复杂性。
  • 流水线是端到端的,非拼接的。损失从预测直通原始 ID。

NeuMF 证明学得的交互优于固定内积;YouTube DNN 展示十亿级规模下召回与排序的拆分之道;Wide & Deep 揭示记忆与泛化应协同学习而非割裂组合。

本系列第四篇 将迈出下一步:CTR 预估模型如何自动化 Wide & Deep 仍依赖的人工交叉特征工程,涵盖 DeepFM、DCN、xDeepFM、AutoInt 与 FiBiNet。


本文是推荐系统系列的第 3 篇,共 16 篇。

本系列

推荐系统 16 篇

  1. 01 推荐系统(一)—— 入门与基础概念
  2. 02 推荐系统(二)—— 协同过滤与矩阵分解
  3. 03 推荐系统(三)—— 深度学习基础模型 当前
  4. 04 推荐系统(四)—— CTR 预估与点击率建模
  5. 05 推荐系统(五)—— Embedding 表示学习
  6. 06 推荐系统(六)—— 序列推荐与会话建模
  7. 07 推荐系统(七)—— 图神经网络与社交推荐
  8. 08 推荐系统(八)—— 知识图谱增强推荐系统
  9. 09 推荐系统(九)—— 多任务学习与多目标优化
  10. 10 推荐系统(十)—— 深度兴趣网络与注意力机制
  11. 11 推荐系统(十一)—— 对比学习与自监督学习
  12. 12 推荐系统(十二)—— 大语言模型与推荐系统
  13. 13 推荐系统(十三)—— 公平性、去偏与可解释性
  14. 14 推荐系统(十四)—— 跨域推荐与冷启动解决方案
  15. 15 推荐系统(十五)—— 实时推荐与在线学习
  16. 16 推荐系统(十六)—— 工业级架构与最佳实践

读有所得?

GitHub 关注我 → 新文周更

GitHub