推荐系统中使用ctr排序的f(x)的设计-dnn篇之PNN模型

推荐系统中使用ctr排序的f(x)的设计-dnn篇之PNN模型

一. FNN回顾

推荐系统中使用ctr排序的f(x)的设计-dnn篇之FNN模型介绍了一下FNN模型。从本质上来讲,FNN是简单的embedding+fcs,只是使用了FM做预训练。站在宏观的ctr的dnn模型中来看,FNN没有shallow part(lr or fm/ffm),只有deep part,而特征之间的交叉性提现在了拼接(concatenate)上。FNN的效果上限是很大程度取决于FM的预训练效果的,如果不做预训练的话,就更加难出很好的效果。而且特征的交叉的方式是使用拼接(concatenate)来提现的,其实拼接后,再接fcs,虽然使用了激活函数增加了非线性,实际上是对特征进行了加权组合(add 操作)。


二.PNN介绍

  1. PNN结构

PNN,全称为Product-based Neural Networks,是一种基于乘法运算来提现特征交叉的dnn模型。其网络结构如下图所示:

PNN网络结构图

从整体结构来看,PNN与FNN的区别在于多了一层Product Layer。PNN的结构为embeddding+product layer + fcs。而PNN使用product的方式做特征交叉的想法是认为在ctr场景中,特征的交叉更加提现在一种“且”的关系下,而add的操作,是一种“或”的关系,所以product的形式更加合适,会有更好的效果。


2. Product Layer的设计

PNN的论文中首先定义了一种矩阵的预算方法: A\odot B=\sum_{}^{}{A_{i,j}B_{i,j}}

而product layer可以分成两个部分,一部分是线性部分 l_{z} ,一部分是非线性部分 l_{p}

l_{z}l_{p} 都是同样的维度的。 其具体形式如下:

l_{z} = (l_{z}^{1},l_{z}^{2}...l_{z}^{n}...l_{z}^{D1}) l_{z}^{n} = W_{z}^{n}\odot z

l_{p} = (l_{p}^{1},l_{p}^{2}...l_{p}^{n}...l_{p}^{D1}) l_{p}^{i} = W_{p}^{n} \odot p

其中 zp 为信号向量, z 为线性信号向量, p 为二次信号向量, W_{z}^{i}W_{p}^{i} 为权重矩阵。

其具体形式为: z = (z_{1}, z_{2}...z_{N}) = (f_{1},f_{2}...f_{N})

p=\{p_{i,j}\}, i=1...N,j=1...N

p_{i,j} = g(f_{i},f_{j})

f_{i} 代表第i个特征(field)的编码后的向量。函数 g(f_{i},f_{j})f_{i},f_{j} 的交叉函数,表示特征i,特征j的交叉。在该论文中,该函数的形式有两种:

(1) inner product-based

g(f_{i},f_{j}) = <f_{i},f_{j}> ,即用内积来表示特征的交叉

(2) outer product-based

g(f_{i},f_{j}) = f_{i}f_{j}^{T} ,即用矩阵乘法来表示特征的交叉

而product layer的输出为: l_{1} = relu(l_z+l_p+b_1) ,其中 b_1 的维度为D1。


3. Product layer的优化

(1) inner product-based的优化

考虑公式 l_{p}^{n} = W_{p}^{n} \odot p ,此时p的维度为N*N,假设 f_{i} 的维度为M,则计算p的时间复杂度为N*N*M,而 l_{p} 的计算代价为N*N*D1,所以总的代价为N*N(D1+M)。而矩阵 W_{p}^{n} 是对称矩阵,受到FM的启发,可以将矩阵 W_{p}^{n} 进行分解: W_{p}^{n} = \theta^{n}\theta^{nT} ,如果令 \theta^{i} 的维度也为M的话,则:

l_{p}^{n} = W_{p}^{n} \odot p=\sum_{i=1}^{N}\sum_{j=1}^{N}{ \theta^{n}_{i} \theta^{n}_{j}<f_{i},f_{j}> }= <\sum_{i=1}^{N}{\delta_{i}^{n}},\sum_{i=1}^{N}{\delta_{i}^{n}}>

其中, \delta_{i}^{n} = \theta_{i}^{n}f_{i}

\delta^{n} = (\delta^{n}_{1},\delta^{n}_{2},...,\delta^{n}_{N}) ,则其维度为N*M。而且 l_{p}^{n} = ||\sum_{i}^{}{\delta_{i}^{n}}||,此时计算 l_{p} 的总的复杂度变成了D1*N*M。

(2) outer product-based的优化

考虑 p_{i,j}=g(f_{i},f_{j}) = f_{i}f_{j}^{T} ,可知此时 p_{i,j} 为M*M的矩阵,计算 l_{p} 的时间复杂度为D1*M*M*N*N,这个显然代价很高的。为了减少负责度,论文使用了叠加的思想,它重新定义了p矩阵:

p= \sum_{i=1}^{N}{\sum_{j=1}^{N}{f_{i}f_{j}^{T}}} = f_{\sum}(f_{\sum})^{T},f_{\sum} = \sum_{i=1}^{N}f_{i}

则此时可以将复杂度降低为D1*M*(M+N)。


三. PNN的改进?

因为PNN的product layer使用了FM的方法做优化,如果替换成FFM做embedding,则代价十分之高,所以在我的代码实现里没有使用FFM做embedding进行新的尝试,只是实现了原生的PNN。值得注意的是,PNN竟然可以end-to-end的训练,不需要预训练也可以得到不错的效果。


四. dnn_ctr中PNN的模型api讲解。


源代码地址为:github.com/nzc/dnn_ctr


1.依赖环境

python版本为2.7。主要使用到的python库有numpy,sklearn, pytorch。


2.模型api

2.1 初始化

PNN(field_size, feature_sizes, embedding_size=4,
h_depth = 3, deep_layers=[32, 32, 32], is_deep_dropout=True, dropout_deep=[0.5, 0.5, 0.5], use_inner_product = True, use_outer_product = False,
deep_layers_activation='relu', n_epochs=64, batch_size=256, learning_rate=0.003,
optimizer_type='adam', is_batch_norm=False, verbose=False, random_seed=950104,weight_decay=0.0, loss_type='logloss', eval_metric=roc_auc_score,
use_cuda=True, n_class=1, greater_is_better=True
)


参数:

  • field_size: int,field的数量
  • feature_sizes: array,长度应为field_size,每一个元素对应一个field的大小
  • embedding_size: int, embedding后的向量的大小
  • h_depth: int,全连接层网络的深度(不包括输入和embedding)
  • deep_layers: array,每一个元素为对应全连接网络层网络的大小
  • is_deep_dropout: bool,是否使用dropout,True为使用
  • dropout_deep: array,长度为h_depth+1(embedding层也做dropout),每一个元素为对应网络层的dropout系数
  • use_inner_product: bool,product layer是否使用inner product-based
  • use_outer_product: bool,product layer是否使用outer product-based
  • deep_layers_activation:string,值为'relu','tanh','sigmoid',激活函数
  • n_epochs: int,最大训练轮数
  • batch_size: int,batch的大小
  • learning_rate: float,学习率
  • optimizer_type: string,值为'adam', 'rmsp', 'sgd', 'adag',优化器
  • is_batch_norm: bool,是否只用batch normalization,True为使用
  • verbose: bool,是否打印中间结果
  • random_seed: int,随机种子
  • weight_decay: float,dnn模型训练时,L2正则化项系数
  • loss_type: string,目前只支持‘logloss’
  • eval_metric: function,评估函数,默认auc
  • use_cuda: bool,是否使用cuda
  • n_class: int,目前只支持二分类,数值为1
  • greater_is_better: bool,评估函数是否值更大更好,比如auc,值越大越好,用于训练时的early stopping

输出:model


2.2 训练

fit(Xi_train, Xv_train, y_train, Xi_valid=None, Xv_valid=None,
y_valid = None, ealry_stopping=False, refit=False, save_path = None)

参数:

  • Xi_train: two-dimensional array,形式为: [[ind_{1,1}, ind_{1,2}, ...], [ind_{2,1}, ind_{2,2}, ...], ..., [ind_{i,1}, ind_{i,2}, ..., ind_{i,j}, ...], ...]ind_{i,j} 为训练集的第i个样本的第j个field不为0的下标(index),每个元素的类型为int
  • Xv_train: two-dimensional array,形式为: [[val_{1,1}, val_{1,2}, ...], [val_{2,1}, val_{2,2}, ...], ..., [val_{i_1}, val_{i,2}, ..., val_{i,j}, ...], ...]val_{i,j} 为训练集的第i个样本的第j个filed不为0的下标的值(value),每个元素的类型为float
  • y_train: array, 训练集的labels
  • Xi_valid: 测试集的下标,与Xi_train一样
  • Xv_valid: 测试集的值,与Xv_train一样
  • y_valid: 测试集label,与y_train一样
  • early_stopping: bool,是否提前结束训练,如果为True,则在发现过拟合后会停止训练,否则一直训练到最大训练轮数
  • refit: bool,是否加入测试集合再训练一下,如果为True,则加入测试集再训练一下模型,方便用全数据集训练的时候用
  • save_path: string,模型保存路径,为None时,不保存模型。

输出:None


2.3 预测

predict(Xi, Xv)

参数:

  • Xi: 参见fit的Xi_train
  • Xv: 参见fit的Xv_train

输出:array,实际的类别(0或者1)的数组


predict_proba( Xi, Xv)

参数:

  • Xi: 参见fit的Xi_train
  • Xv: 参见fit的Xv_train

输出:array,概率值数组(label=1的概率值)


参考文献:

arxiv.org/pdf/1611.0014

发布于 2018-01-29 01:33