多兴趣推荐:用多个向量捕获用户多样化偏好

FreeGuideOnline 最新 2026-06-23

loss = -log( exp(v_hat · e_i) / ( exp(v_hat · e_i) + Σ_{j in 负样本} exp(v_hat · e_j) ) )


这样训练使**最相关的兴趣向量**被推向正物品,并保证其他兴趣不会干扰。

---

## 3. 在线服务:如何利用多个兴趣召回?

### 3.1 多向量索引与召回

用户侧存储 K 个兴趣向量后,推荐流程变为:

- **离线/近线阶段**:提取用户多兴趣向量,将 K 个向量分别写入向量检索库(如 Faiss)。
- **在线请求阶段**:对于用户的 K 个向量,分别执行 K 次最近邻检索,得到 K 批候选物品集合。
- **合并与去重**:合并这 K 批结果,去除重复物品,送入精排层。

### 3.2 计算开销控制

K 通常取 3~5 个,因此检索开销是单一的 K 倍。可通过以下方式优化:

- **流量分层**:活跃用户使用全量 K,低活用户降为 1~2。
- **混合召回**:多兴趣通道与热门通道、协同过滤通道合并,控制总量。
- **稀疏向量**:对每个兴趣向量存储少量高分物品,避免全库检索。

### 3.3 上下文匹配问题

在许多场景中,用户当前意图明确(例如搜索了“插座”)。可以借助**目标商品/上下文**来动态选择最相关的兴趣向量进行召回,而不是无差别的K路召回。实践方法有:

- **目标物品导向选择**:用目标物品的嵌入与 K 个兴趣向量计算内积,选最大者。
- **序列模型转向**:用 Transformer 学习一个基于历史+当前需求的动态向量,取代静态多兴趣。

---

## 4. 实战:构建你的多兴趣召回模型

### 4.1 数据准备

需要用户行为序列数据,格式如:`user_id, item_id, timestamp`,并按时间排序。至少需要百万级交互量,负采样时随机抽取未交互的物品。

### 4.2 模型实现(PyTorch 伪代码)

```python
class MultiInterestNetwork(nn.Module):
    def __init__(self, item_emb_dim, interest_num, route_iter=3):
        super().__init__()
        self.item_emb = nn.Embedding(num_items, item_emb_dim)
        self.C = item_emb_dim   # 胶囊维度
        self.K = interest_num  
        self.route_iter = route_iter

    def forward(self, hist_items):
        # hist_items: (batch, seq_len)
        seq_emb = self.item_emb(hist_items)  # (B, N, C)
        # 初始化路由权重
        B, N, C = seq_emb.shape
        logits = torch.zeros(B, N, self.K).to(seq_emb.device)
        for t in range(self.route_iter):
            route_weights = F.softmax(logits, dim=-1)  # (B, N, K)
            # 计算胶囊输入
            # route_weights 转置并乘物品嵌入
            # (B,K,N) @ (B,N,C) -> (B,K,C)
            weighted_input = torch.bmm(route_weights.transpose(1,2), seq_emb)
            # squash
            v = self.squash(weighted_input)  # (B, K, C)
            if t < self.route_iter - 1:
                # 更新 logits: 物品与胶囊的点积
                # v: (B,K,C) -> (B,1,K,C) 或重复广播
                # seq_emb: (B,N,C) -> (B,N,1,C)
                logits += torch.matmul(seq_emb, v.transpose(1,2))  # (B,N,K)
        # v 即为 K 个兴趣向量
        return v

    def squash(self, s):
        s_norm = torch.norm(s, dim=-1, keepdim=True)
        return (s_norm**2 / (1 + s_norm**2)) * (s / s_norm)