1. 先把APG放回CTR预估的上下文里
做广告点击率预估这行,大家都会有个共同感觉:模型结构越来越花,但真正吃劲的问题永远是那几个——特征太稀疏、行为太碎、用户和商品之间的规律太散。前几年大家拼命做特征交叉,从FM到DeepFM,再到各种注意力网络,本质都是在给模型“增加表达能力”。可表达式再强,用的参数还是同一套:不管今天来的是新用户还是老用户,不管用户在刷短视频还是逛电商,深层网络共享同一份权重。
APG这个名字,全称是Adaptive Parameter Generation Network,中文一般叫自适应参数生成网络。它解决的核心问题很直接:让深度CTR模型不再对每个样本用同一套网络参数,而是根据当前样本的特征,实时生成一组更贴合这个样本的动态参数。这也是它和DIN、DIEN这类注意力模型最大的区别——注意力机制改变的是特征的权重,APG改变的是网络自身的权重。
这篇内容主要写给两类人。一类是已经在做推荐或者广告算法、想把APG接入现有精排模型的人,你可以把它当成一份工程落地笔记看。另一类是刚接触CTR模型、准备在项目里做技术选型的人,你能从中理解APG到底解决了什么问题、在什么场景下值得用、上手时最容易踩哪些坑。
之所以强调“上下文”,是因为APG不是那种能凭空替换掉DIN的东西。它更像是在你已有模型骨架上的一个动态调节器,用得好是锦上添花,用不好反而会给训练集群增加压力。后面我会从设计思路、关键实现、训练调参、在线部署四个维度,把整个方案拆开聊清楚。
2. APG的总体设计:从一个朴素想法讲起
2.1 核心直觉:每个样本都有“自己的”网络权重
先看普通DNN在CTR里是怎么工作的。假设输入特征经过embedding之后拼成一个向量x,然后经过几层全连接:h1 = W1·x,h2 = W2·h1,最后sigmoid出点击概率。这里的W1、W2是全局共享的,不管当前样本来自哪个用户群体、哪个商品类目,用的都是同一套参数。
问题出在哪?我举个例子。一个游戏App里,新用户和回流用户虽然行为上都是“还没怎么点击”,但他们背后的决策逻辑差异很大。新用户可能是在看画风和评分,回流用户可能是在关注版本更新和活动奖励。如果用同一套W去拟合全量样本,模型只能把这两类用户的规律“平均”掉,结果两边都不太准。DIN这类模型做的是给历史行为序列加注意力权重,让模型“更关注这次点击相关的东西”,但W本身仍是静态的。换句话说,注意力能改变你看哪,不能改变你怎么看。
APG的想法很朴素:既然每类样本有每类样本的规律,那就根据当前样本的特征,现场生成一组W。输入是用户、商品、上下文特征的embedding,输出是主网络某一层的权重增量。模型从原来的“一个静态网络”变成“一个参数生成器加一个带动态权重的网络”。
2.2 为什么不能真的给每个样本生成整张权重矩阵
这里有个很实际的问题:全连接层的权重矩阵往往很大。假设某层输入是256维,输出是128维,权重矩阵就有三万多个参数。如果每个样本都直接生成三万个连续值,训练会非常不稳定,因为样本量再多也撑不起每样本一套独立参数的方差,很容易过拟合到噪声上。
APG不是让生成器输出整个W,而是输出W的一个“增量”。主网络保留一份基础参数W_base,生成器根据样本特征输出ΔW,实际使用的时候用W_base + ΔW参与计算。同时,生成器还会输出一个标量或向量门控α,控制ΔW的作用强度。这就像你有一本通用菜谱,大部分菜按基础做法来,但遇到川菜客人时,临时在菜谱上加一点辣椒系数,而不是重新写一整本菜谱。
我实际试过直接生成全量ΔW,AUC确实能涨,但涨得不多,训练速度慢了三倍,而且偶尔会出loss突然飙高的情况。后来改成分组生成,稳定性立刻上来了。后面章节我会专门说分组策略。
2.3 整体计算过程拆解
整个APG模块的前向流程大概是这样:
- 输入侧先做embedding查找,把用户ID、商品ID、类目、标签等转成稠密向量。
- 把embedding聚合成一个条件向量c,聚合方式可以是最简单的mean pooling,也可以加注意力。
- 条件向量c经过一个小型生成网络,输出当前样本的ΔW和门控α。
- 主网络在计算某层输出时,实际使用的权重为W_dynamic = W_base + α·ΔW。
- 继续向上传播,最终输出pCTR。
第3步和第4步是关键。生成网络和主网络是一起训练的,生成网络的输入天然包含了用户和商品信息,所以它输出的ΔW天然能反映当前样本的特性。比如某些商品类目下,模型的历史点击率和价格敏感度呈现特殊关系,ΔW就会在这个类目样本上把相关维度的权重抬高。
这里要提醒一点:条件向量c千万不要直接把原始稀疏one-hot向量拼进去。你想想,几亿维度的用户ID向量,直接送给生成网络,参数量会爆炸,而且生成网络很难学到有意义的东西。必须先用embedding压缩。
3. 核心细节:条件表征、分组生成与融合方式
3.1 条件表征怎么构造最稳
我在实践里发现,条件向量的构造方式对最终效果影响很大。最省事的做法是把所有field的embedding做mean pooling,得到一个几百维的向量。这个方法在大部分场景下都能用,但缺点是会把用户特征和商品特征“揉成一团”,丢失两侧的区分度。
稍微进阶一点的做法,是分别对用户侧和商品侧做pooling,再拼接起来。这样生成网络至少能知道“当前是一个什么样的用户在遇到一个什么样的商品”。比如广告场景里,用户侧偏向兴趣表达,商品侧偏向类目和价格带,分开pooling之后,生成网络可以分别学习用户兴趣权重和商品属性权重。
再进阶一些的做法是使用注意力机制:对每个特征的embedding算一个权重,然后加权求和。注意力权重可以由候选商品和目标特征一起计算,类似DIN的思路。但我建议你先从mean pooling开始,把整个APG链路跑通,再回来加注意力。因为同时引入太多新机制,出问题的时候很难定位是哪一部分导致的。
条件向量的维度我一般控制在128到256。太低了表达不够,太高了生成网络的参数量跟着涨,训练压力变大。如果特征非常多,可以先用一层全连接把拼接后的向量压到目标维度,再接生成网络。
3.2 分组生成:把权重矩阵切成块
这是APG工程落地里最核心的取舍。全量生成不现实,完全靠基础参数又失去了动态能力,所以折中方案是“分组生成”。具体来说,把某一层的权重矩阵按输出维度分成若干组,每一组共享一组动态参数。比如输出是128维,可以分成8组,每组16维,生成器只需要输出8个小块的ΔW。
分组有几个好处。第一,参数量可控,生成网络的输出规模显著下降。第二,同一组内的神经元共享动态权重,相当于给模型加了正则,防止每样本权重过于自由。第三,在Serving阶段可以按组缓存部分生成结果,减少重复计算。
分组粒度也是可以调的。你可以做维度级分组,就是每组包含若干个输出神经元;也可以做字段级分组,根据输入特征的类型决定该字段使用哪一组动态参数。字段级分组的好处是可解释性更强,比如价格有关的特征单独一组,行为序列特征单独一组,调试时能看出APG对不同特征类型产生了怎样的影响。缺点是分组过多时,某些组拿到的训练样本太少,参数得不到充分学习。我一般建议组数控制在4到16之间,具体要看特征规模。
还有一个细节:不是每一层都需要加动态参数。CTR模型的底层靠近原始特征,权重主要负责特征交叉,这个位置加动态参数的收益往往最明显;高层靠近输出,更多是汇总语义信息,加动态参数容易引发波动。我通常只在第一层和倒数第二层加动态参数,中间层保持静态,效果和稳定性都更好。
3.3 融合方式:不是简单地做加法
动态参数和基础参数融合时,很多人直接写W_dynamic = W_base + ΔW,然后就开始训练。我在实际操作中发现,直接加法有两个问题。
第一,ΔW的尺度如果一开始没控制好,会让主网络的输出分布剧烈变化,前面已经学习好的特征表达被破坏。解决方法是加门控系数α,让模型自己决定“这次要用多少动态参数”。具体实现可以是在生成网络最后加一个sigmoid输出,把α压到0到1之间。这样模型在初期可以把α学到接近0,让训练过程更平滑。
第二,ΔW的值域和W_base不一致,导致优化困难。可以在生成网络最后一层加一个缩放操作,比如让每个分组内的ΔW按照该组参数的L2范数做归一化,再乘一个可学习的缩放系数。这样做的好处是,模型可以独立地决定动态增量的方向和幅度,而不需要同时承担“生成一个大数值”的压力。
还有一种做法是用低秩分解来近似ΔW。假如动态权重矩阵是m乘n,直接生成m乘n太大,那就生成两个小矩阵U和V,令ΔW = U·V,U的维度是m乘r,V是r乘n,取一个很小的r比如8或16。低秩近似能极大减少生成网络的输出规模,同时保留足够的表达能力。我们在线广告场景里,低秩近似的效果和直接分组生成差不多,但训练速度明显更快。
3.4 条件复用:一次生成,多层共用
你可能会有个疑问:APG生成出来的ΔW,是只用于某一层,还是可以给多层用?我尝试过两种方案。第一种是每层独立生成,表达能力强,但计算开销也大。第二种是只生成一次ΔW,然后通过线性变换映射到不同层。比如生成一个基础条件向量,再分别为每层输出一个小映射,相当于每层的动态参数来自同一个“决策依据”。第二种方案在线上Serving阶段特别有用,因为基础条件向量可以对同一次请求内复用,减少生成网络调用次数。
从实验效果看,两种方案的AUC差距通常在0.1%以内,但第二种方案的耗时低很多。如果你的线上性能指标很紧张,优先考虑一次生成、多层映射的方案。如果离线实验效果不满意,再回退到逐层生成。
4. 训练环节:损失函数、数值稳定性与调参经验
4.1 损失与样本组织
APG的主网络部分仍然使用标准的LogLoss,也就是交叉熵损失。你不需要改变损失函数,APG只是改变了网络的参数生产机制,不改变学习目标的定义。这里有一个容易忽略的问题:CTR预估里正负样本比例往往极其悬殊,尤其是在广告场景下,点击率可能只有百分之几。直接训练会出现预测值被负样本主导的现象,所以一般需要做负样本降采样或者训练时对负样本分配权重。
样本组织还会影响生成网络的学习效果。我建议尽量保证每个batch里包含多样化的用户和商品类型。如果batch内样本结构单一,生成网络看到的条件向量范围太窄,学到的ΔW会偏向特定类型,泛化能力变差。实践中可以用随机shuffle加多天数据混合来解决,不要按小时把样本切得太整齐。
如果是从零开始训练APG,不建议直接把整个模型随机初始化。最好是先用你现有baseline模型(比如DIN、DCN V2)训练到收敛,然后把模型的embedding和基础参数迁移到APG结构里,genertor部分随机初始化并置零输出。这样做的原因很简单:基础参数已经蕴含了全量数据下的共性规律,APG只需要学那些在共性之上“偏移”的部分,收敛速度和稳定性都会好很多。
4.2 数值稳定性:动态权重的最大隐患
训练过程中我遇到最多的异常是loss突然变成NaN,或者AUC在某个节点掉头向下。排查下来,大部分都是动态权重数值失控导致的。生成网络输出的ΔW如果绝对值过大,作用到主网络之后,激活函数输入值会进入饱和区,梯度要么消失要么爆炸。
一个有效的控制手段是约束生成网络的输出范围。在生成网络最后一层之后,加一个tanh激活,把输出压到-1到1之间,再乘一个较小的缩放因子,比如0.1。这样ΔW的初始量级就受到了限制,模型可以在训练过程中慢慢调节缩放因子的大小。
第二个手段是梯度裁剪。动态权重比静态网络更容易出现梯度剧烈波动,我一般会把全局梯度范数裁剪到1.0以下。单独对生成网络部分也用一次梯度裁剪,效果更稳。注意,如果你用的是TensorFlow或PyTorch,裁剪要放在优化器更新之前,否则还是会崩。
第三个手段是对动态参数加L2正则。正则项的权重系数可以从小往大调,比如先试1e-6,如果发现训练过程中ΔW的L2范数持续上升,再往1e-5或1e-4方向调。私以为L2正则是APG训练里最便宜的保险,加上之后基本不会出现剧烈波动。
4.3 关键超参数与初始化
我直接给一组比较稳的起点值,你可以根据自己场景微调:
| 超参数 | 建议值 | 说明 |
|---|---|---|
| 条件向量维度 | 128 | 特征压缩后的向量维度 |
| 生成网络层数 | 2 | 一层或两层足够,不宜过深 |
| 生成网络隐层维度 | 256 | 比条件向量略大即可 |
| 分组数 | 8 | 按输出维度切分 |
| ΔW缩放因子初始值 | 0.1 | 控制动态权重的起始量级 |
| 门控α初始偏移 | -2 | 让初始α接近0.12,保持稳定 |
| 学习率 | 主网络0.001,生成网络0.0001 | 生成网络收敛较慢,用更小学习率 |
| L2正则 | 1e-6 | 对生成网络输出施加约束 |
| Batch大小 | 4096或以上 | 保证每批样本多样性 |
这里要特别强调生成网络的学习率。不少人直接把整个模型用一个学习率,但生成网络本质上是在学“参数偏移”,它对梯度更敏感,用太大学学习率会导致ΔW来回震荡。我的习惯是生成网络单独用Adam优化器,学习率设为主网络的十分之一到一半。先让主网络稳定下来,再让生成网络慢慢跟进。
4.4 效果评估不能只看AUC
离线评估时,除了总体AUC,我强烈建议分维度看效果。你可以把测试集按用户新老、商品类目、流量来源拆开,分别算AUC。APG通常会带来不同人群的效果不一致,比如对新用户的提升幅度明显高于老用户。这不是坏事,说明动态参数确实在关注个体差异。但如果你想说服团队上线,最好能解释这种差异。
另一个推荐的做法是消融实验。把APG模块替换成“恒等映射”,也就是固定ΔW为零,看看效果掉多少。再试一下关闭门控α固定为1,看看门控机制有没有用。这两步能让你确认AUC提升到底来自APG的动态能力,还是仅仅因为增加了参数量。
5. 在线推理与工程落地:让APG跑起来不难
5.1 在线时延:动态权重不能重复算
把APG接到线上以后,第一个迎面而来的问题就是推理耗时。原来的主网络是一个固定结构,输入特征之后直接前向计算。现在多了生成网络,每个请求都需要先算一遍条件向量,再生成ΔW,再拿ΔW参与主网络计算。如果生成网络本身结构不重,增加的耗时通常在20%到30%之间。
但线上排序服务往往只有几十毫秒的预算,20%已经很高了。我实际用下来的优化思路是:对生成结果做键值缓存。你可以把条件向量的关键部分作为key,比如“用户分群ID + 商品类目ID + 流量位ID”,把对应的ΔW缓存下来。同一批用户、同一类商品在一段时间内的动态权重其实变化不大,完全可以复用缓存结果。这个优化能把APG增加的耗时压到5%以内。
这里有个前提:你要保证缓存不会让模型预测变得过于粗糙。比如一个用户经常在刷新短视频,他的兴趣是缓慢变化的,缓存几分钟完全没问题。但如果做实时竞价广告,用户行为变化可能很快,缓存时间要缩短,或者只在非实时部分复用。
5.2 部署形态:生成器和主网络一起导出
我服务端上线的经验是,把生成网络和主网络合并成一个完整的计算图导出,不要分开部署。分开部署会导致两次RPC调用,第一是取用户特征,第二是取动态权重,延迟很难接受。合并导出之后,虽然模型文件变大,但整体推理链路简单,排障也容易。
如果你用的是TensorFlow Serving,需要注意一下模型输入的shape可能变成动态的,因为张量大小由分组数决定。建议在导出前先确认所有维度都是静态的,或者明确设置允许动态维度。PyTorch的TorchServe相对灵活,但同样要在服务端初始化时提前分配好内存。
在线推理时还有个小坑:真实请求里会出现训练时没见过的特征值,比如新注册的用户ID。传统模型的embedding表查找会返回全零向量,APG生成网络拿到没有任何历史信息的条件向量,可能会输出一个“奇怪”的ΔW。我的对策是在生成网络输入端对条件向量做归一化,同时把生成结果限制在一个预设范围内,避免没见过的情况产生极端预测。
5.3 增量训练:动态参数时效性
CTR模型一般需要每天或每小时更新,APG也不例外。不过动态参数对时效性更加敏感,因为ΔW学到的是当前分布下的偏移。如果线上数据分布已经变了,而增量训练还没跟上,动态权重给的“偏移”可能反而变成干扰。
我建议增量训练的样本窗口不要拉太长。按小时更新的话,用最近72小时的数据就够了;按天更新的话,用最近14天左右。同时增加一个“新鲜度特征”,比如样本所属时间距离当前时间的间隔,作为生成网络的输入之一。我发现加了时间新鲜度特征之后,模型能自动调整动态参数在新旧样本之间的权重,在线效果更稳定。
5.4 A/B实验注意事项
上线前肯定要做A/B,但APG比一般模型更容易出现“离线涨、线上不涨”的情况。原因通常是离线特征和在线特征存在分布差异,或者线上样本经过实时处理后某些字段缺失。我在A/B实验时一般先做小流量验证,观察GAUC是否提升,同时看几个子维度的稳定性,特别是新用户和冷门商品的效果。
还有一点,APG引入的是动态参数,天然会让模型输出变得更“个性化”,这可能导致单用户预测值的方差变大。如果你们线上有保序需求,比如广告排序必须保持某些高价广告的优先级,需要额外加一个控制机制,比如对最终预估分数做平滑。
6. 常见问题与排查实录:我把遇到的坑都列出来
6.1 训练loss不降或下降极慢
这个现象出现时,我第一个怀疑的是生成网络没有被“唤醒”。因为ΔW的初始值接近零,门控α初始值也很小,模型前几步和普通静态网络几乎一样,梯度回传到生成网络时信号会变得非常弱。解决方法是先固定主网络,单独训练生成网络几步,或者给生成网络一个较大的初始学习率做预热。还有一个更简单的方法:在训练前几个epoch,把门控α手动设为1,让生成网络的梯度能充分传导。
6.2 离线AUC提升只有0.1%甚至持平
很多人在这一步就放弃APG了,其实是没找到发力点。如果APG加在深层全连接上效果不明显,试试把生成目标换成embedding层,也就是对embedding做动态缩放。有些场景下,特征动态变化对预测的影响主要体现在embedding层面,而不是全连接层面。我遇到过在DCN V2上怎么调都不涨,把APG移到embedding层后AUC一次性涨了0.4%的情况。
另一个原因是条件向量信息不够。只用mean pooling,生成网络看到的只是用户和商品的平均表达,失去了细节。这时候可以用注意力pooling,或者在条件向量里拼接一些统计特征,比如用户近30天点击次数、当前类目历史CTR等。特征越丰富,生成网络越容易找到有价值的偏移方向。
6.3 训练时loss先降后升,反复震荡
这种状态一般发生在训练进行到中后期,基础参数已经收敛,但ΔW还在持续更新。问题大概率是生成网络学习率偏大,或者L2正则太弱。先把生成网络学习率降到主网络的0.05倍,再把ΔW缩放因子从0.1降到0.05,然后观察曲线是否平滑。如果还在震荡,可以给门控α加一个时间衰减,让模型后期自动收缩动态参数的强度。
6.4 在线耗时飙升
在线耗时高往往是两个原因:一是生成网络算力开销大,二是缓存没有命中。排查时可以先看缓存命中率,如果低于80%,说明key设计得不好。适当扩大缓存粒度,比如把“商品ID”换成“商品二级类目ID”,命中率会明显提高。如果缓存命中率正常但耗时还高,那就是生成网络本身太重了,考虑把生成网络从两层减到一层,或者改用低秩分解。
6.5 动态参数在不同分组上效果差异巨大
有时候你会发现某些分组对AUC提升贡献很大,有些分组可能还是负贡献。这很正常,因为不同输出维度负责的语义不同。处理办法是给每个分组单独设置门控α,让模型学习每个分组的动态参数使用强度。我通常在生成网络输出时,为每个分组输出一个独立的门控值,不再全局共享一个α。这样模型可以自动关掉那些没用的分组,减少干扰。
下面是一个排查速查表,方便你快速定位问题。
| 现象 | 可能原因 | 排查手段 | 解决建议 |
|---|---|---|---|
| loss为NaN | 动态权重数值过大 | 检查ΔW的L2范数 | 加tanh、缩小缩放因子、梯度裁剪 |
| AUC提升微小 | 条件向量信息不足 | 试注意力pooling | 增加统计特征、调整分组位置 |
| 训练震荡 | 生成网络学习率过高 | 降低生成网络学习率 | 分优化器、加L2正则 |
| 在线变慢 | 生成网络重复计算 | 看缓存命中率 | 优化缓存key、简化生成网络 |
| 线上不涨 | 训练/推理特征不一致 | 对比线上特征分布 | 统一特征工程、增加缺失值处理 |
| 子群体效果差 | 分组策略不合理 | 分维度评估 | 给每分组独立门控 |
7. 从APG项目里沉淀的一点心得
做完这个项目,我最大的体会是:APG这类动态参数方法,并不是靠“每样本一套权重”这个噱头取胜的,它真正有价值的地方在于,给模型提供了一个“按需调整”的抓手。CTR预估模型到了中后期,瓶颈往往不是容量不够,而是样本空间里的异质性没有得到显式建模。APG相当于把一个全局模型变成“全局共性 + 局部差异”的组合,这个思路本身是有生命力的。
如果你现在准备把APG集成到自己的模型里,我的建议是先找一个简单的baseline,比如DIN或者DCN V2,在上面加一个分组生成模块。不要一上来就做全量重构,也不要把生成网络设计得太复杂。先把训练流程跑通,确认APG模块的梯度和数值都是稳定的,再做效果迭代。
我后来还尝试过把APG和MMoE结合,在多目标场景下给每个目标单独生成动态参数,效果比共用一套动态参数好不少。如果你也在做多目标排序,可以沿着这个方向去试。另外,动态参数和增量embedding特性组合使用,对冷启动场景的改善也很明显,这是另一个值得深挖的方向。
最后分享一个小技巧:训练APG模型时,每个epoch记录一次ΔW的均值、方差和门控α的均值。这几个数能帮你直观地看到动态参数在学什么。如果方差一直在涨但AUC不动,说明生成网络在拟合噪声;如果方差几乎为零,说明动态参数没有被利用起来。监控好这几个指标,整个项目的调试效率会高很多。