NLP第10课:动手实战基于 CNN 的电影推荐系统

本文从深度学习卷积神经网络入手,基于 Github 的开源项目来完成 MovieLens 数据集的电影推荐系统。

什么是推荐系统呢?

什么是推荐系统呢?首先我们来看看几个常见的推荐场景。

如果你经常通过豆瓣电影评分来找电影,你会发现下图所示的推荐:

image

如果你喜欢购物,根据你的选择和购物行为,平台会给你推荐相似商品:

image

在互联网的很多场景下都可以看到推荐的影子。因为推荐可以帮助用户和商家满足不同的需求:

  • 对用户而言:找到感兴趣的东西,帮助发现新鲜、有趣的事物。

  • 对商家而言:提供个性化服务,提高信任度和粘性,增加营收。

常见的推荐系统主要包含两个方面的内容,基于用户的推荐系统(UserCF)和基于物品的推荐系统(ItemCF)。两者的区别在于,UserCF 给用户推荐那些和他有共同兴趣爱好的用户喜欢的商品,而 ItemCF 给用户推荐那些和他之前喜欢的商品类似的商品。这两种方式都会遭遇冷启动问题。

下面是 UserCF 和 ItemCF 的对比:

image

CNN 是如何应用在文本处理上的?

提到卷积神经网络(CNN),相信大部分人首先想到的是图像分类,比如 MNIST 手写体识别,CAFRI10 图像分类。CNN 已经在图像识别方面取得了较大的成果,随着近几年的不断发展,在文本处理领域,基于文本挖掘的文本卷积神经网络被证明是有效的。

首先,来看看 CNN 是如何应用到 NLP 中的,下面是一个简单的过程图:

image

和图像像素处理不一样,自然语言通常是一段文字,那么在特征矩阵中,矩阵的每一个行向量(比如 word2vec 或者 doc2vec)代表一个 Token,包括词或者字符。如果一段文字包含有 n 个词,每个词有 m 维的词向量,那么我们可以构造出一个 n*m 的词向量矩阵,在 NLP 处理过程中,让过滤器宽度和矩阵宽度保持一致整行滑动。

动手实战基于 CNN 的电影推荐系统

将 CNN 的技术应用到自然语言处理中并与电影推荐相结合,来训练一个基于文本的卷积神经网络,实现电影个性化推荐系统。

首先感谢作者 chengstone 的分享,源码请访问下面网址:

在验证了 CNN 应用在自然语言处理上是有效的之后,从推荐系统的个性化推荐入手,在文本上,把 CNN 成果应用到电影的个性化推荐上。并在特征工程中,对训练集和测试集做了相应的特征处理,其中有部分字段是类型性变量,特征工程上可以采用 one-hot 编码,但是对于 UserID、MovieID 这样非常稀疏的变量,如果使用 one-hot,那么数据的维度会急剧膨胀,对于这份数据集来说是不合适的。

具体算法设计如下:

1. 定义用户嵌入矩阵。

用户的特征矩阵主要是通过用户信息嵌入网络来生成的,在预处理数据的时候,我们将 UserID、MovieID、性别、年龄、职业特征全部转成了数字类型,然后把这个数字当作嵌入矩阵的索引,在网络的第一层就使用嵌入层,这样数据输入的维度保持在(N,32)和(N,16)。然后进行全连接层,转成(N,128)的大小,再进行全连接层,转成(N,200)的大小,这样最后输出的用户特征维度相对比较高,也保证了能把每个用户所带有的特征充分携带并通过特征表达。

具体流程如下:

image

2. 生成用户特征。

生成用户特征是在用户嵌入矩阵网络输出结果的基础上,通过2层全连接层实现的。第一个全连接层把特征矩阵转成(N,128)的大小,再进行第二次全连接层,转成(N,200)的大小,这样最后输出的用户特征维度相对比较高,也保证了能把每个用户所带有的特征充分携带并通过特征表达。

具体流程如下:

image

3. 定义电影 ID 嵌入矩阵。

通过电影 ID 和电影类型分别生成电影 ID 和电影类型特征,电影类型的多个嵌入向量做加和输出。电影 ID 的实现过程和上面一样,但是对于电影类型的处理相较于上面,稍微复杂一点。因为电影类型有重叠性,一个电影可以属于多个类别,当把电影类型从嵌入矩阵索引出来之后是一个(N,32)形状的矩阵,因为有多个类别,这里采用的处理方式是矩阵求和,把类别加上去,变成(1,32)形状,这样使得电影的类别信息不会丢失。

具体流程如下:

image

4. 文本卷积神经网络设计。

文本卷积神经网络和单纯的 CNN 网络结构有点不同,因为自然语言通常是一段文字与图片像素组成的矩阵是不一样的。在电影文本特征矩阵中,矩阵的每一个行构成的行向量代表一个 Token,包括词或者字符。如果一段文字有 n 个词,每个词有 m 维的词向量,那么我们可以构造出一个 n*m 的矩阵。而且 NLP 处理过程中,会有多个不同大小的过滤器串行执行,且过滤器宽度和矩阵宽度保持一致,是整行滑动。在执行完卷积操作之后采用了 ReLU 激活函数,然后采用最大池化操作,最后通过全连接并 Dropout 操作和 Softmax 输出。这里电影名称的处理比较特殊,并没有采用循环神经网络,而采用的是文本在 CNN 网络上的应用。

对于电影数据集,我们对电影名称做 CNN 处理,其大致流程,从嵌入矩阵中得到电影名对应的各个单词的嵌入向量,由于电影名称比较特殊一点,名称长度有一定限制,这里过滤器大小使用时,就选择2、3、4、5长度。然后对文本嵌入层使用滑动2、3、4、5个单词尺寸的卷积核做卷积和最大池化,然后 Dropout 操作,全连接层输出。

具体流程如下:

image

具体过程描述:

(1)首先输入一个 32*32 的矩阵;

(2)第一次卷积核大小为 2*2,得到 31*31 的矩阵,然后通过 [1,14,1,1]max-pooling 操作,得到的矩阵为 18*31

(3)第二次卷积核大小为 3*3,得到 16*29的矩阵,然后通过[1,13,1,1]max-pooling 操作,得到的矩阵为 4*29

(4)第三次卷积核大小 4*4,得到 1*26 的矩阵,然后通过 [1,12,1,1]max-pooling 操作,得到的矩阵为 1*26

(5)第四次卷积核大小 5*5,得到 1*22 的矩阵,然后通过 [1,11,1,1]max-pooling 操作,得到的矩阵为 1*22

(6)最后通过 Dropout 和全连接层,len(window_sizes) * filter_num =32,得到 1*32的矩阵。

5. 电影各层做一个全连接层。

将上面几步生成的特征向量,通过2个全连接层连接在一起,第一个全连接层是电影 ID 特征和电影类型特征先全连接,之后再和 CNN 生成的电影名称特征全连接,生成最后的特征集。

具体流程如下:

image

6. 完整的基于 CNN 的电影推荐流程。

把以上实现的模块组合成整个算法,将网络模型作为回归问题进行训练,得到训练好的用户特征矩阵和电影特征矩阵进行推荐。

image

基于 CNN 的电影推荐系统代码调参过程

在训练过程中,我们需要对算法预先设置一些超参数,这里给出的最终的设置结果:

    # 设置迭代次数
    num_epochs = 5
    # 设置BatchSize大小
    batch_size = 256
    #设置dropout保留比例
    dropout_keep = 0.5
    # 设置学习率
    learning_rate = 0.0001
    # 设置每轮显示的batches大小
    show_every_n_batches = 20

首先对数据集进行划分,按照 4:1 的比例划分为训练集和测试集,下面给出的是算法模型最终训练集合测试集使用的划分结果:

    #将数据集分成训练集和测试集,随机种子不固定
    train_X,test_X, train_y, test_y = train_test_split(features,  
                                                 targets_values,  
                                                 test_size = 0.3,  
                                                 random_state = 0) 

接下来是具体模型训练过程。训练过程,要不断调参,根据经验调参粒度可以选择从粗到细分阶段进行。

调参过程对比:

(1)第一步,先固定,learning_rate=0.01num_epochs=10,测试 batch_size=128 对迭代时间和 Loss 的影响;

(2)第二步,先固定,learning_rate=0.01num_epochs=10,测试 batch_size=256 对迭代时间和 Loss 的影响;

(3)第三步,先固定,learning_rate=0.01num_epochs=10,测试 batch_size=512 对迭代时间和 Loss 的影响;

(4)第四步,先固定,learning_rate=0.01num_epochs=5,测试 batch_size=128对迭代时间和 Loss 的影响;

(5)第五步,先固定,learning_rate=0.01num_epochs=5,测试 batch_size=256对迭代时间和 Loss 的影响;

(6)第六步,先固定,learning_rate=0.01num_epochs=5,测试 batch_size=512对迭代时间和 Loss 的影响;

(7)第七步,先固定,batch_size=256num_epochs=5,测试 learning_rate=0.001 对 Loss 的影响;

(8)第八步,先固定,batch_size=256num_epochs=5,测试 learning_rate=0.0005 对 Loss 的影响;

(9)第九步,先固定,batch_size=256num_epochs=5,测试 learning_rate=0.0001 对 Loss 的影响;

(10)第十步,先固定,batch_size=256num_epochs=5,测试 learning_rate=0.00005 对 Loss 的影响。

得到的调参结果对比表如下:

image

通过上面(1)-(6)步调参比较,在 learning_ratebatch_size 相同的情况下,num_epochs对于训练时间影响较大;而在 learning_ratenum_epochs 相同情况下,batch_size 对 Loss 的影响较大,batch_size 选择512,Loss 有抖动情况,权衡之下,最终确定后续调参固定采用 batch_size=256num_epochs=5 的超参数值,后续(7)-(10)步,随着 learning_rate 逐渐减小,发现 Loss 是先逐渐减小,而在 learning_rate=0.00005 时反而增大,最终选择出学习率为 learning_rate=0.0001 的超参数值。

基于 CNN 的电影推荐系统电影推荐

在上面,完成模型训练验证之后,实际来进行推荐电影,这里使用生产的用户特征矩阵和电影特征矩阵做电影推荐,主要有三种方式的推荐。

1. 推荐同类型的电影。

思路是:计算当前看的电影特征向量与整个电影特征矩阵的余弦相似度,取相似度最大的 top_k 个,这里加了些随机选择在里面,保证每次的推荐稍稍有些不同。

    def recommend_same_type_movie(movie_id_val, top_k = 20):

        loaded_graph = tf.Graph()  #
        with tf.Session(graph=loaded_graph) as sess:  #
            # Load saved model
            loader = tf.train.import_meta_graph(load_dir + '.meta')
            loader.restore(sess, load_dir)

            norm_movie_matrics = tf.sqrt(tf.reduce_sum(tf.square(movie_matrics), 1, keep_dims=True))
            normalized_movie_matrics = movie_matrics / norm_movie_matrics

            #推荐同类型的电影
            probs_embeddings = (movie_matrics[movieid2idx[movie_id_val]]).reshape([1, 200])
            probs_similarity = tf.matmul(probs_embeddings, tf.transpose(normalized_movie_matrics))
            sim = (probs_similarity.eval())
            print("您看的电影是:{}".format(movies_orig[movieid2idx[movie_id_val]]))
            print("以下是给您的推荐:")
            p = np.squeeze(sim)
            p[np.argsort(p)[:-top_k]] = 0
            p = p / np.sum(p)
            results = set()
            while len(results) != 5:
                c = np.random.choice(3883, 1, p=p)[0]
                results.add(c)
            for val in (results):
                print(val)
                print(movies_orig[val])
            return result

  1. 推荐您喜欢的电影。

思路是:使用用户特征向量与电影特征矩阵计算所有电影的评分,取评分最高的 top_k 个,同样加了些随机选择部分。

    def recommend_your_favorite_movie(user_id_val, top_k = 10):

        loaded_graph = tf.Graph()  #
        with tf.Session(graph=loaded_graph) as sess:  #
            # Load saved model
            loader = tf.train.import_meta_graph(load_dir + '.meta')
            loader.restore(sess, load_dir)

            #推荐您喜欢的电影
            probs_embeddings = (users_matrics[user_id_val-1]).reshape([1, 200])
            probs_similarity = tf.matmul(probs_embeddings, tf.transpose(movie_matrics))
            sim = (probs_similarity.eval())

            print("以下是给您的推荐:")
            p = np.squeeze(sim)
            p[np.argsort(p)[:-top_k]] = 0
            p = p / np.sum(p)
            results = set()
            while len(results) != 5:
                c = np.random.choice(3883, 1, p=p)[0]
                results.add(c)
            for val in (results):
                print(val)
                print(movies_orig[val])

            return results

  1. 看过这个电影的人还看了(喜欢)哪些电影。

(1)首先选出喜欢某个电影的 top_k 个人,得到这几个人的用户特征向量;

(2)然后计算这几个人对所有电影的评分 ;

(3)选择每个人评分最高的电影作为推荐;

(4)同样加入了随机选择。

    def recommend_other_favorite_movie(movie_id_val, top_k = 20):
        loaded_graph = tf.Graph()  #
        with tf.Session(graph=loaded_graph) as sess:  #
            # Load saved model
            loader = tf.train.import_meta_graph(load_dir + '.meta')
            loader.restore(sess, load_dir)
            probs_movie_embeddings = (movie_matrics[movieid2idx[movie_id_val]]).reshape([1, 200])
            probs_user_favorite_similarity = tf.matmul(probs_movie_embeddings, tf.transpose(users_matrics))
            favorite_user_id = np.argsort(probs_user_favorite_similarity.eval())[0][-top_k:]

            print("您看的电影是:{}".format(movies_orig[movieid2idx[movie_id_val]]))

            print("喜欢看这个电影的人是:{}".format(users_orig[favorite_user_id-1]))
            probs_users_embeddings = (users_matrics[favorite_user_id-1]).reshape([-1, 200])
            probs_similarity = tf.matmul(probs_users_embeddings, tf.transpose(movie_matrics))
            sim = (probs_similarity.eval())
            p = np.argmax(sim, 1)
            print("喜欢看这个电影的人还喜欢看:")
            results = set()
            while len(results) != 5:
                c = p[random.randrange(top_k)]
                results.add(c)
            for val in (results):
                print(val)
                print(movies_orig[val])
            return results

基于 CNN 的电影推荐系统不足

这里讨论一下基于上述方法所带来的不足:

  1. 由于一个新的用户在刚开始的时候并没有任何行为记录,所以系统会出现冷启动的问题;

  2. 由于神经网络是一个黑盒子过程,我们并不清楚在反向传播的过程中的具体细节,也不知道每一个卷积层抽取的特征细节,所以此算法缺乏一定的可解释性;

  3. 一般来说,在工业界,用户的数据量是海量的,而卷积神经网络又要耗费大量的计算资源,所以进行集群计算是非常重要的。但是由于本课程所做实验环境有限,还是在单机上运行,所以后期可以考虑在服务器集群上全量跑数据,这样获得的结果也更准确。

总结

上面通过 Github 上一个开源的项目,梳理了 CNN 在文本推荐上的应用,并通过模型训练调参,给出一般的模型调参思路,最后建议大家自己把源码下载下来跑跑模型,效果更好。

参考文献及推荐阅读

  1. 推荐系统

  2. Deep Convolutional Neural Networks for Sentiment Analysis of ShortTexts,CND Santos ,M Gattit ,2014.

  3. 推荐系统实践,p50-60,p120-130,项亮。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容