推荐系统遇上深度学习 (六)--PNN 模型理论和实践



转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com

AIQ 机器学习大数据 知乎专栏 点击关注

原文发布于微信公众号 - 小小挖掘机(wAIsjwj)
原文发表时间:2018-04-29

1、原理

PNN,全称为 Product-based Neural Network,认为在 embedding 输入到 MLP 之后学习的交叉特征表达并不充分,提出了一种 product layer 的思想,既基于乘法的运算来体现体征交叉的 DNN 网络结构,如下图:

2aa7c87076fd46dc94c7f488970a158f.png

按照论文的思路,我们也从上往下来看这个网络结构:

输出层
输出层很简单,将上一层的网络输出通过一个全链接层,经过 sigmoid 函数转换后映射到 (0,1) 的区间中,得到我们的点击率的预测值:

ae39fc9a1a444b79900c937e53a9f7d1.png

l2 层
根据 l1 层的输出,经一个全链接层 ,并使用 relu 进行激活,得到我们 l2 的输出结果:
71ee082e26e54758890cb314aa0e6d03.png

l1 层
l1 层的输出由如下的公式计算:

de4b2ac1cc1a437490ffc56daa39f9f0.png

重点马上就要来了,我们可以看到在得到 l1 层输出时,我们输入了三部分,分别是 lz,lp 和 b1,b1 是我们的偏置项,这里可以先不管。lz 和 lp 的计算就是 PNN 的精华所在了。我们慢慢道来

Product Layer

product 思想来源于,在 ctr 预估中,认为特征之间的关系更多是一种 and“且”的关系,而非 add" 加”的关系。例如,性别为男且喜欢游戏的人群,比起性别男和喜欢游戏的人群,前者的组合比后者更能体现特征交叉的意义。

product layer 可以分成两个部分,一部分是线性部分 lz,一部分是非线性部分 lp。二者的形式如下:

3ff7b951dd01440eac8fceaa0c4a34cc.png

在这里,我们要使用到论文中所定义的一种运算方式,其实就是矩阵的点乘啦:

247ba9e0642645aba906ffa9ffe64e6c.png

我们先继续介绍网络结构,有关 Product Layer 的更详细的介绍,我们在下一章中介绍。

Embedding Layer

Embedding Layer 跟 DeepFM 中相同,将每一个 field 的特征转换成同样长度的向量,这里用 f 来表示。

3c41e2c859ab4d4da332c5011c2a1e7d.png

损失函数
使用和逻辑回归同样的损失函数,如下:

fab164bf2e8d4df3a97f818b2d0a30a3.png

2、Product Layer 详细介绍

前面提到了,product layer 可以分成两个部分,一部分是线性部分 lz,一部分是非线性部分 lp。

8a12fefa00aa4d5aa798c765fbb8e65b.png

看上面的公式,我们首先需要知道 z 和 p,这都是由我们的 embedding 层得到的,其中 z 是线性信号向量,因此我们直接用 embedding 层得到:

5dd32817bc48409998a0dbdf4d0873e3.png

论文中使用的等号加一个三角形,其实就是相等的意思,你可以认为 z 就是 embedding 层的复制。

对于 p 来说,这里需要一个公式进行映射:

576b50dd4a064fe9b720d03d9ad0cb4e.png

60f4abd4c32341669f4e25c976ae83ec.png

不同的 g 的选择使得我们有了两种 PNN 的计算方法,一种叫做 Inner PNN,简称 IPNN,一种叫做 Outer PNN,简称 OPNN。

接下来,我们分别来具体介绍这两种形式的 PNN 模型,由于涉及到复杂度的分析,所以我们这里先定义 Embedding 的大小为 M,field 的大小为 N,而 lz 和 lp 的长度为 D1。

2.1 IPNN

IPNN 的示意图如下:

d832bef0aa8b497fbae9f410e1a760c7.png

IPNN 中 p 的计算方式如下,即使用内积来代表 pij:

b5af93bc06a246cabd520bb5d73dcb94.png

所以,pij 其实是一个数,得到一个 pij 的时间复杂度为 M,p 的大小为 NN,因此计算得到 p 的时间复杂度为 NNM。而再由 p 得到 lp 的时间复杂度是 NND1。因此 对于 IPNN 来说,总的时间复杂度为 NN(D1+M)。文章对这一结构进行了优化,可以看到,我们的 p 是一个对称矩阵,因此我们的权重也可以是一个对称矩阵,对称矩阵就可以进行如下的分解:

8d4f6915282248a88799ffa18d4070da.png

因此:

61b5e7c54f54424d95582ba6ce7a077c.png

9b914b36c01c4a69a920941be81338d2.png

因此:

e38f0ad612354d93be06c63833d51f6c.png

从而得到:

ba3e8995678f4be7a2f121e827af056c.png

可以看到,我们的权重只需要 D1 * N 就可以了,时间复杂度也变为了 D1MN。

2.2 OPNN

OPNN 的示意图如下:

f4f27ead82ea44f1babe881c1fbe9685.png

OPNN 中 p 的计算方式如下:

4b4d531c77914f22b5b421e14ac704b0.png

此时 pij 为 MM 的矩阵,计算一个 pij 的时间复杂度为 MM,而 p 是 NNMM 的矩阵,因此计算 p 的事件复杂度为 NNMM。从而计算 lp 的时间复杂度变为 D1 * NNM*M。这个显然代价很高的。为了减少负责度,论文使用了叠加的思想,它重新定义了 p 矩阵:

fff513f91616460288761e8587036779.png

这里计算 p 的时间复杂度变为了 D1M(M+N)

3、代码实战

终于到了激动人心的代码实战环节了,一直想找一个实现比较好的代码,找来找去 tensorflow 没有什么合适的,倒是 pytorch 有一个不错的。没办法,只能自己来实现啦,因此本文的代码严格根据论文得到,有不对的的地方或者改进之处还望大家多多指正。

本文的 github 地址为:
https://github.com/princewen/tensorflow_practice/tree/master/Basic-PNN-Demo.

本文的代码根据之前 DeepFM 的代码进行改进,我们只介绍模型的实现部分,其他数据处理的细节大家可以参考我的 github 上的代码.

模型输入

模型的输入主要有下面几个部分:

self.feat_index = tf.placeholder(tf.int32,
                                 shape=[None,None],
                                 name='feat_index')
self.feat_value = tf.placeholder(tf.float32,
                               shape=[None,None],
                               name='feat_value')

self.label = tf.placeholder(tf.float32,shape=[None,1],name='label')
self.dropout_keep_deep = tf.placeholder(tf.float32,shape=[None],name='dropout_deep_deep')

feat_index 是特征的一个序号,主要用于通过 embedding_lookup 选择我们的 embedding。feat_value 是对应的特征值,如果是离散特征的话,就是 1,如果不是离散特征的话,就保留原来的特征值。label 是实际值。还定义了 dropout 来防止过拟合。

权重构建

权重由四部分构成,首先是 embedding 层的权重,然后是 product 层的权重,有线性信号权重,还有平方信号权重,根据 IPNN 和 OPNN 分别定义。最后是 Deep Layer 各层的权重以及输出层的权重。

对线性信号权重来说,大小为 D1 * N * M
对平方信号权重来说,IPNN 的大小为 D1 * N,OPNN 为 D1 * M * M。

def _initialize_weights(self):
    weights = dict()

    #embeddings
    weights['feature_embeddings'] = tf.Variable(
        tf.random_normal([self.feature_size,self.embedding_size],0.0,0.01),
        name='feature_embeddings')
    weights['feature_bias'] = tf.Variable(tf.random_normal([self.feature_size,1],0.0,1.0),name='feature_bias')


    #Product Layers
    if self.use_inner:
        weights['product-quadratic-inner'] = tf.Variable(tf.random_normal([self.deep_init_size,self.field_size],0.0,0.01))
    else:
        weights['product-quadratic-outer'] = tf.Variable(
            tf.random_normal([self.deep_init_size, self.embedding_size,self.embedding_size], 0.0, 0.01))



    weights['product-linear'] = tf.Variable(tf.random_normal([self.deep_init_size,self.field_size,self.embedding_size],0.0,0.01))
    weights['product-bias'] = tf.Variable(tf.random_normal([self.deep_init_size,],0,0,1.0))
    #deep layers
    num_layer = len(self.deep_layers)
    input_size = self.deep_init_size
    glorot = np.sqrt(2.0/(input_size + self.deep_layers[0]))

    weights['layer_0'] = tf.Variable(
        np.random.normal(loc=0,scale=glorot,size=(input_size,self.deep_layers[0])),dtype=np.float32
    )
    weights['bias_0'] = tf.Variable(
        np.random.normal(loc=0,scale=glorot,size=(1,self.deep_layers[0])),dtype=np.float32
    )


    for i in range(1,num_layer):
        glorot = np.sqrt(2.0 / (self.deep_layers[i - 1] + self.deep_layers[i]))
        weights["layer_%d" % i] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i - 1], self.deep_layers[i])),
            dtype=np.float32)  # layers[i-1] * layers[i]
        weights["bias_%d" % i] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])),
            dtype=np.float32)  # 1 * layer[i]


    glorot = np.sqrt(2.0/(input_size + 1))
    weights['output'] = tf.Variable(np.random.normal(loc=0,scale=glorot,size=(self.deep_layers[-1],1)),dtype=np.float32)
    weights['output_bias'] = tf.Variable(tf.constant(0.01),dtype=np.float32)


    return weights
	

Embedding Layer

这个部分很简单啦,是根据 feat_index 选择对应的 weights[‘feature_embeddings’] 中的 embedding 值,然后再与对应的 feat_value 相乘就可以了:

# Embeddings
self.embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'],self.feat_index) # N * F * K
feat_value = tf.reshape(self.feat_value,shape=[-1,self.field_size,1])
self.embeddings = tf.multiply(self.embeddings,feat_value) # N * F * K

Product Layer 根据之前的介绍,我们分别计算线性信号向量,二次信号向量,以及偏置项,三者相加同时经过 relu 激活得到深度网络部分的输入。

# Linear Singal
linear_output = []
for i in range(self.deep_init_size):
    linear_output.append(tf.reshape(
        tf.reduce_sum(tf.multiply(self.embeddings,self.weights['product-linear'][i]),axis=[1,2]),shape=(-1,1)))# N * 1

self.lz = tf.concat(linear_output,axis=1) # N * init_deep_size

# Quardatic Singal
quadratic_output = []
if self.use_inner:
    for i in range(self.deep_init_size):
        theta = tf.multiply(self.embeddings,tf.reshape(self.weights['product-quadratic-inner'][i],(1,-1,1))) # N * F * K
        quadratic_output.append(tf.reshape(tf.norm(tf.reduce_sum(theta,axis=1),axis=1),shape=(-1,1))) # N * 1

else:
    embedding_sum = tf.reduce_sum(self.embeddings,axis=1)
    p = tf.matmul(tf.expand_dims(embedding_sum,2),tf.expand_dims(embedding_sum,1)) # N * K * K
    for i in range(self.deep_init_size):
        theta = tf.multiply(p,tf.expand_dims(self.weights['product-quadratic-outer'][i],0)) # N * K * K
        quadratic_output.append(tf.reshape(tf.reduce_sum(theta,axis=[1,2]),shape=(-1,1))) # N * 1

self.lp = tf.concat(quadratic_output,axis=1) # N * init_deep_size

self.y_deep = tf.nn.relu(tf.add(tf.add(self.lz, self.lp), self.weights['product-bias']))
self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[0])

Deep Part

论文中的 Deep Part 实际上只有一层,不过我们可以随意设置,最后得到输出:

# Deep component
for i in range(0,len(self.deep_layers)):
    self.y_deep = tf.add(tf.matmul(self.y_deep,self.weights["layer_%d" %i]), self.weights["bias_%d"%i])
    self.y_deep = self.deep_layers_activation(self.y_deep)
    self.y_deep = tf.nn.dropout(self.y_deep,self.dropout_keep_deep[i+1])
self.out = tf.add(tf.matmul(self.y_deep,self.weights['output']),self.weights['output_bias'])

剩下的代码就不介绍啦!
好啦,本文只是提供一个引子,有关 PNN 的知识大家可以更多的进行学习呦。

参考文献

1 、https://zhuanlan.zhihu.com/p/33177517
2、https://cloud.tencent.com/developer/article/1104673?fromSource=waitui
3、https://arxiv.org/abs/1611.00144


更多高质资源 尽在AIQ 机器学习大数据 知乎专栏 点击关注

转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com