尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
CAN:用查表方式建模CTR交叉特征,兼顾效果与serving开销
最近在梳理点击率预测里的交叉特征建模时我把CAN这篇工作又翻了一遍顺手在一侧推荐场景里做了轮对比实验。趁理解还热乎把它的核心思想、工程落地细节和踩坑经验整理出来给同样在做CTR预估、广告排序或者电商推荐的同学一个参考。CAN全称是Co-Action Network中文直译是协同作用网络名字不重要重要的是它解决的问题。点击率预测里有一个很经典的现象用户会不会点击某个商品很大程度上取决于这个商品和用户历史行为里的商品是否“搭”。这个“搭”的语义业内叫特征协同作用。如何低成本、又不过拟合地去建模这种协同作用就是CAN最核心的命题。这篇工作最打动我的一个点是它把“用户历史行为和当前候选商品之间的交叉信号”从动态计算变成了一张可查询的记忆表让训练和在线serving解耦既控制住了模型大小又把推理开销压到了接近查字典的级别。1. 先说结论CAN到底在解决什么问题1.1 点击率预测里最难啃的那块骨头现在常规的CTR模型基本都是一个“塔式结构”底层喂用户画像、商品属性、上下文这些特征中间经过embedding层、若干交叉层和全连接层最后输出一个点击概率。这类模型真正拉开差距的地方往往不在底层特征本身而在“特征之间怎么交互”。举个具体的例子。用户张三最近点过五件商品其中三件是和户外露营相关的两件是厨具。现在来了一个候选商品是防潮垫。你猜张三点不点大概率会点因为他历史行为里有“露营”这个强信号而且防潮垫和帐篷、睡袋这类商品天然强相关。这种“用户行为”和“目标商品”之间的关联就是最典型的特征协同作用英文里叫feature co-action。难点在于这种协同作用不是靠简单拼接就能学出来的。如果你把用户行为商品ID向量和目标商品ID向量拼在一起丢给MLP模型确实理论上能学但实践中会发现两个问题一是ID组合空间巨大行为序列里的商品和目标商品的有效组合量是亿级别的MLP很难在有限样本里把每种组合的模式都记住二是即使记住了也都是死记硬背碰到没见过的新组合就抓瞎。所以大家才会专门设计结构去“显式”建模协同作用而不是等着全连接层自己隐式拟合。CAN要解决的本质上是这个问题在推荐/广告场景里“用户历史行为”与“候选物品”之间的交叉特征到底应该用什么结构去建模才能做到既能捕捉有效协同信号又不让参数量爆炸、不出现在线推理扛不住的情况。1.2 这篇工作适合谁我认真读完CAN之后判断它的适用范围其实比论文标题看起来更宽。如果你是下面这三类人我建议你重点看一下。第一类是正在用DIN、DIEN这套动态注意力机制做行为建模的人。这类方案的推荐效果确实好但线上serving开销也不小每次请求都要把用户全部历史行为拉出来跟目标商品做一轮注意力计算。如果你已经被线上算力压得头疼CAN这种“训练时动态建模、serving时静态查表”的思路会是个不错的替代或补充。第二类是在中小规模场景里试过独立交叉网络、最后被过拟合折磨的人。比如有的团队直接把用户行为商品和目标商品做外积再拿外积特征喂给深层网络样本一稀疏AUC很快就掉下来。CAN用一张共享的记忆张量把“所有pair组合”压缩到固定规模的参数空间里本质上是让模型在交叉特征上学了一个低秩近似天然带正则效果对这个痛点非常对症。第三类是做特征工程做得快吐了的人。协同作用类特征人工构造要处理组合爆炸踩完一轮坑还要担心线下和线上特征不一致。CAN的特征入口只需要行为ID和目标ID模型内部自己算交叉你几乎不需要额外准备离线特征表上线成本低了一个档次。2. 从DIN到CAN建模“用户行为×目标商品”的演进2.1 attention方案的甜与痛DIN出现之前主流做法是直接把用户行为序列里的所有商品embedding求和或者求平均然后再和目标商品embedding拼接。这种做法的毛病在于它把行为序列里每个商品的权重看成一样但真实场景里显然不是这样。用户点过十件商品其中可能只有两件跟当前候选商品相关这两件才应该主导预测结果。DIN的解法是引入一个local activation unit说白了就是做个注意力加权对目标商品embedding和行为序列里每个商品embedding算一个相关性分数再按分数加权求和。这样“行为序列中被目标商品激活的部分”就能被强调出来效果也确实立竿见影。但这个方案的痛点也很直接。线上推理时目标商品是不确定的每来一个请求用户行为序列里每个商品都得和目标商品重新算一遍相似度这个计算量和行为序列长度是正相关的。序列短还好序列一长比如电商场景里用户半年点过的商品可能有几百上千个那每次预估都是一场不小的矩阵运算。在工业级流量下这个成本是很敏感的。更要命的是attention机制虽然灵活但它的训练信号完全来自“当前这个目标商品与历史行为关系”的监督。如果某个用户的行为序列本身很稀疏比如新用户只有一两次浏览记录那attention权重学得就不稳定反而容易在某几个样本上过拟合。DIN的很多变体不断加结构、加辅助loss本质上都是在补这个洞。2.2 朴素交叉网络为什么在工业场景翻车与attention这类动态建模路径不同还一派做法是“静态交叉”路线。典型形态是单独拉出一个交叉模块输入用户行为商品ID和目标商品ID输出一个交叉向量再塞进主模型。这个模块通常是多层的MLP也可以直接做外积、双线性插值公式上大致长这样f(u, v) MLP(concat(embedding(u), embedding(v))) 或者 f(u, v) embedding(u) ⊗ embedding(v)思路很直白但一落地就出事。拿电商场景来说行为侧商品ID如果是十万级目标侧候选商品ID也是十万级pair组合就是百亿级。你让一个MLP去拟合百亿组合空间每个组合平均分到的训练样本可能连个位数都不到模型很容易把注意力放在“记住了谁和谁一起出现在训练集里”而不是真正泛化地学到“什么样的商品搭配合理”。另一个工程问题是这类模块通常只对“出现过的pair”起作用。线上出现一个新的pair组合时这个模块给出的输出基本等于在插值效果并不可控。很多团队到最后不得不给模块加各种约束、正则、dropout换来多少提升不好说但训练稳定性先下降了不少。CAN论文里提到这类静态交叉模块在小规模数据上明显过拟合简单说就是“训练集AUC很好看测试集和线上全都拉胯”。我自己的经验也印证了这点独立交叉网络的有效参数过多而CTR场景里交叉特征又是出了名的稀疏两者放一起就是灾难。2.3 CAN核心思路压缩-查表CAN在这两条路线之间找到了一条折中的路把“协同作用向量”做成一个可以学习的记忆张量用行为商品ID和目标商品ID的pair作为地址去查表。查出来的向量就是这个pair的协同作用表示。用人话说就是准备一本大字典字典的词条是“历史商品目标商品”的组合每个词条的内容是一个K维向量。训练的时候模型看到一条行为记录和一个目标商品就去查这本字典里对应的那个词条把查出来的向量喂给后续网络。字典内容随训练不断更新。部署之后字典内容固定下来线上请求过来直接查对应行拿到向量就继续走网络完全不需要再做历史行为与目标的实时交互计算。“压缩-查表”这个思路的精妙之处在于它用地址共享解决了组合爆炸。真正常见的历史商品和目标商品组合远远小于笛卡尔积的全集。与其为每个组合都准备独立参数不如让它们在同一个有限大小的记忆张量里按地址映射高低频pair自动共享部分表达。这就把“百亿组合”压缩成了“百万槽位”参数量可控泛化能力反而更强因为每个槽位都被更多样本训练得更充分。3. 核心设计拆解Co-Action Memory是怎么工作的3.1 整体架构与数据流CAN在模型层面最大的改动是在原CTR网络基础上增加了一个“记忆模块”这个模块就叫Co-Action Memory。它不是一个独立跑完再拼接的离线公式而是端到端训练的一部分。先说输入侧。常规CTR模型会有三路核心输入用户行为序列比如点击过的商品ID序列、目标商品特征候选商品的ID和属性、以及用户/上下文特征。CAN的交叉建模只关心前两路的交互。对行为序列里的每一个商品ID u以及目标商品ID v模型生成一个pair然后对这个pair做记忆查询得到协同作用向量。这个协同作用向量的去向论文里大致给了两种用法。一种是把查询到的向量重新注入到行为序列侧序列里的每个行为商品向量“增广”了一层来自交叉记忆的信息后续再用attention也好、pooling也好看到的信号更充沛。另一种是把查询到的向量注入到目标侧目标商品embedding会重新融合行为侧带来的反馈再进入全连接部分。实际工程里也可以两路都放关键是看你的主模型结构适合在哪一步吸收这个向量。渠道基本就一句话协同作用向量是锦上添花的附加特征放在哪一层取决于它和已有模块的衔接顺不顺。整个模型还是走标准的CTR训练范式。输入一批样本前向算交叉记忆查询、拼接、送入主网络、输出点击概率然后和真实点击标签算logloss反向传播更新所有embedding和记忆张量。不需要额外设计复杂的辅助目标也不要求你准备额外的联合特征表对已经有一套CTR pipeline的团队来说改造点非常集中。3.2 记忆张量的查询过程记忆张量M可以看成是一个形状为L×D的矩阵L是槽位数量D是每个协同作用向量的维度。查询的核心动作是把行为商品ID u和目标商品ID v这一对通过某个映射函数变成一个整数索引然后用这个索引去M里取对应的那一行向量。最朴素的映射方式是对u和v分别做一次哈希然后把两个哈希值的组合映射到槽位空间。比如c(u, v) M[ (hash(u) * P hash(v)) mod L ]这个写法够简单但哈希冲突会影响效果。工程上更稳的做法是多重哈希查询K次取K行的平均或拼接。公式上像这样c(u, v) mean( M[ h_1(u,v) mod L ], M[ h_2(u,v) mod L ], ..., M[ h_K(u,v) mod L ] )用K个相互独立的哈希函数同一个pair可能映射到K个不同槽位即使单个槽位发生冲突多槽位平均之后冲突带来的噪声会被摊薄。理论上的hash trick在NLP的n-gram模型里已经用了很多年搬到交叉记忆上来是顺理成章的事只是这里每个槽位存的不再是标量权重而是一个向量。也可以用更“深度”的方式做查询不直接取整数值索引而是让两个embedding先做可学习的映射输出一个在L槽位上的软分布再把记忆张量L行做加权求和。这样整个查询过程对u、v的embedding是可微的模型可以更平滑地学习让哪个槽位“负责”哪种pair。代价是查询计算量变大对线上性能不友好所以工业落地大多还是用哈希多槽位方案。说个我自己的理解CAN记忆张量的查询过程本质上是在学一个受限的三角关系。维度D是协同作用向量长度槽位L是交叉模式的容量。槽位越多模型能记住的不同交叉模式越多但也越容易过拟合维度越大每个模式内的表达越丰富但训练所需样本也越多。这两个数不是越大越好和你的样本量、行为集合大小强相关。3.3 为什么训练和serving能解耦CAN最讨喜的地方是训练时那个“记忆张量”可以随便更新迭代但模型一旦定稿线上serving时它就是一个固定数组。拿它和attention方法做个直观对比。DIN在serving时每来一个请求都要把候选商品v的embedding和用户行为序列里所有商品u_j的embedding算一遍点积、softmax、加权求和这整套计算必须在请求实时路径里完成。而CAN在serving时行为商品ID和目标商品ID之间的协同作用向量已经被“固化”在记忆张量里了线上只需要查表查出来的向量直接拼到特征里不用再算任何pair级别的交互函数。这意味着CAN的serving耗时基本不随行为序列长度增长行为序列就算从几十涨到几百多出来的也只是多查几次表而attention多出来的是一整轮矩阵运算。这一点对超大规模推荐场景是实打实的收益。我专门在内部做了一次粗略benchmark。同样一个基础模型加DIN的serving p99耗时比加CAN高出大约30%到60%序列越长差距越明显。当然这个数字跟实现细节有很大关系但量级上的差异是稳定的。如果你正处在“为了提点而加attention模块结果线上机器成本翻倍”的处境里CAN这个方向是值得认真考虑的。4. 从论文到落地实操步骤与调参心得4.1 最小化接入路径如果你的团队已经有一套基础的CTR模型接入CAN不需要把模型推倒重来。我整理了一条最小改动路径照着做基本能跑通第一步确认基础特征入口。你至少需要用户行为侧的商品ID序列以及目标侧的商品ID。如果两侧ID空间不是同一个可以各自做频次截断映射到连续整数空间这一步是为了控制哈希空间的上界。第二步初始化记忆张量M。形状为L×D随机初始化也可以用预训练的item embedding做一个低维初始化加速收敛。第三步实现多重哈希索引函数。选择K个独立哈希函数对(u, v) pair生成K个槽位索引取M对应K行求平均或拼接。第四步把协同作用向量接入主模型。最省事的做法是拼到行为序列每个行为商品的embedding后面或者拼到目标商品embedding后面然后正常走你原有的pooling和全连接层。第五步和主模型一起端到端训练。loss用你原来的CTR loss就可以记忆张量作为普通参数参与BP更新。整个改造成本大约就是一个可学习的Embedding矩阵、一个哈希函数模块、几行拼接代码。比起重训一个复杂attention网络动的东西少得多而且非常容易回滚——把协同作用向量的入口拔掉模型就直接回到原结构。需要提醒的是别一上来就追求论文里的完整CAN形态先把最简版本上线到离线实验确认AUC有正收益之后再增量做复杂融合。工程上我一直信奉“先证明结构有价值再证明结构能扩展”。4.2 关键参数选择与我的调参心得我在实验里趟过一遍后发现CAN相对敏感的四个参数是槽位数量L、向量维度D、哈希函数个数K、以及协同作用向量的接入位置。下面对照表整理了它们的影响和我的经验值参考。参数影响方向我常用的起点调整原则槽位数量L记忆容量决定能区分多少交叉模式行为集合大小的平方根×16左右样本量大可增大样本稀缺要减小观察冲突率向量维度D每个交叉模式的表达能力16~32和主模型其他embedding维度对齐过大会过拟合哈希函数个数K查表稳定性和冲突平抑能力2~4K越大查询越稳定但计算量线性上涨接入位置影响信息如何流动行为侧或目标侧任选先试以离线AUC为准两侧都加不一定更好槽位数量的选择我自己有个习惯先用频次截断后的行为商品数做估算。假设行为侧商品截断后剩约50万平方根是700多乘16就得到1万2左右的槽位初始值。这个量级不会大得让模型记不住也不会小得让冲突爆炸。之后在离线实验里做一轮L的网格搜索看AUC随L的变化曲线找到平台期就行。维度D不要想当然调大。D的本质是每个交叉槽位里“存多细的信息”D越大每个槽位需要越多样本支撑。在样本量不变的情况下D翻倍可以让离线AUC略涨但测试集AUC往往掉得更快这就是典型的过拟合信号。和DIN里的hidden size不同CAN的D更偏向“够用就好”。哈希函数K建议至少取2。K1时如果某个槽位成了热点所有映射到该槽位的pair都被同一个向量表达质量直接拖垮模型。K2或K4之后热点效应大幅缓解继续增大收益递减但serving耗时和训练显存会跟着涨。接入位置没有绝对标准。行为侧的好处是后续attention/pooling能带着协同信号继续聚合适合序列信息丰富的场景目标侧的优点是直接增强目标表征适合目标侧特征较弱、主要靠行为特征打分的场景。我建议你两个位置都跑一轮离线选AUC高的那路别两边同时上都塞容易让梯度信号混乱。4.3 与attention/DIN共存的工程方案很多同学会问一个直击灵魂的问题我线上已经有DIN了还有必要上CAN吗我的答案是可以共存而且共存时两者各司其职效果通常比单用更好。AUC不一定每次都能大涨但serving性能和鲁棒性上的收益是确定的。理解共存之前要先分清这两类模块的定位。DIN这类attention机制捕捉的是“这一次”目标商品与行为序列之间的动态相关性。同样是用户买过帐篷和锅当候选商品是防潮垫时帐篷被激活得多当候选商品是炒锅时锅被激活得多。这种“按需要变权重”的能力是attention天生的强项。CAN捕捉的是“这一类”行为商品和目标商品之间的静态协同倾向比如“露营类商品”和“防潮垫”之间存在稳定的正协同“厨具类”和“防潮垫”的协同就弱一些。一个偏动态、一个偏静态二者在信息层面是互补的。共存时的典型结构是行为序列里的每个行为商品embedding先和通过CAN查表得到的协同作用向量拼接或相加得到增广后的行为embedding再送入DIN的attention单元做动态加权。这样DIN在算权重的时候看到的每个行为商品不再只有孤零零的ID信息还带了一层“和目标商品的默契度”先验注意力分配会更准确。反过来用也成立CAN的协同作用向量可以先和目标商品embedding融合作为attention里的query让目标侧在计算相关性之前就吸收了行为侧的反馈信息。工程上要注意控制两部分的特征比例。我遇到过一上来就把协同作用向量维度设成和原embedding一样长结果CAN信号直接压倒行为ID信息DIN变成了摆设。稳妥做法是协同作用向量先经一层线性压缩到原embedding维度的一半再和原embedding做concat或add让模型自己决定权重。5. 常见问题与排查技巧实录5.1 典型问题速查表实验阶段大家会碰到的问题高度相似我把自己的排查经验整理成表格按症状定位原因再给解法。症状可能原因解法补充说明离线AUC没有变化协同作用向量被后续网络学成无效特征检查向量接入位置尝试注入目标侧基础模型太强时边际收益会变小这是正常现象训练AUC高、测试AUC掉槽位太多或向量维度太大导致过拟合缩小L或D增加哈希数K尤其要注意样本量较小的场景L必须相应缩水行为序列一长训练显存溢出每个行为item都做一次CAN查询累计量太大对序列做截断或负采样不要在全集上做线上serving不受影响因为查表是轻量的冷启动商品完全学不到信号商品ID太稀疏pair几乎没有样本用类目ID或粗粒度ID替代部分细粒度ID做pair查询相当于做了特征泛化牺牲精度换覆盖度与DIN共存后AUC反而降协同向量淹没了行为ID信息降低协同向量维度或改成add而非concat让attention有空间继续发挥作用补充说一点排查逻辑AUC没变不一定代表CAN没用可能是增益被其他强特征吸收了。我建议在看总体AUC的同时单独观察“新pair组合”的预测稳定性或者按行为长度分桶看指标。CAN在行为稀疏桶上的增益往往更明显因为它的参数共享机制对这些样本更友好。5.2 我踩过的三个坑第一个坑是直接用原始ID空间做pair索引。我第一版实验直接用线上几千万商品ID做哈希查询槽位无论怎么配离线一测都有明显的冲突噪声。后来发现正确的做法是做频次截断只保留高频商品ID进入记忆查询低频长尾ID统一映射到一个共享“未知”槽位。这一步做完实验指标直接上了一个台阶。第二个坑是把协同作用向量维度设得和主线一样大。当时想的是信息多存一点总没坏处结果离线训练AUC和验证AUC之间的gap迅速拉大。后来压到16维肉眼可见收敛变稳。原因是每个槽位向量的样本支撑有限维度越高需要越多样本去填满它CTR训练样本虽然多但落到具体某一个pair组合上还是稀疏。第三个坑是让CAN和DIN在行为侧同时叠加但没做特征分离。第一版代码里我把协同作用向量直接拼到行为embedding上然后一起送进DIN的attention计算。结果attention权重开始被协同向量主导行为ID自身的信号反而弱了整体效果不如只用CAN。后来让CAN向量走独立的gate层再和原行为embedding融合问题才解决。这也印证了一个经验新模块加入时先验证它独立有用再验证它组合有效不要默认“越多越强”。最后说一点个人体会。CAN这个思路之所以让我觉得有价值就是它找到了“动态交叉建模效果好但贵”和“静态交叉建模便宜但容易过拟合”这两极之间的平衡点。它不是要淘汰attention而是重新分配了哪些信号需要动态算、哪些信号可以静态存。这套取舍哲学其实比CAN本身更值得借鉴。毕竟在真实业务里模型设计永远是效果、成本、稳定性三者的博弈能用查表解决的事就没必要每次都重新算一遍。
RELATED

相关推荐

华硕ET2230INK一体机深度拆解:卡扣逻辑与硬件升级实战指南

华硕ET2230INK一体机深度拆解:卡扣逻辑与硬件升级实战指南

简介:本资源是一份面向硬件维修人员、DIY爱好者及IT运维工程师的华硕ET2230INK一体机深度拆机实操指南,系统解决紧凑型一体机内部访问难、升级路径不明确、卡扣结构易损等典型问题。文档以图文结合方式完整还原从光驱弹出、挡板拆卸、隐藏螺丝定位、后壳…

📅 2026/10/26 14:22:24
Pandas缺失值处理:类型识别、语义归因与工程化实践

Pandas缺失值处理:类型识别、语义归因与工程化实践

1. 为什么Pandas的缺失值处理不是“填个数”那么简单很多人第一次在数据分析中遇到NaN,第一反应是赶紧用.fillna(0)或者.fillna(methodffill)糊弄过去——我试过,也这么干过。但后来在某次模拟项目X的用户行为分析中翻了大跟头:明明清洗后的数…

📅 2026/10/26 14:17:23
YOLO系列全解析:从算法原理到部署实战

YOLO系列全解析:从算法原理到部署实战

YOLO 从 2015 年首次亮相到现在,版本迭代速度之快、生态覆盖之广,在目标检测领域几乎找不到第二家。很多刚接触的同学问我“现在该学哪个版本”,说实话这个问题没法一句话回答——不同版本的定位、硬件要求、任务支持都不一样。这篇文章把 YO…

📅 2026/10/26 14:17:23
MORE NEWS

更多资讯

📰

修仙类QQ机器人运营模式与技术架构对比分析

1. 修仙类QQ机器人运营模式概述 在QQ社群生态中,修仙题材机器人已经成为垂直领域的重要工具。这类机器人通常以文字互动游戏为核心,通过模拟修仙小说中的境界突破、功法修炼、门派争斗等元素,为社群用户提供沉浸式体验。目前市场上主要存在两…

📰

焊接数值模拟中热源路径规划与FLUENT仿真优化

1. 焊接数值模拟中的热源路径规划挑战焊接数值模拟领域有个特别有意思的现象:当热源沿直线移动时,熔池形态和温度场分布往往能获得不错的仿真精度。可一旦遇到圆弧轨迹,整个模拟结果就开始变得"任性"起来。去年我在模拟一个环形焊缝…

📰

DeepSeek Harness桌面版发布:开箱即用的AI工作台与插件体系解析

1. 桌面端AI工作台到底解决了谁的痛点第一次看到"DeepSeek Harness桌面版正式发布,开箱即用"这个消息,我脑子里冒出来的第一个念头不是"又一个套壳客户端",而是"终于有人把命令行那套东西搬到图形界面了"。过去…

📰

Linux下Redis安装配置与性能优化实战指南

1. Linux环境下Redis的安装与启动全指南Redis作为当下最流行的内存数据库之一,在Linux服务器上的部署是每个开发者必备的基础技能。我在过去五年里为数十家企业部署过Redis集群,发现80%的初级开发者遇到的启动问题其实都源于几个常见的配置误区。本文将带…

📰

Agent定时任务总跑偏?触发与补跑规则设计实战

1. 定时任务跑偏的根因拆解1.1 为什么 Agent 场景下的定时任务格外容易失控做过传统后端定时任务的人,第一次把定时逻辑搬到 Agent 上,大概率会经历一次“信心崩塌”。传统 Cron 任务面对的是一个确定性系统:到点执行、执行完写库、失败重试&…

📰

Python频次统计与多关键字排序:数字次数排序题详解

1. 题目还原:这道“数字次数排序”到底让你干什么第一次看到“数字次数排序”这个题目名,很多人会以为是要把数字本身排个序,结果一读题面才发现压根不是这么回事——它要排的是“数字出现的次数”。这个差别看起来很小,但直接决定…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬