☰
Python实现图书推荐系统:协同过滤与文本相似度实战
2026/10/12 14:56:29 网站建设 项目流程

简介:这份资源是一套基于协同过滤与文本相似度算法的Python图书推荐系统完整源码,面向计算机、软件工程等专业需要完成课程设计、毕业设计或推荐算法入门实践的学生与开发者,帮助解决推荐系统从算法到前后端落地的完整实现问题。压缩包共1828个文件,约218.71MB,以1653张jpg与56张png图片、55个html页面、11个py脚本、18个csv数据集及css、js等前端资源为主,涵盖界面素材、页面模板、算法代码与数据文件。系统功能覆盖用户注册登录、图书热度排行与分类展示、搜索与详情、评论点赞收藏、公告管理、个人中心等级积分与历史续读,后台还提供图书批量导入审核、用户等级与作者认证管理、日志备份及数据统计等模块。目前已有1622人学习下载,适合借此理解协同过滤与文本相似度融合的推荐思路,并参考其模块划分与目录组织完成二次开发。

1. 图书推荐系统为什么值得自己写一遍:从「猜你喜欢」到可复现的协同过滤

电商和图书馆的「猜你喜欢」背后,最经典的一类实现就是协同过滤。它不依赖图书内容,只看「谁借了什么、谁打了多少分」,就能算出你可能喜欢的书。再叠加一层文本相似度,把书名、简介、标签做向量化,就能缓解新书没人借、老书被反复推的冷启动问题。这套组合拳用 Python 写出来并不复杂,几十行核心逻辑就能跑通,但真正落地时会遇到稀疏矩阵、相似度爆炸、推荐结果全是同一本书这些坑。这篇笔记就围绕「Python 实现图书推荐系统(基于协同过滤 + 文本相似度)」这个方向,把数据准备、算法选型、参数设置、排错方法一步步拆开,让新手能照着复现,熟手能看到边界和调参空间。适合有 Python 基础、想做一个能写进简历或直接给小型图书馆用的推荐系统的开发者。

2. 数据从哪来、怎么洗:图书推荐系统的输入决定了上限

2.1 三类数据缺一不可:用户行为、图书元数据、用户画像

协同过滤吃的是用户-物品交互矩阵,文本相似度吃的是图书本身的文字信息。一个能跑的图书推荐系统,至少要有下面三类数据:

数据类型典型字段作用缺失后果
用户行为user_id, book_id, rating/borrow_count构建评分矩阵协同过滤无法启动
图书元数据book_id, title, author, category, description文本向量化新书无法被推荐
用户画像(可选)user_id, age, major, borrow_history冷启动兜底新用户只能推热门

我一般会先用 pandas 把三张表读进来,检查主键是否对齐。图书 ID 和用户 ID 必须全局唯一,否则后面做矩阵映射时会串行。

import pandas as pd # 读取三张基础表 ratings = pd.read_csv('ratings.csv') # user_id, book_id, rating books = pd.read_csv('books.csv') # book_id, title, author, category, description users = pd.read_csv('users.csv') # user_id, age, major # 检查主键唯一性和空值 print(ratings.isnull().sum()) print(books['book_id'].duplicated().sum()) print(ratings['user_id'].nunique(), ratings['book_id'].nunique()) # 过滤掉评分次数少于 3 次的用户和图书,降低噪声 user_counts = ratings['user_id'].value_counts() book_counts = ratings['book_id'].value_counts() ratings = ratings[ratings['user_id'].isin(user_counts[user_counts >= 3].index)] ratings = ratings[ratings['book_id'].isin(book_counts[book_counts >= 3].index)]

这段代码做了三件事:读表、查空值和重复、按交互频次过滤。参数>= 3不是固定的,数据量大可以提到 5 或 10,数据量小就降到 2。过滤的目的是去掉那些只借过一次书的用户和只被借过一次的书,它们对相似度计算的贡献几乎为零,反而增加矩阵稀疏度。

2.2 评分矩阵的稀疏度计算与处理策略

图书推荐场景下,用户-图书矩阵通常极度稀疏。一个高校图书馆有几十万册书、几万用户,实际借阅记录可能只有几万条,稀疏度轻松超过 99%。先算一下稀疏度,心里有数再选算法。

import numpy as np n_users = ratings['user_id'].nunique() n_books = ratings['book_id'].nunique() n_ratings = len(ratings) sparsity = 1 - n_ratings / (n_users * n_books) print(f'稀疏度: {sparsity:.4f}')

如果稀疏度高于 0.995,基于用户的协同过滤(UserCF)会非常吃力,因为两个用户共同借过的书可能只有一两本,相似度不可信。这时候优先考虑基于物品的协同过滤(ItemCF),因为物品之间的相似度可以通过多个用户的共同行为累积,相对稳定。另一个办法是先做矩阵降维,用 TruncatedSVD 把图书向量压到 50 到 100 维,再算相似度。

提示:稀疏度超过 0.99 时,不要直接用余弦相似度在全量矩阵上算,内存和时间都会翻车。先做维度约简或只保留 Top-N 活跃用户。

3. 协同过滤核心实现:ItemCF 和 UserCF 怎么选、参数怎么调

3.1 ItemCF 的完整代码实现与相似度矩阵构建

ItemCF 的思路是:你借了 A 书,系统去找和 A 最像的 B 书推给你。相似度用余弦,基于用户对两本书的共同评分向量。

from sklearn.metrics.pairwise import cosine_similarity from scipy.sparse import csr_matrix # 构建用户-图书稀疏矩阵 user_ids = ratings['user_id'].astype('category') book_ids = ratings['book_id'].astype('category') rows = user_ids.cat.codes cols = book_ids.cat.codes values = ratings['rating'].values sparse_matrix = csr_matrix((values, (rows, cols)), shape=(n_users, n_books)) # 转置成图书-用户矩阵,算图书间相似度 book_user_matrix = sparse_matrix.T item_sim = cosine_similarity(book_user_matrix, dense_output=False) # 只保留每本书最相似的 20 本,其余置零,降低噪声和存储 from sklearn.preprocessing import normalize item_sim = normalize(item_sim, axis=1, norm='l1')

cosine_similarity返回的是稀疏矩阵,dense_output=False避免内存爆炸。normalize做 L1 归一化,让每本书的相似度权重和为 1,后续加权评分时不会因为相似书数量不同导致分数尺度不一致。Top-N 的 N 一般取 10 到 30,图书类目越细,N 可以越小。

3.2 评分预测与 Top-N 推荐生成

有了相似度矩阵,就可以给用户打分并生成推荐列表。

def recommend_for_user(user_idx, sparse_matrix, item_sim, top_n=10): # 取出该用户已评分的图书 user_ratings = sparse_matrix[user_idx].toarray().flatten() rated_items = np.where(user_ratings > 0)[0] # 用相似度加权预测未评分图书的分数 scores = item_sim[rated_items].T.dot(user_ratings[rated_items]) scores = np.asarray(scores).flatten() # 屏蔽已评分的书 scores[rated_items] = -np.inf top_items = np.argsort(scores)[-top_n:][::-1] return top_items, scores[top_items]

item_sim[rated_items]取出用户借过的书对应的相似度行,转置后与评分向量做点积,得到每本候选书的加权分数。scores[rated_items] = -np.inf是必须的,否则推荐结果里会出现用户已经借过的书,体验很差。top_n一般设 10,实际展示时再截取前 5。

3.3 UserCF 的适用场景与相似用户计算

UserCF 适合用户数量远小于图书数量的场景,比如班级图书角、小型读书会。它的逻辑是找到和你借书口味最像的 K 个用户,把他们喜欢但你没借过的书推给你。

user_sim = cosine_similarity(sparse_matrix, dense_output=False) def recommend_by_usercf(user_idx, sparse_matrix, user_sim, k=20, top_n=10): sim_scores = np.asarray(user_sim[user_idx].todense()).flatten() similar_users = np.argsort(sim_scores)[-k-1:-1] # 去掉自己 # 加权汇总相似用户的评分 weighted = user_sim[similar_users].T.dot(sparse_matrix[similar_users]) scores = np.asarray(weighted.todense()).flatten() rated = np.where(sparse_matrix[user_idx].toarray().flatten() > 0)[0] scores[rated] = -np.inf return np.argsort(scores)[-top_n:][::-1]

K 取 20 到 50 比较常见。K 太小,相似用户不够,推荐覆盖面窄;K 太大,会引入不相关用户,精度下降。我一般会跑几组 K 值,看推荐结果的品类多样性再定。

4. 文本相似度补位:让新书和冷门书也能被推出去

4.1 图书文本向量化:TF-IDF 与 Sentence-BERT 的取舍

协同过滤有个硬伤:新上架的书没有任何借阅记录,相似度矩阵里整行是零,永远推不出去。文本相似度就是来解决这个问题的。把书名、作者、分类、简介拼成一个文本字段,做向量化,再算书与书之间的文本相似度。

from sklearn.feature_extraction.text import TfidfVectorizer books['text'] = (books['title'].fillna('') + ' ' + books['author'].fillna('') + ' ' + books['category'].fillna('') + ' ' + books['description'].fillna('')) tfidf = TfidfVectorizer(max_features=5000, stop_words='english') tfidf_matrix = tfidf.fit_transform(books['text']) text_sim = cosine_similarity(tfidf_matrix, dense_output=False)

max_features=5000控制词表大小,太大容易过拟合,太小会丢信息。中文图书需要先分词,用 jieba 把简介切开再喂给 TfidfVectorizer。如果数据量在几万本以内,TF-IDF 足够用;如果追求语义层面的相似,比如「机器学习」和「深度学习」应该接近,可以换 Sentence-BERT,但推理成本会高一个量级。

4.2 混合推荐:协同过滤分数与文本相似度分数的融合

最终推荐分数 = α × 协同过滤分数 + (1-α) × 文本相似度分数。α 是融合权重,需要调。

def hybrid_recommend(user_idx, sparse_matrix, item_sim, text_sim, alpha=0.7, top_n=10): user_ratings = sparse_matrix[user_idx].toarray().flatten() rated_items = np.where(user_ratings > 0)[0] cf_scores = np.asarray(item_sim[rated_items].T.dot(user_ratings[rated_items])).flatten() # 文本相似度分数:用户借过的书的文本相似度均值 text_scores = np.asarray(text_sim[rated_items].mean(axis=0)).flatten() # 归一化到 [0,1] cf_scores = (cf_scores - cf_scores.min()) / (cf_scores.max() - cf_scores.min() + 1e-8) text_scores = (text_scores - text_scores.min()) / (text_scores.max() - text_scores.min() + 1e-8) final_scores = alpha * cf_scores + (1 - alpha) * text_scores final_scores[rated_items] = -np.inf return np.argsort(final_scores)[-top_n:][::-1]

α 取 0.7 表示协同过滤占主导,文本相似度做补充。如果系统里新书比例高,α 可以降到 0.5 甚至 0.4。归一化那一步不能省,否则两个分数尺度不同,融合结果会被大尺度的那一方带偏。

5. 避坑与排查:图书推荐系统上线前必须过的五道坎

5.1 推荐结果全是同一类书

现象:不管用户借过什么,推荐列表里永远是计算机类或文学类。原因:ItemCF 的相似度矩阵被热门类目主导,少数类目的书之间相似度普遍偏高。解决:在相似度计算前对热门类目做降权,或者对推荐结果做类目打散,每类最多出 2 本。

5.2 内存溢出:cosine_similarity 直接吃满 16G

现象:跑相似度计算时进程被系统杀掉。原因:图书数量上万时,稠密相似度矩阵是 N×N 的 float64,内存占用是 N²×8 字节。解决:用dense_output=False保持稀疏,或者分块计算,每次只算 1000 本之间的相似度。

5.3 冷启动用户推荐结果为空

现象:新用户没有任何借阅记录,推荐函数返回空列表。原因:协同过滤和文本相似度都依赖用户历史。解决:给新用户走热门推荐兜底,按借阅次数排序取 Top-N,同时结合用户注册时填的专业或兴趣做规则过滤。

5.4 评分矩阵归一化方式选错导致推荐偏差

现象:推荐结果偏向评分高的书,而不是用户可能喜欢的书。原因:不同用户的评分尺度不同,有人习惯打 5 分,有人最高只打 3 分。解决:对每个用户的评分做均值中心化,减去该用户评分的平均值,再算相似度。

5.5 文本向量化时中文未分词

现象:TF-IDF 矩阵里出现大量单字,相似度计算结果无意义。原因:TfidfVectorizer 默认按空格分词,中文句子没有空格。解决:先用 jieba 分词,把结果用空格拼接后再传入,或者自定义 tokenizer 参数。

6. 把推荐系统跑成服务:离线评估与在线接口的衔接技巧

离线评估用 Precision@K 和 Recall@K 就够了。把每个用户最后借的 5 本书藏起来做测试集,用剩下的数据训练,看推荐列表里命中了几本。

def precision_recall_at_k(recommend_fn, test_dict, k=10): precisions, recalls = [], [] for user_idx, true_items in test_dict.items(): pred_items = recommend_fn(user_idx)[:k] hits = len(set(pred_items) & set(true_items)) precisions.append(hits / k) recalls.append(hits / len(true_items)) return np.mean(precisions), np.mean(recalls)

Precision@10 能到 0.15 以上、Recall@10 到 0.1 以上,在图书场景就算可用。如果指标太低,先检查测试集划分是否随机,再调相似度 Top-N 和融合权重 α。

在线接口用 Flask 或 FastAPI 包一层,把相似度矩阵和稀疏矩阵在启动时加载到内存,请求进来只做查表和排序,单次推荐耗时能压到 10 毫秒以内。注意矩阵文件用 pickle 或 npz 存,别每次请求都重算。

我自己的习惯是:每次调完参数,先把 Precision@K 和推荐结果的类目分布打出来看一眼,确认没有退化成热门榜单,再上线。这套东西不复杂,但细节决定它能不能真的被人用起来。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询