推荐系统(十四)—— 跨域推荐与冷启动解决方案

深入讲清冷启动与跨域推荐的整套打法:三种冷启动场景、EMCDR/PTUPCDR 跨域桥接、MeLU/MAML 元学习、UCB Bandit 探索利用,以及从冷到温的完整线上路由策略。

Netflix 进入一个新国家时,会接手数以百万计零历史的用户和一个本地评分为零的内容库。亚马逊每开一个新品类都要重演一遍。在这种情况下,把推荐系统撑起来的依靠的是一整套技术:首请求时用启发式 Bootstrap,几次交互之后切到元学习,遇到关联领域时做跨域迁移,模型有信心了再用 Bandit 持续探索。本文按这个工程顺序讲清楚每一层的来龙去脉,并直接对应到它们出处的论文。

你将学到什么

  • 三种冷启动场景 —— 新用户、新物品、新系统,分别该拉哪根杠杆
  • 跨域桥接 —— 从 EMCDR 的全局 MLP 到 PTUPCDR 的个性化元网络桥
  • 推荐里的元学习 —— MAML 的双层优化,以及 MeLU 是怎么把它落到推荐场景的
  • 探索 vs 利用 —— UCB1、ε-greedy、Thompson Sampling 的 regret 分析
  • 冷到温的过渡曲线 —— 不同方法在不同交互量下的胜率
  • 基于内容的兜底方案 —— 永远在线的安全网

前置知识

  • 协同过滤与矩阵分解(第 3-4 篇)
  • PyTorch 与梯度下降基本功(第 7 篇)
  • 愿意把一两个公式仔细看完

三种形态的冷启动

三种冷启动场景以用户-物品矩阵呈现:新用户对应一整行空白;新物品对应一整列空白;新系统则整张矩阵都接近空白

一个靠历史训练出来的推荐系统,在历史缺失时会以三种不同方式失效。每一种都对应矩阵里不同形状的空洞。

用户冷启动

一个用户刚注册。我们可能知道他的设备、IP 归属地、广告投放渠道,注册表单里也许填了年龄段。显式行为为零。他在评分矩阵里那一行是空的。一个数据点很直白:消费类 App 上首会话推荐质量差和 30 天流失率有大约 3 倍的相关。这一档的杠杆是从稀疏信号里推断 + 主动探索

物品冷启动

商家上架了新 SKU、片方上线了新片、创作者发了新视频。矩阵里多了一整列空白。协同过滤检索不到这件物品,因为没人和它共现过。如果没人为干预,新物品永远没曝光、永远拿不到评分、永远沉到底部 —— 这就是所谓的"强者恒强"。这一档的杠杆是内容特征:标题、封面、预训练 encoder 出来的 embedding,再加上从相似的"温"物品里借冷启分数。

系统冷启动

新平台上线,或者老平台扩到新业务线。整张矩阵几乎是空的,行和列同时稀疏。这一档的杠杆是迁移:从一个相关的、数据丰富的源域把 embedding、桥接函数甚至排序模型搬过来。

一个统一的形式化

给定用户集 $U$、物品集 $I$,交互矩阵 $R \in \mathbb{R}^{|U| \times |I|}$。冷启动子集定义为:对于 $u \in U_{\text{cold}}$ 满足 $|R_{u,\cdot}| < K$;$I_{\text{cold}}$ 同理。目标是预测至少一边是冷的 $(u, i)$ 对的 $R_{u,i}$。标准 CF 是从共现里学 embedding $\mathbf{e}_u, \mathbf{e}_i$;一旦那一行或那一列没有数据,embedding 要么压根不存在,要么就是个没有梯度信号的随机初始化。下面所有方法本质上都在回答同一个问题:当数据缺位时,我们用什么去替代 $\mathbf{e}_u$ 或 $\mathbf{e}_i$?


跨域推荐

跨域推荐流程图:源域交互 → 源域 embedding → 桥接函数映射 → 目标域 embedding → 目标域打分器为冷启动用户出分

直觉很老:一个钟意《2001 太空漫游》的用户大概率会读阿瑟·克拉克。他的电影行为是他对图书偏好的一个强先验,哪怕物品本身完全不一样。技术问题就一个:怎么把"强先验"变成数学 —— 也就是用什么函数把用户在电影域的表征映到他在图书域的表征。

EMCDR —— 全局 MLP 桥

Man 等人在 IJCAI 2017 提出的 EMCDR (Embedding and Mapping for Cross-Domain Recommendation) 把这件事讲得最干净。三步走:

  1. 分别训练域内 MF。在源域交互 $R^S$ 和目标域交互 $R^T$ 上各自跑一遍矩阵分解,得到用户 embedding $U^S, U^T$ 和物品 embedding $V^S, V^T$。
  2. 在重叠用户上学桥接。对于在两个域都出现过的用户 $i \in U_o = U^S \cap U^T$,训练一个 MLP $f_\phi$ 去最小化 $\sum_{i \in U_o} \|f_\phi(\mathbf{u}_i^S) - \mathbf{u}_i^T\|^2$。
  3. 给目标域冷用户打分。对一个只在源域出现过的用户,令 $\hat{\mathbf{u}}_i^T = f_\phi(\mathbf{u}_i^S)$,然后丢进目标域打分器。

桥接函数是全局的 —— 所有用户共享同一个 $f_\phi$。

PTUPCDR —— 把桥也个性化掉

EMCDR 的命门是:一个映射函数装不下"不同用户跨域翻译方式不同"这件事。一个恐怖片粉丝和一个纪录片粉丝映到图书域的方式显然不一样。Zhu 等人在 WSDM 2022 提出 PTUPCDR (Personalized Transfer of User Preferences for Cross-Domain Recommendation):与其学一个 $\phi$,不如用一个元网络根据用户在源域的行为,为每个用户生成一个 $\phi_i$

$$ \phi_i = h_\theta\bigl(\{\mathbf{v}_j^S : j \in \mathcal{H}_i^S\}\bigr), \qquad \hat{\mathbf{u}}_i^T = f_{\phi_i}(\mathbf{u}_i^S) $$

直白讲:读这个用户在源域的历史,把它压缩成一组桥接权重,再用这组个性化的桥去映他的源域 embedding。在 Amazon 跨品类的标准 benchmark 上,PTUPCDR 比 EMCDR 的 MAE 又降了 5–10%。

EMCDR 与 PTUPCDR 对比:左侧 EMCDR 所有用户共用一个 MLP;右侧 PTUPCDR 用元网络根据用户行为生成个性化桥接参数

一个最小可读的跨域骨架

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


class CrossDomainBridge(nn.Module):
    """EMCDR 风格的共享桥接模型。

    第 1 步(不在此处):源域和目标域各自预训练 MF。
    第 2 步:本模块在重叠用户上训练 f_phi。
    第 3 步:对目标域冷用户,用 f_phi(u^S) 出分。
    """

    def __init__(self, embedding_dim=64, hidden_dim=128):
        super().__init__()
        self.bridge = nn.Sequential(
            nn.Linear(embedding_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, embedding_dim),
        )

    def forward(self, user_emb_source: torch.Tensor) -> torch.Tensor:
        return self.bridge(user_emb_source)


def emcdr_loss(bridge, u_source, u_target_true):
    """只在重叠用户上训 —— 本质是 embedding 空间里的回归。"""
    u_target_pred = bridge(u_source)
    return torch.mean((u_target_pred - u_target_true) ** 2)

负迁移 —— 必须盯住的失败模式

跨域不是免费午餐。如果源域和目标域关联很弱(比如新闻阅读 vs. 生鲜购物),桥接反而会拖累目标域。识别信号很简单:跑一个只用目标域数据的 baseline 做 A/B,盯着冷用户那条指标。如果跨域在同样的训练成本下打不过 baseline,就说明这两个域离太远了。


元学习冷启动

跨域的故事建立在"你刚好有个相关的数据丰富的源域"上。元学习换了个角度:哪怕只在单域里,能不能让模型学会快速适应 —— 一个新用户只给我几次交互就能出分?

一段话讲清 MAML

Finn、Abbeel、Levine(ICML 2017) MAML (Model-Agnostic Meta-Learning) 想法:与其学一个让任务平均损失最小的 $\theta$,不如学一个 $\theta$,使得它在任意任务上走几步梯度之后表现良好。这是个双层优化。内层做适应:

$$ \theta'_i = \theta - \alpha \nabla_\theta \mathcal{L}_{T_i}(f_\theta, \mathcal{S}_i) $$

外层透过适应后的参数去优化初始化:

$$ \theta \leftarrow \theta - \beta \nabla_\theta \sum_{T_i \sim p(\mathcal{T})} \mathcal{L}_{T_i}(f_{\theta'_i}, \mathcal{Q}_i) $$

几何上看,MAML 把 $\theta$ 推到参数空间里这样一个区域:从这里出发,无论碰到哪个任务,几步梯度就能到达对应任务的最优。

MAML 损失曲面图:三个任务最优点环绕在中央的元初始化星标周围;右侧给出内外层循环的公式

MeLU —— 给推荐场景定制的 MAML

Lee 等人(KDD 2019) 把 MAML 落到推荐里,提出 MeLU (Meta-Learned User preference estimator)。一个关键的工程取舍:内层只适应决策层embedding 层在内层冻住。这背后的归纳偏置很合理 —— 物品和品类的 embedding 应该稳定共享,每个用户之间真正不同的是"如何把它们组合起来"。

每个"任务"对应一个用户。支持集 $\mathcal{S}_i$ 是用户最早的 1–5 次交互;查询集 $\mathcal{Q}_i$ 留给外层算损失。在几十万用户上元训练完之后,碰到一个全新用户的处理流程是:

  1. 拿他最早的 $K$ 条评分作为支持集。
  2. 在决策层上跑 1–5 步梯度。
  3. 用适应后的模型给所有候选物品打分。
 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
class MeLU(nn.Module):
    """MeLU 风格的推荐器:embedding 共享,决策头自适应。

    内层适应只动 decision_head 的参数。
    Embedding 层只通过外层循环更新。
    """

    def __init__(self, num_items, num_genres, embedding_dim=32):
        super().__init__()
        self.item_embedding = nn.Embedding(num_items, embedding_dim)
        self.genre_embedding = nn.Embedding(num_genres, embedding_dim)
        self.decision_head = nn.Sequential(
            nn.Linear(embedding_dim * 2, 64),
            nn.ReLU(),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Linear(32, 1),
        )

    def forward(self, item_ids, genre_ids):
        x = torch.cat(
            [self.item_embedding(item_ids), self.genre_embedding(genre_ids)],
            dim=-1,
        )
        return self.decision_head(x).squeeze(-1)


def melu_inner_adapt(model, support_items, support_genres, support_ratings,
                     inner_lr=0.01, n_steps=3):
    """只在决策头上走几步梯度。

    返回一个 requires_grad=True 的参数列表,
    便于外层循环对它做反向传播(二阶梯度)。
    """
    fast_params = [p.clone().detach().requires_grad_(True)
                   for p in model.decision_head.parameters()]

    for _ in range(n_steps):
        x = torch.cat([model.item_embedding(support_items),
                       model.genre_embedding(support_genres)], dim=-1)
        for i in range(0, len(fast_params), 2):
            w, b = fast_params[i], fast_params[i + 1]
            x = torch.nn.functional.linear(x, w, b)
            if i < len(fast_params) - 2:
                x = torch.relu(x)
        pred = x.squeeze(-1)

        loss = torch.mean((pred - support_ratings) ** 2)
        grads = torch.autograd.grad(loss, fast_params, create_graph=True)
        fast_params = [p - inner_lr * g for p, g in zip(fast_params, grads)]

    return fast_params

元学习什么时候值得

MAML/MeLU 因为内层展开和二阶梯度,训练算力是普通模型的 3–10 倍。下面三种情况下值得上:

  • 用户量大但每人交互很少(典型的长尾用户分布)。
  • 任务边界清晰可分 —— 一个用户、一次会话或一个人群粒度。
  • Bootstrap 启发式不够用,但你又没有相关源域可以迁移。

如果二阶梯度成本扛不住,FOMAML 把它丢掉、只保留一阶项,能拿回大部分收益。


Bandit —— 探索与利用

模型对一个用户有点把握之后,下一个问题是该出哪个。永远出最高分,你永远不知道用户会不会对模型分数稍低的某件物品一见钟情;永远随机出,你这场会话就废了。教科书的工具叫多臂老虎机

UCB1 —— 置信上界

Auer、Cesa-Bianchi、Fischer(2002) 证明了 UCB1 规则

$$ a_t = \arg\max_a \left[ \hat\mu_a + \sqrt{\frac{2 \ln t}{n_a}} \right] $$

可以达到 $O(\log t)$ 的累积 regret —— 也就是说,UCB 与"永远出最优臂"的 oracle 之间的差距,只随轮数对数增长。公式的语义很清楚:选置信上界最高的物品。被拉次数 $n_a$ 少的物品有一个大的探索奖励,所以会被尝试;被拉次数多的物品置信区间已经很窄,只有真的高均值才会被选中。

左:四个臂的估计奖励柱状图,每根柱子上方加了一段表示不确定性奖励的须;少拉的臂须更长。右:累积 regret 对比,UCB 与 Thompson 是对数级,ε-greedy 线性增长但更慢,pure greedy 卡在次优臂上线性涨,random 涨得最快

Thompson Sampling —— 贝叶斯的方案

Thompson Sampling 给每个臂维护一个奖励的后验分布;每轮各臂采样一次,谁的样本最高就出谁。经验上常常和 UCB1 持平甚至略胜,而且 Beta-Bernoulli 奖励下的实现极其简单。

 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
import numpy as np


class UCB1Recommender:
    """K 个候选物品上的 UCB1。奖励范围 [0, 1]。"""

    def __init__(self, n_items: int):
        self.n_items = n_items
        self.counts = np.zeros(n_items, dtype=np.int64)
        self.means = np.zeros(n_items, dtype=np.float64)
        self.t = 0

    def select(self) -> int:
        self.t += 1
        # 先把每个臂都拉一遍
        unpulled = np.where(self.counts == 0)[0]
        if len(unpulled) > 0:
            return int(unpulled[0])
        ucb = self.means + np.sqrt(2 * np.log(self.t) / self.counts)
        return int(np.argmax(ucb))

    def update(self, item: int, reward: float) -> None:
        self.counts[item] += 1
        n = self.counts[item]
        self.means[item] += (reward - self.means[item]) / n


class ThompsonSampling:
    """二值奖励(点击 / 未点击)下的 Beta-Bernoulli Thompson 采样。"""

    def __init__(self, n_items: int, alpha=1.0, beta=1.0):
        self.alpha = np.full(n_items, alpha)
        self.beta = np.full(n_items, beta)

    def select(self) -> int:
        samples = np.random.beta(self.alpha, self.beta)
        return int(np.argmax(samples))

    def update(self, item: int, reward: float) -> None:
        self.alpha[item] += reward
        self.beta[item] += 1 - reward

线上更常用的是上下文 Bandit(LinUCB 以及神经网络版本),它们用用户/物品特征替代单纯的计数器。在元学习已经给出模型、但还需要高效收集数据的少样本档,上下文 Bandit 是规范工具。


内容兜底

对新物品,再厉害的元学习也救不了"信号完全为零"的情况。基于内容的检索是一直挂着的安全网。

新物品经过 BERT/CLIP/TF-IDF 等 encoder 编码为内容 embedding,再用余弦相似度匹配温物品库,最后从最相似的 K 个温物品中按相似度加权聚合评分作为冷启动预测

流程是机械的:

  1. 编码新物品:文本走 BERT / sentence-transformers,图像走 CLIP,结构化数据走人工特征 + MLP。
  2. 找 K 个最相似的温物品:在内容 embedding 上做余弦相似度。
  3. 预测评分:用相似度作为权重,对邻居的评分加权平均:
$$ \hat r_{u,i} = \frac{\sum_{j \in N_K(i)} \mathrm{sim}(i, j) \cdot r_{u,j}}{\sum_{j \in N_K(i)} \mathrm{sim}(i, j)} $$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import numpy as np


class ContentFallback:
    """从 K 个最近的温物品借评分,给冷物品出分。"""

    def __init__(self, item_features: np.ndarray, K: int = 20):
        # item_features: (n_items, d) 预计算好的 embedding(如 BERT、CLIP)
        norms = np.linalg.norm(item_features, axis=1, keepdims=True) + 1e-8
        self.normed = item_features / norms
        self.K = K

    def predict(self, cold_item_idx: int, warm_ids: np.ndarray,
                warm_ratings: np.ndarray) -> float:
        sims = self.normed[warm_ids] @ self.normed[cold_item_idx]
        top = np.argpartition(-sims, kth=min(self.K, len(sims) - 1))[: self.K]
        s, r = sims[top], warm_ratings[top]
        s = np.maximum(s, 0)  # 负相似度直接忽略
        return float((s * r).sum() / (s.sum() + 1e-8))

同样的套路也能用在用户冷启动:只要能拿到一点内容信号(注册问卷、首次引导页一次点击),就把信号编码成向量、找最相似的温用户、借他们的偏好。


从冷到温的线上路由

没有任何单一方法能在整个交互量轴上一统天下。线上系统的做法是按用户当前积累的交互数,把请求路由到不同的方法上。

NDCG@10 随用户交互量变化:纯 CF 起步接近零、爬升缓慢;内容兜底有一条不高也不低的水平线;MeLU 在 1-10 这个区间斜率最陡;PTUPCDR 起步最高但很快趋平;混合方案是上包络,全程压在最上面

每条曲线背后都对应一个故事:

  • 纯 CF 在 5 次交互以下基本无用,一直要到 20 次左右才像样。
  • 内容兜底有一条平稳的下限 —— 不会很好但也不会失败,永远在线。
  • **元学习(MeLU)**在 1–10 次交互这个区间增长最快,正是它被设计出来要解决的。
  • **跨域(PTUPCDR)**起手最高,因为白嫖了源域知识,但抬升早早趋平。
  • 混合方案是上包络:当前用户落在哪个区间,就让对应方法主导。

一份能直接抄的路由表:

交互次数主方法兜底
0跨域 / 热门内容兜底
1-3跨域 + Bandit内容兜底
3-20元学习(MeLU)跨域
20+完整 CF / DIN / 序列模型新会话回退到元学习

并行挂一条"热门基线"作为电路保护:上面任意一种方法置信度过低时,直接退回到用户所在人群的热门物品。


常见问答

Q:我的平台没有可迁移的源域,从哪里下手?

先用内容兜底解决物品侧、用热门先验解决用户侧;积累到一定交互量再上 MeLU。跨域大概能再带来 10–20% 的相对提升,但它不是必备项。

Q:MeLU 大约在多少次交互之后能压过内容 baseline?

公开 benchmark(MovieLens、Bookcrossing)上,MeLU 一般在 2–3 次交互开始领先,5 次以后稳定碾压。具体取决于支持集本身的信息量 —— 多样化的品类比同质化的更有效。

Q:FOMAML 真的够用,还是非得上二阶 MAML?

推荐场景下 FOMAML 离 MAML 的差距大概 1–2%,但训练快 3 倍。只有当你度量到这点差距确实影响业务指标时,才有理由上二阶。

Q:怎么察觉跨域出现了负迁移?

并行跑一个只用目标域数据的 baseline。如果跨域模型在冷用户上不能赢 baseline,说明两个域离太远。最常见的坑是按 ID 对齐用户,但同一个 ID 在两个域里对应的行为模式南辕北辙(比如工作号 vs. 个人号)。

Q:Bandit 对 Top-K 推荐有用吗?还是只适合单选场景?

主要在 feed 的前一两个槽位上有价值 —— 那里的探索价值最高。再往后就该切回排序。组合 Bandit 是存在的,但工程成本很高。

Q:怎么离线评估冷启动系统?

用户整体留出(不要按交互随机留出)。对每个留出用户,只把他最早 $K$ 次交互喂给模型作为支持集,预测剩下的交互。指标按 $K \in \{1, 3, 5, 10\}$ 分档报,单一数字会把冷到温的过渡完全淹掉。


总结

冷启动不是一个有单一解法的问题,它是一个区间 —— 在交互量轴的不同位置,需要不同的机器。

  • 三种分类(用户 / 物品 / 系统)决定了你实际可拉的杠杆是探索、内容还是迁移。
  • EMCDRPTUPCDR 把数据丰富的源域转成对稀疏目标域的先验。当前实务上的甜蜜点是 PTUPCDR 那种个性化桥。
  • MAML 与它在推荐里的特化版 MeLU 学的是"几步就能适应"的初始化,正好对应 3–10 次交互这一段。
  • UCB1Thompson Sampling 给少样本档配上对数级 regret 的探索规则。
  • 内容兜底是不光鲜但永远在线的安全网。
  • 混合方案按交互量路由,并挂上一条热门电路保护。

按这个顺序逐层搭:先内容 + 热门,等用户量够大再上元学习,等出现相关源域再加跨域。冷用户和温用户的指标必须分开统计 —— 聚合数据会盖掉那个真正在亏钱的区间。


参考文献


系列导航

本文是推荐系统 16 篇系列的第 14 篇

上一篇下一篇
第十三篇:公平性、去偏与可解释性所有文章第十五篇:实时推荐与在线学习

Liked this piece?

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

GitHub