PNN模型详解:从特征交互到广告点击率预估的工程实践 1. 广告点击率预估里最让人头疼的问题特征交互我做广告点击率预估这些年最大的体会是模型的效果上限通常不取决于单个特征有多强而取决于特征和特征之间的组合信号能不能被有效挖掘。拿搜索广告举个典型例子“用户性别”和“广告品类”单独看都是很普通的特征但“男性用户 3C数码广告”这个组合点击倾向往往远高于两个特征单独线性叠加出来的预估值。类似这样的交互信号在推荐系统里比比皆是新用户 高折扣商品、深夜时段 外卖品类、高收入地区 奢侈品广告每一条背后都是存在明确业务含义的特征组合。传统上这种交互靠什么捕获靠人工特征工程。把“性别男 品类3C”显式地叉乘成一个新特征扔进逻辑回归。这在中小规模场景里确实有效但随着特征数量增长组合空间是爆炸式增长的。几十个特征字段做二阶叉乘就可能产生几百万甚至上千万的组合特征人工枚举根本不可能覆盖全即便做部分叉乘长尾组合因为样本稀疏参数也基本学不动。到了这个阶段特征工程变成了一门“手工艺术”成本极高而且每上线一个新业务都要重新做一遍。深度学习进入排序领域之后大家一开始的思路很朴素把稀疏的one-hot特征映射成稠密的Embedding向量然后拼接起来喂给多层全连接网络也就是经典的EmbeddingMLP结构。这个结构确实省去了人工叉乘的繁琐工作理论上多层非线性网络也能拟合任意复杂的函数关系。但我在实践中发现一个问题EmbeddingMLP对特征交互的建模能力其实是偏弱的。原因在于向量拼接本身不产生任何交互信息两个特征之间的联合效应完全依赖后续网络的隐式学习。如果训练数据充足、网络足够深隐式学习还能work可现实中的特征组合服从长尾分布大量低频组合在训练集里只出现一两次靠深度网络的黑盒拟合去捕捉这些稀疏交互效果非常不稳定。所以业界一直在寻找一种方案能不能在模型结构里显式地把特征交互算出来不需要人工定义交叉特征而是让模型在Embedding之后、进入深层网络之前先做一次系统性的、结构化的特征交互相乘。产品神经网络Product-based Neural Network简称PNN正是沿着这条思路提出的经典方案。它通过在Embedding层和全连接层之间插入一个乘积层把特征两两之间的交互信号显式建模出来然后传给上层网络继续学习。这篇文章我就把PNN的原理、工程实现和落地经验完整地拆开讲一遍。2. PNN的核心机制乘积层到底在算什么2.1 整体数据流从离散特征到点击率先看PNN的完整数据流方便后面理解每一层的职责。输入是多个字段的离散特征比如用户性别、年龄段、商品类目、广告主ID等等。每个字段经过独立的Embedding查表得到一个稠密向量。假设有M个字段第i个字段的Embedding记为向量f_i。Embedding层的作用是把高维稀疏的one-hot特征压缩成低维稠密表示这一步和大部分深度排序模型没有区别。接下来进入PNN最关键的乘积层Product Layer。乘积层接收M个Embedding向量输出两类信号一类是线性信号z它把各个字段的Embedding信息保留并传递给上层相当于确保基础特征信息不丢失另一类是乘积信号p它由M个Embedding两两计算乘积得到显式地把特征对之间的交互信号提取出来。最后z和p拼接在一起经过若干层全连接网络再经过sigmoid函数输出点击率预估值。这里有一个值得注意的设计哲学PNN并不是让乘积层计算完交互就结束而是让交互信号和原始特征信号一起进入深层网络。这样一来上层网络既可以基于原始特征做进一步的非线性组合也可以基于交互信号做更高阶的抽象。这个“显式交互 隐式学习”的组合是PNN效果好于纯粹隐式模型的关键。2.2 线性信号z给上层网络保底线性信号z的构造方式在论文里用了拼接加权的形式。简单理解就是把M个Embedding向量做一个整体的汇总可以看成是把每个字段单独的信息原封不动地送到上层。用向量拼接的方式来构造z信息保留最完整但会让后续全连接层的输入维度变大。也有人尝试用求和或者平均池化来压缩z减少参数的同时也会丢失一部分字段内的细节信息。我的建议是在特征字段数量不多比如20个以内时z用拼接方式保留全部信息字段很多时再考虑先用一个轻量的全连接层把M个Embedding压缩一下再拼接进乘积层输出。这样既控制了参数规模又不会因为过度压缩丢失底层特征信息。2.3 乘积信号p特征交互的显式提取p是PNN的灵魂也是“Product”这个名字的由来。对于M个字段p中包含了M个Embedding两两之间即C(M,2)对的乘积结果。可问题是两个向量的“乘积”定义方式有两种一种是内积一种是外积两种方式在信息粒度和计算开销上差别巨大由此也衍生出了PNN的两种经典变体IPNN和OPNN。先看内积。两个Embedding向量做内积得到一个标量表示两个向量在表示空间中的方向相似度。这个标量越大说明两个特征向量的语义越“对齐”交互关系越强。把所有特征对的内积结果收集起来就得到一个维度为C(M,2)的向量这个向量可以被上层网络直接使用。内积操作的计算复杂度是O(M²D)D是Embedding维度整体开销不大。再看外积。两个Embedding向量做外积得到一个D×D的矩阵矩阵中的每个元素都表示两个向量在特定维度上的乘积关系。相比内积外积明显保留了更多细粒度信息。但问题也来了C(M,2)个D×D矩阵如果直接展平拼接那维度是C(M,2)×D²M稍微大一点这个数字就会爆炸。所以OPNN在实际工程中很少把外积结果直接展平通常会引入额外的压缩结构来降维这个我后面会详说。2.4 为什么“乘积”能捕捉交互而不是加或者拼接有人可能问为什么特征交互要用“乘积”用加或者拼接不行吗这个问题的答案要从语义上说。两个特征相加表达的其实还是两个特征的线性叠加并没有产生新的联合信息。拼接也一样只是把两个向量放在一起它们之间的关系依然要靠后续网络去拟合。乘积则不一样它在单个运算内就让两个向量的每一对维度发生了数值上的互动相当于在特征空间中创造了一个新的联合语义。内积捕捉的是方向相似度是“这两个特征在语义上是否齐平”外积捕捉的是分量级别的交互是“特征A的第k个语义分量和特征B的第l个语义分量之间如何共现”。无论哪种都比简单的拼接更有信息量。这也是PNN能显著改善EmbeddingMLP对低频组合特征拟合能力不足问题的根本原因。另外顺带说一句现在大火的Transformer里的自注意力机制本质上也是在计算特征token之间的交互只不过用的是缩放点积注意力。从这个角度看PNN的乘积层算是显式特征交互建模的早期探索之一思路一脉相承只是没有引入query/key/value的变换和softmax归一化。3. IPNN与OPNN的取舍复杂度、效果和工程部署3.1 先算一笔复杂度账选IPNN还是OPNN第一步不是看论文里的效果数字而是先算复杂度账。假设特征字段数M50Embedding维度D16。IPNN的输出维度是C(50,2)1225个标量加上z部分的50×16800维乘积层输出大约2025维。这个维度后面接一两层256或128个神经元的全连接层计算量完全可控。再看OPNN输出是1225个16×16256维的矩阵如果全部展平拼接乘积层输出维度是1225×256313600维。要把这么大一个向量接进全连接层权重参数的规模是313600×256接近8000万个参数。这个参数量对于广告场景动辄上亿的样本量来说勉强能训但训练速度和内存开销都很不友好更别说线上推理时的延迟。所以工业界的常规做法是对外积矩阵做压缩处理。比如先对所有特征对的外积矩阵做一个逐元素的加和得到一个全局的D×D交互矩阵再对这个矩阵做展平或池化这样输出维度就从C(M,2)×D²降到了D²。这个方案计算量是O(MD² D²)比原始OPNN的O(M²D²)低了一个数量级工程上完全可行。3.2 效果对比OPNN的理论优势去哪了如果你去读原始论文会看到作者在多个数据集上的对比实验一个比较有意思的现象是IPNN和OPNN的AUC差距其实很小甚至在一些数据集上IPNN还略好一点。我在自己做的公开数据集复现实验中也观察到了相似的结论。为什么理论上表达信息更丰富的外积实际效果没有碾压内积我的理解有三点。第一点击率预估场景中的特征交互信号绝大多数是“有无型”的即两个特征是否在语义上相关而不是需要精确到分量级的细粒度交互这正好是内积擅长捕捉的。第二OPNN参数量更大在有限训练数据下更容易过拟合尤其是长尾特征组合本来就稀疏给模型更多参数反而学出噪声。第三OPNN为了控制维度必须做压缩压缩过程本身会丢失信息如果压缩方式不合适理论优势还没发挥出来就已经被抵消了。基于这些观察我的建议是第一版模型直接上IPNN。IPNN结构简单、训练稳定、推理开销小通常能在效果和效率之间取得很好的平衡。如果后续发现IPNN的离线AUC不够或者需要建模一些特定类型的高阶特征语义再尝试OPNN或更先进的交互结构不迟。3.3 OPNN的压缩方式与落地方案如果你的场景确实需要OPNN或者你想做对照组实验我推荐一个经过验证的工程实现方案第一先把所有Embedding做外积得到一个整体的交互张量维度是D×D。这一步相当于把所有特征对的外积信息聚合在一起避免了C(M,2)个矩阵爆炸的问题。第二对这个D×D的矩阵做展平或者按行/列做池化得到D²维或D维的向量。展平信息保留更完整池化则进一步压缩。第三在展平后的向量上接一层低秩全连接层把维度压到与IPNN的p维度相近的量级再与z拼接后输入上层网络。低秩全连接层的参数可以通过矩阵分解来减少进一步降低过拟合风险。我实测下来这个“全局外积和展平低秩全连接”的方案离线AUC和原始OPNN差距在0.1%以内但训练速度提升明显线上推理也能接受。如果你用的是TensorFlow或PyTorch这种实现方式也很容易向量化不需要写复杂的自定义算子。3.4 场景约束广告高并发下的延迟考量在广告推荐这类高并发场景里模型推理的算力成本和延迟直接决定了这个结构能不能上线。IPNN的乘法次数约是O(M²D)OPNN约是O(M²D²)。当D从16涨到32IPNN的乘法量级只涨一倍OPNN却要涨四倍当M从50涨到100两者都会成平方增长。所以特征字段数一旦比较多OPNN的工程代价会快速放大。我见过一些团队在上一版模型里用了很重的交互结构结果线上单次推理延迟比原来多了好几毫秒在高峰流量下不得不砍QPS或者扩充机器。如果你用的是PNN这类基础结构并且目标是在现有的服务资源下保持稳定的线上效果IPNN会比OPNN省出大量算力这部分算力还可以留给特征侧、模型侧的后续优化。4. PNN训练与工程落地那些论文里没写的细节把模型结构讲清楚只是第一步真正让PNN在业务里稳定产出效果靠的是训练和工程细节。这一节我把实践中最容易影响最终指标的几个点逐一说明。4.1 损失函数与类别不平衡处理PNN在点击率预估任务上用的是标准的二分类交叉熵损失。真正需要注意的是点击率场景里正负样本极不平衡的问题。如果不做任何处理模型会趋向于把大多数样本预测为负类整体的logloss看上去很低但AUC和线上实际效果都不会好。我的做法是负样本降采样。先把负样本按比例降到与正样本接近的量级然后在训练时给正样本加权权重大约是原始正负比的倒数。这里有一个容易踩的坑离线评估时一定要用未采样的原始样本分布去计算AUC和GAUC否则指标会虚高跟线上对不上。另外当你调整了负采样率模型预测的分数会系统性偏移。如果你想保留一个无偏的点击率分数比如用于后续的出价或阈值控制最好在训练后做一个简单的分数校正把后验点击率恢复到真实概率空间。4.2 Embedding维度的选择不要所有特征一刀切Embedding维度D对PNN的效果影响很大因为它同时影响特征表达能力和乘积层的交互质量。我在一个公开的广告数据集上做过一组实验D从8、16、32、64变化其他条件不变离线AUC呈现出“先快速上升、后趋于平稳、再略有下降”的形态。D8时表达能力明显不足D32和D64基本持平D128时AUC不再提升但参数量和训练时间显著增加。所以我建议第一轮训练把D设在16到32之间先把基线跑通然后根据特征基数做差异化调整。高基数的ID类特征可以给到64甚至128低基数的类别特征给到8就足够了。如果你用统一维度省事是省事但要么让低基数特征占了过多参数要么让高基数特征的表达能力不够两头不讨好。4.3 优化器、学习率与正则化策略PNN这类深度模型的训练对优化器的选择比较敏感。我用得最多的是Adam初始学习率设在0.001附近配合指数衰减或者余弦退火。如果训练日志里AUC曲线震荡剧烈我会把学习率降到0.0003重新跑或者加一个warm-up阶段让学习率从0缓慢拉升到目标值前几个epoch先稳定住Embedding的训练节奏。正则化方面我踩过一个明显的坑直接在Embedding层上加一个全局L2正则结果模型效果不升反降。原因在于低频特征的样本量本来就少L2把它往零向量方向压缩学出来的Embedding几乎失去表达能力乘积层的交互信号也就跟着废掉了。后来我把L2的主要力度放在全连接层权重上Embedding层仅靠Dropout和早停来控制过拟合效果明显改善。Dropout的比例我一般从0.2开始调。如果训练集AUC明显高于验证集AUC出现过拟合迹象就把Dropout加到0.4甚至0.5。要注意Dropout加在哪个位置也有讲究通常加在拼接z和p之后的全连接层输入处效果比只加在输出层要好得多。4.4 批大小与训练稳定性批大小的选择对PNN的收敛行为影响很大。我在分布式训练时一开始用了一个很大的batch size比如4096来提升吞吐结果模型收敛慢离线AUC比小batch版本低了将近0.3%。后来我改成先在小batch512到1024下确认模型效果再通过线性学习率缩放的方式逐步增大batch size才算把大batch训练的效果补回来。为什么大batch会伤害效果一个常见的解释是大batch降低了梯度的噪声让模型更容易收敛到尖锐的局部极小值区域泛化性变差。虽然有一些SGD的变体可以缓解这个问题但在实践中最简单的做法还是控制batch size在一个合理的区间不要盲目追求吞吐而牺牲精度。4.5 特征预处理与归一化PNN不是万能的最后说一个经常被忽略的事实PNN虽然把特征交互从人工变成了自动学习但它不负责解决原始特征的质量问题。在实操中连续特征如果不做分桶直接输入模型效果通常不如分桶后再Embedding。分桶相当于把连续变量的非线性区间切分出来给模型提供了一个先验的离散化边界这个先验即使对PNN也是有用的。缺失值处理也一样。广告场景里很多特征是有缺失的如果你在Embedding层直接设一个默认值向量去查表网络可能把这个默认值学成有意义的语义反而引入噪声。更稳的做法是给每个字段单独设一个“未知”取值和真实值分开处理。这些预处理工作在PNN里的收益往往比从IPNN换到OPNN大得多建议优先把精力放在这块。5. 横向对比从LR到Deep CrossingPNN的历史位置在哪5.1 与LR和FM的对比从人工叉乘到自动二阶交互逻辑回归是排序模型的起点把所有特征做线性加权求和。如果要捕获特征交互就必须人工做特征叉乘。因子分解机FM是第一个在结构上解决二阶交互建模的模型它用隐向量的内积来表示特征对的交互权重不再需要人工叉乘。但FM的交互是固定二阶的而且没有深层非线性网络来做更高阶的组合。PNN和FM的关系很有意思FM的二阶交互本质上是所有特征隐向量两两内积的加权和这一点和IPNN的乘积层非常相似。区别在于PNN把内积结果作为一种特征输入继续接多层全连接网络让模型具备高阶非线性拟合能力。某种程度上可以说IPNN可以看成是“FM二阶交互的深度化版本”。如果你熟悉FM的原理理解IPNN会非常快两者在交互的数学形式上是一脉相承的。5.2 与FNN的对比端到端学习的优势FNNFactorization-machine supported Neural Network是PNN之前提出的深度模型它用FM预训练出特征的低维Embedding然后把这些Embedding作为固定输入接入DNN网络做拟合。FNN的缺点在于Embedding在预训练阶段就已经固定了上半部分网络只能在这套固定的表示上做组合无法针对目标任务反馈优化底层特征表示。PnN则不同整个模型从Embedding到交互层、再从交互层到全连接层是端到端联合训练的。Embedding会在交互信号的监督下不断调整这就避免了“预训练表示限制了交互上限”的问题。5.3 与Deep Crossing的对比显式交互与隐式交互的分水岭Deep Crossing是微软提出的经典深度排序模型结构上是Embedding 残差网络 全连接层。它已经做到了端到端但特征交互完全依赖残差层的非线性变换来隐式学习。PNN与Deep Crossing最大的差异就是多了一个显式的乘积层。用大白话说Deep Crossing是“让网络自己去猜哪些特征组合有用”PNN是“先把所有两两组合算出来让网络在组合之上再学习”。后者多了一条显式的交互通道训练难度更低对数据稀疏性也更鲁棒。从特征工程演进的角度看Deep Crossing依然带有“通过模型结构隐式拟合”的色彩PNN则明确把“交互建模”变成了一等公民。5.4 五个模型的综合对比表模型特征交互方式显式二阶交互端到端训练高阶交互工程落地难度LR人工叉乘否是否低FM隐向量两两内积是是否低FNNFM预训练Embedding 隐式DNN部分否是中Deep Crossing隐式残差网络否是是中PNN显式乘积层 隐式DNN是是是中从表里能看出PNN是第一个同时具备“显式交互”和“端到端训练”的深度模型。这张表也解释了为什么2016年前后PNN能在工业界迅速铺开——它不是某一个维度上的激进创新而是在工程可行性、效果和可解释性之间取了一个非常好的平衡点。5.5 PNN的边界在哪里PNN也有局限性最明显的是它的交互层只建模二阶交互。虽然上层DNN能继续做高阶组合但这种高阶组合依然是隐式的未必能准确捕获三阶以上显式的联合效应。如果你的业务场景里存在很强的三阶交互比如“新用户 深夜时段 外卖广告”PNN可以通过多层非线性叠加来近似学习但效率可能不如专门的高阶交互模型比如DCN或者xDeepFM这类拥有显式高阶交互结构的网络。但这不意味着PNN过时了。在特征字段数量适中的场景里PNN的实现复杂度低、效果稳定完全值得作为主力baseline长期使用。我到现在做排序模型迭代时仍然会保留一个IPNN版本作为对照用它来判断新模型在交互建模上的增益到底有多少。6. 复现PNN的踩坑记录从特征对齐到数值稳定这一节我集中写一写复现PNN过程中遇到的各种坑以及对应的解决方案。这些经验主要来自我自己团队的项目实践不一定每个环境都适用但排查思路应该有参考价值。6.1 字段ID空间冲突所有问题的根源PNN训练的第一步就是为每个特征字段建独立的Embedding字典。最容易犯的错误是全局共享一个ID空间。举个例子用户性别字段里“男性”的ID是1商品品类字段里“家电”的ID恰好也是1如果共用一个Embedding表这两个完全无关的特征就会指向同一个向量交互信号自然全乱了。解决思路很简单每个特征字段单独建字典、单独分配Embedding表ID空间互相隔离。工程上建议在特征名后面加前缀比如user_gender_1和item_category_1避免数据管道里的ID冲突。这个坑在离线训练时通常不报错模型也能收敛但线上AUC会莫名低于预期排查起来非常费劲。最好在特征构建阶段就做好字段前缀和ID映射校验不要等到训练完了再回头查。6.2 Embedding维度不统一导致乘积层报错前面提到我建议按特征基数设置不同维度的Embedding但乘积层要求所有字段的向量维度一致否则无法做两两内积或外积。很多同学在实现时先用不同维度Embedding结果进入乘积层后直接报shape不匹配的错。正确的做法是在Embedding层之后加一个统一的映射层把每个字段的向量映射到同一个语义维度D上。这个映射层可以是一个普通的全连接层也可以用一个小型MLP。字段向量维度统一之后再进入乘积层计算。需要提醒的是这个映射层本身也参与训练不要在预训练阶段就把它固化下来。6.3 梯度爆炸与数值稳定LayerNorm是你要的好帮手PNN在字段数M较大时z和p拼接后的向量维度可能达到几千如果网络初始化不当梯度爆炸很容易发生尤其是Embedding维度偏大时。我建议做两步操作第一全连接层权重用Xavier或He初始化bias初始化为0。不要用默认的随机初始化否则训练前期容易出现NaN损失。第二在拼接后的向量输入全连接层之前加一层LayerNorm或BatchNorm。LayerNorm在特征交互类模型里非常有效它能把z和p拼接后的不同数值范围统一到同一尺度稳定训练的同时加快收敛。我在一次对比实验中加了LayerNorm之后达到相同AUC所需的epoch数减少了将近一半这一点新手可能没想到。6.4 在线推理时的Embedding查表优化PNN的推理过程性能瓶颈通常不在全连接层而在Embedding查表。线上请求的特征是稀疏的一个请求可能只命中几十个特征的ID如果每次请求都做全量Embedding矩阵的查表再mask浪费非常大。我用的优化方式是预先构建ID到Embedding索引的稀疏映射表线上按命中ID直接取出对应向量再进入乘积层计算。对于那些超高基数的ID类特征可以考虑用hash技巧压缩ID空间把任意长度ID映射到固定位数。压缩会引入碰撞概率但只要碰撞率控制在0.1%以下对离线AUC的影响几乎可以忽略工程收益却很可观。6.5 一个推荐的超参调整顺序最后分享一套我经常用的调参顺序这套思路适用于PNN也适用于其他类似的深度排序模型第一把特征工程做扎实保证字段覆盖、缺失值处理、连续特征分桶都到位。这步做不好后面所有调参都是白费。第二用一组保守的默认参数把PNN完整跑通确认训练流程、评估指标都正常。第三调整Embedding维度和全连接层的宽度深度找到参数量和效果的最佳平衡点。这一步通常对最终AUC的影响最大。第四再调Dropout、L2、学习率这些正则化相关超参。第五最后才考虑替换为OPNN、增加特征字段或者尝试更复杂的交互结构。我见过不少团队在一开始就把大量时间花在第五步上模型换来换去实际最大的收益反而来自第三步的Embedding维度和特征预处理。先把基础工作做扎实比追求模型结构的“高级”可靠得多。PNN不是银弹但如果你能把它吃透、调顺会发现它始终是一个值得信赖的baseline和后续复杂模型的最佳对照物。