简介:这份资源是面向高校计算机相关专业毕业设计的完整项目包,主题为Python+Vue基于协同过滤算法的图书推荐系统,适合正在准备毕设、需要机器学习与前后端分离实战案例的学生参考。系统涵盖用户模块、图书模块、推荐算法模块与推荐结果展示模块,核心采用用户基于协同过滤、物品基于协同过滤及混合型算法,通过分析用户阅读喜好预测其可能感兴趣的书籍;后端可用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( index='user_id', columns='book_id', values='rating', fill_value=0 ) # 计算稀疏度 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_value=0是为了后续计算相似度时方便,但要注意: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_size=0.2, random_state=42) # 使用基于物品的协同过滤,相似度用余弦 algo = KNNWithMeans( k=20, # 取最相似的 20 个物品 sim_options={ 'name': 'cosine', 'user_based': False # False 表示 ItemCF } ) algo.fit(trainset) predictions = algo.test(testset) accuracy.rmse(predictions)k=20表示只取最相似的 20 个邻居做加权,k 太大会引入噪声,太小会欠拟合。一般从 10 到 40 之间调,看 RMSE 最低点。user_based=False就是 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(axis=0) + 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(debug=True, port=5000)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 class="recommend-list"> <h3>为你推荐</h3> <div v-for="book in books" :key="book.id" class="book-card"> <img :src="book.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 开发服务器在 8080,Flask 在 5000,浏览器同源策略拦截。解决:Flask 端加flask-cors扩展,一行CORS(app)搞定。如果生产环境用 Nginx,在 Nginx 配置里加add_header Access-Control-Allow-Origin *;。注意不要用*带上 cookie,否则会有安全问题。
4.5 评分数据用 0 填充导致推荐偏差
现象:推荐结果总是偏向那些被借阅次数多的书,小众好书永远不出现。原因:构建评分矩阵时fill_value=0,计算相似度时把 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_factors=50, # 隐向量维度 n_epochs=20, # 迭代轮数 lr_all=0.005, # 学习率 reg_all=0.02 # 正则化系数 ) cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)n_factors=50是经验值,图书场景一般 30-100 之间。n_epochs=20通常够收敛,再多会过拟合。reg_all=0.02控制正则化强度,防止隐向量过大。我实测下来,SVD 比 ItemCF 的 RMSE 能低 0.1 左右,换算成百分比大概 15%。但 SVD 的缺点是推荐结果不好解释,你没法告诉用户“因为和你相似的人喜欢这本书”。所以我的习惯是:线上用 ItemCF 做召回,保证可解释性;离线用 SVD 做排序,提升精度。两者结合,毕设答辩时既有工程落地又有算法深度。
还有一个容易被忽略的技巧:把图书的文本信息(标题、作者、分类)用 TF-IDF 转成向量,和协同过滤的隐向量拼接,做混合推荐。这样能缓解纯协同过滤的冷启动问题——新书没有评分记录,但可以通过内容相似度找到相似的老书,把老书的评分迁移过来。代码不复杂,sklearn的TfidfVectorizer几行就能搞定,但效果提升很明显。我一般会在毕设论文里把这一块作为“创新点”写进去,答辩老师很吃这一套。
最后说一个我踩过的坑:不要一上来就调参。先把 baseline 跑通,记录 RMSE 和 MAE,然后再逐个参数调。每次只改一个参数,观察指标变化。我见过太多人同时改学习率、正则化、隐向量维度,最后指标崩了都不知道是哪个参数的问题。调参是个耐心活,急不得。希望帮到你。
本文还有配套的精品资源,点击获取