常见的ctr模型

以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

image

重点就在于左侧的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

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)$

代码没看完,感觉跟论文里面不一致啊

autoInt

宁雨 /
Published under (CC) BY-NC-SA in categories MachineLearning  tagged with
comments powered by Disqus