推荐算法(八)——显式特征交互模型 PNN

1 介绍

本文为 推荐系统专栏 的第八篇文章,内容围绕 PNN 的原理及代码展开。

PNN 出自上海交大,通过引入特征交互层 Product Layer,显式的对特征进行交互,以提升模型的表达能力。

在这里插入图片描述
论文传送门:Product-based Neural Networks for User Response Prediction

代码传送门:PNN

2 原理

在这里插入图片描述

2.1 Embedding Layer

该层为嵌入层,用于将 input 中的每个 Field 特征映射成低维稠密特征,然后每个特征的 embedding 横向拼接,作为下一层的输入。

2.2 Product Layer

该层为特征交互层,由 z 和 p 两部分组成,其中 z 为上层的输出结果,p 为上层输出的特征交互结果,低维与高维特征的直接拼接。

Product Layer 以 Field 为粒度进行特征之间的交叉,交叉方式有两种:内积 IPNN外积 OPNN

Embedding Layer输出的张量形状为 [None, field, k],其中 None 表示 batchsize 大小,field 为原始输入的特征个数,k 为每个特征嵌入之后对应稠密向量维度。

Inner Product:

filed 个特征两两进行内积,每两个 k 维特征的内积可得到一个一维变量,总共可得到 field * (field-1) / 2 个变量,拼接在一起即为特征交互的结果 p,形状为 [None,field * (field-1) / 2] 。

Outer Product:

与内积不同的是,每两个 k 维特征的外积不再是一个一维变量,而是形状为 [k, k] 的二维张量。

所以又引入等形状的权重矩阵 W,与二维张量进行对应元素乘积,然后求和得到一维变量。

文中引入 field * (field-1) / 2 个可训练的权重矩阵,与每个二维张量计算元素积,得到 field * (field-1) / 2 个变量,然后拼接得到特征交互的结果 p,形状跟内积结果相同,也为 [None,field * (field-1) / 2] 。

外积与内积的唯一差别,就是多了一次矩阵的元素积计算。


得到特征交互结果之后,与上层输出 z 拼接即为 Product Layer 层的输出。

Tips: 内积与外积两种特征交互方式可同时使用,把内积外积的交互结果横向拼接即可。

2.3 Hidden Layer

三层全连接,最后一层映射到一维接 sigmoid 得到概率输出,即为预测的 CTR 概率。

3 总结

优点:

  1. 显式的进行特征交互,提高模型表达能力;
  2. 以 field 为粒度进行特征交互,保留的域的概念;
  3. 同时保留了低维与高维特征

缺点:

  1. 外积交互方式参数量较大,随着特征维度平方级增长;

5 代码实践

Layer 搭建:

import tensorflow as tf
from tensorflow.keras.layers import Layer, Dense, Dropout, Flatten, Conv2D, MaxPool2D
import tensorflow.keras.backend as K

class DNN_layer(Layer):
    def __init__(self, hidden_units, output_dim, activation='relu', dropout=0.2):
        super().__init__()
        self.hidden_layers = [Dense(i, activation=activation) for i in hidden_units]
        self.output_layer = Dense(output_dim, activation=None)
        self.dropout_layer = Dropout(dropout)

    def call(self, inputs, **kwargs):
        if K.ndim(inputs) != 2:
            raise ValueError(
                "Unexpected inputs dimensions %d, expect to be 2 dimensions" % (K.ndim(inputs)))

        x = inputs
        for layer in self.hidden_layers:
            x = layer(x)
            x = self.dropout_layer(x)
        output = self.output_layer(x)
        return tf.nn.sigmoid(output)

class InnerProductLayer(Layer):
    def __init__(self):
        super().__init__()

    def call(self, inputs, **kwargs):  #[None, field, k]
        if K.ndim(inputs) != 3:
            raise ValueError(
                "Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))

        field_num = inputs.shape[1]

        # for循环计算点乘,复杂度高
        # InnerProduct = []
        # for i in range(field_num - 1):
        #     for j in range(i + 1, field_num):
        #         Inner = inputs[:, i, :] * inputs[:, j, :]  #[None, k]
        #         Inner = tf.reduce_sum(Inner, axis=1, keepdims=True)   #[None, 1]
        #         InnerProduct.append(Inner)
        # InnerProduct = tf.concat(InnerProduct, axis=1) #[None, field*(field-1)/2]

        # 复杂度更低,先将要相乘的emb找出,存为两个矩阵,然后点乘即可
        row, col = [], []
        for i in range(field_num - 1):
            for j in range(i + 1, field_num):
                row.append(i)
                col.append(j)
        p = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), row), [1, 0, 2]) # [None, pair_num, k]
        q = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), col), [1, 0, 2]) # [None, pair_num, k]
        InnerProduct = tf.reduce_sum(p*q, axis=-1)  # [None, pair_num]
        return InnerProduct

class OuterProductLayer(Layer):
    def __init__(self):
        super().__init__()

    def build(self, input_shape):
        self.field_num = input_shape[1]
        self.k = input_shape[2]
        self.pair_num = self.field_num*(self.field_num-1)//2

        # 该形状方便计算,每个外积矩阵对应一个,共pair个w矩阵
        self.w = self.add_weight(name='W', shape=(self.k, self.pair_num, self.k),
                                 initializer=tf.random_normal_initializer(),
                                 regularizer=tf.keras.regularizers.l2(1e-4),
                                 trainable=True)

    def call(self, inputs, **kwargs):  #[None, field, k]
        if K.ndim(inputs) != 3:
            raise ValueError(
                "Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))

        row, col = [], []
        for i in range(self.field_num - 1):
            for j in range(i + 1, self.field_num):
                row.append(i)
                col.append(j)
        p = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), row), [1, 0, 2])  # [None, pair_num, k]
        q = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), col), [1, 0, 2])  # [None, pair_num, k]
        p = tf.expand_dims(p, axis=1)  # [None, 1, pair_num, k] 忽略掉第一维,需要两维与w一致才能进行点乘

        tmp = tf.multiply(p, self.w)   # [None, 1, pair_num, k] * [k, pair_num, k] = [None, k, pair_num, k]
        tmp = tf.reduce_sum(tmp, axis=-1)   # [None, k, pair_num]
        tmp = tf.multiply(tf.transpose(tmp, [0, 2, 1]), q)  # [None, pair_num, k]
        OuterProduct = tf.reduce_sum(tmp, axis=-1)   # [None, pair_num]
        return OuterProduct

Model 搭建:

from layer import DNN_layer, InnerProductLayer, OuterProductLayer
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Embedding

class PNN(Model):
    def __init__(self, feature_columns, mode, hidden_units, 
    			 output_dim, activation='relu', dropout=0.2):
        super().__init__()
        self.mode = mode
        self.dense_feature_columns, self.sparse_feature_columns = feature_columns
        self.dnn_layer = DNN_layer(hidden_units, output_dim, activation, dropout)
        self.inner_product_layer = InnerProductLayer()
        self.outer_product_layer = OuterProductLayer()
        self.embed_layers = {
            'embed_' + str(i): Embedding(feat['feat_onehot_dim'], feat['embed_dim'])
            				   for i, feat in enumerate(self.sparse_feature_columns)
        }

    def call(self, inputs, training=None, mask=None):
        dense_inputs, sparse_inputs = inputs[:, :13], inputs[:, 13:]

        # sparse inputs embedding
        embed = [self.embed_layers['embed_{}'.format(i)](sparse_inputs[:, i])
                 for i in range(sparse_inputs.shape[1])]
        embed = tf.transpose(tf.convert_to_tensor(embed), [1, 0, 2])  #[None, field, k]

        z = embed  #[None, field, k]
        embed = tf.reshape(embed, shape=(-1, embed.shape[1]*embed.shape[2]))  # [None, field*k]
        # inner product
        if self.mode=='inner':
            inner_product = self.inner_product_layer(z)   # [None, field*(field-1)/2]
            inputs = tf.concat([embed, inner_product], axis=1)
        # outer product
        elif self.mode=='outer':
            outer_product = self.outer_product_layer(z)   # [None, field*(field-1)/2]
            inputs = tf.concat([embed, outer_product], axis=1)
        # inner and outer product
        elif self.mode=='both':
            inner_product = self.inner_product_layer(z)   # [None, field*(field-1)/2]
            outer_product = self.outer_product_layer(z)   # [None, field*(field-1)/2]
            inputs = tf.concat([embed, inner_product, outer_product], axis=1)
        # Wrong Input
        else:
            raise ValueError("Please choice mode's value in 'inner' 'outer' 'both'.")

        output = self.dnn_layer(inputs)
        return output

完整可运行的代码可在文末 Github 仓库中查看。

写在最后

下一篇预告: 推荐算法(九)——阿里经典深度兴趣网络 DIN

推荐算法Github 仓库:

Recommend-System-tf2.0

希望看完此文的你,能够有所收获!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值