APG自适应参数生成网络:突破CTR预估共享MLP的瓶颈
做点击率预估的应该都见过这个经典结构特征过 Embedding拉平之后接几层 MLP最后压成一个 Logit。这套框架简单、稳定、上线快是绝大多数推荐系统、广告系统的主干。但时间一长我总觉得哪里不对——年龄、用户ID、商品ID、类目ID这些语义完全不同的特征共用同一套 MLP 权重本质上是在要求它们能被同一个非线性函数加工。APGAdaptive Parameter Generation Network自适应参数生成网络就是来回答这个问题的它不想换一个更大的模型而是把“模型参数”本身变成“按特征动态生成”的东西。这篇博文会拆开讲 APG 解决的问题、三层参数分解的核心原理、复现过程中的关键配置以及我实际调参时踩过的坑和排查经验。适合正在做 CTR 模型优化、或者对参数级个性化建模感兴趣的算法同学下面直接开聊。1. 点击率预测里的隐痛共享参数的局限与独立参数的代价1.1 共享参数到底“共享”了什么先回到常见的 Embedding MLP 框架。假设有 N 个特征每个特征查表得到一个向量拼在一起成为一个大向量然后经过若干全连接层。这里隐藏层权重 W 对 batch 内所有样本、所有特征都是同一份。你对年龄做“向上取整”这种非线性变换对商品 ID 也做同一套变换你对高频特征的变换方式跟低频特征也完全一致。这在业务上说得通吗不太说得通。举个具体的例子价格带 0-50、50-100、100-200 这几个档位量纲相近共享一套非线性变换问题不大。但用户性别、商品详情页停留时长、类目 ID 这几种特征底层分布和语义差距非常大共用一个 MLP 层模型只能学出一个“平均最优”的变换函数。说好听点叫共享泛化说难听点就是削足适履。传统做法怎么补偿加宽加深 MLP、上更复杂的特征交叉结构。这有效但本质上还是在同一个“一套参数打天下”的框架里兜圈子。模型能力确实增强了可每个特征实际拿到的仍然是一套平均化的映射特征自己的个性还是被淹没了。1.2 独立参数的代价又太大那换个极端每个特征单独给一套 MLP 参数能不能解决问题能但代价非常现实。假设 Embedding 维度是 16MLP 第一层输出 128 维一个特征的专属参数就是 16×1282048 个。淘宝、头条这种规模的 CTR 模型特征量级动辄十亿百亿给每个特征实例单独存一份参数矩阵直接内存爆炸训练也基本不可能收敛。于是有人做折中按 field 共享参数比如用户侧的字段用一套参数物品侧的字段用另一套。这种“分桶共享”缓解了一部分问题但分桶的粒度很难拍。分太粗跟全局共享差别不大分太细又会落入参数爆炸的老路。还有一些工作用 SENet、FiBiNET 这类注意力机制去给特征加权这确实让模型知道“哪些特征更重要”但注意力的权重是标量它改变的是特征的“大小”不是“变换函数本身的形状”灵活性仍然有限。我自己看 APG 这篇论文时的第一反应是作者想找的是一条中间路线——让不同特征拥有不同参数的形状又不能让参数数量随着特征数线性增长。要做到这一点就不能再按“一个特征一份参数”去想而是换一个角度参数本身能不能由一个轻量网络来生成2. APG 的核心设计三层参数分解加一个轻量超网络2.1 从“参数不重要”到“参数按需分配”APG 的思路可以这样理解不再为每个特征显式存储一套参数而是维护几套公共的“基底”然后根据特征的值动态组合出属于这个特征的参数。一套参数 全量共享基底 模式级偏移 特征级偏移。这个公式看起来简单落到实现上需要回答三个问题基底是什么、偏移怎么生成、生成完怎么保证推理时不增加开销。先说基底。全量共享基底 W_shared 就是原来那套公共参数它保证模型的基础表达能力。模式级偏移 W_mode 解决“一类特征”的差异比如用户侧和物品侧特征级偏移 W_feature 解决“具体到某个特征”的差异比如 iPhone 17 和一支笔在模型底层所需的变换完全不同。三部分相加得到这个特征最终使用的参数矩阵。这一步把“特征的个性化”从显式存储变成了合成生成参数总量不再随特征数量线性增长。这里有个容易混淆的点这里说的“参数”并不是 Embedding 本身而是特征向量进入 MLP 后的变换矩阵。APG 改变的是特征从 Embedding 到隐层之间的映射方式不是特征怎么查表。2.2 轻量生成网络与低秩近似偏移参数不能真拿一个 MLP 去直接输出几百个权重数字这会很重。APG 解决这个问题的办法是低秩分解把偏移参数 W_offset 拆成 U × diag(s) × V 的形式。U 和 V 是全局共享的投影矩阵训练之前就确定维度、参与训练但不随特征变化真正由生成网络输出的只有中间那个对角向量 s。这一步非常关键也是很典型的工程思维偏移的“形状”由公共的 U、V 刻画“个性”只体现在少量奇异值上。比如 U 是 16×8 的矩阵V 是 8×128 的矩阵特征级偏移只输出 8 个数字加上模式级偏移的 8 个数字一共 16 个浮点数。这个东西再小不过了。那生成网络用什么输入论文的答案是用特征值本身而不是 Embedding 向量。特征值经过一个很小的 MLP 后输出 s。我复现的时候尝试过用 Embedding 作为输入发现有两个问题一是梯度链路变长生成网络和 Embedding 互相耦合训练初期相当不稳定二是 Embedding 维度通常很高生成网络想消化好它自己就得做得不小轻量的优势就没了。用归一化后的特征值比如 one-hot 或者频次的归一化统计值不仅输入维度低而且天然携带“特征是否被激活、激活强度多少”的信息生成网络可以做得非常小两层 MLP、隐层 64 到 128 维就足够了。2.3 训练与推理的两套路径用 APG 的完整流程是训练和推理两套路径。训练阶段每个 batch 里特征值进入生成网络产出模式级和特征级的奇异值向量再组合成当前 batch 特征对应的 MLP 权重完成一次前向和反传。推理阶段特征值是什么、生成网络长什么样都不重要了。部署之前把所有特征的参数矩阵提前实例化W_final W_shared U_mode × diag(s_mode) × V_mode U_feature × diag(s_feature) × V_feature算完之后存成一张静态参数表。线上推理就是普通的矩阵乘法生成网络的耗时完全被抹掉了。这一点要专门圈出来APG 的动态参数生成只在训练阶段存在推理阶段它退化成一张预计算好的参数表。很多同学看完论文会觉得“每来一个样本我都要过一遍生成网络那还怎么上线”其实完全不是这样。这正是 APG 最聪明的地方它把“动态”用在训练和模型能力上把“静态”留给线上服务。注意训练阶段和推理阶段是两套完全不同的路径最容易踩坑的地方就在这后面会单独列一节讲部署时的具体操作。3. 复现 APG 的关键配置特征分组、低秩维度与训练节奏3.1 特征分组策略别一开始就按 field 一刀切APG 的“模式级”参数是按特征组区分的所以第一步要回答“怎么分组”。论文里按特征聚类来做实际操作中我建议三步走。第一先按业务域分组。用户特征、商品特征、上下文特征、交叉特征各是一组。这一步比较粗但绝对安全能保证每组特征在语义上有内部一致性。第二看组内特征数量。真正的难点是特征组之间的规模不平衡。比如用户特征只有 20 个商品特征可能有 5 万个两组的参数数量差太大。我的经验是对大的组做二次细分可以按特征频次分桶高频特征一组、中频一组、低频一组。这样做有一个额外好处高频特征学到的模式更稳定低频特征反而能借助同组的模式参数做平滑泛化。第三控制总数。模式数量不是越多越好。我测过从 4 组到 32 组的情况超过 16 组之后验证集 AUC 几乎不再涨参数量和过拟合风险开始上升。一个比较稳妥的经验值是 8 到 16 组先按业务分再按特征规模微调。还要注意频率分桶时的统计问题频次统计要用训练数据的统计值而且要离线算好不要用在线实时统计。一旦特征频次统计口径变化生成网络输入分布就得跟着变模型效果会莫名其妙下降。踩过一次之后我现在会把频次统计和归一化配置直接固化在特征配置里不允许线上改动。3.2 低秩维度 r 和生成网络的结构选择低秩维度 r 是 APG 最敏感的超参数。它同时影响特征级偏移和模式级偏移的表达力。按照我实验的经验r8 到 16 之间比较合适。特征维度如果是 16 或 32r 太小比如 1 或 2表达力不够等于只给参数做了一次标量缩放r 太大比如 32 甚至 64低秩约束形同虚设参数数量上升训练难度也变大。具体判断方法很简单先固定其他配置把 r 从 4 加到 32观察 AUC 曲线和验证集 Loss。如果提高 r 后 AUC 还在明显涨说明特征本身确实需要更强的个性化表达如果曲线已经基本走平说明继续加 r 只是在加大存储和过拟合风险没必要。生成网络我的搭配是输入层接特征归一化值中间一层 128 维的 ReLU输出层每组 2r 个标量r 个给模式级 s_moder 个给特征级 s_feature最后一层接 tanh 激活。这里有一个细节s 的值域要控制住。如果让 MLP 直接输出任意实数低秩矩阵乘出来数值可能很大训练初期 loss 会剧烈震荡。tanh 把输出锁在 -1 到 1配合初始化在 0 附近的 U、V能保证 APG 的偏移量一开始是接近 0 的小扰动而不是充满整个特征空间的大信号。U、V 的初始化用均值为 0、标准差 0.01 的正态分布。太大的初始化会让我们三部分相加变成“公共基底 一个随机大偏移”模型一开始就在一个很怪的点上训练收敛会慢很多。3.3 训练细节与数值稳定性训练 APG 踩过最多的坑就是梯度不稳定。生成网络虽然小但它参与训练的是整条链路任何一个环节数值溢出都会让它抖动。我总结了三个必须注意的点。第一特征归一化值不要直接用 0 和 1。CTR 特征里大量 one-hot直接用 0/1 让生成网络的输入分布过于稀疏。我做了一次 log 变换后再归一化到 0 到 1 区间收敛明显更稳。第二生成网络建议单独控制学习率。APG 论文里的做法是生成网络和主网络统一训练我在复现时发现生成网络的学习率可以适当降一半比如主网络 lr1e-3 时生成网络用 5e-4。降低生成网络的学习率会小幅延长训练时间但能换来稳定性和最终效果的提升值得。第三混合精度训练要小心。低秩分解里 s 是特别小的浮点数如果用半精度训练有概率下溢成 0 或者被舍入噪声干扰。我的建议是生成网络相关部分强制保留 FP32只在 Embedding 和主网络部分开 AMP。开启 AMP 前先关闭一切低精度优化选项跑一组全 FP32 的基准再逐步开关这样出了问题能很快定位。3.4 训练完成后如何固化参数训练结束后需要把动态参数“具体化”。具体操作不复杂遍历全部特征组和特征对每个特征计算 W_shared W_mode W_feature保存为稠密矩阵。这里有一个实现细节特征级参数是按特征 ID 逐一生成的如果一个特征的覆盖率很低在训练集里几乎没出现过那么生成网络喂给它的输入大概率趋于 0生成出来的 s 也可能贫血。固化阶段如果照单全收这些特征在服务端就等于拿到了一个小噪声矩阵。我的处理方式是对频次低于阈值的特征强制把特征级偏移置为 0只留模式级偏移和共享基底。比如覆盖样本数低于 500 的特征直接走默认路径。这个策略不改变训练逻辑只影响固化阶段的生成方式实测能小幅度稳定线上效果。4. 效果验证与调优方向从离线指标到线上 AB4.1 离线实验怎么设才能看出 APG 的收益评估 APG 至少得有三组对照。第一组是当前线上主模型通常是 Embedding MLP 或者带一点交叉结构。第二组是把主模型中的 MLP 某一层替换成 APG 生成参数的版本其他完全不变。第三组是同等参数规模的传统加宽加深版本。这里要特别说明基线模型里的“加宽加深”对照组很重要。如果不加这一组APG 提高的 AUC 可能只是“多了一组小网络”带来的收益而不是参数动态化本身的收益。控制变量的原则APG 版本相比基线增加的参数量应该保持在极小范围内。以我自己复现的经验在 Criteo 小样本集上把第一层 MLP 换成 APG 后AUC 相比纯 MLP 基线有千分位级别的稳定提升。这个幅度看着不大但在 CTR 场景里千分位 AUC 往往意味着几个点的收入差异是可用的增量。4.2 三组消融实验模式级和特征级分别贡献了什么按论文的思路我一共做了三组消融只用共享基底、共享基底模式级、三者全开。第一组就是普通 MLP第二组加了模式级偏移后AUC 有小幅提升说明“分桶共享”本身有用第三组三者全开收益明显又上一层楼。从实验记录看特征级偏移带来的提升大约是模式级的两倍。这也符合直觉模式级只能区分“用户特征 vs 商品特征”这类粗粒度差异而特征级才能真正做到“这件商品它的 ID 需要的映射和另一个 ID 不同”。这个结论对实际项目也有指导意义如果训练资源有限优先保特征级如果特征数量太大了可以牺牲模式级的复杂度但别把特征级砍了。4.3 线上 AB 需要注意的三个隐藏坑APG 离线效果能复现不代表线上就稳。第一个坑是特征分布漂移。训练集里的特征频次统计和线上实时分布总会有差异一旦某些特征的实时激活率变化超过 30%固化好的特征级参数就不再匹配当前样本效果可能回退到基线之下。解决办法是离线先做一次“即时重算”用最近 7 天的日志重新生成参数表线上配置一个异步任务定期按月或按周更新参数表。第二个坑是冷启动特征。新特征上线时会没有历史频次统计生成网络给不出可靠的输入。我的做法是在固化阶段统一落成默认模式参数并给这类特征打上“冷启动标记”等统计量积累到阈值后再参与常规更新。第三个坑是参数表的存储与读取。特征数很大的项目固化后的参数表可能占几百 MB 内存。做在线服务时不能每台机器全量加载要按特征组拆成分片按需加载。上线的时候先做小流量验证重点看延迟曲线有没有毛刺。APG 推理时本来就是一次静态矩阵乘如果发现延迟涨了十有八九是参数表加载和特征组索引的问题而不是计算本身的问题。5. 常见问题与排查实录一张表解决 80% 的坑实际动手做 APG 的过程里我把高频问题整理成了一张速查表先放在这里现象可能原因处理方式Loss 前期剧烈震荡不收敛生成网络输出值域过大U/V 初始化方差太大给 s 加 tanhU/V 初始化降到 0.01AUC 比基线还低模式分组不合理组内特征语义太杂按业务域分组再用频次分桶验证集涨但线上跌特征分布漂移离线统计和线上不一致用最近 7 天日志重算参数表训练速度明显变慢生成网络在各层重复计算没有复用把生成网络输出缓存到 batch 级别复用固化后推理结果对不上特征级参数和模式级参数维度没对齐检查 s_mode 和 s_feature 的拆分位置低频特征贡献为 0归一化输入太小导致生成值趋近 0对低频特征直接走默认模式不用零偏移下面把几个特别容易出问题的细节展开讲。5.1 生成网络的输入分布与输出值域生成网络的输入分布会直接影响训练稳定性。如果直接用 one-hot 0/1MLP 的输入大部分是 0输出 s 很容易退化成只依赖于模式分组、跟特征本身无关的常量。这样一来所谓“特征级个性化”就名存实亡了。所以输入的特征值一定要经过有区分度的变换log 变换加归一化是我最常用的做法。输出值域方面对 s 我做了一个更细的约束模式级 s 用 tanh特征级 s 用 0.5×tanh。特征级偏移给更小的范围是为了防止它对某个样本产生过大的参数扰动尤其是 batch 内个别高频特征主导梯度时。这个不对称设置在我的实验里能降低 10% 左右的验证集波动。5.2 训练速度为什么比预期慢APG 训练时每个 batch 都要过一遍生成网络虽然网络小但架不住 batch 多。一个常见优化是把生成网络的结果在 batch 内做缓存。因为同一个 batch 里同一个特征会重复出现对同一个特征 ID 只生成一次 s然后在整个 batch 内复用。这个缓存操作很关键实测能省掉大约 30% 的训练时间。另一个容易被忽视的点是如果模型同时开了正则、Dropout、BatchNorm 等操作要注意生成网络产生的参数和这些随机机制叠加后的效果。APG 直接改变了 MLP 权重的数值BatchNorm 的统计量更新策略可能不再适用于最后一层我在实验里是建议 APG 替换的那层不接 BatchNorm让偏移本身承担归一化的角色效果更平滑。5.3 固化阶段最容易出的错固化阶段维度不匹配是非常常见的错误。s_mode 和 s_feature 都是从生成网络里切出来的很容易在拼接、拆分的时候把顺序弄反。一个保险的做法是不要在代码里硬编码切分位置直接用命名张量或者字典保存固定字段名。另外固化不是简单地把模型 ckpt 里的权重导出来。APG 的权重是“生成逻辑”不是“最终参数”。很多同学在这块容易出错加载了 checkpoint线上直接跑模型发现推理时要临时走生成网络延迟暴涨。正确流程是训练结束后写一个独立的“实例化脚本”读 ckpt加载生成网络、U、V、特征表遍历所有特征生成最终参数表再用参数表做常规推理。这个脚本要单独测并且建议固化后跑一遍全量训练集的重算比对两侧输出的 logit 是否一致一致再上线。6. 一些个人体会与扩展想法跑完 APG 之后我的一个明显感受是做模型优化的思路不一定每次都是把网络做深做宽“让参数随输入条件而变”这条路同样有价值。而且 APG 用低秩近似加固化推理的方式把动态化带来的计算和部署成本压到了几乎为零这在我看到的超网络类工作里是很少见的。它没有为了“动态”而动态每一步设计都在盯着一件事到底能不能落地。也提醒想直接复现的同学APG 适合的是那种底层共享 DNN 参数已经成为瓶颈的模型。如果你现在的模型已经上了 DIN、DIEN 这类 attention 机制先想清楚是哪一层参数还存在“一刀切”的问题。APG 不是万能插件它替换的是特定网络层不是整个模型。我个人后续还做了一些扩展尝试比如把 APG 那套生成加固化的思路用到多任务学习的共享塔上效果也很有意思。还有一个小技巧在生成网络的输入里加入特征出现的上下文计数比如该特征在用户历史序列中的出现次数能让生成结果的语义更丰富。这个方向还没有系统验证但初步结果让我觉得值得继续挖。如果大家在 CTR 模型里也遇到“共享参数不够灵”的问题推荐再回头好好看看 APG。