ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

K-Means聚类在校园美食推荐系统中的实践:从协同过滤困境到高效方案

K-Means聚类在校园美食推荐系统中的实践:从协同过滤困境到高效方案 1. 校园美食场景下为什么K-Means比协同过滤更友好1.1 经典推荐算法在课设中的现实困境每年到了课程设计和毕业设计选题的时候推荐系统都是最热门的方向之一。你去看知网上一堆本科论文十篇里有三篇是某某推荐系统的设计与实现剩下七篇是某某管理系统的设计与实现。但真正动手做你会发现推荐系统这个看似高大上的选题落到校园美食这个垂直场景里绝大多数经典算法都跑不起来。为什么先说说协同过滤。UserCF基于用户的协同过滤和ItemCF基于物品的协同过滤的核心思想都是统计用户的历史行为——评分、点单、收藏——然后计算用户之间或物品之间的相似度。这需要一张足够稠密的用户-物品行为矩阵。但校园美食场景下菜品总量撑死几十上百种用户群是本校几百个学生而且绝大多数用户不可能对每个窗口的菜品都评过分。结果就是矩阵稀疏得惨不忍睹算出来的相似度全是空值推荐质量根本没法看。我当年做这个题目的时候第一版想的也是ItemCF后来跑出来的结果让我直接放弃了大部分用户的推荐列表里全是热门口味的大众菜没有任何个性化可言。后来我把方案换成了K-Means聚类整个推荐链路瞬间顺了很多算法复杂度也降下来了。这篇文章就把我完整跑通的思路、代码结构、数据表设计以及答辩时被老师反复追问的细节全部拆开讲清楚。1.2 K-Means的定位用口味聚类替代共生矩阵K-Means的原理其实特别朴素初中生都能听懂把一群人按口味相似度分成几个圈子圈子内部的人饮食偏好接近圈子之间差异明显然后新来的人看他更像哪个圈子就用那个圈子的人爱吃的菜去推荐。用聚类做推荐的底层逻辑和协同过滤完全不同。协同过滤依赖用户之间的显式行为重叠而K-Means只需要你把每个用户描述成一组特征向量然后让算法自己去发现兴趣团体。这意味着就算你的行为数据很少只要有特征向量照样能聚类、能推荐。这在课设数据规模下简直是救命稻草。具体到校园美食场景特征向量的构造方式非常灵活。我用的方案是把每个用户表示成几个维度的偏好值对辣的耐受度、对甜口的偏好度、平均每餐可接受价格、荤食偏好程度、对油炸食品的偏好度等。这些值可以从用户注册时的口味标签、历史评分、点单记录里算出来。菜品也可以用同样的维度空间去表征这样用户和菜品就落在同一套坐标系里后面做推荐就好办多了。1.3 冷启动与可解释性课设答辩的加分点用K-Means还有一个隐性好处——冷启动和可解释性都天然解决了。冷启动方面传统协同过滤最怕新用户没有任何历史行为。但K-Means方案里新用户注册时填一下口味偏好问卷其实就是选几个标签就能构造出初始特征向量然后直接丢进训练好的模型里预测簇归属马上就有推荐结果。这也意味着你的系统demo可以随时给答辩老师现场演示注册新账号不用提前刷一堆数据。可解释性就更妙了。答辩的时候老师问你你的推荐依据是什么协同过滤的回答是因为相似度高的用户也点了这个这个解释绕来绕去老师不一定满意。而K-Means的回答是我们把你分到了第2簇这个簇的共同偏好是口味偏辣、人均15-20元、爱吃川湘菜所以给你推荐以下菜品这种可解释性对评委来说非常直观也是你毕业论文创新点包装的绝佳素材。注意K-Means不是万能的它做的是口味分群而不是精确预测。如果你志在做一个生产级推荐系统那还得上矩阵分解或深度模型但作为课设/毕业设计K-Means无论是在工程量、算法理解难度还是答辩说服力上都是性价比极高的选择。2. 系统总体架构与数据库设计2.1 功能模块拆分整个系统走的是Django最经典的MTV模式前后端不分离页面渲染用Django模板接口部分用JsonResponse返回兼顾展示和交互能力。功能模块我拆成了四个app各管一摊users用户注册、登录、个人口味标签维护。dishes菜品信息管理名称、所属食堂/窗口、类别、价格、口味标签、图片、评分附带Django Admin做后台增删改查。ratings用户对菜品的评分和评论这是构造特征向量的原始数据来源。recommend核心推荐模块负责特征提取、聚类训练、推荐结果缓存与展示。模块划分的原则很简单算法相关的代码隔离在recommend里不要混到views里写不然写到后面你自己都找不到逻辑在哪。2.2 核心数据表设计数据库直接用Django内置的SQLite就够用了不需要上MySQL。但表结构必须设计清楚下面是我最终用到表每张表都有存在的理由表名关键字段作用说明UserProfileuser(OneToOne)、spicy_level、sweet_level、price_sensitivity、meat_preference、fry_preference用户扩展表存口味特征向量所需的基础偏好数据Dishname、canteen、category、price、spicy_level、sweet_level、meat_or_veg、is_fried、avg_rating菜品主表字段设计尽量让菜品的特征空间和用户对齐Ratinguser(FK)、dish(FK)、score、comment、created_at用户评分记录用来动态修正用户偏好ClusterResultcluster_id、center_vector、member_count、created_at聚类结果缓存表避免每次请求都重新训练DishRecommand中间产出cluster_id、dish(FK)、rank_score每个簇对应的推荐菜品Top-N训练完成后持久化存储2.3 技术栈与版本选型版本匹配这个问题看起来不起眼但每年能卡住一批新手。我测试过的组合如下互相很兼容Python 3.10Django 4.1.xscikit-learn 1.2.xnumpy 1.24.xpandas 1.5.x特别要提醒的是sklearn和numpy的版本绑定非常紧网上很多教程让你直接pip install sklearn结果numpy版本冲突import就报错。我的建议是装的时候指定版本比如pip install scikit-learn1.2.2 numpy1.24.3一次性装好比出了错再排查省事得多。Django版本不要选太新的5.x出了之后有些第三方库的兼容还没跟上4.1这个版本文档多、生态稳踩坑了随便一搜就能找到答案。Python也别用3.12有些老版本的编译好的依赖装不上用3.10最省心。3. 核心算法从用户行为到推荐结果的完整链路3.1 特征向量的构建把吃没吃过变成可计算的数字这是整个系统最关键的一步。K-Means吃进去的是数值矩阵所以你得先把用户和菜品都翻译成一个固定维度的向量。我用的维度一共5个设计的时候遵循少而可解释的原则spicy_level辣度偏好0-5连续值0是完全不吃辣5是无辣不欢。sweet_level甜口偏好0-5。price_sensitivity价格敏感度我把它定义为人均可接受价格除以学校食堂平均价小于1说明偏向便宜窗口大于1说明消费水平偏高。meat_preference荤食偏好0-1之间的小数越接近1越爱点荤菜。fry_preference油炸偏好0-1。用户的这几个值怎么来三个来源注册问卷里用户可以自评辣度和甜度1-5分制互斥历史评分记录里Dish表同样存了每道菜在这5个维度上的取值用户评过分的菜对应维度加权平均就得到行为修正后的偏好值把问卷值和行为修正值做一个加权融合问卷权重0.4行为权重0.6防止用户随手乱填导致特征失真。菜品端的特征向量构造就简单多了录入菜品时直接按同一个五维空间打标签。比如麻辣香锅就是spicy5, sweet0, price1.2, meat0.8, fry0.6。不需要用户相似度矩阵也不需要奇异值分解思路完全透明。3.2 数据预处理与K值选择特征向量构造完之后不能直接丢进KMeans必须先做标准化。原因很简单价格敏感度的数值范围可能是0.5到2.5辣度只有0到5K-Means是基于欧氏距离的数值量纲大的特征会主导距离计算价格维度直接把口味差异淹没了。我用的StandardScaler做z-score标准化代码就两行from sklearn.preprocessing import StandardScaler scaler StandardScaler() user_features_scaled scaler.fit_transform(user_features)这里注意一个细节fit_transform是对全体用户一起做的训练完保存scaler对象后面新用户注册进来预测簇时只能用scaler.transform不能重新fit否则分布一变簇的划分就漂移了。K值的选择我用了最常见的手肘法加轮廓系数双重验证。手肘法看的是每个K值对应的SSE簇内误差平方和找到下降趋势明显变缓的拐点轮廓系数则衡量每个样本对自身簇和相邻簇的贴近程度取值[-1,1]越大越好。我在自己的数据集上跑出来的结果是K4最合适也就是全校用户大概能分成四类口味人群清淡养生党、无辣不欢党、甜口奶茶党、啥都吃干饭党。3.3 聚类与推荐生成算法核心部分代码不长但每一步都有讲究from sklearn.cluster import KMeans kmeans KMeans(n_clusters4, random_state42, n_init10) kmeans.fit(user_features_scaled) # 新用户推荐 new_user_vector build_feature_vector(request.user) new_user_vector_scaled scaler.transform([new_user_vector]) cluster_id kmeans.predict(new_user_vector_scaled)[0]聚类训练完之后怎么生成推荐列表我的做法分三步把每个簇里的用户评过分且分数4的菜品统计出来按出现频次排序再用菜品向量和该簇簇中心向量做一次余弦相似度排序取Top-N两者加权合并频次权重0.6相似度权重0.4得到最终推荐序。这个组合方式比单纯用某一项靠谱只看频次会变成全小区间热门榜一点都不个性化只看向量相似度又会把一些明明在簇里没人吃过的冷门菜推上去。加权的思路说白了就是你们圈子都爱吃 口味上确实很配双重把关。训练和推荐结果不允许实时跑。聚类模型训练是个离线任务定义成Django自定义管理命令数据量变了就手动python manage.py update_clusters执行一次执行完把每个簇的推荐菜写进DishRecommand表。线上推荐接口只查表不做任何计算响应速度毫秒级。4. Django工程落地的关键实现与避坑4.1 项目结构与自定义管理命令工程初始化阶段有两条命令你要记牢# 创建项目 django-admin startproject campus_food # 在项目内创建四个app python manage.py startapp users python manage.py startapp dishes python manage.py startapp ratings python manage.py startapp recommend新手经常犯的错是忘把app写进INSTALLED_APPS或者改完models不跑makemigrations和migrate这些属于常规操作我就不啰嗦了。我要重点讲的是自定义管理命令这是整个算法模块和Django结合的桥梁。更新聚类结果的命令文件放在recommend/management/commands/update_clusters.py结构大概是from django.core.management.base import BaseCommand from recommend.services import build_features, train_and_save_clusters class Command(BaseCommand): help 训练K-Means模型并更新推荐缓存 def handle(self, *args, **options): user_features build_features() train_and_save_clusters(user_features) self.stdout.write(self.style.SUCCESS(聚类模型更新完成))这里有个生产环境里常见的需求如果不想每次手动跑命令可以把命令挂到Django的定时任务里。我测试的时候用的是django-crontabLinux下配个cron表达式就行但注意Windows开发环境下cron不生效课设演示阶段还是手动执行更稳妥。4.2 推荐接口与后台管理推荐列表接口我写在了recommend的views里面走的是类视图。import json from django.http import JsonResponse from django.views import View from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from .models import ClusterResult method_decorator(login_required, namedispatch) class RecommendView(View): def get(self, request): # 查当前用户所在簇在登录时或注册时已经算好 cluster request.user.userprofile.cluster dishes DishRecommand.objects.filter( cluster_idcluster.cluster_id ).select_related(dish)[:10] data [{ name: d.dish.name, canteen: d.dish.canteen, price: str(d.dish.price), score: str(d.dish.avg_rating) } for d in dishes] return JsonResponse({code: 0, data: data})这里有两个细节容易被忽略一是select_related优化。查推荐列表时如果你要靠外键拿菜品信息不打这个标记的话N条推荐菜就是N次查询加了之后一次联表查询搞定。课设虽然数据量不大但代码里写出这种优化点答辩的时候说出去很加分。二是中文乱码问题。JsonResponse默认能处理中文但如果你用了json.dumps自己拼字符串记得加上ensure_asciiFalse。我第一次做的时候接口返回的是\u4e2d\u6587这种转义字符页面直接展示成了编码串这个坑可以说十个人里有八个踩过。后台管理方面我给Dish、Rating都注册了Django Admin在admin.py里用list_display、list_filter把字段展示优化了一下。菜品图片上传要注意在settings.py里配置好媒体文件路径MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media然后在项目级urls.py里加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)不加这个配置开发环境下图片永远403上传功能直接白瞎。4.3 我踩过的几个坑第一个坑是sklearn版本和Python版本匹配问题前面已经提过。第二个坑是Django的CSRF验证。如果你做了前后端分离用fetch或者axios从页面向接口发POST请求Django默认会拦你报403 CSRF验证失败。课设如果页面和接口是同源的同一个Django服务最简单粗暴的办法是从cookie里取csrftoken塞到请求头里// 页面内获取CSRF Token const csrftoken document.cookie .split(; ) .find(row row.startsWith(csrftoken)) ?.split()[1]; fetch(/api/recommend/, { method: POST, headers: { X-CSRFToken: csrftoken }, // ... });如果你偷懒想直接用csrf_exempt装饰器去掉验证我劝你不要。一是答辩时容易被问CSRF是什么、为什么不用二是这在真实项目里是安全漏洞没必要为了省事给自己埋雷。第三个坑是Django的delete()操作容易出关联问题。比如你要在admin里删一个菜品如果其他表里有外键指向它默认的PROTECT模式下会直接拒绝删除报ProtectedError。这个其实是个设计好的保护机制但新手看到报错会以为是bug。你需要在设计模型时想清楚每个外键的on_delete行为——用户删除时级联删他的评分用CASCADE菜品被评过分则用SET_NULL保留评分记录字段要设nullTrue。改完别忘重新migrate。还有跟token和登录态相关的我用了Django自带的login_required装饰器登录态默认走session。如果你想做记住我功能request.session.set_expiry()设置过期时间课设要求不高这样完全够用。如果你用了rest_framework想上JWT那又是另一个量级的复杂度了不建议在课设阶段引入。4.4 可选升级WebSocket实时推送如果你的毕设想冲个高分可以在最后附加一个WebSocket实时推送的功能。场景是当用户给某道菜打了高评分后如果他的簇里其他人也喜欢这道菜后台可以实时推送和你口味相同的人也在吃这道菜。Django官方推荐的通道是Channels库实现思路也不难前端用WebSocket连接后端加一个consumer评分保存后触发group_send把消息推给该用户所在簇的所有在线用户。这个功能演示效果非常惊艳老师会以为你做了实时推荐系统但工程量也就多一天。不过我要提醒一句别为了这个功能把项目复杂度拉爆。Channels需要ASGI部署Daphne或Uvicorn跟常规的WSGI部署还不一样如果配置没弄好整个项目在Windows上开发时经常起不来。我的建议是WebSocket做加分项放在核心功能全部稳定之后再加加不上也不影响主线。5. 课程设计答辩与文档包装经验5.1 系统功能演示的完整链路从零开始演示系统前我建议你保证一条最顺滑的路径进入登录页注册一个新账号注册问卷页勾选口味标签选偏辣、爱吃肉、人均15-20进入首页展示的推荐列表十几秒内出现里面全是川湘菜和烤肉饭点击任一菜品跳转详情看到口味标签、评分、食堂位置信息去后台管理页面管理员身份删除一条菜品演示数据完整性保护最后切换另一个口味比如清淡的账号展示推荐列表明显变化。这条链路走下来不超过五分钟但覆盖了用户模块、推荐算法、后台管理、数据一致性四个核心功能。演示之前记得预置好数据至少30道菜覆盖五个口味维度每个维度上都有明显的差异化菜品不然聚类出来的簇分不开推荐效果就砸了。5.2 万字文档的结构与高频答辩问题万字文档听起来吓人但真正实际写起来就那么几个大块。我建议的章节结构是第一章 绪论1.5k字背景、国内外研究现状、课题意义。研究现状部分千万别花大篇幅写深度学习你的系统用的是K-Means重点写聚类推荐和传统推荐算法的对比第二章 相关技术介绍1.5k字Django框架、K-Means算法原理、SQLite数据库第三章 需求分析1.5k字功能性需求注册登录、菜品管理、评分、推荐、非功能性需求响应时间、并发量、安全性第四章 系统设计2k字总体架构图、数据库E-R图、表结构、算法流程第五章 系统实现2k字核心功能截图加关键代码代码只贴核心的每段配两行注释说明逻辑第六章 测试1k字功能测试用例表加结果把登录、注册、推荐、评分、管理员删除这几条用例走一遍第七章 总结与展望0.5k字就说解决了什么问题、哪些地方还可以改进。图表是整个文档的灵魂。数据表结构用E-R图画出来算法这部分画一张K-Means聚类的示意图系统架构画一张分层的模块图。这三张图下来老师对你的评价直接上一个档次。可以用draw.io或者ProcessOn画导出PNG插进去。答辩的时候最容易撞上的问题基本都在下面这张表里。老师高频问题推荐回答思路为什么不用协同过滤协同过滤依赖用户行为交叉覆盖校园数据稀疏计算出的相似度可信度低K-Means按口味特征分群不受矩阵稀疏影响K值是怎么确定的手肘法看SSE拐点轮廓系数验证我在实验数据上K4最佳如果新用户没有行为数据怎么办注册问卷构造初始特征向量行为累积后加权修正这也是系统的冷启动方案你的推荐结果怎么评估好坏说明是规则验证人工评估抽取测试用户对比推荐菜品与其历史高分菜品的口味标签重合度聚类算法有哪些缺陷对初始质心敏感我用了random_state42固定和n_init10多次初始化降低随机性另外K-Means假设簇是凸的真实口味分布不一定符合答辩时有个通用技巧提问之后先别急着答把问题复述一遍然后用我们系统是这样处理的来开头先讲你采用的方案再讲为什么不做备选方案。这会让老师觉得你不仅做了实现也做了选型对比这才是课设论文该有的深度。写在最后的一点经验说了这么多最后分享一个我自己真正用过的技巧在推荐接口的返回值里加上一个reason字段。后端生成推荐列表时把该簇的共同偏好描述拼成一句人话返回比如第2簇用户口味画像偏辣、荤食、人均15-20元。前端页面直接把这句话展示成推荐理由。这个成本几乎为零但效果特别好。答辩老师看到这句文案会觉得你的系统能讲出推荐逻辑比干巴巴的菜品列表有说服力得多。我在最后demo时老师就顺着这句话追问了特征工程的设计思路之前的准备全部派上用场。做课设也好做毕设也好技术难度从来不是唯一的评分维度。把推荐链路讲清楚、把数据表设计合理、把核心算法的选型逻辑论证充分就已经能拿到一个非常不错的成绩了。希望这篇分享能帮你少走点弯路把时间花在真正拿分的地方。
返回列表