多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Python+Vue的协同过滤图书推荐系统:从算法选型到前后端联调实战

基于Python+Vue的协同过滤图书推荐系统:从算法选型到前后端联调实战 简介这份资源是面向高校计算机相关专业毕业设计的完整项目包主题为PythonVue基于协同过滤算法的图书推荐系统适合正在准备毕设、需要机器学习与前后端分离实战案例的学生参考。系统涵盖用户模块、图书模块、推荐算法模块与推荐结果展示模块核心采用用户基于协同过滤、物品基于协同过滤及混合型算法通过分析用户阅读喜好预测其可能感兴趣的书籍后端可用Flask或Django构建RESTful API前端以Vue.js实现流畅交互并配套数据库文档说明表结构、字段类型、索引与主外键关系。压缩包共346个文件约24.6MB包含32个py源码、55个vue组件、47个js脚本、19个css样式、2个sql建表文件及1个mp4演示视频另有jpg、png等界面截图与doc文档目录结构清晰。已有80人学习下载可帮助读者快速理解推荐算法落地流程、前后端协作方式与数据库设计思路为毕设开发与答辩提供完整参考。1. 从零到一这套协同过滤图书推荐系统到底能解决什么问题很多同学做毕业设计时一看到“推荐系统”四个字就头大觉得必须上深度学习、上大模型才算高级。但真实情况是企业里大量推荐场景用的还是协同过滤及其变种因为它可解释、易调试、冷启动方案成熟。这套基于 Python Vue 的图书推荐系统核心就是用用户对图书的评分或借阅行为算出“和你口味相似的人还喜欢什么”再把结果推给你。它适合谁适合正在找毕设题目、想快速跑通一个前后端分离项目、又希望算法部分有真东西可讲的本科生。你不需要先成为算法专家只要会 Python 基础语法、能装环境、能看懂 Vue 的模板语法就能跟着把整套系统跑起来。这一章先不碰代码把“为什么是协同过滤而不是别的”说清楚后面再动手。2. 协同过滤选型与数据建模为什么不用深度学习也能打2.1 用户协同与物品协同的取舍逻辑协同过滤分两大流派UserCF 和 ItemCF。UserCF 是“找相似用户推他们喜欢的书”ItemCF 是“找相似图书推和你借过的书相似的书”。在图书场景里ItemCF 通常更稳因为图书的数量远小于用户数量且图书之间的相似关系相对静态不会因为某个用户今天心情好借了本哲学书就剧烈波动。我一般会先算物品相似度矩阵再根据用户历史借阅记录做加权推荐。这样做的好处是当新用户进来时只要他借过一两本书就能立刻基于物品相似度给出推荐冷启动比 UserCF 友好得多。当然UserCF 也不是不能用如果你的系统用户量很小、图书量极大UserCF 反而能捕捉到跨品类的惊喜推荐。选型时记住一个经验值用户数 : 物品数 10 : 1 时优先 ItemCF否则两者都跑一遍看离线指标。2.2 评分矩阵的构建与稀疏度处理图书推荐系统最头疼的是评分矩阵极度稀疏。一个学校图书馆几万本书、几千个用户实际产生的借阅记录可能只有几万条矩阵稀疏度轻松超过 99%。直接拿稀疏矩阵做余弦相似度计算量大且相似度不可靠。常见做法是先过滤掉借阅次数少于 5 次的用户和少于 10 次的图书再用 Surprise 库或自己写 ALS 做矩阵分解降维。下面这段代码演示如何用 pandas 构建用户-图书评分矩阵并计算稀疏度import pandas as pd import numpy as np # 假设原始数据有三列user_id, book_id, rating # rating 可以是显式评分1-5也可以是借阅次数归一化后的隐式评分 df pd.read_csv(borrow_records.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)] # 构建评分矩阵缺失值填 0 rating_matrix df.pivot_table( indexuser_id, columnsbook_id, valuesrating, fill_value0 ) # 计算稀疏度 total_cells rating_matrix.shape[0] * rating_matrix.shape[1] non_zero np.count_nonzero(rating_matrix.values) sparsity 1 - non_zero / total_cells print(f矩阵形状: {rating_matrix.shape}, 稀疏度: {sparsity:.4f})这段代码的关键参数是user_counts 5和book_counts 10这两个阈值不是固定的需要根据你的数据量调整。如果数据量本来就少阈值要降低否则过滤完就没剩多少了。fill_value0是为了后续计算相似度时方便但要注意0 代表“未评分”不是“评了 0 分”计算余弦相似度时要把 0 排除掉否则会把没借过的书当成差评。我一般会在计算相似度前把矩阵转成稀疏矩阵用scipy.sparse存储内存能省 80% 以上。2.3 相似度计算的三种实现与选择相似度计算是协同过滤的核心。常用的有余弦相似度、皮尔逊相关系数和调整余弦相似度。余弦相似度对评分尺度不敏感适合隐式反馈皮尔逊相关系数会减去用户平均分能消除用户打分偏严或偏松的影响适合显式评分。在图书场景里如果用的是借阅次数作为隐式评分我推荐用余弦相似度如果用的是 1-5 星评分皮尔逊更稳。下面用 Surprise 库演示 ItemCF 的相似度计算from surprise import Dataset, Reader, KNNWithMeans from surprise.model_selection import train_test_split from surprise import accuracy # 数据格式user, item, rating reader Reader(rating_scale(1, 5)) data Dataset.load_from_df(df[[user_id, book_id, rating]], reader) trainset, testset train_test_split(data, test_size0.2, random_state42) # 使用基于物品的协同过滤相似度用余弦 algo KNNWithMeans( k20, # 取最相似的 20 个物品 sim_options{ name: cosine, user_based: False # False 表示 ItemCF } ) algo.fit(trainset) predictions algo.test(testset) accuracy.rmse(predictions)k20表示只取最相似的 20 个邻居做加权k 太大会引入噪声太小会欠拟合。一般从 10 到 40 之间调看 RMSE 最低点。user_basedFalse就是 ItemCF改成 True 就是 UserCF。Surprise 的好处是封装好了相似度计算和评分预测你不需要自己写矩阵运算适合快速验证。但如果你要自己控制相似度矩阵的稀疏化还是得手写。3. 后端接口与前端联调把推荐结果塞进 Vue 页面3.1 Flask 接口设计三个必须有的路由后端我一般用 Flask轻量、和 Python 算法代码无缝衔接。核心就三个接口/api/books返回图书列表/api/recommend/user_id返回推荐结果/api/rating接收用户评分。下面是一个最小可用的 Flask 实现from flask import Flask, jsonify, request from flask_cors import CORS import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity app Flask(__name__) CORS(app) # 解决 Vue 开发时的跨域问题 # 加载预计算的相似度矩阵和评分矩阵 rating_matrix pd.read_pickle(rating_matrix.pkl) item_sim cosine_similarity(rating_matrix.T) # 物品相似度矩阵 app.route(/api/recommend/int:user_id, methods[GET]) def recommend(user_id): if user_id not in rating_matrix.index: return jsonify({error: 用户不存在}), 404 # 获取用户已评分的图书索引 user_ratings rating_matrix.loc[user_id].values rated_idx np.where(user_ratings 0)[0] # 对未评分的图书计算预测得分 scores item_sim[rated_idx].dot(user_ratings[rated_idx]) / (np.abs(item_sim[rated_idx]).sum(axis0) 1e-8) scores[rated_idx] -1 # 已评分的排除 # 取 Top 10 top_idx np.argsort(scores)[-10:][::-1] book_ids rating_matrix.columns[top_idx].tolist() return jsonify({user_id: user_id, recommendations: book_ids}) if __name__ __main__: app.run(debugTrue, port5000)CORS(app)是必须的否则 Vue 在 8080 端口调 5000 端口会被浏览器拦截。item_sim[rated_idx].dot(...)这一行是 ItemCF 的预测核心用用户已评分图书的相似度加权求和。分母加1e-8是防止除零。scores[rated_idx] -1把已经借过的书排除掉避免推荐重复。这个接口每次请求都实时计算如果物品相似度矩阵很大建议提前算好存 Redis否则响应会慢。3.2 Vue 前端调用与推荐列表渲染Vue 这边用 axios 调接口把推荐结果渲染成卡片列表。下面是一个 Vue 3 的 Composition API 写法template div classrecommend-list h3为你推荐/h3 div v-forbook in books :keybook.id classbook-card img :srcbook.cover alt封面 / p{{ book.title }}/p p{{ book.author }}/p /div /div /template script setup import { ref, onMounted } from vue import axios from axios const books ref([]) const userId 1 // 实际项目中从登录态获取 onMounted(async () { try { const res await axios.get(http://localhost:5000/api/recommend/${userId}) // 拿到 book_id 列表后再调 /api/books 获取详情 const detailRes await axios.get(http://localhost:5000/api/books, { params: { ids: res.data.recommendations.join(,) } }) books.value detailRes.data } catch (err) { console.error(推荐接口失败, err) } }) /script这里有个容易翻车的点推荐接口只返回 book_id前端还需要再调一次图书详情接口。我一般会在后端直接 join 好图书信息一次返回完整数据减少前端请求次数。userId硬编码只是演示实际要从 Vuex 或 Pinia 的登录态里取。如果推荐列表为空检查一下用户是否在评分矩阵里以及rated_idx是否为空——新用户没有任何借阅记录时ItemCF 是没法推荐的这时候要降级到热门图书推荐。3.3 数据库表设计与索引优化图书推荐系统至少需要三张表用户表、图书表、借阅/评分记录表。下面是一个 MySQL 建表语句CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), cover VARCHAR(500), INDEX idx_title (title) ); CREATE TABLE rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, score TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id), INDEX idx_user (user_id), INDEX idx_book (book_id) );uk_user_book唯一索引保证一个用户对一本书只有一条评分记录避免重复插入。idx_user和idx_book是为了加速“查某用户的所有评分”和“查某本书的所有评分”这两个高频查询。如果数据量超过百万rating表要按月分表否则单表查询会越来越慢。我见过一个毕设项目rating 表没加索引推荐接口响应 8 秒加了索引后降到 200 毫秒血泪经验。4. 避坑与排查协同过滤图书推荐系统最常见的五个翻车现场4.1 推荐结果全是同一本书现象推荐列表里反复出现同一本热门书用户觉得系统很傻。原因热门图书的相似度普遍偏高ItemCF 加权时热门书权重过大导致“赢家通吃”。解决在相似度计算时对热门物品做惩罚公式是sim(i,j) / log(1 |N(i)|)其中|N(i)|是物品 i 的流行度。或者直接在推荐结果里做多样性重排限制同一作者或同一分类的书不超过 3 本。4.2 新用户进来推荐接口报 404现象刚注册的用户打开首页推荐接口返回“用户不存在”。原因新用户没有评分记录不在评分矩阵的 index 里。解决在接口里加判断如果用户不在矩阵中返回热门图书 Top 10 作为降级方案。热门图书按借阅次数排序从rating表里GROUP BY book_id ORDER BY COUNT(*) DESC就能拿到。4.3 相似度矩阵内存溢出现象本地跑得好好的一部署到服务器就 OOM。原因图书数量 2 万本相似度矩阵是 20000 x 20000 的浮点矩阵内存占用 20000200008 字节 ≈ 3.2 GB。解决用scipy.sparse存储稀疏相似度矩阵只保留相似度大于 0.1 的边内存能降到几十 MB。或者用 Faiss 做近似最近邻搜索不存全量矩阵。4.4 Vue 跨域请求被浏览器拦截现象前端控制台报Access-Control-Allow-Origin错误。原因Vue 开发服务器在 8080Flask 在 5000浏览器同源策略拦截。解决Flask 端加flask-cors扩展一行CORS(app)搞定。如果生产环境用 Nginx在 Nginx 配置里加add_header Access-Control-Allow-Origin *;。注意不要用*带上 cookie否则会有安全问题。4.5 评分数据用 0 填充导致推荐偏差现象推荐结果总是偏向那些被借阅次数多的书小众好书永远不出现。原因构建评分矩阵时fill_value0计算相似度时把 0 当成了真实评分导致“没借过”和“借了但没评分”混为一谈。解决计算相似度前用rating_matrix.replace(0, np.nan)把 0 变成 NaN然后用np.nanmean或掩码矩阵计算。或者直接用 Surprise 的Reader它内部会处理缺失值。5. 进阶技巧用矩阵分解把 RMSE 再降 15%协同过滤的邻居模型可解释性强但预测精度有天花板。如果你想让毕设的算法指标好看一点可以加一层矩阵分解MF。MF 把用户-物品评分矩阵分解成两个低秩矩阵的乘积本质是学习隐向量。Surprise 里直接有 SVD 实现from surprise import SVD from surprise.model_selection import cross_validate algo SVD( n_factors50, # 隐向量维度 n_epochs20, # 迭代轮数 lr_all0.005, # 学习率 reg_all0.02 # 正则化系数 ) cross_validate(algo, data, measures[RMSE, MAE], cv5, verboseTrue)n_factors50是经验值图书场景一般 30-100 之间。n_epochs20通常够收敛再多会过拟合。reg_all0.02控制正则化强度防止隐向量过大。我实测下来SVD 比 ItemCF 的 RMSE 能低 0.1 左右换算成百分比大概 15%。但 SVD 的缺点是推荐结果不好解释你没法告诉用户“因为和你相似的人喜欢这本书”。所以我的习惯是线上用 ItemCF 做召回保证可解释性离线用 SVD 做排序提升精度。两者结合毕设答辩时既有工程落地又有算法深度。还有一个容易被忽略的技巧把图书的文本信息标题、作者、分类用 TF-IDF 转成向量和协同过滤的隐向量拼接做混合推荐。这样能缓解纯协同过滤的冷启动问题——新书没有评分记录但可以通过内容相似度找到相似的老书把老书的评分迁移过来。代码不复杂sklearn的TfidfVectorizer几行就能搞定但效果提升很明显。我一般会在毕设论文里把这一块作为“创新点”写进去答辩老师很吃这一套。最后说一个我踩过的坑不要一上来就调参。先把 baseline 跑通记录 RMSE 和 MAE然后再逐个参数调。每次只改一个参数观察指标变化。我见过太多人同时改学习率、正则化、隐向量维度最后指标崩了都不知道是哪个参数的问题。调参是个耐心活急不得。希望帮到你。本文还有配套的精品资源点击获取
返回列表