news 2026/10/26 6:03:38

CAN模型详解:向量级协同如何提升广告点击率预估

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CAN模型详解:向量级协同如何提升广告点击率预估

做广告点击率预估这些年,我遇到最多的一个争论就是:用户历史行为序列到底该怎么用。直接平均池化吧,信息丢得太狠;上个注意力机制吧,DIN 那套用下来又总觉得差口气。直到我把 CAN(Feature Co-Action for Click-Through Rate Prediction)这篇论文认真啃完,又在广告和电商数据上亲手复现了几轮,才真正想明白“特征协同作用”到底指什么,以及为什么把协同结果从“一个权重”升级成“一组向量门控”,能在点击率预估任务上带来实打实的提升。这篇文章就把 CAN 的动机、核心结构、PyTorch 实现思路和我实际踩过的坑一次讲透。想在自己的排序模型里引入行为序列协同建模的同学,可以直接把它当成一份参考。

1. 先把问题说清楚:CTR预估里的“协同作用”到底指什么

1.1 一个具体的排序场景

假设用户最近一个月点过的商品是:蓝牙耳机、机械键盘、鼠标垫、显示器支架。现在系统要给他推一个候选商品:87 键机械键盘。任何一个有经验的排序模型都知道,这个候选和用户历史里的“机械键盘”高度相关,和“蓝牙耳机”弱相关。但相关性不是凭空出现的,它藏在候选商品的 embedding 和每个历史商品 embedding 的交互里。更准确地说,模型必须同时看到“候选是什么”和“用户历史里有什么”,再把两者放进同一个向量空间里去算。

如果只把历史商品平均池化成一个向量,候选来了以后这个向量不会变,等于用户兴趣在推不同候选时始终是同一副面孔,这显然不合理。所以业界主流做法是用候选商品去“查询”历史序列,也就是 DIN 那套注意力池化的基本思路:候选向量作为 query,历史序列作为 key 和 value,算一堆权重再加权求和。这套思路有效,但它只是故事的开头。

1.2 从特征交叉到协同作用的升级

CTR 里常说特征交叉,主要指静态字段之间的交互,比如“年龄×类目”“城市×价格档”,FM、DCN、FiBiNET 都是处理这类问题。但用户行为序列和候选商品之间的交互,本质上也是一种交叉,只不过它是一对多的结构化交叉:一个候选商品对整整一串历史行为。FM 那套二阶内积处理不了这种带序列结构的交叉,DCN 的 cross layer 也不是为变长序列设计的。

CAN 这篇论文把这种交互换了个名字,叫 co-action,也就是特征协同作用。意思很直白:候选商品和历史商品要在特征层面发生“化学反应”,互相影响对方的值,而不只是算出一个权重就算完。名字换掉以后,整个建模思路也跟着变了。传统特征交叉关注的是“哪些特征组合起来有预测力”,协同作用关注的是“当候选对象出现时,用户的历史行为应该如何被重新解读”。后者是动态的、依赖输入的,前者是静态的、预先固定的。

1.3 DIN 的注意力到底丢掉了什么

DIN 的公式写出来很简洁:对每个历史行为 item b,把它的 embedding e_b 和候选 embedding e_t 拼起来过一个小 MLP,得到一个标量分数 a,归一化后做加权求和。问题在于,a 只告诉模型“这条历史行为值不值得关注”,却没法告诉模型“如果值得关注,应该放大历史向量的哪些维度”。

举个例子:历史里有“机械键盘”和“鼠标垫”,候选是“机械键盘”。DIN 会给两个历史 item 都打上较高的注意力权重,然后各自乘以权重再相加。但“机械键盘”这个商品 embedding 里有几十个维度,其中可能只有一部分维度代表“青轴红轴、87 键、RGB 灯”,另一部分代表“品牌、价格、颜值”。标量权重没法选择性地放大前者、弱化后者,它只能整段向量一起加权。

CAN 的切入点就在这里:协同的结果不应该是一个标量,而应该是一个和被作用向量同等维度的向量,用它逐维度去控制历史行为表征。这看起来只是输出维度的变化,但它让整个模型从“选 item”进化到了“改表征”,后续所有结构设计都围绕这个核心理念展开。

2. 拆解 CAN 的核心模块:协同单元在算什么

2.1 四路输入:不要把信息压得太狠

协同单元的输入,我习惯拼四路:e_t、e_b、e_t 与 e_b 的逐元素乘积、e_t 减 e_b。前两路保留原始信息,让后续 MLP 不至于丢掉基础信号;逐元素乘积这一路最重要,它天然地在做“两个向量在同一维度上是否同时激活”的检测——两个都在,乘积就大;一个在另一个不在,乘积就小,这本身就是一种维度级的协同信号。减法那路捕获差异,比如候选是电竞键盘、历史是办公键盘,减法向量能把两者在“定位”维度上的偏移暴露出来。

拼接之后的向量维度是 embedding 维度的四倍,所以协同单元里第一层 Linear 的参数量明显比 DIN 同层大,这是 CAN 训练成本略高的主要原因。工程上如果 embedding 维度设得过大,可以先降到 32 或 64 再进协同单元。我见过有人直接把 256 维的 embedding 拼四路,第一层 Linear 直接多了上千万参数,小数据集上过拟合得一塌糊涂。

2.2 向量门控与序列池化的两层加权

协同向量 c 我一般用带 sigmoid 的小 MLP 输出,把每个维度约束到 (0,1) 区间。这样它本质上是一组逐维度开关:某个维度上候选和历史相关性强,对应的门就开大一点;无关维度就压到接近 0。然后拿 c 去点乘历史行为向量 e_b,得到被协同改造过的表征,这一步叫做维度级协同。

但协同向量只解决了“每个历史 item 内部哪些维度该被激活”的问题,还没解决“这一串历史 item 之间谁该占更大权重”的问题。所以还要再加一层标量注意力打分,同样基于四路拼接输入,softmax 之后做加权求和。整个协同单元就是两层加权:第一层在向量维度上做门控,作用是改表征;第二层在序列维度上做注意力,作用是选 item。这两个东西各管各的,千万别混在一起。

个人经验:门控的 sigmoid 输出初始时接近 0.5,等于每个维度先各开一半,训练前期梯度会比较平。可以把 gate 部分最后一个 Linear 初始化成小方差,让初始门控偏向 0.1 到 0.3,模型收敛会明显更快。

2.3 多头协同单元 MHCU

CAN 里更完整的设计是 Multi-head Co-action Unit。思路和 Transformer 的多头差不多:不要在一个完整向量上只学一种协同模式,而是把 embedding 切成若干段,每段各自学一个协同门控,最后拼起来。这样不同 head 可以自动分化,比如一个 head 负责类目匹配,另一个 head 负责价格带匹配,还有一个 head 可能专门捕捉“互补型”关系。

我自己的实验里多头数并不是越大越好。head=2 或 head=4 通常稳定有增益,加到 8 头之后参数量和训练时间都上去了,AUC 反而有回落。原因也好理解:embedding 总共 64 维,切 8 头每头只剩 8 维,协同门控能利用的信息太少,反而学不干净。多头协同单元在训练时还有一个隐性好处:不同 head 的梯度天然不同,相当于给模型增加了一点正则,不容易过拟合。

2.4 位置编码和多行为类型

行为序列天然有序,用户昨天点的商品和三个月前点的商品,对当前候选的意义完全不一样。CAN 在把行为 embedding 送进协同单元之前,会给序列加上可学习的位置编码。这里有个细节:位置编码最好加在行为 item embedding 上,而不是加在池化后的结果上,否则位置信息会被后面的 MLP 抹掉。我踩过这个坑,一开始把位置编码加在协同单元输出上,结果 AUC 基本没变化,改回加在输入侧才看到稳定提升。

另外,用户行为不止点击一种。加购、下单、收藏、搜索这些行为意图强度差异很大,混在一个序列里会让协同单元无所适从。稳妥的做法是每种行为单独建一个序列、单独过一个协同单元,最后把多个兴趣向量拼起来。论文管这个叫多关系协同,工程上其实就是复制几个子网络,参数不共享,成本可控,效果通常立竿见影。比如我在一个电商场景里加了“点击”和“下单”两条行为序列后,GAUC 比单序列明显高一截。

3. 从论文到代码:一份可以直接改的 CAN 实现

3.1 数据准备:序列怎么切、mask 怎么做

CAN 对数据格式的要求和 DIN 基本一样,就是把用户行为按时间排序后截成定长序列。长度我默认定 50,超过 50 的截掉最早的,不足 50 的用 0 补齐,同时生成一个 bool mask 标记哪些位置是有效行为。序列物品和候选物品必须共用同一个 embedding 表,因为协同单元要靠同一个向量空间里的点乘、减法去计算关系,各建一套 embedding 就完全失去意义了。

还有一个很容易犯的错误:用点击日志做训练时,当前样本的候选商品如果恰好出现在历史序列里,会造成信息泄漏。工业上一般把“当前曝光时刻之前的行为”作为历史,并且把当次曝光对应的 item 从序列里挖掉。做离线实验时也要做同样处理,不然 AUC 虚高得离谱,上线以后立刻被打回原形。数据侧另外要注意的是时间排序:很多日志表本身不保证有序,离线任务里一定要显式按时间排序,否则序列语义直接乱了。

3.2 协同单元的 PyTorch 实现

下面是单头版本,逻辑最清楚:

class CoActionUnit(nn.Module): def __init__(self, emb_dim=64, hidden_dim=128): super().__init__() inp_dim = emb_dim * 4 # t, b, t*b, t-b self.co_net = nn.Sequential( nn.Linear(inp_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, emb_dim), nn.Sigmoid(), # 逐维度门控 ) self.att_net = nn.Sequential( nn.Linear(inp_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, target_emb, behav_emb, behav_mask): B, L, D = behav_emb.shape t = target_emb.unsqueeze(1).expand(B, L, D) x = torch.cat([t, behav_emb, t * behav_emb, t - behav_emb], dim=-1) gate = self.co_net(x) # [B, L, D] gated = gate * behav_emb # 维度级协同 score = self.att_net(x).squeeze(-1) # [B, L] score = score.masked_fill(~behav_mask, -1e9) attn = torch.softmax(score, dim=-1) out = (attn.unsqueeze(-1) * gated).sum(dim=1) return out # [B, D]

多头版本在单头基础上把 embedding 切片,每个 head 各跑一个小协同网络,再把门控结果拼回去:

class MultiHeadCoActionUnit(nn.Module): def __init__(self, emb_dim=64, hidden_dim=128, num_heads=2): super().__init__() self.num_heads = num_heads self.head_dim = emb_dim // num_heads inp_dim = emb_dim * 4 self.co_nets = nn.ModuleList([ nn.Sequential( nn.Linear(inp_dim, hidden_dim // num_heads), nn.ReLU(), nn.Linear(hidden_dim // num_heads, self.head_dim), nn.Sigmoid(), ) for _ in range(num_heads) ]) self.att_net = nn.Sequential( nn.Linear(inp_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, target_emb, behav_emb, behav_mask): B, L, D = behav_emb.shape t = target_emb.unsqueeze(1).expand(B, L, D) x = torch.cat([t, behav_emb, t * behav_emb, t - behav_emb], dim=-1) behav_parts = behav_emb.reshape(B, L, self.num_heads, self.head_dim) gates = torch.stack([net(x) for net in self.co_nets], dim=2) # gates: [B, L, H, head_dim] gated_parts = behav_parts * gates gated = gated_parts.reshape(B, L, D) score = self.att_net(x).squeeze(-1) score = score.masked_fill(~behav_mask, -1e9) attn = torch.softmax(score, dim=-1) out = (attn.unsqueeze(-1) * gated).sum(dim=1) return out

这个实现里所有 head 的输入都是同一个四路拼接,靠输出维度来强制分工。有人会问为什么不让每个 head 只吃自己的那一段输入,理由是四路拼接本身包含的交叉信号是全量的,每个 head 只输出自己负责的那段门控,既能拿到全局信息,又保持了子空间的独立性,实际效果比完全隔离输入更稳。

3.3 整体模型与训练配置

整体结构很简单:类别特征进 embedding,数值特征直接保留,用户行为序列过 CAN 协同单元,所有输出拼起来进一个两到三层的 MLP,最后接 sigmoid。损失用 BCEWithLogits,优化器 Adam,learning rate 1e-3 起,batch size 可以给到 512 甚至更大,序列长度 50 时单卡能扛住。前期我习惯跑 10 个 epoch 用验证集早停,后面再换余弦退火精调。

embedding 维度我默认 64,hidden_dim 给 128,位置编码可学习、维度跟 embedding 一致,多个行为序列的协同单元参数不共享。还有一个值得加的小改动:给协同单元的输出加一个残差连接,out = out + target_emb,让协同结果和候选向量保持在同一语义空间,喂给后面的 MLP 时梯度更稳。我在几个数据集上都验证过,这个残差基本是白捡的收益,代价只是多一次加法。

4. 效果观察与优化记录:复现结果和踩过的坑

4.1 相对 DIN/DIEN 到底能涨多少

先说结论:在行为序列足够长的数据上,CAN 相对 DIN 的 AUC 提升大概在千分之一到千分之几这个量级,GAUC 的幅度会更明显一些。如果数据平均行为长度不到 5,这个差距基本看不出来;序列越长,向量级协同的优势越大,因为池化过程要融合的信息多了,靠一个标量权重去概括交互就越吃力。

我体感上还有一个规律:embedding 维度太小时,比如 8、16,CAN 的向量门控等于把信息压得太粗,跟 DIN 拉不开差距;维度大到 256 以上时,协同单元第一层参数量膨胀,训练变慢还容易过拟合。64 到 128 之间是性价比最高的区间。这些规律跟模型容量、数据量都有关系,建议选型时拿自己的数据小规模对比一下,别直接照搬超参。

4.2 线上推理:把协同单元压成一次批量矩阵运算

协同单元在线上的主要矛盾在于序列变长、并且目标向量要对整个序列做广播展开。如果不优化,服务端每个请求都要显式构造一个 L×4D 的大矩阵,内存和算力都浪费。工程上我会把整个单元改写成批量矩阵乘:既然目标向量是同一个,先把目标向量相关的交叉项在进入单元前算好,序列侧只保留需要拼接的部分,再一次性过 batch matmul。序列填充到固定长度后,还可以把 padding mask 合并进 softmax,跟离线完全对齐。

模型导出 ONNX 时有个细节:torch.cat、expand 这类动态维度操作在 ONNX 里容易出警告,最好把序列长度固定成一个常量,或者用 view 和 repeat 代替 expand。FP16 对这类 MLP 门控几乎无精度损失,我线上一直用 FP16,AUC 和全精度基本一致,但延迟能降三成左右。还有一点:如果服务端有多个模型共用同一个行为序列特征,尽量把序列 embedding 的计算结果缓存起来复用,CAN 最贵的就是序列展开那一步,缓存能省掉很大一部分重复计算。

4.3 我踩过的一些具体的坑

问题现象解决办法
显存溢出序列 50、batch 512、多头全开时 OOM降低 batch 验证;序列截到 30 对比效果;用梯度累积
门控初始值过大训练前几个 epoch AUC 涨得很慢gate 最后一层 Linear 用小方差初始化,初始门控偏小
多头过多AUC 反而下降head 控制在 2 到 4,别学 Transformer 堆 8 头
位置编码加错位置加了位置编码反而掉点加在行为 item embedding 上,别加在池化结果上
行为序列泄漏验证集 AUC 虚高 1% 以上训练和测试统一挖掉当次曝光 item,按时间截断

另外,多行为联合训练时不同行为的序列长度分布差异很大:点击可能有几千条历史,下单可能只有几条。如果两条序列用同一个 max_len 截断,下单序列几乎全是 padding,协同单元学不到东西。我后来是每种行为单独定 max_len,下单给 20,点击给 100,效果立竿见影。这个细节很容易被忽略,但对多行为 CAN 的影响比想象中的大。

5. 选型判断:什么时候该用 CAN,怎么跟现有模型共存

5.1 CAN、DIN、DIEN、DSIN 各自该站哪

模型核心思路适合场景主要局限
DIN候选与历史 item 的注意力权重行为序列中等、候选强个性化交互被压成标量,不改造表征
DIENGRU 建模行为时序演化序列长、时序效应明显训练重,建模的是“演化”而非“协同”
DSIN按会话切段建模跨会话兴趣漂移明显结构复杂,线上工程量大
CAN候选与历史的向量级协同行为丰富、候选频繁变化计算量略高,序列太短时收益有限

表格只是大致坐标。实际工业里很少互相替换,更多是叠加:CAN 处理“候选 vs 行为”的协同,DIEN 或 GRU 分支继续保留时序演化建模,DIN 的注意力打分也可以并到 CAN 的标量打分里。我在粗排用单头 CAN,精排用多头加多行为,两层在线上能明显看到漏斗转化改善。如果你现在的模型已经用了 DIN,把它升级到 CAN 的改动量其实很小,协同单元完全可以作为 DIN 的一个分支并行接入,再对比是否保留原分支。

5.2 CAN 和特征交叉底座并不冲突

前面说过,CAN 管的是“候选商品对行为序列”这种结构化协同,它替不了 FM、DCN、FiBiNET 这些处理静态字段交叉的底座。用户年龄、性别、城市、商品类目、价格档这些字段间的二阶、高阶交互,该做还是要做。所以一套比较顺手的配置是:底座用 DCN 或 FiBiNET,中间接 CAN 模块,最后一起进 MLP。这样静态交叉和动态协同各司其职,不会互相抢参数。

有些同学会担心模块太多导致模型太重。我的做法是把静态底座做得尽量轻,只用 DCN 的 cross 层,不加太深的 MLP,把主要容量留给 CAN。因为行为序列里的协同信号通常比静态字段交叉信号更个性化、更有区分度,把容量分配给协同模块的性价比更高。这当然不是绝对规则,数据不同结论可能不同,但可以作为起步配置。

5.3 成本账和适用条件

我粗算过一笔账:CAN 训练时间大约是同结构 DIN 的 1.2 到 1.5 倍,主要贵在四路拼接后第一层 Linear 的参数量和序列维度的展开;推理延迟高约 10% 到 20%,换来的 AUC 提升如果达不到千分之一以上,在流量很大的场景可能就不划算。所以我的判断标准很简单:行为序列平均长度大于 20、候选物品变化频繁(广告、电商、短视频都符合)、训练资源充裕,这三个条件满足再上;新用户占比特别高的场景,可以给一个退路——行为序列为空时,让协同单元退化成普通 MLP,用兜底向量顶上。

另外,CAN 对数据增益的敏感性还和负样本构造有关。广告场景里负样本往往是“曝光未点击”,如果曝光分布本身有偏,协同单元很容易把“没点过相似商品”误解成“不喜欢”。我习惯在样本侧加一点随机负采样,让协同单元见过更多“用户没见过但可能感兴趣”的候选,这样学出来的门控更接近真实兴趣分布。

5.4 协同思想后续还能往哪延展

CAN 的底层思想可以迁移到不少方向。多任务场景里,CTR 和 CVR 两个目标对同一批行为序列的协同需求不一样,可以共享协同单元的输入、让不同任务各学一套门控;跨场景迁移时,协同门控可以作为场景相关参数去适配;再往后看,行为不只有 item id,还有文本、图片 embedding,向量级协同的思路同样可以套到多模态特征上。我自己接下来的实验就在试共享底座加多任务门控的方向,等跑出稳定结果再单独写一篇。

最后说点个人体会。CAN 真正打动我的,不是它多涨的那几个点 AUC,而是它纠正了我一个长期默认的假设:注意力算完权重之后,行为向量还是原来那个行为向量。五年前我觉得注意力已经是很高级的玩法了,现在回头看,那只是给向量重新排了序,真正改变向量内容的是协同门控这类设计。如果你手头正好有行为序列数据,我建议别急着上更重的序列模型,先把这种向量级协同的底座打牢,后面很多花活其实都是在这上面包装出来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 6:03:30

2026年AI健康APP核心能力测评与选购指南

1. 2026年AI健康类APP专业测评方法论三年前我评测第一代AI健康应用时,大部分产品还停留在基础数据记录阶段。如今打开应用商店,声称能"智能解读体检报告"的应用已超过200款,但真正具备医学实用价值的可能不到5%。这次我耗时三个月&…

作者头像 李华
网站建设 2026/10/26 6:03:28

Charles抓包工具跨平台安装与配置指南

1. Charles 抓包工具安装指南作为一款老牌HTTP抓包工具,Charles在移动端调试、接口分析领域已经深耕十余年。我最初接触它是在2013年调试Android应用的API请求,至今仍保持着每周至少使用3次的频率。相比Fiddler等同类工具,Charles的跨平台特性…

作者头像 李华
网站建设 2026/10/26 6:03:22

AI编程代理的工程控制层:可观测、可恢复、可编排

1. 为什么 AI 编程代理需要一套“工程控制层”1.1 Pi Coding Agent 的定位和现实的短板最近很长一段时间,我大部分时间都在跟各类 AI Coding Agent 打交道。从补一个函数、修一个 bug,到直接让代理生成一个完整模块,效率提升确实肉眼可见。但…

作者头像 李华
网站建设 2026/10/26 6:03:03

金属3D打印在航空制造中的应用与突破

1. 金属3D打印如何重塑航空制造格局2015年,GE航空通过金属3D打印技术一次性成型了燃油喷嘴,将原本由20个零件组装的部件变为整体制造,不仅减重25%,还将使用寿命延长5倍。这个标志性案例揭开了金属增材制造在航空领域大规模应用的序…

作者头像 李华
网站建设 2026/10/26 6:02:43

太阳能热水器检测报告智能审核系统技术解析

1. 项目背景与核心价值太阳能热水器作为清洁能源应用的重要产品,其热性能检测报告直接关系到产品质量认证和市场准入。传统人工审核方式存在效率低、标准不统一、易漏检等问题。IACheck智能审核系统的出现,为检测机构提供了一种全新的解决方案。这套系统…

作者头像 李华
网站建设 2026/10/26 6:01:20

Flex布局实现响应式导航栏的完整指南

1. 为什么Flex布局是导航栏的最佳选择十年前我刚入行前端的时候,导航栏布局简直是场噩梦。float、clearfix、负边距这些技巧玩得飞起,每次调整间距都要重新计算百分比。直到Flexbox的出现,才真正解放了我们的生产力。Flex布局(Fle…

作者头像 李华