推荐系统召回模型之YouTubeNet

召回的主要工作是从全体商品库中筛选出用户感兴趣的商品,此过程要求检索速度快,并且所检索出的商品与用户的历史行为和偏好相关。所以召回模型和特征都较为简单

1、问题建模

我们把推荐问题建模成一个“超大规模多分类”问题。即在时刻 t t t,为用户 U U U(上下文信息 C C C)在视频库 v v v中精准的预测出视频 i i i的类别(每个具体的视频视为一个类别, i i i即为一个类别),用数学公式表达如下:
推荐系统召回模型之YouTubeNet
很显然上式为一个softmax多分类器的形式。向量推荐系统召回模型之YouTubeNet是<user, context>信息的高纬“embedding”,而向量推荐系统召回模型之YouTubeNet
则是视频 j j j的embedding向量。所以DNN的目标就是在用户信息和上下文信息为输入条件下学习用户的embedding向量 u u u和视频向量 v v v。而这种超大规模分类问题上,至少要有几百万个类别,实际训练采用的是Negative Sampe,类似于word2vec的Skip-Gram方法。

2、模型架构

推荐系统召回模型之YouTubeNet

3、主要特征

(1)用户历史搜索query:把历史搜索的query分词后的token的embedding向量进行加权平均,能够反映用户的整体搜索历史状态
(2)用户观看视频:对用户历史观看的视频信息生成embedding向量进行加权平均(如文本|类目|品牌等),反映出视频的信息
(3)用户基本信息:年龄、性别、地域等,使用 x x x、 x 2 x^2 x2等增加数值型特征的非线性
(4)其他上下文信息:设备、视频上架时间(example age)?

4、label and context selection

在有监督学习问题中,最重要的选择是label了,因为label决定了你做什么,决定了你的上限,而feature和model都是在逼近label。我们的几个设计如下:

  • 使用更广的数据源:不仅仅使用推荐场景的数据进行训练,其他场景比如搜索等的数据也要用到,这样也能为推荐场景提供一些explore。
  • 为每个用户生成固定数量训练样本:我们在实际中发现的一个practical lessons,如果为每个用户固定样本数量上限,平等的对待每个用户,避免loss被少数active用户domanate,能明显提升线上效果。
  • 抛弃序列信息:我们在实现时尝试的是去掉序列信息,对过去观看视频/历史搜索query的embedding向量进行加权平均。这点其实违反直觉,可能原因是模型对负反馈没有很好的建模。
  • 不对称的共同浏览(asymmetric co-watch)问题:所谓asymmetric co-watch值的是用户在浏览视频时候,往往都是序列式的,开始看一些比较流行的,逐渐找到细分的视频。下图所示图(a)是hled-out方式,利用上下文信息预估中间的一个视频;图(b)是predicting next watch的方式,则是利用上文信息,预估下一次浏览的视频。我们发现图(b)的方式在线上A/B test中表现更佳。而实际上,传统的协同过滤类的算法,都是隐含的采用图(a)的held-out方式,忽略了不对称的浏览模式。
    推荐系统召回模型之YouTubeNet

5、不同网络深度和特征的实验

简单介绍下我们的网络构建过程,采用的经典的“tower”模式搭建网络,基本同2.2所示的网络架构,所有的视频和search token都embedded到256维的向量中,开始input层直接全连接到256维的softmax层,依次增加网络深度(+512–>+1024–>+2048–> …)
推荐系统召回模型之YouTubeNet
下图反映了不同网络深度(横坐标)下不同特征组合情况下的holdout-MAP(纵坐标)。可以很明显看出,增加了观看历史之外的特征很明显的提升了预测得准确率;从网络深度看,随着网络深度加大,预测准确率在提升,但继续增加第四层网络已经收益不大了。
推荐系统召回模型之YouTubeNet

6、数据准备

(1)按点击时间排序,使用滑动窗口构建组合正样本。以及类似Word2vec负采样获取负样本
(2)用户点击序列截取(大于2,小于10)
(3)正负样本比1:5

  • 例如:某用户在06月02号的点击序列为[3455,3525,2774,2775,2776,2777], 用户基本特征向量为[0, 1, 0, 23 ,4.795831523312719, 529]。
    样本组合数等于 点击序列长度-1, 结果为[(uid), (前n个历史点击内容商品序列id),(正样本id:第n+1个内容商品序列id),(5个负样本),(用户基本向量]
  • 训练集:
    [(uid),(3455,3525,2774), (2775), (采样5个负样本id), [0, 1, 0, 23 ,4.795831523312719, 529]]
    [(uid),(3455,3525,2774,2775), (2776), (采样5个负样本id), [0, 1, 0, 23 ,4.795831523312719, 529]]
  • 测试集:为防止时间穿越,选取用户最后一次最为测试集正样本
    [(uid),(3455,3525,2774,2775,2776), (2777), (采样5个负样本id), [0, 1, 0, 23 ,4.795831523312719, 529]]

7、模型框架

class Model():
    def __init__(self):
        pass
    def init_args(self, args):
        self.embedding_size = args['embedding_size']
        self.brand_list = args['brand_list']
        self.fcategory3_list = args['fcategory3_list']
        self.spu_count = args['spu_count']
        self.brand_count = args['brand_count']
        self.fcategory3_count = args['fcategory3_count']

    def build_model(self):
        self.uid = tf.placeholder(tf.int32, [None, ], name='uid')  # user idx [B]
        self.hist_click = tf.placeholder(tf.int32, [None, None],name='hist_click')  # history click[B, T]    [B,T]B可以理解为一个batch的用户数,T可以理解为用户点击的商品个数(每个用户可能不一样)
        self.hist_click_len = tf.placeholder(tf.int32, [None, ], name='hist_click_len')  # history click len
        self.last_click = tf.placeholder(tf.int32, [None, ], name='last_click')  # last click
        self.basic_feature = tf.placeholder(tf.float32, [None, None], name='basic_feature')  # user basic feature
        self.sub_sample = tf.placeholder(tf.int32, [None, None],
                                         name='sub_sample')  # soft layer (pos_sample, neg_sample)[B, sub_size]
        self.label = tf.placeholder(tf.float32, [None, None], name='label')  # label
        self.keep_prob = tf.placeholder(tf.float32, [], name='keep_prob')  # 每个元素被保留的概率
        self.lr = tf.placeholder(tf.float64, [], name='lr')  # learning rate

        self.sku_emb_w = tf.Variable(tf.random_normal([self.spu_count, self.embedding_size]))
        self.brand_emb_w = tf.Variable(tf.random_normal([self.brand_count, self.embedding_size]))
        self.fcategory3_emb_w = tf.Variable(tf.random_normal([self.fcategory3_count, self.embedding_size]))

        input_b = tf.Variable(tf.random_normal([self.spu_count]))

        brand_list = tf.convert_to_tensor(self.brand_list, dtype=tf.int32)
        fcategory3_list = tf.convert_to_tensor(self.fcategory3_list, dtype=tf.int32)

        hist_brand = tf.gather(brand_list, self.hist_click)
        hist_fcategory3 = tf.gather(fcategory3_list, self.hist_click)

        # 这一部分的操作是对用户的历史点击求平均
        # tf.concat(axis=2) 维度从0算  按第2维度拼接
        h_emb = tf.concat([tf.nn.embedding_lookup(self.sku_emb_w, self.hist_click),
                           tf.nn.embedding_lookup(self.brand_emb_w, hist_brand),
                           tf.nn.embedding_lookup(self.fcategory3_emb_w, hist_fcategory3)], axis=2)  # [B, T, 3*e]
        mask = tf.sequence_mask(self.hist_click_len, tf.shape(h_emb)[1], dtype=tf.float32)  # [B, T]
        mask = tf.expand_dims(mask, -1)  # [B, T, 1]
        mask = tf.tile(mask, [1, 1, tf.shape(h_emb)[2]])  # [B, T, 3*e]
        h_emb *= mask  # [B, T, 3*e]
        hist = tf.reduce_sum(h_emb, 1)  # [B, 3*e]
        hist = tf.div(hist, tf.cast(tf.tile(tf.expand_dims(self.hist_click_len, 1), [1, 3 * self.embedding_size]),
                                    tf.float32))  # [B, 3*e]

        last_brand = tf.gather(brand_list, self.last_click)
        last_fcategory3 = tf.gather(fcategory3_list, self.last_click)
        l_emb = tf.concat([tf.nn.embedding_lookup(self.sku_emb_w, self.last_click),
                           tf.nn.embedding_lookup(self.brand_emb_w, last_brand),
                           tf.nn.embedding_lookup(self.fcategory3_emb_w, last_fcategory3)], axis=1)

        self.input = tf.concat([hist, l_emb], axis=1)

        bn = tf.layers.batch_normalization(inputs=self.input, name='b15')
        layer_1 = tf.layers.dense(bn, 1024, activation=tf.nn.relu, name='f15')
        # layer_1 = tf.nn.dropout(layer_1, keep_prob=self.keep_prob)
        layer_2 = tf.layers.dense(layer_1, 512, activation=tf.nn.relu, name='f25')
        # layer_2 = tf.nn.dropout(layer_2, keep_prob=self.keep_prob)
        layer_3 = tf.layers.dense(layer_2, 3 * self.embedding_size, activation=tf.nn.relu, name='user_vector1')
        outputs = tf.identity(layer_3, name="outputs")

        # softmax
        sam_brand = tf.gather(brand_list, self.sub_sample)
        sam_fcategory3 = tf.gather(fcategory3_list, self.sub_sample)

        sample_b = tf.nn.embedding_lookup(input_b, self.sub_sample)
        sample_w = tf.concat([tf.nn.embedding_lookup(self.sku_emb_w, self.sub_sample),
                              tf.nn.embedding_lookup(self.brand_emb_w, sam_brand),
                              tf.nn.embedding_lookup(self.fcategory3_emb_w, sam_fcategory3)], axis=2)  # [B, sample, 3*e]

        user_v = tf.expand_dims(layer_3, 1)  # [B, 1, 3*e]
        sample_w = tf.transpose(sample_w, perm=[0, 2, 1])  # [B, 3*e, sample]

        self.logits = tf.squeeze(tf.matmul(user_v, sample_w), axis=1) + sample_b

        self.loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.label))

8、离线存储和线上应用

(1)模型输出user_v 为用户embedding
(2)保存模型输出sku_emb_w为商品embedding
(3)将商品embedding存到annoy
(4)线上实时返回用户embedding,与annoy中的商品embedding计算相似度,取TOP最为返回的推荐商品

参考文献:
1、Deep Neural Network for YouTube Recommendation论文精读:https://zhuanlan.zhihu.com/p/25343518
2、推荐系统召回模型之YouTubeNet:https://blog.csdn.net/wdh315172/article/details/106581377

上一篇:springboot @Validated分组功能用在service层校验对象解决方案


下一篇:SQL:1158. 市场分析 1-2