当前位置: 首页 > news >正文

推荐系统实战(四)精排-交叉结构

一、传统推荐系统精排

(一)LR技术

在利用深度学习进行精排之前,主要使用LR(linear regression)技术。

1、LR公式

w是特征权重向量,xi是样本的第i个特征的值。针对推荐系统中的类别问题,比如有无收藏、有无点赞,特征值设置为0或1,因此可由1式简化获得2式。

CTR_{predict}=sigmod(\sum{w_ix_i})

CTR_{predict}=sigmod(\sum{w_i})

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基础上引入二阶交叉特征。得到下面公式:

logit_{FM}=b+\sum_{i=1}^nw_ix_i+\sum_{j=i+1}^nw_{ij}x_ix_j

但是由于推荐系统数据海量且特征高维稀疏,xi或xj为0的可能性很高,交叉特征学习不到,剥夺了小众模式被挖掘的可能。考虑到以上弊端,虽然二阶交叉特征可能没有出现过,但是xi和xj有单独出现,FM引入特征的embedding,在下式中以向量v表示。于是在训练的过程中相当于间接训练了wij,使得数据利用率更高,训练更加充分,便于挖掘小众模式。

(二)FM的优点

1、减少了学习的参数量

在如下公式中,若一个样本有n个特征,需要学习参数量的量级为n²:

logit_{FM}=b+\sum_{i=1}^nw_ix_i+\sum_{j=i+1}^nw_{ij}x_ix_j

而在下面公式中,通过引入embedding,学习参数的量级为nk,k为embedding的长度:

2、提高了扩展性

FM为每个特征引入embedding,且引入了允许所有特征进行自动二阶交叉的结构,大大提升了扩展性,可以有效地处理稀疏数据。

(三)FM的进一步优化

如果两个隐向量vi和vj按位相乘,则得到的结果是一个新向量:

这种优化后的模型最后一项可以表示为:

优点:①按位相乘允许模型在每个维度上单独控制特征之间的交互,而不是像内积那样得到一个总体的交互评分。这种方式能捕捉到更细粒度的特征关系,有利于处理更加复杂和非线性的交互场景。②按位相乘的方式能够更好地捕捉非线性关系,特别是在推荐系统中,用户和物品之间的交互往往是非线性的。通过按位相乘,可以更好地表达这些非线性关系。

三、Wide & Deep:兼具记忆和扩展

(一)Wide & Deep算法原理

1、Wide & Deep整体网络结构

Wide侧是一个浅层网络,Deep侧是一个深层网络。

Wide & Deep网络结构

2、Deep侧

  • Deep侧遵守的设计范式:Embedding+MLP。可以简单表示为如下公式: 

logit_{DNN}=DNN(Concat(Embedding(x_{deep})))

其中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。可以简单表示为以下公式:

logit_{wide}=\omega _{wide}x_{wide}

  • Wide侧的用处:①LR的优点在于强于记忆,而wide侧则用于记忆一些高频、大众的模式。②防止Deep侧过度扩展影响精度
  • Wide侧输入的特征:①一些先验知识认定的精华特征,比如一些人工筛选出的交叉特征。②一些影响推荐系统的偏差特征,比如位置偏置等。

4、两侧共同训练

拿CTR举例,可以简单表示模型的预测结果为如下公式:

CTR_{predict}=sigmoid(logit_{wide}+logit_{deep})

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端二阶特征自动交叉,减少了人工设计交叉特征的人力物力浪费。

DeepFM网络结构

2、DeepFM原理简述

DeepFM原理可由以下公式简述:

logit_{DNN}=DNN(Concat(Embedding(x_{deep})))

logit_{FM}=FM(x_{fm},Embedding(x_{fm}))

logit_{lr}=\omega_{lr}x_{lr}

CTR_{predict}=sigmoid(logit_{lr}+logit_{fm}+logit_{dnn})

一般来说输入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_{l+1}=x_0x_l^T\omega_l+b_l+x_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_{l+1}=x_0\bigodot(W_lx_l+b_l)+x_l

但是由于实际场景中,x_0的维度往往很高,引入W_l后要学习的参数量过多,因此提出将W_l分解为两个低维矩阵的乘积。

x_{l+1}=x_0\bigodot(U_l(V_lx_l)+b_l)+x_l

3、DCN的实战缺点

①喂给DCN的特征一般都是经过挑选的潜在特征。

②DCN输入和输出都是d维,只做了信息交叉,没有做到信息的压缩和提取。

4、DCN和DNN的融合

①并联Parallel

x_0分别喂给DCN和DNN,将两者的结果相加得到最终结果。

②串联Stacked:

将x_0喂给DCN以后输出的结果再喂给DNN,得到最终结果。

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向量。公式如下所示:

Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V

其中通过QK^T可以计算得到当前查询与Key的相似度,通过除以\sqrt{d_k}来平缓相似度,再通过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投射到不同的子空间进行特征的交叉,可以描述为以下公式:

head_i=Attention(QW_i^Q,KW_i^K,VW_i^V) MultiHeadAttention(Q,K,V)=Concat(head_1,head_2,...)W^O

其中可以通过三个W_i矩阵,将输入映射到规定的形状,这通过三个线性层实现。

head_i为各个头的attention结果,最后拼接起来,再通过Wo矩阵映射成需要的形状,这是通过顶层线性层实现的。

MultiHeadAttention的结果喂给MLP做进一步的非线性变换,为了训练稳定性,引入Layer Norm和Residual结构,这样一个transformer就构建完成了。

可以通过叠加多个transformer来实现序列特征向更高阶的交叉。

Multi-Head Attention结构示意
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作为一个特征交叉的模块,可以只选择一部分重要特征喂入。

 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【第55课】XSS防御HttpOnlyCSP靶场工具等
  • 如何使用ssm实现游戏攻略网站的设计与实现+vue
  • 测试员阿聪的破局之路:从迷茫到帝都站稳脚跟,大佬亲授良方
  • 想学网络,为什么要先学数通?
  • 【图机器学习系列】(二)从传统机器学习角度理解图(一)
  • 正交试验法(或PICT)来设计测试用例
  • 如何使用ssm实现在线云音乐系统的设计与实现
  • 探索提示工程 Prompt Engineering的奥妙
  • 通过 OpenAI Embedding 接口计算相似度
  • 四川财谷通,信息科技引领者!
  • GAMES101——作业5 光线与三角形相交(菲涅尔反射率)
  • Java笔试面试题AI答之线程(11)
  • 解决 Navicat 删除唯一键(unique)后保存失败的问题:1-near “)“:syntax error
  • arthas源码刨析:arthas 命令粗谈(3)
  • MySQL数据库锁机制(全面讲解)
  • ES6指北【2】—— 箭头函数
  • 2019.2.20 c++ 知识梳理
  • Angular 4.x 动态创建组件
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • HTML5新特性总结
  • JavaScript创建对象的四种方式
  • Java面向对象及其三大特征
  • k8s 面向应用开发者的基础命令
  • PermissionScope Swift4 兼容问题
  • php中curl和soap方式请求服务超时问题
  • Python连接Oracle
  • React as a UI Runtime(五、列表)
  • Spring Cloud中负载均衡器概览
  • Vim Clutch | 面向脚踏板编程……
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 来,膜拜下android roadmap,强大的执行力
  • 爬虫模拟登陆 SegmentFault
  • 双管齐下,VMware的容器新战略
  • 微服务入门【系列视频课程】
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • (C语言)逆序输出字符串
  • (六)c52学习之旅-独立按键
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • (一)Docker基本介绍
  • (一)Kafka 安全之使用 SASL 进行身份验证 —— JAAS 配置、SASL 配置
  • (转) ns2/nam与nam实现相关的文件
  • .axf 转化 .bin文件 的方法
  • .bat批处理(三):变量声明、设置、拼接、截取
  • .libPaths()设置包加载目录
  • .NET Core 控制台程序读 appsettings.json 、注依赖、配日志、设 IOptions
  • .net mvc部分视图
  • .Net(C#)常用转换byte转uint32、byte转float等
  • .net(C#)中String.Format如何使用
  • .net对接阿里云CSB服务
  • .NET框架类在ASP.NET中的使用(2) ——QA
  • .Net实现SCrypt Hash加密
  • /etc/skel 目录作用
  • [023-2].第2节:SpringBoot中接收参数相关注解
  • [ABP实战开源项目]---ABP实时服务-通知系统.发布模式
  • [BZOJ1877][SDOI2009]晨跑[最大流+费用流]