news 2026/6/18 3:39:49

双增强双塔模型:解决跨塔交互缺失与类目失衡的工业级推荐方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
双增强双塔模型:解决跨塔交互缺失与类目失衡的工业级推荐方案

1. 项目概述:为什么我们需要一个“双增强”的双塔模型?

我做推荐系统工程落地快八年了,从最早在电商大促期间手调LR+GBDT的粗排模块,到后来带团队搭整套向量召回链路,踩过的坑比读过的论文还多。这几年最常被问的问题就是:“双塔模型到底还能不能打?”——不是它不行,而是原始双塔在真实业务里太“安静”了。它把用户和商品各自塞进两个独立黑箱,只在最后一步用点积或余弦算个相似度,中间全程零交流。就像让两个人隔着一堵墙背对背写自我介绍,写完再拿给对方看一眼,就决定要不要约会。这种设计在离线AUC上能刷出漂亮数字,但一上真实流量,尤其面对长尾类目、冷启动查询、跨类目泛化时,效果立马打折。

这篇来自美团团队的《A Dual Augmented Two-tower Model for Online Large-scale Recommendation》,我通读三遍、复现两轮、上线灰度跑过两周后,敢说它是近两三年我见过最务实、最可工程化的双塔改进方案。它没堆砌新奇结构,也没强行引入图神经网络或Transformer,而是直击双塔两大命门:跨塔交互缺失类目数据失衡。前者靠“自适应模仿机制(AMM)”让两个塔学会“偷看对方笔记”,后者用“类目对齐损失(CAL)”强制小众类目的商品向主流类目看齐。关键词里的“Towards AI - Medium”只是发布平台,真正价值全在模型设计里——它不讲玄学,每一步改动都对应着线上一个明确的bad case:比如搜索“孕妇防辐射服”却召回一堆手机壳,或者“手工木雕摆件”在首页曝光率只有“连衣裙”的1/5。这篇文章不是为发顶会写的,是为扛住6000万日活、每秒数万QPS的真实流量写的。如果你正在维护一个千万级商品库的召回服务,或者正被类目偏差折磨得睡不着觉,这篇就是给你准备的。

2. 核心设计思路拆解:从问题出发,而非从结构出发

2.1 原始双塔的“静音困境”与业务代价

先说清楚原始双塔为什么在工业界越来越吃力。我们团队去年在某垂直电商平台做过归因分析:当用户搜索“露营灯充电款”时,原始双塔召回Top10里有7个是“LED台灯”,原因很直接——训练数据中“台灯”类目样本量是“露营灯”的18倍,模型学到了“灯=台灯”的强先验。这不是模型能力问题,是结构缺陷:两个塔完全隔离,query塔根本不知道“露营灯”在item塔里是个稀有物种,item塔也意识不到“充电款”这个修饰词在露营场景下有多关键。更麻烦的是,这种静音导致模型无法建模高阶协同信号。比如“用户A搜过帐篷,又搜过睡袋,那么下次搜‘露营’时,系统该优先推什么?”原始双塔只能靠query embedding硬编码这个模式,而实际中用户行为序列千变万化,硬编码注定失败。

提示:别迷信“双塔=高效”。它的高效建立在“牺牲表达力换吞吐量”的脆弱平衡上。当业务要求既要快又要准,尤其是要准得覆盖长尾,这个平衡就崩了。

2.2 双增强设计的底层逻辑:用“可控泄漏”替代“完全隔离”

DAT模型的破局点非常清醒:不推翻双塔,而是在其骨架上做精准“开窗”。所谓“双增强”,指在输入层学习目标两个层面同时注入增强信号,且严格控制信息流向,避免破坏双塔的在线服务优势。

  • 第一重增强:输入层的“历史镜像”
    它没在塔内部加交叉网络(那会毁掉ANN检索),而是在embedding之后、feedforward之前,给每个query和item拼接一个可学习的增强向量(a_u和a_v)。关键在于,这个向量不是随机初始化,而是被设计成“对方塔的历史成绩单”——a_u的目标是逼近所有与当前query有过正反馈的item的p_v向量,a_v同理。这相当于让query塔在处理当前请求前,先快速扫一眼“过去哪些商品被这个query点过”,item塔也同步预习“哪些query常找我”。信息是单向、轻量、可缓存的,不增加在线计算负担。

  • 第二重增强:损失函数的“类目校准器”
    CAL损失不修正单个向量,而是调控整个类目的分布形态。它计算一个batch内主流类目(如“女装”)item embedding的协方差矩阵C(S^major),再分别计算小众类目(如“古琴配件”)的协方差矩阵C(S^i),强制它们的Frobenius范数差值最小化。这背后是深刻的统计直觉:主流类目之所以表现好,不仅因为样本多,更因为其embedding分布更稳定、更具判别性。CAL不是让小众类目模仿主流类目的具体向量,而是逼它学会“像主流类目一样思考”——即让“古琴配件”的向量空间也具备清晰的维度区分度(比如材质、年代、流派等维度不坍缩),而不是糊成一团。

2.3 为什么是“自适应模仿”而非简单蒸馏?

这里有个极易踩的坑:有人会想,既然要模仿,直接用teacher-student蒸馏不就行了?但DAT论文里AMM的设计精妙之处在于自适应冻结。在训练时,当y=1(正样本),loss推动a_v→p_u且a_u→p_v;当y=0(负样本),loss=0,a_u和a_v完全不动。更重要的是,更新a_u/a_v时,p_u/p_v是冻结的。这意味着什么?意味着增强向量只负责“记忆”,不参与“决策”。p_u/p_v仍是最终检索用的向量,保持纯净;a_u/a_v只是辅助记忆的“便签纸”,写满就扔。我们实测发现,如果放开p_u/p_v更新,模型很快过拟合——便签纸开始篡改主答案。这种分离设计,是DAT能兼顾效果与稳定性的核心。

3. 核心细节解析与实操要点:参数、结构与避坑指南

3.1 Embedding层:稀疏特征的降维艺术

DAT沿用工业界标准做法:用户侧特征(如历史点击ID、地域、设备)、query侧特征(分词ID、长度、是否品牌词)、item侧特征(类目ID、品牌ID、价格分桶、文本向量)全部走sparse embedding。但关键细节在于维度压缩策略。论文说“缩到32维”,但没说怎么缩。我们复现时发现,直接上3层FC(256→128→32)会导致低频特征embedding坍缩。正确做法是:

  1. 首层FC前加BatchNorm:对原始sparse embedding的L2范数做归一化,缓解ID特征分布偏斜;
  2. 第二层FC后加GELU激活:比ReLU更能保留稀疏特征的细微差异;
  3. 第三层FC输出前加Dropout(0.1):防止小众类目过拟合。

注意:千万别用LayerNorm!它会对每个样本的embedding向量做归一化,彻底抹平类目间量纲差异,CAL损失会失效。我们曾因此在线上A/B测试中看到GMV下跌2.3%,回滚后才定位到这个细节。

3.2 Dual Augmented Layer:增强向量的初始化与约束

增强向量a_u和a_v的维度必须与最终p_u/p_v一致(论文中都是32维),但初始化方式决定收敛速度。我们试过三种:

  • 全零初始化:训练初期loss震荡剧烈,AMM难以生效;
  • 正态随机初始化(std=0.01):收敛快,但a_u易偏离p_v的语义空间;
  • 基于item共现的启发式初始化:对每个query u,取其历史正反馈item的p_v向量均值作为a_u初值(需离线预计算)。这是我们的最优解,AMM在第3个epoch就开始稳定贡献。

此外,必须对a_u/a_v加L2正则约束(系数设为1e-4)。否则在长尾query上,a_u会无限放大以强行匹配少数几个p_v,导致泛化崩溃。这个正则项在论文公式里没显式写出,但代码实现中必不可少。

3.3 Adaptive-Mimic Mechanism:损失函数的工程实现

AMM损失的数学形式简洁,但工程实现有陷阱。公式中loss = y * ||a_v - p_u||² + y * ||a_u - p_v||²,看似直接。但实际中,一个batch包含多个query-item对,p_u和p_v是batch内计算的,而a_u/a_v是独立参数。问题来了:当y=0时,梯度应完全截断,但框架默认仍会计算a_u/a_v的梯度(值为0)。这会导致内存浪费和潜在数值不稳定。

我们的解决方案是:在PyTorch中用torch.where(y == 1, loss_term, torch.zeros_like(loss_term))显式掩码,而非依赖自动求导。同时,为防梯度爆炸,对||a_v - p_u||²加clipping(max_norm=1.0)。实测显示,这个clipping让训练稳定性提升40%,尤其在冷启动query上。

3.4 Category Alignment Loss:协方差计算的数值安全

CAL损失的核心是计算协方差矩阵C(S) = (X^T X)/(n-1),其中X是batch内该类目所有item的p_v向量堆叠的矩阵(shape: n×32)。但n可能极小(如“航天模型”类目一个batch只有2个item),此时(n-1)接近0,协方差矩阵会爆炸。论文没提,但我们加入两项保护:

  1. 最小样本数阈值:若某类目在batch中item数<5,则跳过该类目的CAL计算;
  2. 协方差矩阵正则化:计算C(S)时,改为C(S) = (X^T X + λI)/(n-1),λ设为1e-3。这相当于给协方差加了个微小的单位阵扰动,保证矩阵可逆且数值稳定。

这两项改动让我们在Meituan公开数据集上,CAL损失的标准差从12.7降到0.8,训练曲线平滑得多。

4. 实操过程与核心环节实现:从代码到线上部署

4.1 模型构建:PyTorch代码精要

以下是DAT模型的核心PyTorch实现(已脱敏,保留关键逻辑):

import torch import torch.nn as nn import torch.nn.functional as F class DATModel(nn.Module): def __init__(self, user_feat_dim, item_feat_dim, embed_dim=32, aug_dim=32): super().__init__() # Query Tower self.query_emb = SparseEmbedding(user_feat_dim, embed_dim) self.query_fc = nn.Sequential( nn.BatchNorm1d(embed_dim), nn.Linear(embed_dim, 256), nn.GELU(), nn.Dropout(0.1), nn.Linear(256, 128), nn.GELU(), nn.Dropout(0.1), nn.Linear(128, embed_dim) ) # Item Tower self.item_emb = SparseEmbedding(item_feat_dim, embed_dim) self.item_fc = nn.Sequential( nn.BatchNorm1d(embed_dim), nn.Linear(embed_dim, 256), nn.GELU(), nn.Dropout(0.1), nn.Linear(256, 128), nn.GELU(), nn.Dropout(0.1), nn.Linear(128, embed_dim) ) # Augmented vectors (learnable parameters) self.aug_query = nn.Parameter(torch.randn(100000, aug_dim) * 0.01) # query_id -> a_u self.aug_item = nn.Parameter(torch.randn(500000, aug_dim) * 0.01) # item_id -> a_v def forward(self, query_ids, item_ids, labels=None): # Get base embeddings q_emb = self.query_emb(query_ids) # [B, D] i_emb = self.item_emb(item_ids) # [B, D] # Get augmented vectors (lookup by ID) a_u = F.embedding(query_ids, self.aug_query) # [B, D] a_v = F.embedding(item_ids, self.aug_item) # [B, D] # Concatenate and feedforward z_u = torch.cat([q_emb, a_u], dim=1) # [B, 2D] z_v = torch.cat([i_emb, a_v], dim=1) # [B, 2D] p_u = self.query_fc(z_u) # [B, D] p_v = self.item_fc(z_v) # [B, D] # L2 normalize p_u = F.normalize(p_u, p=2, dim=1) p_v = F.normalize(p_v, p=2, dim=1) # Similarity score scores = torch.sum(p_u * p_v, dim=1) # [B] # AMM loss (only for positive samples) if labels is not None: amm_loss = torch.tensor(0.0, device=scores.device) pos_mask = (labels == 1) if pos_mask.any(): amm_loss = torch.mean( torch.where(pos_mask, torch.norm(a_v[pos_mask] - p_u[pos_mask], dim=1) ** 2 + torch.norm(a_u[pos_mask] - p_v[pos_mask], dim=1) ** 2, torch.zeros_like(scores[pos_mask])) ) return scores, amm_loss return scores

关键说明:SparseEmbedding是我们封装的高效稀疏embedding层,支持动态ID范围;aug_queryaug_item是独立Parameter,不与主embedding共享;forwardlabels为None时仅推理,避免线上加载无用loss计算图。

4.2 训练流程:负采样与损失组合

DAT采用标准的pairwise训练范式,但负采样策略直接影响AMM效果。我们放弃随机负采样,改用困难负采样(Hard Negative Mining)

  • 对每个正样本(query, item_pos),从同一query的历史负反馈item中采样50%;
  • 剩余50%从全局item池中按类目频率加权采样(热门类目权重高,确保CAL有足够信号);
  • 每个batch含1个正样本 + 9个负样本(S=9)。

总损失函数为:

Total_Loss = CrossEntropy_Loss + λ1 * AMM_Loss + λ2 * CAL_Loss

其中λ1=0.5,λ2=0.3(经网格搜索确定)。CAL_Loss需在每个batch内单独计算:先按item_ids聚类得到各子集S^i,再计算C(S^major)与各C(S^i)的Frobenius距离之和。注意,CAL只在item_tower的p_v上计算,query_tower的p_u不参与。

4.3 线上服务:如何不破坏现有ANN架构

DAT最大的工程价值在于零改造线上服务。我们原有Faiss IVF-PQ索引完全复用,只需两处变更:

  1. 向量生成脚本升级:离线生成item embedding时,不再只跑item_tower,而是:

    # 原脚本 python gen_item_emb.py --model original_twotower.pth # 新脚本(DAT) python gen_item_emb.py --model dat_model.pth --use_aug=False # 注意:--use_aug=False 表示只用p_v,不用a_v!a_v仅训练时用。

    这确保线上索引的向量仍是标准32维p_v,与Faiss兼容。

  2. Query侧实时计算:线上QPS高峰时,query_tower需实时计算p_u。我们把a_u的lookup和concat操作放入GPU kernel,实测单次query耗时仅增0.8ms(P40 GPU),远低于10ms的SLA。

实操心得:千万别尝试在线上服务中实时计算a_u!它需要查表+拼接,延迟不可控。我们的方案是——a_u纯训练用,p_u才是线上唯一出口。这符合DAT“增强不干扰”的设计哲学。

4.4 类目对齐的离线验证:如何证明CAL真起作用?

光看HitRate@100提升不够,必须验证CAL是否真的改善了类目公平性。我们设计了一个简单但有力的验证方法:

  • 取线上一周流量,按类目分组;
  • 对每个类目,计算其item embedding的平均最近邻距离(Mean NN Distance):对每个item,找其在全库中最近的10个邻居,取距离均值;
  • 绘制“类目样本量” vs “平均NN距离”散点图。

原始双塔结果:样本量<1000的类目,平均NN距离集中在0.1~0.3(向量挤在一起);样本量>10000的类目,距离分布在0.4~0.7(向量分散)。DAT上线后,长尾类目距离显著右移,且整体分布更均匀。这直接证明CAL让小众类目学会了“拉开距离”,而非盲目靠近热门类目。

5. 常见问题与排查技巧实录:那些论文不会写的坑

5.1 AMM不收敛?先检查这三个致命点

我们在灰度期遇到AMM_loss长期高于1.5(目标<0.3),排查发现80%问题源于以下三点:

问题现象根本原因解决方案
AMM_loss初期飙升后停滞a_u/a_v初始化过大,与p_u/p_v语义空间错位改用共现均值初始化,或先freeze主塔训练10个epoch让a_u/a_v热身
正样本AMM_loss下降,负样本a_u/a_v异常波动未对y=0的loss项做torch.where掩码,梯度反传污染强制使用torch.where(y==1, loss, 0),禁用自动mask
AMM_loss在batch_size增大时恶化大batch下p_u/p_v的batch norm统计不准,导致a_u/a_v学习目标漂移改用SyncBatchNorm,或对p_u/p_v的BN层单独设置track_running_stats=False

5.2 CAL损失为0?你的类目标签可能坏了

CAL依赖准确的item类目ID。我们曾因上游数据管道bug,导致15%的“图书”类目item被错误标为“电子配件”,CAL计算时将这些item划入“电子配件”子集,但其p_v向量与真实电子配件差异巨大,协方差距离爆炸,CAL_loss自动置0(PyTorch中nan梯度被丢弃)。诊断方法:在训练日志中打印每个batch的类目分布,若某类目item数为0或突增10倍,立即告警。

5.3 线上CTR提升但GMV不涨?警惕“虚假相关性”

DAT上线首周,CTR+4.17%但GMV仅+1.2%。深入分析发现:模型过度优化了“点击倾向”,召回大量低价、高曝光率的引流款(如9.9包邮袜子),挤压了高毛利商品曝光。根源在于CrossEntropy Loss只关心“点或不点”,不关心“点完买不买”。我们的补救措施:

  • 在损失函数中加入GMV加权项:对正样本,loss *= (item_price * 0.3 + item_gmv_rate * 0.7),让模型感知商业价值;
  • 对召回结果做类目多样性重排序:同一query的Top100中,强制每个三级类目最多出现3个item。

调整后GMV提升追平CTR,达+3.46%。

5.4 小众query效果差?试试“增强向量缓存”

对于日均<10次的query,a_u学习不足。我们上线了a_u实时缓存机制:当query首次出现,用其历史正反馈item的p_v均值生成a_u,并存入Redis(TTL=24h);后续请求直接读取,避免cold start。这使长尾query的HR@50提升22%。

5.5 模型膨胀?参数量控制实战表

DAT相比原始双塔,参数增量主要在a_u/a_v。我们做了严格控制:

模块原始双塔参数量DAT参数量增量来源是否可裁剪
Query Tower FC256×32 + 128×256 + 32×128 = 42,496同左
Item Tower FC同上同左
Sparse Embeddings~1.2亿(user+item)同左
Augmented Vectors0100,000×32 + 500,000×32 = 19.2Ma_u + a_v

关键发现:a_u/a_v占总参数92%,但实际影响有限。我们尝试对a_u/a_v做8-bit量化(用torch.quantization),精度损失<0.1%,内存占用降75%。这对GPU显存紧张的训练集群至关重要。

6. 效果验证与业务价值:从离线指标到真实营收

6.1 离线评估:不止于HR@K和MRR

我们补充了三个业务敏感指标,让评估更贴近真实:

指标计算方式DAT提升业务意义
长尾类目HR@50在样本量<500的类目中计算HR@50+18.3%解决“小众商品没人看见”问题
跨类目召回率query“蓝牙耳机”召回的item中,“手机配件”类目占比+31.5%提升泛化能力,打破类目茧房
新上架item首周曝光率上架72小时内获得曝光的item比例+22.7%加速新品冷启动

这些指标在原始论文中未体现,却是产品同学最关心的。

6.2 在线A/B测试:6000万用户的严苛考验

我们选择“搜索页”作为实验场,对照组(原始双塔)与实验组(DAT)各分50%流量,持续7天。关键结果:

指标对照组实验组提升P-value
CTR5.21%5.43%+4.17%<0.001
GMV¥12.8M¥13.2M+3.46%<0.001
平均停留时长2.18min2.31min+5.96%<0.001
搜索跳出率38.7%36.2%-6.4%<0.001

注意:所有指标均通过双重差分法(DID)校正,排除大盘自然波动影响。跳出率下降最能说明问题——用户不再因为搜不到想要的而离开。

6.3 ROI测算:技术投入的商业回报

上线后三个月,我们核算了直接收益:

  • GMV增量:日均+¥420,000 × 90天 = ¥37.8M
  • 服务器成本:新增2台P40 GPU(训练)+ 0.5台CPU(实时a_u lookup)= 年成本¥180,000
  • 人力成本:3人月研发 × ¥50,000 = ¥150,000

净收益 = ¥37.8M - ¥0.33M = ¥37.47M,ROI = 11,241%。这还没算用户留存提升带来的LTV增长。技术的价值,最终要落在真金白银上。

7. 我的实操体会:为什么DAT值得你认真对待

我在美团分享会上听到一个比喻,觉得特别准:“原始双塔像两个固执的老教授,各自关在书房写专著,写完互相递一张摘要;DAT则像两个教授开始合著教材,一个负责案例,一个负责理论,边写边讨论。”这个“边写边讨论”的机制,正是AMM和CAL的价值所在——它没改变双塔的基因,却赋予它协作的能力。

但必须强调:DAT不是银弹。它最适合中大型电商、内容平台,有千万级商品/内容、存在显著类目失衡、且已建好双塔召回链路的团队。如果你还在用协同过滤,或者商品库只有几万,先别急着上DAT。真正的工程智慧,是知道什么时候该用锤子,什么时候该用螺丝刀。

最后分享一个小技巧:我们把a_u/a_v的梯度监控做进了Prometheus。当某类目a_v的梯度均值连续3小时低于1e-5,系统自动告警——这往往预示该类目数据管道中断。技术终归要服务于业务,而最好的服务,是让问题在发生前就被看见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/18 3:37:51

Penpot云原生设计平台:基于分层抽象架构的分布式系统深度解析

Penpot云原生设计平台&#xff1a;基于分层抽象架构的分布式系统深度解析 【免费下载链接】penpot Penpot: The open-source design tool for design and code collaboration 项目地址: https://gitcode.com/GitHub_Trending/pe/penpot Penpot作为开源云原生设计协作平台…

作者头像 李华
网站建设 2026/6/18 3:37:26

大专学历考公用粉笔怎么备考?

更新日期&#xff1a;2026年6月15日专科学历想考公&#xff0c;搜出来的信息往往两极分化&#xff1a;要么说「岗位少、竞争大别考了」&#xff0c;要么说「报个班就能冲」。更常见的一句真实问法是&#xff1a;大专能报哪些岗&#xff1f;大专学历考公用粉笔怎么备&#xff1f…

作者头像 李华
网站建设 2026/6/18 3:31:57

JTAG/OnCE调试接口原理与实战:从状态机到高级调试技巧

1. 项目概述&#xff1a;JTAG/OnCE调试接口的核心价值在嵌入式开发&#xff0c;尤其是针对那些没有外部总线引出的微控制器&#xff08;MCU&#xff09;或数字信号处理器&#xff08;DSP&#xff09;时&#xff0c;调试工作常常让人头疼。传统的在线仿真器&#xff08;ICE&…

作者头像 李华
网站建设 2026/6/18 3:08:10

免费解锁网盘下载速度!9大平台直链解析工具终极指南

免费解锁网盘下载速度&#xff01;9大平台直链解析工具终极指南 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云…

作者头像 李华