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 总结
优点:
- 显式的进行特征交互,提高模型表达能力;
- 以 field 为粒度进行特征交互,保留的域的概念;
- 同时保留了低维与高维特征
缺点:
- 外积交互方式参数量较大,随着特征维度平方级增长;
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):
if K.ndim(inputs) != 3:
raise ValueError(
"Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))
field_num = inputs.shape[1]
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])
q = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), col), [1, 0, 2])
InnerProduct = tf.reduce_sum(p*q, axis=-1)
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
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):
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])
q = tf.transpose(tf.gather(tf.transpose(inputs, [1, 0, 2]), col), [1, 0, 2])
p = tf.expand_dims(p, axis=1)
tmp = tf.multiply(p, self.w)
tmp = tf.reduce_sum(tmp, axis=-1)
tmp = tf.multiply(tf.transpose(tmp, [0, 2, 1]), q)
OuterProduct = tf.reduce_sum(tmp, axis=-1)
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:]
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])
z = embed
embed = tf.reshape(embed, shape=(-1, embed.shape[1]*embed.shape[2]))
if self.mode=='inner':
inner_product = self.inner_product_layer(z)
inputs = tf.concat([embed, inner_product], axis=1)
elif self.mode=='outer':
outer_product = self.outer_product_layer(z)
inputs = tf.concat([embed, outer_product], axis=1)
elif self.mode=='both':
inner_product = self.inner_product_layer(z)
outer_product = self.outer_product_layer(z)
inputs = tf.concat([embed, inner_product, outer_product], axis=1)
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
希望看完此文的你,能够有所收获!
|