文章目录
简介
AutoInt: Automatic Feature Interaction Learning viaSelf-Attentive Neural Networks。
AutoIn将transform中的 multi-head self attention引入CTR预估模型。实现了自动特征交叉学习以提升CTR预测任务的精度。
AutoInt的结构图如下:
该结构是典型的四段式深度学习CTR模型结构:输入,嵌入,特征提取,
AutoInt的目标是:将原始稀疏高维特征向量映射到低维空间,同时对高阶特征交互进行建模。
如上图所示,(1)稀疏特征向量x作为输入,然后是投影所有特征(包括类别型特征和数值型特征)的嵌入层进入同样的维度的低维空间。(2)将所有的嵌入后的低维fields输入到一个新的交互层中,该层被实现为一个多头自聚焦神经网络(a multi-head self-attentive neural network.)。对于每个交互层,通过注意机制对高阶特征进行组合,并利用多头部机制对不同的组合进行评估,将特征映射到不同的子空间中。通过叠加多个交互层,可以对不同阶的组合特征进行建模。
核心结构
输入和嵌入
模型的输入:
x
=
[
x
1
;
x
2
;
⋯
;
x
M
]
\mathbf x = [\mathbf x_1; \mathbf x_2;\cdots;\mathbf x_M]
x=[x1;x2;⋯;xM]
其中M是总特征数目,
x
i
x_i
xi是第
i
i
i个特征表示,第i(m-1)为类别型特征,第mM个特征为数值型特征。
对于类别型特征,
x
i
x_i
xi是一个高维的稀疏的one-hot向量,AutoInt 采用如下方式进行降维
e
i
=
V
i
x
i
\mathbf e_i = \mathbf V_i \mathbf x_i
ei=Vixi
其中,
V
i
\mathbf V_i
Vi是field i 的嵌入矩阵。
注:若
x
i
\mathbf x_i
xi是一个多值的one-hot向量(比如一个电影可以同时是Drama and Romance),则采用以下的方式进行降维:
e
i
=
1
q
V
i
x
i
\mathbf e_i =\frac{1}{q} \mathbf V_i \mathbf x_i
ei=q1Vixi
q是多值向量的值的个数。
数值型变量也采用同样的方式:
e
m
=
v
m
x
m
\mathbf e_m = \mathbf v_m x_m
em=vmxm
其中
v
m
v_m
vm是嵌入向量,
x
m
x_m
xm是一个标量值
通常在CTR任务中我们对连续值特征对处理方式有三种:
- 进行归一化处理拼接到embedding向量侧
- 进行离散化处理作为类别特征
- 赋予其一个embedding向量,每次用特征值与embedding向量的乘积作为其最终表示
AutoInt采取的是第三种方式。
将类别型变量和数值型变量的embedding结果进行catenation。作为下一层的输入。
InteractingLayer(交互层)
在理解这个层之前,需要先了解Attention机制,可以参考:深入理解 Bert核心:Self-Attention与transformer
交互层使用多头注意力机制将特征投射到多个子空间中,在不同的子空间中可以捕获不同的特征交互模式。通过交互层的堆叠,可以捕获更高阶的交互模式。下图展示了在特定的子空间 h h h下,对于特征 e m \mathbf e_m em,交互层是如何计算其交互特征 e ~ m ( h ) \tilde{\mathbf e}^{(h)}_m e~m(h)
(1)首先,输入特征通过矩阵的线性乘法变换为不同注意力空间下的向量表示,对于每个特征 e m \mathbf e_m em
在特定的注意力空间
h
h
h中,都有三个向量表示:
E
m
h
:
Q
u
e
r
y
=
W
Q
u
e
r
y
(
h
)
e
m
E
m
h
:
V
a
l
u
e
=
W
V
a
l
u
e
(
h
)
e
m
E
m
h
:
K
e
y
=
W
K
e
y
(
h
)
e
m
E^{h:Query}_m=\mathbf W^{(h)}_{Query}\mathbf e_m \\ E^{h:Value}_m=\mathbf W^{(h)}_{Value}\mathbf e_m \\ E^{h:Key}_m=\mathbf W^{(h)}_{Key} \mathbf e_m
Emh:Query=WQuery(h)emEmh:Value=WValue(h)emEmh:Key=WKey(h)em
(2)计算
e
m
e_m
em 与其他特征
e
k
e_k
ek的相似度(向量内积):
ϕ
(
h
)
(
e
m
,
e
k
)
=
<
W
Q
u
e
r
y
(
h
)
e
m
,
W
K
e
y
(
h
)
e
k
>
\phi^{(h)}(\mathbf e_m,\mathbf e_k)= <\mathbf W^{(h)}_{Query}\mathbf e_m,\mathbf W^{(h)}_{Key} \mathbf e_k>
ϕ(h)(em,ek)=<WQuery(h)em,WKey(h)ek>
(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的新特征: e ~ 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最终的结果为 e ~ m = e ~ m ( 1 ) ⊕ e ~ m ( 2 ) ⊕ ⋯ ⊕ e ~ 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),其中 ⊕ \oplus ⊕是连接操作符,H多头transform机制中的头的个数。
为了保留一些原始信息作为下一层的输入,AutoInt使用残差神经网络:
e
m
R
e
s
=
R
e
L
U
(
e
~
m
+
W
R
e
s
e
m
)
e^{Res}_m=ReLU(\tilde{e}_m+W_{Res}e_m)
emRes=ReLU(e~m+WResem)
最后,将每个特征的结果拼接,计算最终的输出值:
y
^
=
σ
(
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(e1Res⊕e2Res…eMRes)+b)
损失函数
AutoInt采用log loss 作为模型的损失函数:
L
o
g
l
o
s
s
=
−
1
N
∑
y
i
N
(
y
j
log
y
^
j
+
(
1
−
y
j
)
log
(
1
−
log
y
^
j
)
)
Logloss = - \frac{1}{N}\sum_{y_i}^{N}(y_j\log \hat y_j + (1-y_j)\log(1-\log \hat y_j ))
Logloss=−N1yi∑N(yjlogy^j+(1−yj)log(1−logy^j))
模型需要更新的参数为:
V
i
v
m
W
Q
u
e
r
y
(
h
)
W
V
a
l
u
e
(
h
)
W
K
e
y
(
h
)
W
r
e
s
w
b
\mathbf V_i \quad \mathbf v_m \quad \mathbf W^{(h)}_{Query} \quad \mathbf W^{(h)}_{Value} \quad \mathbf W^{(h)}_{Key} \quad \mathbf W_{res} \quad \mathbf w \quad \mathbf b
VivmWQuery(h)WValue(h)WKey(h)Wreswb
tensorflow 2.0 实现AutoInt
此代码基于tensorflow 2.0 实现AutoInt。
引入必要的库
import tensorflow as tf
import numpy as np
import pandas as pd
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
import tensorflow.keras.backend as K
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder
import os
os.environ['CUDA_VISIABLE_DEVICES'] = '0'
%matplotlib inline
数据预处理
Criteo 数据集一共39个特征,1个label特征,如下图所示:以 l 开头的是数值型特征,以 C 开头的是类别型特征。
train = pd.read_csv('./criteo_sampled_data.csv')
cols = data.columns.values
train.head()
首先,我们对数据进行简单的处理。
定义特征组
dense_feats = [f for f in cols if f[0] == 'I']
sparse_feats = [f for f in cols if f[0] == 'C']
对于数值型特征,用 0 填充,然后进行log变换。
def process_dense_feats(data,feats):
d = data.copy()
d = d[feats].fillna(0.0)
for f in feats:
d[f] = d[f].apply(lambda x: np.log(x+1) if x>-1 else -1)
return d
data_dense = process_dense_feats(train, dense_feats)
对于类别型特征,用’-1’填充,然后进行类别编码。
def process_spares_feats(data,feats):
d = data.copy()
d = d[feats].fillna('-1')
for f in feats:
d[f] = LabelEncoder().fit_transform(d[f])
return d
data_sparse = process_spares_feats(train,sparse_feats)
合并处理后的特征,加入label
total_data = pd.concat([data_dense,data_sparse],axis=1)
total_data['label'] = train['label']
模型的构建
embedding层
dense特征Embedding
k=8 # embedding 维度
# 构造输入
dense_inputs = []
for f in dense_feats:
_input = Input([1], name=f)
dense_inputs.append(_input)
# 对输入进行嵌入
dense_kd_embed = []
for i, _input in enumerate(dense_inputs):
f = dense_feats[i]
embed = tf.Variable(tf.random.truncated_normal(shape=(1, k), stddev=0.01), name=f)
scaled_embed = tf.expand_dims(_input * embed, axis=1)
dense_kd_embed.append(scaled_embed)
print(dense_kd_embed)
sparse特征Embedding
# 构造输入
sparse_inputs = []
for f in sparse_feats:
_input = Input([1], name=f)
sparse_inputs.append(_input)
# 对输入进行嵌入
sparse_kd_embed = []
for i, _input in enumerate(sparse_inputs):
f = sparse_feats[i]
voc_size = data[f].nunique()
_embed = Embedding(voc_size+1, k, embeddings_regularizer=tf.keras.regularizers.l2(0.5))(_input)
sparse_kd_embed.append(_embed)
print(sparse_kd_embed)
合并embedding层
input_embeds = dense_kd_embed + sparse_kd_embed
embed_map = Concatenate(axis=1)(input_embeds) # ?, 39, 8
Interacting Layer
def auto_interacting(embed_map, d=6, n_attention_head=2):
"""
实现单层 AutoInt Interacting Layer
@param embed_map: 输入的embedding feature map, (?, n_feats, n_dim)
@param d: Q,K,V映射后的维度
@param n_attention_head: multi-head attention的个数
"""
assert len(embed_map.shape) == 3, "Input embedding feature map must be 3-D tensor."
k = embed_map.shape[-1]
# 存储多个self-attention的结果
attention_heads = []
W_Q = []
W_K = []
W_V = []
# 1.构建多个attention
for i in range(n_attention_head):
# 初始化W_Q, W_K, W_V
W_Q.append(tf.Variable(tf.random.truncated_normal(shape=(k, d)), name="query_"+str(i))) # k, d 8,6
W_K.append(tf.Variable(tf.random.truncated_normal(shape=(k, d)), name="key_"+str(i))) # k, d
W_V.append(tf.Variable(tf.random.truncated_normal(shape=(k, d)), name="value_"+str(i))) # k, d
for i in range(n_attention_head):
# 映射到d维空间
embed_q = tf.matmul(embed_map, W_Q[i]) # ?, 39, d
embed_k = tf.matmul(embed_map, W_K[i]) # ?, 39, d
embed_v = tf.matmul(embed_map, W_V[i]) # ?, 39, d
# 计算attention
energy = tf.matmul(embed_q, tf.transpose(embed_k, [0, 2, 1])) # ?, 39, 39
attention = tf.nn.softmax(energy) # ?, 39, 39
attention_output = tf.matmul(attention, embed_v) # ?, 39, d
attention_heads.append(attention_output)
# 2.concat multi head
multi_attention_output = Concatenate(axis=-1)(attention_heads) # ?, 39, n_attention_head*d
# 3.ResNet
w_res = tf.Variable(tf.random.truncated_normal(shape=(k, d*n_attention_head)), name="w_res_"+str(i)) # k, d*n_attention_head
output = Activation("relu")(multi_attention_output + tf.matmul(embed_map, w_res)) # ?, 39, d*n_attention_head)
return output
通过循环建立多个交互层
def build_autoint(x0, n_layers):
xl = x0
for i in range(n_layers):
xl = auto_interacting(xl, d=6, n_attention_head=2)
return xl
调用
# 构建3层interacting layer
autoint_layer = build_autoint(embed_map, 3)
展开返回的结果:
autoint_layer = Flatten()(autoint_layer)
输出层
output_layer = Dense(1, activation="sigmoid")(autoint_layer)
模型的编译
# 声明模型:指定输入和输出
model = Model(dense_inputs+sparse_inputs, output_layer)
# 模型的编译,指定loss、优化器、评估指标
model.compile(optimizer="adam",
loss="binary_crossentropy",
metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])
模型的训练与验证
切分训练集和验证集
train_data = total_data.loc[:500000-1]
valid_data = total_data.loc[500000:]
train_dense_x = [train_data[f].values for f in dense_feats]
train_sparse_x = [train_data[f].values for f in sparse_feats]
train_label = [train_data['label'].values]
val_dense_x = [valid_data[f].values for f in dense_feats]
val_sparse_x = [valid_data[f].values for f in sparse_feats]
val_label = [valid_data['label'].values]
此处注意模型的输入与输出的格式:
将数据装入list容器。
模型的训练与验证
model.fit(train_dense_x+train_sparse_x,
train_label, epochs=1, batch_size=256,
validation_data=(val_dense_x+val_sparse_x, val_label),
)
deepctr库实现AutoInt
使用库之前,要先进行安装
!pip install deepctr
引入必要的库
import pandas as pd
import tensorflow as tf
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.feature_column import SparseFeat,DenseFeat,get_feature_names
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
读取并处理数据
data = pd.read_csv('criteo_sampled_data.csv')
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']
对sparse_features进行LabelEncoder,对dense_features进行归一化
for f in sparse_features:
data[f] = LabelEncoder().fit_transform(data[f])
data[dense_features] = MinMaxScaler(feature_range=(0,1)).fit_transform(data[dense_features])
对特征进行嵌入
fixlen_feature_columns = [SparseFeat(feat,data[feat].nunique(),embedding_dim=4) for feat in sparse_features] + \
[DenseFeat(feat,1,) for feat in dense_features]
linear_feature_columns = fixlen_feature_columns
dnn_feature_columns = fixlen_feature_columns
fixlen_feature_names = get_feature_names(fixlen_feature_columns)
构造输入特征,有字典形式和list包裹形式:
# 方式一
train, test = train_test_split(data, test_size=0.2)
train_model_input = [train[name].values for name in fixlen_feature_names]
test_model_input = [test[name].values for name in fixlen_feature_names]
# 方式二
train, test = train_test_split(data, test_size=0.2)
train_model_input = {name: train[name].values for name in fixlen_feature_names}
test_model_input = {name: test[name].values for name in fixlen_feature_names}
模型的声明和编译
model = AutoInt(linear_feature_columns,dnn_feature_columns,att_layer_num=3, att_embedding_size=8, att_head_num=2)
model.compile('adam','binary_crossentropy',metrics=[tf.keras.metrics.AUC(name='auc')])
模型的训练
history = model.fit(train_model_input,train[target].values,batch_size=256, epochs=1,
validation_data = (test_model_input,test[target].values)
)
模型的预测
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))
参考: