1. 项目概述:当美食遇上算法
去年帮学弟调试这个毕设系统时,我们站在回民街的烤肉摊前突然意识到:在西安这座碳水之都,游客面对琳琅满目的泡馍、肉夹馍、凉皮时,选择困难症比代码报错还难解决。这个基于协同过滤的美食推荐系统,本质上是用算法模拟本地老饕的决策过程——当你在钟楼附近搜索"午饭"时,系统不会给你推荐人均300的米其林,而是直接指向巷子深处那家三代人经营的水盆羊肉。
技术栈选择Spring Boot+MyBatis组合(虽然被学弟吐槽不够新潮),但考虑到高校实验室电脑普遍还跑着JDK8,这种经典组合反而最保险。源码里最精妙的部分是处理"美食冷启动"问题的混合推荐策略——新开业的葫芦头泡馍店,会先通过内容相似度匹配到同区域的优质老店获得曝光机会。
2. 核心算法拆解:协同过滤的西安特调版
2.1 数据维度的本土化改造
常规的协同过滤可能只关注用户-评分矩阵,但我们在Scrapy爬取大众点评数据时,额外抓取了三个关键维度:
- 地理衰减因子:大雁塔附近的推荐权重随距离呈指数衰减(步行可达性比米其林星级更重要)
- 时间适配系数:早餐时段自动提升胡辣汤、油茶麻花的推荐优先级
- 口味迁移特征:连续三天选择"重口味"标签后,系统会试探性加入小炒泡馍等进阶美食
# 相似度计算代码片段(加入地域衰减) def adjusted_cosine_sim(u1, u2, loc_weight=0.6): base_sim = cosine_similarity(u1['ratings'], u2['ratings']) distance = haversine(u1['location'], u2['location']) return base_sim * (loc_weight ** distance)2.2 混合推荐策略实现
系统采用Item-CF与内容推荐的混合模式:
- 新用户冷启动阶段:基于LDA主题模型分析店铺简介文本,构建"麻酱凉皮-清真小吃"等隐含主题关联
- 行为数据积累后:切换到改进的Slope One算法,特别处理西安常见的"一次打卡"现象(游客往往只给某家店评一次分)
踩坑记录:最初直接用Surprise库的KNNBaseline,结果发现回民街商家的评分分布呈现明显的"两极分化"(要么5星要么1星),后来改用TunedUserKNN才解决预测偏差问题
3. 工程实现关键点
3.1 性能优化方案
在华为云1核2G的学生服务器上,我们通过三级缓存实现200ms内的推荐响应:
- Guava本地缓存:存储热门商圈的Top50店铺(TTL 15分钟)
- Redis集群:用户最近浏览记录的实时更新(使用HyperLogLog去重)
- MySQL预处理:提前计算好"钟楼-三件套"等经典组合的关联规则
// Spring Cache配置示例 @Cacheable(value = "foodCluster", key = "#regionId", unless = "#result == null || #result.size() < 5") public List<Store> getRegionalRecommendations(String regionId) { // 关联查询+随机采样逻辑 }3.2 风味图谱构建
最耗时的不是算法实现,而是建立西安美食的知识图谱:
- 用BERT-wwm提取店铺评论中的风味特征("腊汁肉肥而不腻"→ 油脂感:0.82)
- 人工标注300家老店的"隐藏吃法"(如:老米家泡馍要自己掰成黄豆大小)
- 通过OpenCV分析美食图片的色泽饱和度,建立视觉特征向量
4. 典型问题排查实录
4.1 推荐结果地域漂移
现象:用户在兵马俑附近查询,却推荐了20公里外的泡馍店
排查过程:
- 检查Redis地理位置缓存,发现GEORADIUS命令返回空
- 追溯发现高德API逆地理编码时,"秦始皇陵博物院"被识别为"西安市临潼区"而非具体坐标
- 添加景点别名映射表解决
4.2 新商家曝光不足
优化方案:
- 在召回阶段设置15%的新店配额
- 构建"师徒关系图":将老字号分店与新开业分店建立关联
- 引入模拟点击:当新店连续3天无曝光时,自动生成虚拟用户行为
5. 毕设答辩加分技巧
根据指导老师的反馈,这些细节最受青睐:
- 可视化设计:用ECharts绘制"美食势力分布热力图",展示算法如何发现洒金桥的隐藏美食带
- 对比实验:在回民街、永兴坊、小寨三个区域分别测试准确率,解释地域文化对推荐效果的影响
- 商业价值:计算推荐系统能为商户提升的翻台率(实测优质推荐使平均用餐时间缩短8分钟)
源码中最值得细读的是RecommendationStrategy接口的设计,采用策略模式方便切换算法。调试时建议先用mock_user_data.json测试,避免反复调用美团API被限流。