以deepctr的实现为基础,介绍特点及实现细节
LR
DNN
def call(self, inputs, training=None, **kwargs):
deep_input = inputs
for i in range(len(self.hidden_units)):
fc = tf.nn.bias_add(tf.tensordot(
deep_input, self.kernels[i], axes=(-1, 0)), self.bias[i])
if self.use_bn:
fc = self.bn_layers[i](fc, training=training)
fc = self.activation_layers[i](fc)
fc = self.dropout_layers[i](fc, training=training)
deep_input = fc
return deep_input
wide&Deep
LR的输出加上DNN的输出,然后再加上一个激活函数
FM
FM 在LR的基础上加上了二阶交叉,且使用embedding向量作为特征表示。 通过推导,复杂度可以由O(n^2)降为O(n)
def call(self, inputs, **kwargs):
if K.ndim(inputs) != 3:
raise ValueError(
"Unexpected inputs dimensions %d, expect to be 3 dimensions"
% (K.ndim(inputs)))
concated_embeds_value = inputs #B,field, emb
square_of_sum = tf.square(reduce_sum(
concated_embeds_value, axis=1, keep_dims=True)) #B, 1, emb
sum_of_square = reduce_sum(
concated_embeds_value * concated_embeds_value, axis=1, keep_dims=True) #B, field, emb => B, 1, emb
cross_term = square_of_sum - sum_of_square
cross_term = 0.5 * reduce_sum(cross_term, axis=2, keep_dims=False)
return cross_term
DeepFM
将wide&deep模型中的LR换为FM。且FM和DNN之间共享embedding。
最终将LR/FM/DNN三部分的logits相加,经过一层激活函数得到预估值
在deepctr的实现中,FM部分只有离散特征, DNN和LR部分既有离散特征也有连续特征
NFM
NFM 和DeepFM不同的地方在于,deepFM 的Deep部分和FM部分只是共享emb,之后就分开进行各自的建模。而NFM的Deep部分以FM的二阶交叉特征作为输入,并在其上构建DNN网络。
NFM 引入了一个bi-linear层,对特征进行两两交叉,其实跟FM的操作是一样的,唯一不同的是FM中特征的embedding向量交叉时使用的内积,NFM使用的是两两交叉乘积(element-wise)。所以FM中二阶交叉的结果是一个标量,而NFM 二阶交叉的结果是向量。
def call(self, inputs, **kwargs):
if K.ndim(inputs) != 3:
raise ValueError(
"Unexpected inputs dimensions %d, expect to be 3 dimensions" % (K.ndim(inputs)))
concated_embeds_value = inputs
square_of_sum = tf.square(reduce_sum(
concated_embeds_value, axis=1, keep_dims=True))
sum_of_square = reduce_sum(
concated_embeds_value * concated_embeds_value, axis=1, keep_dims=True)
cross_term = 0.5 * (square_of_sum - sum_of_square)
从上面的实现上也可以看到,不同点在于最后一行NFM并没有对结果进行求和,所以得到了一个向量
AFM
AFM没有DNN部分,只是在FM的基础上对特征交叉部分加入了注意力机制,对不同的特征交叉计算权重。
\[y=w_o+\sum_{i=1}^N w_i*x_i + P^T\sum_{i=1}^N \sum_{j=i+1}^N a_{ij} v_i \circ v_j x_i x_j\] def call(self, inputs, training=None, **kwargs):
embeds_vec_list = inputs
row = []
col = []
for r, c in itertools.combinations(embeds_vec_list, 2):
row.append(r)
col.append(c)
#p/q分别存储了embedding向量的所有两两组合对的对应embedding
p = tf.concat(row, axis=1)
q = tf.concat(col, axis=1)
inner_product = p * q #N, F*(F-1)/2, emb ,两两相乘
bi_interaction = inner_product
attention_temp = tf.nn.relu(tf.nn.bias_add(tf.tensordot(
bi_interaction, self.attention_W, axes=(-1, 0)), self.attention_b))
self.normalized_att_score = softmax(tf.tensordot(
attention_temp, self.projection_h, axes=(-1, 0)), dim=1)
attention_output = reduce_sum(
self.normalized_att_score * bi_interaction, axis=1)
attention_output = self.dropout(attention_output,training=training) # training
afm_out = self.tensordot([attention_output, self.projection_p])
return afm_out
DeepCrossNet
重点就在于左侧的cross network。
每一层的表达式为:
\[x_{l+1}=x_0 x_l^T w_l + b_l +x_l = f(x_l,w_l,b_l)+x_l\]$x_l$表示第l层的输出,为d为列向量,$w_l,b_l$为第l层的参数,也是d维。
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_0 = tf.expand_dims(inputs, axis=2)
x_l = x_0
for i in range(self.layer_num):
xl_w = tf.tensordot(x_l, self.kernels[i], axes=(1, 0))
dot_ = tf.matmul(x_0, xl_w)
x_l = dot_ + self.bias[i] + x_l
x_l = tf.squeeze(x_l, axis=2)
return x_l
inputs的维度是batch_size * units, kernels[i]的维度是units * 1
这样,xl_w的维度就是batch_size * 1 * 1 ,dot_的维度是batch_size * units * 1, x_l的维度还是batch_size * units * 1,最终输出的x_l维度是batch_size * units
缺点:
在xdeepfm论文中。有提到DCN的问题
第一层交叉时,
\[x_1=x_0 x_0^T w_1 +x_0 = x_0 ( x_0^T w_1) +x_0 \\ =x_0(x_0^T w_1 +1)=\alpha^1 x_0\]$\alpha^1$是一个标量,所以相当于x_0的一个线性回归
第k+1层交叉时
\[x_{k+1}=x_0 x_k^T w_{k+1} +x_k = x_0 ( (\alpha^k x_0)^T w_{k+1}) +\alpha^k x_0 \\ =x_0(\alpha^k(x_0^T w_{k+1} +1) )=\alpha^{k+1} x_0\]$\alpha_{k+1}$也是一个标量
所以第k层交叉出来的向量中的每个元素都是输入向量的标量倍数
每一层交叉出来的特征其实都是输入特征的线性组合,这样其实是限制了特征交叉的表达能力。
PNN
该模型的创新在于引入field的乘积作为DNN的输入,而乘积又分为内积和外积,分别对应IPNN和OPNN网络.
第一个隐层的输出为
\[l_1=relu(l_z+l_p+b_1)\]product layer 包含两个部分,一个是线性部分$l_z$,一个是二阶部分$l_p$。
\[l_z=(l_z^1,l_z^2,...,l_z^n,...,l_z^{D_1}), l_z^n = W_z^n \odot z \\ l_p=(l_p^1,l_p^2,...,l_p^n,...,l_p^{D_1}), l_p^n = W_p^n \odot p\]其中,$\odot$的定义如下:
\[A\odot B = \sum_{i,j}A_{ij}B_{ij}\] \[z=(z_1,z_2,...,z_N)=(f_1,f_2,...,f_N) \\ p=(p_{ij}), i=1,...,N, j=1,...,N\]其中, $f_i\inR^M$ 就是第i个特征field的embedding向量,共有N个field。
\[p(i,j) = g(f_i, f_j)\]表示特征交叉函数,文中使用了两种交叉,分别是内积和外积。
\[l_z^n =W_z^n \odot z = \sum_{i=1}^N \sum_{j=1}^M (W_z^n)_{i,j} z_{i,j}\] \[l_p^n = \sum_{i=1}^N \sum_{j=1}^N (W_p^n)_{i,j} p_{i,j}\]$W_z^n$ 维度是 N * M , 共有$D_1$个,所以维度是 $D_1*N *M $, $W_p^n$ 维度是N * N , 共有$D_1$个,所以维度是$ D_1 * N * N $
所以,空间复杂度是 $ D_1 * N * (N+M)$,时间复杂度是 $ D_1 * N * (N +M) $
为了降低复杂度,作者引入矩阵分解技巧,
\[W_p^n =\theta^n {\theta^n }^T\]$\theta^n\in R^{N * M}$ 是一个N * M 的矩阵,相当于把N *N 的矩阵 变成了 N * M 的矩阵。
所以
\[W_p^n \odot p = \sum_{i=1}^N \sum_{j=1}^N \theta_i^n {\theta_j^n }<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\]所以,$W_p^n$的空间复杂度由$O(N^2)$变成O(N * M ), 时间复杂度由$O(N^2)$变成O(N*M),$l_1$的空间复杂度变成$D_1 * N * M $, 时间复杂度变成 $D_1 * N * M $.
在 OPNN 中, 特征交叉函数是外积
\[g(f_i,f_j) = f_i * f_j ^T \in R^{M, M}\]所以,\(W_p^n\)的维度是(N,N,M,M),时间复杂度也是一样
作者为了降低复杂度,进行了superposition:
\[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\]这样,维度就从$N * N * M * M $变成了$M * M$
所以 整体复杂度变成 $D_1 *M * (M+N)$,时间复杂度也是$D_1 *M * (M+N)$
代码没看完,感觉跟论文里面不一致啊