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

资讯详情

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

基于SpringBoot的电影推荐系统:协同过滤算法与源码实战解析

基于SpringBoot的电影推荐系统:协同过滤算法与源码实战解析 这套基于SpringBoot的电影推荐系统是我前段时间完整梳理过的一个毕业设计项目源码结构、算法逻辑和部署流程都跑通了。如果你正打算做类似课题或者只是想把推荐系统的核心逻辑吃透这篇内容可以直接拿来当参考。我尽量把选型原因、算法实现、数据库设计、踩坑记录这些东西都说清楚你拿到源码之后能少走很多弯路。1. 核心需求拆解电影推荐系统到底在解决什么问题1.1 先想清楚“推荐”这个动作的本质电影推荐系统表面上看是一个“根据用户喜好推电影”的功能但真正设计起来你会发现它其实是在解决两个层面的问题。第一层是用户层面的用户打开系统面对几千部电影无从下手他需要一个入口快速找到自己可能感兴趣的内容。第二层是数据层面的用户对电影的评分、收藏、浏览行为都是散落的数据点系统需要把这些数据点串联起来形成对用户兴趣的量化描述。我见过不少同学做这类毕设上来就急着写代码结果写完发现推荐的电影完全不符合用户预期。根本原因在于没有想清楚推荐链路先有用户行为数据再构建用户兴趣画像最后才谈得上“推荐”。你连评分和收藏的入口都没设计好算法再花哨也跑不出效果。1.2 为什么这个项目选择SpringBoot作为技术底座选SpringBoot不是因为它“热”而是因为它非常适合这种体量的项目。一方面SpringBoot的自动配置机制大幅减少了搭建成本你不用像早期SSH项目那样写一堆XML配置文件一个Application启动类就能把项目跑起来。另一方面SpringBoot与前端交互、数据库操作的整合非常顺滑内置的Tomcat让部署也省了不少事。从毕设答辩的角度看SpringBoot也是加分项它本身就代表了行业主流的开发方式。评委看到你的项目用的是SpringBoot 推荐算法 可视化数据管理第一印象就比单纯写一个SSM增删改查好很多。尤其我下面要讲的协同过滤算法放在SpringBoot的Service层里实现逻辑清晰、便于拆分也方便你写论文时画架构图。1.3 系统功能边界不要贪大把主链路做扎实很多毕设选题失败不是因为题目太难而是因为功能范围没有收住。电影推荐系统最核心的功能就三块电影信息管理、用户评分收藏、个性化推荐。你把这三大块做扎实再搭配用户登录注册、推荐结果展示、后台管理这些配套功能整个系统已经非常完整了。我自己在梳理源码时特别注意了功能模块之间的粒度控制。比如评分功能一定要和推荐算法打通用户每次评分都必须实时更新到推荐计算的输入数据中。收藏功能同理它代表用户的隐式偏好权重可以比评分稍低但必须纳入推荐逻辑。处理好这些联动关系系统才能“活”起来。2. 整体架构设计与技术选型解析2.1 前后端交互的整体结构在这套源码里前端部分可以选择两种实现方式一种是传统的Thymeleaf模板引擎渲染页面另一种是前后端分离的Vue SpringBoot模式。我建议如果是为了快速完成毕设Thymeleaf方案更省时间因为前后端不分离部署和答辩演示都简单。如果你对前端比较熟想做前后端分离源码里也留了RESTful接口切换成本不高。后端整体走的是经典分层架构Controller接收请求、Service处理业务逻辑、Mapper操作数据库。推荐算法的核心代码放在Service层里单独抽出一个RecommendService不跟电影管理的逻辑混在一起。这样做的好处是算法逻辑调整时不会波及整个系统答辩时也能很清晰地说明“这里是推荐模块”。2.2 推荐算法选型为什么用协同过滤而不是深度学习现在一谈推荐系统很多人第一反应就是深度学习模型。但说实话对于毕设级的项目深度学习的投入产出比不高。你需要大量训练数据、GPU环境、复杂的模型调参最后解释性还很差。而协同过滤Collaborative Filtering算法在数据量不大的情况下效果已经很不错而且逻辑清晰论文里也更好写。这套源码采用的是基于物品的协同过滤ItemCF为主、基于用户的协同过滤UserCF为辅的混合策略。ItemCF的思路很简单给你推荐那些“和你喜欢的电影相似”的其他电影。这里“相似”不是指题材相同而是指“喜欢这部电影的人也喜欢另一部电影”。比如用户A喜欢《肖申克的救赎》和《教父》用户B喜欢《肖申克的救赎》和《霸王别姬》那么系统会认为《教父》和《霸王别姬》有相似性把《霸王别姬》推荐给用户A。2.3 数据存储方案与缓存层的取舍数据存储这块源码用的是MySQL因为整个系统的数据量级和关系结构用MySQL完全够用。用户表、电影表、评分表、收藏表四张核心表加上电影类型表关系清晰查询也不复杂。关于缓存我个人的建议是不必引入Redis至少一开始不要加。加了Redis之后整个项目的复杂度会上升一截缓存更新策略、缓存与数据库一致性都是很麻烦的问题而且毕设演示时并不需要支撑高并发。有些同学的论文里喜欢画复杂的架构图把Redis、RabbitMQ、ElasticSearch全画上去但实际代码里根本就没有。答辩时评委一问细节就露馅了。这套源码的做法是算法计算结果通过定时任务缓存到内存中用Spring自带的Scheduled定时刷新推荐结果既保证了性能又不引入额外的中间件依赖是个非常务实的方案。3. 推荐算法核心实现细节3.1 构建用户-电影评分矩阵推荐算法的第一步是把用户行为数据转化为算法能处理的矩阵。源码里维护了一个用户-电影评分矩阵行为用户ID列为电影ID矩阵的值就是用户对电影的评分。如果用户没有看过某部电影矩阵对应位置就是0或空。这里有一个容易踩的坑评分数据不能直接用“没看过”和“评分0”混为一谈。在协同过滤算法中0代表的是“缺失值”而不是“用户打了0分”。所以在构建矩阵时要确保原始评分数据中不存在0分记录也就是说评分表里只存用户真正评过分的数据。3.2 相似度计算余弦相似度与皮尔逊相关系数物品相似度计算是ItemCF的核心环节。常见的相似度算法包括余弦相似度、皮尔逊相关系数、杰卡德相似系数这套源码用的是余弦相似度。拿两部电影i和j来举例它们分别对应评分矩阵中的两列向量如果用户同时对这两部电影有评分则这些评分可以用来计算它们的相似度。余弦相似度的公式是两个向量夹角的余弦值取值范围在-1到1之间越接近1表示越相似。public double cosineSimilarity(MapInteger, Double movieIRatings, MapInteger, Double movieJRatings) { SetInteger commonUsers new HashSet(movieIRatings.keySet()); commonUsers.retainAll(movieJRatings.keySet()); if (commonUsers.isEmpty()) { return 0.0; } double dotProduct 0.0; double normI 0.0; double normJ 0.0; for (Integer userId : commonUsers) { double ratingI movieIRatings.get(userId); double ratingJ movieJRatings.get(userId); dotProduct ratingI * ratingJ; } for (Double rating : movieIRatings.values()) { normI rating * rating; } for (Double rating : movieJRatings.values()) { normJ rating * rating; } if (normI 0.0 || normJ 0.0) { return 0.0; } return dotProduct / (Math.sqrt(normI) * Math.sqrt(normJ)); }3.3 生成推荐列表的计算流程得到物品相似度矩阵后就可以为用户生成推荐列表了。流程分四步找到用户已经评过高分的电影集合比如评分4的电影。对每个高分电影找出与之最相似的N部电影。排除掉用户已经看过的电影。对候选电影按照“相似度与用户评分的加权求和”排序取前K部作为推荐结果。public ListInteger recommend(Integer userId, int topN) { MapInteger, Double userRatings ratingService.getUserRatings(userId); ListInteger highlyRatedMovies userRatings.entrySet().stream() .filter(e - e.getValue() 4.0) .map(Map.Entry::getKey) .collect(Collectors.toList()); MapInteger, Double candidateScores new HashMap(); for (Integer movieId : highlyRatedMovies) { MapInteger, Double similarMovies itemSimilarityCache.get(movieId); for (Map.EntryInteger, Double entry : similarMovies.entrySet()) { int candidateId entry.getKey(); double similarity entry.getValue(); if (userRatings.containsKey(candidateId)) { continue; } double weight similarity * userRatings.get(movieId); candidateScores.merge(candidateId, weight, Double::sum); } } return candidateScores.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); }这里要注意两点。第一相似度缓存一定要提前计算好不能在推荐接口里现场计算否则用户点一次推荐接口就要跑一遍全量相似度计算性能完全扛不住。实践里我一般用PostConstruct注解在项目启动时加载一次数据、算好相似度放进ConcurrentHashMap缓存。第二排序时如果遇到评分相同的情况等分并列会出现随机排序的情况建议再加一个“热度因子”作为次级排序条件比如以电影的评分人数作为参考。3.4 冷启动问题的变通处理冷启动是推荐系统没法回避的问题但它的应对方案其实并不复杂。新用户没有评分数据无法计算相似度这是典型的“用户冷启动”。新电影没有评分数据无法参与相似度计算这是“物品冷启动”。这套源码的处理策略很直接当用户的历史评分数据少于一定阈值比如少于5条时不启用协同过滤推荐转而推荐热门电影榜也就是按所有用户的平均评分从高到低排序取前N部。当电影新入库但还没有评分时按照电影类型匹配规则推荐如果用户的历史行为中没有该类型电影则按全局热度兜底。说白了冷启动问题的核心不是“算法多高级”而是产品策略怎么兜底。热门榜、高分榜、类型榜用好了其实效果并不差这也是很多真实商业系统的做法。4. 数据库设计与关键模块实现4.1 核心表结构设计数据库设计直接决定推荐算法的数据获取难度。我用表格把四张核心表的结构整理出来这个结构在源码里是直接建好的你导入就能用。表名字段说明userid、username、password、avatar、create_time用户表密码用MD5加密存储movieid、title、poster、director、actors、type、description、release_date、rating、rating_count电影信息表rating预计算的平均分ratingid、user_id、movie_id、score、rate_time评分表user_id和movie_id建联合索引favoriteid、user_id、movie_id、fav_time收藏表与评分表结构类似这里有个关键点值得展开说movie表中的rating字段是冗余字段它存储的是电影的平均评分这个字段不是人工维护的而是通过定时任务从rating表聚合计算出来的。之所以要冗余是因为推荐列表、热门电影榜、电影详情页都需要展示评分每次实时计算AVGscore的代价会随数据量增长迅速变大与其这样不如维护一个冗余字段查询时直接取性能好很多。4.2 MyBatis-Plus的使用与分页查询源码的持久层用的是MyBatis-Plus它在MyBatis基础上封装了通用Mapper和分页插件写单表CRUD几乎不用手写SQL。用户管理、电影管理这些模块的代码密度很低很大一部分功劳来自MyBatis-Plus。电影列表页的分页查询源码里用的是MyBatis-Plus的分页插件配置一个PaginationInnerInterceptor即可Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询的Service层代码大概是这个结构public IPageMovie getMoviePage(int current, int size, String keyword) { PageMovie page new Page(current, size); LambdaQueryWrapperMovie wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Movie::getTitle, keyword); } wrapper.orderByDesc(Movie::getRating); return movieMapper.selectPage(page, wrapper); }这里需要提醒一件事MyBatis-Plus的Wrapper查询字段名用的是Lambda表达式引用这种方式的好处是编译期就能发现字段名拼写错误但性能上要注意like查询走不了索引。好在这个场景是电影列表页全表扫描的量级可控但如果数据量上了百万就得考虑改用全文索引了。4.3 评分与收藏模块的实现要点评分和收藏模块的代码本身不难但有几个业务细节容易忽略。打分的时候必须先判断用户是否已经评过这部电影。评过了是更新没评过是新增。我见过很多半成品源码就是在这一步偷懒用户重复点击评分就插入重复记录导致推荐算法拿到的评分矩阵出现同用户同电影多条记录相似度计算直接出错。正确的做法是在rating表中对user_id和movie_id两者建唯一约束或者先在代码里检查。public void rateMovie(Integer userId, Integer movieId, Double score) { Rating existing ratingMapper.selectOne(new LambdaQueryWrapperRating() .eq(Rating::getUserId, userId) .eq(Rating::getMovieId, movieId)); if (existing null) { Rating rating new Rating(); rating.setUserId(userId); rating.setMovieId(movieId); rating.setScore(score); ratingMapper.insert(rating); } else { existing.setScore(score); ratingMapper.updateById(existing); } }另外用户评分之后推荐结果不应该立即重新计算全量相似度矩阵而是先把该用户的实时行为记录下来由定时任务统一触发推荐结果的刷新。你可以在评分接口里打一条日志也可以在内存中用Set记录受影响用户的ID定时任务扫描时再对这些用户单独刷推荐缓存这样既保证了实时性又不会频繁跑全量计算。5. 系统的分层架构与SpringBoot核心配置5.1 后端代码分层结构与包组织我拿到一套源码第一件事就是看它的包结构包结构不乱项目基本上不会乱。这套源码的分层如下com.example.movie ├── controller // 接口层RestController ├── service // 业务层推荐算法的核心 ├── mapper // 持久层MyBatis-Plus的Mapper接口 ├── entity // 实体类对应数据库表 ├── config // 配置类比如跨域、分页插件 ├── common // 通用返回体、工具类、常量 └── task // 定时任务负责预计算推荐数据这种分包方式在SpringBoot项目中很主流答辩讲解时也可以直接按这个过程讲请求从Controller进来经过Service处理业务逻辑和推荐算法再通过Mapper访问数据库。Common模块里的Result包装类统一返回code、msg、data结构前端页面或移动端调用比较方侼。5.2 SpringBoot配置文件的关键项源码的application.yml配置有几个关键项值得注意尤其是第一次用的时候很多人会在这里卡住。spring: datasource: url: jdbc:mysql://localhost:3306/movie_recommend?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true server: port: 8080这里重点提醒两处。第一MySQL连接串里必须带上serverTimezoneAsia/Shanghai否则本地时间的时区偏差会在插入数据时产生8小时的错乱。第二map-underscore-to-camel-case要设置为true这样数据库里user_id这种下划线字段才能自动映射到userId实体属性。另外还有一个很容易忽视的点如果使用的是MySQL 8以上版本驱动类要用com.mysql.cj.jdbc.DriverMySQL 5.x用的才是com.mysql.jdbc.Driver。毕设阶段不少人的MySQL版本和自己引入的jar版本对不上报错内容又长又吓人其实多半就是驱动类名或时区配置问题。5.3 定时任务在推荐系统中的妙用这套源码里定时任务扮演了非常重要的角色。协同过滤算法涉及的相似度矩阵计算数据一多耗时就会变得不可接受如果把计算放进用户请求链路里接口迟早会被拖垮。源码的做法是把“计算物品相似度矩阵”和“刷新用户推荐列表”拆成两个定时任务错峰执行。Component public class RecommendTask { private final RecommendService recommendService; Scheduled(cron 0 0 2 * * ?) public void refreshItemSimilarity() { recommendService.calculateAndCacheItemSimilarity(); } Scheduled(cron 0 30 2 * * ?) public void refreshUserRecommendations() { recommendService.refreshAllRecommendations(); } }定时任务跑完之后把结果写入内存缓存或数据库中。这样设计用户请求推荐接口时只是做一次缓存查询加上简单的排序过滤性能表现会好很多。在论文里“异步预计算缓存查询”也是一个可以展开讲的优化点比单纯堆配置更有说服力。6. 从源码到运行环境搭建与老司机避坑指南6.1 环境准备与启动流程拿到源码后第一步不是双击运行而是确认环境齐了再动手。这不是啰嗦我见过太多人连依赖都没下载完就启动报错了才在群里喊“源码有问题”。需要准备的工具和版本我按这套源码实际验证过的版本列出来环境推荐版本说明JDK1.8不要用17以上版本很多旧依赖会不兼容Maven3.6.3依赖管理镜像配置建议用阿里云MySQL5.7或8.0建库语句在sql目录下直接导入即可IDEIntelliJ IDEA社区版或专业版均可Node.js可选14如果使用前后端分离版前端启动流程并不复杂用IDEA打开源码项目等待Maven加载完依赖这一步可能较慢耐心点。创建一个名为movie_recommend的数据库字符集选择utf8mb4然后把sql目录下的movie_recommend.sql导入。修改application.yml里的数据库用户名和密码。直接运行Application主类看到“Started Application”字样说明启动成功。浏览器访问http://localhost:8080。6.2 几个高频报错的排查思路端口被占用SpringBoot默认端口是8080如果你的机器上已经有程序占用了8080启动会报“Port already in use”。这时候不用改代码直接换个端口就行。在启动参数里加--server.port8081或者改配置文件里的server.port字段比费劲找哪个进程占用了8080快捷得多。数据库连接失败报错内容通常是“Access denied for user”或“Communications link failure”。前者是用户名或密码不对去配置文件核对后者是MySQL服务没启动或端口不对先确认MySQL能不能在命令行连进去再排查其他原因。中文乱码这个问题在导入SQL或查询结果时比较常出现。导入SQL前先确认数据库字符集是utf8mb4配置文件里也加了characterEncodingutf8。千万不要忽略这个不然推荐列表里出现一堆“”真的很难看。6.3 推荐效果不好的排查方法我遇到过有人说“推荐效果很糟糕推的根本不是用户喜欢的类型”。排查这个问题不能只靠肉眼感觉要回到算法数据的源头找原因。先检查评分数据量。如果整个系统只有十个用户有效评分推荐结果基本就是随机的。数据量少的时候不要期望协同过滤有神奇的效果先用热门榜兜底是合理的。再检查评分分布。如果绝大多数用户只评过一两部电影相似度矩阵会非常稀疏找不到“共同评分过的用户”相似度自然趋近于零。解决办法是降低“共同评分数量”对相似度计算的影响阈值或者改用基于内容电影的类型、导演、演员的推荐作为补充。最后检查相似度计算逻辑。用一个小样本手工验证假设只有三个用户对两部电影评分算出来的相似度是否符合直觉。把中间变量打印出来逐层排查。推荐算法代码调试起来没有捷径打印中间变量是最有效的手段。提示如果你只是想应付毕设验收推荐结果准确率不一定要达到很高的水平但工程师的排查思路一定要有答辩时这会成为你的加分项。最后再分享一个我自己的实操心得如果你打算在自己的项目上做二次开发我建议第一个改造点放在“基于用户UserCF的协同过滤”上。ItemCF更看重物品的相似性UserCF更看重用户之间的相似性两者在场景上各有优势ItemCF适合用户兴趣相对稳定的场景UserCF适合新物品快速曝光或需要时效性的场景。你可以在UserCF的实现里套用ItemCF同样的矩阵逻辑实现成本并不高但能展示你对两种算法都有理解这对毕设论文的技术含量会有明显提升。另外这个项目后续还能扩展的方向其实很多。比如把电影介绍文本纳入推荐维度用简单的TF-IDF做关键词匹配或者给评分数据加入时间衰减因子让近期行为权重更高。沿着这几条线往下做系统会越来越接近真实产品的水准而不只是一个毕业设计。
返回列表