☰
协同过滤图书推荐系统实战:Django+MySQL构建基于商品的推荐引擎
2026/10/12 12:57:51 网站建设 项目流程

简介:基于Python Django框架的图书推荐系统完整项目,后端采用基于物品的协同过滤算法(Item-based CF),依托豆瓣图书与评分数据集,结合Mysql存储,实现从用户登录、书目管理到推荐结果展示的完整流程。项目包含源码、数据集和说明文档,适合计算机相关专业学生用于毕业设计、课程设计或项目立项演示,也适合希望上手推荐系统与Web开发的初学者。资源包共52个文件,以20个Python源码文件为核心,涵盖模型、视图、URL路由及协同过滤算法实现,另有pyc编译文件、Django配置文件、前端HTML/CSS模板和README说明,整体压缩包约797KB,体积小、结构清晰,便于快速部署与二次开发。压缩包内还包含登录、首页、推荐结果展示等页面模板,以及Mysql相关配置。当前已有78人学习下载,代码经运行验证、测试通过,可放心使用;也可在此基础之上扩展新功能或调整算法逻辑,进一步提升学习价值。

1. 基于商品的协同过滤图书推荐系统:不写神经网络,照样做出推荐

拿到这份源码的第一感觉是「干净」:一个 Django 工程、一份豆瓣图书评分数据集、两个协同过滤算法文件,加起来代码量不大,却能把“用户登录 → 提交评分 → 生成推荐列表”完整跑通。它用的不是深度学习,正是推荐系统入行必学的基于商品的协同过滤(Item-based Collaborative Filtering)。用户给哪几本书打过分,系统就去找与这些书“口味相似”的其他书,按预测分从高到低排出 Top-N。全流程不依赖图书标题、作者、分类等文本特征,只靠评分共现关系,逻辑简单但效果不差。如果你正在做推荐系统课设或毕设,或者想搞清楚“物品相似度到底怎么算”,这套源码值得下下来亲手跑一遍。

2. 先把评分数据理顺:uid_score_bid 文件的读取、矩阵构建与稀疏度评估

这个资源里真正起决定作用的文件只有一个:uid_score_bid。它没有扩展名,用文本编辑器打开后,每一行就是一条「谁给哪本书打了多少分」的记录。字段顺序在源码里是用户 ID(uid)、评分(score)、图书 ID(bid),中间用逗号分隔。整份数据集本质上就是一张用户–物品评分表,协同过滤算法所有计算都从它出发。

2.1 uid_score_bid 文件到底存了什么

import pandas as pd # 文件没有表头,必须手动指定三列名称 ratings = pd.read_csv('data/uid_score_bid.csv', names=['uid', 'score', 'bid']) print("评分记录总数:", len(ratings)) print("参与评分的用户数:", ratings['uid'].nunique()) print("被评分的图书数:", ratings['bid'].nunique()) print(ratings.head(10))

names参数把三列分别命名为uid、score、bid;nunique()统计去重后的用户数和图书数,这两个数字直接决定后面相似度矩阵的规模。项目里大概是不到一千个用户、数千本图书、上万条评分记录,规模不大,但足够把一条 Item-based 推荐链路完整跑通。

再看一眼评分分布:

# 豆瓣评分以 1~5 的整数为主,统计每个分值出现的次数 print(ratings['score'].value_counts().sort_index())

输出结果里 4 分和 5 分占了大头,2 分以下记录很少。这是豆瓣用户真实打分习惯——不喜欢的书往往直接不打分。这个分布会影响后续推荐权重:既然整体评分偏高,单纯给高分书找相似书意义有限,更好的做法是额外统计每个用户自己的评分均值,把「用户给分高」和「用户真的喜欢」区分开,后面讲排序时会再提到。

2.2 从 CSV 到两种内存结构:正排表和倒排表

协同过滤算法的输入不是 DataFrame,而是嵌套字典。常见做法是构造两个结构:正排表user_items[uid][bid] = score,供推荐阶段遍历某个用户评过哪些书;倒排表item_users[bid][uid] = score,供相似度阶段统计每个物品被哪些用户评过。

from collections import defaultdict # 正排表:用户 -> {图书: 评分} user_items = defaultdict(dict) for uid, bid, score in ratings.itertuples(index=False): user_items[uid][bid] = score # 倒排表:图书 -> {用户: 评分} item_users = defaultdict(dict) for uid, bid, score in ratings.itertuples(index=False): item_users[bid][uid] = score # 简单验证:用户 1 应该至少有一条评分记录 assert len(user_items[1]) > 0, "用户 1 没有评分记录" print("用户 1 评分过的图书:", list(user_items[1].keys())[:5])

这里用itertuples而不是iterrows,是因为前者在遍历 DataFrame 时快一个量级,处理几万条记录几乎无感。defaultdict(dict)省去了每一层的判空逻辑。多花这几行代码把结构转好,后面算法部分就不再依赖 Pandas,读取原始文件的次数也变成一次——这也正是项目里算法模块的典型做法,启动时全部加载进内存。

2.3 稀疏度:决定了算法效果的下限

n_users = ratings['uid'].nunique() n_items = ratings['bid'].nunique() sparsity = 1 - len(ratings) / (n_users * n_items) print(f"稀疏度: {sparsity:.4f}")

算出来的值通常落在 0.9x,说明评分矩阵非常稀疏——大量用户只评过几本书,物品之间缺乏共同评分用户。稀疏度直接影响两个决策:第一,相似度计算要不要设置“共现次数下界”,比如两个物品至少被 3 个用户同时评过才保留相似度,否则算出来的相似度纯属噪音;第二,用户至少评过多少本书才触发协同过滤,否则推荐结果全凭一两条评分得出来,可信度极低。

3. 物品相似度是怎么算出来的:余弦公式、共现矩阵与 Top-N 推荐生成

理解了数据结构,接下来就是核心逻辑。基于商品的协同过滤分两步:先离线计算所有物品之间的相似度,再在线根据用户已评分的物品生成推荐列表。

3.1 余弦相似度为什么是 Item-based 的默认选择

物品 i 和 j 之间的相似度,数学上定义为:

sim(i, j) = |N(i) ∩ N(j)| / sqrt(|N(i)| * |N(j)|)

其中 N(i) 是给物品 i 评过分的用户集合。这个公式只看「谁评过」,不关心评分具体是多少,天然规避了用户打分尺度不同带来的偏差。相比皮尔逊相关系数,余弦相似度在稀疏数据上更稳定——两个物品共同评分用户往往只有个位数,皮尔逊在这种样本量下极易算出接近 ±1 的极端值,参考价值很低。等项目数据量涨上去,再替换成皮尔逊或调整余弦相似度也不迟,这是推荐系统最常见的演进路径。

3.2 共现矩阵的核心实现

import math from collections import defaultdict def build_item_similarity(user_items): """ 输入正排表 user_items: {uid: {bid: score}} 输出物品相似度字典: {(item_a, item_b): sim_score} """ # 1. 倒排表:物品 -> 评过分的用户集合 item_users = defaultdict(set) for uid, items in user_items.items(): for bid in items: item_users[bid].add(uid) # 2. 共现矩阵:统计两个物品被同一用户评分的次数 cooccur = defaultdict(int) for uid, items in user_items.items(): bids = list(items.keys()) for i in range(len(bids)): for j in range(i + 1, len(bids)): a, b = bids[i], bids[j] cooccur[(a, b)] += 1 cooccur[(b, a)] += 1 # 3. 代入余弦公式计算相似度 item_sim = {} for (a, b), cnt in cooccur.items(): item_sim[(a, b)] = cnt / math.sqrt(len(item_users[a]) * len(item_users[b])) return item_sim

第一步构建倒排表,把「物品有哪些用户评过」做成集合,后面算 |N(i)| 直接取集合长度即可。第二步是性能关键:如果对全量物品两两组合计算,复杂度是 O(items²),数据稍大就扛不住;改成遍历每个用户已评分的物品组合,实际只统计共现过的物品对,内存和耗时都小很多。第三步的余弦公式里,cnt就是共现次数,len(item_users[a])和len(item_users[b])分别是两个物品各自的评分用户数。

3.3 推荐排序:加权求和而不是只看相似度

相似度算完,怎么给用户生成推荐列表?基本逻辑是:用户给某本书打了 5 分,那就要优先推荐与这本书高度相似的书;如果用户只给另一本书打了 2 分,即使它的相似书很多,也不应该占太高的推荐权重。

def recommend(user_id, user_items, item_sim, top_n=10): """给指定用户生成 Top-N 推荐,返回图书 ID 列表""" rated = set(user_items[user_id].keys()) scores = defaultdict(float) sim_sum = defaultdict(float) # 遍历用户已评分的每本书,累加相似物品的加权得分 for bid, rating in user_items[user_id].items(): for (a, b), sim in item_sim.items(): if a == bid and b not in rated: scores[b] += sim * rating sim_sum[b] += sim # 用加权平均值排序,避免热门书靠累加得分霸榜 ranked = [] for bid in scores: if sim_sum[bid] == 0: continue ranked.append((scores[bid] / sim_sum[bid], bid)) ranked.sort(reverse=True) return [bid for _, bid in ranked[:top_n]]

rated集合用来排除用户已经读过的书,这是协同过滤的基本约束。scores[b]累加的是相似度乘以用户评分,sim_sum[b]累加的是相似度本身,两者相除得到的是一个归一化预测分。如果不做这一步归一化,候选物品的原始得分会被相似度总和带偏——那些和很多本书都有弱相似度的热门书,累加得分必然虚高,这就是为什么单纯的加权求和会推出一堆豆瓣高分书。归一化之后,排序结果的解释性也更强:预测分接近用户自己的历史平均评分水平。

3.4 和基于用户的协同过滤对比

项目里同时存在basedUserCF.py,这给了对照实验的机会。两种算法本质都是找“相似对象”,关键区别在相似对象是谁:

维度UserCF(基于用户)ItemCF(基于物品)
相似对象与你评分习惯相似的用户与你读过书相似的图书
适合场景新闻、短视频,用户兴趣变化快图书、电商,物品相对稳定
冷启动难度新用户无行为时更难推荐新用户只要评过一两本书即可推荐
可解释性解释起来绕「因为你喜欢 A,所以推荐相似的 B」

图书这种物品特征稳定的场景,ItemCF 天然更合适,这也是这套源码把基于物品作为核心方案的原因。basedUserCF.py的存在更多是用于课设对比,写论文或答辩时可以放一组实验:两种算法在同一数据集上的推荐效果差异。

4. 把算法搬上 Django 和 MySQL:模型建模、数据导入与视图集成

算法代码只是推荐系统的内核,完整系统还要能被人通过网页用起来。这一章梳理 Django 工程结构,以及怎么把离线算法和 Web 层连接起来。

4.1 Django 工程文件结构与职责划分

打开工程包,典型 Django 项目结构清晰可见:项目配置目录bookRecommend/里是settings.py、urls.py、wsgi.py;login/应用处理登录注册;主应用里放着models.py、views.py、basedUserCF.py等核心模块。这种划分很常规,但有一点值得留意:算法模块和 Web 逻辑没有强耦合,views.py负责从数据库取数据、调用算法、把结果交给模板渲染,算法计算细节封装在独立文件中。

# 切换到工程根目录,先跑迁移再启动服务 python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000

makemigrations会根据 models 定义生成数据库迁移脚本,migrate把表结构同步到 MySQL。runserver是 Django 自带开发服务器,本地调试够用;真要上线,后面要换 gunicorn 或 uwsgi。

4.2 评分表建模与 MySQL 导入

数据文件uid_score_bid最终要落到 MySQL 里,ORM 模型定义很直接:

# bookRecommend/models.py from django.db import models class Rating(models.Model): uid = models.IntegerField(db_index=True) bid = models.IntegerField(db_index=True) score = models.FloatField() class Meta: db_table = 'uid_score_bid' unique_together = ('uid', 'bid')

db_index=True给 uid 和 bid 各建索引,推荐查询时按用户 ID 取评分记录会快很多。unique_together保证同一用户对同一本书只会有一条评分,避免重复数据污染算法。db_table指明表名,方便和 CSV 文件名对应上。

MySQL 连接配置放在settings.py:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'bookrecommend', 'USER': 'root', 'PASSWORD': '你的数据库密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }

如果手上已经有整理好的 CSV,可以直接用 SQL 批量导入,不必写 Python 脚本逐行 insert:

LOAD DATA LOCAL INFILE 'uid_score_bid.csv' INTO TABLE uid_score_bid FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' (uid, score, bid);

注意列顺序要和 CSV 文件一致:uid、score、bid,千万别按 DataFrame 里看到的顺序想当然。用LOAD DATA导入百万行数据也是秒级完成,比 ORM 批量创建快得多。

4.3 在视图层把推荐结果渲染成网页

算法模块跑完,如何把推荐结果展示到网页上?核心在views.py:

from django.shortcuts import render from .models import Rating from .recommend import recommend def see_recommendations(request): # 从 URL 参数读取当前用户 ID,默认给用户 1 演示 uid = int(request.GET.get('uid', 1)) # 从数据库加载该用户的评分记录 rows = Rating.objects.filter(uid=uid) if not rows.exists(): return render(request, 'see.html', {'error': '该用户还没有评分记录,先去 index 页评几本书吧'}) # 组装成正排表送给算法模块 user_items = {uid: {r.bid: r.score for r in rows}} # 正式环境建议把相似度矩阵做成启动时缓存,不要每次请求都重算 item_sim = build_item_similarity(all_user_items) rec_bids = recommend(uid, user_items, item_sim, top_n=15) books = BookInfo.objects.filter(bid__in=rec_bids) return render(request, 'see.html', {'books': books})

filter(uid=uid)返回的是 QuerySet,按用户 ID 取评分记录;如果 QuerySet 为空,说明该用户冷启动,直接返回提示页。build_item_similarity这里有个性能优化点:每次请求都重建相似度矩阵会拖垮页面响应。常见做法是启动时算好存内存,或者用 Redis 做缓存,这套源码里由于数据量小,直接每次重建也能接受。

模板see.html里的渲染逻辑很简单,{% for book in books %}循环输出书籍封面和书名即可。登录页面login.html提交表单后重定向到推荐页,整个流程就闭环了。

5. 协同过滤落地常见问题排查:冷启动、数据稀疏、热门偏差与数据库坑

推荐系统的坑不在算法公式里,而在真实运行环境中。这几条踩坑记录是跑这套源码时最容易撞上的,按「现象 → 原因 → 解决」逐个说清楚。

5.1 新用户冷启动:推荐列表永远是空的

现象:一个新注册用户,没评过任何书,打开推荐页直接空白;评了 1 本书,推荐列表只有零星几条,还都是豆瓣上随便哪本都在推荐的热门书。
原因:Item-based 协同过滤的输入是评分记录,没有评分就没有共现关系,算法无从下手。
解决:加一层兜底逻辑——用户评分数量小于 3 条时,直接返回全局热门图书榜。热门榜由整个数据集评分数最多的前 10 本书构成,不涉及个性化计算。等用户评到 5 本以上,再切换到协同过滤。这套源码的默认页面是从 index 页引导用户先评分,但新用户直接访问推荐页时依然会触发空列表,这个边界值得处理。

5.2 评分太稀疏:相似度矩阵全是噪音

现象:推荐结果出现明显不相关的书,比如用户只读过 Python 编程书,推荐列表里混进来一本情感小说。
原因:稀疏矩阵下,两本书的共现次数只有 1 或 2,余弦相似度算出来虚高,相似关系完全不具备统计意义。
解决:在build_item_similarity里加一个共现次数下界阈值,比如if cnt < 3: continue,过滤掉低置信度的相似关系。调阈值后矩阵会稀疏很多,但剩下的每条相似度才勉强算“有点可信”。

5.3 热门书霸榜:推荐的多样性失效

现象:每次推荐列表里都是《小王子》《活着》这类高分热门书,用户根本没有被个性化的感觉。
原因:余弦相似度对热门物品有天然偏向。热门书出现在大量用户的评分列表里,共现次数远超小众书,即使归一化之后,它们的相似度整体仍然偏高。
解决:在排序阶段对热门物品做降权,常见公式是把相似度乘以1 / log(1 + 该物品的评分人数),热度越高的书降权越狠。改完后推荐列表会冒出一些“不完美但更对口”的书,这也是推荐效果看起来更像推荐的关键一步。

5.4 MySQL 8 连不上:认证插件不兼容

现象:执行python manage.py migrate时报错Authentication plugin 'caching_sha2_password' cannot be loaded。
原因:MySQL 8 默认的认证插件是caching_sha2_password,而 Django 连接 MySQL 的客户端库版本较老时不支持这个插件。
解决:把数据库账号的认证方式改为旧版兼容模式:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

改完后重启 MySQL 服务再跑migrate。如果用的是 PyMySQL,可以在项目__init__.py里加一行pymysql.install_as_MySQLdb(),效果相同。

5.5 Python 环境报错 No module named MySQLdb

现象:本地 Python 3.10+ 环境运行时,ModuleNotFoundError: No module named 'MySQLdb'。
原因:Django 原生依赖mysqlclient,而它在 Windows 上安装经常因为没有预编译的 wheel 包失败。
解决:先试pip install mysqlclient,装不上就改用 PyMySQL:

pip install PyMySQL

然后在 Django 项目__init__.py中加入:

import pymysql pymysql.install_as_MySQLdb()

这一步做完,Django 的mysqldb调用会自动转给 PyMySQL,不需要改settings.py。

6. 推荐结果怎么验证和调参:离线评估与三处最值得动的参数

推荐系统做完不是能跑就行,得知道推荐得准不准。最轻量有效的评估方法是留出法:把用户的历史评分随机切成训练集和测试集,用训练集生成推荐,看推荐结果里有多少本书出现在测试集里。

def evaluate(user_items, item_sim, users, test_ratio=0.2, top_n=10): """用留出法估算推荐准确率:命中数 / 测试集评分数""" total_hit = 0 total_test = 0 for uid in users: rated = list(user_items[uid].keys()) if len(rated) < 5: continue test_count = max(1, int(len(rated) * test_ratio)) test_items = set(rated[:test_count]) train_items = {bid: user_items[uid][bid] for bid in rated[test_count:]} # 注意这里用的是完整 user_items,实际应该重新训练;数据量小这个近似也能看趋势 recs = set(recommend(uid, {uid: train_items}, item_sim, top_n)) total_hit += len(recs & test_items) total_test += len(test_items) return total_hit / total_test if total_test else 0

test_ratio=0.2表示留出 20% 的评分作为验证集,recs & test_items计算推荐命中数,最后返回的是召回率——也就是测试集里的书有多少比例出现在了推荐列表中。这个数字一般不会太高,0.05 到 0.15 之间都算正常,毕竟每本书只有几条评分,能推荐中已经很难得。

参数调整主要看三处。第一处是top_n,推荐列表从 5 条改到 20 条,命中率会上升,但用户感知的精准度会下降,实际系统中通常 10 到 15 条比较均衡。第二处是共现次数下界,从 0 调到 5,相似度矩阵噪音减少,推荐结果会更纯净,但覆盖率会下降,常见做法是在 2 到 5 之间试几组值,挑命中率最高的。第三处是热门降权系数,上面提到的1 / log(1 + 热度)中,把 log 换成平方根可以调节降权力度,数值越大越偏向小众书。

那次调完参数后,我把这套源码改成了自己的一个习惯模板:每次拿到推荐系统数据集,先花五分钟统计稀疏度和评分分布,再决定要不要设置共现下界、要不要降权热门物品。数据说话,比凭感觉调参数靠谱得多。希望你也能在这份源码上跑出自己的推荐列表,希望这些调试路径能让你少绕几个弯。

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

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

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

立即咨询