本文主要介绍一下Behavior sequence transformer for e-commerce recommendation in Alibaba文章和美团基于此文章所改进的算法。
1
BST模型
Behavior sequence transformer for e-commerce recommendation in Alibaba (https://arxiv.org/pdf/1905.06874.pdf) 模型简称 BST,基于用户的历史行为挖掘用户过去行为的关联性,及对候选商品的影响。其整体的架构如下:
从架构图中看出,主要包含4部分:
(1)输入层;
(2)Embedding层;
(3)Transformer层;
(4)MLP层和输出层;
1.1 输入层
输入的特征包含:other特征,用户行为序列特征,候选商品特征。
(1)other特征,包括:用户画像特征、商品属性特征、上下文特征和交叉特征,这些特征均属于人工基于业务经验提取出的特征,具体见下表:
(2)用户行为序列特征,包括:历史行为中的商品ID序列(item_id)和商品品类ID序列(category_id),位置信息;
(3)候选商品的特征,包括:商品ID和商品品类ID,位置信息;
1.2 Embedding层
(1)other特征Embedding
other特征通过Embedding层将所有的特征映射成固定维度的向量,然后进行 Concat 操作。
(2)用户行为序列特征Embedding
将行为序列中的每个 item(包括目标 item)通过 Embedding 层映射成低维度的向量。每个 item 通过两部分来表示:“序列 item 特征”(红色部分)和“位置特征”(深蓝色部分)。其中:
“序列 item 特征”包括:商品ID序列(item_id)和商品品类ID序列(category_id)。位置特征用来刻画用户历史行为序列中的顺序信息,文中将“位置”作为中每个 item 的另一个输入特征,然后将其投射为低维向量。第 个位置的位置特征计算方式为 。其中: 表示推荐的时间戳, 表示用户点击商品 时的时间戳。作者表示该方式的效果优于 Attention Is All You Need 论文中使用的 sin 和 cos 函数。
(3)候选商品的特征Embedding
“ item 特征”(红色部分)和“位置特征”(深蓝色部分),同(2)
1.3 Transformer layer
本文中使用的 Transformer 层仅是 Attention Is All You Need 论文中的 Encoder 部分,捕捉用户历史行为序列中的各个 item 的关联特征,与此同时,加入候选 item 来达到抽取行为序列中的商品与待推荐商品之间的相关性。
主要包含:自注意力层、前向网络层、堆积自注意力模块。此处做简单介绍,感兴趣的朋友请阅读原论文 Attention Is All You Need 。
(1)自注意力层
使用多头注意力机制,将特征的 Embedding 表达分为多个头,形成多个子空间,可以让模型去关注不同方面的信息,最后再将各个方面的信息综合起来。多次 Attention 综合的结果至少能够起到增强模型的作用,也可以类比 CNN 中同时使用多个卷积核的作用,直观上讲,多头注意力有助于网络捕捉到更丰富的信息。
(2)前向网络层
目的是增加非线性。在 Self-Attention 和 FFN 中都使用了 Dropout 和 LeakyReLU,最终 Self-Attention 和 FFN 的输出为:
(3)堆积自注意力模块
上面的两步操作被称为一个 Self-Attention 单元,为了抽取出 item 序列中更加复杂的潜在关联特征,该模型堆叠了N层 Self-Attention 单元。
1.4 MLP层和输出层
将所有的 Embedding 进行拼接,输入到三层的神经网络中,并最终通过 Sigmoid 函数转换为 0-1 之间的值,代表用户点击目标商品的概率。Loss 函数如下所示:
2
美团改进版:BST+DIN模型
美团改进版地址见:Transformer 在美团搜索排序中的实践 (https://mp.weixin.qq.com/s/Oixc46P9rQeiMDjI-0j0cw) 一文。
主要模型结构如下图所示:
可以看到较 BST 模型,增加了候选商品与 Transformer 的结果做 Attention 的环节(即DIN模型),DIN模型使用注意力机制来捕获目标商品与用户先前行为序列中商品之间的相似性。
美团实践经验:
Transformer 编码为什么有效?Transformer 编码层内部的自注意力机制,能够对序列内 item 的相互关系进行有效的建模来实现更好的表达,并且我们离线实验不加 Transformer 编码层的 Attention-pooling,发现离线 NDCG 下降,从实验上证明了 Transformer 编码有效。
Transformer 编码为什么优于 GRU ?忽略 GRU 的性能差于 Transformer;我们做过实验将行为序列长度的上限往下调,Transformer 的效果相比 GRU 的效果提升在缩小,但是整体还是行为序列的长度越大越好,所以Transformer 相比 GRU 在长距离时,特征捕获能力更强。
位置编码(Pos-Encoding)的影响:我们试过加 Transformer 里面原生的正余弦以及距当前预测时间的时间间隔的位置编码都无效果,分析应该是我们在处理行为序列的时候,已经将序列切割成不同时间段,一定程度上包含了时序位置信息。为了验证这个想法,我们做了仅使用一个长序列的实验(对照组不加位置编码,实验组加位置编码,离线 NDCG 有提升),这验证了我们的猜测。
Transformer 编码层不需要太多,层数过多导致模型过于复杂,模型收敛慢效果不好。
调节多头注意力的“头”数对效果影响不大。
3
BST+DIN 模型实现
根据美团的 BST+DIN 算法结构,人造特征做了简单版的实现。
import tensorflow as tf
from tensorflow.keras.layers import Input, Embedding, concatenate, Flatten, Dense, Dropout
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.utils import plot_model
from transformer import Encoder, padding_mask
from din import DinAttentionLayer, din_padding_mask
def bst_model(sparse_input_length = 1, \
max_seq_length = 50, \
vocab_size_dict = None, \
embedding_dim = 512, \
dnn_unit_list = [512, 128, 32], \
activation = 'relu', \
dropout_rate = 0.2, \
n_layers = 2, \
num_heads = 8, \
middle_units = 1024, \
training = False
):
# 1. Input layer
# 1.1 user
user_id_input_layer = Input(shape=(sparse_input_length, ), name="user_id_input_layer")
gender_input_layer = Input(shape=(sparse_input_length, ), name="gender_input_layer")
age_input_layer = Input(shape=(sparse_input_length, ), name="age_input_layer")
user_click_item_seq_input_layer = Input(shape=(max_seq_length, ), name="user_click_item_seq_input_layer")
user_click_cate_seq_input_layer = Input(shape=(max_seq_length, ), name="user_click_cate_seq_input_layer")
# 1.2 item
item_input_layer = Input(shape=(sparse_input_length, ), name="item_input_layer")
cate_input_layer = Input(shape=(sparse_input_length, ), name="cate_input_layer")
# 2. Embedding layer
# 2.1 user
user_id_embedding_layer = Embedding(vocab_size_dict["user_id"]+1, embedding_dim, \
mask_zero=True, name='user_id_embedding_layer')(user_id_input_layer)
gender_embedding_layer = Embedding(vocab_size_dict["gender"]+1, embedding_dim, \
mask_zero=True, name='gender_embedding_layer')(gender_input_layer)
age_embedding_layer = Embedding(vocab_size_dict["age"]+1, embedding_dim, \
mask_zero=True, name='age_embedding_layer')(age_input_layer)
item_id_embedding = Embedding(vocab_size_dict["item_id"]+1, embedding_dim, \
mask_zero=True, name='item_id_embedding')
cate_id_embedding = Embedding(vocab_size_dict["cate_id"]+1, embedding_dim, \
mask_zero=True, name='cate_id_embedding')
user_click_item_seq_embedding_layer = item_id_embedding(user_click_item_seq_input_layer)
user_click_cate_seq_embedding_layer = cate_id_embedding(user_click_cate_seq_input_layer)
# 2.2 item
target_item_embedding_layer = item_id_embedding(item_input_layer)
target_cate_embedding_layer = cate_id_embedding(cate_input_layer)
# 3. Concat layer
# 3.1 user: other features
other_features_concat_layer = concatenate([user_id_embedding_layer, gender_embedding_layer, \
age_embedding_layer], axis=-1)
# 3.1 user: sequence features
input_transformer_layer = concatenate([user_click_item_seq_embedding_layer, \
user_click_cate_seq_embedding_layer], axis=-1)
# 3.2 item
input_din_layer_query = concatenate([target_item_embedding_layer, \
target_cate_embedding_layer], axis=-1)
# 4. Transformer layer
d_model = input_transformer_layer.shape[-1]
padding_mask_list = padding_mask(user_click_item_seq_input_layer)
output_tranformer_layer = Encoder(n_layers, d_model, num_heads,
middle_units, max_seq_length, training)([input_transformer_layer, padding_mask_list])
# 5. Din attention layer
query = input_din_layer_query
keys = output_tranformer_layer
vecs = output_tranformer_layer
din_padding_mask_list = din_padding_mask(user_click_item_seq_input_layer)
output_din_layer = DinAttentionLayer(d_model, middle_units, dropout_rate)([query, keys, vecs, din_padding_mask_list])
# 6. DNN layer
input_dnn_layer = concatenate([other_features_concat_layer, output_din_layer], \
axis=-1)
input_dnn_layer = tf.squeeze(input=input_dnn_layer, axis=[1])
for inx in range(len(dnn_unit_list)):
input_dnn_layer = Dense(dnn_unit_list[inx], activation=activation, \
name="FC_{0}".format(inx+1))(input_dnn_layer)
input_dnn_layer = Dropout(dropout_rate, name="dropout_{0}".format(inx+1))(input_dnn_layer)
output = Dense(1, activation='sigmoid', \
name='Sigmoid_output_layer')(input_dnn_layer)
# Output model
inputs_list = [user_id_input_layer, gender_input_layer, age_input_layer, \
user_click_item_seq_input_layer, user_click_cate_seq_input_layer, \
item_input_layer, cate_input_layer]
model = Model(inputs = inputs_list, outputs = output)
return model
if __name__ == "__main__":
vocab_size_dict = {
"user_id": 300,
"gender": 2,
"age": 10,
"item_id": 5000,
"cate_id": 213}
bst_model = bst_model(vocab_size_dict=vocab_size_dict)
print(bst_model.summary())
plot_model(bst_model, to_file='bst_model.png')
模型结构图如下所示:
其中:Encoder 层和 DinAttentionLayer 层为自定义层。详情请参考:https://github.com/wziji/deep_ctr/tree/master/BST
欢迎关注 “python科技园” 及 添加小编 进群交流。
喜欢的话请分享、点赞、在看吧~