1. 项目背景与核心问题
在大规模推荐系统中,双塔DNN结构(query2item)已成为召回阶段的主流解决方案。但在实际应用中,我们遇到了两个棘手的工程问题:
-
数据分布倾斜问题:用户行为数据天然呈现"长尾分布",头部热门item占据了绝大多数曝光和点击。以某电商平台为例,Top 1%的商品贡献了超过80%的交互数据,导致模型对尾部商品的特征学习严重不足。
-
反馈信号单一问题:显式负反馈(如"不感兴趣"标记)在实际业务中极为稀缺。我们只能依赖曝光未点击作为隐式负样本,这种"负样本"实际上混杂了大量未充分曝光的优质商品。
实践发现,传统双塔模型在测试集上的Recall@100指标虽然达到0.38,但长尾商品的覆盖率不足15%。这意味着系统陷入了"马太效应"——越热门的商品获得越多曝光,而新上架或小众商品难以获得展示机会。
2. 自监督学习的解决方案设计
2.1 整体架构思路
我们采用"主任务+辅任务"的联合训练框架:
- 主任务:保持标准的双塔召回模型,使用用户真实行为数据(点击=正样本,曝光未点击=负样本)
- 辅任务:引入对比学习(Contrastive Learning)作为自监督信号,通过数据增强构建正负样本对

2.2 关键创新点:差异化采样策略
- 主任务采样:保留原始数据分布(即自然用户行为分布)
- 辅任务采样:对item侧采用均匀采样,确保每个商品无论热度都有平等机会参与对比学习
python复制# 伪代码示例:双分布采样器
class DualSampler:
def __init__(self, items):
self.pop_dist = get_natural_distribution(items) # 原始分布
self.uniform_dist = UniformDistribution() # 均匀分布
def sample_main(self, n):
return sample_from(self.pop_dist, n) # 主任务采样
def sample_ssl(self, n):
return sample_from(self.uniform_dist, n) # 辅任务采样
3. 技术实现细节
3.1 数据增强策略
为避免简单的特征mask导致任务过于简单,我们设计了一种基于互信息的动态mask机制:
- 计算特征间互信息矩阵 $M \in \mathbb{R}^{d \times d}$
- 对每个样本,选择互信息最高的特征对 $(f_i, f_j)$
- 以概率 $p=0.3$ mask掉其中一个特征
math复制M_{ij} = I(x_i; x_j) = \sum_{x_i,x_j} p(x_i,x_j)\log\frac{p(x_i,x_j)}{p(x_i)p(x_j)}
3.2 损失函数设计
3.2.1 主任务损失(改进的NCE Loss)
$$
\mathcal{L}{main} = -\mathbb{E} \left[ \log \frac{e^{s(q_i,x_i)/\tau_m}}{\sum^N e^{s(q_i,x_j)/\tau_m}} \right]
$$
其中温度系数 $\tau_m$ 控制对困难负样本的关注程度,经验值设为0.2。
3.2.2 辅任务损失(InfoNCE Loss)
$$
\mathcal{L}{ssl} = -\mathbb{E} \left[ \log \frac{e^{s(x_i,x_i^+)/\tau_s}}{\sum^M e^{s(x_i,x_j^-)/\tau_s}} \right]
$$
辅任务温度系数 $\tau_s$ 通常设置更小(如0.1),以增强对相似样本的区分能力。
3.2.3 联合训练
最终损失为加权和:
$$
\mathcal{L}{total} = \alpha \mathcal{L} + (1-\alpha) \mathcal{L}_{ssl}
$$
平衡系数 $\alpha$ 通过网格搜索确定为0.7。
4. 工程实践与调优经验
4.1 温度系数的选择技巧
通过ab实验发现:
- 当 $\tau_m > 0.3$ 时,主任务对困难负样本的区分度下降,导致Recall下降2-3%
- 当 $\tau_s < 0.05$ 时,辅任务容易陷入局部最优,长尾商品覆盖率降低
建议初始值:$\tau_m=0.2$, $\tau_s=0.1$,然后以0.05为步长微调
4.2 负样本数量权衡
在分布式训练中,我们发现:
- 主任务负样本数 $N$ 在512-1024时性价比最高
- 辅任务负样本数 $M$ 可以更大(2048+),因为均匀采样质量更高
python复制# 典型配置
train_config = {
'main_neg': 768, # 主任务负样本数
'ssl_neg': 2048, # 辅任务负样本数
'batch_size': 4096 # 总batch size
}
4.3 特征mask的工程实现
实际部署时需要注意:
- 互信息矩阵需要定期更新(如每4小时)
- Mask操作要在embedding层之后进行,避免破坏原始特征
- 对数值型特征采用高斯噪声代替mask
python复制def apply_mask(features, mi_matrix):
masked_features = features.clone()
for i in range(len(features)):
if random.random() < 0.3:
idx = mi_matrix[i].argmax() # 找互信息最大的特征
masked_features[i,idx] = 0 # 执行mask
return masked_features
5. 效果验证与业务收益
在某电商平台上线后取得显著提升:
| 指标 | 基线模型 | 我们的方案 | 提升幅度 |
|---|---|---|---|
| Recall@100 | 0.38 | 0.43 | +13.2% |
| 长尾商品覆盖率 | 14.7% | 28.5% | +93.9% |
| 首屏点击率 | 6.2% | 6.8% | +9.7% |
特别值得注意的是:
- 新上架商品的7日转化率提升22%
- 用户人均访问品类数从3.2增加到4.1
6. 常见问题排查
6.1 损失震荡问题
现象:联合训练初期损失剧烈波动
解决方案:
- 先单独训练主任务5个epoch
- 固定主任务encoder,训练辅任务2个epoch
- 解冻全部参数联合训练
6.2 长尾效果不显著
检查清单:
- 确认辅任务item采样器确实是均匀分布
- 检查特征mask是否正常工作(可统计mask比例)
- 适当增大 $\mathcal{L}_{ssl}$ 的权重(如 $\alpha=0.6$)
6.3 线上服务延迟
优化方向:
- 对辅任务分支使用浅层网络
- 将互信息矩阵计算移到离线阶段
- 使用量化后的embedding
7. 扩展思考
这种双分布采样策略实际上构建了一个动态的正则化机制:主任务学习自然用户偏好,而辅任务强制模型关注尾部商品。在实际业务中,我们还尝试了以下变体:
- 渐进式采样:随着训练进行,逐步增大均匀采样的比例
- 类别感知采样:在均匀采样的基础上,保证每个品类至少出现K个样本
- 混合负采样:主任务使用曝光未点击+随机负采样组合
对于特别稀疏的品类(如奢侈品),额外添加品类内对比学习损失可以进一步提升效果约5-8%。
