Skip to content
matt90luo's Blog
Go back

推荐系统-YouTubeDNN详解

最近阅读了一篇2016年的推荐经典论文Deep Neural Network for YouTobe Recommendations, 网上也有关于这篇文章的许多讨论,特写此篇博客用以记录我对此模型的理解以及上线经历。

<!— more —>

候选集生成

这部分和word2vec的做法非常相似,可以参考我的这篇博客,把召回过程看作是一个多分类问题,整个模型结构如下图所示

可以看到最后一层是softmax层,然后在实际求解中使用NCE方法。最终可以通过基于LSH(局部哈希敏感)算法的ANN引擎做线上的召回。和word2vec不同的地方在多了两层非线性的隐藏层,全联接形式。 整个输入向量可以分为两大类:embedding向量和连续维度的数值。其中提出的example age这个特征比较特别,它的含义是上架时间到生成样本所经过的时长。所以会含有一个英文单词age,这就是年龄的含义。 在实际inference时,我们会给一个0值或者很小的负值。这样做的目的是让新鲜的视频有更多曝光的机会。有利于召回上传时间短,但是视频内容与用户兴趣更相关的新视频。 其实加入这个特征的原因是上架越早的视频,更有机会成为热门的视频,与大多数用户相关,因此在训练时加入该特征,有利于该特征的权重较大,而在线上召回时,将该特征的权重进行打压,则有利于其他相关性特征,因此对上传时间较短的新视频的召回有一定的帮助。

排序模型

排序模型使用的weighed LR模型,具体结构如下

weighed LR有两种实现方式

  1. up-sampling方式 上采样通过增加正样本数量达到加权目的

  2. magnify weight 这种方法就是在计算损失函数是引入权重。典型的如tensorflow提供的函数weighted_cross_entropy_with_logits

我们来复习一些概念

\begin{equation} \begin{split} y = \frac{1}{1 + e^{-w^Tx+ b}} \ odds = e^{w^Tx+b} = \frac{p(y=1|x)}{p(y=0|x)} \ p = \frac{odds}{odd + 1} \end{split} \end{equation}

odds 表示一件事情的发生比,是该事件发生和不发生的比率。logit就是对这个比率取对数。其中odds=e{wTx+b}=frac{p(y=1x)}{p(y=0x)}odds = e^\{w^Tx+b\} = \\frac\{p(y=1|x)\}\{p(y=0|x)\} 在weighted logistic regression中,odds=frac{sum{Ti}}{Nk}odds = \\frac\{\\sum\{T_i\}\}\{N-k\},解释一下含义,假设总共的曝光次数为N,有观看的次数为k,我们把观看时长TiT_i看成权重,没有观看的曝光的权重是单位1 同样每一次曝光的观看时长的期望可以写做 E(T)=frac{sumTi}{N}E(T) = \\frac\{\\sum T_i\}\{N\}, 这个式子也很好理解,从古典概型的角度去看。所以我们会有

E(T)=frac{sumTi}{N}=frac{frac{sumTi}{Nk}}{frac{N}{Nk}}=frac{odds}{frac{N}{Nk}}E(T)=\\frac\{\\sum T_i\}\{N\} = \\frac\{\\frac\{\\sum T_i\}\{N-k\}\}\{\\frac\{N\}\{N-k\}\} =\\frac\{odds\}\{\\frac\{N\}\{N-k\}\}

因为一般情况下k远小于N,所以E(T)simeqoddsE(T) \\simeq odds 或者通过级数的角度理解

odds=E(T)frac{N}{Nk}=E(T)frac{1}{1p}simeqE(T)(1+p)simeqE(T)odds = E(T)\\frac\{N\}\{N-k\} = E(T)\\frac\{1\}\{1-p\} \\simeq E(T)*(1+p) \\simeq E(T)
  1. tensorflow 小试牛刀 tensorflow是google推出的深度学习框架,实现了底层线性代数运算方法。我们使用tensorflow实现了召回和排序模型,一些核心代码如下
class sort_model(tf.Module):
    def __init__(self, name=None):

        super().__init__(name=name)
        with tf.name_scope("sort"):
            self.weight = tf.compat.v1.placeholder(tf.float32)
            self.label = tf.compat.v1.placeholder(tf.float32)
            self.input_embedding = tf.compat.v1.placeholder(tf.float32)
            with tf.variable_scope("sort", reuse=tf.AUTO_REUSE):
                self.hidden_layer_0 = tf.get_variable('hidden_layer_0', initializer=tf.random.normal([512, 256], 0.0, 1.0), )
                self.hidden_layer_1 = tf.get_variable('hidden_layer_1', initializer=tf.random.normal([256, 128], 0.0, 1.0))
                self.output_layer = tf.get_variable('output_layer', initializer=tf.random.normal([128, 1], 0.0, 1.0))

    def forward(self):
        tmp = tf.nn.swish(
            tf.matmul(
                tf.nn.swish(tf.matmul(self.input_embedding, self.hidden_layer_0)), self.hidden_layer_1))  # shape T, 128

        return tf.matmul(tmp, self.output_layer) #logit


    def cost_func(self):
        tmp = tf.nn.swish(
            tf.matmul(
                tf.nn.swish(tf.matmul(self.input_embedding, self.hidden_layer_0)), self.hidden_layer_1))  # shape T, 128

        cost = tf.reduce_mean(
            tf.squeeze(
                tf.nn.weighted_cross_entropy_with_logits(labels=self.label, logits=tf.matmul(tmp, self.output_layer),
                                                         pos_weight=self.weight)))
        return cost

class recall_model(tf.Module):
    def __init__(self, VOCABULARY_NUM, HIDDEN_NUM, name=None):

        super().__init__(name=name)
        with tf.name_scope("recall"):
            self.pos = tf.compat.v1.placeholder(tf.int32)
            self.input_embedding = tf.compat.v1.placeholder(tf.float32)
            self.neg = tf.compat.v1.placeholder(tf.int32)
            with tf.variable_scope("recall", reuse=tf.AUTO_REUSE):
                self.hidden_layer_0 = tf.get_variable('hidden_layer_0', initializer=tf.random.normal([256, 512], 0.0, 1.0), )
                self.hidden_layer_1 = tf.get_variable('hidden_layer_1', initializer=tf.random.normal([512, 256], 0.0, 1.0))
                self.output_layer = tf.get_variable('output_layer', initializer=tf.zeros([VOCABULARY_NUM, HIDDEN_NUM]))

    def forward(self):

        tmp_softmax_input = tf.expand_dims(
            tf.nn.swish(tf.matmul(tf.nn.swish(tf.matmul(self.input_embedding, self.hidden_layer_0)), self.hidden_layer_1)),
            axis=1)

        return tmp_softmax_input

    def cost_func(self):
        tmp_pos = tf.nn.embedding_lookup(self.output_layer, self.pos)  # shape T,1,256
        tmp_neg = tf.nn.embedding_lookup(self.output_layer, self.neg)  # shape T,K,256
        tmp_softmax_input = self.forward()

        p = tf.matmul(tmp_softmax_input, tf.transpose(tmp_pos, perm=[0, 2, 1]))  # shape T, 1, 1

        n = tf.matmul(tmp_softmax_input, tf.transpose(tmp_neg, perm=[0, 2, 1]))  # shape T, 1, k

        pos_part = tf.compat.v1.reduce_sum(
            tf.math.log(
                tf.nn.sigmoid(
                    tf.clip_by_value(p, -8, 8))), axis=1)  # shape T,1

        neg_part = tf.compat.v1.reduce_sum(
            tf.math.log(1 -
                        tf.nn.sigmoid(
                            tf.clip_by_value(n, -8, 8))), axis=1)  # shape T,K

        cost = - tf.reduce_mean(
            tf.compat.v1.reduce_sum(pos_part, axis=1) + tf.compat.v1.reduce_sum(neg_part, axis=1))

        return cost

下面我简单汇总一下使用tensorflow过程中的注意点

  1. variable_scope vs name_scope 作为一种图式计算引擎,应该使用命名空间来做变量隔离。name_scope只会决定对象属于哪个范围,并不会对对象的作用范围产生任何影响。通常name_scope配合tensorboard可视化使用 variable_scope 配合 get_variable 使用。可以共享变量

  2. 一些有用的tf函数 在使用tf时,始终要保证shape的运算准确性,有一些函数可以方便的改变tensor的shape。tf.sqeeze可以去除所有度量为1的维度,比如shape(1,4,1)变成shape(4,). tf.reduce_sum 对应axis的维度做sum操作。

  3. 模型部署 tf的模型部署(单机)并不复杂,比较绕的地方在它的配置文件,在进行模型热更新的时候,至少需要两步,第一步新模型load,第二步修改label的绑定。这样模型才会正式生效

小结

  1. youTubeDNN的召回模型需要类比word2vec,样本的生成同样考虑subsampling,优化手段也是negative sampling
  2. 排序模型是使用的weighted LR,计算出的odds 就是观看时长的期望。这样一个优化目标符合视频网站的业务要求
  3. 使用tensorflow serving作为模型的上线部署,注意其配置文件的修改顺序。

Share this post on:

Previous Post
机器学习-embedding之word2vec
Next Post
scala-模式匹配