作者: 负雪明烛
id: fuxuemingzhu
个人博客: http://fuxuemingzhu.cn/


本文记录了我对owllook.net这一网络小说搜索引擎的推荐系统的分析与设计过程。这个项目是开源项目,地址在owllook repo,目前已经积累了一定的数据量,需要一个靠谱的推荐系统。注意我用到的数据是内部数据,并没有开源。

本文使用推荐算法包括:基于用户的协同过滤(UserCF)、基于物品的协同过滤(ItemCF)、基于流行度的推荐、随机推荐、基于用户标签相似度的推荐等算法。通过对比发现基于用户标签相似度的推荐算法表现效果最佳,目前已经将该算法在线上使用。

数据

本文的数据主要是用户收藏的小说、小说的类型两大类。其中用户收藏的小说这个数据比较有说服力,相对于搜索和点击记录,收藏数据更能显现出用户对这个小说的喜爱。

在我使用的这批数据中,共有用户2420个,共有书籍1596本,所有用户共收藏书籍4780本。为了验证推荐系统的有效性,我们使用每个用户的70%的数据当做训练集,把剩下的30%当做测试集。也就是说基本上每个用户都会在训练集和测试集同时出现,但是这个用户读过的书不会同时出现。我们的目的就是通过分析这个用户读过的书(训练集),去给这个用户推荐他可能感兴趣的书籍。如果我们推荐的书籍中包括了用户测试集的书,说明我们这个推荐命中了。这里的推荐没有考虑时间因素。

每本书都会有至少一个标签,比如《文化入侵异世界》这本书的标签是“科幻、时空穿梭、二次元”,这个标签是起点中文网给打的,可以认为比较准确。

下面是对用户收藏的书籍的个数统计,横坐标代表收藏个数,纵坐标代表该收藏个数有多少人。可以看出大部分人都只收藏了很少的书籍,符合长尾效应。

下面是对每本书被收藏次数统计,横坐标代表被收藏次数,纵坐标代表该被收藏次数有多少本。可以看出大部分书都只被收藏了很少次,只有少部分书被大量收藏,符合长尾效应。

推荐算法

下面开始分析推荐算法。

协同过滤

一提到推荐算法,首先反应过来的就是协同过滤。协同过滤分为基于用户的协同过滤和基于物品的协同过滤。

  • 基于用户的协同过滤算法(UserBasedCF) 这种算法给用户推荐和他兴趣相似的其他用户喜欢的物品。
  • 基于物品的协同过滤算法(ItemBasedCF) 这种算法给用户推荐和他之前喜欢的物品相似的物品。

这两种推荐算法只需要使用用户的历史收藏数据,所以算法比较简单,也有较好的可解释性。但是问题也很明显:如果用户的历史收藏数据太少,那么这两种算法就无效了。在我们的这个小说推荐系统中,数据只有几千条,并不算很多,不能保证协同过滤算法有太大的威力。

更多关于协同过滤的内容,可以看我这篇博客动手实现基于协同过滤的电影推荐系统

基于流行度的推荐

在用户收藏书籍个数统计和书籍被收藏个数统计的分析图中我们可以看到,事实上只有活跃的用户占少数、也只有少量的书籍比较热门。热门书籍大概可以从owllook.net这个搜索框下面的搜索排行中有所反映。

这个暗示着我们,或许可以使用基于流行度的推荐算法。即给用户直接推荐比较流行的书籍即可,很可能这个用户就喜欢。虽然这个方法很粗暴,但是对于整体效果来说,未必就差。

基于用户标签相似度的推荐

这个想法来自基于用户的协同过滤算法,本质上是个基于内容的推荐算法。所谓基于内容的推荐算法,就是把物品做映射,使用更低维的数据表示该物品,在本项目中就是用户标签。在UserCF中,我们使用两个用户的收藏历史来做相似性分析,但是对于owllook来说,收藏历史不算多,那么就会造成两个用户的共同的收藏书籍很少,那么就无法有效地求用户余弦相似度。所以,这就引导了我们做一个书籍到标签的映射,计算两个用户的相似度不是使用两个用户的共同书籍,而是使用两个用户的共同标签。标签的个数肯定是很少的,所以就解决了无法求两个用户相似度的问题。

其实仔细想想还是很有道理的:只有看的书一样的两个用户才比较相似吗?其实并不是。直观上,对于小说来说,如果两个用户的口味比较一致就行。比如两个人都喜欢科幻,那么这两个人就比较相似。所以可以把其中一个人喜欢的,另一个人没看过的《三体》推荐过去。

总之,我们可以把每个用户收藏的书转化成标签之后,然后利用每个人的标签,求相似用户,然后根据相似用户做书籍推荐。

具体做法是:

  1. 求出每个用户的所有书籍累积的标签计数,比如用户A : {"科幻":3, "时空穿梭":2, "二次元":4}
  2. 求该用户和其他用户的余弦相似度,比如用户A : {"用户B" : 0.97, "用户C" : 0.86, "用户D" : 0.72};
  3. 遍历每个相似用户的书籍,累加用户相似度得到书籍平分,比如用户B看过《三体》、《球形闪电》,用户C看过《球形闪电》,用户D看过《流浪地球》,那么给用户A推荐的书籍的打分为:用户A : {"《三体》" : 0.97,"《球形闪电》" : 1.83,"《流浪地球》" : 0.72
  4. 对推荐的书籍按照打分排序,并把得分最高的TopK推荐给用户A.

评价指标

本文采用的评价指标和《推荐系统实践》一致,分为精确率,召回率,覆盖率和新颖度。

  • 精确率描述最终的推荐列表中有多少比例是发生过的用户—物品评分记录;

  • 召回率描述有多少比例的用户—物品评分记录包含在最终的推荐列表中;

  • 覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户;

  • 新颖度反映了推荐列表中物品的平均流行度。如果推荐出的物品都很热门,说明推荐的新颖度较低,否则说明推荐结果比较新颖。

算法对比

在上面的几节内容中,我们已经分析了本文的主要几个算法的工作原理,推荐系统的评价指标。真正地推荐过程分为4步:

  1. 构造训练集和预测集
  2. 训练推荐系统模型
  3. 给出推荐结果
  4. 评价推荐结果

所以我一共实现了5种推荐算法:

  • UserCF,基于用户的协同过滤算法
  • ItemCF,基于物品的协同过滤算法
  • Random,随机推荐
  • Most Popular,推荐最流行物品
  • Most Similar,根据用户标签相似度推荐

推荐10本书籍

为了对比,我们一致使用推荐10本书籍,比如下面的含义就是用户h****的所有书籍标签、所有收藏的书籍、给他推荐的书籍:

User: h****
User tags: {'上古先秦', '东方玄幻', '异术超能', '都市', '架空历史', '玄幻', '二次元', '灵异', '恐怖惊悚', '幻想修仙', '武侠仙侠', '仙侠', '历史'}
User books: ['剑来', '圣墟', '修真聊天群', '牧神记', '汉乡', '逍遥游', '深夜书屋', '秦吏', '三寸人间']
recommend for userid = h****:
['大王饶命', '飞剑问道', '修真聊天群', '赘婿', '太初', '帝霸', '一念永恒', '诡秘之主', '天道图书馆', '永夜君王']

下面是当给每个用户推荐10本书的时候,各个推荐算法的结果对比:

推荐算法\评测指标 Precision Recall Coverage Popularity
UserCF 0.49% 4.93% 45.69% 1.1230
ItemCF 0.08% 0.08% 52.77% 0.6141
Random 0.04% 0.34% 99.83% 0.8827
Most Popular 0.74% 7.45% 0.90% 3.5024
Most Similar 1.43% 14.08% 7.96% 2.2894

在上面的结果中,我们可以看到,对于协同过滤来说,UserCF比ItemCF的准确和召回率多高出了不少,说明在我们的实验中,求相似用户然后做推荐的方式是有效果的。ItemCF表现差是因为书籍很多、被收藏的书主要是同样的那几个,导致我们不能很好的求不同书籍的相似情况。另外,我们看到ItemCF比UserCF的覆盖率更广、流行度更低,这说明ItemCF能更好的挖掘出书籍的长尾效应,能够把更冷门的书籍推荐出来。

随机推荐推荐准确率和召回率都比较低,说明随机猜的推荐效果很差。其覆盖率极高,流行度很低,这说明随机猜地推荐算法最能把所有的书籍都推荐出去了,但是我们都知道这种推荐做法不可取。

基于流行度的推荐算法,准确率和召回率比UserCF还要高。这说明我们在上面的分析是对的!由于owllook首页给出了排行榜,另外也可能由于比较火的书确实有很多人喜欢看,所以我们只要把最火的10本书无脑推荐给所有人都能得到很高的准确和召回。其实这引发了我们思考:推荐算法越复杂越好吗?未必见得。基于流行度的推荐的覆盖率很低,这是显然地,毕竟只推荐了最火的10本。流行度是所有推荐算法中最高的,这个指标说明了我们推荐的确实都是最流行的书,并没有很好地把所有书籍都推荐出去的能力。

最后,我们设计的基于用户标签相似度的算法表现最好,准确率和召回率都是最高的,而且领先了一大截。这说明用户对于小说的追求确实是和小说的类型有关,给用户推荐口味相投的用户收藏的书籍确实是是个明智之举。值得注意的是这个算法的召回率,因为召回率反映了推荐算法能不能把用户想要的东西都推荐给他,14%的召回非常高了。这个算法的覆盖率8%左右,相对于协同过滤和随机猜测都很小,说明这个算法并不能太好的挖掘书籍的长尾效应;但是这个覆盖率也远高于基于流行度的推荐,说明这个算法还是推荐了不少的新鲜书籍给用户。最后,这个算法的流行度小于基于流行度的推荐算法,大于UserCF,这个说明了该算法也推荐了不少流行书籍。但上文已经解释过了,推荐流行书籍并不代表是错的,因为流行书籍确实受欢迎。

上面是推荐10本书的情况下,不同推荐算法的对比,我们可以得出结论:本文设计的基于用户标签相似度的推荐算法表现非常好,给用户推荐的书籍受到了用户喜爱,同时也有一定地挖掘长尾效应的能力。

推荐20本书籍

为了做进一步地对比,我将推荐书籍的个数扩大到20本,比如下面的含义就是用户y***的所有书籍标签、所有收藏的书籍、给他推荐的书籍:

User: y***
User tags: {'武侠仙侠', '玄幻', '都市', '武侠幻想', '架空历史', '都市生活', '两晋隋唐', '虚拟网游', '未来世界', '东方玄幻', '武侠', '科幻', '二次元', '游戏', '历史'}
User books: ['尘劫录', '大王饶命', '凡人修仙传仙界篇', '天骄战纪', '霸汉', '修炼狂潮', '惊悚乐园', '江山美色', '死人经', '孺子帝', '多宝浮屠', '赘婿', '枭臣', '长宁帝军', '天下第九', '三寸人间', '牧神记', '剑来', '行镖', '最强反派系统', '星际游轮']
recommend for userid = y***:
['圣墟', '太初', '剑来', '赘婿', '帝霸', '飞剑问道', '修真聊天群', '诡秘之主', '天道图书馆', '汉乡', '永夜君王', '大道朝天', '三寸人间', '武炼巅峰', '元尊', '一念永恒', '明朝败家子', '斗战狂潮', '斗破苍穹', '将夜']

下面是当给每个用户推荐20本书的时候,各个推荐算法的结果对比:

推荐算法\评测指标 Precision Recall Coverage Popularity
UserCF 0.28% 5.50% 59.08% 0.8878
ItemCF 0.08% 1.48% 62.53% 0.5854
Random 0.02% 0.36% 100.00% 0.8739
Most Popular 0.65% 12.65% 1.57% 3.2299
Most Similar 0.87% 17.52% 15.33% 2.1372

从上面的结果中,我们可以看出,我们设计的基于用户标签相似度的推荐算法准确率、召回率依然最高,覆盖率和流行率处于中等水平,仍然是效果最好的算法。总体来看,当推荐书的个数变多时,所有的算法准确率降低了、召回率提高了。很显然,当推荐数目变多时,推荐的书籍就更不准了,但推荐的书籍就更了。这告诉我们应该根据项目需求,合理选择推荐系统的优化目标。另外,我们注意到Most Similar算法与Most Popular算法的准确率和召回率更加接近了,这是否说明Most Similar已经退化成Most Popular了呢?这么说有一定道理,但并不完全是。有道理是因为比较流行的书籍本来就受欢迎,Most Similar也做推荐是理所当然的;不完全对的地方在于Most Similar的覆盖率还是非常大的,说明对长尾的书籍也做了推荐;流行度指标较低,说明推荐了一些冷门的书籍。

推荐解释

这里说的推荐解释,是给我们一个直观上的感受:我们的推荐结果是有说服力的。对于我们的基于用户标签相似度的推荐算法,可解释性非常强,因为我们只要证明推荐的结果书籍的标签是用户喜欢的标签即可。我们把推荐书籍的个数设为10,推荐书籍的同时打印出这个书籍的标签。

对于用户s********,他喜欢的标签是幻想修仙仙侠,推荐的这10本书基本比较符合,同时也推荐了两本玄幻的书籍,相当于去探索用户的其他兴趣。

User: s********
User tags: {'幻想修仙', '仙侠'}
User books: ['系统的超级宗门', '明朝败家子', '兔子必须死', '我真是医二代', '万界之最强大', '听说我死后超凶', '神豪的悠闲人生', '创业吧学霸大人', '这个末世有点槽', '逆天邪神']
recommend for userid = s********:
一念永恒 ['仙侠', '幻想修仙']
飞剑问道 ['仙侠', '古典仙侠']
圣墟 ['玄幻', '东方玄幻']
牧神记 ['玄幻', '东方玄幻']
道君 ['仙侠', '幻想修仙']
凡人修仙传 ['仙侠', '幻想修仙']
剑来 ['武侠仙侠']
斗战狂潮 ['仙侠', '修真文明']
蛊真人 ['仙侠', '幻想修仙']
大王饶命 ['都市', '都市生活', '二次元']
['一念永恒', '飞剑问道', '圣墟', '牧神记', '道君', '凡人修仙传', '剑来', '斗战狂潮', '蛊真人', '大王饶命']

对于用户陈**,他喜欢的书籍标签是科幻二次元,看出推荐的书籍都是比较接近他的口味的。

User: 陈**
User tags: {'未来世界', '史诗奇幻', '奇幻', '科幻', '时空穿梭', '二次元'}
User books: ['放开那个女巫', '两界搬运工', '修真四万年']
recommend for userid = 陈**:
大王饶命 ['都市', '都市生活', '二次元']
诡秘之主 ['玄幻', '异世大陆', '二次元']
修真聊天群 ['都市', '异术超能', '二次元']
修真四万年 ['科幻', '未来世界']
赘婿 ['历史', '架空历史']
学霸的黑科技系统 ['科幻', '超级科技']
牧神记 ['玄幻', '东方玄幻']
异常生物见闻录 ['科幻', '时空穿梭', '二次元']
天道图书馆 ['玄幻', '异世大陆']
['大王饶命', '诡秘之主', '修真聊天群', '修真四万年', '赘婿', '学霸的黑科技系统', '牧神记', '异常生物见闻录', '大医凌然', '天道图书馆']

对于用户风**,他喜欢的标签是历史武侠,这个推荐的书籍非常符合他的口味。

User: 风**
User tags: {'清史民国', '传统武侠', '两宋元明', '历史', '历史军事', '架空历史', '武侠'}
User books: ['刺明', '明扬天下', '草清', '三国之兵临天下', '顺明', '指南录', '纸花船', '明贼', '乱世扬明', '明末传奇', '大明1617', '大明最后一个太子', '汉儿不为奴']
recommend for userid = 风**:
赘婿 ['历史', '架空历史']
汉乡 ['历史', '架空历史']
唐砖 ['历史', '两晋隋唐']
带着仓库到大明 ['历史', '两宋元明']
剑来 ['武侠仙侠']
秦吏 ['历史', '上古先秦']
锦衣夜行 ['历史', '两宋元明']
圣墟 ['玄幻', '东方玄幻']
晚明 ['历史', '两宋元明']
['赘婿', '汉乡', '唐砖', '带着仓库到大明', '剑来', '秦吏', '锦衣夜行', '明朝败家子', '圣墟', '晚明']

当然,也存在一些口味比较复杂的用户,收藏了很多种类的书籍,推荐的结果也应该包括了对应的种类。比如对于用户青**,推荐的结果留给读者自行判断吧。

User: 青**
User tags: {'游戏异界', '二次元', '恐怖惊悚', '史诗奇幻', '灵异', '都市', '古典仙侠', '东方玄幻', '进化变异', '修真文明', '异世大陆', '都市生活', '灵异鬼怪', '幻想修仙', '黑暗幻想', '奇幻', '科幻', '游戏', '玄幻', '仙侠'}
User books: ['怪谈研究会', '就是个道士', '最初的寻道者', '恶神当道', '我是仙凡', '道辟九霄', '当个法师闹革命', '炼道长生', '一世之尊', '问镜', '原始战记', '极度尸寒', '恐怖邮差', '玄门封神', '封仙', '蛊真人', '这世界的土著好凶猛', '诸天纪', '仙路云霄', '旧日篇章', '深夜书屋', '修真新时代', '白中仙的修道生涯', '侠道行', '道门振兴系统', '难道我是神', '我假装会异能', '我当道士那些年', '绿洲中的领主', '与妖怪的二三事', '道吟', '氪金魔主', '一品修仙', '全球高武', '黑夜玩家', '民国谍影']
recommend for userid = 青**:
圣墟 ['玄幻', '东方玄幻']
飞剑问道 ['仙侠', '古典仙侠']
牧神记 ['玄幻', '东方玄幻']
一念永恒 ['仙侠', '幻想修仙']
大王饶命 ['都市', '都市生活', '二次元']
剑来 ['武侠仙侠']
斗战狂潮 ['仙侠', '修真文明']
太初 ['玄幻', '东方玄幻']
逆天邪神 ['仙侠', '幻想修仙']
道君 ['仙侠', '幻想修仙']
['圣墟', '飞剑问道', '牧神记', '一念永恒', '大王饶命', '剑来', '斗战狂潮', '太初', '逆天邪神', '道君']

探讨

本文设计的基于用户标签相似度的推荐算法有没有什么缺点呢?我认为也是有的。

首先,这个算法仍然是基于相似度的算法,那么就不可避免地涉及到推荐系统冷启动问题。当一个新用户加入的时候,他的喜好是未知的,那么该算法也就失效了。

其次,在实验过程中发现,并不是每一本书都会有对应的标签,那么用户如果看了几本没有标签的书,那么就没法给他做推荐了。另外,如果我们的推荐算法有效的前提是书籍的标签是准确的,这个并不是那么容易确定的。

再者,本文对每个要推荐的小说累加了用户的相似度,然后根据小说得分的TopK做的推荐。那么不同的相似度计算方式会影响推荐结果。这个需要在实验中去验证选择。

对于前两个问题,可以考虑使用基于流行度的推荐算法作为补充。即,我们遇到无法推荐的情况时,直接把流行的书籍推荐给他。上文的推荐算法对比已经说明了,流行书籍推荐是有效的。

结论

本文设计的基于用户标签相似度的网络小说推荐系统,在推荐系统的各个评测指标上,表现都很出色。目前已经投入线上使用当中。

设计owllook网络小说推荐系统的更多相关文章

  1. 【Python3爬虫】网络小说更好看?十四万条书籍信息告诉你

    一.前言简述 因为最近微信读书出了网页版,加上自己也在闲暇的时候看了两本书,不禁好奇什么样的书更受欢迎,哪位作者又更受读者喜欢呢?话不多说,爬一下就能有个了解了. 二.页面分析 首先打开微信读书:ht ...

  2. Python爬取十四万条书籍信息告诉你哪本网络小说更好看

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: TM0831 PS:如有需要Python学习资料的小伙伴可以加点击 ...

  3. python从爬虫基础到爬取网络小说实例

    一.爬虫基础 1.1 requests类 1.1.1 request的7个方法 requests.request() 实例化一个对象,拥有以下方法 requests.get(url, *args) r ...

  4. 【RS】Collaborative Memory Network for Recommendation Systems - 基于协同记忆网络的推荐系统

    [论文标题]Collaborative Memory Network for Recommendation Systems    (SIGIR'18) [论文作者]—Travis Ebesu (San ...

  5. 利用Smith圆图设计匹配网络

    要实现射频最大的功率传输,必须使负载阻抗与源阻抗相匹配(即信号源阻抗与负载阻抗共轭相等),实现匹配的通常做法是在源和负载之间插入一个匹配网络,该网络不仅仅为减少功率损耗而设计,还可减少噪声干扰.提高功 ...

  6. python 爬取网络小说 清洗 并下载至txt文件

    什么是爬虫 网络爬虫,也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人.其目的一般为编纂网络索引. 网络搜索引擎等站点通过爬虫软件更新自身的网站内容或其对其他网站的索引.网络爬虫可以 ...

  7. 课程设计之"网络考试系统"(php、Extjs)

    1.TestSystem大概结构框图 2.数据库设计(11张表) 数据库名称:db_testsystem 数据库表: tb_admin 记录题库管理员帐户信息 代码 tb_allcontent 记录随 ...

  8. ZOJ1372 POJ 1287 Networking 网络设计 Kruskal算法

    题目链接:problemCode=1372">ZOJ1372 POJ 1287 Networking 网络设计 Networking Time Limit: 2 Seconds     ...

  9. 造轮子 | 怎样设计一个面向协议的 iOS 网络请求库

    近期开源了一个面向协议设计的网络请求库 MBNetwork,基于 Alamofire 和 ObjectMapper 实现,目的是简化业务层的网络请求操作. 须要干些啥 对于大部分 App 而言,业务层 ...

随机推荐

  1. python检验代理ip是否可用、代理ip检验

    python检验代理ip是否可用.代理ip检验 安装相关模块: pip install requests 验证代理IP是否可用脚本: import random import telnetlib im ...

  2. 蛋白质组DIA深度学习之谱图预测

    目录 1. 简介 2. 近几年发表的主要工具 1.DeepRT 2.Prosit 3. DIANN 4.DeepDIA 1. 简介 基于串联质谱的蛋白质组学大部分是依赖于数据库(database se ...

  3. R包 tidyverse 分列

    代码: 1 library(tidyverse) 2 separate(data = df,col=chr_pos,into=c("chr","pos"),se ...

  4. ubuntu20.04安装EasyConnect兼容性问题解决

    目录 1. 命令行启动EasyConnect 2. 降级pango 3. 重新启动EasyConnect,即可成功启动 Ubuntu20.04安装EasyConnect后无法启动的解决方案 工作使用操 ...

  5. Excel-给出指定数值的日期 date()

    DATE函数 函数名称:DATE 主要功能:给出指定数值的日期. 使用格式:DATE(year,month,day) 参数说明:year为指定的年份数值(小于9999):month为指定的月份数值(可 ...

  6. 二叉树——Java实现

    1 package struct; 2 3 interface Tree{ 4 //插入元素 5 void insert(int value); 6 //中序遍历 7 void inOrder(); ...

  7. String.split()与StringUtils.split()的区别

    import com.sun.deploy.util.StringUtils; String s =",1,,2,3,4,,"; String[] split1 = s.split ...

  8. nodejs代码初探之nodejs启动

    nodejs启动 入口在node_main.cc,解析参数后进入node.cc 中的node::Start() V8::Initialize() //初始化v8SetupProcessObject() ...

  9. 【Windows】github无法访问/hosts文件只能另存为txt

    因为我的github访问不了了,搜索解决方案为修改host文件 https://blog.csdn.net/curry10086/article/details/106800184/ 在hosts文件 ...

  10. java 整型

    byte(1字节).short(2字节).int(4字节).long(16字节) java中前缀加上0b或者0B就可以写二进制数,前缀加上0就可以写八进制数,前缀加上0x或者0X就可以写十六进制数 一 ...