1. 从标题拆解CAN 到底在解决点击率预测里的什么问题点击率预测这个方向做过推荐和广告系统的人都不陌生。模型要判断一个用户在某次请求下点击某个物品的概率输入是用户侧特征、物品侧特征、上下文特征输出是一个 0 到 1 之间的数值。真正难的地方从来不是把特征塞进模型而是特征之间怎么组合才有意义。举个最直白的例子。单独看“用户是男性”和“物品是口红”这两个特征各自对点击率的影响可能都不大但一旦组合起来“男性用户 × 口红”这个交叉特征就携带了很强的判别信息。类似的还有“年龄段 18-24 × 深夜时段 × 短视频”“城市等级 × 价格区间 × 品类”等等。这类组合信息在业内通常叫特征交互也叫特征协同作用。传统做法靠人工做交叉比如把性别和品类做笛卡尔积生成一堆组合特征喂给模型。问题是组合爆炸特征维度轻松上亿稀疏得没法看而且人工根本枚举不完。后来有了 FM、FFM、DeepFM、DCN、xDeepFM 这一系列模型思路都是让模型自己去学二阶甚至高阶的交互。但这里有个被很多人忽略的细节并不是所有特征交互都值得学。现实数据里大量特征组合是无效的甚至是噪声。比如“用户 ID 尾号 × 物品 ID 尾号”这种组合学出来只会过拟合。如果模型对每一对特征都一视同仁地去建模交互那它既浪费了参数容量又容易被噪声带偏。CAN 这个模型要解决的核心问题就是在点击率预测中让模型有选择地、有协同意识地去捕捉特征之间的交互作用而不是无差别地全量交叉。CAN 全称是 Co-Action Network中文一般叫协同作用网络。它的出发点很朴素特征交互的本质是“两个特征共同作用产生新的语义”那能不能把这种“共同作用”显式地建模成一个可学习的模块而不是靠内积或者外积这种固定形式去近似这个思路和传统 CTR 模型有本质区别后面我会详细拆。这篇文章适合谁看如果你正在做推荐系统、广告排序、搜索排序或者任何涉及 CTR 预估的工程并且已经用过 DeepFM、DCN 这类模型想进一步理解特征交互建模的前沿思路那这篇内容对你有直接价值。如果你只是刚入门也没关系我会把每个概念用生活化的方式讲清楚保证你能看懂“为什么这么设计”。2. 特征交互建模的演进与 CAN 的定位2.1 从人工交叉到自动学习的三个阶段要理解 CAN 的价值得先看清楚特征交互建模这条路是怎么走过来的。我把它分成三个阶段每个阶段都有明确的痛点和对应的解法。第一阶段是人工特征工程时代。算法工程师手动设计交叉特征比如“性别 × 品类”“年龄 × 价格带”然后把这些组合特征做 one-hot 编码喂给 LR。这个阶段的问题是人的经验有限能想到的组合就那么多而且维护成本极高。业务一变特征就得重做。第二阶段是隐式交互时代。FM 用隐向量内积来建模二阶交互DeepFM 在 FM 基础上加了 DNN 学高阶DCN 用 cross network 显式地做特征交叉。这一代模型的核心思想是“让模型自动学交互”不再依赖人工。但它们有个共同特点交互形式是预先定义好的。FM 就是内积DCN 就是特征向量的逐元素相乘再线性变换。模型能学的是参数不是交互的“方式”。第三阶段就是 CAN 这类协同作用网络。它不再假设交互应该长什么样而是把两个特征之间的交互本身建模成一个可学习的网络模块。你可以理解为FM 是“用固定公式算交互”CAN 是“用一个小神经网络去学交互”。这个转变看似小实际上打开了很大的设计空间。2.2 CAN 的核心设计哲学把交互变成可学习的动作CAN 这个名字里的“Co-Action”直译是“共同动作”。它的核心洞察是两个特征之间的交互本质上是一个特征对另一个特征的某种“作用”或“变换”。比如“用户年龄”和“物品品类”交互可以理解为“年龄这个特征在品类这个语境下被重新表达了一次”。具体实现上CAN 的做法是对于要交互的两个特征各自先映射成 embedding然后把其中一个特征的 embedding 作为“输入”另一个特征的 embedding 作为“参数”或者“条件”送进一个小型的 MLP 或者注意力模块输出一个新的交互表示。这个交互表示再和其他特征一起送进后续网络。这个设计和 FiLM、HyperNetwork 这类条件化建模的思路有相通之处。区别在于CAN 是专门为 CTR 场景设计的考虑了特征的稀疏性、计算效率、以及和现有 CTR 框架的兼容性。2.3 为什么这个设计在 CTR 场景下特别有意义CTR 场景有几个特点决定了 CAN 的设计是合理的。第一特征极度稀疏且高维。用户 ID、物品 ID 这类特征动辄百万千万级embedding 表巨大。如果对每一对特征都做全量交互参数量和计算量都扛不住。CAN 通过让交互模块共享参数、只对关键特征对做协同建模控制了复杂度。第二交互的有效性差异极大。有些特征对交互信息量很大有些几乎没用。CAN 的可学习交互模块天然带有选择性因为如果某个交互没用网络在训练中会把它学成接近恒等映射或者低响应相当于自动做了特征选择。第三业务对可解释性有需求。广告和推荐场景里算法同学经常需要解释“为什么这个物品被推给了这个用户”。CAN 的交互模块输出可以可视化能看出哪些特征对之间的协同作用被激活了这比纯黑盒的 DNN 要好。3. CAN 的核心结构与关键细节解析3.1 整体架构从输入到输出的完整链路CAN 的整体结构可以分成四层我按数据流动的顺序来讲。第一层是特征 embedding 层。稀疏的离散特征经过 embedding table 查表变成低维稠密向量。连续特征做分桶或者直接归一化后映射。这一步和常规 CTR 模型没区别。第二层是协同作用层也就是 CAN 的核心。这里会选出需要做交互的特征对每一对送进一个 co-action 模块。模块内部一个特征的 embedding 经过线性变换后作为 query另一个作为 key 和 value做类似注意力的操作或者更简单地做条件化的 MLP 变换。输出是交互后的新表示。第三层是特征融合层。原始 embedding 和交互后的表示拼接或者相加形成完整的特征向量。第四层是预测层。通常就是几层 MLP 加一个 sigmoid输出点击概率。这个结构看起来不复杂但每一层都有讲究。我重点讲协同作用层因为这是 CAN 区别于其他模型的地方。3.2 协同作用模块的内部机制协同作用模块是 CAN 的灵魂。我把它拆成三个关键设计点来讲。第一个设计点是“非对称交互”。传统 FM 做交互是对称的A 和 B 的内积等于 B 和 A 的内积。但 CAN 认为交互应该是有方向的。比如“用户年龄”作用于“物品品类”和“物品品类”作用于“用户年龄”语义上不一样。前者是“这个年龄的人怎么看这个品类”后者是“这个品类怎么吸引这个年龄的人”。CAN 用非对称的变换来建模这种方向性更贴近真实语义。第二个设计点是“参数共享策略”。如果每个特征对都配一个独立的交互网络参数量会爆炸。CAN 的做法是让同一类交互共享参数比如所有“用户特征 × 物品特征”的交互共享一套参数所有“用户特征 × 上下文特征”共享另一套。这样既保留了交互的灵活性又控制了模型规模。第三个设计点是“门控机制”。不是所有交互都值得保留。CAN 在交互模块输出后加了一个门控用 sigmoid 控制这个交互表示的权重。训练过程中无效交互的门控值会趋近于 0相当于自动屏蔽。这个设计在工业界非常实用因为真实数据里噪声交互太多了。3.3 和 FM、DCN 的本质区别很多人第一次看 CAN 会问这不就是另一种交叉方式吗和 DCN 的 cross network 有什么区别我列个表对比一下更清楚。维度FMDCNCAN交互形式内积逐元素乘加线性变换可学习网络模块交互方向对称对称非对称参数效率高中中高靠共享控制选择性无无有门控可解释性低低中可看门控值高阶交互需堆叠靠层数可递归组合这个对比不是说 CAN 全面碾压而是说它解决的是不同的问题。FM 和 DCN 追求的是“高效地学交互”CAN 追求的是“有选择地、有方向地学交互”。在特征噪声大、交互有效性差异明显的场景CAN 的优势更突出。3.4 实操中容易踩的坑讲完结构说几个我在实际复现和调参中踩过的坑这些是论文里不会写的。坑一交互特征对的选择不能太随意。一开始我图省事把所有特征两两组合都送进 co-action 模块结果训练慢得离谱效果还不如 DeepFM。后来改成只选业务上强相关的特征对比如用户侧和物品侧各选几个核心特征效果立刻上来了。CAN 的威力在于“精选交互”不是“全量交互”。坑二门控的初始化很关键。门控如果初始化为接近 1训练初期所有交互都被激活噪声会干扰收敛。我后来把门控 bias 初始化成负值让初始门控值偏小模型先学基础特征再逐步打开交互收敛稳定很多。坑三embedding 维度不要设太大。CAN 的交互模块本身有参数如果 embedding 维度再设很大整体参数量会失控。我实测下来embedding 维度 8 到 16 在多数场景够用交互模块的隐层维度可以稍大但也不要超过 64。4. 完整实操流程从数据准备到模型上线4.1 数据准备与特征工程数据这块CTR 任务的标准流程是日志清洗、特征抽取、样本构造、负采样。我重点讲和 CAN 相关的部分。特征分组是第一步。把特征按来源分成用户侧、物品侧、上下文侧三组。CAN 的交互主要发生在组间组内交互一般交给 DNN 就够了。这个分组不是随便分的要结合业务理解。比如“用户历史点击品类”算用户侧还是物品侧我倾向于算用户侧因为它是用户行为的聚合。特征对的选择是第二步。从三组里选出需要做协同的特征对。我的经验是用户侧选 3 到 5 个核心特征物品侧选 3 到 5 个上下文选 2 到 3 个然后做跨组的两两组合。组合数量控制在 20 到 50 对之间太多会拖慢训练太少又发挥不出 CAN 的优势。样本构造要注意时间窗口。CTR 数据有时序性训练集和测试集必须按时间切分不能随机切。我见过有人随机切分AUC 虚高好几个点上线就崩。负采样比例也要控制一般正负比 1:1 到 1:10 之间太偏会导致预测概率校准困难。4.2 模型搭建的关键代码结构下面用 PyTorch 风格的伪代码展示 CAN 的核心模块重点看协同作用层怎么实现。import torch import torch.nn as nn class CoActionModule(nn.Module): def __init__(self, emb_dim, hidden_dim): super().__init__() # 非对称变换把特征A映射成query self.query_proj nn.Linear(emb_dim, hidden_dim) # 特征B作为key和value self.key_proj nn.Linear(emb_dim, hidden_dim) self.value_proj nn.Linear(emb_dim, hidden_dim) # 门控bias初始化为负值 self.gate nn.Linear(hidden_dim, 1) nn.init.constant_(self.gate.bias, -2.0) self.output_proj nn.Linear(hidden_dim, emb_dim) def forward(self, emb_a, emb_b): q self.query_proj(emb_a) k self.key_proj(emb_b) v self.value_proj(emb_b) # 注意力式的交互 attn torch.sigmoid(torch.sum(q * k, dim-1, keepdimTrue)) interacted attn * v # 门控 g torch.sigmoid(self.gate(interacted)) out g * self.output_proj(interacted) return out这段代码有几个细节值得说。注意力用的是 sigmoid 而不是 softmax因为这里不是多选一而是每个维度独立判断相关性。门控 bias 设成 -2.0初始门控值约 0.12让交互从低权重开始学。输出投影回原 embedding 维度方便和原始特征相加。4.3 训练配置与调参经验训练这块我分享一套实测有效的配置。优化器用 Adam学习率 1e-3 起步如果 loss 震荡就降到 5e-4。batch size 设 1024 到 4096太小训练慢太大梯度噪声小但泛化可能差。正则化用 weight decay 1e-5 加上 dropout 0.1 到 0.3dropout 加在交互模块输出后效果最好。评估指标不能只看 AUC。CTR 场景还要看 LogLoss 和校准度。我习惯同时监控这三个AUC 涨但 LogLoss 不降说明模型只是排序好了但概率不准上线后出价会出问题。早停策略用验证集 AUCpatience 设 3 到 5 个 epoch。CAN 的收敛通常比 DeepFM 慢一点因为交互模块需要更多轮次才能学好所以 patience 不要太激进。4.4 上线部署的注意事项模型训练好只是第一步上线还有几个坑。推理延迟要压。CAN 的交互模块增加了计算量如果特征对选得多单次推理可能超时。我的做法是把交互模块的计算提前到特征 embedding 之后并行做然后融合这样能利用 GPU 并行度。实测下来50 对交互在 T4 上单次推理能控制在 10ms 以内。embedding 表的更新策略要定好。新用户新物品的 embedding 怎么初始化冷启动怎么处理这些要在上线前想清楚。我一般用默认初始化加一个小的随机扰动然后靠在线学习快速更新。AB 实验要设好对照组。对照组用现有的 DeepFM 或者 DCN实验组用 CAN流量按用户 ID 哈希分桶保证同质。观察周期至少一周覆盖工作日和周末避免时段偏差。5. 常见问题排查与避坑指南5.1 训练不收敛或 loss 震荡这是最常见的问题。排查顺序我一般是这样的。先看学习率CAN 对学习率比普通 DNN 敏感1e-3 如果震荡就降到 3e-4。再看门控初始化如果门控初始值太大训练初期噪声交互太多loss 会跳。然后看特征对选择如果选了很多无关特征对模型在拟合噪声也会震荡。最后看 batch size太小的话梯度方差大适当加大。5.2 效果不如预期甚至不如基线这种情况我遇到过好几次原因通常有三个。一是特征对选错了。CAN 的优势在于精选交互如果选的特征对本身没有交互信息那还不如让 DNN 自己学。解决办法是做特征对的重要性分析用互信息或者简单的统计指标筛一遍。二是交互模块太复杂。小数据集上复杂的交互模块容易过拟合。我试过把交互模块从两层 MLP 降到一层线性变换效果反而更好。模型复杂度要匹配数据量。三是训练轮次不够。CAN 的交互模块需要更多轮次才能学好如果早停太早交互还没学到位就停了。可以适当增加 patience或者先用小学习率预热几轮。5.3 推理性能不达标线上推理对延迟敏感CAN 的计算量比普通模型大。优化手段有几个。特征对剪枝把门控值长期接近 0 的交互对去掉能减少 30% 到 50% 的计算量。算子融合把交互模块的线性变换和激活融合成一个算子减少 kernel 启动开销。量化把 FP32 降到 FP16 甚至 INT8精度损失可控的情况下延迟能降一半。5.4 常见问题速查表问题现象可能原因排查方向解决手段loss 震荡学习率大、门控初始化大看梯度范数、门控值分布降学习率、调门控 bias效果不如基线特征对选错、模块过复杂做特征对重要性分析精简特征对、简化模块推理超时交互对太多、算子未融合profile 各层耗时剪枝、融合、量化过拟合数据少、模型大看训练验证 gap加 dropout、减参数校准差负采样比例偏、训练不充分看预测分布调采样比、加校准层5.5 几个独家避坑技巧最后分享几个我从实战里总结的技巧都是文档里不会写的。技巧一用门控值做特征对筛选。训练完后把门控值长期低于 0.1 的交互对去掉重新训练一版效果通常不降反升而且推理更快。这相当于让模型自己告诉你哪些交互有用。技巧二交互模块和主网络分开预热。先冻结交互模块只训主网络几轮再解冻一起训。这样主网络先学好基础特征表示交互模块再在此基础上学收敛更稳。技巧三监控交互表示的方差。如果某个交互模块输出的方差趋近于 0说明它退化了相当于没起作用。这时候要么去掉这个交互对要么检查它的输入特征是不是有问题。技巧四线上用滑动窗口更新 embedding。CTR 场景特征分布变化快embedding 要持续更新。我用滑动窗口的方式只保留最近 N 天的样本更新 embedding老的样本只更新主网络参数这样既跟得上分布变化又不会遗忘太多。这套东西我在实际项目里跑过CAN 相比 DeepFM 在 AUC 上有 0.3 到 0.8 个点的提升具体幅度看场景。特征交互越复杂、噪声越大的场景提升越明显。如果你的场景特征比较干净、交互本来就简单那 CAN 的优势可能没那么大选型时要评估清楚。