行为序列建模:基于Transformer行为表征的BST推荐算法
关键词
:Transformer
,BST
,推荐算法
内容摘要
- 推荐算法中行为序列建模概述
- BST网络结构解析
- BST代码实战,对比WDL,DIN的增益
推荐算法中行为序列建模概述
Embedding+MLP是推荐算法中使用深度学习的一般泛式,即将用户,商品,上下文等其他特征映射到低维稠密向量,再拼接送入全连接层完成二分类任务,这种模型架构忽略了用户的行为序列,包括用户在一段连续时间内对商品的点击收藏购买等行为,而行为序列表达了用户强烈的兴趣信息可以辅助对用户的下一个商品进行预测,以往有基于求和/均值池化的方法将用户交互的商品id序列直接求和/平均,以及基于DIN Attention通过计算和候选商品的相似度对交互的商品id加权求和的方式,这些方法丢弃了行为序列的顺序信息,而随着Transformer在自然语言处理上的成功和对上下文的强大表征能力,阿里巴巴团队将Transformer应用在用户行为序列的表征上,充分学习商品和商品之间的依赖关系,从而引出本文介绍的BST算法。
DIN中候选商品和用户行为序列商品的权重关系BST网络结构解析
BST(Behavior Sequence Transformer)是阿里巴巴团队在2019年提出的推荐排序算法,网络结构如下
BST网络结构
自下而上解析网络,底层有三块输入分别时:
- Other Feature:包括user特征,item特征,上下文特征,cross特征,其中cross特征为人工设计的交叉统计特征,比如年龄和目标商品的交互特征,性别对目标商品的交互特征等
- User Behavior Sequence:用户行为序列特征,输入1~N长度的历史交互商品id和位置信息,其中位置信息采用历史商品交互时间和当前推荐时点的时间差在分箱的embedding进行表征
- Target Item:目标候选商品特征,也包括商品embedding和位置编码embedding
每块的特征进行拼接输入上层网络,其中历史行为序列和候选商品一齐进入Transformer层,通过Transformer层得到每个商品的表征,BST将待预测的 item 也加入Transformer来达到抽取行为序列中的商品与待推荐商品之间的相关性的目的。最终所有特征进行拼接进入一个三层的全连接,通过sigmod进行输出预测结果。 右上角是标准的Transformer的Encoder层,包括多头self attention,残差连接,layer norm等操作,在BST中同样可以堆叠多层Encoder层。
BST的网络结构比较清楚明了,除此之外作者对一些细节进行了补充描述
- 1.Transformer的目的是用来学习商品之间的依赖关系
- 2.序列中的商品特征仅使用商品id和商品类别id已经足够好,增加其他维度表征序列成本太高
- 3.为什么使用时间相减表征位置编码,而不是使用Transformer的sin-cos,作者说是在他们的数据上时间相减的效果好于sin-cos编码
- 4.仅使用一层Transformer Encoder就够了,推叠越多效果越差,原因可能是在推荐场景下学习序列远远没有机器翻译中学习上下文那么复杂
BST代码实战,对比WDL,DIN的增益
本例使用自有的业务数据进行复现测试,特征如下
特征归属 | 特征 | 类型 |
---|---|---|
user | 性别 | 离散 |
user | 年龄 | 分箱离散 |
user | 注册年限 | 分箱离散 |
user | 渠道来源 | 离散 |
user | 会员等级 | 离散 |
... | ... | ... |
item | 商品id | 离散 |
item | 大类id | 离散 |
item | 中类id | 离散 |
item | 小类id | 离散 |
item | 产地id | 离散 |
item | 品牌id | 离散 |
... | ... | ... |
行为序列 | 商品id,行为日期 | 序列 |
使用均值池化序列的WDL模型
先建立WDL(wide deep learn)和DIN两个模型进行对比,其中WDL加入了序列的平均池化,WDL核心代码部分如下
history_seq_emb = tf.concat(
[self.history_seq_item_id_emb, self.history_seq_pty1_id_emb, self.history_seq_pty2_id_emb,
self.history_seq_pty3_id_emb, self.history_seq_brand_id_emb, self.history_seq_origin_id_emb], axis=2)
# 去除mask的均值 [None, seq_len] => [None, seq_len]
mask_weight = tf.expand_dims(self.mask / tf.reduce_sum(self.mask, axis=1, keepdims=True), axis=-1)
history_seq_emb = tf.reduce_sum(history_seq_emb * mask_weight, axis=1)
# [None, emb_size * 6]
target_item_emb = tf.concat(
[self.target_item_id_emb, self.target_item_pty1_id_emb, self.target_item_pty2_id_emb,
self.target_item_pty3_id_emb, self.target_item_brand_id_emb, self.target_item_origin_id_emb], axis=1)
# concat => user_emb_size + emb_size * 6 + emb_size * 6
input = tf.concat([self.user_feature_emb, history_seq_emb, target_item_emb], axis=1)
其中行为序列每个元素由6个embedding拼接而成,采用带有mask的求和平均,最终将用户特征,序列特征,目标商品特征输入给全连接,最终验证集AUC维持在0.796±0.002
使用Attention加权序列的DIN模型
DIN采用DIN Attention采用序列的加权求和,,DIN的核心代码如下
def din_attention(query, facts, mask=None, stag='null', mode='SUM', softmax_stag=1, time_major=False,
return_alphas=False):
queries = tf.tile(query, [1, tf.shape(facts)[1]])
queries = tf.reshape(queries, tf.shape(facts)) # [None, seq_len, emb_size * 6]
din_all = tf.concat([queries, facts, queries - facts, queries * facts], axis=-1)
d_layer_1_all = tf.layers.dense(din_all, 80, activation=tf.nn.sigmoid, name='f1_att' + stag)
d_layer_2_all = tf.layers.dense(d_layer_1_all, 40, activation=tf.nn.sigmoid, name='f2_att' + stag)
d_layer_3_all = tf.layers.dense(d_layer_2_all, 1, activation=None, name='f3_att' + stag)
d_layer_3_all = tf.reshape(d_layer_3_all, [-1, 1, tf.shape(facts)[1]])
scores = d_layer_3_all
if mask is not None:
mask = tf.equal(mask, tf.ones_like(mask))
key_masks = tf.expand_dims(mask, 1) # [B, 1, T]
paddings = tf.ones_like(scores) * (-2 ** 32 + 1)
scores = tf.where(key_masks, scores, paddings) # [B, 1, T]
if softmax_stag:
scores = tf.nn.softmax(scores) # [B, 1, T]
if mode == 'SUM':
output = tf.matmul(scores, facts) # [B, 1, H]
else:
scores = tf.reshape(scores, [-1, tf.shape(facts)[1]])
output = facts * tf.expand_dims(scores, -1)
output = tf.reshape(output, tf.shape(facts))
return output
with tf.name_scope('Attention_layer'):
attention_output = din_attention(target_item_emb, history_seq_emb, self.mask)
att_fea = tf.reduce_sum(attention_output, 1)
input = tf.concat([self.user_feature_emb, att_fea, target_item_emb], axis=1)
输出和WDL类似,只是行为序列变为了注意力加权的结果,DIN的最终验证集AUC稳定在0.801±0.001
BST模型
下面进入进入BST的复现,核心Encoder部分代码如下
def multi_head_attention(self, input):
output_list = []
k_dim = self.token_emb_size * 7 // self.n_head # 28
for n in range(self.n_head):
k_out = tf.layers.dense(input, k_dim, activation=None) # [None, seq_len, 28]
q_out = tf.layers.dense(input, k_dim, activation=None) # [None, seq_len, 28]
v_out = tf.layers.dense(input, k_dim, activation=None) # [None, seq_len, 28]
# dot [None, seq_len, seq_len]
dot = tf.matmul(k_out, tf.transpose(q_out, [0, 2, 1])) / tf.sqrt(
tf.cast(tf.shape(k_out)[-1], dtype='float32'))
self.dot = dot
# mask [None, 1, seq]
mmask = tf.expand_dims((-1e+9) * (1 - self.mask), axis=1)
dot = dot + mmask
attn = tf.nn.dropout(tf.nn.softmax(dot), self.dropout)
# [None, seq_len, seq_len] => [None, seq_len, 24]
output = tf.matmul(attn, v_out)
output_list.append(output)
# [None, seq_len, 24] => [None, seq_len, 192]
outputs = tf.concat(output_list, axis=-1)
outputs = tf.layers.dense(outputs, self.token_emb_size * 7)
outputs = tf.nn.dropout(outputs, self.dropout)
return outputs
def encoder_block(self, input):
"""多头 -> add&norm -> ff -> add&norm"""
attn_output = self.multi_head_attention(input)
attn_output = tf.contrib.layers.layer_norm(inputs=attn_output + input, begin_norm_axis=-1, begin_params_axis=-1)
pff = tf.layers.dense(attn_output, 512, activation=tf.nn.relu)
pff = tf.layers.dense(pff, self.token_emb_size * 7, activation=None)
pff = tf.nn.dropout(pff, self.dropout)
output = tf.contrib.layers.layer_norm(attn_output + pff) # [None, seq_len, 224]
return output
def build_hidden_layer(self):
# transformer的输入
self.history_seq_emb = tf.concat(
[self.history_seq_item_id_emb, self.history_seq_pty1_id_emb, self.history_seq_pty2_id_emb,
self.history_seq_pty3_id_emb, self.history_seq_brand_id_emb, self.history_seq_origin_id_emb,
self.pos_id_emb], axis=2)
encoder_output = self.history_seq_emb
for i in range(self.n_block):
encoder_output = self.encoder_block(encoder_output)
output = tf.reshape(encoder_output, [-1, encoder_output.get_shape()[1] * encoder_output.get_shape()[2]])
output = tf.concat([self.user_feature_emb, output], axis=1)
return output
其中build_hidden_layer为构建Transformer层的入口,将所有商品的Transformer结果全部concat作为输出,在和用户特征进行合并,输入全连接层。最终验证集AUC维持在0.798±0.004。
模型效果总结
从验证集AUC来看BST基本优于均值池化这种base模型,但是不能明显优于DIN,且预测耗时明显高于DIN,存在继续优化的必要。