19|NeuralCF:如何用深度学习改造协同过滤?

你好,我是王喆,今天,我们来学习协同过滤的深度学习进化版本,NeuralCF。

第15讲里,我们学习了最经典的推荐算法,协同过滤。在前深度学习的时代,协同过滤曾经大放异彩,但随着技术的发展,协同过滤相比深度学习模型的弊端就日益显现出来了,因为它是通过直接利用非常稀疏的共现矩阵进行预测的,所以模型的泛化能力非常弱,遇到历史行为非常少的用户,就没法产生准确的推荐结果了。

虽然,我们可以通过矩阵分解算法增强它的泛化能力,但因为矩阵分解是利用非常简单的内积方式来处理用户向量和物品向量的交叉问题的,所以,它的拟合能力也比较弱。这该怎么办呢?不是说深度学习模型的拟合能力都很强吗?我们能不能利用深度学习来改进协同过滤算法呢?

当然是可以的。2017年,新加坡国立的研究者就使用深度学习网络来改进了传统的协同过滤算法,取名NeuralCF(神经网络协同过滤)。NeuralCF大大提高了协同过滤算法的泛化能力和拟合能力,让这个经典的推荐算法又重新在深度学习时代焕发生机。这节课,我们就一起来学习并实现NeuralCF!

NeuralCF模型的结构

在学习NeuralCF之前,我们先来简单回顾一下协同过滤和矩阵分解的原理。协同过滤是利用用户和物品之间的交互行为历史,构建出一个像图1左一样的共现矩阵。在共现矩阵的基础上,利用每一行的用户向量相似性,找到相似用户,再利用相似用户喜欢的物品进行推荐。

矩阵分解则进一步加强了协同过滤的泛化能力,它把协同过滤中的共现矩阵分解成了用户矩阵和物品矩阵,从用户矩阵中提取出用户隐向量,从物品矩阵中提取出物品隐向量,再利用它们之间的内积相似性进行推荐排序。如果用神经网络的思路来理解矩阵分解,它的结构图就是图2这样的。

图2 中的输入层是由用户ID和物品ID生成的One-hot向量,Embedding层是把One-hot向量转化成稠密的Embedding向量表达,这部分就是矩阵分解中的用户隐向量和物品隐向量。输出层使用了用户隐向量和物品隐向量的内积作为最终预测得分,之后通过跟目标得分对比,进行反向梯度传播,更新整个网络。

把矩阵分解神经网络化之后,把它跟Embedding+MLP以及Wide&Deep模型做对比,我们可以一眼看出网络中的薄弱环节:矩阵分解在Embedding层之上的操作好像过于简单了,就是直接利用内积得出最终结果。这会导致特征之间还没有充分交叉就直接输出结果,模型会有欠拟合的风险。针对这一弱点,NeuralCF对矩阵分解进行了改进,它的结构图是图3这样的。

我想你一定可以一眼看出它们的区别,那就是NeuralCF用一个多层的神经网络替代掉了原来简单的点积操作。这样就可以让用户和物品隐向量之间进行充分的交叉,提高模型整体的拟合能力。

NeuralCF模型的扩展,双塔模型

有了之前实现矩阵分解和深度学习模型的经验,我想你理解起来NeuralCF肯定不会有困难。事实上,NeuralCF的模型结构之中,蕴含了一个非常有价值的思想,就是我们可以把模型分成用户侧模型和物品侧模型两部分,然后用互操作层把这两部分联合起来,产生最后的预测得分。

这里的用户侧模型结构和物品侧模型结构,可以是简单的Embedding层,也可以是复杂的神经网络结构,最后的互操作层可以是简单的点积操作,也可以是比较复杂的MLP结构。但只要是这种物品侧模型+用户侧模型+互操作层的模型结构,我们把它统称为“双塔模型”结构。

图4就是一个典型“双塔模型”的抽象结构。它的名字形象地解释了它的结构组成,两侧的模型结构就像两个高塔一样,而最上面的互操作层则像两个塔尖搭建起的空中走廊,负责两侧信息的沟通。

对于NerualCF来说,它只利用了用户ID作为“用户塔”的输入特征,用物品ID作为“物品塔”的输入特征。事实上,我们完全可以把其他用户和物品相关的特征也分别放入用户塔和物品塔,让模型能够学到的信息更全面。比如说,YouTube在构建用于召回层的双塔模型时,就分别在用户侧和物品侧输入了多种不同的特征,如图5所示。

我们看到,YouTube召回双塔模型的用户侧特征包括了用户正在观看的视频ID、频道ID(图中的seed features)、该视频的观看数、被喜欢的次数,以及用户历史观看过的视频ID等等。物品侧的特征包括了候选视频的ID、频道ID、被观看次数、被喜欢次数等等。在经过了多层ReLU神经网络的学习之后,双塔模型最终通过softmax输出层连接两部分,输出最终预测分数。

看到这里,你可能会有疑问,这个双塔模型相比我们之前学过的Embedding MLP和Wide&Deep有什么优势呢?其实在实际工作中,双塔模型最重要的优势就在于它易上线、易服务。为什么这么说呢?

你注意看一下物品塔和用户塔最顶端的那层神经元,那层神经元的输出其实就是一个全新的物品Embedding和用户Embedding。拿图4来说,物品塔的输入特征向量是x,经过物品塔的一系列变换,生成了向量u(x),那么这个u(x)就是这个物品的Embedding向量。同理,v(y)是用户y的Embedding向量,这时,我们就可以把u(x)和v(y)存入特征数据库,这样一来,线上服务的时候,我们只要把u(x)和v(y)取出来,再对它们做简单的互操作层运算就可以得出最后的模型预估结果了!

所以使用双塔模型,我们不用把整个模型都部署上线,只需要预存物品塔和用户塔的输出,以及在线上实现互操作层就可以了。如果这个互操作层是点积操作,那么这个实现可以说没有任何难度,这是实际应用中非常容易落地的,也是工程师们喜闻乐见的,这也正是双塔模型在业界巨大的优势所在。

正是因为这样的优势,双塔模型被广泛地应用在YouTube、Facebook、百度等各大公司的推荐场景中,持续发挥着它的能量。

NeuralCF的TensorFlow实现

熟悉了NerualCF和双塔模型的结构之后,我们就可以使用TensorFlow来实现它们了。通过之前Embedding+MLP模型以及Wide&Deep模型的实现,我想你对TensorFlow中读取数据,定义特征,训练模型的过程肯定已经驾轻就熟了。我们只要更改之前代码中模型定义的部分,就可以实现NeuralCF。具体的代码你可以参考SparrowRecsys项目中的NeuralCF.py,我只贴出了NeuralCF模型部分的实现。下面,我们重点讲解一下它们的实现思路。

# neural cf model arch two. only embedding in each tower, then MLP as the interaction layers
def neural_cf_model_1(feature_inputs, item_feature_columns, user_feature_columns, hidden_units):
    # 物品侧特征层
    item_tower = tf.keras.layers.DenseFeatures(item_feature_columns)(feature_inputs)
    # 用户侧特征层
    user_tower = tf.keras.layers.DenseFeatures(user_feature_columns)(feature_inputs)
    # 连接层及后续多层神经网络
    interact_layer = tf.keras.layers.concatenate([item_tower, user_tower])
    for num_nodes in hidden_units:
        interact_layer = tf.keras.layers.Dense(num_nodes, activation='relu')(interact_layer)
    # sigmoid单神经元输出层
    output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(interact_layer)
    # 定义keras模型
    neural_cf_model = tf.keras.Model(feature_inputs, output_layer)
    return neural_cf_model

你可以看到代码中定义的生成NeuralCF模型的函数,它接收了四个输入变量。其中 feature_inputs 代表着所有的模型输入, item_feature_columnsuser_feature_columns 分别包含了物品侧和用户侧的特征。在训练时,如果我们只在 item_feature_columns 中放入 movie_id ,在 user_feature_columns 放入 user_id, 就是NeuralCF的经典实现了。

通过DenseFeatures层创建好用户侧和物品侧输入层之后,我们会再利用concatenate层将二者连接起来,然后输入多层神经网络进行训练。如果想要定义多层神经网络的层数和神经元数量,我们可以通过设置 hidden_units 数组来实现。

除了经典的NeuralCF实现,我还基于双塔模型的原理实现了一个NeuralCF的双塔版本。你可以参考下面的模型定义。与上面的经典NerualCF实现不同,我把多层神经网络操作放到了物品塔和用户塔内部,让塔内的特征进行充分交叉,最后使用内积层作为物品塔和用户塔的交互层。具体的步骤你可以参考下面代码中的注释,实现过程很好理解,我就不再赘述了。

# neural cf model arch one. embedding+MLP in each tower, then dot product layer as the output
def neural_cf_model_2(feature_inputs, item_feature_columns, user_feature_columns, hidden_units):
    # 物品侧输入特征层
    item_tower = tf.keras.layers.DenseFeatures(item_feature_columns)(feature_inputs)
    # 物品塔结构
    for num_nodes in hidden_units:
        item_tower = tf.keras.layers.Dense(num_nodes, activation='relu')(item_tower)
    # 用户侧输入特征层
    user_tower = tf.keras.layers.DenseFeatures(user_feature_columns)(feature_inputs)
    # 用户塔结构
    for num_nodes in hidden_units:
        user_tower = tf.keras.layers.Dense(num_nodes, activation='relu')(user_tower)
    # 使用内积操作交互物品塔和用户塔,产生最后输出
    output = tf.keras.layers.Dot(axes=1)([item_tower, user_tower])
    # 定义keras模型
    neural_cf_model = tf.keras.Model(feature_inputs, output)
    return neural_cf_model

在实现了Embedding MLP、Wide&Deep和NeuralCF之后,相信你可以感觉到,实现甚至创造一个深度学习模型并不难,基于TensorFlow提供的Keras接口,我们可以根据我们的设计思路,像搭积木一样实现模型的不同结构,以此来验证我们的想法,这也正是深度推荐模型的魅力和优势。相信随着课程的进展,你不仅对这一点能够有更深刻的感受,同时,你设计和实现模型的能力也会进一步加强。

小结

这节课,我们首先学习了经典推荐算法协同过滤的深度学习进化版本NerualCF。相比于矩阵分解算法,NeuralCF用一个多层的神经网络,替代了矩阵分解算法中简单的点积操作,让用户和物品隐向量之间进行充分的交叉。这种通过改进物品隐向量和用户隐向量互操作层的方法,大大增加了模型的拟合能力。

利用NerualCF的思想,我们进一步学习了双塔模型。它通过丰富物品侧和用户侧的特征,让模型能够融入除了用户ID和物品ID外更丰富的信息。除此之外,双塔模型最大的优势在于模型服务的便捷性,由于最终的互操作层是简单的内积操作或浅层神经网络。因此,我们可以把物品塔的输出当作物品Embedding,用户塔的输出当作用户Embedding存入特征数据库,在线上只要实现简单的互操作过程就可以了。

最后,我们继续使用TensorFlow实现了NerualCF和双塔模型,相信你能进一步感受到利用TensorFlow构建深度学习模型的便捷性,以及它和传统推荐模型相比,在模型结构灵活性上的巨大优势。

为了帮助你复习,我把刚才说的这些重点内容总结在了一张图里,你可以看看。

课后思考

对于我们这节课学习的双塔模型来说,把物品侧的Embedding和用户侧的Embedding存起来,就可以进行线上服务了。但如果我们把一些场景特征,比如当前时间、当前地点加到用户侧或者物品侧,那我们还能用这种方式进行模型服务吗?为什么?

欢迎把你的思考和疑惑写在留言区,也欢迎你把这节课转发出去,我们下节课见!

精选留言

  • Evan-wyl

    2020-11-25 09:02:28

    不可以,如果是新闻推荐的话,地点信息会产生很大的影响;这时把地点信息仅仅是加入到用户侧没有任何作用。
    作者回复

    这是从效果的层面考虑。如果从model serving的角度考虑,如果在物品侧或者用户侧加入场景特征的话,就没法做到预存embedding的serving方式了。因为场景特征是在线上不断变化的。

    2020-11-25 12:51:08

  • AstrHan

    2021-01-16 23:12:26

    embedding之后,如果使用点积,那么这两个embedding是在同一个向量空间;如果使用的MLP则不在同一个向量空间。因为点积不影响向量空间,线性变换矩阵会影响。老师这么说对吧。
    作者回复

    是这样的。

    2021-01-18 13:30:45

  • 定春一号

    2020-11-26 11:27:52

    把context特征放进user塔或者item塔,那么离线生成user embedding或者item embedding的数量就要翻好多倍,能否考虑把context特征单独作为context塔呢?
    作者回复

    是这样的,离线有组合爆炸的风险。
    一般不会有context塔,如果希望引入context特征,最好就不用双塔模型,因为双塔模型易线上serving的优势就不存在了。

    2020-11-26 15:40:44

  • 小小的天

    2020-12-15 08:38:25

    双塔模型对于新闻场景是不是也不太好?新闻时效性很强,在我们公司的数据里,大部分新闻曝光在2个小时内,双塔的训练数据有足够的曝光时,新闻的价值也失去了很多了
    作者回复

    这是个好问题。新闻的场景确实是很有意思的场景,因为时效性很强。这时候新闻id类的特征就不太管用了,因为如果不重新训练,id类特征对应的embedding没办法引入。

    所以对于这类时效性很强的场景,还是推荐基于一些与id无关的feature来构建模型,比如新闻的类型、人物、地点、关键词等等。

    2020-12-16 08:22:15

  • Sebastian

    2020-12-01 16:19:37

    老师,我还是没理解为什么不能加入context的特征。在训练DSSM的时候除了user和item的特征外,在user塔加入context的特征,比如用户的地理位置、手机型号等等,训练完后,将user和item的embedding存入redis后。线上请求时,将user的静态特征和实时context特征再过一遍DSSM,得到新的user embedding后,与存入redis的item emebdding取topN即可,为什么不妥呢?
    作者回复

    因为将user的静态特征和实时context特征再过一遍DSSM是一个非常重的操作,需要把深度模型部署上线,做实时推断。

    这样当然是可以的。但跟离线生成user 和 item emb预存起来,线上只做简单dot product这样的部署方式,显然复杂了许多。

    业界往往追求的实用性的效率,这就是加不加入context特征的区别所在。

    2020-12-02 02:33:59

  • Geek_790c43

    2021-01-29 10:22:06

    不可以,因为如有地点或者时间这种波动比较大的特征就不能用预存embedding来表示当前的用户或者当前的物品了。例如外卖推荐,在公司和家时用户的embedding应该是不同的。或者新闻网站早晨和晚上的也应该不同
    作者回复

    非常好

    2021-02-03 22:05:50

  • 🍃

    2021-05-08 13:42:39

    老师,我还是不理解为什么用用户id和物品id做one-hot编码?直接数值特征不好么?
    作者回复

    数值特征从理解上就是错了,比如 用户id 1761 和用户id 1881,如果是数值特征说明他们两个是有可比性的,但其实并没有,我们需要id类的特征来单独表达他们这两个用户的行为特点。

    2021-05-10 01:46:01

  • Eayon

    2021-04-26 15:14:28

    老师前面提到Embedding+MLP中的物品,Embedding不能计算物品和用户的相似度,提到不在一个空间向量(也说不能点乘就不在一个空间,其实还是不太理解)这里就有两个问题
    1.不在一个空间究竟是什么概念,真就是不能点积就行了,还是跟里面数据内涵有点关系?
    2.另外Embedding+MLP 中的Embedding能不能计算物品与物品之间的相似度呢?
    然后是这节课双塔模型中又可以得到物品和用户的Embedding,可以通过点积得到相似度
    3.那这时候的物品embedding能用来计算物品间的相似度吗?
    作者回复

    1. 由于他们不在一个空间内,所以不能点积,而不是反过来
    2. 可以,物品和物品之间是一定在一个空间内的,但Embedding+MLP不可以直接物品和用户进行点积,因为他们是两个独立的embedding层。双塔模型如果训练的时候就是用用户emb点积物品emb训练的,那么他们是在一个空间内。

    3.可以

    2021-04-30 04:49:07

  • Geek_790c43

    2021-01-29 10:01:45

    图二中,和之前deep crossing 以及 wide&deep,把用户id的one hot向量作为输入, 如果有几亿的用户也这么处理么?
    作者回复

    也是。这就是为什么大厂模型动辄上亿维的原因。当然,如果需要,可以使用一些降维的方法。

    2021-02-06 14:55:59

  • AstrHan

    2021-01-18 14:04:02

    双塔版本的模型是不是有些问题,点积之后应该还要加一层输出层吧?
    output = tf.keras.layers.Dense(1, activation='sigmoid')(output)
    作者回复

    理论上不是一定要加sigmoid函数。但tf这里如果值超过1会报错,已经接收了你的PR,感谢。

    2021-01-21 09:09:42

  • Alan

    2021-03-18 14:54:44

    答:肯定会有影响的!
    首先,NeuralCF 模型计算的用户与物品之间的协同过滤后的计算结果,加入任何的维度,都会导致矩阵变化,失去其意义。
    其次,因为时间、地点这两类特征因素具有很强的影响力!基于用户-物品的协同过滤在此情况下失去意义。就以视频推荐类App来说,白天推荐给我(在公司工作)的新闻、娱乐短视频的协同过滤的结果,到了晚上推荐给我(在校学习)学习类、游戏类长视频为主
    作者回复

    基本是这个原因。简单来说就是加入context feature之后就无法使用纯预训练的方式进行model serving了。

    2021-03-25 09:42:53

  • Geek_8ac51c

    2021-01-15 05:49:54

    老师,mlp层为啥可以替代点积操作。有理论资料学习吗?
    作者回复

    基础知识部分说了,mlp其实模拟的是一个函数,既然两个向量间的互操作当然是一个函数,所以mlp当然可以模拟。

    2021-01-15 07:47:47

  • 范闲

    2020-12-02 14:04:26

    不可以直接加入用户侧或者物品侧,会产生组合爆炸的问题。
    可以考虑变成3塔的结构,加个other塔~~,other embedding也可以预存。但是有个问题other的这些变化可能会比较快,对模型、embedding的更新要求会很高~~
    作者回复

    预存这里所谓的other塔确实有点得不偿失,因为线上的context特征,比如时间,地点变化会非常快。理想的方式就是做实时inference

    2020-12-03 04:17:57

  • Geek_e8024b

    2022-03-13 17:19:53

    老师,想问一下比如新增了一个用户或者商品,比如用户数量变了,用户的onehot,那模型需要重新训练吗
  • lyx

    2021-10-05 11:40:43

    请问下DenseFeatures这层是做embedding的么,搜了半天不知道这层是干嘛的。
    作者回复

    是的,生成embedding

    2021-10-09 02:59:54

  • Geek_71fed1

    2021-03-22 18:18:21

    老师,为什么两个embedding向量是在同一向量空间?
    作者回复

    最后用点积作为互操作的embedding都可以视为在一个空间内。

    2021-03-25 10:02:54

  • Geek1254

    2021-03-02 20:41:33

    老师您好,Youtube的案例中。用户侧有用户正在观看的视频ID,这是需要及时获取的,然后视频的views和likes也需要及时更新。那么这些信息在变化,需要即时获取。那么u(x)和v(y)需要重新计算,请问是在什么时候放入特征数据库呢?
    作者回复

    这是个特征实时更新的问题,目前主流的方案是在一些流计算平台上完成特征的不断更新。

    2021-03-06 17:46:42

  • 努力学习

    2021-02-15 10:19:47

    关于课后思考请问老师,如果只考虑已有数据是不是就可以添加位置等信息了?
    如果就是做位置推荐,那么位置信息还算是context吗?位置推荐若不能使用双塔模型应该优选什么模型那?谢谢老师
    作者回复

    不可以,只要需要复杂online inference的都不可以加进去。

    如果一定要加,可以考虑embedding+MLP,MLP做线上inference的结构。

    2021-02-17 10:37:22

  • LUO FAN

    2021-02-05 21:58:59

    请问在neuralCF的实现中,为什么是把物品特征和用户特征拼接起来送入网络,而不是先分别过embedding 层然后再拼接
    作者回复

    我觉得完全可以做embedding后再拼接,没问题。

    2021-02-06 16:26:22

  • 罗辑

    2021-10-07 20:39:43

    老师,tf.feature_column.embedding_column 中得到embedding向量是通过矩阵分解还是通过神经网络得到的?
    作者回复

    神经网络end2end训练得到

    2021-10-09 03:00:44