PNN 模型架构简介

589 阅读5分钟

实验和完整代码

完整代码实现和jupyter运行:github.com/Myolive-Lin…

引言

PNN(Product-based Neural Network)模型同样是为了解决 CTR(点击率)预估和推荐系统中的问题而提出的。与其他模型相比,PNN 模型在捕捉特征之间的交叉信息上有独特的优势。本文将详细介绍 PNN 模型的架构、数学模型及其主要特点。


1. 模型背景

在推荐系统中,如何有效地捕捉用户与物品之间以及特征之间的复杂交互关系一直是核心问题。传统模型通常采用简单的特征拼接方式,而 PNN 模型则利用乘积层(Product Layer) 来对不同特征的 Embedding 向量进行两两交互,从而更有针对性地提取交叉特征信息,提升模型的表现。


2. 模型架构概述

PNN 模型的整体架构与其他深度神经网络模型(例如 Deep Crossing 模型)类似,主要包含输入层、Embedding 层、多层全连接神经网络以及最终的输出层。唯一的区别在于:

  • 乘积层(Product Layer):
    在 Deep Crossing 模型中,特征的 Embedding 向量是简单地拼接后送入全连接层,而 PNN 模型引入了乘积层,用于计算不同特征 Embedding 向量之间的交互项。

    “PNN 模型在输入、Embedding 层、多层神经网络中与其他模型大体相似,唯一的区别在于 PNN 的中间层部分采用了乘积层(Product Layer),代替了传统模型中的简单拼接方式,从而使不同特征的 Embedding 向量能够进行两两交互,更有针对性地捕捉特征之间的交叉信息。”

  • 结构图参考:

image.png

下面我们从数学模型的角度具体介绍 PNN 模型的各个部分。

3. PNN 模型的数学描述

3.1 输入与 Embedding 层

输入层的主要任务是接收原始特征。通常推荐系统中的特征主要为离散型的,例如用户 ID、物品 ID、类别等。假设有 m个离散特征,每个特征需要进行后续的嵌入处理。输入层将这些特征进行编码(例如 one-hot 编码或稀疏编码),为后续的 Embedding 层提供基础数据。

关键点

  • 数据预处理阶段通常包括归一化、缺失值填充等工作。
  • 离散特征在输入层被编码为稀疏向量,便于 Embedding 层进行查表操作。

假设有 m个离散特征,每个特征经过 Embedding 后映射到 Rd\mathbb{R}^d 的向量空间中。令第 i个特征的 Embedding 表示为:

xiRd,i=1,2,,m\mathbf{x}_i \in \mathbb{R}^d, \quad i = 1, 2, \dots, m

通常将所有特征的 Embedding 向量拼接形成原始输入向量:

x=[x1,x2,,xm]Rm×d\mathbf{x} = [\mathbf{x}_1, \mathbf{x}_2, \dots, \mathbf{x}_m] \in \mathbb{R}^{m \times d}

3.2 乘积层(Product Layer)

乘积层由以下两部分组成:

1.线性操作部分(z部分)

这一部分对各特征向量进行线性拼接。假设我们有 n 个特征,每个特征的Embedding维度为 d,则线性操作部分的输出可以表示为一个 n×dn×d 的矩阵 ZZ

2.乘积操作部分(p部分)

这一部分通过特征向量之间的两两交互来捕捉特征之间的交叉信息。根据交互方式的不同,PNN模型分为两种变体:

  1. 内积(Inner Product):
    对任意两个特征 xi\mathbf{x}_ixj\mathbf{x}_j,计算其内积:

    pij=xi,xj=xixjp_{ij} = \langle \mathbf{x}_i, \mathbf{x}_j \rangle = \mathbf{x}_i^\top \mathbf{x}_j

    内积反映了两个特征之间的相似程度。

  2. 外积(Outer Product):
    有时也可以考虑外积操作,将两个向量的交互信息显式表达为一个矩阵:

    Pij=xixjPij=xi \odot xj

    外积操作的结果是一个 d×dd×d 的矩阵,表示两个特征向量之间的交互信息。这种方式能更全面地捕捉特征之间的交互关系,不过计算复杂度较高,通常需要经过降维处理。

    对于降维,还可以通过与同样维度(dd)(d *d)维的w权重矩阵进行对应位置相乘再相加得到标量的方式进行。

论文介绍了一种外积降维方法:

p=i=1Nj=1Ngouter(fi,fj)=i=1Nj=1NfifjT=ffTwhere:f=i=1Nfip = \sum_{i=1}^{N}\sum_{j=1}^{N}g_{outer}(f_i,f_j) = \sum_{i=1}^{N}\sum_{j=1}^{N}f_if_j^T = f_{\sum}f_{\sum}^T \\where :f_{\sum} =\sum_{i=1}^{N}f_i

在实际应用中,上述操作需要谨慎使用。直接对不同特征的Embedding进行平均,隐含假设这些特征在各维度上具有相似语义,而实际上可能并非如此。例如,年龄和天气的Embedding通常不具备直接比较的基础,简单平均会削弱关键信息。


在实际应用中,常见的做法是计算所有两两特征的内积,形成一个交叉特征向量:

p=[p12,p13,,p(m1)m]Rm(m1)2\mathbf{p} = [p_{12}, p_{13}, \dots, p_{(m-1)m}] \in \mathbb{R}^{\frac{m(m-1)}{2}}

注意事项

  • 乘积层设计需要权衡计算复杂度和信息量,内积计算相对简单,而外积能够捕捉更多维度的交互,但通常成本更高。
  • 部分实现中,也会引入参数化的乘积层,通过学习权重对不同交互项进行加权,以进一步提升模型表达能力。

乘积层的后续处理

在乘积层内部,PNN模型还进行了局部全连接层的转换。具体来说,线性部分 Z 和乘积部分 P 分别被映射成 D1​ 维的输入向量 lz​ 和 lp​,然后将这两个向量叠加,送入后续的全连接层。这部分操作可以表示为:

l1=relu(lz+lp+b1)l1​=relu(lz​+lp​+b1​)

其中,b1b1 是偏置向量,relurelu 是激活函数。

3.3 全连接层及非线性变换

接下来,模型通过多层全连接网络(MLP)进行特征变换。对于第 l 层,全连接层的计算公式为:

zl=f(Wlzl1+bl),l=1,2,,L\mathbf{z}_l = f\left( \mathbf{W}_l \mathbf{z}_{l-1} + \mathbf{b}_l \right), \quad l = 1, 2, \dots, L

其中,Wl\mathbf{W}_lbl\mathbf{b}_l 分别为第 l 层的权重矩阵和偏置向量,f()f(\cdot) 为非线性激活函数(例如 ReLU、sigmoid 等)。

3.4 输出层

在最后一层全连接网络之后,PNN 模型会将输出映射到最终的预测值上。对于 CTR 预估任务,通常使用 Sigmoid 函数来输出预测概率:

y^=σ(wzL+b)\hat{y} = \sigma\left( \mathbf{w}^\top \mathbf{z}_L + b \right)

其中,w\mathbf{w} 和 b 为输出层的权重和偏置,#\sigma(\cdot)# 表示 Sigmoid 激活函数,确保输出在 [0,1]范围内。


代码实现

乘积层实现:

class ProductLayer(nn.Module):
    def __init__(self, num_fields, embed_dim, l1_hidden_dim,mode='inner'):
        super().__init__()
        self.mode = mode
        self.num_fields = num_fields
        self.bias = torch.nn.Parameter(torch.zeros(1))
        
        if mode == 'outer':
            # 外积模式需要参数矩阵降维
            self.weight = nn.Parameter(torch.randn(embed_dim, embed_dim))
        
        # 线性部分权重 (保留原始Embedding)
        self.linear_weights = nn.Linear(num_fields * embed_dim, num_fields * embed_dim)

        self.lz_fc = nn.Linear(num_fields * embed_dim, l1_hidden_dim)
        self.lp_fc = nn.Linear(num_fields * (num_fields-1) // 2, l1_hidden_dim)
        
    def forward(self, embeddings):
        """
        Input: 
            embeddings: List of tensors [B, embed_dim] * num_fields
        Output:
            product_output: Tensor [B, output_dim]
        """
        batch_size = embeddings[0].size(0)
        stacked = torch.stack(embeddings, dim=1)  # [B, num_fields, embed_dim]
        
        # 线性部分
        linear_part = stacked.view(batch_size, -1)  # [B, num_fields*embed_dim]
        linear_out = self.linear_weights(linear_part)
        
        # 交互部分
        if self.mode == 'inner':
            # 内积模式:所有向量两两点积
            inner_products = []
            for i in range(self.num_fields):
                for j in range(i+1, self.num_fields):
                    ip = torch.sum(embeddings[i] * embeddings[j], dim=1, keepdim=True)
                    inner_products.append(ip)
            product_out = torch.cat(inner_products, dim=1)  # [B, C(num_fields,2)]
            
        elif self.mode == 'outer':
            # 外积模式:外积矩阵压缩
            outer_products = []
            for i in range(self.num_fields):
                for j in range(i+1, self.num_fields):
                    op = torch.bmm(embeddings[i].unsqueeze(2),  # [B, embed_dim, 1]
                                 embeddings[j].unsqueeze(1))    # [B, 1, embed_dim]
                    
                    #op:# [B, embed_dim, embed_dim], self.weight # [embed_dim, embed_dim]
                    op = (op * self.weight).sum(dim = (1,2), keepdim = True).squeeze(1)  # 降维,将对位运算的结果相加,并且保持维度为[B,1]
                    outer_products.append(op)
            product_out = torch.cat(outer_products, dim=1)
        
        # 合并线性与交互部分
        lz_out = self.lz_fc(linear_out)
        lp_out = self.lp_fc(product_out)
        combined = lz_out + lp_out
        combined += self.bias
        return torch.relu(combined)

PNN实现

class PNN(nn.Module):
    def __init__(self,fields_feature, embed_dim, hidden_dims, mode = 'inner'):
        """
        fields_feature: list, 每个特征域的类别数目
        embed_dim: 嵌入的维度
        hidden_dims: list, 隐藏层维度
        mode: 计算方式
        """
        super().__init__()
        #Embedding层
        self.embeddings = nn.ModuleList([
            nn.Embedding(input_dim, embed_dim) for input_dim in fields_feature
        ])

        # 乘积层
        self.product_layers = ProductLayer(len(fields_feature), embed_dim, hidden_dims[0],mode)

        # 全连接层
        product_dim = hidden_dims[0]

        fc_layer = []
        for hidden_dim in hidden_dims:
            fc_layer.append(nn.Linear(product_dim, hidden_dim))
            fc_layer.append(nn.ReLU())
            product_dim = hidden_dim
        self.fc = nn.Sequential(*fc_layer)

        # 输出层
        self.output_layer = nn.Linear(hidden_dims[-1], 1)

    def forward(self,field):
        embeds = [self.embeddings[i](field[:, i]) for i in range(len(field[0]))]
        product_output = self.product_layers(embeds)
        fc_output = self.fc(product_output)
        output = self.output_layer(fc_output)
        return torch.sigmoid(output)

        

5. 总结

PNN 模型通过在传统深度神经网络中加入乘积层,有效解决了特征交叉信息的捕捉问题。其数学模型主要包含:

  • Embedding 层:将离散特征映射到连续向量空间;
  • 乘积层:通过内积或外积操作计算两两特征交互;
  • 多层全连接网络:对拼接后的特征进行深度变换;
  • 输出层:利用 Sigmoid 等激活函数给出最终预测。

这种结构不仅提升了模型在 CTR 预估中的表现,也为其他推荐系统任务提供了有力的工具。未来的工作可以在乘积操作和特征交互上进行更多探索,以进一步优化推荐效果。


Reference

  1. 推荐系统(二):PNN源论文整理和思考
  2. Qu, Y., Cai, H., Ren, K., Zhang, W., Yu, Y., Wen, Y., & Wang, J. (2016). Product-based Neural Networks for User Response Prediction
  3. 王喆 《深度学习推荐系统》
  4. 计算广告CTR预估系列(八)--PNN模型理论与实践