news 2026/10/15 5:04:47

基于用户行为的美食推荐系统:Python与混合推荐算法实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于用户行为的美食推荐系统:Python与混合推荐算法实践

1. 这个选题到底在做什么

1.1 先从真实痛点入手

做美食信息推荐系统,最开始是因为我自己点外卖和刷菜谱时的一个直观感受:平台上的美食信息实在太多了,翻来翻去反而不知道吃什么。对一个毕业生来说,如果能做一个基于用户行为数据的推荐系统,把“推荐”这件事跑通,既能把大学期间学的数据处理、算法、Web开发串起来,又是一个非常直观、能展示完整工程链的毕设项目。

所以在项目立项时,我就把这个系统定义为:一个带完整前后端的Web应用,用户可以注册登录、浏览美食库、对菜品进行评分和收藏,系统根据这些行为数据,在“为你推荐”模块里给出个性化美食列表。这个定位非常关键,它决定了后面算法选型和功能模块的边界,也让我在中期汇报时能说清楚“我到底在推荐什么”。

如果你也准备做这个方向,建议先想清楚两件事:一是推荐的数据从哪来,二是推荐的逻辑怎么评估。对毕设来说,数据来源通常是人工录入的种子美食数据加上用户产生的评分记录,评估不要求做到业界精度,但必须能证明“推荐结果确实在变化、确实在个性化”。

1.2 为什么选Python而不是其他语言

选Python做推荐系统,核心原因有三个:库生态完整、原型迭代快、代码可读性好。尤其是pandas和numpy这两个库,处理相似度矩阵、评分归一化、TopN筛选这些推荐算法常用操作,基本就是几行代码的事,不用像Java那样写大量模板类。

Web框架方面,我用的是Flask。有人可能觉得Django更“正规”,但对于推荐系统这种以接口和算法为核心的毕设,Flask的轻量更合适:路由简洁,ORM可以单独用Flask-SQLAlchemy,配合SQLite起步非常快。不过如果你后期要扩展用户权限管理这类功能,Django自带的后台管理确实省事,这点可以根据自己的舒适度选。

注意:如果你准备参考网上的源码进行二次开发,最优先看的不是界面多漂亮,而是数据处理层和推荐算法模块是否独立封装。很多毕设源码把推荐逻辑写在视图函数里,后期改算法会非常痛苦。

2. 技术选型与推荐算法设计

2.1 数据存储与数据处理的组合方案

我最终确定的技术栈是:Flask + Flask-SQLAlchemy + SQLite + pandas + numpy。SQLite在本地开发时完全够用,数据集规模也就是几千条美食记录和几万条评分记录,SQLite文件型数据库反而比MySQL更方便迁移和备份。如果你想把项目放到服务器上演示,代码里只需要改一行数据库URI就能切到MySQL。

数据层设计采用“原始行为表 + 计算矩阵”双轨制:用户产生的每一次评分、收藏、浏览都实时写进行为表;推荐引擎需要计算时,从行为表里聚合出用户-美食评分矩阵,再在内存里用pandas完成相似度计算。这样做的原因是推荐计算不要求实时,但行为记录必须实时,否则用户每次刷新推荐结果都全量计算,系统撑不住并发。

2.2 推荐算法的三层混合设计

这是整个项目的灵魂。我没有只做一个协同过滤,而是设计了三层混合推荐策略,每一层解决不同阶段的问题:

第一层是基于内容的推荐。美食记录里有分类、食材标签、口味标签这些属性,我先把每个美食的标签向量化,比如“川菜、麻辣、鸡肉”对应一个多维向量,然后用余弦相似度计算每个美食之间的相似度。用户对某道菜评分高,就找出和它最像的其他菜推荐给他。好处是冷启动也能工作,因为有标签就有相似度。

第二层是基于用户的协同过滤。核心是“和你口味相似的人喜欢什么,你也可能喜欢”。我先构建用户-美食评分矩阵,计算用户之间的余弦相似度,找到当前用户最相似的K个用户,再把这K个用户评分过且当前用户没看过的美食按加权得分排序。这里我用了评分的均值中心化处理,避免有的用户习惯打高分、有的用户习惯打低分带来的偏差。

第三层是热度修正。对于新用户或者行为记录很少的用户,直接使用局部美食热度榜推荐——按平均评分和评分次数的加权值排序。这样能保证新用户进入系统时推荐列表不是空的。

三层结果融合时,我按用户行为量动态调整权重:行为记录少于5条时,内容推荐占70%、热度占30%;行为记录多时,协同过滤占50%、内容推荐占30%、热度占20%。这个比例是我自己调的,没有固定标准,思路是让推荐结果既有协同过滤的个性化,又保留内容的可解释性。

2.3 为什么放弃模型化算法路线

有同学问过我,为什么不用矩阵分解或者引入某种深度学习模型。我的回答是:模型化算法在毕设里很加分,但前提是数据量足够支撑训练。实训环境下,你可以轻易获得百万级评分数据,但毕设想自己造出这个数据量,时间成本完全不可控。矩阵分解模型在用户只有几十个人、评分记录只有几千条时,训练出来的效果反而不如简单的近邻协同过滤。

而且毕设答辩时,老师更看重的往往是“你知不知道为什么这样设计”。你能说清楚余弦相似度的含义、均值中心化的作用、混合策略的互补性,比直接跑一个模型更有优势。所以我的建议是:基础算法吃透,再考虑模型扩展。

3. 系统架构与数据库设计

3.1 整体模块是怎么划分的

系统按功能拆成五个模块:用户模块、美食模块、行为采集模块、推荐引擎模块、前端展示模块。这种划分方式不只是为了写代码方便,更重要的是它让每部分都能独立测试。比如推荐引擎可以单独写测试脚本传一份假行为数据进去,不用登录系统就能验证算法对不对。

用户模块负责注册、登录、状态保持和用户基本信息维护。美食模块负责菜品分类展示、菜品详情、标签管理。行为采集模块是一个全局的埋点逻辑,用户在菜品详情页停留、点收藏、打分数,都会记录到行为表里。推荐引擎模块是核心,包含离线相似度计算和在线推荐两个部分。前端展示用的是经典服务端渲染,模板引擎加载,这个选择对毕设来说比前后端分离更省时间,也不用处理跨域问题。

3.2 数据表设计与字段规划

数据表我设计了五张:用户表、美食表、美食标签表、评分记录表、用户收藏表。实际代码里还有一张用于缓存相似度矩阵的表,但那是可选的,不建表直接存成pickle文件也行。

用户表的核心字段是id、用户名、密码哈希、注册时间。密码必须做哈希存储,我用了安全哈希算法,绝对不要明文存密码,这是最基本的底线。美食表包含id、菜品名称、分类、描述、图片路径、平均评分和评分人数。平均评分字段是冗余字段,用来做热度排序,避免每次推荐都要聚合计算。美食标签表用多对多关系关联美食和标签,标签直接决定内容推荐的质量,所以我在种子数据里给每道菜都整理了5到10个标签。评分记录表存储用户对美食的每一次评分,字段包含用户id、美食id、评分值和创建时间,这个表是协同过滤的数据来源。用户收藏表则是行为数据的补充,收藏行为在推荐权重中比评分略低一点。

注意:评分记录表一定要加唯一约束,同一用户对同一美食只能有一条评分记录。如果没有这个约束,用户多次评分会产生重复数据,推荐结果会出现同一个菜品被反复推荐的情况,排查起来非常隐蔽。

4. 核心功能模块的实现细节

4.1 行为采集模块的实现思路

行为采集看似简单,但它是推荐系统数据质量的源头。我的做法是在视图函数里统一封装一个记录函数,评分、收藏、详情浏览都调用它。采集的字段包含用户标识、美食标识、行为类型、操作时间。行为类型用数字编码,比如1代表评分、2代表收藏、3代表浏览,这样在计算相似度时可以根据行为类型分配不同权重。

采集函数还做了一层数据校验:非法用户id直接跳过,评分必须介于1到5之间,重复评分先更新原记录而不是插入新记录。这些校验看起来不起眼,但能避免大量数据清洗工作。我第一版没有做重复评分校验,结果跑了几天发现评分表里同一对“用户-美食”出现多条记录,还得额外写脚本合并,典型的返工教训。

4.2 用户相似度计算的代码实现

协同过滤的核心就是构建评分矩阵和计算相似度。我用pandas实现的过程中,最简洁的思路是先做一个透视表:行为表里的用户id作为行索引,美食id作为列,评分作为值,空值填0。然后对行向量做余弦相似度计算。

参考代码如下:

import pandas as pd import numpy as np def compute_user_similarity(rating_df): # rating_df 包含 user_id, food_id, score 三列 # 透视成用户-美食评分矩阵,行是用户,列是美食 pivot_df = rating_df.pivot_table( index='user_id', columns='food_id', values='score' ).fillna(0) # 对每个用户评分做均值中心化 mean_centered = pivot_df.sub(pivot_df.mean(axis=1), axis=0).fillna(0) # 计算用户之间的余弦相似度 norm = np.linalg.norm(mean_centered.values, axis=1, keepdims=True) normalized = mean_centered.values / np.where(norm > 0, norm, 1) sim_matrix = normalized.dot(normalized.T) sim_df = pd.DataFrame(sim_matrix, index=pivot_df.index, columns=pivot_df.index) return sim_df, pivot_df

这段代码中均值中心化是关键的细节。如果不做中心化,一个评分普遍偏高的用户会跟几乎所有用户都“相似”,推荐结果会失去个性化。中心化之后,相似度反映的是评分趋势的一致性,而不是评分绝对值的高度。

4.3 TopN推荐的生成逻辑

在拿到用户相似度矩阵之后,推荐生成遵循“找到最相似用户 -> 取候选美食 -> 加权排序 -> 过滤已看菜品”的过程。我参考的实现是:先筛选出当前用户未评价过的美食,然后根据相似用户的评分进行加权求和,权重就是相似度值。

def generate_topn_recommendations(user_id, sim_df, pivot_df, top_k=10, top_n=5): # 获取除了当前用户以外最相似的K个用户 sim_scores = sim_df.loc[user_id].drop(index=user_id).sort_values(ascending=False) similar_users = sim_scores.head(top_k).index # 当前用户已经评价过的美食 rated_foods = pivot_df.loc[user_id][lambda x: x > 0].index # 候选美食:相似用户评价过且当前用户未评价 candidates = pivot_df.loc[similar_users] candidate_foods = [f for f in candidates.columns if f not in rated_foods] scores = {} for food in candidate_foods: # 加权求和,只使用对这道菜有评分的相似用户 valid_users = [u for u in similar_users if pivot_df.loc[u, food] > 0] if not valid_users: continue w_sum = sum(sim_scores[u] * pivot_df.loc[u, food] for u in valid_users) w_count = sum(abs(sim_scores[u]) for u in valid_users) scores[food] = w_sum / w_count top_foods = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return top_foods

这里有一个细节值得注意:加权求和的归一化用相似度绝对值之和,而不是相似用户数量。这样做的原因是不同用户相似用户数量不一样,如果直接用数量平均,相似度分布稀疏时结果会不稳定。归一化之后得到的是一个加权平均分,代表候选美食在“与你相似的一群人”中的综合口碑。

4.4 混合推荐的融合逻辑

混合推荐的重点是动态权重。我在代码里封装了一个策略选择器:

def hybrid_recommend(user_id, behavior_count): if behavior_count < 5: weights = {"content": 0.7, "popularity": 0.3, "collaborative": 0.0} else: weights = {"content": 0.3, "popularity": 0.2, "collaborative": 0.5} # 分别调用三个引擎得到分数集合 # 每个引擎返回 美食id -> 归一化得分 的字典 # 最后按权重相加排序

归一化这一步也踩过坑。内容相似度得分和协同过滤加权平均分不在同一个量级,直接加权会导致分数高的那一方完全主导结果。我的做法是:先对每个引擎的结果做min-max标准化到0到1之间,再按权重融合,这样每个引擎的贡献才是真实的。

5. 实操过程与核心环节实现

5.1 环境准备与依赖管理

我的开发环境是Windows系统,Python版本3.9。强烈建议用虚拟环境隔离专案,避免系统里的包版本冲突。虚拟环境激活后安装依赖,核心依赖就这些:

  • Flask 2.x
  • Flask-SQLAlchemy
  • pandas
  • numpy
  • Werkzeug

我把依赖写进了requirements.txt,这样在答辩演示时换一台机器就能快速还原环境。如果你的评分数据量大,需要用到数据库连接池,那就再装一个对应的数据库驱动,但开发阶段SQLite完全够用。

注意:pandas安装包体积较大,如果网络环境不好,可以用国内镜像源安装,一条命令就能指定镜像地址。这个细节对于演示设备快速准备环境很有实际价值。

5.2 从零搭建项目骨架

项目目录我按模块拆分,顶层只有启动文件和配置。主要目录包括:models目录放数据库模型、recommend目录放推荐算法相关代码、views目录放路由视图、templates目录放页面模板、static目录放样式和图片。

数据库初始化脚本我单独放在一个文件里,它负责创建所有数据表并且灌入种子美食数据。种子数据我用了一个列表,手动整理了常见菜系的代表菜品,每道菜配好分类和标签。这一步非常耗时,但值得认真做,因为推荐效果的直观体验很大程度上取决于种子数据的标签质量。如果标签都是“好吃”“不错”这类无效词,内容推荐算法基本就废了。

5.3 推荐接口调试实录

系统跑通后,我最常调试的是“为你推荐”接口。调试方式很简单:注册两个测试账号,一个专门给高分评价,一个专门给低分评价,然后对比两个账号的推荐结果是否不同。如果推荐结果完全一样,说明协同过滤没有生效,基本就是相似度矩阵计算或者评分矩阵透视出了问题。

有次调试发现推荐结果全是某个分类的菜,排查后发现是内容推荐引擎里有一个bug:相似度计算时没有排除美食自身。自己和自己的相似度永远是1,导致每道菜的“最相似菜品列表”第一条都是自己。这个bug的教训是,计算美食相似度矩阵时一定要在索引层面排除自身,否则所有美食都会默认推荐自己的同类菜,列表多样性会很差。

5.4 前端页面与交互逻辑

前端我用了一套开源的Bootstrap管理模板,重点页面有三个:登录注册页、菜品浏览页、推荐结果页。推荐结果页在加载时调用推荐接口,把返回的菜按评分顺序渲染成卡片。为了直观展示推荐理由,我在每张卡片上加了两行小字:如果是协同过滤推荐,就显示“用户和你口味相似”;如果是内容推荐,就显示“你评分过的菜中与它相似”。这种可解释性设计在答辩时很吸引眼球,因为你能直接展示推荐结果和行为依据的对应关系。

6. 常见问题与排查技巧实录

6.1 冷启动问题如何破

冷启动有两种:新用户冷启动和新菜冷启动。新用户没有历史行为,协同过滤模型对他完全无效,我选择用热度榜兜底。热度榜的排序公式是:平均评分乘以评分人数加权的复合分数。这个公式会让“评分人数多且评分也高”的菜排前面,避免出现一个人的五星好评把新菜顶到榜首的情况。

新菜冷启动则更麻烦,系统分配给它的评分人数天然为0。我的做法是把“新品推荐”做成一个独立区域,里面按上传时间倒序展示最近一周录入的美食,配合随机打乱,让新菜有机会被看见,被评分后逐渐融入热度算法。这个区域不参与常规推荐排序,但能让数据冷启动循环转起来。

6.2 评分数据稀疏导致相似度失真的修复

毕设数据量小的时候,用户之间根本没有共同评分的美食,相似度矩阵全是0,协同过滤完全失效。我用了两种办法缓解:一是把行为类型扩展到收藏和浏览,不只依赖评分。浏览行为权重低但数量多,能给矩阵增加大量非零值。二是把相似度计算从“美食维度”降成“分类维度”。用户不直接对同一道菜评分,也可以看他们偏好的菜品分类是否一致。这个降维操作大幅提升了稀疏场景下的推荐稳定性。

如果你做的项目想要更好的稀疏处理效果,还可以尝试“物品聚类”的思路,把相似的美食先归类,再在类级别上做协同过滤。但这需要额外维护聚类逻辑,毕设时间有限的话,扩展到分类维度已经够用了。

6.3 中文乱码与图片路径问题

中文乱码是Web项目老问题。我在创建数据库时统一指定UTF-8编码,页面模板也声明了UTF-8,表单提交的数据后端都做一次编码清洗。这三个环节少一个,就会出现页面显示正常但存入数据库乱码的情况。排查乱码问题时,最快的方法是直接查看数据库存储的原始内容,如果数据库里乱码,说明是写入链路的问题;如果数据库正常但页面乱码,说明是读取渲染的问题。

图片路径问题属于纯前端坑:种子数据里的图片地址有的是本地相对路径,有的是外部链接。开发时本地路径能显示,部署到演示环境时路径一换就全挂。最后我把所有图片统一放到static目录下,用菜名作为文件名,图片地址在美食数据表里只存相对路径,前端渲染时再拼接完整路径。

6.4 推荐结果实时性差的问题

我的推荐引擎采用“定时预计算 + 实时读取”策略。相似度矩阵这种耗时计算,不是每次请求时都现场算,而是等用户行为数据有明显变化后重新计算一次,计算结果缓存到文件里。接口环节只做读取和TopN筛选,响应速度能控制在50毫秒以内。

这个设计在答辩时也可以讲一下:你体现了对“计算密集型任务”和“查询密集型任务”的区分能力,这是真实工程系统里很重要的架构思维。

7. 个人经验与扩展建议

整套系统从零开始到跑通,我大概花了两周多的时间。最耗时间的其实不是推荐算法本身,而是种子数据整理和页面调试。所以如果你想复现这个项目,我的第一个建议就是:提前准备好结构化的美食数据,标签的质量直接决定内容推荐的上限。

对于扩展方向,我觉得有两条路很有价值:一条是增加“排行榜解释”功能,比如在推荐卡片上展示“这道菜和你喜欢的三道菜相似度都在80%以上”,解释性更强;另一条是引入时间衰减,让用户的近期行为权重更高,解决用户口味随时间变化的问题。这两个方向实现成本都不高,但能让项目的完整度和答辩深度提升一个档次。

最后再分享一个小技巧:项目里一定要留一个“重置演示环境”的脚本,一键清空所有用户行为数据、重新灌入种子数据、重新计算相似度矩阵。答辩时老师如果要看“新用户从零开始的推荐效果”,这个脚本能帮你快速恢复初始状态,避免现场演示翻车。我在实测中每次恢复环境后都会用两个测试账号重新打一次评分,确认推荐结果确实随行为数据发生了变化,整个演示流程就会非常可控。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:04:44

C++代码规范化:clang-format与clang-tidy的工程实践

接手一个迭代过七八年的 C 服务端代码库是什么体验&#xff1f;文件数两千出头&#xff0c;参与过提交的少说四十个人&#xff0c;代码能跑&#xff0c;业务稳定&#xff0c;但每次 code review 都像考古。有人用两空格缩进&#xff0c;有人用四空格&#xff0c;还有人看心情&a…

作者头像 李华
网站建设 2026/10/15 5:04:42

标识符设计指南:从数据库自增到雪花ID的选型与避坑

第一次看见这个项目名时&#xff0c;我确实没忍住笑了一下。“一emmmm&#xff08;标识符&#xff09;”&#xff0c;六个字符七上八下的&#xff0c;像极了我们写代码时在键盘前卡住的样子——光标一闪一闪&#xff0c;脑子里全是“这到底该怎么设计”。但笑归笑&#xff0c;在…

作者头像 李华
网站建设 2026/10/15 5:04:11

Install-Package完全指南:NuGet包管理器控制台安装实战

很多 .NET 开发者第一次听到 Install-Package&#xff0c;是在某篇教程里看到一句“打开包管理器控制台&#xff0c;输入 Install-Package 某某包”。但真自己动手时&#xff0c;才发现还有版本号、包源、目标项目、依赖解析这一堆绕不开的概念。这篇文章就把“如何执行 Instal…

作者头像 李华
网站建设 2026/10/15 5:02:32

Unity Invoke延迟调用详解:原理、避坑与最佳实践

1. 为什么一个延迟调用值得单独拿出来讲刚接触 Unity 那会儿&#xff0c;我写过一个特别蠢的逻辑&#xff1a;敌人死亡后要等 0.5 秒再播放爆炸特效&#xff0c;我当时的做法是在Update里挂一个计时器变量&#xff0c;每帧累加Time.deltaTime&#xff0c;到了阈值再触发。代码能…

作者头像 李华
网站建设 2026/10/15 5:02:19

Spark 2.4.6与HBase 1.3.1多列BulkLoad实战:从HFile生成到加载避坑指南

1. 为什么批量写入场景下 BulkLoad 比 Put 更值得选做过 HBase 写入的同学大概都有过这样的经历&#xff1a;用Table.put()一批一批往表里灌数据&#xff0c;刚开始几十万条还行&#xff0c;数据量一上千万&#xff0c;RegionServer 的写缓存开始频繁 flush&#xff0c;compact…

作者头像 李华
网站建设 2026/10/15 5:01:52

Oracle数据字典基表CDEF$与CON$竞争引发的性能问题排查与优化

做DBA这些年&#xff0c;性能问题见了不少&#xff0c;有一种问题最让人头疼&#xff1a;你翻遍AWR、盯了半天活动会话&#xff0c;发现罪魁祸首居然是Oracle数据字典里那几个不起眼的内部基表。CDEF$和CON$就是这类角色。它们平时藏在数据字典深处&#xff0c;几乎没人关心&am…

作者头像 李华