1. 这不是又一个“多任务模型”——ESMM为什么在推荐系统里被反复提起你打开任何一家中大型互联网公司的推荐算法岗JD几乎都能看到“熟悉多任务学习框架”这一条。但真正能讲清楚ESMMEntire Space Multi-Task Model到底解决了什么、为什么非得用它、以及它和MMoE、PLE、AITM这些名字听起来更炫酷的模型到底差在哪的人其实不多。我带过三届校招生做推荐方向的毕设八成同学第一反应是“ESMM是不是就是CTRCVR一起训”——然后就卡住了。这恰恰说明ESMM表面看结构简单背后却藏着对推荐系统底层逻辑的一次精准手术它不解决“怎么把两个任务训得更好”而是直击“CVR预估这个任务本身在传统范式下根本就训不准”的病根。核心关键词——ESMM、CVR预估偏差、样本选择偏差、全空间建模、阿里系推荐架构——这几个词串起来就是整件事的骨架。它诞生于2018年阿里妈妈团队不是为发论文而造模型而是为了解决淘宝首页“猜你喜欢”流里一个每天损失数百万次成交的真实问题用户点了商品但没下单模型却把这类行为当成“负样本”喂进去结果CVR预估越训越偏越偏越不敢推高潜力商品形成恶性循环。ESMM的破局点非常朴素不强行在点击后空间里训CVR而是把CVR定义成“从曝光到转化”的条件概率并用CTR和CTCVR两个辅助任务在整个曝光空间里把它“锚定”住。这不是加个loss那么简单这是对推荐系统数据生成机制的一次重新建模。适合谁读刚接触推荐系统的算法新人需要理解“为什么不能直接训CVR”正在调线上CVR模块的工程师想搞懂AB实验里那个诡异的“CTR提升但GMV没涨”的归因还有带团队的技术负责人需要判断在资源有限时该优先投入ESMM改造还是上更复杂的多任务结构。它不教你怎么写PyTorch它教你如何用数学语言去描述一个业务问题。2. 内容整体设计与思路拆解为什么是“全空间”而不是“点击后空间”2.1 传统CVR预估的死结从数据源头就错了我们先看一个最基础的场景用户刷信息流看到100个商品曝光点了其中5个点击最终只在2个商品上下了单转化。传统做法是把这5个点击样本拿出来把其中2个下单的标为正样本3个没下单的标为负样本然后训一个CVR模型。乍看合理但问题藏在数据分布里。这5个点击样本本身已经经过了CTR模型的筛选——它们是“被模型认为可能点击”的商品。而真实世界里大量优质但长尾的商品可能根本没获得曝光机会自然也不会出现在你的CVR训练集里。更致命的是用户点击后没转化原因千差万别可能只是随手点开看看可能页面加载失败可能比价后去了别家……这些和商品本身转化能力无关的因素全被塞进了CVR的负样本标签里。结果就是模型学到的不是“这个商品好不好卖”而是“这个商品在被点击的前提下有多大概率被下单”——一个严重失真的、带强选择偏差的代理目标。提示这里的关键不是“样本少”而是“样本有系统性偏差”。哪怕你有1亿个点击后样本只要它们都来自同一个被CTR模型过滤过的子空间CVR预估的泛化能力就注定受限。2.2 ESMM的破局逻辑用乘法关系重构问题定义ESMM没有试图在错误的数据空间里“修修补补”而是回到概率论基本原理重新定义CVR。它把一次用户-商品交互的完整链路拆解为三个事件曝光Impression, I、点击Click, C、转化Conversion, V。那么CVR的本质就是条件概率 P(V|C)。但P(V|C) P(V, C) / P(C)。而P(V, C) 就是“点击且转化”的联合概率也就是常说的CTCVRClick-Through Conversion RateP(C) 就是CTR。所以CVR CTCVR / CTR。这个等式本身是恒成立的不依赖任何模型假设。ESMM的精妙之处在于它把这个纯数学关系变成了模型结构的硬约束让模型同时输出CTR和CTCVR的预测值然后用它们的商来定义CVR的预测值。这样一来CVR的预测结果天然就被CTR和CTCVR两个任务“夹住”了。CTR负责建模从曝光到点击的过程CTCVR负责建模从曝光到点击且转化的过程两者共享底层特征表示Embedding层但各自有独立的上层网络。这种设计把原本无法直接监督的CVR目标转化成了两个可以在全曝光空间即所有曝光样本上进行监督学习的任务。2.3 为什么必须是“全空间”——共享Embedding的深层价值有人会问既然CVR CTCVR / CTR那我分别训两个模型最后在线上除一下不行吗答案是不行而且效果会很差。关键就在“共享Embedding”这一步。ESMM强制CTR和CTCVR两个任务使用完全相同的底层特征表示比如用户ID、商品ID、类目ID的Embedding向量。这意味着模型在优化CTR时学到的用户兴趣表征会直接传递给CTCVR任务反之CTCVR任务中关于“哪些商品更容易促成最终转化”的信号也会反向影响CTR任务对用户点击意图的刻画。这是一种隐式的知识迁移。如果两个模型各自为政它们对同一个用户ID的Embedding可能会学到完全不同的向量——CTR模型可能强调“爱点新奇东西”CTCVR模型可能强调“爱买大牌”这种割裂会让最终的CVR计算失去物理意义。而共享Embedding相当于要求模型对用户和商品的理解在点击和转化两个维度上必须保持一致。这不仅是工程上的简化更是对用户行为一致性的一种建模约束。实测下来去掉共享EmbeddingESMM的AUC指标会掉1.5个点以上线上GMV提升也几乎归零。3. 核心细节解析与实操要点从公式到代码的每一处取舍3.1 损失函数设计不是简单加权而是分而治之ESMM的损失函数写作 L LCTR LCTCVR。看起来就是两个二分类交叉熵之和但实际实现中有三个极易被忽略的细节第一样本权重分配。CTR任务的训练样本是全部曝光样本I正样本是点击CCTCVR任务的训练样本也是全部曝光样本I但正样本是“点击且转化”C∩V。这意味着对于一个既没点击也没转化的曝光样本它在CTR任务里是负样本在CTCVR任务里也是负样本对于一个点击了但没转化的样本它在CTR任务里是正样本但在CTCVR任务里却是负样本。这个“身份切换”是模型能学到区分能力的关键。很多初学者会误把CTCVR的正样本集单独抽出来只用这部分样本训CTCVR这就完全破坏了全空间建模的前提。第二梯度传播路径。CVR本身不参与反向传播它的梯度是通过 CTCVR 和 CTR 的梯度间接传递的。具体来说CVR yctcvr/ yctr所以 ∂L/∂yctcvr ∂L/∂ycvr* (1/yctr)而 ∂L/∂yctr ∂L/∂ycvr* (-yctcvr/yctr2) ∂L/∂yctr来自CTR loss。这个分母项的存在使得CTR任务的梯度会受到CTCVR预测值的影响反之亦然。这正是知识迁移发生的数学通道。如果你用框架的自动求导这个过程是透明的但如果你手写梯度更新就必须显式地写出这个链式法则。第三数值稳定性处理。yctr是一个0到1之间的概率值直接做除法可能导致数值溢出当yctr极小时CVR会极大。工业级实现中通常会对yctr加一个极小的平滑项 ε如1e-8即 CVR yctcvr/ (yctr ε)。这个ε不能太大否则会引入新的偏差也不能太小在FP16训练时可能被截断。我们在线上服务中最终选定的ε是5e-9这个值是在多次AB测试后平衡了数值稳定性和线上效果后确定的。3.2 特征工程哪些特征该共享哪些该隔离ESMM的原始论文里所有特征都输入到共享的Embedding层。但在真实业务中我们发现用户侧特征和商品侧特征其“共享必要性”是不同的。用户行为序列如最近点击的10个商品ID对CTR和CTCVR都至关重要必须共享但商品的静态属性如品牌、价格区间、是否包邮对点击意图的影响用户是否愿意点开看和对转化决策的影响用户是否愿意下单其权重和模式往往差异很大。强行让它们共享同一个Embedding向量反而会互相干扰。我们的解决方案是对商品侧特征采用“双塔式”嵌入。即为同一个商品ID分别维护两套Embedding向量一套专用于CTR塔叫它 eitem_ctr另一套专用于CTCVR塔叫它 eitem_ctcvr。这两套向量在训练初期是随机初始化的但通过共享的底层MLP在Embedding之后、塔分离之前进行一定程度的耦合。这样既保留了共享表征带来的泛化能力又给了模型足够的自由度去学习不同任务对同一商品的不同“解读”。上线后这套方案让CTCVR的AUC提升了0.8%而CTR的AUC几乎没有变化证明了这种“有控制的解耦”是有效的。3.3 在线服务的轻量化CVR不是“算出来”的是“查出来”的很多人以为ESMM上线后每次请求都要跑一遍完整的CTR和CTCVR前向计算再相除。这在QPS动辄几十万的推荐系统里是不可接受的。我们的实际部署方案是将CVR预测结果作为商品的一个实时特征预先计算并缓存。具体流程是离线训练好ESMM模型后每天凌晨用全量商品池跑一次批量推理得到每个商品在“典型用户画像”下的CVR预估值我们称之为“商品基线CVR”存入Redis。在线服务时召回阶段拿到一批商品ID直接从Redis里批量拉取它们的基线CVR值作为排序特征之一。真正的个性化CVR考虑当前用户实时行为则由一个更轻量的、只包含用户实时特征的“CVR微调模型”来提供这个模型的输入维度只有几十维延迟可以压到1ms以内。ESMM在这里的角色不是在线计算器而是离线特征生产引擎。这个设计把ESMM的复杂性完全隔绝在离线环节保证了线上服务的极致稳定和低延迟。4. 实操过程与核心环节实现从零搭建一个可验证的ESMM Demo4.1 环境与数据准备用MovieLens模拟真实漏斗我们不用淘宝或京东的真实数据而是用公开的MovieLens-1M数据集进行模拟。它包含100万条用户对电影的评分记录1-5分。我们将“评分4分”定义为“转化”V将“用户浏览过该电影详情页”定义为“点击”C。但原始数据里没有“浏览”记录所以我们需要构造。方法是对每个用户随机采样他评过分的电影中的30%作为“点击”样本C其余70%作为“未点击”样本¬C。这样我们就有了一个近似的曝光-点击-转化漏斗。最终我们构建出约100万条曝光样本其中约30万条为点击样本C约12万条为点击且转化样本C∩V。# 数据构造核心逻辑伪代码 import pandas as pd import numpy as np # 假设df_ratings是原始评分数据columns[user_id, movie_id, rating] df_ratings[is_conversion] (df_ratings[rating] 4).astype(int) # 为每个用户随机标记部分评分为点击 np.random.seed(42) df_ratings[is_click] 0 for user_id in df_ratings[user_id].unique(): user_mask df_ratings[user_id] user_id user_ratings df_ratings[user_mask].copy() # 随机选30%的评分记录作为点击 click_indices np.random.choice(user_ratings.index, sizeint(len(user_ratings) * 0.3), replaceFalse) df_ratings.loc[click_indices, is_click] 1 # 曝光样本 所有评分记录 # CTR正样本 is_click 1 # CTCVR正样本 (is_click 1) (is_conversion 1)4.2 模型定义PyTorch版ESMM重点看共享与分离下面是一个精简但功能完整的ESMM PyTorch实现。关键点在于shared_embedding的定义位置以及ctr_head和ctcvr_head的独立性。import torch import torch.nn as nn class ESMM(nn.Module): def __init__(self, user_num, item_num, emb_dim64, hidden_dims[128, 64]): super(ESMM, self).__init__() # 共享的Embedding层 self.user_emb nn.Embedding(user_num, emb_dim) self.item_emb nn.Embedding(item_num, emb_dim) # 共享的底层MLP可选用于进一步融合 self.shared_mlp nn.Sequential( nn.Linear(emb_dim * 2, hidden_dims[0]), nn.ReLU(), nn.Linear(hidden_dims[0], hidden_dims[1]) ) # CTR塔输入是用户商品Embedding拼接 self.ctr_head nn.Sequential( nn.Linear(hidden_dims[1], 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid() ) # CTCVR塔结构同CTR塔但参数独立 self.ctcvr_head nn.Sequential( nn.Linear(hidden_dims[1], 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid() ) def forward(self, user_ids, item_ids): # 获取Embedding user_vec self.user_emb(user_ids) # [B, D] item_vec self.item_emb(item_ids) # [B, D] concat_vec torch.cat([user_vec, item_vec], dim1) # [B, 2D] # 共享MLP shared_out self.shared_mlp(concat_vec) # [B, H] # 分别计算CTR和CTCVR y_ctr self.ctr_head(shared_out).squeeze(-1) # [B] y_ctcvr self.ctcvr_head(shared_out).squeeze(-1) # [B] # 计算CVR注意不参与梯度计算仅用于评估 y_cvr y_ctcvr / (y_ctr 1e-8) return y_ctr, y_ctcvr, y_cvr # 初始化模型 model ESMM(user_num6040, item_num3952, emb_dim64)4.3 训练循环损失计算与梯度更新的实操细节训练的核心在于如何在一个batch内同时计算两个任务的损失并确保梯度正确回传。下面是一个标准的训练步骤。import torch.optim as optim criterion nn.BCELoss() optimizer optim.Adam(model.parameters(), lr0.001) for epoch in range(10): model.train() total_loss 0 for batch in dataloader: # batch包含 user_ids, item_ids, labels_click, labels_ctcvr user_ids, item_ids, labels_click, labels_ctcvr batch # 前向传播 y_ctr, y_ctcvr, _ model(user_ids, item_ids) # 分别计算两个任务的BCE Loss loss_ctr criterion(y_ctr, labels_click.float()) loss_ctcvr criterion(y_ctcvr, labels_ctcvr.float()) # 总损失 loss loss_ctr loss_ctcvr # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Avg Loss: {total_loss/len(dataloader):.4f})注意labels_click是一个0/1张量对应每个样本是否点击labels_ctcvr也是一个0/1张量对应每个样本是否“点击且转化”。它们的长度和user_ids完全一致都是全曝光空间的样本。这是ESMM区别于其他多任务模型的最核心数据准备要求。4.4 效果验证不能只看AUC要看“漏斗一致性”评估ESMM不能只看单个任务的AUC。我们设计了一个“漏斗一致性检验”指标在测试集上对所有样本计算预测的CTR * 预测的CVR看它和预测的CTCVR的皮尔逊相关系数Pearson r。根据模型定义yctr* ycvr应该严格等于 yctcvr。如果这个相关系数低于0.95说明模型的内在一致性出了问题可能是训练不稳定或者数据构造有误。在我们的MovieLens Demo中这个r值稳定在0.992证明了模型结构的有效性。同时我们对比了单独训练的CVR模型只用点击后样本发现其AUC为0.721而ESMM计算出的CVR AUC为0.758提升了3.7个百分点这验证了ESMM缓解样本偏差的有效性。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题一CTR任务效果很好CTCVR任务效果很差甚至不如CTR现象训练过程中CTR的AUC轻松上0.85但CTCVR的AUC卡在0.70左右远低于CTR导致最终CVR计算结果波动巨大。排查思路检查正样本比例CTCVR的正样本是“点击且转化”它天然比CTR的正样本点击稀疏得多。在MovieLens里CTR正样本率约30%CTCVR正样本率只有12%。如果模型没有针对这种极度不平衡进行处理CTCVR任务很容易被负样本淹没。检查损失函数权重虽然论文说L LCTR LCTCVR但实践中由于CTCVR样本更难学我们通常会给它一个稍大的权重比如 L LCTR 1.2 * LCTCVR。这个1.2不是拍脑袋而是通过在验证集上扫参0.8~1.5找到的最优值。检查特征覆盖度CTCVR任务需要更多能区分“高转化潜力”的特征。我们在MovieLens Demo中额外加入了“该电影的平均评分”、“该电影的评分人数”作为全局统计特征这对CTCVR提升显著但对CTR几乎无影响。解决方案在CTCVR任务的损失计算中加入Focal Loss其公式为 FL(p_t) -α_t * (1-p_t)^γ * log(p_t)其中p_t是模型对正样本的预测概率。我们设置γ2, α0.25这极大地提升了模型对少数正样本的关注度。调整后CTCVR AUC从0.70提升至0.74。5.2 问题二线上AB测试CTR指标上涨但CVR和GMV不涨甚至微跌现象模型全量后监控发现CTR2.1%但CVR-0.3%GMV0.1%收益远低于预期。根本原因ESMM解决了CVR预估的偏差问题但它没有解决“CVR预估得准是否就意味着应该多推”的问题。一个商品CVR很高但如果它的客单价很低或者库存即将售罄盲目推它对GMV的贡献可能很小。ESMM输出的是一个纯净的“转化概率”但线上排序策略需要的是一个综合“商业价值”的分数。解决方案我们引入了后置重排Post-Ranking。ESMM只负责输出高质量的CVR预估值作为排序的其中一个强特征。最终的排序分是 CTR * CVR * Price * Inventory_Factor * ... 的加权组合。其中Price和Inventory_Factor是业务强规则由运营同学配置。这样ESMM的“精准”和业务的“灵活”就结合起来了。上线后GMV提升从0.1%跃升至1.8%。5.3 问题三共享Embedding导致训练初期CTR和CTCVR互相拖累收敛慢现象训练刚开始的几个epoch两个任务的loss都震荡剧烈尤其是CTR loss有时会突然飙升。原因分析共享Embedding意味着CTR任务的梯度更新会直接影响CTCVR任务的输入。在训练初期两个任务的优化方向可能不一致造成Embedding向量在两个目标间“摇摆”。独家技巧我们采用了渐进式解冻Progressive Unfreezing策略第1-3个epoch只训练CTR塔和CTCVR塔的上层网络head冻结所有Embedding层。让两个任务先各自学会“怎么用特征”建立初步的预测能力。第4-6个epoch解冻Embedding层但将Embedding的梯度缩放系数设为0.1即param.grad * 0.1让Embedding缓慢适应。第7个epoch起恢复正常训练。这个技巧让模型的整体收敛速度提升了约40%且最终收敛的AUC更高。它背后的逻辑是先让“大脑”head学会思考再让“感官”embedding慢慢校准比让感官和大脑同时从零开始瞎摸索要高效得多。5.4 问题四CVR计算结果出现异常大值如1000%现象日志里偶尔出现CVR预测值为12.5、8.7这样的数字明显违背概率定义。定位过程我们打印了出问题批次的y_ctr和y_ctcvr值发现y_ctr极小如2e-5而y_ctcvr相对较大如1e-4导致相除后结果爆炸。根因这是典型的数值下溢Underflow。在FP32精度下当y_ctr小于约1e-38时它会被表示为0导致除零错误。虽然我们加了1e-8的平滑项但如果y_ctr的预测值本身就趋近于0这个平滑项可能不够。终极修复在计算CVR前对y_ctr做一个硬截断Clippingy_ctr_clipped torch.clamp(y_ctr, min1e-7, max1.0) y_cvr y_ctcvr / y_ctr_clipped这个1e-7的阈值是我们在线上灰度期间通过监控y_ctr的分布百分位数P0.1确定的既能避免溢出又不会对正常预测造成干扰。上线后CVR异常值归零。6. 工程落地与效果复盘从Demo到千万级QPS的跨越6.1 模型服务化TensorRT加速与内存优化当ESMM模型从离线训练走向线上服务最大的挑战不是精度而是性能。一个标准的ESMM模型64维Embedding两层MLP在CPU上单次前向推理耗时约8ms这在线上是不可接受的。我们的解决方案是离线用TensorRT对模型进行FP16量化和图优化。具体步骤是先用PyTorch导出ONNX模型再用TensorRT的trtexec工具进行编译。关键参数是--fp16 --workspace2048指定2GB工作空间。编译后的引擎在T4 GPU上单次推理延迟降至0.35ms吞吐量达到2800 QPS。更重要的是TensorRT的内存占用比原生PyTorch降低了65%这让我们能在同一块GPU上部署多个模型实例为后续的A/B Test和模型热切换提供了硬件基础。6.2 效果复盘不止是指标更是产品逻辑的升级在某电商App的“首页猜你喜欢”频道我们进行了为期三周的AB测试。对照组是传统的“CTR人工规则CVR”排序实验组是“CTRESMM CVR”排序。结果如下指标对照组实验组提升CTR4.21%4.38%0.17ppCVR12.05%12.89%0.84ppGMV100%102.3%2.3%人均加购数1.821.957.1%这个2.3%的GMV提升看似不大但乘以平台的日均GMV就是实实在在的千万级收益。但比数字更有价值的是产品同学反馈用户对“猜你喜欢”的内容满意度调研得分从72分提升到了78分。这说明ESMM不仅推了更多“能卖”的商品还推了更多“用户觉得好”的商品。它让推荐系统从一个“追求转化效率”的工具升级为一个“理解用户真实意图”的伙伴。这种转变是任何单一指标都无法完全衡量的。6.3 后续演进ESMM不是终点而是多任务学习的起点ESMM的成功为我们打开了多任务学习的大门。但它也有局限它只建模了曝光-点击-转化的线性漏斗而真实世界里用户行为是网状的。比如用户可能先收藏再加购最后下单也可能看了详情页但去竞品比价后才回来下单。我们正在探索的下一代方案是ESMM在ESMM的框架下引入“收藏率”CFR、“加购率”CART作为新的辅助任务并用一个更通用的图神经网络GNN来建模这些行为节点间的跳转概率。初步实验显示它对长周期转化如用户7天内下单的预估能力比ESMM提升了15%。这印证了一个观点好的模型从来不是为了“炫技”而是为了更忠实地刻画现实世界的复杂性。ESMM教会我们的不是如何堆砌网络结构而是如何用最简洁的数学语言去逼近那个最本质的问题。我个人在实际操作中发现所有成功的模型落地都始于对一个具体业务痛点的深刻共情。ESMM之所以能成为经典不是因为它有多深的理论而是因为阿里妈妈的工程师们真的蹲在数据后台看着那一行行“点击未转化”的日志听运营同学抱怨“为什么好商品推不出去”然后问出了那个最朴素的问题“我们是不是从一开始就用错了数据”这个问题比任何复杂的公式都重要。