推荐系统排序模型的迭代,这几年几乎都绕不开一个词:Target Attention。从DIN到DIEN,再到各类长序列建模方案,拿当前候选item作为query去历史行为序列上算注意力,已经成了工业界的标准玩法。但真正推到线上你会发现,经典target attention是一笔巨额的算力账。ETA(End-to-end Target Attention)这个方向之所以值得关注,就是因为它把"注意力+长序列"这套高成本操作,做成了端到端可训练、线上近似常数延迟的工程方案:模型自己学如何压缩历史行为、如何用哈希检索替代全量注意力计算,排序效果不掉,在线开销还能降一个量级。这篇文章适合正在做推荐排序、广告预估、搜索精排的算法和工程同学,尤其是那些被长用户行为序列折磨到想砍序列长度的团队。
1. 什么场景逼出了ETA:Target Attention在工业界的三堵墙
1.1 用户行为建模的"长序列红利"与"算力税"
先明确一点:这里的Target Attention,指的是在排序模型里拿当前候选item作为query,去用户的历史行为序列上算注意力权重,再把加权后的行为向量喂给上层网络。这套范式从DIN、DIEN一路下来,工业界基本没人质疑它的效果——它让模型学会了"用户上一次买iPhone和这次看手机壳之间有什么关联",这种细粒度的目标交互,是简单的用户向量拼接给不了的。
可事情到了长序列这里就变味了。样本里每个用户平均几十到几百个行为item,看起来不算多;一旦你把序列长度拉到500、1000,线上推理要做的内积运算量直接奔着千万级去。长序列确实带来收益,不少团队实测把序列从50拉到300,离线AUC能涨千分之三到千分之五,但单条请求的target attention计算量也跟着线性涨。这就是"长序列红利"背后的"算力税"。
我在实际项目里遇到过特别典型的情况:序列长度提到250以后,单机QPS掉了一半,P99延迟从30ms膨胀到80ms以上。模型花在注意力计算上的时间比后面整个多层MLP还要长。绝大多数团队这时候会按下暂停键,转去做所谓的"离线预计算"——把用户表示提前算好存起来。可问题在于,target是请求时才知道的,用户表示一旦固定,就丢失了和候选item之间的交互信息,这不就是退化成简单的DIN without target吗?离线AUC跌得很快。长序列、实时target交互、低延迟,这三件事在当时几乎是一个"不可能三角"。
工业界起初靠工程手段去缓解:用向量指令集、做算子融合、上GPU推理。但这些都是"省算力",没有改变问题的本质——复杂度是O(序列长度×候选数量),做再多算子优化,遇到上千长度的序列还是吃力。我当时就意识到,靠工程微调治标不治本,需要在模型设计层面把复杂度结构性地降下来,比如把"全量算注意力"改成"先检索最相关的子集,再算注意力"。这个思路就是ETA的起点——用检索来替代全量计算,并且让检索参数跟着排序目标一起训练。
1.2 在线链路里target attention为什么算不动
我们来把一个实际请求的分摊过程算一笔账。假设用户行为序列长度L=800,一次请求通常要评估N=500个候选商品,经典target attention需要计算800×500=40万次内积,再配合softmax、加权求和,单次请求的浮点运算量在几百万到千万级别。而且这不是算一次,每个候选的query不一样,整个注意力操作要执行N次。在普通CPU部署下,这就是几毫秒到几十毫秒的开销。单看数字不吓人,但叠加到全站流量上,QPS一旦上千,在线服务立马吃紧。
更麻烦的是,这种计算完全没法预取。用户的800个行为item的embedding可以离线存好,但候选item的query是实时变化的,你不能预先算好"这个用户对所有商品"的注意力权重——那等价于一个用户×全量商品的大矩阵,存储和更新都不可行。所以线上必须实时跑target attention,哪怕你把行为序列截断、做负采样,只要还保留attention交互,这部分的延迟和算力成本就躲不掉。
所以ETA解决的核心痛点是:能不能在保持target attention这种"target自适应加权"效果的前提下,把在线复杂度降到对数级或常数级?ETA给出的答案是——把注意力计算先转化成近似检索问题,再利用哈希索引把最相关的行为item快速捞出来,只对这部分算真正的attention。这思路的原理类似搜索引擎:你搜索"蓝牙耳机降噪",不会对全库几亿文档逐一打分,而是先靠倒排索引粗筛出几百篇再做精细排序。ETA就是承认了"全量打分不现实",在用户行为序列上搭了一套轻量级的倒排索引。
2. ETA的端到端改造逻辑:一段从近似到可微的进化史
2.1 先搞清楚经典Target Attention在算什么
先复习一下标准公式。给定用户历史行为集合,每个行为item用d维embedding向量表示;当前候选target也映射成d维query向量。经典做法是拿query和每个key做内积,经过softmax归一化得到权重,再对所有value求加权和。简化讲就是"内积—归一化—加权求和"三步。内积越大,表示历史行为与当前候选越相关,它在最终用户兴趣表示里的发言权越大。
这套计算在离线训练时毫无问题,GPU可以并行跑几十万次内积。上线后你会发现,你需要的不是算一次,而是针对实时请求的候选集合反复算。如果候选集合大,比如粗排层要评估几万个商品,target attention会直接成为整个链路的瓶颈。这也是为什么很多团队把带attention的模型只放到精排,召回和粗排仍然用向量内积这类简单交互。
经典模型里还有一层隐患:训练和推理时的"注意力上下文"其实是不一致的。训练时拿全部历史行为做softmax,等于告诉模型"你可以看到用户的全部历史";推理时为了省算力,有的团队会把序列截断到最近N条——很可能把真正相关的长尾兴趣截掉了。ETA好一些,它训练时就是通过检索只挑选topK个相关行为,推理时同样只取topK,两边看到的上下文保持一致。这是端到端方案在一致性上带来的额外红利,也是很多人一开始没注意到的。
2.2 ETA用哈希把注意力成本打下来
ETA的具体做法,是把每个行为item的embedding通过一个哈希函数族,映射成一个固定长度的二进制码,比如64位。然后用二进制码之间的汉明距离来近似原来embedding的内积。为什么能这么近似?因为局部敏感哈希(LSH)的原理保证:内积越大(语义越相似)的两个向量,经过随机投影后落在同一侧的概率越高,对应哈希码的汉明距离越小。这个近似虽然粗糙,但完全够用——它不需要精确打分,只需要把"最可能相关"的一批行为捞出来。
在实际训练时,把用户所有行为item都做一次哈希编码,对当前target item也做同样的哈希编码,然后到汉明空间里找距离最近的K个行为item。K是个超参数,比如64或128。最后只对这K个行为item做真正的attention内积与加权求和。检索这一步已经不再是O(L)的遍历了,借助哈希表的桶结构或近邻索引,可以做到近似O(logL)甚至常数级,取决于桶的数量。序列长度从800涨到8000,对计算量的冲击被极大地削弱了。
这个设计最妙的地方在于,它没有完全丢弃target attention的交互性。你只是把"给所有历史行为分配权重"改成了"先把大部分无关行为直接丢弃,只对少数潜在相关的历史行为精算权重"。这其实是在模拟人脑的注意力机制——你不会对过去每一次购买都回顾一遍,而是瞬时想起那几个相关场景。对推荐系统来说,这既保住了行为序列的信息量,又把单次请求的耗时从线性降到了近似常数。
2.3 为什么必须"端到端"而不是两阶段
这里有一个容易被忽略的设计关键:ETA名字里最重的不是Target Attention,而是End-to-end。为什么强调端到端?最直接的答案是,如果你先把embedding训练好,再离线生成哈希码,再丢给下游attention去用,那哈希码是固定的,不会随着排序目标调整,检索出来的topK可能并不是真正对排序最有帮助的那一批。这就造成了"检索阶段优化检索目标、排序阶段优化排序目标"的割裂,两阶段各自最优,整体不一定最优。
更具体地说,排序模型的最终目标是点击率、转化率,不是哈希近邻精确度。如果哈希码无法感知最终排序目标,它可能把历史item按"语义相似"聚堆,可排序模型想要的是"对当前转化最有解释力的行为"。比如用户昨天刚看了婴儿奶粉,又看了游戏手柄,这两个行为都真实存在。从语义相似度看,它们和当前母婴候选都不算远;但从转化意图看,奶粉显然更该被赋予高权重。这种细粒度差异,只有让哈希检索跟着端到端损失一起调,模型才有机会学到"我该为当前候选重点捞出什么行为"。
端到端在工程上当然贵:哈希函数离散不可导,没法直接反向传播。训练时需要用可微的近似,比如用Gumbel-Softmax对每一位哈希码做连续采样,让梯度能流过哈希层,再配合温度退火,让训练后期逼近真正的离散编码。你可能觉得这有点绕——但换来的是"检索的每一步都服务于最终排序指标",从embedding到哈希码到topK到attention再到点击概率,一条管线走到底。这种一致性的价值,在实际A/B里通常比两阶段方案多带来零点几个百分点的AUC提升,同时线上链路反而更简单,因为推理时不需要维护一套独立的检索模型。
3. ETA实操落地全流程:从模拟训练到索引部署
3.1 特征结构与embedding接入细节
先说我理解的ETA特征接入方式,更贴近工程侧。用户的个性化特征主要分两部分:一部分是静态特征,比如用户画像、设备、城市;另一部分就是行为序列。行为序列里每个元素至少包含item_id,可能还有行为类型(点击、加购、下单)、时间戳、场景来源。item_id查表得到embedding,可以做成hash embedding放在超大表里。ETA对item_id embedding的维度不敏感,我们在实际项目里用64维,哈希码位长也取64,效果和128维差距不大,但显存和内存压力小很多。
序列长度这个参数,建议不用一刀切。ETA的设计优势就是不害怕长序列,但我们还是做了"最近300个行为+全局抽样300个行为"的混合窗口,这样既照顾近期兴趣,又能捕捉长周期稳定偏好。每个行为item进入模型时,除了embedding,最好还拼上时间差编码或位置编码。ETA论文里也有类似设计,时间差编码能告诉模型"这个行为发生在多久前",对注意力权重的语义很重要。如果不加任何位置信号,长序列里早先的行为和当天的行为在模型看来同权,排序效果会明显劣化,这点我们踩过坑。
时间特征建议用相对时间差而不是绝对时间戳。不同用户活跃度不同,统一用绝对时间戳会导致归一化困难。相对时间差做一次log变换,就能把"3分钟前"和"3天前"映射到合适尺度。另外,行为类型也要区分对待:点击行为和成交行为的置信度完全不同,ETA的attention打分如果对两者一视同仁,会偏向高频但低价值的点击。我们当时给不同行为类型单独设置缩放系数,让attention权重乘以行为置信度标量,实测对下单转化率更友好。
3.2 可微哈希层:训练时的两段式设计
可微哈希层是整个ETA改造的关键,也是复现时最容易卡住的地方。直接说结论:哈希层不能简单硬编码,否则梯度直接中断。我当时采用的做法是,给每个embedding x先经过一个线性层映射到b维实数向量h,再用可微方式逼近离散哈希码。最常用的是Gumbel-Softmax处理每一位:把h的每一维看成二分类logit,训练时从Gumbel-Softmax分布采样出0/1的概率值(连续值),推理时直接取h大于0输出1、h小于等于0输出0。
Gumbel-Softmax有个温度参数T。T越大,采样结果越平滑、梯度噪声小,但离离散哈希远;T越小,越接近真离散采样,但梯度方差大、训练不稳定。经验做法是从5.0开始,每训练几个epoch衰减一次,衰减到0.1左右拉平。这一步听起来简单,实际调起来非常费时间。我们第一次尝试时T恒定在1.0,离线AUC几乎不动;退火到0.1之后指标才慢慢上来。
训练时还可以做多次哈希采样取期望,比如同一个样本内对哈希层跑4次采样,把4次attention结果做bagging,能显著降低离散化带来的方差。代价是训练显存涨,我们当时用4卡并行才扛住。硬件有限的话,也可以简化成单次采样加梯度裁剪,实测效果略降但训练时间减半。还有个小技巧:哈希投影矩阵的初始化不要太小,否则h的方差过小,模型很难快速走出"几乎全是0或1"的区域;建议用Xavier初始化并加一层LayerNorm。
3.3 在线推理:汉明索引与全链路一致性
在线部分,ETA的落地可以分为两个阶段。第一阶段是预先计算所有行为item的哈希码。item的embedding和哈希投影在训练完后固定下来,离线把所有item的embedding转成64位二进制哈希码,跑一次全量任务存到内存索引里。这里推荐用Faiss的BinaryFlat或BinaryIVF索引,专门支持汉明距离近邻查找。BinaryFlat全量扫描,几亿条item放内存有压力;BinaryIVF先粗聚类再细分桶,检索快但需要额外维护聚类中心。我们最后用的策略是:2000万量级item直接用BinaryFlat,单条item才8字节(64位),2000万条才160MB,全量扫描汉明距离也就是几十微秒的事,比IVF省心得多。
第二阶段是query侧实时编码。线上每个候选item的embedding在模型加载后固定,把它和同一个哈希投影层作用后取符号位,得到64位哈希码,然后请求索引做近邻检索取TopK个行为item id。注意,这里拿到的是item id,不要在线上传item embedding去算attention,否则会退化成线上仍需查embedding表、传输大向量的局面。正确做法是离线把每个item id和它的哈希码、attention计算所需的value embedding一起打包进缓存服务;线上请求时按检索出来的item id取出value embeddings和注意力需要的键值信息,在本地小张量上快速算attention。
全链路一致性有一点特别容易被忽视——训练和推理的topK方式要对齐。训练时用Gumbel采样的近似哈希码做近邻检索,推理时用确定性argmax的二进制码,两者检索出来的topK可能不完全一样。这个gap不会让模型崩,但会影响离线评估的可信度。我的做法是:在模型训练收敛后,额外做一个"推理模拟"阶段,用推理模式的硬哈希码重新做一次评估,确认离线指标没有明显回退再提交上线。如果回退明显,多半是Gumbel采样和argmax的分布差异过大,这时要把温度调得更低、训练更久,直到硬哈希码下效果稳定。
4. 常见问题与排查技巧实录
4.1 现象一:排序指标没涨,日志却显示覆盖率在掉
这是ETA上线后最典型的"假没效果"案例。从监控面板上看,离线AUC涨了,线上点击率、转化率却原地不动;再深看日志,发现行为序列里item被检索出来的覆盖率(检索到的不同item数除以全部item数)在下降。原因往往是topK取得太小,比如K=32。用户兴趣是多元的,哪怕有很强的即时目标,32个历史行为也很难覆盖足够的兴趣面。处理办法是把K从32上调到96或128,你会发现排序指标开始松动。
另一个可能是哈希码位数不够。我们最初用48位,碰撞率高,近邻检索结果同质化严重;换到64位后覆盖率和多样性都恢复了。建议实验阶段从64位起步,观察汉明距离分布:如果大量item距离集中在0到4,说明位数太少;如果集中在25到35,说明编码空间利用充分。记住,哈希码不只是给attention用的,它还承担了"负反馈过滤"的职责——距离太远的item本来就应该被打压,所以不要一味追求精度。
4.2 现象二:离线AUC涨,线上大盘无收益
另一个高频现象是离线AUC上去了,线上却没有正向收益。这类问题十有八九出在训练与推理的数据分布不一致上。ETA训练时如果只用了全部历史序列作为检索源,但线上索引里只有一部分行为(比如只索引最近3个月的item),那么线上可检索集合比训练时小很多。解决方法是严格控制训练和线上使用同一份"索引物料":从训练样本构造时就只保留相同的item池和序列窗口,不要训练用长窗口、线上用短索引。
我也遇到过线上收益被链路其他环节吃掉的案例。排序模型输出变了,但粗排或召回阶段仍按旧逻辑过滤,最终进精排的候选集合变化不大。ETA这类模型的价值在于从长序列里重新挖掘弱相关的长尾兴趣,前提是下游有能力把这些兴趣对应的item送进精排。我们后来把ETA的topK行为item映射成召回关键词,反哺了召回侧,整体收益才真正显现。这提醒你:ETA不是孤立模型,它是整条推荐链路的组成部分,上线前最好把上下游链路的候选传递逻辑一起过一遍。
4.3 现象三:延迟没降下来,CPU反而上去了
有些团队复现时发现延迟不降反升。原因通常是哈希检索与attention计算之间的调度没做好。比如线上每来一个候选item,如果都实时去Faiss索引里做一次近邻查询,查询本身有开销;之前直接算attention虽然复杂,但也只是向量内积,CPU指令集优化得很好。ETA要真正降延迟,核心是让检索结果可以被批量复用。我给一个经验:对同一个target item,在一个请求内只计算一次哈希码,然后并行对多个topK结果做attention再合流,不要每个候选独立重复查表。
还有一个容易忽略的点是索引服务的更新。item池每天都有新增、下架,如果索引更新延迟,线上检索结果和模型训练时期的分布会越拉越远。建议用每日增量构建而不是全量重建,并且上线前做一轮"索引命中率"监控:检索引擎返回的item里,有多少能命中线上特征缓存。如果命中率低于90%,就得排查embedding表、哈希码计算逻辑是否有版本错位。这类问题往往不报错,只会在指标层面悄无声息地拖后腿,排查起来最耗耐心。
5. 项目实战中的经验沉淀与下一步扩展空间
从ETA这个项目里,我最深的体感是:它的核心不是某个花哨的注意力变体,而是"用检索思维重构注意力计算"这套方法论。我们把哈希检索、近似近邻、Gumbel技巧全部串起来之后,序列长度从300提到2000,整体排序AUC没有掉,线上P99延迟反而稳定在原有水平以下。这件事给了我两个启发。第一,很多"算不动"的模型结构,问题不一定在算子层面,而在于计算范式本身——ETA证明attention不需要全量扫遍序列,只要检索做得足够好,效果可以逼近甚至超过全量。第二,端到端训练的意义不只是一个技术名词,它决定了检索出来的每一条行为是否真正服务于排序目标。两阶段方案永远有信息损耗,而端到端让损耗变得可学习、可优化。
线上稳定运行一段时间后,我们也在思考ETA的扩展方向。一个是把同样的哈希近邻思路迁移到粗排场景,让粗排也能吃到长序列的交互信息;另一个是结合多兴趣建模,让检索出来的topK行为先聚类成几个兴趣簇,再分别和target做attention,进一步缓解长尾兴趣被平均掉的问题。ETA不是终点,它更像一个"高性能注意力基座",未来任何需要和长上下文交互的模块,都可以尝试用这套检索加端到端优化的思路重新设计。如果你正在做序列建模,并且被在线延迟卡得束手束脚,不妨从ETA的思路入手——先把全量注意力换成检索式注意力,再让检索跟着损失函数一起学,线上指标和工程成本都会给你惊喜。