简介:基于协同过滤算法的美食推荐系统毕业论文章节资料,适合高等院校计算机、软件工程等专业学生参考,可用于毕业设计选题、系统框架搭建与论文写作。资源为单个docx文档,压缩包大小约6.61MB;文档包含中英文摘要、目录、研究背景与意义、国内外研究现状、系统总体设计、管理员端与用户端功能设计、协同过滤算法介绍、Mysql数据库设计、软件测试及总结等模块。已有128人学习下载。内容围绕如何利用Python、Django和Mysql实现个性化美食推荐展开,既阐述了基于用户相似度和基于物品相似度的推荐机制,也覆盖了购买记录、收藏、美食资讯等业务功能设计。对于希望快速理解推荐系统毕业设计全流程、梳理系统开发步骤或借鉴论文结构的学生而言,这份资料能提供较完整的写作与实现参考,并有助于把握系统测试、数据安全和后续优化方向。
1. 为什么「协同过滤美食推荐系统」值得照着做一遍
拿到这篇文章的读者,手里大概率有一份叫做《基于协同过滤算法的美食推荐系统设计与实现》的毕业论文文档,想看的不是目录里写了什么,而是它能不能变成一套能跑、能答辩、能复用核心代码的系统。这份论文是一份标准到不能再标准的本科毕设骨架:Python 做后端,Django 做 Web 框架,MySQL 存数据,协同过滤算法在后台算“你可能会喜欢什么菜”。它没有微服务,没有消息队列,但优点也在这里——用户表、美食表、购买记录表和相似度计算这一条链路是完整闭环,按着论文结构一个星期内可以复现,协同过滤那一段还能单独摘出来嫁接到别的项目里。适合两类人:做毕设需要完整系统参考的学生,想快速搭推荐原型验证算法效果的开发者。
2. 协同过滤算法落地:相似度矩阵、Top-N 推荐与两个公式
论文摘要里那句“通过对比用户间的相似度,自动将高相似度用户喜爱的美食推荐给目标用户”,是协同过滤最朴素的一句话。这句话落成程序,其实就两件事:算相似度、按相似度加权取 Top-N。算法本身不复杂,复杂的是你手里的数据长什么样。论文里给的是行为数据——购买记录、收藏、浏览历史,这些不是显式评分,需要先换算成能算距离的数字矩阵,然后再谈推荐。
2.1 基于用户的协同过滤:从行为矩阵到 Pearson 相似度
基于用户的协同过滤(UserCF)思考方式很直观:找一群和你口味最像的人,把他们吃过而你没吃过的东西推给你。数学上需要一个行为矩阵,行是用户,列是美食,单元格是用户对美食的行为分。有了矩阵之后,相似度计算最常用的不是余弦相似度,而是 Pearson 相关系数。
Pearson 公式写出来是 r = Σ(xi-x̄)(yi-ȳ) / [√Σ(xi-x̄)² · √Σ(yi-ȳ)²]。分子是共同行为过的美食上两个用户行为分的中心化协方差,分母是两个用户各自行为分的标准差乘积。它和余弦相似度的差别在于先去中心化再算方向,这就在意一件事:用户的行为频率差异太大,有人天天点收藏,有人三个月才买一次,绝对值没法比,但偏好方向可比。美食推荐这个场景里用户活跃度差异非常明显,所以 Pearson 比余弦更合适。
这一段还有一个容易忽略的参数:共同评分项目数。两个用户只共同吃过一道菜,算出来的相关系数不是 1 就是 -1,没有任何统计意义。常见做法是设置一个阈值,比如共同项目少于 2 就往回填 0,或者乘一个惩罚系数 min(common_count, K) / K,把“共同食物少”这个不确定性压进去。后期调参的时候,这个阈值比相似度公式本身的作用更大。
2.2 基于物品的协同过滤:Jaccard 系数与候选集排序
基于物品的协同过滤(ItemCF)是另一个方向:给用户推荐和他以前喜欢的食物相似的其他食物。关键不是内容属性相似,而是行为相似——两道菜经常被同一批用户购买,它们就是相似的。相似度计算在美食数据上有个很实用的选择:Jaccard 系数。
J(A,B) = |A∩B| / |A∪B|,分子是同时吃过 A 和 B 的用户数,分母是吃过 A 或 B 的用户数。分母大的时候,意味着 A 和 B 都是热门菜,它们之间的相似度会被稀释,这在推荐里是好事,避免推荐结果永远被热门菜品霸占。在论文提到的“根据物品间的相似性进行推荐,例如为用户推荐他们过去喜欢的类似美食”这一段,走的就是这条链路:先找用户历史行为涉及的美食集合,逐一找相似美食生成候选集,排除已经消费过的,按相似度加权求和,取 Top-N。
ItemCF 和 UserCF 怎么选,看数据规模。论文这个体量,几千条行为记录、一两百个用户,两个算法都能跑。如果用户行为非常稀疏,ItemCF 一般更稳,因为一个用户的历史行为物品数远小于总用户数,物品之间更容易找到交集。如果你的系统是用户少、但每个用户行为密集,UserCF 结果更有解释性,能讲出“和你口味相似的用户还喜欢吃什么”这套答辩话术。
2.3 论文里的数据映射:购买记录、收藏、浏览历史换算成行为分
论文里的行为数据不是评分制,系统没有让用户打星。常见的处理方式是把三种行为映射成权重分:购买记 1.0,收藏记 0.6,浏览记 0.2。同一用户对同一美食有多条行为记录时取最大值,不能让它线性叠加,否则浏览五次比购买一次权重还高,这不符合直觉。下面是这个过程的完整可运行代码,我一般会先把它单独跑通,再嵌进 Django 视图里。
# 行为权重映射:论文里的购买记录、收藏、浏览历史都是隐式反馈 WEIGHT = {'buy': 1.0, 'collect': 0.6, 'view': 0.2} # 模拟论文后台日志表的三类行为记录(user, food, action) logs = [ ('u1', '宫保鸡丁', 'buy'), ('u1', '水煮鱼', 'buy'), ('u1', '糖醋排骨', 'collect'), ('u2', '宫保鸡丁', 'buy'), ('u2', '水煮鱼', 'collect'), ('u2', '糖醋排骨', 'buy'), ('u3', '水煮鱼', 'buy'), ('u3', '糖醋排骨', 'buy'), ('u3', '麻婆豆腐', 'collect'), ] def build_matrix(logs): """日志 -> {user: {food: score}},同一用户对同一美食取最大行为分""" matrix = {} for user, food, action in logs: matrix.setdefault(user, {}) score = WEIGHT.get(action, 0) matrix[user][food] = max(matrix[user].get(food, 0), score) return matrix def pearson(m1, m2): """计算两个用户在前 dict 里的 Pearson 相关系数,共同项目少于 2 直接返回 0""" common = set(m1) & set(m2) if len(common) < 2: return 0.0 avg1 = sum(m1[f] for f in common) / len(common) avg2 = sum(m2[f] for f in common) / len(common) num = sum((m1[f] - avg1) * (m2[f] - avg2) for f in common) d1 = sum((m1[f] - avg1) ** 2 for f in common) ** 0.5 d2 = sum((m2[f] - avg2) ** 2 for f in common) ** 0.5 if d1 == 0 or d2 == 0: return 0.0 return num / (d1 * d2) def user_cf_recommend(matrix, target, top_n=3): """基于用户的协同过滤:找最接近的 top_n 个用户,相似度加权汇总候选美食""" sims = [(pearson(matrix[u], matrix[target]), u) for u in matrix if u != target] sims = [(s, u) for s, u in sims if s > 0] sims.sort(key=lambda x: x[0], reverse=True) result = {} for score, user in sims[:top_n]: for food, val in matrix[user].items(): if food not in matrix[target]: result[food] = result.get(food, 0) + val * score return sorted(result.items(), key=lambda x: x[1], reverse=True) matrix = build_matrix(logs) print(user_cf_recommend(matrix, 'u1'))build_matrix 负责把日志表里的原始记录转换成算法能用的行为矩阵,这一步其实对应论文里“收集用户历史行为数据”这句话,权重值可以根据数据分布手动调,我常用的是 buy=1、collect=0.6、view=0.2,如果收藏行为非常少,可以把收藏权重提上去。pearson 里的 common_count 判断必须保留,这是数值稳定性的底线。user_cf_recommend 输出的就是首页推荐模块的数据源:一个按分数排序的 (food, score) 列表。Django 视图拿到这个列表,再查 food_info 表把图片、价格带出来渲染就完事了。
3. Django + MySQL 搭系统:从 ER 图到页面模块
论文第 4 章的“系统总体设计”里最有价值的部分是功能结构图和数据库设计。功能语言转成 Django 项目结构,本质就是建几个 app、几张表、几个视图函数。先用数据库把业务边界定死,后面写代码才不容易翻车。
3.1 数据库表设计:五张核心表与字段说明
根据论文的功能描述,完整的表结构至少包含下面这些。字段名我按 Django 的命名习惯做了调整,下划线风格在 Python 里不用到处转驼峰。
| 数据表 | 用途 | 关键字段 |
|---|---|---|
| user_info | 用户账号信息 | username / password / nickname / phone |
| food_category | 美食分类 | name |
| food_info | 特色美食信息 | category_id(FK) / name / price / description / browse_count / is_recommend |
| purchase_record | 购买记录 | user_id(FK) / food_id(FK) / purchase_time |
| collect_record | 收藏记录 | user_id(FK) / food_id(FK) / collect_time |
| food_news | 美食资讯 | title / content / publish_time |
user_info 就是论文里管理员端“用户管理”的数据来源,food_category 管分类,food_info 管特色美食,purchase_record 和 collect_record 是协同过滤算法的数据源头。还有一个细节块:论文提到“浏览历史”参与推荐。如果你只是累计浏览次数,在 food_info 上放一个 browse_count 整数就够;如果想做时间衰减,就得单独建 browse_record 表,每条浏览记一行。毕业论文这个体量,用 browse_count 累计即可,算法那边把浏览权重设成 0.2 就能参与计算。
CREATE DATABASE food_recommend DEFAULT CHARACTER SET utf8mb4; CREATE TABLE user_info ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20) ); CREATE TABLE food_category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL ); CREATE TABLE food_info ( id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(8, 2), description TEXT, browse_count INT DEFAULT 0, is_recommend TINYINT DEFAULT 0, FOREIGN KEY (category_id) REFERENCES food_category(id) ); CREATE TABLE purchase_record ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, food_id INT NOT NULL, purchase_time DATETIME, FOREIGN KEY (user_id) REFERENCES user_info(id), FOREIGN KEY (food_id) REFERENCES food_info(id) ); CREATE TABLE collect_record ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, food_id INT NOT NULL, collect_time DATETIME, FOREIGN KEY (user_id) REFERENCES user_info(id), FOREIGN KEY (food_id) REFERENCES food_info(id) );这里有两个关键选择。第一,外键约束一定要加,后面做 ORM 关联查询、级联删除都依赖它,不加后面写 Django 模型会多出很多手工维护代码。第二,建库语句直接指定 utf8mb4,别用默认字符集,中文菜名和 emoji 表情都能存,后面省掉一堆乱码问题。
3.2 Django 模型层与前后台模块划分
论文里的功能清单很清晰:管理员端有用户管理、美食分类管理、特色美食管理、美食信息管理、购买记录、系统管理;用户端有首页推荐、特色美食展示、美食资讯、个人中心。Django 项目里通常按业务拆成四个 app:users、foods、orders、recommend,对应上面四类功能。模型定义直接映射数据库表。
# foods/models.py from django.db import models class FoodCategory(models.Model): name = models.CharField(max_length=50, verbose_name='分类名') class FoodInfo(models.Model): category = models.ForeignKey(FoodCategory, on_delete=models.CASCADE, related_name='foods', verbose_name='分类') name = models.CharField(max_length=100, verbose_name='菜名') price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='价格') browse_count = models.IntegerField(default=0, verbose_name='浏览次数') is_recommend = models.BooleanField(default=False, verbose_name='是否推荐') class PurchaseRecord(models.Model): user = models.ForeignKey('users.UserInfo', on_delete=models.CASCADE, verbose_name='用户') food = models.ForeignKey(FoodInfo, on_delete=models.CASCADE, verbose_name='美食') purchase_time = models.DateTimeField(auto_now_add=True, verbose_name='购买时间') class CollectRecord(models.Model): user = models.ForeignKey('users.UserInfo', on_delete=models.CASCADE, verbose_name='用户') food = models.ForeignKey(FoodInfo, on_delete=models.CASCADE, verbose_name='美食') collect_time = models.DateTimeField(auto_now_add=True, verbose_name='收藏时间')on_delete=models.CASCADE 的意思是用户或美食删除时,关联的购买记录和收藏记录也跟着删,这在管理后台删除数据时能避免留下大量孤儿记录。related_name='foods' 是给反向查询取的别名,模板里写 category.foods.all() 就能拿到该分类下的所有美食,不用手动过滤。购买记录和收藏记录是推荐算法的数据来源,外键字段建好之后,Django ORM 可以直接把记录取出来组装成行为矩阵。
URL 路由这一层没什么玄学,就是页面和视图函数的映射。管理员端可以复用 Django 自带 admin 快速管理用户和分类,自定义页面只需要覆盖首页、推荐位、购买记录导出;用户端按论文的模块拆开:
| 端侧 | 功能模块 | 对应 URL 路径 | 视图职责 |
|---|---|---|---|
| 管理员 | 用户管理 | /admin/users/ | 列表、禁用、重置密码 |
| 管理员 | 美食管理 | /admin/foods/ | 菜品增删改、设置推荐位 |
| 管理员 | 购买记录 | /admin/orders/ | 列表、按用户/时间筛选 |
| 用户 | 首页推荐 | / | 调用协同过滤接口渲染推荐 |
| 用户 | 美食列表 | /foods/ | 分类筛选、分页 |
| 用户 | 美食资讯 | /news/ | 资讯列表与详情 |
| 用户 | 个人中心 | /user/center/ | 密码、收藏、记录、浏览历史 |
管理员端如果直接复用 Django admin,开发工作量能省一半,论文测试章节也能写得更有内容。
3.3 初始化与数据准备:一周跑通系统的标准流程
项目初始化的标准流程基本是固定的,这里每一步都对应一个可执行命令。先把环境隔离好,再装依赖,最后做数据迁移。
# 创建虚拟环境并安装依赖,Windows 上 venv 激活命令不同 python -m venv venv source venv/bin/activate pip install django mysqlclient # 创建项目和 app django-admin startproject food_recommend_sys cd food_recommend_sys python manage.py startapp users python manage.py startapp foods python manage.py startapp orders python manage.py startapp recommend # 迁移数据库,需要先在 MySQL 里建好同名库 python manage.py makemigrations python manage.py migrate # 创建管理员账号,按提示输入用户名和密码 python manage.py createsuperuser # 启动开发服务器,访问 http://127.0.0.1:8080 python manage.py runserver 8080python -m venv venv 创建的是独立环境,依赖不会污染系统 Python 环境,换机器部署时只用 requirements.txt 就能恢复。mysqlclient 是 MySQL 的 Python 驱动,Django 官方推荐,比 pymysql 稳定。makemigrations 和 migrate 是 Django 的自动建表机制,前提是 settings.py 里已经把 DATABASES 指向 MySQL,并且在 INSTALLED_APPS 里注册了四个 app。
数据准备这一步容易被新手跳过。论文里没有提供真实数据集,你需要自己造一批足够支撑推荐算法的数据。我一般会在后台手工录入 10 个分类、100 道菜,然后写一个 Python 脚本往 purchase_record 和 collect_record 里随机生成 500 条行为记录,保证每个用户至少有 5 条行为。数据量太少时算法跑出来没有说服力,500 条是底线。
4. 避坑实战:协同过滤与 Django 开发中的五个常见问题
这段是复现系统过程中最容易踩坑的地方,每个问题我都踩过或看别人踩过。按“现象、原因、解决”说清楚,省得你再试一遍。
4.1 相似度全为 0:先查数据稀疏度再调算法
现象:算出来的相似度矩阵全是 0,推荐列表为空,或者更离谱——只有 1 和 -1,推荐结果和没推荐一样。
原因:行为数据太稀疏。100 道菜、10 个用户、每人只买了 2 道,任意两个用户共同吃过的菜大概率小于 2,Pearson 公式的 common_count 判断直接返回 0。反过来,某道菜只有两个人买过,那这两个人的相似度一定会被算成 1,推荐就废了。
解决:先打印一行统计:用户数、美食数、行为数、平均每个用户的行为数。平均行为数低于 5 时,先别调算法,回去造数据。另外,稀疏场景下优先用基于物品的协同过滤,因为物品之间的交集更容易命中。相似度公式里加一个惩罚系数也是个办法:共同项目少于 5 个时乘一个权重 0.5,少得越多压得越低。
4.2 新用户首页空白:冷启动必须有兜底策略
现象:新注册用户打开首页,推荐模块什么都没有。新上架的菜永远不进入任何用户的推荐列表。
原因:用户没有历史行为,相似度算不出来;美食没有购买记录,物品相似度也没有分子。协同过滤本质是“用历史预测未来”,没有历史就无解,这不是代码 bug,是算法边界。
解决:在推荐接口里做分层策略。第一层走用户协同过滤,命中就返回;第二层走物品协同过滤,再兜一层热门榜。热门榜用购买次数和浏览数加权的排行——SELECT food_id, COUNT(*) AS hot_score FROM purchase_record GROUP BY food_id ORDER BY hot_score DESC。论文里的“首页推荐美食”模块完全可以直接这样实现。新上架的菜在后台把 is_recommend 置为 1 先推到推荐位,等行为数据攒够了协同过滤自然接管。
4.3 美食列表卡顿:ORM 的 N+1 查询问题
现象:管理员端“特色美食管理”列表页每翻一页卡两三秒,美食数量超过 300 条后尤其明显。
原因:视图里写 FoodInfo.objects.all(),模板里循环时再访问关联分类,Django ORM 为每条美食记录单独执行一次分类查询。300 条美食产生 301 条 SQL,数据库连接和结果集组装的开销全部压在列表页上。
解决:视图里改成 FoodInfo.objects.select_related('category'),一次性用 JOIN 把关联分类查出来。外键字段用 select_related,多对多字段用 prefetch_related。再一个容易被忽略的点是给 purchase_record 加联合索引:ALTER TABLE purchase_record ADD INDEX idx_user_food(user_id, food_id),推荐算法在组装行为矩阵时也快很多。
4.4 中文乱码:MySQL 字符集配置是必修课
现象:网页上显示一串问号或者乱码,用 mysql 命令行导入 CSV 报错 Incorrect string value。
原因:数据库建库时用了默认字符集,实际是 latin1,中文菜名和生僻字根本存不进去;Django 连接的 charset 没指定,读写两侧编码不一致。
解决:建库语句直接写 DEFAULT CHARACTER SET utf8mb4,之前写过的建表语句里已经体现。Django 的 settings.py 里 DATABASES 配置要加 OPTIONS 指定字符集。
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'food_recommend', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }如果已经建好的库是 latin1,用 ALTER TABLE food_info CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 逐张表转换。用 Navicat 导数据时,导入界面的字符集也要手动切到 utf8mb4,不然数据进去就是乱码。
4.5 测试章节翻车:用例设计要能证伪
现象:论文第 6 章的实例测试写的是“输入用户名密码,点击登录,登录成功”,全部是这种描述,没有一组测试数据,没有预期结果。
原因:把系统测试写成了操作手册。测试章节的价值在于证明系统行为符合预期,你得给出可验证的输入和输出。答辩老师随便问一个“推荐结果为空你怎么验证”,论文里找不到答案就尴尬了。
解决:按测试用例表格写,每个用例包含测试名称、操作步骤、输入数据、预期结果、实际结果。这里给三个直接能用的例子:登录测试,输入已存在的用户名和错误密码,预期登录失败且提示“用户名或密码错误”;推荐测试,在行为记录齐全的用户上验证推荐列表里不包含已购买的菜;购买记录回显测试,下单后个人中心的购买记录列表出现该条记录。有了表格和实际运行截图,测试章节质量直接上一个台阶。
5. 进阶:冷启动缓解与推荐效果验证
论文做到能跑只是第一步,答辩时要讲得出“这个推荐到底有没有效果”,需要补两个东西:冷启动缓解方案和离线评估手段。
冷启动的缓解可以从两个方向做。一个是热门榜兜底,推荐接口拿不到协同过滤结果时返回购买次数最多的 Top 10,新用户和新上架美食都有出口。另一个是利用美食分类做基于内容的冷启动,新用户可以勾选偏好分类,比如选择“川菜”“粤菜”,系统先按分类下浏览量和销量排序推几道热门菜;新上架的菜在分类内部有了本地热度后,才有机会进入协同过滤的候选集。这两个方案都不涉及改动算法核心,只是给推荐模块加了热闹的入口和出口。
离线评估是论文里最薄弱也最好补的部分。常见做法是把用户行为记录按时间排序,前 80% 作为训练集,后 20% 作为测试集,用训练集跑推荐,看测试集的实际消费命中多少。评估指标用三个就够:精确率、召回率、覆盖率。精确率是推荐列表里有多少用户真的消费了,召回率是用户实际消费里有多少被推荐到了,覆盖率是推荐结果覆盖了多少道菜。一个简单的实现如下:
def evaluate_hit_rate(test_records, recommend_func, top_n=5): """根据测试集计算推荐命中率,test_records 为 {user: [实际消费的food]}""" hits = 0 total = 0 for user, actual_foods in test_records.items(): rec_foods = [food for food, score in recommend_func(user, top_n=top_n)] hits += len(set(rec_foods) & set(actual_foods)) total += len(set(actual_foods)) return hits / total if total else 0这个函数接受一个推荐函数作为参数,方便你替换 UserCF、ItemCF 或者其他改装版本做对比。实际跑的时候,把 80% 训练 20% 测试的切分做 5 次,取平均命中率,比单次切分稳得多。第一次跑完如果发现命中率低于 10%,不用慌,先看数据稀疏度——平均用户行为数低导致的低命中率,是数据质量问题,不是算法问题,白调参数没用。
从那以后我每次接推荐类系统,第一件事不是写相似度矩阵,而是先打印一行统计:用户数、美食数、行为数、平均每个用户的行为条数。这个数字决定了我用 UserCF 还是 ItemCF,决定我要不要加热门榜,也决定答辩时怎么解释推荐效果。这份论文文档能帮你把推荐系统壳子搭完整,但算法能不能出效果,决定权在你手里行为数据的质量上。希望帮到你。
本文还有配套的精品资源,点击获取