常见的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=wo+Ni=1wixi+PTNi=1Nj=i+1aijvivjxixj
 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。

每一层的表达式为:

xl+1=x0xTlwl+bl+xl=f(xl,wl,bl)+xl

$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的问题

第一层交叉时,

x1=x0xT0w1+x0=x0(xT0w1)+x0=x0(xT0w1+1)=α1x0

$\alpha^1$是一个标量,所以相当于x_0的一个线性回归

第k+1层交叉时

xk+1=x0xTkwk+1+xk=x0((αkx0)Twk+1)+αkx0=x0(αk(xT0wk+1+1))=αk+1x0

$\alpha_{k+1}$也是一个标量

所以第k层交叉出来的向量中的每个元素都是输入向量的标量倍数

每一层交叉出来的特征其实都是输入特征的线性组合,这样其实是限制了特征交叉的表达能力。

PNN

PNN

该模型的创新在于引入field的乘积作为DNN的输入,而乘积又分为内积和外积,分别对应IPNN和OPNN网络.

第一个隐层的输出为

l1=relu(lz+lp+b1)

product layer 包含两个部分,一个是线性部分$l_z$,一个是二阶部分$l_p$。

lz=(l1z,l2z,...,lnz,...,lD1z),lnz=Wnzzlp=(l1p,l2p,...,lnp,...,lD1p),lnp=Wnpp

其中,$\odot$的定义如下:

AB=i,jAijBij
z=(z1,z2,...,zN)=(f1,f2,...,fN)p=(pij),i=1,...,N,j=1,...,N

其中, $f_i\inR^M$ 就是第i个特征field的embedding向量,共有N个field。

p(i,j)=g(fi,fj)

表示特征交叉函数,文中使用了两种交叉,分别是内积和外积。

lnz=Wnzz=Ni=1Mj=1(Wnz)i,jzi,j
lnp=Ni=1Nj=1(Wnp)i,jpi,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) $

为了降低复杂度,作者引入矩阵分解技巧,

Wnp=θnθnT

$\theta^n\in R^{N * M}$ 是一个N * M 的矩阵,相当于把N *N 的矩阵 变成了 N * M 的矩阵。

所以

Wnpp=Ni=1Nj=1θniθnj<fi,fj>=<Ni=1δni,Ni=1δni>
δni=θnifi

所以,$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(fi,fj)=fifTjRM,M

所以,Wnp的维度是(N,N,M,M),时间复杂度也是一样

作者为了降低复杂度,进行了superposition:

p=Ni=1Nj=1fifTj=fsum(fsum)T,f=Ni=1fi

这样,维度就从$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