DSSM双塔模型实战:从零搭建电影推荐系统

1. 为什么选择双塔模型?

在推荐系统领域,双塔架构因其高效的向量检索能力已成为工业界标配。想象一下这样的场景:当用户打开视频平台时,系统需要在毫秒级从百万级影片库中筛选出几十部可能感兴趣的内容——这正是双塔模型的用武之地。

传统协同过滤方法面临两大瓶颈:一是难以处理冷启动问题,二是无法融合多源特征。而DSSM(Deep Structured Semantic Model)通过深度学习网络分别构建用户和物品的向量表示,既保留了语义匹配能力,又具备以下优势:

  • 离线计算友好:物品塔可预先计算全量embedding
  • 在线响应迅速:用户请求时只需实时计算单塔向量
  • 特征融合灵活:支持用户画像、行为序列等多模态特征
# 典型双塔结构伪代码
user_tower = DNN(user_features)
item_tower = DNN(item_features)
similarity = dot_product(user_tower.output, item_tower.output)

2. 数据准备与特征工程

2.1 MovieLens数据集解析

我们使用MovieLens 1M数据集,包含:

  • 6040名用户
  • 3900部电影
  • 100万条评分记录
  • 用户 demographic 信息
  • 电影类型标签

关键处理步骤:

  1. 将评分≥4的作为正样本
  2. 为每个用户随机采样未观看电影作为负样本
  3. 构建用户历史行为序列特征
# 负采样示例
def negative_sampling(user_items, item_pool, n_neg=5):
    neg_samples = []
    for user in user_items:
        seen = set(user_items[user])
        candidates = list(item_pool - seen)
        neg_samples.extend([(user, item) for item in np.random.choice(candidates, n_neg)])
    return neg_samples

2.2 特征组合策略

特征类型用户侧特征物品侧特征
类别特征性别、年龄、职业电影类型、上映年份
数值特征平均评分平均评分、评分次数
序列特征最近观看的10部电影ID-
交叉特征年龄×最喜爱类型类型×年代

提示:对于电影类型这种多值特征,建议采用embedding加权平均方式处理

3. 模型架构设计与实现

3.1 双塔结构详解

用户塔:

  • 输入层:融合用户画像、历史行为等特征
  • 隐藏层:2-3层全连接网络(ReLU激活)
  • 输出层:L2归一化的128维向量

物品塔:

  • 输入层:电影元数据、统计特征
  • 隐藏层:与用户塔对称结构
  • 输出层:同维度归一化向量
import torch.nn as nn

class Tower(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.LayerNorm(128)
        )
    
    def forward(self, x):
        return self.net(x)

3.2 损失函数选择

采用Batch内负采样+交叉熵损失:

  1. 每个batch包含B个正样本对
  2. 将batch内其他用户的物品作为自然负样本
  3. 计算相似度矩阵:

$$ \text{sim}_{i,j} = \cos(u_i, v_j) $$

  1. 使用温度系数缩放softmax:
class DSSMLoss(nn.Module):
    def __init__(self, temp=0.1):
        super().__init__()
        self.temp = temp
        self.cross_entropy = nn.CrossEntropyLoss()
    
    def forward(self, user_emb, item_emb):
        sim_matrix = user_emb @ item_emb.t() / self.temp
        labels = torch.arange(sim_matrix.size(0)).to(user_emb.device)
        return self.cross_entropy(sim_matrix, labels)

4. 工业级优化技巧

4.1 负采样策略对比

策略优点缺点适用场景
随机负采样实现简单样本过于简单初期快速验证
Batch内负采样符合数据分布受batch size限制大多数推荐场景
热度加权采样缓解流行度偏差需维护物品热度统计存在长尾分布场景
Hard负样本挖掘提升模型区分度实现复杂度高精排阶段

4.2 服务化部署方案

离线阶段:

# 定期全量更新物品embedding
python generate_embeddings.py \
  --model_checkpoint ./model.pt \
  --item_data ./movies.csv \
  --output ./item_emb.npy

在线服务:

  1. 用户请求到达时实时计算user embedding
  2. 使用FAISS进行近似最近邻搜索
  3. 返回Top-K相似物品
import faiss

# 构建FAISS索引
item_emb = np.load('./item_emb.npy')
index = faiss.IndexFlatIP(item_emb.shape[1])
index.add(item_emb)

# 在线查询
D, I = index.search(user_emb, k=50)  # 返回距离和索引

5. 效果评估与调优

5.1 关键评估指标

指标计算公式说明
HitRate@K$\frac{#hit}{#user}$用户正样本出现在TopK的比例
NDCG@K考虑排序位置的加权得分反映推荐列表质量
响应延迟P99 < 50ms线上服务关键指标

5.2 典型调优方向

特征工程:

  • 加入用户长短期兴趣表征
  • 尝试不同序列建模方式(GRU/Transformer)

模型结构:

# 添加SENet特征权重层
class SENet(nn.Module):
    def __init__(self, channel, ratio=4):
        super().__init__()
        self.avg_pool = nn.AdaptiveAvgPool1d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel//ratio),
            nn.ReLU(),
            nn.Linear(channel//ratio, channel),
            nn.Sigmoid()
        )
    
    def forward(self, x):
        b, c, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1)
        return x * y

训练技巧:

  • 渐进式调整温度系数
  • 混合负采样策略
  • 嵌入层正则化

在实际项目中,我们通过以下优化使HitRate@10提升23%:

  1. 引入用户7天行为序列特征
  2. 采用AdamW优化器
  3. 添加Label Smoothing
  4. 实施动态负采样

6. 完整代码框架

项目目录结构:

movie_recsys/
├── data/
│   ├── prepare.py       # 数据预处理
│   └── sampler.py       # 负采样实现
├── model/
│   ├── dssm.py          # 双塔模型定义
│   └── losses.py        # 自定义损失函数
├── serving/
│   ├── faiss_server.py  # 向量检索服务
│   └── tf_serving.py    # 模型导出
└── train.py             # 主训练脚本

核心训练流程:

# 初始化组件
model = TwoTowerModel(feature_dim).cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
loss_fn = DSSMLoss(temp=0.2)

# 训练循环
for epoch in range(100):
    for batch in train_loader:
        user_feat, item_feat = batch
        user_emb = model.user_tower(user_feat)
        item_emb = model.item_tower(item_feat)
        
        loss = loss_fn(user_emb, item_emb)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
    
    # 验证集评估
    hit_rate = evaluate(val_loader, model)
    print(f"Epoch {epoch}: HR@10={hit_rate:.4f}")

7. 前沿扩展方向

  1. 多模态融合:结合封面图像、剧情文本
  2. 因果推断:消除曝光偏差
  3. 课程学习:逐步增加负样本难度
  4. 跨域推荐:迁移学习应用
# 多模态融合示例
class MultiModalTower(nn.Module):
    def __init__(self):
        super().__init__()
        self.text_net = TextCNN()
        self.image_net = ResNet()
        self.fusion = nn.Linear(512, 128)
    
    def forward(self, text, image):
        text_emb = self.text_net(text)
        image_emb = self.image_net(image)
        return self.fusion(torch.cat([text_emb, image_emb], dim=1))

在快手短视频推荐中,团队通过引入多模态特征使人均观看时长提升15%。这提示我们,双塔模型作为基础架构,与业务场景的深度结合能持续释放价值。

更多推荐