首页 / 资讯中心 / 文章详情

CAN模型解析:用参数生成与特征调制突破CTR预估的表示瓶颈

CAN模型解析:用参数生成与特征调制突破CTR预估的表示瓶颈 ★ FEATURED ARTICLE
做点击率预测这几年我越来越觉得特征交叉才是模型效果的分水岭。CTR预估领域里有一个模型叫CAN——Co-Action Network专门用来建模特征协同作用Feature Co-action。它不靠隐式交叉也不靠简单的embedding内积而是把特征之间的相互作用直接参数化一个特征生成变换参数去调制另一个特征的表示。这篇文章我会从问题背景、核心设计、训练细节到实际踩坑完整拆一遍CAN适合正在做推荐系统或广告CTR模型的算法工程师也适合想系统了解特征交叉建模的同学。1. 先把问题说透CTR预估里的特征协同到底是什么1.1 一个特征单独看不值钱协同才值钱广告和推荐场景里的CTR预估本质上是这样一个问题给定一个用户、一个商品或广告以及当前上下文环境预测用户点击的概率。输入特征通常分几类用户侧特征年龄、性别、历史点击类目分布、商品侧特征类目、品牌、价格带、上下文特征时间、渠道、位置。关键问题在于任何一个特征单独拿出来其实都说明不了太多。光看性别男没有意义只有把性别和商品类目放在一起才有判断价值——男性看电竞类目可能点得很欢看美妆类目大概率不点。这就是特征协同作用多个特征联合在一起才产生完整的预测信号。用生活里的例子最好理解盐本身不是菜和白菜搭配是腌菜和牛肉搭配是酱牛肉和冰激凌放在一起就是黑暗料理。同一个特征在不同的搭配下语义完全不同。CTR模型要做的就是把这种搭配关系学出来。实际业务里特征协同的收益很大。我曾经做过一个电商场景的模型单纯加一个用户活跃层级×商品价格带的交叉特征离线AUC就能涨千分之三到五。在几亿用户的量级下这个涨点对应的可能是千万级的GMV差异。所以行业里从不缺做特征交叉的模型从FM到DeepFM再到各种attention方案都是为了把协同作用榨干。1.2 老方法的问题表示瓶颈和参数共享为什么有了FM和DeepFM还要折腾新结构因为现有方法有两个绕不过去的坎。第一个坎叫表示瓶颈representation bottleneck。FM系列的做法是把特征映射成固定维度的embedding然后对两个特征算内积把协同作用压缩成一个标量。问题是一个32维的向量内积就是一个数它能携带的信息量极其有限。两个特征之间的协同理论上是一个高维的、细粒度的交互用一个标量去概括丢掉的细节远比留下来的多。就好比两个人聊了半天最后只总结出一个嗯信息全丢了。第二个坎是参数共享过于粗放。DeepFM、DCN这类模型把交叉交给MLP隐式学习理论上MLP能逼近任意函数但实际训练中MLP要同时学所有特征的交叉模式参数是全局共享的。某个特定男性用户×电竞类目组合的专属规律没有一个专门的参数去表达它必须和几万个其他组合挤在同一个网络里。数据量足够大的组合也许能学出来数据量不够的组合就学不好。DIN、Target Attention这类方法用attention机制算是往前走了一步。但它们做的事情本质上是从行为序列里加权组合已有表征而不是为每一对特征生成新的、专属的变换参数。attention学到的是加权重要性CAN想学的是这个组合到底应该产生一个什么样的协同向量。1.3 CAN一句话解题CAN的核心思路如果用一句话概括就是把特征对之间的协同作用建模成一个特征生成变换参数去调制另一个特征的表征。拿用户性别和商品类目举例用户性别作为触发特征先生成一组变换参数缩放权重和偏置然后作用到商品类目的目标特征embedding上。不同的性别取值生成的变换参数不同再往上组合不同上下文里同一取值也会产生不同结果。于是每一对特征组合得到的不是一个标量相似度而是一个完整的、带着专属调制的协同向量这个向量再喂给后续MLP处理信息量比一个内积标量大得多。这个思路在WSDM 2023上由工业界团队提出后在推荐圈讨论度不低。原因不难理解它没有引入特别复杂的结构却精准打在了表示瓶颈这个要害上。和CAN容易混淆的是汽车电子里的CAN总线协议平时聊到这个名字要留意语境搜索资料时建议加上CTR点击率这类限定词。2. CAN核心设计拆解协同作用单元是怎么工作的2.1 整体网络结构CAN整体上还是一个Embedding 多层网络 输出层的框架并没有跳出常见CTR模型的骨架。它的特殊之处在中间多了一层协同作用层Co-Action Layer。输入的类别特征包括用户侧、商品侧、上下文侧先查表得到各自embedding。然后按事先选好的特征对比如用户性别×商品类目把这两个embedding送进协同作用单元计算出一个协同向量。所有协同向量拼接起来再和原始embedding或部分原始embedding一起送入多层全连接网络最后接sigmoid做二分类用交叉熵损失训练。这个结构和DeepFM最大的区别就是中间那层协同作用层在显式计算特征对的交互而且交互结果是向量不是标量。后续的MLP不需要再从零开始猜测交叉规律它只需要在已经完成协同调制的向量上做更高层的组合。2.2 触发特征和目标特征谁调制谁在CAN里一个特征对被拆成两个角色触发特征trigger和目标特征target。对于用户性别×商品类目这对用户性别作为触发特征商品类目作为目标特征。触发特征的embedding不直接参与协同向量计算而是先生成一组变换参数目标特征的embedding带着原始信息被这组参数调制得到协同向量。为什么要有角色之分而不是对称地做内积因为内积对称的代价是把交互压成一个标量。如果让一边显式地生成参数、另一边被参数调制交互的高维空间就被保留下来了。实际使用时角色分配通常按业务方向来定用户特征触发、商品特征被调制很常见反过来做也可以同一对特征双向各算一次再拼接也行但计算量翻倍。我从实践角度建议如果不是对某个特征对特别有把握先做单向就够了。双向当然信息更足但训练成本和推理成本都会明显上涨很多场景下用户到商品的方向已经完全够用。2.3 两个基本操作缩放weight和偏置bias协同作用单元内部核心是两个生成操作。设触发特征embedding为 e_i目标特征embedding为 e_j维度都是d。触发特征的embedding经过两个参数生成网络一般就是小MLP分别生成一个缩放向量 w 和一个偏置向量 bw G_w(e_i)维度d代表触发特征对目标特征各维度的加权系数b G_b(e_i)维度d代表触发特征为这个组合添加的偏移然后目标特征embedding被这样调制z_ij w ⊙ e_j b其中 ⊙ 是逐元素乘最终得到 z_ij 就是特征对 (i,j) 的协同向量。拆开看每个部分的含义w ⊙ e_j 相当于根据触发特征把目标特征embedding的每个语义维度重新加权——男性用户在商品类目的语义空间里把电竞相关的维度放大把美妆相关维度压低。b 则是加性修正捕捉那些不能被缩放表达的偏移。整个过程下来一个特征对得到一个d维向量每个维度都有明确的语义调制而不是一个干巴巴的相似度分数。这里要注意CAN的交互方式是vector-wise的整个embedding作为整体被调制不是把embedding拍平后做bit-wise交叉。这个区别在xDeepFM那篇论文里讨论过vector-wise操作能更好保留embedding内部的语义结构这也是CAN在实践里表现扎实的原因之一。2.4 参数规模怎么控低秩和共享一个需要正视的问题如果触发特征生成的是一个完整的变换矩阵 W ∈ R^{d×d}变量规模会非常吓人。假设d32一个矩阵就是1024个参数几十个特征对、每个特征又有几十上百个取值参数量会迅速失控。CAN的做法分两条路。第一把完整的矩阵换成对角矩阵也就是逐元素缩放参数从d×d降到d第二如果确实需要更丰富的变换用低秩分解把W拆成两个低秩矩阵的乘积比如 W ≈ U V^TU和V的维度各取4或8参数量就明显可控。参数生成网络本身可以跨特征对共享。也就是说所有特征对共用同一套G_w和G_b的MLP参数区分度来自输入e_i的差异。这样一来增加一个特征对新增的参数主要就是那对特征自身的embedding整体参数增长是可控的。我实际算过一笔账d32、生成网络两层MLP32→64→64一个参数生成网络的参数量大约几千整个模型如果embedding参数占大头协同层增加的参数量占比可能不到5%但带来的AUC提升往往在千分之几量级。这个性价比在工业界是很划算的。2.5 多个特征对怎么组合实际模型不可能只做一对特征的协同。一般会按业务经验选3到10个特征对比如用户性别 × 商品类目用户年龄段 × 商品价格带用户城市等级 × 商品品牌用户历史点击类目分布 × 商品类目上下文渠道 × 商品类目每个特征对各算各的协同向量然后把所有向量拼接成长向量喂给最终的MLP。有些实现还会把原始embedding一并拼进去相当于给高层一个直连通路防止调制过程把原始信息带偏——这个思路类似ResNet的残差结构实测对收敛速度有帮助。这里要提醒一点特征对不是越多越好。协同层是显式建模每一对都有计算成本而且冗余特征对会让模型学到重复信息离线指标并不一定继续涨。我在一个项目里从5对加到8对AUC基本没动训练时间涨了30%。后面我就学乖了先按业务直觉选少数几对跑通后做ablation逐对看贡献有价值的留下没贡献的删掉。3. 训练策略与工程落地从论文到真正能上线3.1 两阶段训练预训练与微调CAN论文里提到了一个很实际的问题协同参数如果只靠CTR主任务从头学低频特征对比如冷门用户群体×冷门商品类目会学得稀烂。所以它设计了两阶段训练思路。第一阶段是预训练目标是让协同作用单元在大规模语料上先把普遍规律学会。这个阶段可以理解为先把特征共现模式见个够把参数生成网络训练得足够稳定。第二阶段是微调在目标任务数据上继续训练让模型逐步适配具体场景和新鲜数据。工业落地时这个思路很实用。我见过不少团队直接用全量数据预训练近期数据微调的方式来做效果比单阶段好而且收敛快不少。一个关键细节是预训练阶段学习率可以稍大微调阶段要降下来通常降一个数量级否则容易把预训练学到的稳定结构冲坏。3.2 Embedding维度、初始化与训练参数说几个实操层面的关键参数。Embedding维度建议从32起步。CAN的target embedding会被逐元素调制维度太小时调制作用在过低的维度上信息不够用维度太大参数生成网络也跟着变大训练变慢。16维和64维我都试过32到48维是比较舒服的区间具体看特征规模。初始化是重灾区。参数生成网络G_w的输出如果随机初始化比如默认的均匀分布训练初期 w ⊙ e_j 等于把目标embedding随机打乱再叠加随机偏置模型开局就是一团噪声loss下降特别慢。一定要把G_w最后一层的权重初始化成输出接近1至少让w的均值落在1附近G_b的输出初始化成接近0。这样训练初期协同向量约等于原始目标embedding模型在一个合理起点上开始学习。这个坑我踩过改了初始化后收敛速度快了将近一倍。训练参数上没有太特殊的要求Adam优化器学习率1e-3起步配warmupbatch size尽量大4096以上损失函数用标准BCE。如果发现训练不稳定先看是不是初始化问题再看学习率最后查数据里有没有异常稀疏的特征。3.3 特征对选取的三个原则前面提了特征对数量这里展开说选取原则。第一业务优先。用户侧×商品侧是默认高价值区域因为CTR的本质就是谁在什么情况下看到什么商品点不点。上下文和商品的配对也值得做比如渠道×类目在信息流广告里能抓到很强的位置/场景效应。第二注意触发特征的稀疏度。触发特征是生成网络的输入如果取值分布太稀疏比如某个取值只出现几百次生成网络根本学不出稳定参数。处理办法是把低频取值做桶化或者用embedding校验/聚类合并相似取值实在不行就换一对特征。第三用模型辅助验证。我习惯先用GBDT之类的树模型跑一版看哪些特征组合分裂增益高再把这些组合拿去做CAN的特征对。树模型对特征交叉的发现能力很强拿它来筛候选特征对比拍脑袋靠谱得多。3.4 PyTorch简化实现参考下面给一个简化实现方便理解核心逻辑。真实业务里还会有特征Hash、embedding查找、多特征对管理这些工程细节。import torch import torch.nn as nn class CoActionLayer(nn.Module): 协同作用单元触发特征生成缩放和偏置调制目标特征。 简化版只演示核心计算。 def __init__(self, embed_dim32, hidden_dim64): super().__init__() self.embed_dim embed_dim # 两个参数生成网络输出缩放w、偏置b self.weight_net nn.Sequential( nn.Linear(embed_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, embed_dim) ) self.bias_net nn.Sequential( nn.Linear(embed_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, embed_dim) ) # 关键把权重网初始化为输出接近1偏置网输出接近0 for m in self.weight_net: if isinstance(m, nn.Linear): nn.init.normal_(m.weight, std0.01) nn.init.zeros_(m.bias) # 简化示意让最后一层输出接近1 nn.init.ones_(self.weight_net[-1].weight) nn.init.zeros_(self.weight_net[-1].bias) def forward(self, e_trigger, e_target): # e_trigger: [batch, d], e_target: [batch, d] w self.weight_net(e_trigger) b self.bias_net(e_trigger) return w * e_target b模型里对每个选定特征对调用一次协同层然后把所有协同向量concat起来# 伪代码对两个特征对做协同 z1 co_action(user_gender_emb, item_category_emb) z2 co_action(user_age_emb, item_price_emb) # 和原始embedding一起拼接 x torch.cat([z1, z2, user_gender_emb, item_category_emb], dim-1) out mlp(x)这里的初始化方式只是示意具体要根据激活函数和网络结构调整。核心原则就一条保证训练初期输出接近不调制的状态。4. 实操中的常见问题与排查实录4.1 离线涨点线上没效果这是CTR模型最常见的坑CAN也不例外。训练和线上特征产出不一致时协同层会放大这种差异因为输入上的任何小偏差经过参数生成后都可能被放大到整个协同向量上。排查顺序我一般是这样先检查特征一致性确认训练日志里用的特征和线上打分用的特征同源、同时段、同口径再查特征穿越看看有没有用到点击之后才产生的信息最后拉一个简单模型比如去掉协同层的同结构模型做在线对照确认增量到底来自协同层。还有一个容易忽视的点线上推理时某些特征值比如新上线的类目在训练集里低频协同参数学得不好。这种情况建议在特征工程层面做兜底把新值映射到unknown桶而不是让模型处理从未见过的embedding。4.2 训练初期不稳定、loss降不下去如果遇到这个问题先检查协同层的初始化。之前说过w输出接近1、b输出接近0很关键。如果训练刚开始一两千步loss基本不动大概率是初始化没做好。其次是学习率。生成网络虽然小但对学习率敏感建议配合warmup。我试过直接把学习率从1e-3调到1e-4训练稳定了但收敛变慢正确做法是1e-3起步、前1000到2000步线性warmup再正常衰减。最后梯度裁剪可以兜底。CTR这类稀疏特征模型偶尔出现超大梯度很正常不影响大局但加上clip能减少训练抖动。4.3 低频特征对的协同学不出来CAN的一个天然弱点是触发特征的值分布。如果某个特征取值在训练集里只有几千次曝光生成网络给它算出的变换参数基本就是噪声。我的处理办法有三个第一做频率桶化把出现次数少于阈值的取值映射到共享的低频桶embedding第二用预训练阶段多学习这类低频组合至少让它们在普遍规律层面是合理的第三实在不行就放弃这对特征用其他特征对替代。别恋战不是每对特征都适合做显式协同。4.4 计算量太大上线扛不住CAN的协同层参数量可控但计算量是实打实的每个特征对都要跑生成网络在线推理时特征对一多时延压力不小。有一个工程上很好用的优化触发特征的取值数量是有限的比如性别就几个值城市等级就几档可以提前把每个触发值对应的w和b算好缓存。在线推理时直接查表拿到w和b再对目标embedding做逐元素乘加整个协同层退化成两次向量运算生成网络的MLP直接省掉。实测可以把协同层单次推理耗时压到微秒级别。另一个方案是量化生成网络到低精度但一般用不到缓存大法已经够香。4.5 常见问题速查表现象可能原因处理建议离线AUC涨线上无变化特征穿越/训练线上特征不一致查特征一致性和数据穿越跑简单模型在线对照loss初期不降协同层随机初始化破坏target embeddingw初始化输出接近1b初始化为0训练波动大学习率过高/稀疏特征噪声加warmup降低学习率梯度裁剪低频特征协同无效触发特征取值太稀疏频率桶化、预训练兜底、删除该特征对推理时延高协同层生成网络在线计算预计算触发值的w、b并缓存查表参数量增长快特征对过多/生成矩阵过大低秩分解、共享生成网络、控制特征对数量5. 最后说点个人体会CAN这个模型我最初以为又是一个堆结构刷AUC的paper真正在自己数据集上跑过之后我改变了一点看法。它最值得学习的不是某个具体结构而是把特征交互显式参数化这个建模思路——在很多场景下显式的、可解释的协同建模确实比让MLP在暗处瞎猜要可靠。如果想把CAN用到自己的模型里我的建议很朴素先把特征对选到最少用最简单的基础版本跑通把初始化、预训练、缓存这些工程细节做扎实再逐步加对。不要一上来就堆十个八个特征对那样你根本说不清涨点来自哪里。我自己还有个习惯会把每个协同向量单独拉出来看统计分布如果某个特征对的协同向量方差明显异常通常说明那对特征的数据质量或者取值分布有问题比盲目调结构有效得多。特征协同这件事模型结构只是放大器真正的基础还是对业务的理解。谁和谁在一起会产生特殊的化学反应这个问题业务同学最清楚算法工程师要做的是找到一类能够把这种化学反应用显式结构表达出来的模型。CAN给了我们一个很好的角度这个方向后面还可以继续往多阶协同、序列场景延伸值得持续跟踪。
阅读完成 · 觉得有帮助?
咨询建站