轻量级前端框架助力开发者提升项目效率与性能
618
2022-10-12
Product-based Neural Network (PNN) 介绍与源码浅析
Product-based Neural Network (PNN) 介绍与源码浅析
前言
继续介绍论文~ 本文初看的时候有些懵逼, 多看几次总算有些 Get 到了, 总结一下.
广而告之
Product-based Neural Network (PNN)
文章信息
论文标题: Product-based Neural Networks for User Response Prediction论文地址:这是作者提供的代码; 另外在我看到了另外的精彩的实现, 是完全按照论文中介绍的公式进行编码的;论文作者提供的代码和论文中的介绍稍有差别.发表时间: ICDM 2016论文作者: Yanru Qu, Han Cai, Kan Ren, Weinan Zhang, Yong Yu, Ying Wen, Jun Wang作者单位: Shanghai Jiao Tong University
请注意
后面在博文中介绍的代码主要以 中的为主, 其实现是完全按照论文的思路, 本文原作者的代码实现再其后介绍, 原作者的代码和论文介绍的内容稍有差别, 但是写的很有意思, 也是不能错过的精彩.
核心观点
本文主要介绍了 Product Layer 用于捕获类别特征 (Categorical Features) 的二阶交互特性 (Inter-field Feature Interaction). 就具体实现来说, 主要实现了基于向量 Inner-Product 的 IPNN 网络以及基于向量 Outer-Product 的 OPNN 网络. 这两个网络处理在 Product Layer 中使用的向量交互方式不同之外, 其他结构完全相同.
核心观点介绍
下面来看具体的网络结构:
将网络分为三个部分来看, 分别是:
Embedding Layer: 将[Field 1, ..., Field N] 等 N 个输入的类别特征转换为稠密向量, 分别为[Feature 1, ..., Feature N];Product Layer: 这里面实际上要分为两个部分, 其中z 表示的内容实际上仍然是一阶特征, 并未进行特征交互, 而p 表示的内容由本文提出的 Product Layer 生成, 以获取特征间的相关性;MLP: 上一层得到的z 和p 进行 concat 后 (注意在论文的公式中z 和p 的结果是相加起来), 再输入到一个 MLP 中学习高阶特征交互, 并输出最终的预估结果.
符号介绍
在做进一步介绍之前, 先明确一下之后会用到的符号, 以方便讨论:
(突然发现有的符号其实一眼看过去就能明白啥函数, 算了就不描述了 -?)
网络结构图中 z 部分的计算
从公式来看, 会有些抽象, 我们转化成代码来看:
# B * Nfeat_index = tf.placeholder(tf.int32, shape=[None,None], name='feat_index')# B * N * Membeddings = tf.nn.embedding_lookup(weights['feature_embeddings'], feat_index)## embeddings 的大小为 B * N * M## weights['product-linear'] 的大小为 D1 * N * Mlinear_output = []for i in range(D1): linear_output.append( tf.reshape( tf.reduce_sum( tf.multiply(embeddings, weights['product-linear'][i]), # B * N * M axis=[1, 2]), # B shape=(-1, 1) ) # B * 1 )lz = tf.concat(linear_output, axis=1) # B * D1
网络结构图中 p 部分的计算
Inner Product
下面继续来看看代码:
## embeddings 的大小为 B * N * M## weights['product-quadratic-inner'] 的大小为 D1 * Nquadratic_output = []for i in range(D1): theta = tf.multiply(embeddings, # B * N * M tf.reshape(weights['product-quadratic-inner'][i], (1, -1, 1)) # 1 * N * 1 ) # N * N * M quadratic_output.append( tf.reshape( tf.norm( tf.reduce_sum(theta, axis=1), # B * M, 注意这里是对 N 个特征向量求和得到 theta axis=1), # B, 这里是对 theta 求范数 shape=(-1, 1) ) # B * 1 ) lp = tf.concat(quadratic_output, axis=1) # B * D1
Outer Product
用公式体现如下:
下面来看看具体的代码实现:
## embeddings 的大小为 B * N * M## weights['product-quadratic-outer'] 的大小为 D1 * M * Mquadratic_output = [] embedding_sum = tf.reduce_sum(embeddings, axis=1) ## B * M, 即公式中对 f 进行累加p = tf.matmul(tf.expand_dims(embedding_sum, 2), # B * M * 1 tf.expand_dims(embedding_sum, 1) # B * 1 * M ) # B * M * M, 做外积for i in range(D1): theta = tf.multiply(p, # B * M * M tf.expand_dims(weights['product-quadratic-outer'][i], 0) # 1 * M * M ) # B * M * M quadratic_output.append( tf.reshape(tf.reduce_sum(theta, axis=[1, 2]), # B shape=(-1, 1)) # B * 1 ) lp = tf.concat(quadratic_output, axis=1) # B * D1
Product Layer 的输出
按照论文中的介绍, Product Layer 最终的输出为:
代码实现如下:
## lz 和 lp 大小均为 B * D1## weights['product-bias'] 大小为 D1y_deep = tf.nn.relu(tf.add(tf.add(lz, lp), weights['product-bias'] ))
至此, PNN 的核心思路已经介绍好了~ 鼓掌~~----
原作者的代码介绍
下面再简单分析下原作者提供的代码: 其实现和论文中的公式还是有很多区别的, 但是实践过程中, 这些实现还是有很大的参考意义的. 另外作者的实现看起来特别精炼, 代码很值得学习. 作者总共实现了 LR, FM, DeepFM, FNN, CCPM, PNN1, PNN2 等 7 个算法. 注意, PNN1 和 PNN2 不是分别代表 inner-product 和 outer-product, 其中 PNN2 中完整实现了 inner-product 和 outer-product 方法, 可以先看看 PNN1 进行学习理解, 再来看 PNN2 会轻松一些.
下面主要介绍 PNN2 的实现.
(2020-08-14 TODO: 放心, 不是 Flag, 先去搬砖. ---- )
(2020-08-14 夜, 来填坑啦, ----)
PNN2 的实现在 中:
初始化部分代码如下:
class PNN2(Model): def __init__(self, field_sizes=None, embed_size=10, layer_sizes=None, layer_acts=None, drop_out=None, embed_l2=None, layer_l2=None, init_path=None, opt_algo='gd', learning_rate=1e-2, random_seed=None, layer_norm=True, kernel_type='mat'): Model.__init__(self) init_vars = [] num_inputs = len(field_sizes) for i in range(num_inputs): init_vars.append(('embed_%d' % i, [field_sizes[i], embed_size], 'xavier', dtype)) num_pairs = int(num_inputs * (num_inputs - 1) / 2) node_in = num_inputs * embed_size + num_pairs if kernel_type == 'mat': init_vars.append(('kernel', [embed_size, num_pairs, embed_size], 'xavier', dtype)) elif kernel_type == 'vec': init_vars.append(('kernel', [num_pairs, embed_size], 'xavier', dtype)) elif kernel_type == 'num': init_vars.append(('kernel', [num_pairs, 1], 'xavier', dtype)) for i in range(len(layer_sizes)): init_vars.append(('w%d' % i, [node_in, layer_sizes[i]], 'xavier', dtype)) init_vars.append(('b%d' % i, [layer_sizes[i]], 'zero', dtype)) node_in = layer_sizes[i]
其中: (下面描述中用到的数学符号会完全和论文中的一致, 以便对比)
继续分析:
with self.graph.as_default(): if random_seed is not None: tf.set_random_seed(random_seed) self.X = [tf.sparse_placeholder(dtype) for i in range(num_inputs)] self.y = tf.placeholder(dtype) self.keep_prob_train = 1 - np.array(drop_out) self.keep_prob_test = np.ones_like(drop_out) self.layer_keeps = tf.placeholder(dtype) self.vars = utils.init_var_map(init_vars, init_path) w0 = [self.vars['embed_%d' % i] for i in range(num_inputs)] xw = tf.concat([tf.sparse_tensor_dense_matmul(self.X[i], w0[i]) for i in range(num_inputs)], 1) xw3d = tf.reshape(xw, [-1, num_inputs, embed_size])
下面高能来了:
row = []col = []for i in range(num_inputs - 1): for j in range(i + 1, num_inputs): row.append(i) col.append(j)# batch * pair * kp = tf.transpose( # pair * batch * k tf.gather( # num * batch * k tf.transpose( xw3d, [1, 0, 2]), row), [1, 0, 2])# batch * pair * kq = tf.transpose( tf.gather( tf.transpose( xw3d, [1, 0, 2]), col), [1, 0, 2])# b * p * kp = tf.reshape(p, [-1, num_pairs, embed_size])# b * p * kq = tf.reshape(q, [-1, num_pairs, embed_size])
下面接着高能:
k = self.vars['kernel']if kernel_type == 'mat': # batch * 1 * pair * k p = tf.expand_dims(p, 1) # batch * pair kp = tf.reduce_sum( # batch * pair * k tf.multiply( # batch * pair * k tf.transpose( # batch * k * pair tf.reduce_sum( # batch * k * pair * k tf.multiply( p, k), -1), [0, 2, 1]), q), -1)else: # 1 * pair * (k or 1) k = tf.expand_dims(k, 0) # batch * pair kp = tf.reduce_sum(p * q * k, -1)
kernel_type == 'mat' 部分是 Outer Product Layer 的结果, 而 else 的部分是 Inner Product Layer 的结果. 上面的代码需要体会, 咱们从最简单的方式入手.
可以进一步表示为:
import torchimport torch.nn as nn## p 的大小为 P * K, 这个大写的 P 相当于作者代码中的 pair, 也是论文中的 D1 参数p = torch.arange(4).view(2, 2).float() # P * Kq = torch.arange(1, 5).view(2, 2).float() # P * Kw = torch.arange(8).view(2, 2, 2).float() # P * K * K"""求解方式一"""## 如果 w 权重为 P * K * K, 那么按照下面的方式进行求解p1 = torch.unsqueeze(p, 2)d = torch.sum(p1 * w, dim=1) * q # P * Kprint(d)"""求解方式二"""## 如果 w 权重为 K * P * K, 那么按照下面的方式求解, 这个求解顺序和作者代码中的顺序相同w1 = w.permute(2, 0, 1) # K * P * Kp1 = torch.unsqueeze(p, 0) # 1 * P * Kd = torch.sum(p1 * w1, dim=-1).permute(1, 0) * qprint(d)"""结果""""""均为"""tensor([[ 2., 6.], [ 78., 124.]])
最后两种求解方式得到的结果是相同的.
总结
可能看作者的代码会有些懵逼, 建议是, 多看看, 多看看就好, 我看了 3 遍终于有点眉目.
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~