ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python协同过滤图书推荐系统:从零搭建ItemCF实战

Python协同过滤图书推荐系统:从零搭建ItemCF实战 简介这份资源是基于Python实现的协同过滤图书推荐系统面向对推荐算法与图书推荐感兴趣的学者、研究人员和开发者尤其适合图书馆、在线书店从业者及毕业设计学习者。系统采用基于物品的协同过滤算法涵盖用户评分、图书列表、推荐列表、图书详情与管理页面等核心功能可帮助读者理解个性化推荐服务的实现思路。压缩包共68个文件约3.02MB包含5个py源码文件、20个js与6个html/css前端文件、8个xml配置、7张jpg与6张png界面截图、1份docx论文及1个sql数据库脚本源码、论文与截图三者配套便于从理论到实现完整学习。目前已有154人学习下载。读者可借助源码掌握协同过滤算法与数据库交互细节通过论文了解课题背景、系统设计与测试方法结合截图快速把握界面与功能结构适合作为课程设计、毕业设计或推荐系统入门实践的参考方案。1. 图书推荐系统为什么值得用协同过滤从零搭一遍电商平台的“猜你喜欢”、图书馆的“借阅推荐”、知识付费的“课程搭配”背后都绕不开一个经典问题如何从海量书目里挑出用户真正想看的那几本。基于 Python 实现的协同过滤图书推荐系统就是解决这个问题的入门级完整方案。它不依赖深度学习框架不需要 GPU一台普通笔记本就能跑通全流程。适合两类人一是想拿推荐系统练手的学生和转行者二是需要快速验证推荐效果的产品或运营同学。核心逻辑只有一句话——相似的人喜欢相似的书或者相似的书籍会被同一批人喜欢。前者叫 UserCF后者叫 ItemCF。图书场景天然适合 ItemCF因为书的数量相对稳定用户兴趣漂移慢而且“买了这本书的人还买了那本”这种推荐理由用户一看就懂。接下来我会把数据准备、相似度计算、推荐生成、评估排错整条链路拆开讲每一步都给出可复现的代码和参数说明。2. 协同过滤的两种路线UserCF 和 ItemCF 到底选哪个2.1 从图书借阅矩阵理解相似度计算协同过滤的输入是一张用户-物品评分矩阵。图书场景里评分可以是显式的 1 到 5 星也可以是隐式的借阅次数、停留时长、是否收藏。矩阵通常极其稀疏一个用户借过的书可能只占总书量的千分之一。计算相似度时余弦相似度最常用因为它对评分尺度不敏感。公式是两向量点积除以模长乘积。如果评分数据有用户偏严或偏松的问题可以先做中心化再算皮尔逊相关系数。实际写代码时我一般先用余弦跑一版基线再根据评估结果决定要不要换。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 构造一个 5 用户 x 6 本书的评分矩阵0 表示未评分 ratings np.array([ [5, 3, 0, 1, 0, 0], [4, 0, 0, 1, 0, 2], [1, 1, 0, 5, 0, 0], [0, 0, 5, 4, 0, 0], [0, 1, 4, 4, 0, 0], ]) # 计算用户之间的余弦相似度 user_sim cosine_similarity(ratings) print(用户相似度矩阵\n, np.round(user_sim, 2)) # 计算物品之间的余弦相似度需要转置矩阵 item_sim cosine_similarity(ratings.T) print(物品相似度矩阵\n, np.round(item_sim, 2))这段代码先构造了一个小规模评分矩阵然后分别计算用户维度和物品维度的余弦相似度。ratings.T把矩阵转置让每一行代表一本书在所有用户上的评分向量。cosine_similarity返回的是对称矩阵对角线为 1。参数上唯一需要注意的是输入矩阵不能有全零行或全零列否则模长为零会导致除零错误。真实数据里如果出现这种情况直接把这行或这列从矩阵里剔掉或者填充一个极小的默认值。2.2 ItemCF 在图书场景的三个落地优势图书推荐和电影、短视频推荐有一个本质区别书的内容消费周期长用户不会一天借十本但一本书借出去之后相似的书在接下来几周都有推荐机会。ItemCF 的第一个优势是物品相似度可以离线算好线上只做查表和加权求和响应速度极快。第二个优势是可解释性强推荐理由可以直接写成“因为您借过《XXX》所以推荐《YYY》”。第三个优势是物品数量远小于用户数量时相似度矩阵的存储和更新成本更低。我一般会先算物品相似度取 TopN 相似书再根据用户历史借阅记录做加权打分。权重可以用相似度本身也可以用相似度乘以用户对已借书籍的评分。def item_based_recommend(user_id, ratings, item_sim, top_n3): 基于物品相似度的推荐 user_id: 用户索引 ratings: 用户-物品评分矩阵 item_sim: 物品相似度矩阵 top_n: 推荐数量 user_ratings ratings[user_id] # 只取用户评过分的物品 rated_items np.where(user_ratings 0)[0] scores {} for i in rated_items: # 遍历该物品的所有相似物品 for j in range(ratings.shape[1]): if user_ratings[j] 0: # 只推荐未评分的 scores[j] scores.get(j, 0) item_sim[i][j] * user_ratings[i] # 按分数降序排列 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n] # 对第 0 个用户做推荐 recs item_based_recommend(0, ratings, item_sim) print(给用户 0 的推荐, recs)函数先拿到目标用户评过分的物品列表然后对每个已评分物品遍历所有未评分物品用物品相似度乘以用户评分累加得到预测分数。top_n控制返回数量图书场景一般取 5 到 10 本太多会稀释点击率。这里没有做热门惩罚实际落地时如果某本书被推荐太频繁可以除以它的流行度做打压避免马太效应。3. 用 Python 把图书推荐系统跑起来数据、模型、接口3.1 图书数据的清洗和评分矩阵构建真实图书数据通常来自图书馆借阅记录、电商订单或公开数据集。字段一般包括用户 ID、图书 ID、评分或借阅时间。清洗步骤分四步去重、去空、过滤低频用户和低频图书、构建稀疏矩阵。低频过滤的阈值我一般设用户至少 5 条记录、图书至少 10 条记录低于这个数的直接丢掉否则相似度计算会被噪声主导。构建矩阵时用scipy.sparse的 CSR 格式内存占用比稠密矩阵小一个数量级。import pandas as pd from scipy.sparse import csr_matrix # 假设 df 有三列user_id, book_id, rating df pd.read_csv(book_ratings.csv) # 过滤低频用户和低频图书 user_counts df[user_id].value_counts() book_counts df[book_id].value_counts() df df[df[user_id].isin(user_counts[user_counts 5].index)] df df[df[book_id].isin(book_counts[book_counts 10].index)] # 构建索引映射 user_ids df[user_id].astype(category) book_ids df[book_id].astype(category) df[user_idx] user_ids.cat.codes df[book_idx] book_ids.cat.codes # 构建稀疏评分矩阵 matrix csr_matrix( (df[rating], (df[user_idx], df[book_idx])), shape(df[user_idx].nunique(), df[book_idx].nunique()) ) print(矩阵形状, matrix.shape, 非零元素, matrix.nnz)value_counts()统计每个用户和每本书的出现次数然后用isin过滤。astype(category)把字符串 ID 转成整数编码cat.codes拿到编码值。csr_matrix的三个参数分别是数据、行索引、列索引和形状。matrix.nnz可以快速查看稀疏程度如果非零元素占比低于 1%说明数据非常稀疏相似度计算时需要考虑降维或使用隐语义模型兜底。3.2 相似度计算的向量化实现和 TopN 截断上一章的相似度计算用的是稠密矩阵数据量一大就会内存溢出。生产环境必须用稀疏矩阵的向量化计算。sklearn的cosine_similarity支持稀疏输入但返回的仍是稠密矩阵。如果物品数量超过一万建议只保留每个物品的 TopN 相似物品用堆排序或argpartition做截断。from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 用稀疏矩阵计算物品相似度 item_sim_sparse cosine_similarity(matrix.T, dense_outputFalse) # 只保留每个物品的 Top 20 相似物品 def top_n_similarity(sim_matrix, n20): 对相似度矩阵每行只保留前 n 个最大值 sim_dense sim_matrix.toarray() for i in range(sim_dense.shape[0]): row sim_dense[i] # 用 argpartition 找到第 n 大的位置 threshold np.partition(row, -n)[-n] row[row threshold] 0 return sim_dense item_sim_top top_n_similarity(item_sim_sparse, n20) print(截断后非零元素, np.count_nonzero(item_sim_top))dense_outputFalse让cosine_similarity返回稀疏矩阵节省内存。top_n_similarity函数把稀疏矩阵转成稠密后逐行处理用np.partition找到第 n 大的值作为阈值低于阈值的全部置零。参数n控制保留的相似物品数量图书场景一般 20 到 50 足够太小会丢失长尾推荐太大则失去截断意义。注意这个函数在大矩阵上仍然会占用较多内存如果物品超过十万需要改用分块计算。3.3 推荐接口的封装和冷启动兜底推荐接口对外只需要暴露一个函数传入用户 ID返回推荐图书列表。内部逻辑是查用户历史、查物品相似度、加权打分、排序截断。冷启动用户没有历史记录时直接返回热门图书榜单。热门榜单按借阅次数或平均评分排序每天离线更新一次。def recommend(user_id, user_item_matrix, item_sim, book_ids, top_n10): 推荐主入口 user_id: 用户索引 user_item_matrix: 用户-物品稀疏矩阵 item_sim: 物品相似度矩阵稠密 book_ids: 图书 ID 映射表 top_n: 推荐数量 user_vec user_item_matrix[user_id].toarray().flatten() if user_vec.sum() 0: # 冷启动返回热门图书 popular user_item_matrix.sum(axis0).A1 top_books np.argsort(popular)[::-1][:top_n] return [book_ids[i] for i in top_books] scores user_vec item_sim # 矩阵乘法一次算出所有候选分数 # 屏蔽已借阅的图书 scores[user_vec 0] -np.inf top_books np.argsort(scores)[::-1][:top_n] return [book_ids[i] for i in top_books]user_vec item_sim是整段代码的核心一次矩阵乘法就能算出该用户对所有未借图书的预测分数。scores[user_vec 0] -np.inf把已经借过的书分数设为负无穷排序后自然排除。book_ids是索引到原始图书 ID 的映射列表。冷启动分支用sum(axis0)统计每本书的总借阅量取 TopN 返回。这个兜底策略虽然简单但在新用户占比高的场景下能显著提升推荐覆盖率。4. 推荐效果评估和离线指标怎么看4.1 召回率、准确率和 F1 的计算方式推荐系统不能只看“推了什么”还要看“推对了多少”。离线评估一般把数据集按时间切分用前 80% 做训练后 20% 做测试。对每个测试用户把他实际借阅的书作为正样本推荐列表作为预测结果。召回率等于推荐命中数除以用户实际借阅数准确率等于命中数除以推荐列表长度。F1 是两者的调和平均。图书场景召回率比准确率更重要因为漏推一本书的代价远大于多推一本。def evaluate(recommend_func, test_matrix, train_matrix, k10): 计算召回率和准确率 recommend_func: 推荐函数返回图书索引列表 test_matrix: 测试集用户-物品矩阵 train_matrix: 训练集用户-物品矩阵 k: 推荐列表长度 hits, n_recall, n_precision 0, 0, 0 for user_id in range(test_matrix.shape[0]): test_items set(test_matrix[user_id].nonzero()[1]) if not test_items: continue rec_items set(recommend_func(user_id, train_matrix, k)) hits len(rec_items test_items) n_recall len(test_items) n_precision k recall hits / n_recall precision hits / n_precision f1 2 * recall * precision / (recall precision) if (recall precision) 0 else 0 return recall, precision, f1函数遍历测试集里每个有借阅记录的用户取实际借阅集合和推荐集合的交集作为命中数。n_recall累加所有用户实际借阅总数n_precision累加推荐列表总长度。最后算召回率、准确率和 F1。参数k一般取 10和线上推荐位数量保持一致。如果测试集用户没有借阅记录直接跳过否则会拉低指标。4.2 覆盖率、多样性和惊喜度这些补充指标召回率和准确率只能反映“推得准不准”不能反映“推得全不全”。覆盖率衡量推荐系统能触达多少比例的图书计算公式是推荐过的图书数除以总图书数。多样性衡量推荐列表里图书类目的分布可以用基尼系数或信息熵。惊喜度衡量推荐结果和用户历史兴趣的偏离程度图书场景里适度惊喜能提升用户探索欲但太高会显得莫名其妙。我一般把覆盖率作为硬指标低于 30% 就说明推荐过于集中在头部图书需要调整相似度截断参数或加入随机扰动。指标含义图书场景参考值召回率实际借阅中被推荐到的比例15% 到 30%准确率推荐列表中命中的比例10% 到 20%覆盖率被推荐过的图书占总图书比例30% 以上多样性推荐列表类目分布熵越高越好但需平衡准确率5. 避坑图书推荐系统落地时最容易翻车的五个地方5.1 相似度矩阵全为零现象跑完相似度计算后推荐结果为空打印相似度矩阵发现大部分值都是零。原因通常是评分矩阵太稀疏两个物品没有任何共同评分用户余弦相似度为 0。解决方法是降低低频过滤阈值或者改用基于内容的相似度做兜底比如用图书的类别、作者、出版社计算文本相似度和协同过滤相似度加权融合。5.2 推荐结果全是热门书现象不管给哪个用户推荐返回的都是借阅量最高的那几本。原因是热门图书在相似度计算中占据主导冷门书因为共同评分用户少相似度被稀释。解决方法是在打分阶段除以图书流行度的对数做热门惩罚。公式是score score / log(1 popularity)popularity是图书的借阅次数。这个改动通常能把覆盖率提升 10 到 20 个百分点。5.3 训练集和测试集时间穿越现象离线指标很高上线后效果暴跌。原因是切分数据集时随机划分导致未来数据泄露到训练集。图书借阅有时间顺序必须按时间切分用过去的数据预测未来的行为。我一般用用户最后一次借阅之前的记录做训练最后一次借阅做测试。如果数据量足够还可以做滚动窗口验证。5.4 内存溢出和计算超时现象物品数量超过五万时相似度计算直接卡死或报 MemoryError。原因是稠密矩阵占用的内存是物品数的平方乘以 8 字节。解决方法是全程使用稀疏矩阵相似度计算用dense_outputFalseTopN 截断用分块处理。如果还不行就上隐语义模型做降维把物品向量降到 64 维或 128 维再算相似度。5.5 推荐理由无法解释现象用户看到推荐位但不知道为什么要推这本书点击率上不去。原因是纯协同过滤只输出分数没有可读的理由。解决方法是在推荐结果里附带“因为您借过《XXX》”这样的说明。实现上在打分时记录贡献最大的已借图书作为推荐理由返回。这个改动对图书场景的点击率提升非常明显因为读者选书时很看重关联性。6. 把推荐结果写进 Excel 和生成可执行文件的两个实用技巧离线评估跑通之后下一步是把推荐结果交付出去。运营同学通常要 Excel开发同学要可执行文件。写 Excel 用pandas的to_excel就行但要注意图书标题可能包含特殊字符需要先做转义。生成可执行文件用PyInstaller把模型文件和依赖库一起打包对方双击就能跑。我一般会把相似度矩阵和图书映射表存成.npz和.csv主程序启动时加载避免每次重新计算。import pandas as pd import numpy as np # 假设 recs 是推荐结果列表格式为 [(user_id, book_title, score), ...] df_recs pd.DataFrame(recs, columns[user_id, book_title, score]) # 处理特殊字符避免 Excel 打开乱码 df_recs[book_title] df_recs[book_title].str.replace(r[^\w\s], , regexTrue) df_recs.to_excel(recommendations.xlsx, indexFalse, engineopenpyxl) # 保存相似度矩阵和映射表 np.savez_compressed(model_data.npz, item_simitem_sim_top) pd.Series(book_ids).to_csv(book_ids.csv, indexFalse, headerFalse)to_excel的engine参数指定openpyxl支持.xlsx格式。正则替换把非字母数字和空格的字符去掉防止 Excel 解析异常。np.savez_compressed把相似度矩阵压缩存储加载时用np.load即可。book_ids.csv保存索引到原始 ID 的映射推荐接口返回时查这张表。打包成 exe 的命令是pyinstaller --onefile --add-data model_data.npz;. --add-data book_ids.csv;. main.py。--onefile生成单个文件--add-data把模型文件打进包内。注意 Windows 和 Linux 的路径分隔符不同Windows 用分号Linux 用冒号。打包后第一次运行会解压到临时目录启动稍慢之后正常。最后说一个我踩过的坑相似度矩阵不要每次启动都重算图书数据每天更新一次就够了。我习惯用schedule库挂一个定时任务凌晨三点重新计算并覆盖模型文件主程序只负责加载和查询。这样线上接口的响应时间能稳定在 50 毫秒以内。希望帮到你。本文还有配套的精品资源点击获取
返回列表