网上关于YouTubeDNN的介绍有很多,这里就不详细介绍
1.把推荐看做多分类问题
可以把推荐当作一个多分类问题,把每一个视频当作一个分类,则给定用户U和上下文C的条件下,在时间t观看第i个video(第i类)的概率为:
其中u是用户的向量表示(embedding),v表示video的向量表示(embedding)。在模型中,利用用户历史和上下文来学习用户的embedding,利用用户的embedding对每个用户进行视频推荐;类似于Word2vec,模型在训练过程中可以利用负采样进行优化。
2.模型架构
模型架构如图所示:
-
模型输入:用户历史观看embedding,历史搜索embedding,画像特征,年龄、性别等
-
模型输出:用户观看每个视频的概率,topn推荐
模型中,首先获取各个特征进行拼接,作为网络的输入,经过一系列隐藏层得到用户embedding,根据用户embedding与video embedding即可计算该用户在时间t观看视频的概率,依据概率分布选取topn作为候选推荐子集
3.模型trick
模型特征:
-
历史观看:类似于Word2vec中的CBOW模型,对观看序列进行建模,得到每个video的embedding,然后把每个用户的历史观看embedding平均值作为输入
-
历史搜索:同上,对历史搜索序列unigram, bigram建模得到embedding,将搜索序列embedding平均作为输入
-
统计学特征:地理位置、设备
-
画像特征:用户性别、登录状态、年龄(归一化到[0,1] )
-
视频被上传的时间(Example Age):YouTube随时都会有一些新视频(fresh),研究发现,用户更加喜欢这些新视频,即使这些视频和他们过去的行为不太相关,因此考虑把视频被上传的时间作为特征。实验结果如图所示:
横坐标为视频上传时间,纵坐标为条件似然概率(衡量视频的流行度),绿色线为视频上传后随着时间的推移,流行度变化的经验概率分布,蓝色线为未考虑视频上传时间时,模型的概率分布,红色为加入视频上传时间后的分布,可以看出,在加入该变量的条件下,概率分布与经验分布大致相符。
4.实现代码
模型部分:
class YoutubeDNN(Model):
def __init__(self, user_sparse_feature_columns, item_sparse_feature_columns, user_dense_feature_columns=(),
item_dense_feature_columns=(), num_sampled=1,
user_dnn_hidden_units=(64, 32), item_dnn_hidden_units=(64, 32), dnn_activation='relu',
l2_reg_embedding=1e-6, dnn_dropout=0, **kwargs):
super(YoutubeDNN, self).__init__(**kwargs)
self.num_sampled = num_sampled
self.user_sparse_feature_columns = user_sparse_feature_columns
self.user_dense_feature_columns = user_dense_feature_columns
self.item_sparse_feature_columns = item_sparse_feature_columns
self.item_dense_feature_columns = item_dense_feature_columns
self.user_embed_layers = {
'embed_' + str(feat['feat']): Embedding(input_dim=feat['feat_num'],
input_length=feat['feat_len'],
output_dim=feat['embed_dim'],
embeddings_initializer='random_uniform',
embeddings_regularizer=l2(l2_reg_embedding))
for feat in self.user_sparse_feature_columns
}
self.item_embed_layers = {
'embed_' + str(feat['feat']): Embedding(input_dim=feat['feat_num'],
input_length=feat['feat_len'],
output_dim=feat['embed_dim'],
embeddings_initializer='random_uniform',
embeddings_regularizer=l2(l2_reg_embedding))
for feat in self.item_sparse_feature_columns
}
self.user_dnn = DNN(user_dnn_hidden_units, dnn_activation, dnn_dropout)
self.item_dnn = DNN(item_dnn_hidden_units, dnn_activation, dnn_dropout)
self.sampled_softmax = SampledSoftmaxLayer(num_sampled=self.num_sampled)
def call(self, inputs, training=None, mask=None):
user_sparse_inputs, item_sparse_inputs, labels = inputs
user_sparse_embed = tf.concat([self.user_embed_layers['embed_{}'.format(k)](v)
for k, v in user_sparse_inputs.items()], axis=-1)
user_dnn_input = user_sparse_embed
self.user_dnn_out = self.user_dnn(user_dnn_input)
item_sparse_embed = tf.concat([self.item_embed_layers['embed_{}'.format(k)](v)
for k, v in item_sparse_inputs.items()], axis=-1)
item_dnn_input = item_sparse_embed
self.item_dnn_out = self.item_dnn(item_dnn_input)
output = self.sampled_softmax([self.item_dnn_out, self.user_dnn_out, labels])
return output
Sampled Softmax:
SampledSoftmaxLayer(Layer):
"""Sampled Softmax Layer"""
def __init__(self, num_sampled=5, **kwargs):
super(SampledSoftmaxLayer, self).__init__(**kwargs)
self.num_sampled = num_sampled
def build(self, input_shape):
self.size = input_shape[0][2]
self.zero_bias = self.add_weight(shape=[self.size],
initializer=Zeros,
dtype=tf.float32,
trainable=False,
name="bias")
super(SampledSoftmaxLayer, self).build(input_shape)
def call(self, inputs_with_label_idx, training=None, **kwargs):
"""
The first input should be the model as it were, and the second the
target (i.e., a repeat of the training data) to compute the labels
argument
"""
item_embeddings, user_embeddings, label_idx = inputs_with_label_idx
item_embeddings = tf.squeeze(item_embeddings, axis=1) # (None, len)
# item_embeddings = tf.transpose(item_embeddings)
user_embeddings = tf.squeeze(user_embeddings, axis=1) # (None, len)
loss = tf.nn.sampled_softmax_loss(weights=item_embeddings, # self.item_embedding.
biases=self.zero_bias,
labels=label_idx,
inputs=user_embeddings,
num_sampled=self.num_sampled,
num_classes=self.size, # self.target_song_size
)
return tf.expand_dims(loss, axis=1)