我做了一个基于知识图谱的图书推荐系统,踩了不少坑

7 阅读6分钟

我做了一个基于知识图谱的图书推荐系统,踩了不少坑

起因

去年做毕设的时候,导师给了个课题:做一个图书推荐系统。一开始想的很简单,不就是协同过滤嘛,sklearn 调个包就完事了。结果导师说:"你这推荐出来的书,用户问你为什么推荐,你怎么解释?"

这一问把我问住了。确实,传统的协同过滤、矩阵分解这些方法,推荐结果就是个黑盒,说不清楚为什么推荐这本书。

于是开始调研,看了一堆论文,最后决定用知识图谱 + 评论关键词的方案。现在系统已经上线了,分享一下整个过程。

在线体验地址http://47.110.250.188:5000/

GitHub 地址github.com/yangqunfeng… (欢迎 Star)

数据准备

爬虫部分

数据是用的 Scrapy 爬了大概一个月,最后拿到:

  • 68 万本图书的基本信息(书名、作者、出版社、评分等)
  • 367 万条用户评论

这里有个坑:网站的反爬很严格,IP 封得很快。最后是买了代理池 + 设置随机延迟才搞定的。

数据清洗

原始数据质量参差不齐,主要问题:

  1. 作者名字格式不统一(有的带国籍,有的不带)
  2. 出版社名字有各种变体
  3. 评论里有大量无意义的短评("好看"、"不错"之类的)

清洗代码写了好几版,最后用正则 + 人工规则搞定。

技术方案

知识图谱构建

用 NetworkX 构建了一个异构图,包含 5 种实体:

  • 图书
  • 作者
  • 出版社
  • 译者
  • 系列

关系有:

  • 图书-作者(写作关系)
  • 图书-出版社(出版关系)
  • 图书-译者(翻译关系)
  • 图书-系列(系列关系)

最后构建出来的图谱有 70 万+ 实体,100 万+ 关系。

评论关键词提取

这部分是核心创新点。传统的推荐系统只看图书的结构化信息,但评论里其实包含了很多有价值的特征。

比如《三体》的评论里,高频词有:科幻、宇宙、文明、物理、黑暗森林等。这些词能很好地描述这本书的特点。

关键词提取用了 TF-IDF + TextRank 双算法:

# TF-IDF 提取
tfidf_keywords = jieba.analyse.extract_tags(
    comment_text,
    topK=50,
    withWeight=True
)

# TextRank 提取
textrank_keywords = jieba.analyse.textrank(
    comment_text,
    topK=40,
    withWeight=True
)

# 合并权重
for word, weight in tfidf_keywords:
    keyword_dict[word] = weight
for word, weight in textrank_keywords:
    keyword_dict[word] = keyword_dict.get(word, 0) + weight * 0.8

但这样提取出来的关键词质量不高,有很多无意义的词("作者"、"小说"、"故事"之类的)。

后来加了智能过滤,只保留真正能描述图书特征的词:

  • 主题词(科幻、历史、爱情等)
  • 情节元素(战斗、阴谋、复仇等)
  • 人物特征(主角、英雄、反派等)
  • 风格特征(幽默、深刻、细腻等)

这部分调了很久,最后效果还不错。

推荐算法

提供了三种推荐策略:

1. 知识图谱推荐

基于图结构,找相似的书。比如用户喜欢《三体》,那就推荐:

  • 同作者的书(刘慈欣的其他作品)
  • 同系列的书(三体 2、三体 3)
  • 同出版社的科幻书

这种推荐的好处是可解释性强,能明确告诉用户为什么推荐。

2. 关键词推荐

基于评论关键词的语义相似度。用户喜欢《三体》,系统提取出关键词:科幻、宇宙、文明、物理等,然后找其他书的评论里也有这些关键词的。

这种推荐能发现一些跨作者、跨系列的相似书籍。

3. 混合推荐

结合上面两种策略:

最终得分 = 0.5 × 知识图谱得分 + 0.5 × 关键词相似度

实测效果最好。

性能优化

多进程加速

评论关键词提取很慢,367 万条评论,单进程要跑好几个小时。

后来改成多进程并行:

from multiprocessing import Pool, cpu_count

num_processes = cpu_count() - 1
with Pool(processes=num_processes) as pool:
    results = pool.imap_unordered(process_book_comments, tasks)

速度提升了 6 倍,半小时就跑完了。

缓存机制

知识图谱和关键词数据都做了缓存,用 pickle 序列化。首次运行需要 30-60 分钟构建,之后启动只要几秒。

前端界面

前端用的原生 JavaScript,没用框架(主要是懒得学 React/Vue)。

做了几个功能:

  1. 搜索框自动补全
  2. 三种推荐策略切换
  3. 关键词可视化选择
  4. 中英文双语切换

界面还算简洁,主要精力都在后端算法上了。

踩过的坑

坑 1:内存爆炸

一开始把所有数据都加载到内存,结果程序跑着跑着就 OOM 了。后来改成分批处理 + 及时释放内存才解决。

坑 2:中文分词不准

jieba 默认的分词效果不太好,"三体世界"会被分成"三体"和"世界"。后来加了自定义词典,把书名都加进去了。

坑 3:推荐结果太单一

最开始只用知识图谱推荐,结果推荐出来的都是同一个作者的书。后来加了关键词推荐,多样性才上来。

效果展示

随便测试几个:

输入:三体 推荐

  1. 球状闪电(同作者)
  2. 银河帝国(关键词匹配:科幻、宇宙、文明)
  3. 2001太空漫游(关键词匹配:科幻、太空)

输入:活着 推荐

  1. 许三观卖血记(同作者)
  2. 平凡的世界(关键词匹配:苦难、人性、时代)
  3. 白鹿原(关键词匹配:历史、家族、命运)

效果还可以,至少比纯协同过滤要好。

开源

技术栈:

  • 后端:Flask + NetworkX + Pandas + Jieba + scikit-learn
  • 前端:原生 JavaScript + CSS3
  • 算法:知识图谱 + TF-IDF + TextRank

后续计划

  1. 加入用户行为数据,做个性化推荐
  2. 优化关键词提取算法,提高准确率
  3. 加入图书封面展示
  4. 做个移动端适配

总结

整个项目做下来,最大的感受是:推荐系统不只是算法,数据质量和工程实现同样重要

算法再好,数据质量差也白搭。工程实现不好,性能上不去也没用。

另外,可解释性真的很重要。用户不会因为你的算法多先进就信任你的推荐,但如果你能告诉他"推荐这本书是因为和你喜欢的《三体》作者相同",他就更容易接受。

最后,欢迎大家体验和提意见!