AutoInt:使用Multi-head Self-Attention进行自动特征学习的CTR模型

本文首发于知乎专栏 https://zhuanlan.zhihu.com/p/53462648

简介

这篇论文提出使用multi-head self attention(类似Transformer里的那个) 机制来进行自动特征交叉学习以提升CTR预测任务的精度。
废话不多说,先看下主要结构。典型的四段式深度学习CTR模型结构:输入,嵌入,特征提取,输出。这里我们重点看下嵌入和特征提取部分

核心结构

输入和嵌入


针对类别特征,通过embedding方式转换为低维稠密向量
e i = V i x i e_i=V_ix_i ei=Vixi
其中, V i V_i Vi是特征组 i i i对应的嵌入字典(嵌入矩阵), x i x_i xi是特征组特征的独热编码表示向量(通常处于节省空间的考虑,只存储非零特征对应的索引)

对于连续特征有,
e m = v m x m e_m=v_mx_m em=vmxm
其中 v m v_m vm是嵌入向量, x m x_m xm是一个标量值

通常在CTR任务中我们对连续值特征对处理方式有三种:

  1. 进行归一化处理拼接到embedding向量侧
  2. 进行离散化处理作为类别特征
  3. 赋予其一个embedding向量,每次用特征值与embedding向量的乘积作为其最终表示

本文采取的是第三种方式,具体这三种方式孰优孰劣,要在具体场景具体任务下大家自己去验证了~

从实现的角度看第三种是比较便捷的。

InteractingLayer(交互层)

交互层使用多头注意力机制将特征投射到多个子空间中,在不同的子空间中可以捕获不同的特征交互模式。通过交互层的堆叠,可以捕获更高阶的交互模式。

下面展示在特定子空间 h h h下,对于特征组 m m m下的特征 e m e_m em,交互层是如何计算与其相关的交互特征 <mover accent="true"> e ~ </mover> m ( h ) \tilde{e}^{(h)}_m e~m(h)

  1. 首先输入特征通过矩阵乘法线性变换为在注意力空间下的向量表示,对于每个特征 e m e_m em在特定的注意力空间 h h h中,都有三个表示向量 E m h : Q u e r y = W Q u e r y ( h ) e m E^{h:Query}_m=W^{(h)}_{Query}e_m Emh:Query=WQuery(h)em, E m h : K e y = W K e y ( h ) e m E^{h:Key}_m=W^{(h)}_{Key}e_m Emh:Key=WKey(h)em, E m h : V a l u e = W V a l u e ( h ) e m E^{h:Value}_m=W^{(h)}_{Value}e_m Emh:Value=WValue(h)em

  2. 计算 e m e_m em与其他特征 e k e_k ek的相似度,本文使用向量内积表示:
    ϕ ( h ) ( e m , e k ) = < E m h : Q u e r y , E k h : K e y > \phi^{(h)}(e_m,e_k)=<E^{h:Query}_m,E^{h:Key}_k> ϕ(h)(em,ek)=<Emh:Query,Ekh:Key>

  3. 计算softmax归一化注意力分布: a m , k ( h ) = e x p ( ϕ ( h ) ( e m , e k ) ) l = 1 M e x p ( ϕ ( h ) ( e m , e l ) ) a^{(h)}_{m,k}=\frac{exp(\phi^{(h)}(e_m,e_k))}{\sum_{l=1}^M{exp(\phi^{(h)}(e_m,e_l))}} am,k(h)=l=1Mexp(ϕ(h)(em,el))exp(ϕ(h)(em,ek))

  4. 通过加权求和的方式得到特征m及其相关的特征组成的一个新特征 <mover accent="true"> e ~ </mover> m ( h ) = k = 1 M a m , k ( h ) E m h : V a l u e \tilde{e}^{(h)}_m=\sum_{k=1}^Ma^{(h)}_{m,k}E^{h:Value}_m e~m(h)=k=1Mam,k(h)Emh:Value

假设有 H H H个注意力空间,对每个注意力空间下的结果进行拼接,得到特征 m m m最终的结果表示

<mover accent="true"> e ~ </mover> m = <mover accent="true"> e ~ </mover> m ( 1 ) <mover accent="true"> e ~ </mover> m ( 2 ) <mover accent="true"> e ~ </mover> m ( H ) \tilde{e}_m=\tilde{e}^{(1)}_m\oplus\tilde{e}^{(2)}_m\oplus\dots\oplus\tilde{e}^{(H)}_m e~m=e~m(1)e~m(2)e~m(H)
我们可以选择使用残差网络保留一些原始特征的信息留给下一层继续学习
e m R e s = R e L U ( <mover accent="true"> e ~ </mover> m + W R e s e m ) e^{Res}_m=ReLU(\tilde{e}_m+W_{Res}e_m) emRes=ReLU(e~m+WResem)

最后,将每个特征的结果拼接,计算最终的输出值
<mover accent="true"> y ^ </mover> = σ ( w T ( e 1 R e s e 2 R e s e M R e s ) + b ) \hat{y}=\sigma(w^T(e^{Res}_1\oplus e^{Res}_2 \dots e^{Res}_M)+b) y^=σ(wT(e1Rese2ReseMRes)+b)

我不想看数学,我想看代码:OK

下面就是核心代码啦,可以看到其实很短。
我们使用tensorflow进行实现的时候,可以充分利用矩阵运算的特性来简化实现。

先说明一些定义,att_embedding_size为注意力空间下隐向量的长度,head_num为注意力空间的个数,use_res为一个布尔变量,表示是否使用残差连接。

首先假设输入inputs的shape为(batch_size,field_size,embedding_size),四个投影矩阵(W_Query,W_Key,W_Value,W_Res)的shape均为(embedding_size, att_embedding_size * head_num)

  1. 通过矩阵乘法得到注意力空间下的三组向量表示
    querys = tf.tensordot(inputs, self.W_Query, axes=(-1, 0))  # (batch_size,field_size,att_embedding_size*head_num)
    keys = tf.tensordot(inputs, self.W_key, axes=(-1, 0))
    values = tf.tensordot(inputs, self.W_Value, axes=(-1, 0))
  1. 为了同时在不同的子空间下计算特征相似度,需要先进行一些变换
    querys = tf.stack(tf.split(querys, self.head_num, axis=2))  # (head_num,batch_size,field_size,att_embedding_size)
    keys = tf.stack(tf.split(keys, self.head_num, axis=2))
    values = tf.stack(tf.split(values, self.head_num, axis=2))
  1. 计算相似度及归一化注意力分布
    inner_product = tf.matmul(querys, keys, transpose_b=True)  # (head_num,batch_size,field_size,field_size)
    self.normalized_att_scores = tf.nn.softmax(inner_product)
  1. 计算加权和
    result = tf.matmul(self.normalized_att_scores, values)  # (head_num,batch_size,field_size,att_embedding_size)
  1. 将不同子空间下的结果进行拼接
    result = tf.concat(tf.split(result, self.head_num, ), axis=-1)
    result = tf.squeeze(result, axis=0)# (batch_size,field_size,att_embedding_size*head_num)
  1. 使用残差连接保留原始信息,
    if use_res:
        result += tf.tensordot(inputs, self.w_res, axes=(-1, 0))
    result = tf.nn.relu(result)# (batch_size,field_size,att_embedding_size*head_num)

我不想看代码,我想直接拿来用:没问题

首先确保你的python版本>=3.4,然后pip install deepctr
再去下载一下demo数据

然后直接运行下面的代码吧!

import pandas as pd
from sklearn.metrics import log_loss, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, MinMaxScaler

from deepctr.models import AutoInt
from deepctr.inputs import  SparseFeat, DenseFeat,get_fixlen_feature_names

if __name__ == "__main__":
    data = pd.read_csv('./criteo_sample.txt')

    sparse_features = ['C' + str(i) for i in range(1, 27)]
    dense_features = ['I' + str(i) for i in range(1, 14)]

    data[sparse_features] = data[sparse_features].fillna('-1', )
    data[dense_features] = data[dense_features].fillna(0, )
    target = ['label']

    # 1.Label Encoding for sparse features,and do simple Transformation for dense features
    for feat in sparse_features:
        lbe = LabelEncoder()
        data[feat] = lbe.fit_transform(data[feat])
    mms = MinMaxScaler(feature_range=(0, 1))
    data[dense_features] = mms.fit_transform(data[dense_features])

    # 2.count #unique features for each sparse field,and record dense feature field name

    fixlen_feature_columns = [SparseFeat(feat, data[feat].nunique())
                           for feat in sparse_features] + [DenseFeat(feat, 1,)
                          for feat in dense_features]

    dnn_feature_columns = fixlen_feature_columns
    linear_feature_columns = fixlen_feature_columns

    fixlen_feature_names = get_fixlen_feature_names(linear_feature_columns + dnn_feature_columns)

    # 3.generate input data for model

    train, test = train_test_split(data, test_size=0.2)
    train_model_input = [train[name] for name in fixlen_feature_names]

    test_model_input = [test[name] for name in fixlen_feature_names]

    # 4.Define Model,train,predict and evaluate
    model = AutoInt( dnn_feature_columns, task='binary')
    model.compile("adam", "binary_crossentropy",
                  metrics=['binary_crossentropy'], )

    history = model.fit(train_model_input, train[target].values,
                        batch_size=256, epochs=10, verbose=2, validation_split=0.2, )
    pred_ans = model.predict(test_model_input, batch_size=256)
    print("test LogLoss", round(log_loss(test[target].values, pred_ans), 4))
    print("test AUC", round(roc_auc_score(test[target].values, pred_ans), 4))

deepctr是个什么玩意??

请移步~

参考资料

想要了解更多关于推荐系统,点击率预测算法的同学,欢迎大家关注我的公众号 浅梦的学习笔记,关注后回复“加群”一起参与讨论交流
. . . . . . .

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务