一、传统推荐系统精排
(一)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的优点
1、维持学习的稳定性
FTRL是通过累积梯度来保证在线学习的稳定性,在每一步都最小化前几步损失函数的总和。
2、保证解的稀疏
通过在FTL中引入了正则项来保证解的稀疏性。
3、解决高维稀疏特征受训机会不均匀
自适应学习率根据每个特征的频次动态调整学习率,频繁更新的特征学习率会逐渐降低,而稀疏更新的特征学习率保持较高。
二、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的进一步优化NFM
如果两个隐向量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的常规优化器。
5、特征处理策略
特征层面,主要分为类别型特征和连续型特征。对于类别型特征,会将它们的字符串值,编码为int型的ID值。比如用户性别,“男性”可以编码为0,“女性”编码为1,“未知性别”编码为2。同时设置了一个准入阈值,即在训练数据中至少出现一定次数的枚举值,才会被编码并加入特征中。这有利于降低长尾值对模型的干扰,同时减小模型体积。连续型特征,会归一化到 [0,1]区间内。
(二)Wide & Deep源码
TensorFlow2中自带对Wide & Deep的实现。
class WideDeepModel(keras_training.Model):
def call(self, inputs, training=None):
linear_inputs, dnn_inputs = inputs
# Wide部分前代,得到logit
linear_output = self.linear_model(linear_inputs)
# Deep部分前代,得到logit
dnn_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得到ctr
return 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计算loss
loss = 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_grads
linear_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端二阶特征自动交叉,减少了人工设计交叉特征的人力物力浪费。两部分的优化器均采用Adam算法。
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对应的bias
for 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了,特别是ReLU
return 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 id
sp_values = features[fieldname + "_values"] # 当前field下所有稀疏特征对应的值
# --------- embedding
embed_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 embedding
squared_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部分贡献的logits
logits = tf.layers.dense(bi_interaction, units=1, use_bias=True, activation=None)
# 因为FM与DNN共享embedding,所以除了logits,还返回各field的embedding,方便搭建DNN
return 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 graph
embedding_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_logits
logits = 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 spec
if 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 ME
loss = 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 spec
assert mode == tf.estimator.ModeKeys.TRAIN
train_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, Optional
import tensorflow as tf
class 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/4
self._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 * x
return 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 tf
def 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位置上的权重都是0
if 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_weights
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, num_heads, dim_key, dim_val, dim_out):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.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_weights
def 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中哪些位置是填充的,不需要attention
dim_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的embedding
X = ...
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作为一个特征交叉的模块,可以只选择一部分重要特征喂入。