主要是按照以下3篇介绍+评论的脉络来整理(讲的真的超级好!),再加上我自己在实际运用过程中产生的问题。其实模型大概内容都能看懂。有困扰的地方主要在于user embedding和video embedding是如何生成的?以及本文将推荐问题转为多分类问题,那么这个多分类指的是什么? train和serve的不同之处?
- 文中把推荐问题转换成多分类问题,在next watch的场景下,每一个备选video都会是一个分类,因此总共的分类有数百万之巨,这在使用softmax训练时无疑是低效的,这个问题Youtube是如何解决的?
这个问题原文的回答是这样的
We rely on a technique to sample negative classes from the background distribution ("candidate sampling") and then correct for this sampling via importance weighting.
简单说就是进行了负采样(negative sampling)并用 importance weighting 的方法对采样进行calibration(校准)。文中同样介绍了一种替代方法,hierarchical softmax,但并没有取得更好的效果。当然关于采样的具体技术细节以及优劣可能再开一篇文章都讲不完,感兴趣的同学可以参考tensorflow中的介绍(https://www.tensorflow.org/extras/candidate_sampling.pdf)以及NLP领域的经典论文https://link.zhihu.com/?target=http%3A//www.aclweb.org/anthology/P15-1001
- 这个user vector和video vector是怎么生成的?
(另外的,这里embedded video watches是一个一开始就随机按照分布初始化的矩阵,不是从item2vec里取的。)
candidate generation model, video vector是如何生成的?(主要答案在参考的第二篇中的评论)
看这里的时候,建议和我一样的小白可以画一下word2vec的图,方便理解。具体细节可以看这里【深度学习】word2vec(上)
总结一下,如下word2vec示意图
user_embedding:对应上图的hidden layer;(user_emb
到底是网络的最后一层呢?还是网络隐层呢?是隐层哦)video_embedding:对应上图的
;
@做最闲的咸鱼
这里说的softmax层是 dense + softmax 激活函数,假设最后一个hidden layer维度是100代表user embedding,输出节点维度200w表示videos,全连接权重维度就是[100,200w],而hidden layer与每一个输出节点的权重维度就是[100,1],这就是一个video对应embedding,计算一个video的概率时是u*v,即两个100维向量做内积,是可以在一个空间的。
u是[1*100],v是[100*1]
@老昶信
user vector和video vector的问题,这个DNN输入的特征是<user, context>类的特征,即根据用户每次刷新的状态去输入到网络中得到的最后一层的向量作为user vector,可以做到实时反馈,每次最新的浏览点击都能反馈到输入层进而得到不同的user vector。
而video vector则是 softmax 中 video 与最后一层的w作为这个video的vector,最终通过user_vec与item_vec的内积最大索引就能快速得到结果。在这个线上实时反馈工程上去之前,也可以直接拿video vector做i2i,效果也是很棒的。
@科学之友
其中 class probability里的class指的是所有的video,不考虑hash降规模的话每个video有一个id。在 train的阶段把用户观看历史,用户人口统计学特征等编码表示出 user vector,(注意这里已经到了隐层->输出层部分了!!!)接着 user vector 乘以一个矩阵 W(这个矩阵其实就是各个 video 的embedding vector矩阵,它的大小必然是video_size*embedding_dim,然后矩阵里的每一行都对应一个video_id,也就是那个video vector),最后得到 class probability (softmax的结果)
注意以上的矩阵 W 是训练阶段得到的,在serving阶段直接取用即可,就和word2vec里预训练出来的词向量矩阵,到时候根据id去取矩阵的某一行就行啦。
不过在word2vec里词向量一般是取的输入层到隐层层的权重;在yotube这个模型中,video是取的最后一个隐层到softmax分类输出层的权重。
@杨旭东
负采样通过tf.nn.nce_loss函数来实现,这个函数有两个重要的参数:nce_weights和nce_biases,也就是你说的softmax的权重,也就是video的embedding。具体地,
nce_weights = tf.Variable( tf.truncated_normal([params['n_classes'], params['last_hidden_units']], stddev=1.0/math.sqrt(params['last_hidden_units'])),name='nce_weights') nce_biases = tf.Variable(tf.zeros([params['n_classes']]), name='nce_biases') loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights, biases=nce_biases, labels=labels, inputs=net, num_sampled=params['num_sampled'], num_classes=params['n_classes'], num_true=1, remove_accidental_hits=True, partition_strategy='div', name='match_model_nce_loss'))
这里的最后一个 hidden 层和 softmax 层组成的结构与 word2vec 一模一样。
唯一的区别是 word2vec 用输入层与 hidden 层之间的权重作为 word embedding,没用 hidden 层与输出层的权重;而这里用了 hidden 层与输出层的权重作为 video embedding,用 hidden 层作为 user embedding。
@龙赛罗
线上使用时候,user embedding实时生成,video embedding离线训练好之后推到线上,使用类LSH方法进行匹配(比如使用现成工具faiss)
这里我想知道的是!word2vec结构导致在最后一个relu前还是softmax前?
def YoutubeDNN(user_feature_columns, item_feature_columns, num_sampled=5,
user_dnn_hidden_units=(64, 32),
dnn_activation='relu', dnn_use_bn=False,
l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, output_activation='linear', seed=1024, ):
"""Instantiates the YoutubeDNN Model architecture.
:param user_feature_columns: An iterable containing user's features used by the model.
:param item_feature_columns: An iterable containing item's features used by the model.
:param num_sampled: int, the number of classes to randomly sample per batch.
:param user_dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of user tower
:param dnn_activation: Activation function to use in deep net
:param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net
:param l2_reg_dnn: float. L2 regularizer strength applied to DNN
:param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector
:param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate.
:param seed: integer ,to use as random seed.
:param output_activation: Activation function to use in output layer
:return: A Keras model instance.
"""
if len(item_feature_columns) > 1:
raise ValueError("Now YoutubeNN only support 1 item feature like item_id")
item_feature_name = item_feature_columns[0].name
item_vocabulary_size = item_feature_columns[0].vocabulary_size
# 为稀疏特征创建对应的Embedding字典
embedding_matrix_dict = create_embedding_matrix(user_feature_columns + item_feature_columns, l2_reg_embedding,
seed=seed)
# 获得用户输入特征
user_features = build_input_features(user_feature_columns)
user_inputs_list = list(user_features.values())
user_sparse_embedding_list, user_dense_value_list = input_from_feature_columns(user_features, user_feature_columns,
l2_reg_embedding, seed=seed,
embedding_matrix_dict=embedding_matrix_dict)
user_dnn_input = combined_dnn_input(user_sparse_embedding_list, user_dense_value_list)
item_features = build_input_features(item_feature_columns)
item_inputs_list = list(item_features.values())
user_dnn_out = DNN(user_dnn_hidden_units, dnn_activation, l2_reg_dnn, dnn_dropout,
dnn_use_bn, output_activation=output_activation, seed=seed)(user_dnn_input)
item_index = EmbeddingIndex(list(range(item_vocabulary_size)))(item_features[item_feature_name])
item_embedding_matrix = embedding_matrix_dict[item_feature_name]
#获得每一个item的Embedding向量
item_embedding_weight = NoMask()(item_embedding_matrix(item_index))
pooling_item_embedding_weight = PoolingLayer()([item_embedding_weight])
output = SampledSoftmaxLayer(num_sampled=num_sampled)(
[pooling_item_embedding_weight, user_dnn_out, item_features[item_feature_name]])
model = Model(inputs=user_inputs_list + item_inputs_list, outputs=output)
# 设置属性接口,调用对应的数据
model.__setattr__("user_input", user_inputs_list)
model.__setattr__("user_embedding", user_dnn_out)
model.__setattr__("item_input", item_inputs_list)
model.__setattr__("item_embedding",
get_item_embedding(pooling_item_embedding_weight, item_features[item_feature_name]))
return model
- 在确定优化目标的时候,Youtube为什么不采用经典的CTR,或者播放率(Play Rate),而是采用了每次曝光预期播放时间(expected watch time per impression)作为优化目标?
这个问题从模型角度出发,是因为 watch time更能反应用户的真实兴趣,从商业模型角度出发,因为watch time越长,YouTube获得的广告收益越多。而且增加用户的watch time也更符合一个视频网站的长期利益和用户粘性。
- Youtube的用户对新视频有偏好,那么在模型构建的过程中如何引入这个feature?
- 在对训练集的预处理过程中,Youtube没有采用原始的用户日志,而是对每个用户提取等数量的训练样本,这是为什么?
- Youtube为什么不采取类似RNN的Sequence model,而是完全摒弃了用户观看历史的时序特征,把用户最近的浏览历史等同看待,这不会损失有效信息吗?
- 在处理测试集的时候,Youtube为什么不采用经典的随机留一法(random holdout),而是一定要把用户最近的一次观看行为作为测试集?
只留最后一次观看行为做测试集主要是为了避免引入future information,产生与事实不符的数据穿越。
- 在进行video embedding的时候,为什么要直接把大量长尾的video直接用0向量代替?
- 针对某些特征,比如 previous impressions,为什么要进行开方和平方处理后,当作三个特征输入模型?
这是很简单有效的工程经验,引入了特征的非线性
- 在candidate generation model的serving过程中,Youtube为什么不直接采用训练时的model进行预测,而是采用了一种最近邻搜索的方法?
这个问题的答案是一个经典的工程和学术做trade-off的结果,在model serving过程中对几百万个候选集逐一跑一遍模型的时间开销显然太大了,因此在通过candidate generation model得到user 和 video的embedding之后,通过最近邻搜索的方法的效率高很多。我们甚至不用把任何model inference的过程搬上服务器,只需要把user embedding和video embedding存到redis或者内存中就好了。
评价指标
推荐系统Top-K问题(或者HR@K, HR为hit ratio),需要划分训练集和测试集吗? - Estero的回答 - 知乎
比如我曾经购买了30个商品,如果全部作为训练集,那么当推荐系统给我推荐新商品时,我就没法判断是否推荐准确了。
反之,将其中20个商品作为训练集来训练推荐模型,然后基于top-10来推荐。此时,如果推荐的10个商品,和我剩余的那10个商品(去掉训练集)相同,那hit ratio就是100%;如果只包括了10个商品中的3个,那就是30%,以此类推。
推荐系统遇上深度学习(十六)--详解推荐系统中的常用评测指标 - 梁勇的文章 - 知乎
在top-K推荐中,HR是一种常用的衡量召回率的指标,其计算公式如下:
分母是所有的测试集合,分子式每个用户top-K推荐列表中属于测试集合的个数的总和。举个简单的例子,三个用户在测试集中的商品个数分别是10,12,8,模型得到的top-10推荐列表中,分别有6个,5个,4个在测试集中,那么此时HR的值是 (6+5+4)/(10+12+8) = 0.5。
def hit(gt_items, pred_items):
count = 0
for item in pred_items:
if item in gt_items:
count += 1
return count
暂时性小结
youtubednn召回是u2i的召回
之前学习din的时候,是将排序问题看做是ctr预估问题,即这个item点或者不点击。
这里youtubednn召回的问题,是将问题转为下一个视频,将会看哪一个视频,转为了多分类问题。
线上使用时候,user embedding实时生成,video embedding离线训练好之后推到线上,使用类LSH方法进行匹配(比如使用现成工具faiss)
推荐系统遇上深度学习(三十四)--YouTube深度学习推荐系统
https://github.com/onozeam/YoutubeDNN (github代码)
YouTube DNN论文精读 - 旷野孤灯的文章 - 知乎(中文翻译)
https://blog.csdn.net/weixin_35154281/article/details/112493756 (code)