尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

黑马点评项目升级:集成语义相似度实现智能菜品推荐

黑马点评项目升级:集成语义相似度实现智能菜品推荐 黑马点评项目升级集成语义相似度实现智能菜品推荐不知道你有没有过这样的经历打开一个外卖或者点评App看着首页推荐的“猜你喜欢”心里嘀咕“这都什么跟什么啊我明明昨天刚吐槽过这家店太辣今天怎么又给我推川菜”传统的推荐系统比如我们熟悉的“黑马点评”项目大多依赖用户评分、购买历史、菜品标签这些“硬数据”。它们能算出你可能喜欢“高评分”的菜或者“很多人点”的菜但很难理解你藏在文字评论里的那些小心思。比如你评论说“这道红烧肉肥而不腻入口即化是我吃过最地道的本帮味道”系统可能只知道你点了红烧肉给了五星却读不懂你对“本帮菜”、“口感软糯”的深深偏爱。今天我们就来给这个经典项目做个“大脑升级”。我们不再只看分数和标签而是引入一个能“读懂”文字的AI模型——nlp_structbert_sentence-similarity_chinese-large。它的任务很简单分析你过去的所有评论理解你的口味偏好然后去海量菜品描述里找到那些在“语义”上最对你胃口的菜。这样一来推荐就从“猜你喜欢”变成了“懂你推荐”。1. 为什么要在推荐里加入“语义理解”我们先看看传统推荐方法的局限。在黑马点评原有的体系里推荐逻辑可能是这样的协同过滤喜欢A菜的用户也喜欢B菜那你可能也喜欢B。基于内容的推荐你常点“辣”标签的菜那就多给你推“辣”菜。热门推荐把销量最高、评分最好的菜放在前面。这些方法有效但不够细腻。问题出在哪第一标签太粗糙。“辣”是一个标签但“麻辣鲜香”的川菜辣和“酸辣开胃”的泰式辣是两种完全不同的体验。用户评论里“这辣味很醇厚”和“辣得有点烧心”也表达了不同的情感倾向但标签系统无法区分。第二丢失了大量隐藏信息。用户花时间写的评论是宝贵的偏好数据。“锅气十足”、“有妈妈的味道”、“食材很新鲜”这些描述蕴含了比“好吃”丰富得多的信息但在传统系统里它们只是文本无法被量化利用。第三冷启动和长尾问题。一个新用户没打过几次分或者一道小众但优质的菜没什么人评价传统方法就很难给出精准推荐。而语义相似度模型就像给系统装上了一双“读懂人心”的眼睛。它能把一段文字比如你的评论和另一段文字比如菜品描述都转换成高维空间中的“向量”可以理解为一串代表语义的数字。然后通过计算这两个向量之间的距离或相似度系统就能判断“哦这位用户夸赞的‘肉质鲜嫩多汁’和这道‘精选牛小排慢火煎制锁住肉汁’的描述在语义上非常接近他很可能也会喜欢这道菜。”这样一来我们就能捕捉到那些超越简单标签的、细腻的口味偏好实现真正的个性化。2. 核心武器语义相似度模型如何工作我们这次选用的模型是nlp_structbert_sentence-similarity_chinese-large。名字有点长我们拆开看StructBERT这是模型的基础架构它在经典的BERT模型上做了优化能更好地理解句子内部的结构比如词语顺序、语法关系对于中文这种注重语序和意合的语言特别友好。sentence-similarity它的核心任务就是计算句子之间的相似度。chinese-large这是一个针对中文训练的大规模版本对中文词汇、成语、网络用语的理解更准确。它的工作流程可以想象成一个“语义翻译匹配”的过程编码模型把你写的评论“这道红烧肉肥而不腻入口即化”和菜品的描述“本帮红烧肉采用五花肉文火慢炖两小时口感酥烂”分别转换成两个数学向量比如768维的一串数字。这个向量就是这句话的“语义指纹”。计算系统计算这两个向量之间的余弦相似度。这个值介于-1到1之间越接近1说明两句话的语义越相似越接近0说明越不相关。应用系统遍历所有候选菜品计算你的评论向量与每个菜品描述向量的相似度然后按分数从高到低排序把最相似的菜品推荐给你。在实际工程中我们不会在用户每次请求时都实时计算所有菜品那样太慢。通常的做法是离线处理提前用模型把所有菜品的描述都计算好“语义向量”存入数据库如Redis或向量数据库Milvus/Chroma。在线计算当需要为用户推荐时只需计算一次用户偏好向量基于其近期多条评论的向量综合得出然后与数据库中预存的菜品向量进行快速相似度检索。混合推荐最后将语义相似度的得分与原有的评分、热度等分数进行加权融合得到一个最终推荐列表。这样既保留了传统方法的稳定性又增加了语义理解的智能性。3. 动手升级代码实现关键步骤理论说完了我们来看看怎么在黑马点评项目里落地。假设我们原有的推荐服务是基于Spring Boot的。3.1 环境准备与模型服务搭建首先我们需要让这个中文大模型跑起来。这里我们使用ModelScope魔搭社区的镜像它提供了简单的一键部署方式。你不需要从零开始训练模型。# 假设我们使用Docker进行部署这是一个简化的示例命令 # 从ModelScope拉取预置的语义相似度模型镜像并运行 docker run -d --name structbert-similarity \ -p 8080:8080 \ -v /your/local/model/path:/app/models \ registry.cn-hangzhou.aliyuncs.com/modelscope-repo/structbert_sentence-similarity_chinese-large:latest服务启动后会提供一个HTTP API端点例如http://localhost:8080/encode接收文本返回对应的语义向量。3.2 核心服务层代码改造我们在原有的推荐服务RecommendationService中新增一个基于语义的推荐方法。Service Slf4j public class SemanticRecommendationService { Autowired private RedisTemplateString, String redisTemplate; Autowired private DishService dishService; // 原有的菜品服务 Autowired private RestTemplate restTemplate; // 用于调用模型API // 模型服务的地址 private static final String MODEL_API_URL http://localhost:8080/encode; /** * 为指定用户生成基于语义的菜品推荐 * param userId 用户ID * param topN 返回推荐数量 * return 菜品ID列表 */ public ListLong recommendBasedOnSemantics(Long userId, int topN) { // 1. 获取用户近期评论例如最近30条 ListString userRecentComments getRecentCommentsByUserId(userId); if (userRecentComments.isEmpty()) { log.info(用户 {} 暂无评论退回热门推荐, userId); return fallbackToPopular(topN); // 退回热门推荐 } // 2. 综合用户评论生成用户偏好向量 float[] userPreferenceVector generateUserPreferenceVector(userRecentComments); // 3. 从缓存或数据库获取所有菜品的向量首次需要全量计算并缓存 MapLong, float[] dishVectors getAllDishVectors(); // 4. 计算余弦相似度并排序 PriorityQueueDishScore minHeap new PriorityQueue(Comparator.comparingDouble(DishScore::getScore)); for (Map.EntryLong, float[] entry : dishVectors.entrySet()) { Long dishId entry.getKey(); float[] dishVector entry.getValue(); double similarity cosineSimilarity(userPreferenceVector, dishVector); // 维护一个大小为topN的小顶堆保留相似度最高的菜品 minHeap.offer(new DishScore(dishId, similarity)); if (minHeap.size() topN) { minHeap.poll(); } } // 5. 取出堆中元素按相似度从高到低返回 ListLong recommendedDishIds new ArrayList(topN); while (!minHeap.isEmpty()) { recommendedDishIds.add(0, minHeap.poll().getDishId()); // 逆序插入 } return recommendedDishIds; } /** * 将用户多条评论综合成一个偏好向量简单采用平均法 */ private float[] generateUserPreferenceVector(ListString comments) { Listfloat[] commentVectors new ArrayList(); for (String comment : comments) { float[] vector getSentenceVector(comment); // 调用模型API if (vector ! null) { commentVectors.add(vector); } } // 对所有评论向量求平均 return averageVectors(commentVectors); } /** * 调用远程模型服务获取单句文本的向量 */ private float[] getSentenceVector(String sentence) { try { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); MapString, String request Map.of(text, sentence); HttpEntityMapString, String entity new HttpEntity(request, headers); ResponseEntityMap response restTemplate.postForEntity(MODEL_API_URL, entity, Map.class); if (response.getStatusCode().is2xxSuccessful() response.getBody() ! null) { // 假设API返回格式为 {vector: [0.1, 0.2, ...]} ListDouble vectorList (ListDouble) response.getBody().get(vector); float[] vector new float[vectorList.size()]; for (int i 0; i vectorList.size(); i) { vector[i] vectorList.get(i).floatValue(); } return vector; } } catch (Exception e) { log.error(调用语义模型API失败: {}, sentence, e); } return null; } /** * 计算两个向量的余弦相似度 */ private double cosineSimilarity(float[] vectorA, float[] vectorB) { double dotProduct 0.0; double normA 0.0; double normB 0.0; for (int i 0; i vectorA.length; i) { dotProduct vectorA[i] * vectorB[i]; normA Math.pow(vectorA[i], 2); normB Math.pow(vectorB[i], 2); } if (normA 0 || normB 0) return 0.0; return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } // 内部类用于存储菜品ID和相似度得分 Data AllArgsConstructor private static class DishScore { private Long dishId; private double score; } }3.3 混合推荐策略单一的语义推荐可能不够全面我们需要将它和原有策略结合。Service public class HybridRecommendationService { Autowired private SemanticRecommendationService semanticService; Autowired private CollaborativeFilteringService cfService; // 原有的协同过滤服务 Autowired private PopularRecommendationService popularService; // 原有的热门推荐服务 public ListDishDTO getHybridRecommendations(Long userId) { // 1. 获取各渠道推荐结果及原始分数 ListLong semanticDishes semanticService.recommendBasedOnSemantics(userId, 50); ListLong cfDishes cfService.recommendForUser(userId, 50); ListLong popularDishes popularService.getPopularDishes(50); // 2. 定义权重可根据A/B测试调整 double weightSemantic 0.5; // 语义推荐权重 double weightCF 0.3; // 协同过滤权重 double weightPopular 0.2; // 热门权重 // 3. 加权分数融合简化示例按排名赋分 MapLong, Double finalScores new HashMap(); scoreByRank(semanticDishes, finalScores, weightSemantic); scoreByRank(cfDishes, finalScores, weightCF); scoreByRank(popularDishes, finalScores, weightPopular); // 4. 按最终分数排序取TopN ListLong finalRecommendations finalScores.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(10) .map(Map.Entry::getKey) .collect(Collectors.toList()); // 5. 根据菜品ID列表查询完整的菜品信息返回 return dishService.listByIds(finalRecommendations); } private void scoreByRank(ListLong dishList, MapLong, Double scoreMap, double weight) { for (int i 0; i dishList.size(); i) { Long dishId dishList.get(i); // 排名越靠前得分越高例如第1名得 weight*1.0第50名得 weight*0.02 double rankScore weight * (1.0 - (double) i / dishList.size()); scoreMap.put(dishId, scoreMap.getOrDefault(dishId, 0.0) rankScore); } } }4. 效果对比与业务价值升级之后推荐效果有什么不同我们模拟一个场景用户历史评论“这家店的糖醋排骨酸甜度刚好外酥里嫩孩子特别爱吃。”、“清蒸鲈鱼很鲜美火候掌握得不错保留了原汁原味。”传统推荐可能继续推荐“糖醋排骨”、“清蒸鲈鱼”或同为“鱼类”、“排骨类”的菜品如“红烧鱼”、“椒盐排骨”。语义推荐模型从评论中提取出“孩子爱吃”可能关联“健康”、“清淡”、“易咀嚼”和“原汁原味”关联“新鲜”、“清淡”、“烹饪手法简单”等深层偏好。它可能会推荐“虾仁蒸蛋”口感嫩滑原汁原味适合孩子“白灼菜心”清淡保留蔬菜原味“山药排骨汤”清淡滋补排骨的另一种健康做法可以看到语义推荐跳出了单纯的食材类别进入了“口味”、“口感”、“烹饪理念”的层面进行匹配推荐结果更具惊喜感和精准度。从业务角度看这次升级带来的价值是实实在在的提升点击率与转化率更“懂你”的推荐意味着用户更愿意点击和下单。增强用户粘性当用户发现系统总能推荐到心坎上自然会更频繁地使用。挖掘长尾商品一些描述精准但销量不高的小众优质菜品有机会通过语义匹配被推荐给真正会欣赏它们的用户。优化冷启动体验新用户只要发表一两条评论系统就能快速捕捉其偏好比等待足够多的评分数据要快得多。5. 总结给黑马点评项目集成语义相似度模型听起来是个技术活但归根结底是为了解决一个很实际的问题让机器更懂人。我们不再满足于让用户通过点击和打分这种“肢体语言”来交互而是尝试去理解他们留下的“文字心声”。实际操作下来整个流程的关键在于把离线计算菜品向量化和在线服务用户向量计算与检索做好拆分保证推荐接口的响应速度。同时采用混合推荐策略是个稳妥的做法既引入了AI的智能也保留了原有逻辑的稳定性避免因为模型波动导致推荐质量雪崩。当然这条路还可以走得更远。比如可以考虑引入更复杂的用户长期/短期兴趣建模或者尝试结合知识图谱把菜系的渊源、食材的性味也关联进去。但无论如何从“标签匹配”到“语义理解”这第一步迈出去项目的推荐系统就已经有了质的飞跃。下次当你再看到推荐列表里那道意想不到却让你眼前一亮的菜时或许就是这套新系统在默默工作的成果。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
返回列表