推荐系统实战(四)精排-交叉结构
一、传统推荐系统精排
(一)LR技术
在利用深度学习进行精排之前,主要使用LR(linear regression)技术。
1、LR公式
w是特征权重向量,xi是样本的第i个特征的值。针对推荐系统中的类别问题,比如有无收藏、有无点赞,特征值设置为0或1,因此可由1式简化获得2式。
2、LR的优缺点
优点是强于记忆,缺点是扩展性差。
(二)推荐系统对模型的技术要求
1、模型必须可以实时、在线学习的能力
(1)原因
用户和系统交互频繁,模型需要根据用户反馈快速调整。
(2)在线学习流程
- 用户在前端交互,触发后台服务将需要处理的数据(如用户信息和候选物料信息)打包成一个请求,发送给Ranker服务。
- Ranker从当前用户信息和当前物料信息中提取出特征,喂给排序模型,对候选物料进行打分排序,推送给用户。
- 与此同时提取好的特征组成特征快照,发送给拼接服务joiner。
- 用户对第二步推送的物料进行交互,反馈发送给拼接服务joiner。
- 拼接服务将同一条请求的特征快照和反馈进行拼接作为新样本,发送给trainer进行训练。
- trainer利用这批新样本进行训练,增量更新模型参数。
- 更新后的模型参数传递给ranker。
2、模型参数必须稀疏
针对海量数据加高维稀疏特征,推荐系统需要挖掘相对关键的特征,而非关键特征的权重就需要相对稀疏。LR的OGD解法,虽然预测精度不错,但是输出的权重还是不够稀疏。
一、FTRL
(一)FTRL原理
1、FTL:Follow The Leader
FTL并不单指某个算法,指的是一种在线学习的思路。即为了减少单个样本的随机扰动,第t步的最优参数并不是单单最小化第t步的损失,而是使得前t步的损失之和最小。
2、 FTRL与FTL的区别
①FTRL在FTL基础之上添加了正则项。
②FTRL放弃统一步长,为每个特征单独设置步长。
(二)FTRL的Python实现:略
二、FM
(一)FM与LR的渊源
LR模型强于记忆而弱于扩展,并且聚焦于一阶特征。FM是在此LR基础上引入二阶交叉特征。得到下面公式:
但是由于推荐系统数据海量且特征高维稀疏,xi或xj为0的可能性很高,交叉特征学习不到,剥夺了小众模式被挖掘的可能。考虑到以上弊端,虽然二阶交叉特征可能没有出现过,但是xi和xj有单独出现,FM引入特征的embedding,在下式中以向量v表示。于是在训练的过程中相当于间接训练了wij,使得数据利用率更高,训练更加充分,便于挖掘小众模式。
(二)FM的优点
1、减少了学习的参数量
在如下公式中,若一个样本有n个特征,需要学习参数量的量级为n²:
而在下面公式中,通过引入embedding,学习参数的量级为nk,k为embedding的长度:
2、提高了扩展性
FM为每个特征引入embedding,且引入了允许所有特征进行自动二阶交叉的结构,大大提升了扩展性,可以有效地处理稀疏数据。
(三)FM的进一步优化
如果两个隐向量vi和vj按位相乘,则得到的结果是一个新向量:
这种优化后的模型最后一项可以表示为:
优点:①按位相乘允许模型在每个维度上单独控制特征之间的交互,而不是像内积那样得到一个总体的交互评分。这种方式能捕捉到更细粒度的特征关系,有利于处理更加复杂和非线性的交互场景。②按位相乘的方式能够更好地捕捉非线性关系,特别是在推荐系统中,用户和物品之间的交互往往是非线性的。通过按位相乘,可以更好地表达这些非线性关系。
三、Wide & Deep:兼具记忆和扩展
(一)Wide & Deep算法原理
1、Wide & Deep整体网络结构
Wide侧是一个浅层网络,Deep侧是一个深层网络。
2、Deep侧
- Deep侧遵守的设计范式:Embedding+MLP。可以简单表示为如下公式:
其中x_deep表示输入到Deep侧的特征,Embedding表示将稀疏特征映射为稠密向量的过程,通过若干Embedding层来实现。每个Feature Field都对应着一个Embedding层,而当一个feature field中包含若干feature时,这个field的embedding就是域中若干feature的embedding池化后的结果。而Concat则表示将若干field的embedding拼接成一个大向量,随后喂给上层DNN,最终得到一个结果。
- Deep侧的优点:①通过引入Embedding扩展了特征内涵。②通过DNN对特征进行高阶隐式交叉。通过以上两个优点结合,提高了模型的扩展性,助于挖掘小众、独特的模式。
3、Wide侧
- Wide侧遵守的设计范式:一个简单的LR。可以简单表示为以下公式:
- Wide侧的用处:①LR的优点在于强于记忆,而wide侧则用于记忆一些高频、大众的模式。②防止Deep侧过度扩展影响精度。
- Wide侧输入的特征:①一些先验知识认定的精华特征,比如一些人工筛选出的交叉特征。②一些影响推荐系统的偏差特征,比如位置偏置等。
4、两侧共同训练
拿CTR举例,可以简单表示模型的预测结果为如下公式:
Wide侧采用FTRL优化,Deep侧采用DNN的常规优化器。
(二)Wide & Deep源码
TensorFlow2中自带对Wide & Deep的实现。
class WideDeepModel(keras_training.Model):def call(self, inputs, training=None):linear_inputs, dnn_inputs = inputs# Wide部分前代,得到logitlinear_output = self.linear_model(linear_inputs)# Deep部分前代,得到logitdnn_output = self.dnn_model(dnn_inputs)# Wide logits与Deep logits相加output = tf.nest.map_structure(lambda x, y: (x + y), linear_output, dnn_output)# 一般采用sigmoid激活函数,由logit得到ctrreturn tf.nest.map_structure(self.activation, output)def train_step(self, data):x, y, sample_weight = data_adapter.unpack_x_y_sample_weight(data)# ------------- 前代# GradientTape是TF2自带功能,GradientTape内的操作能够自动求导with tf.GradientTape() as tape:y_pred = self(x, training=True) # 前代# 由外界设置的compiled_loss计算lossloss = self.compiled_loss(y, y_pred, sample_weight, regularization_losses=self.losses)# ------------- 回代linear_vars = self.linear_model.trainable_variables # Wide部分的待优化参数dnn_vars = self.dnn_model.trainable_variables # Deep部分的待优化参数# 分别计算loss对linear_vars的导数linear_grads# 和loss对dnn_vars的导数dnn_gradslinear_grads, dnn_grads = tape.gradient(loss, (linear_vars, dnn_vars))# 一般用FTRL优化Wide侧,以得到更稀疏的解linear_optimizer = self.optimizer[0]linear_optimizer.apply_gradients(zip(linear_grads, linear_vars))# 用Adam、Adagrad优化Deep侧dnn_optimizer = self.optimizer[1]dnn_optimizer.apply_gradients(zip(dnn_grads, dnn_vars))
四、DeepFM
(一)DeepFM算法原理
1、DeepFM与Wide & Deep
DeepFM是在Wide & Deep的Wide端加以改进的,引入了FM进行Wide端二阶特征自动交叉,减少了人工设计交叉特征的人力物力浪费。
2、DeepFM原理简述
DeepFM原理可由以下公式简述:
一般来说输入FM的feature和输入DNN的feature相同。而x_lr是先验知识中认为重要的feature,比如位置偏差等。
(二)Tensorflow实现DeepFM
重要概念澄清:
①比如我们的特征集中包括active_pkgs(app活跃情况)、install_pkgs(app安装情况)、uninstall_pkgs(app卸载情况)。每列所包含的内容是一系列tag和其数值,比如qq:0.1, weixin:0.9, taobao:1.1,但是这些tag都来源于同一份名为package的字典。
②feature field就是active_pkgs、install_pkgs、uninstall_pkgs这些大类,是DataFrame中的每一列。tag就是每个field下包含的具体内容,一个field下允许多个tag存在。
③vocabulary,若干个field下的tag可以来自同一个vocabulary,即若干field共享vocabulary。
举个例子,有三个feature field,分别为active_pkgs、install_pkgs、uninstall_pkgs。在这些field下有若干tag,tag是以键值对的形式捆绑存在的,假设tag有qq、weixin、taobao。而词汇表中就包含着所有的tag。
1、Embedding
class EmbeddingTable:def __init__(self):self._weights = {}def add_weights(self, vocab_name, vocab_size, embed_dim):""":param vocab_name: 一个field拥有两个权重矩阵,一个用于线性连接,另一个用于非线性(二阶或更高阶交叉)连接:param vocab_size: 字典总长度:param embed_dim: 二阶权重矩阵shape=[vocab_size, order2dim],映射成的embedding既用于接入DNN的第一屋,也是用于FM二阶交互的隐向量:return: None"""linear_weight = tf.get_variable(name='{}_linear_weight'.format(vocab_name),shape=[vocab_size, 1],initializer=tf.glorot_normal_initializer(),dtype=tf.float32)# 二阶(FM)与高阶(DNN)的特征交互,共享embedding矩阵embed_weight = tf.get_variable(name='{}_embed_weight'.format(vocab_name),shape=[vocab_size, embed_dim],initializer=tf.glorot_normal_initializer(),dtype=tf.float32)self._weights[vocab_name] = (linear_weight, embed_weight)def get_linear_weights(self, vocab_name): return self._weights[vocab_name][0]def get_embed_weights(self, vocab_name): return self._weights[vocab_name][1]def build_embedding_table(params):embed_dim = params['embed_dim'] # 必须有统一的embedding长度embedding_table = EmbeddingTable()for vocab_name, vocab_size in params['vocab_sizes'].items():embedding_table.add_weights(vocab_name=vocab_name, vocab_size=vocab_size, embed_dim=embed_dim)return embedding_table
2、LR输出
def output_logits_from_linear(features, embedding_table, params):field2vocab_mapping = params['field_vocab_mapping']combiner = params.get('multi_embed_combiner', 'sum')fields_outputs = []# 当前field下有一系列的<tag:value>对,每个tag对应一个bias(待优化),# 将所有tag对应的bias,按照其value进行加权平均,得到这个field对应的biasfor fieldname, vocabname in field2vocab_mapping.items():sp_ids = features[fieldname + "_ids"]sp_values = features[fieldname + "_values"]linear_weights = embedding_table.get_linear_weights(vocab_name=vocabname)# weights: [vocab_size,1]# sp_ids: [batch_size, max_tags_per_example]# sp_weights: [batch_size, max_tags_per_example]# output: [batch_size, 1]output = embedding_ops.safe_embedding_lookup_sparse(linear_weights, sp_ids, sp_values,combiner=combiner,name='{}_linear_output'.format(fieldname))fields_outputs.append(output)# 因为不同field可以共享同一个vocab的linear weight,所以将各个field的output相加,会损失大量的信息# 因此,所有field对应的output拼接起来,反正每个field的output都是[batch_size,1],拼接起来,并不占多少空间# whole_linear_output: [batch_size, total_fields]whole_linear_output = tf.concat(fields_outputs, axis=1)tf.logging.info("linear output, shape={}".format(whole_linear_output.shape))# 再映射到final logits(二分类,也是[batch_size,1])# 这时,就不要用任何activation了,特别是ReLUreturn tf.layers.dense(whole_linear_output, units=1, use_bias=True, activation=None)
3、二阶交叉结果输出
def output_logits_from_bi_interaction(features, embedding_table, params):# 见《Neural Factorization Machines for Sparse Predictive Analytics》论文的公式(4)fields_embeddings = [] # 每个field的embedding,是每个field所包含的feature embedding的和fields_squared_embeddings = [] # 每个元素,是当前field所有feature embedding的平方的和for fieldname, vocabname in field2vocab_mapping.items():sp_ids = features[fieldname + "_ids"] # 当前field下所有稀疏特征的feature idsp_values = features[fieldname + "_values"] # 当前field下所有稀疏特征对应的值# --------- embeddingembed_weights = embedding_table.get_embed_weights(vocabname) # 得到embedding矩阵# 当前field下所有feature embedding求和# embedding: [batch_size, embed_dim]embedding = embedding_ops.safe_embedding_lookup_sparse(embed_weights, sp_ids, sp_values,combiner='sum',name='{}_embedding'.format(fieldname))fields_embeddings.append(embedding)# --------- square of embeddingsquared_emb_weights = tf.square(embed_weights) # embedding矩阵求平方# 稀疏特征的值求平方squared_sp_values = tf.SparseTensor(indices=sp_values.indices,values=tf.square(sp_values.values),dense_shape=sp_values.dense_shape)# 当前field下所有feature embedding的平方的和# squared_embedding: [batch_size, embed_dim]squared_embedding = embedding_ops.safe_embedding_lookup_sparse(squared_emb_weights, sp_ids, squared_sp_values,combiner='sum',name='{}_squared_embedding'.format(fieldname))fields_squared_embeddings.append(squared_embedding)# 所有feature embedding,先求和,再平方sum_embedding_then_square = tf.square(tf.add_n(fields_embeddings)) # [batch_size, embed_dim]# 所有feature embedding,先平方,再求和square_embedding_then_sum = tf.add_n(fields_squared_embeddings) # [batch_size, embed_dim]# 所有特征两两交叉的结果,形状是[batch_size, embed_dim]bi_interaction = 0.5 * (sum_embedding_then_square - square_embedding_then_sum)# 由FM部分贡献的logitslogits = tf.layers.dense(bi_interaction, units=1, use_bias=True, activation=None)# 因为FM与DNN共享embedding,所以除了logits,还返回各field的embedding,方便搭建DNNreturn logits, fields_embeddings
4、DNN输出和最终结果输出
def output_logits_from_dnn(fields_embeddings, params, is_training):dropout_rate = params['dropout_rate']do_batch_norm = params['batch_norm']X = tf.concat(fields_embeddings, axis=1)tf.logging.info("initial input to DNN, shape={}".format(X.shape))for idx, n_units in enumerate(params['hidden_units'], start=1):X = tf.layers.dense(X, units=n_units, activation=tf.nn.relu)tf.logging.info("layer[{}] output shape={}".format(idx, X.shape))X = tf.layers.dropout(inputs=X, rate=dropout_rate,training=is_training)if is_training:tf.logging.info("layer[{}] dropout {}".format(idx, dropout_rate))if do_batch_norm:# BatchNormalization的调用、参数,是从DNNLinearCombinedClassifier源码中拷贝过来的batch_norm_layer = normalization.BatchNormalization(momentum=0.999, trainable=True,name='batchnorm_{}'.format(idx))X = batch_norm_layer(X, training=is_training)if is_training:tf.logging.info("layer[{}] batch-normalize".format(idx))# connect to final logits, [batch_size,1]return tf.layers.dense(X, units=1, use_bias=True, activation=None)def model_fn(features, labels, mode, params):for featname, featvalues in features.items():if not isinstance(featvalues, tf.SparseTensor):raise TypeError("feature[{}] isn't SparseTensor".format(featname))# ============= build the graphembedding_table = build_embedding_table(params)linear_logits = output_logits_from_linear(features, embedding_table, params)bi_interact_logits, fields_embeddings = output_logits_from_bi_interaction(features, embedding_table, params)dnn_logits = output_logits_from_dnn(fields_embeddings, params, (mode == tf.estimator.ModeKeys.TRAIN))general_bias = tf.get_variable(name='general_bias', shape=[1], initializer=tf.constant_initializer(0.0))logits = linear_logits + bi_interact_logits + dnn_logitslogits = tf.nn.bias_add(logits, general_bias) # bias_add,获取broadcasting的便利# reshape [batch_size,1] to [batch_size], to match the shape of 'labels'logits = tf.reshape(logits, shape=[-1])probabilities = tf.sigmoid(logits)# ============= predict specif mode == tf.estimator.ModeKeys.PREDICT:return tf.estimator.EstimatorSpec(mode=mode,predictions={'probabilities': probabilities})# ============= evaluate spec# 这里不设置regularization,模仿DNNLinearCombinedClassifier的做法, L1/L2 regularization通过设置optimizer=# tf.train.ProximalAdagradOptimizer(learning_rate=0.1,# l1_regularization_strength=0.001,# l2_regularization_strength=0.001)来实现# STUPID TENSORFLOW CANNOT AUTO-CAST THE LABELS FOR MEloss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=tf.cast(labels, tf.float32)))eval_metric_ops = {'auc': tf.metrics.auc(labels, probabilities)}if mode == tf.estimator.ModeKeys.EVAL:return tf.estimator.EstimatorSpec(mode=mode,loss=loss,eval_metric_ops=eval_metric_ops)# ============= train specassert mode == tf.estimator.ModeKeys.TRAINtrain_op = params['optimizer'].minimize(loss, global_step=tf.train.get_global_step())return tf.estimator.EstimatorSpec(mode,loss=loss,train_op=train_op,eval_metric_ops=eval_metric_ops)
五、DCN:Deep&Cross Network
(一)原理
1、DCNv1
DCNv1有多层Cross Layer,每层需要优化的参数只有两个w_l和b_l:
其中x_0是原始输入(也就是最低层Cross Layer的输入),由embedding和稠密特征向量构成。
x_l和x_l+1分别是第l层Cross Layer的输入和输出。
假设一个DCN中有L层Cross Layer,那么对于原始输入x_0=[f1,f2,f3,...,fd]来说,可以获得不大于L+1阶的所有可能的高阶特征交叉。
2、DCNv2
DCNv2认为DCNv1中需要优化的参数量过小,因此提出用矩阵W_l代替w_l。于是得到下式:
但是由于实际场景中,x_0的维度往往很高,引入W_l后要学习的参数量过多,因此提出将W_l分解为两个低维矩阵的乘积。
3、DCN的实战缺点
①喂给DCN的特征一般都是经过挑选的潜在特征。
②DCN输入和输出都是d维,只做了信息交叉,没有做到信息的压缩和提取。
4、DCN和DNN的融合
①并联Parallel
x_0分别喂给DCN和DNN,将两者的结果相加得到最终结果。
②串联Stacked:
将x_0喂给DCN以后输出的结果再喂给DNN,得到最终结果。
(二)源码
# Copyright 2022 The TensorFlow Recommenders Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License."""Implements `Cross` Layer, the cross layer in Deep & Cross Network (DCN)."""from typing import Union, Text, Optionalimport tensorflow as tfclass Cross(tf.keras.layers.Layer):"""一层Cross Layer"""def __init__(self, ......):super(Cross, self).__init__(**kwargs)self._projection_dim = projection_dim # 矩阵分解时采用的中间维度self._diag_scale = diag_scale # 非负小数,用于改善训练稳定性self._use_bias = use_bias......def build(self, input_shape): # 定义本层要优化的参数last_dim = input_shape[-1] # 输入的维度# [d,r]的小矩阵,d是原始特征的长度,r就是这里的_projection_dim# r << d以提升模型的计算效率,一般取r=d/4self._dense_u = tf.keras.layers.Dense(self._projection_dim, use_bias=False, )# [r,d]的小矩阵self._dense_v = tf.keras.layers.Dense(last_dim, use_bias=self._use_bias,)def call(self, x0: tf.Tensor, x: Optional[tf.Tensor] = None) -> tf.Tensor:""" x0与x计算一次交叉x0: 原始特征,一般是embedding layer的输出。一个[B,D]的矩阵B=batch_size,D是原始特征的长度x: 上一个Cross层的输出结果,形状也是[B,D]输出: 也是形状为[B,D]的矩阵 """if x is None:x = x0 # 针对第一层# 输出是x_{i+1} = x0 .* (W * xi + bias + diag_scale * xi) + xi,# 其中.* 代表按位相乘,# W分解成两个小矩阵的乘积,W=U*V,以减少计算开销,# diag_scale非负小数,加到W的对角线上,以增强训练稳定性prod_output = self._dense_v(self._dense_u(x))if self._diag_scale:# 加大W的对角线,增强训练稳定性prod_output = prod_output + self._diag_scale * xreturn x0 * prod_output + x
六、AutoInt:基于transformer的特征交叉
(一)Transformer
Transformer的核心就是注意力机制。以下的例子中,输入由三部分构成:Q,K,V。其中Q代表Query,表示当前的查询信息;K代表Key,通过计算Query与Key的相似度来确定权重;V代表Value,权重与Value Embedding进行加权求值,输出最终结果为一个embedding向量。公式如下所示:
其中通过可以计算得到当前查询与Key的相似度,通过除以来平缓相似度,再通过softmax进行归一化。随后与V进行加权求值输出有意义的embedding。
其中Attention机制对输入的形状有要求,Q矩阵的大小为[B,Lq,dk],K矩阵的大小为[B,Lk,dk],V矩阵的大小为[B,Lk,dv]。其中B代表batch size,Lq是Query序列的长度,Lk是Key和Value序列的长度,dk是Query和key嵌入向量的长度,dv是value嵌入向量的长度。最终输出的结果是一个形状为[B,Lq,dv]的矩阵,它的第i行第j列表示第i个样本中第j个Query的视角。
举一个电影推荐系统的简单例子,如果要得到当前查询电影A的推荐程度。Query矩阵由要查询的候选电影的embedding向量组成,Key矩阵由用户历史消费电影的embedding组成,Value矩阵由用户历史消费电影对应的评价、消费次数等附加信息的embedding组成。输入电影A的query embedding向量,计算它与Key中用户历史消费电影的相似度,计算出权重后,与value中的embedding向量相乘,最后得到一个融合了所有value embedding信息的embedding向量,可以表示预测的电影A的推荐信息。
(二)multi-head attention
为了增强模型的表达能力,Transformer采用multi-head attention机制。将原始的Q/K/V投射到不同的子空间进行特征的交叉,可以描述为以下公式:
其中可以通过三个W_i矩阵,将输入映射到规定的形状,这通过三个线性层实现。
head_i为各个头的attention结果,最后拼接起来,再通过Wo矩阵映射成需要的形状,这是通过顶层线性层实现的。
MultiHeadAttention的结果喂给MLP做进一步的非线性变换,为了训练稳定性,引入Layer Norm和Residual结构,这样一个transformer就构建完成了。
可以通过叠加多个transformer来实现序列特征向更高阶的交叉。
源码如下:
import tensorflow as tfdef create_padding_mask(seq):"""seq: [batch_size, seq_len]的整数矩阵。如果某个元素==0,代表那个位置是padding"""# (batch_size, seq_len)seq = tf.cast(tf.math.equal(seq, 0), tf.float32)# 返回结果:(batch_size, 1, 1, seq_len)# 加入中间两个长度=1的维度,是为了能够broadcast成希望的形状return seq[:, tf.newaxis, tf.newaxis, :] # (batch_size, 1, 1, seq_len)def scaled_dot_product_attention(q, k, v, mask):"""输入:q: (batch_size, num_heads, seq_len_q, dim_key)k: (batch_size, num_heads, seq_len_k, dim_key)v: (batch_size, num_heads, seq_len_k, dim_val)mask: 必须能够broadcastableto (..., seq_len_q, seq_len_k)的形状输出:output: q对k/v做attention的结果, (batch_size, num_heads, seq_len_q, dim_val)attention_weights: q对k的注意力权重, (batch_size, num_heads, seq_len_q, seq_len_k)"""# q: (batch_size, num_heads, seq_len_q, dim_key)# k: (batch_size, num_heads, seq_len_k, dim_key)# matmul_qk: 每个head下,每个q对每个k的注意力权重(尚未归一化)# (batch_size, num_heads, seq_len_q, seq_len_k)matmul_qk = tf.matmul(q, k, transpose_b=True)# 为了使训练更稳定,除以sqrt(dim_key)dk = tf.cast(tf.shape(k)[-1], tf.float32)scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)# 在mask的地方,加上一个极负的数,-1e9,保证在softmax后,mask位置上的权重都是0if mask is not None:# mask的形状一般是(batch_size, 1, 1, seq_len_k)# 但是能够broadcast成与scaled_attention_logits相同的形状# (batch_size, num_heads, seq_len_q, seq_len_k)scaled_attention_logits += (mask * -1e9)# 沿着最后一维(i.e., seq_len_k)用softmax归一化# 保证一个query对所有key的注意力权重之和==1# attention_weights: (batch_size, num_heads, seq_len_q, seq_len_k)attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)# attention_weights: (batch_size, num_heads, seq_len_q, seq_len_k)# v: (batch_size, num_heads, seq_len_k, dim_val)# output: (batch_size, num_heads, seq_len_q, dim_val)output = tf.matmul(attention_weights, v)# output: (batch_size, num_heads, seq_len_q, dim_val)# attention_weights: (batch_size, num_heads, seq_len_q, seq_len_k)return output, attention_weightsclass MultiHeadAttention(tf.keras.layers.Layer):def __init__(self, num_heads, dim_key, dim_val, dim_out):super(MultiHeadAttention, self).__init__()self.num_heads = num_headsself.dim_key = dim_key # 每个query和key都要映射成相同的长度# 每个value要映射成的长度self.dim_val = dim_val if dim_val is not None else dim_key# 定义映射矩阵self.wq = tf.keras.layers.Dense(num_heads * dim_key)self.wk = tf.keras.layers.Dense(num_heads * dim_key)self.wv = tf.keras.layers.Dense(num_heads * dim_val)self.wo = tf.keras.layers.Dense(dim_out) # dim_out:希望输出的维度长def split_heads(self, x, batch_size, dim):# 输入x: (batch_size, seq_len, num_heads * dim)# 输出x: (batch_size, seq_len, num_heads, dim)x = tf.reshape(x, (batch_size, -1, self.num_heads, dim))# 最终输出:(batch_size, num_heads, seq_len, dim)return tf.transpose(x, perm=[0, 2, 1, 3])def call(self, q, k, v, mask):"""输入:q: (batch_size, seq_len_q, old_dq)k: (batch_size, seq_len_k, old_dk)v: (batch_size, seq_len_k, old_dv),与k序列相同长度mask: 可以为空,否则形状为(batch_size, 1, 1, seq_len_k),表示哪个key不需要做attention输出:output: Attention结果,(batch_size, seq_len_q, dim_out)attention_weights: Attention权重,(batch_size, num_heads, seq_len_q, seq_len_k)"""# **************** 将输入映射成希望的形状batch_size = tf.shape(q)[0]q = self.wq(q) # (batch_size, seq_len_q, num_heads * dim_key)k = self.wk(k) # (batch_size, seq_len_k, num_heads * dim_key)v = self.wv(v) # (batch_size, seq_len_k, num_heads * dim_val)# (bs, nh, seq_len_q, dim_key)q = self.split_heads(q, batch_size, self.dim_key)# (bs, nh, seq_len_k, dim_key)k = self.split_heads(k, batch_size, self.dim_key)# (bs, nh, seq_len_k, dim_val)v = self.split_heads(v, batch_size, self.dim_val)# **************** Multi-Head Attention# scaled_attention: (batch_size, num_heads, seq_len_q, dim_val)# attention_weights:(batch_size, num_heads, seq_len_q, seq_len_k)scaled_attention, attention_weights = scaled_dot_product_attention(q, k, v, mask)# **************** 将Attention结果映射成希望的形状# (batch_size, seq_len_q, num_heads, dim_val)scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])# (batch_size, seq_len_q, num_heads * dim_val)concat_attention = tf.reshape(scaled_attention,(batch_size, -1, self.num_heads * self.dim_val))output = self.wo(concat_attention) # (batch_size, seq_len_q, dim_out)return output, attention_weightsdef target_attention():target_item_embedding = ... # 候选item的embedding, [batch_size, dim_target]user_behavior_seq = ... # 某个用户行为序列, [batch_size, seq_len, dim_seq]padding_mask = ... # user_behavior_seq中哪些位置是填充的,不需要Attention# 把候选item,变形成一个长度为1的序列query = tf.reshape(target_item_embedding, [-1, 1, dim_target])# atten_result: (batch_size, 1, dim_out)attention_layer = MultiHeadAttention(num_heads, dim_key, dim_val, dim_out)atten_result, _ = attention_layer(q=query, # query就是候选物料k=user_behavior_seq,v=user_behavior_seq,mask=padding_mask)# reshape去除中间不必要的1维# user_interest_emb是提取出来的用户兴趣向量,喂给上层模型,参与CTR建模user_interest_emb = tf.reshape(atten_result, [-1, dim_out])def double_attention():target_item_embedding = ... # 候选item的embedding, [batch_size, dim_target]user_behavior_seq = ... # 某个用户行为序列, [batch_size, seq_len, dim_in_seq]padding_mask = ... # user_behavior_seq中哪些位置是填充的,不需要attentiondim_in_seq = tf.shape(user_behavior_seq)[-1] # sequence中每个element的长度# *********** 第一层做Self-Attention,建模序列内部的依赖性self_atten_layer = MultiHeadAttention(num_heads=n_heads1,dim_key=dim_in_seq,dim_val=dim_in_seq,dim_out=dim_in_seq)# 做self-attention,q=k=v=user_behavior_seq# 输入q/k/v与输出self_atten_seq,它们的形状都是# [batch_size, len(user_behavior_seq), dim_in_seq]self_atten_seq, _ = self_atten_layer(q=user_behavior_seq,k=user_behavior_seq,v=user_behavior_seq,mask=padding_mask)# *********** 第二层做Target-Attention,建模候选item与行为序列的相关性target_atten_layer = MultiHeadAttention(num_heads=n_heads2,dim_key=dim_key,dim_val=dim_val,dim_out=dim_out)# 把候选item,变形成一个长度为1的序列target_query = tf.reshape(target_item_embedding, [-1, 1, dim_target])# atten_result: (batch_size, 1, dim_out)atten_result, _ = target_atten_layer(q=target_query, # 代表候选物料k=self_atten_seq, # 以self-attention结果作为target-attention的对象v=self_atten_seq,mask=padding_mask)# reshape去除中间不必要的1维# user_interest_emb是提取出来的用户兴趣向量,喂给上层模型,参与CTR建模user_interest_emb = tf.reshape(atten_result, [-1, dim_out])def auto_int():# 原始特征的拼接而成的矩阵,[batch_size, num_fields, dim]# num_fields:一共有多少个field# dim:每个field都被映射成相当长度为dim的embeddingX = ...attention_layer = MultiHeadAttention(num_heads, dim_key, dim_val, dim_out)
在代码中引入了mask。mask的作用是让attention忽略序列中指定的Key/Value。比如说在一个batch中有两个用户历史观看序列,两者的序列长短不一,为了保持长度一致便于后续处理,采取填充或者截断的方式,如下图所示。其中有效的历史观看记录只有v1-v4四个,其他填充为0的部分即mask要求attention忽略的。
在执行 Softmax 操作之前,我们通过应用掩码(mask)来处理填充(padding)部分。具体来说,我们将掩码与一个非常大的负数相乘(例如 -1e9
),然后将这个结果加到已经计算得到的注意力分数上。这样做的目的是使得填充部分在经过 Softmax 函数转换后,其对应的权重变得极其接近于零,从而确保这些部分在后续的计算中几乎没有影响。
(三)AutoInt的实现
1、AutoInt的实现流程
- 将所有feature field映射成embedding向量。
- 将所有feature field的embedding向量拼接成一个矩阵。
- 喂给transformer。
- transformer得到的结果再喂给一个浅层DNN得到最后的预测结果。
2、AutoInt的缺点
①为了使用self-attention要求所有feature field的embedding具有相同的长度。
②AutoInt和DCN一样只做信息交叉而不做压缩,导致时间开销大。
3、实践中的AutoInt应用
将AutoInt作为一个特征交叉的模块,可以只选择一部分重要特征喂入。