CTR预估这个方向做了几年的人,大概率都绕不开一个词:特征交叉。无论是推荐系统还是广告系统,用户行为、物品属性、上下文信息最终都要转化成能落地的预估值,而怎么让模型真正“理解”特征之间的交互关系,是决定效果上限的关键。PNN网络(Product-based Neural Network)就是解决这个问题的一个经典方案,它提出了一种比简单拼接Embedding更直观、更强力的特征交叉方式:在进入深度网络之前,先用内积或外积把特征对“显式地”交叉一遍。这篇博客我打算从PNN的设计动机、网络结构、核心公式、代码实现到实际训练中踩过的坑,完整展开聊聊,希望对正在做推荐模型、或者刚接触深度CTR模型的朋友有帮助。
1. 为什么会出现PNN:从传统CTR模型到深度特征交叉
在讲PNN之前,得先理解当时(2016年前后)CTR预估模型遇到了什么问题。那个阶段主流的做法是LR(Logistic Regression)加人工特征组合,而FM(Factorization Machine)虽然通过隐向量的内积实现了二阶特征交叉,但它本质上还是线性模型,交叉的表达能力有限,而且只停留在二阶。
深度学习兴起后,大家开始思考怎么把神经网络用在CTR预估上。比较早的尝试是Deep Crossing模型:把所有特征的Embedding拼接成一个长向量,直接喂给多层全连接网络。这种做法的优点是结构简单,但问题也很明显——特征交叉完全依赖DNN去“隐式学习”。DNN对特征交叉的确有建模能力,但它是非结构化的、难以控制的,很多情况下需要非常深的网络和大量数据才能拟合出有效的交叉信息,实际工程中往往不够高效。
1.1 FM模型带来的启发
FM的核心是:对于每一对特征i和j,用两个隐向量Vi和Vj的内积来表示它们的交叉权重。这个设计妙在没有引入额外的全局参数,而是用特征自身的向量表达来计算交互,这样即使遇到训练中没出现过的特征组合,只要它们对应的Embedding存在,也能计算出交叉结果。这种“向量内积”的思路,直接启发了PNN的Product层设计。
不过我最初看FM的时候,心里也有个疑问:内积的表达能力够吗?两个向量做内积,得到的是一个标量,等于把两个向量的逐元素乘积全部加起来,这个标量只能表达一种“相似度”或者说“共现强度”。真实场景里,特征交叉的形式显然更丰富——比如用户性别为“男”和物品类目为“游戏”的交叉,和用户性别为“男”与物品类目为“运动”的交叉,它们的影响方向可能完全不同,用单一标量去表达难免有些粗糙。这也是后来OPNN(外积版本)出现的原因之一,外积能保留更多的交互信息,不再压缩成一个点。
1.2 FNN与Deep Crossing:交叉不够直观的问题
FNN(Factorization-machine supported Neural Network)的思路是用FM预训练好的隐向量来初始化DNN的Embedding层,再通过DNN做高阶交叉。这种做法在训练初期效果不错,因为Embedding有了一个相对合理的起点,但它仍然没有把“特征交叉”作为显式操作放入网络中,交叉还是要靠DNN自己慢慢学。而且FNN依赖FM的预训练,整个训练流程相对复杂,工业化部署时还要多维护一套模型。
Deep Crossing则是完全端到端,把所有特征Embedding拼接后交给残差网络或全连接网络。它的优势是不需要预训练,但问题同样存在:拼接操作本身没有生成任何新的交叉特征,信息要一直到后续隐层中才能慢慢组合。我做过一些对比实验,在数据量有限的场景下,Deep Crossing的表现往往还不如FM+人工特征,原因就在于DNN没有足够的样本去学习有效的特征交互模式。
PNN就是在这个背景下提出来的:与其让DNN隐式地、缓慢地学习特征交叉,不如在Embedding层和DNN之间加一个专门的Product层,先把特征两两之间的交互信号显式地计算出来,再把这些信号和原始Embedding一起送入DNN。这种设计把特征交叉从“黑盒摸索”变成了“白盒计算”,理论上更高效,实践上也确实有效。
1.3 PNN的核心思路:用product操作来建模特征交叉
PNN的“P”就是Product,它的核心创新在于:对任意两个特征域的Embedding,不再只是拼接,而是先做一个product操作(内积或外积),得到一组新的向量/标量作为交叉特征,再送入深度网络。
这里的思路可以类比成“预处理”:拼接相当于把原材料直接扔给厨师(DNN)自由发挥,而Product层相当于先由配菜师傅把食材按特定方式搭配好,再交给厨师加工。配菜师傅的搭配逻辑虽然简单(就是内积或外积),但它保证了交叉信息以显式的形式存在,不会被DNN在传递过程中稀释掉。
从后来的发展看,PNN这种“Embedding + Product + DNN”的架构,也成为了FNN、DeepFM、xDeepFM等一系列模型的共同范式。不同模型的区别,本质上就是“用什么方式在什么时候做特征交叉”的差异。
2. PNN网络整体架构与核心环节解析
2.1 网络整体结构:四层一输出
PNN的整体结构可以拆成四层加一个输出层,从底往上分别是:
- 输入层(稀疏特征):原始特征经过One-Hot或Multi-Hot编码,变成高维稀疏向量。例如用户ID、物品ID、类目ID、年龄分段等,每条特征对应一个特征域(Field)。
- Embedding层:每个特征域对应一个Lookup Table(嵌入矩阵),把稀疏的One-Hot向量映射成稠密的低维Embedding向量。
- Product层:这是PNN的精华。它将所有特征域的Embedding向量两两之间进行product操作,输出交叉后的结果向量,同时也会保留一份原始Embedding信息(相当于“一阶项”)。
- 全连接隐层(L1、L2等):把Product层的输出展平后,通过多层全连接网络进行高阶非线性变换。
- 输出层:用Sigmoid函数输出点击率/转化率的预估概率。
整体流程可以用一句话概括:原稀疏特征 → 稠密Embedding → 显式特征交叉 → 深度网络 → 预估概率。
我觉得这个结构最聪明的地方在于,Product层的位置恰到好处——它没有取代DNN,而是“辅助”DNN。如果只有Product层,那模型就退化成了类似FM的形式,高阶交叉学不了;如果取消Product层,那就退化成Deep Crossing。PNN是同时把“低阶显式交叉”和“高阶隐式交叉”塞进了同一个网络,各司其职。
2.2 Embedding层:把稀疏特征变成稠密向量
Embedding层的概念今天来看已经很普及了,但在PNN刚提出的那会儿,很多人还没有把它和“特征表达”深度联系起来。PNN中的Embedding层本质上是做了两件事:
第一,降维。原始稀疏特征动辄百万甚至千万维,直接喂给DNN是不现实的。Embedding把每个特征映射到一个低维空间(通常16~64维),相当于把“1-of-N”编码变成了“分布式表达”,维度大幅下降,同时保留了语义信息。
第二,为Product层的交叉计算提供基础。不管是内积还是外积,都需要向量化的输入。如果特征还是One-Hot形式,那两两内积绝大多数情况下都是0,计算交叉就失去了意义。只有转成稠密Embedding后,两两之间的内积/外积才有信息量。
这里有个细节值得注意:不同特征的维度不一定非要一样,但在PNN中,为了让内积/外积操作可行,通常会把所有特征的Embedding维度统一。如果有的特征特别稀疏、有的特别稠密,可以适当调整初始化方式,但维度必须对齐,否则代码层面会很麻烦。
2.3 Product层:PNN的“杀手锏”
Product层是PNN的核心,也是它和普通“Embedding+MLP”模型最不一样的地方。它的输入是N个特征域的Embedding向量(假设每个向量维度为M),输出则分两部分:
第一部分是线性项(z),也就是把每个特征域的Embedding向量拼接(或求和)后保留下来,相当于保留了每个特征本身的信息,可以理解为一阶特征。第二部分是交叉项(p),也就是对N个Embedding两两做product操作,得到的输出。最终Product层的输出是z和p拼接在一起(或者分别处理后相加),再喂给DNN。
其中交叉项p的计算方式有两种:
- 内积(IPNN):对特征i和j的Embedding做内积,得到标量p_ij = <fi, fj>。把所有特征对的标量拼接起来,形成p向量。
- 外积(OPNN):对特征i和j的Embedding做外积,得到矩阵p_ij = fi * fj^T(维度M×M)。把所有矩阵展平(或压缩)后拼接起来。
这两种方式各有优劣,后面我会详细对比。
Product层放在Embedding之后、DNN之前,实际上相当于人为地构造了一个“特征交叉池”。在传统LR里,特征交叉需要人工设计,成本极高;在FM里,只做二阶内积;而PNN把内积/外积的结果作为DNN的输入,DNN可以在其基础上继续学习更高阶的非线性交叉。这相当于既给了模型原材料,又给了模型一个半成品加工车间,效率自然不一样。
2.4 内积与外积的数学原理与选择
内积和外积听起来都是线性代数的基础操作,但在PNN的语境下,它们的含义和效果差异非常大。
内积的定义很简洁:两个同维向量逐元素相乘再求和,得到一个标量。它的本质是在度量两个向量的“方向一致性”。在推荐场景下,内积越大,表示两个特征向量在语义空间中的方向越接近,可以解释为“用户特征与物品特征越匹配”。FM用的就是内积,所以从数学原理上讲,IPNN可以看作FM在深度网络框架下的“增强版”——它不仅做了二阶内积,还把内积结果继续交给DNN处理,从而获得高阶交叉能力。
外积则不同:两个向量做外积,得到一个M×M的矩阵。这个矩阵包含了两个向量所有维度之间的两两乘积组合,信息量远大于标量。比如向量f1 = (a1, b1),向量f2 = (a2, b2),外积矩阵为:
[[a1a2, a1b2], [b1a2, b1b2]]
矩阵里既有a1和a2的交互,也有a1和b2、b1和a2、b1和b2的交互,可以说把交叉粒度细化到了向量内部的每个维度。从表达能力上讲,外积上限比内积高很多,这也是OPNN理论上更吸引人的原因。
但在实际工程中,我个人的经验是:外积虽然信息更丰富,却更容易导致过拟合和训练不稳。因为外积矩阵的维度是M×M,如果Embedding维度是64,那一个特征对就产生4096维的向量,N个特征域两两组合后,维度会变得非常夸张。而且很多外积矩阵的元素是稀疏的(因为Embedding向量中某些维度的值接近0),在DNN反向传播时,这些稀疏值对应的梯度也容易出现极端情况,导致训练不稳定。
所以在选择内积还是外积时,我的建议是:
- 如果数据量不大、特征规模较小,优先选IPNN。稳定、收敛快、实现简单。
- 如果数据量很大、且特征交互模式确实很复杂,可以尝试OPNN,但要做好正则化和梯度裁剪。
- 也可以做融合:部分强关联的特征域对内积,其他弱关联特征域用外积,但这样做实现复杂度会上升,需要根据业务情况权衡。
3. PNN的两种变体:IPNN与OPNN的实现细节
3.1 IPNN的公式与实现
IPNN(Inner Product-based Neural Network)的核心是:对任意两个特征域的Embedding做内积,得到标量p_ij,然后将所有的p_ij拼接成一个向量,与一阶项拼接后送入DNN。
假设有N个特征域,每个域的Embedding向量维度为M。那么特征对的数量是:
C(N, 2) = N(N-1)/2
如果N=10,特征对数量就是45;如果N=50,特征对数量就是1225。每个特征对内积后是一个标量,所以IPNN得到的交叉向量维度就是C(N, 2)维。
这个维度其实不算大(相比外积动辄M×M的维度),所以IPNN在工程上更容易处理。它的公式可以写成:
p = (p_12, p_13, ..., p_(N-1)N),其中p_ij = <Ei, Ej>
这里的Ei是第i个特征域的Embedding向量。
在实现的时候,有一种比较实用的做法:把N个Embedding向量堆叠成一个矩阵E(维度N×M),那么所有两两内积的结果可以直接用矩阵乘法E * E^T一次性算出来,得到的是一个N×N的对称矩阵。我们只需要取上三角(或下三角)的非对角元素,加上对角元素(或者去掉对角元素)作为交叉向量。
这里有一个小经验:IPNN的交叉向量是否包含“自身内积”(i=j的项)对效果影响不大,因为自身内积就是向量L2范数的平方,相当于给模型提供了一个特征重要性信息。但在实际实现时,通常取非对角元素就够了,这样既能减少冗余,又能降低计算量。
3.2 OPNN的公式与实现
OPNN(Outer Product-based Neural Network)则是把内积换成外积。对任意两个特征域的Embedding做外积,得到M×M的矩阵。将所有外积矩阵展平成向量后拼接,维度是C(N, 2) * M * M。
这个维度通常非常吓人。举个例子:N=10,M=64,那么外积向量的维度是:
C(10, 2) * 64 * 64 = 45 * 4096 = 184320维
接近18万维的向量直接输入DNN,第一层全连接的参数数量就是18万×隐层单元数,计算量和过拟合风险都极大。所以原始PPN论文以及很多实际实现中,对OPNN都会做一个压缩处理:把每个外积矩阵通过一个权重矩阵加权求和,压缩成一个k维向量(k远小于M×M),或者将外积矩阵先经过一个卷积层/池化层再来拼接。
在动手实现OPNN时,我建议把外积矩阵先做一次“全局平均池化”或“单层卷积”,将每个M×M矩阵压缩成一个标量或低维向量,再进入DNN。这样虽然损失了一部分信息,但能控制计算量,增加训练稳定性。
从我个人经验来看,OPNN在数据集比较大、Embedding维度适中(比如8或16)的时候,能获得优于IPNN的AUC。但一旦Embedding维度超过32,OPNN的参数量和过拟合风险就会显著上升,训练速度和效果都会受到很大影响。所以如果你决定用OPNN,建议Embedding维度初始化得小一点,并且配合Dropout使用。
3.3 完整Keras代码示例
理论讲了一大堆,直接上代码更直观。这里我写一个基于TensorFlow Keras的简化版PPN实现,包含IPNN和OPNN两种Product层。代码不是工业级最优,重心在于把核心逻辑讲清楚。
import tensorflow as tf from tensorflow.keras import layers, Model class ProductLayer(layers.Layer): def __init__(self, use_inner=True, use_outer=False, compress_dim=64): super().__init__() self.use_inner = use_inner self.use_outer = use_outer self.compress_dim = compress_dim def build(self, input_shape): # input_shape: (batch_size, num_fields, embed_dim) self.num_fields = input_shape[1] self.embed_dim = input_shape[2] if self.use_outer: # 外积压缩矩阵,将 M*M 压缩到 compress_dim self.outer_w = self.add_weight( shape=(self.embed_dim * self.embed_dim, self.compress_dim), initializer='glorot_uniform', trainable=True ) def call(self, inputs): # inputs: (batch, num_fields, embed_dim) field_emb = inputs batch_size = tf.shape(field_emb)[0] inner_outputs = [] outer_outputs = [] for i in range(self.num_fields): for j in range(i+1, self.num_fields): ei = field_emb[:, i, :] # (batch, embed_dim) ej = field_emb[:, j, :] if self.use_inner: inner_val = tf.reduce_sum(ei * ej, axis=-1, keepdims=True) # (batch, 1) inner_outputs.append(inner_val) if self.use_outer: outer_mat = tf.einsum('bi,bj->bij', ei, ej) # (batch, embed_dim, embed_dim) outer_flat = tf.reshape(outer_mat, (batch_size, -1)) # (batch, embed_dim*embed_dim) outer_val = tf.matmul(outer_flat, self.outer_w) # (batch, compress_dim) outer_outputs.append(outer_val) if self.use_inner: inner_concat = tf.concat(inner_outputs, axis=-1) # (batch, num_pairs) if self.use_outer: outer_concat = tf.concat(outer_outputs, axis=-1) # (batch, num_pairs * compress_dim) # 还要保留一份原始 embedding 作为一阶项 flatten_emb = tf.reshape(field_emb, (batch_size, -1)) # (batch, num_fields*embed_dim) result = [flatten_emb] if self.use_inner: result.append(inner_concat) if self.use_outer: result.append(outer_concat) return tf.concat(result, axis=-1) def build_pnn(item_num, cate_num, embed_dim=8, num_fields=3, use_inner=True, use_outer=False): # 假设三个特征域:item_id, cate_id, user_behavior_avg item_input = layers.Input(shape=(1,), name='item_id') cate_input = layers.Input(shape=(1,), name='cate_id') user_input = layers.Input(shape=(1,), name='user_feat') item_emb = layers.Embedding(item_num, embed_dim, name='item_emb')(item_input) cate_emb = layers.Embedding(cate_num, embed_dim, name='cate_emb')(cate_input) user_emb = layers.Embedding(100, embed_dim, name='user_emb')(user_input) # 将不同特征域的 embedding 合并成一个 tensor field_embeddings = tf.concat([item_emb, cate_emb, user_emb], axis=1) # (batch, 3, embed_dim) prod_out = ProductLayer(use_inner=use_inner, use_outer=use_outer)(field_embeddings) x = layers.Dense(64, activation='relu')(prod_out) x = layers.Dropout(0.3)(x) x = layers.Dense(32, activation='relu')(x) x = layers.Dropout(0.3)(x) output = layers.Dense(1, activation='sigmoid')(x) model = Model(inputs=[item_input, cate_input, user_input], outputs=output) return model model = build_pnn(item_num=10000, cate_num=100, embed_dim=8, use_inner=True, use_outer=False) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['AUC']) model.summary()上面这段代码里,我特意用了for循环来计算特征对,这样逻辑直观。实际工程中,如果特征域数量很多(比如超过20个),纯for循环的效率会比较低,建议把Embedding矩阵整理成一个大矩阵,用矩阵乘法一次性计算内积矩阵,或者用tf.vectorized_map来做batch维度上的并行。
另外要注意,这里的user_feat我用Embedding来表示,实际业务里用户侧的连续特征(如活跃天数、近7天消费金额)不需要Embedding,可以直接作为数值特征拼接到DNN层之前。这类特征不需要参与Product层的交叉,直接保留下来即可,否则会让模型参数膨胀不少。
4. 我踩过的坑:PNN训练中的常见问题与排查技巧
4.1 外积特征稀疏导致梯度爆炸
我第一次跑OPNN的时候,训练没几步就出现了loss变成NaN的情况。排查了半天,最后定位到是外积矩阵的数值范围问题:某些Embedding向量的初始值如果过大,外积矩阵的元素就会变得非常大,进而导致梯度爆炸。
外积的数学本质是向量元素两两相乘,它会把数值范围“放大”。如果两个向量中的元素值都在0.5左右,外积后矩阵元素可能在0.25左右;但如果元素值在2左右,外积矩阵就可能变成几十甚至上百,继续经过多层网络后极容易溢出。
解决办法有几种:
- Embedding初始化用小方差,比如用标准差为0.01的正态分布,而不是默认的0.1。
- 对Embedding向量做L2归一化,让它的模长固定为1,这样内积有界、外积矩阵元素也有界。
- 在Product层之后加BatchNormalization,强制把数据拉回正常范围。
- 使用梯度裁剪(clipnorm=1.0或5.0)。
我后来在线上稳定训练时,通常的组合是:Embedding初始化标准差0.01 + BatchNormalization + 梯度裁剪。三者配合,基本上可以杜绝外积导致的数值爆炸问题。
4.2 维度爆炸与计算开销
这是PPN,尤其是OPNN的“老毛病”。前面算过,N=10、M=64时,OPNN的输出维度可以到18万维。这种情况下,即使模型能训练,线上推理的开销也无法接受。我一开始在业务中直接用标准OPNN结构,结果单条样本的推理耗时从原来的1.2ms飙升到23ms,性能完全不可接受。
解决方案有三个方向:
第一,限制参与Product层的特征域数量。不是所有特征都值得做交叉,像“用户ID”和“物品ID”这种高基数特征,交叉后虽然有信息量,但没必要和“小时段”“星期几”这些低阶特征做外积。可以手动筛选重要的特征域参与Product,其他特征只保留一阶Embedding。
第二,用压缩矩阵降维。就像前面代码示例里的outer_w,把M×M的外积矩阵压缩到低维空间,维度可以从M×M降低到64甚至32。代价是模型表达能力的损失,但换来的是训练和推理速度数量级的提升。
第三,用内积替代外积。IPNN在大多数情况下已经能取得不错的效果,而且计算量小很多。如果业务迭代节奏快、需要频繁实验,先用IPNN验证思路,再决定是否升级到OPNN,是比较务实的路线。
4.3 一阶项的重要性
在调试PPN的过程中,我发现一个容易被忽视的细节:Product层输出的交叉项虽然重要,但一阶项(原始Embedding的拼接或求和)同样不可或缺。如果没有一阶项,网络就只能依赖交叉特征去预测,而交叉特征在很多情况下是稀疏的,会导致模型对高频特征过度拟合,对低频特征则几乎无法学习。
举个实际例子:一个物品在训练集里只出现过几十次,它的ID Embedding本身就学得不太好。这时如果只保留它与其他特征的交叉项,噪声会被放大。而如果有一阶项作为“兜底”,模型至少能直接从该物品的Embedding本身获取预测信号,效果会稳定很多。
我在代码实现里始终保留了flatten_emb作为一阶项,也推荐大家这么做。有些开源实现会把一阶项和二阶项融合到同一个DNN输入里,有些则会把一阶项单独用一个线性模型并行输出,两种方式都可以。但千万不要去掉一阶项,这是我在多次消融实验里验证过的结论:去掉一阶项,AUC平均下降0.3~0.5个百分点,这个幅度在CTR场景中已经算非常显著了。
4.4 特征域顺序与Embedding维度对齐
PPN对输入特征域的排列顺序及其Embedding维度有一个“隐性要求”:所有参与Product的特征域,其Embedding维度必须一致,否则内积/外积操作无法进行。这一点在代码实现中一定要写断言来检查,否则维度不匹配的报错信息会很绕。
另外,特征域的排列顺序会影响内积矩阵的结构,但理论上不影响最终效果,因为DNN可以自行学到不同特征对的重要性。不过在调试时,固定顺序会更方便排查。我个人习惯在数据管道中先定义好field的顺序,并在训练和预测阶段保持一致。
这里有一个容易踩的坑:如果某个特征域是Multi-Hot(比如用户历史点击的多个物品ID),它的Embedding输出是一个(batch, seq_len, embed_dim)的序列,不能直接和普通特征域的(batch, embed_dim)向量做内积。遇到这种情况,通常需要对序列Embedding做池化(sum pooling或attention pooling),先压缩成一个向量,再参与Product层计算。
5. 性能评估与建模经验:PNN到底好不好用,以及与后续模型的比较
5.1 离线评估指标与我的习惯
评估PPN模型效果时,离线阶段我一般优先看AUC,其次看GAUC(Group AUC)。AUC反映整体排序能力,但CTR场景中用户群体差异很大,GAUC按用户分组计算AUC再按曝光加权平均,更能反映线上实际的排序质量。
在实际对比测试中,IPNN相比Deep Crossing(Embedding+MLP)通常能带来0.5~1.5个百分点的AUC提升,这个幅度相当可观。OPNN在数据量大的场景下,相比IPNN还能再提升0.1~0.3个百分点,但代价是参数量和训练时间的显著增加。所以我的建议是:先做IPNN,把它调优到位,再考虑是否升级到OPNN。
除了AUC,我还习惯同时看RIG(Relative Information Gain)或LogLoss的变化。这两个指标在数据分布变化时会比AUC更敏感,有助于发现过拟合问题。特别是使用OPNN时,如果训练集RIG持续上升但验证集RIG开始下降,那就说明过拟合了,需要增加Dropout比例或降低Embedding维度。
5.2 线上A/B测试经验
离线效果好,线上不一定好,这是做推荐模型的人都懂的“鬼故事”。PPN上线的过程中,我遇到过两个典型的线上问题。
第一是特征覆盖度不足导致的“冷启动”现象。PPN对特征交叉的表达依赖Embedding质量,如果某些新物品的Embedding没有充分训练,交叉项的贡献会变成噪声,甚至拉低线上指标。解决方法是给这类稀疏特征加一个“置信度”权重,或者在特征侧增加统计特征(如点击率、曝光量)来辅助预测。
第二是推理性能。OPPNN在离线测试时P99耗时勉强达标,上到生产环境后,因为并发量的增加,系统CPU直接被打满。后来做了两个优化:一是把外积矩阵的压缩层提前到Embedding层之后,利用该层权重矩阵的低秩分解来近似计算外积,大幅减少乘法次数;二是把模型切分成两段,第一段(Embedding+Product)在线下预计算好结果存入缓存,线上只计算后续DNN部分。这两个优化把P99耗时降回了3ms以内,线上指标也稳定住了。
5.3 PNN和DeepFM、DCN、xDeepFM等模型的关系
PNN出现之后,特征交叉方向又涌现了不少模型,了解它们之间的差异,能帮你更好地理解PPN的定位。
DeepFM可以看作是“FM + Deep”的并行结构,它的FM部分专门负责二阶特征交叉,Deep部分负责高阶隐式交叉。与PPN相比,DeepFM更强调低阶项和高阶项的解耦,而且它的FM部分天然支持在稀疏特征上直接做交叉,实现上更加简洁。从实验结果看,DeepFM在大多数公开数据集上与IPNN效果相当,但实现难度更低,所以目前工业界的应用更广泛。
DCN(Deep & Cross Network)的核心是设计了Cross Network,用多层交叉进行高阶特征交叉。它和PPN的区别在于:PPN只做了一次显式二阶交叉,后续高阶交叉交给DNN;而DCN是每一层都在做特征交叉,交叉的阶数逐层递增。DCN对高阶交叉的表达能力更强,在特征间关系复杂的场景下效果往往更好。
xDeepFM则是在DCN基础上,用向量级的外积替代了标量级的交叉,同时引入了CIN(Compressed Interaction Network)来显式学习高阶特征交互。从表达力上讲,xDeepFM和OPNN有些类似,但xDeepFM用压缩交互的方式,让高阶交叉的计算变得更加可控。
如果你是在做工程落地,我的建议是:用IPNN或DeepFM作为基线模型,快速验证特征交叉带来的增益;如果业务对高阶交叉的需求很明确(比如用户行为序列很长、特征分布很复杂),再考虑DCN或xDeepFM这类更复杂的模型。PNN最大的价值在于,它帮你理解了“显式特征交叉”为什么重要,以及“内积/外积”这种基础操作在特征交互建模中的意义。
从我个人这些年的实践经验来看,PPN最大的贡献不是它本身的线上效果有多极致,而是它把“特征交叉”这件事从人工设计变成了网络结构设计,为后来各种深度CTR模型打下了方法论基础。如果你正在做推荐系统、广告排序或搜索排序,花时间把PPN的结构、内积/外积的取舍、一阶项的作用彻底搞明白,绝对不亏。而且按这个思路去理解和调试DeepFM、xDeepFM这些后续模型,也会顺手很多。