电影推荐系统毕业设计:协同过滤算法与SpringBoot工程落地实战

发布时间:2026/10/27 6:38:22
电影推荐系统毕业设计:协同过滤算法与SpringBoot工程落地实战

简介:这是一套面向高校计算机专业学生与Java初学者、用于毕业设计或课程设计场景的完整项目资料,基于Java与SpringBoot框架开发,搭配MySQL数据库,实现了一个前后台分离的电影推荐系统。前台支持用户注册登录、按动作片、喜剧片、恐怖片、爱情片、纪录片、历史片、科幻片、战争片等分类浏览影片、查看详情、评论打分与个人信息修改;后台则提供评论审核、分类管理、用户管理与公告推送等功能,覆盖了推荐类系统的典型业务闭环。压缩包共813个文件,约45.52MB,包含107个Java源码、43个Vue组件、53个CSS样式、164个JS脚本及SQL建库脚本、说明文档与演示视频等,前端页面、后端逻辑与数据库结构一应俱全。目前已有408人学习下载,适合需要完整赛题方案、可运行源码与部署参考的读者,便于快速理解项目结构、二次开发或作为答辩素材。

1. 电影推荐系统毕业设计:从协同过滤到 SpringBoot 落地的完整路径

很多计算机专业的同学拿到「基于 Java+SpringBoot 的电影推荐系统毕业设计」这个题目时,第一反应是去 GitHub 找一个现成项目改改交差。但真正答辩时被问到「你的推荐算法为什么选协同过滤而不是基于内容」「用户冷启动怎么处理」「评分矩阵稀疏度多高时效果开始崩」,就答不上来了。这个题目的核心难点从来不是 SpringBoot 的增删改查,而是推荐算法怎么在真实数据上跑出可解释的结果,以及工程上怎么把算法、数据库、接口三层串起来。它适合正在做毕业设计、需要一套能讲清楚原理又能演示效果的完整方案的同学,也适合想从纯 CRUD 项目升级到带算法模块的 Java 工程师。下面按「数据准备 → 算法实现 → 工程集成 → 踩坑排查」的顺序拆开讲。

2. 推荐算法选型:为什么协同过滤是毕业设计的最优解

2.1 三种主流推荐策略的适用边界

做电影推荐,摆在面前的无非三条路:基于内容的推荐、协同过滤、以及深度学习排序。基于内容的推荐需要为每部电影打上类型、导演、演员等标签,然后计算物品相似度推荐给用户。它的优点是解释性强,缺点是永远推不出用户没看过的新类型,容易陷入信息茧房。深度学习排序(比如用双塔模型做召回)效果上限高,但对毕业设计来说,训练数据量、GPU 资源、调参周期都是硬门槛,很容易做成一个跑不通的黑匣子。

协同过滤分两类:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。UserCF 的逻辑是「和你口味相似的人还看了什么」,适合用户数少于物品数的场景;ItemCF 的逻辑是「你喜欢这部电影,那和它相似的电影你可能也喜欢」,适合物品数相对稳定的场景。电影推荐里,用户量通常远大于电影数量(一个中等规模数据集 MovieLens-1M 有 6000 多用户、4000 多部电影),所以 ItemCF 更合适。我一般会建议毕业设计以 ItemCF 为主算法,再用基于内容的推荐做冷启动兜底,这样既有算法深度,又能覆盖答辩时老师必问的冷启动问题。

提示:不要一上来就上矩阵分解(SVD/NMF)。矩阵分解数学推导复杂,答辩时容易被追问梯度下降的每一步,而且调参不当时 RMSE 反而比 ItemCF 差。先用 ItemCF 跑通基线,再考虑加矩阵分解做对比实验。

2.2 ItemCF 的数学原理与相似度计算

ItemCF 的核心是两步:计算物品之间的相似度,然后根据用户历史评分加权预测。相似度用余弦相似度衡量,公式是:

sim(i, j) = Σ(u∈U) (r_ui · r_uj) / (√Σ r_ui² · √Σ r_uj²)

其中 r_ui 是用户 u 对电影 i 的评分,U 是同时评过 i 和 j 的用户集合。这个公式的直觉是:如果两部电影被同一批人打了高分,它们就相似。但直接算有个问题——热门电影(比如《肖申克的救赎》)和任何电影都有共同评分用户,相似度会被高估。解决办法是给热门物品降权,用改进的余弦相似度:

sim(i, j) = Σ(u∈U) (r_ui · r_uj) / (√Σ r_ui² · √Σ r_uj²) · 1 / log(1 + |N(i)|)

其中 |N(i)| 是电影 i 被评分的次数。这个 log 惩罚项能让冷门但精准的相似关系浮出来。实际写代码时,我会先把评分矩阵转成稀疏矩阵,只对共同评分用户数超过阈值(比如 3 个)的物品对计算相似度,否则噪声太大。

2.3 用 Python 离线验证算法效果

在集成到 SpringBoot 之前,先用 Python 快速验证算法能不能跑出合理结果。这一步很关键,因为 Java 写推荐算法调试成本高,Python 的 pandas 和 numpy 能让你十分钟内看到推荐列表。

import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 读取 MovieLens 格式数据:user_id, movie_id, rating, timestamp ratings = pd.read_csv('ratings.csv') # 构建用户-物品评分矩阵,缺失值填 0 pivot = ratings.pivot_table(index='user_id', columns='movie_id', values='rating').fillna(0) # 转置成物品-用户矩阵,计算物品间余弦相似度 item_matrix = pivot.T.values item_sim = cosine_similarity(item_matrix) # 热门惩罚:统计每部电影的评分次数,取对数倒数作为权重 rating_counts = (pivot > 0).sum(axis=0).values popularity_penalty = 1.0 / np.log(1 + rating_counts) # 对相似度矩阵按列乘以惩罚系数 item_sim = item_sim * popularity_penalty.reshape(-1, 1) # 预测用户 1 对未评分电影的得分 user_id = 1 user_ratings = pivot.loc[user_id].values scores = item_sim.dot(user_ratings) / (np.abs(item_sim).sum(axis=1) + 1e-8) # 过滤已评分电影,取 Top10 rated_mask = user_ratings > 0 scores[rated_mask] = -1 top_items = np.argsort(scores)[-10:][::-1] print(pivot.columns[top_items])

这段代码的逻辑是:先构建评分矩阵,再用余弦相似度算物品间关系,然后乘上热门惩罚系数,最后用加权平均预测得分。参数上,fillna(0)表示把未评分当作 0 分处理,这是协同过滤的常见做法;1e-8是防止除零的平滑项;top_items取的是得分最高的 10 部电影。跑完后你可以人工检查推荐结果是否合理——如果给喜欢《星球大战》的用户推了《教父》,说明相似度计算有问题,需要检查数据里是否有大量噪声评分。

3. SpringBoot 工程集成:从推荐结果到 REST 接口

3.1 数据库表设计与评分数据导入

推荐系统的数据库设计要围绕「用户-电影-评分」三张核心表展开。用户表存基本信息,电影表存电影元数据,评分表存用户对电影的评分记录。评分表是推荐算法的数据源,设计时要注意加联合索引 (user_id, movie_id),否则数据量上万后查询会明显变慢。

CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `email` VARCHAR(100), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `movie` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `genres` VARCHAR(100), `release_year` INT, `avg_rating` DECIMAL(3,2) DEFAULT 0.00 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `rating` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `movie_id` BIGINT NOT NULL, `score` TINYINT NOT NULL COMMENT '评分 1-5', `rate_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_user_movie` (`user_id`, `movie_id`), INDEX `idx_user` (`user_id`), INDEX `idx_movie` (`movie_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时有两个细节容易翻车:一是评分表的唯一索引uk_user_movie防止同一用户重复评分同一电影,二是score用 TINYINT 而不是 INT,因为评分范围只有 1 到 5,省空间。导入 MovieLens 数据时,用LOAD DATA INFILE比逐条 INSERT 快几十倍,但要注意 MySQL 的secure_file_priv配置,否则会报权限错误。

3.2 用 MyBatis-Plus 实现评分数据访问层

SpringBoot 集成 MyBatis-Plus 后,评分表的增删改查基本不用手写 SQL。但推荐算法需要一次性拉取全量评分矩阵,这时候用 MyBatis-Plus 的list()方法配合自定义 SQL 会更高效。

@Mapper public interface RatingMapper extends BaseMapper<Rating> { // 查询所有评分记录,用于构建相似度矩阵 @Select("SELECT user_id, movie_id, score FROM rating") List<RatingDTO> selectAllRatings(); // 查询某用户已评分的电影 ID 列表 @Select("SELECT movie_id FROM rating WHERE user_id = #{userId}") List<Long> selectRatedMovieIds(@Param("userId") Long userId); // 批量插入评分,用于数据导入 @Insert("<script>INSERT INTO rating (user_id, movie_id, score) VALUES " + "<foreach collection='list' item='item' separator=','>" + "(#{item.userId}, #{item.movieId}, #{item.score})" + "</foreach></script>") int batchInsert(@Param("list") List<Rating> list); }

selectAllRatings返回的是扁平列表,在 Service 层再组装成矩阵。batchInsert用<foreach>拼接批量插入语句,比循环单条插入快很多,但要注意单次插入不要超过 1000 条,否则 SQL 长度可能超过 MySQL 的max_allowed_packet限制。实际项目中我会把批量大小控制在 500 条一批。

3.3 推荐服务层:把算法封装成可调用的 Bean

推荐算法的 Java 实现放在 Service 层,核心思路和 Python 版本一致,但要用 Java 的数据结构重新组织。这里用Map<Long, Map<Long, Double>>存用户-物品评分,用Map<Long, Double>存物品相似度。

@Service public class RecommendationService { @Autowired private RatingMapper ratingMapper; @Autowired private MovieMapper movieMapper; // 缓存物品相似度矩阵,避免每次请求都重算 private Map<Long, Map<Long, Double>> itemSimilarityCache = new ConcurrentHashMap<>(); @PostConstruct public void initSimilarityMatrix() { List<RatingDTO> ratings = ratingMapper.selectAllRatings(); // 构建物品-用户评分映射 Map<Long, Map<Long, Double>> itemUserMap = new HashMap<>(); for (RatingDTO r : ratings) { itemUserMap.computeIfAbsent(r.getMovieId(), k -> new HashMap<>()) .put(r.getUserId(), r.getScore().doubleValue()); } // 计算物品间余弦相似度 List<Long> itemIds = new ArrayList<>(itemUserMap.keySet()); for (int i = 0; i < itemIds.size(); i++) { Long itemA = itemIds.get(i); Map<Long, Double> userA = itemUserMap.get(itemA); double normA = Math.sqrt(userA.values().stream().mapToDouble(v -> v * v).sum()); for (int j = i + 1; j < itemIds.size(); j++) { Long itemB = itemIds.get(j); Map<Long, Double> userB = itemUserMap.get(itemB); // 找共同评分用户 double dot = 0.0; for (Map.Entry<Long, Double> e : userA.entrySet()) { Double scoreB = userB.get(e.getKey()); if (scoreB != null) { dot += e.getValue() * scoreB; } } if (dot == 0) continue; double normB = Math.sqrt(userB.values().stream().mapToDouble(v -> v * v).sum()); double sim = dot / (normA * normB); // 热门惩罚 sim = sim / Math.log(1 + userB.size()); itemSimilarityCache.computeIfAbsent(itemA, k -> new HashMap<>()).put(itemB, sim); itemSimilarityCache.computeIfAbsent(itemB, k -> new HashMap<>()).put(itemA, sim); } } } public List<Movie> recommendForUser(Long userId, int topN) { List<Long> ratedIds = ratingMapper.selectRatedMovieIds(userId); Map<Long, Double> scores = new HashMap<>(); for (Long ratedId : ratedIds) { Map<Long, Double> simMap = itemSimilarityCache.getOrDefault(ratedId, Collections.emptyMap()); for (Map.Entry<Long, Double> e : simMap.entrySet()) { if (ratedIds.contains(e.getKey())) continue; scores.merge(e.getKey(), e.getValue(), Double::sum); } } return scores.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(topN) .map(e -> movieMapper.selectById(e.getKey())) .collect(Collectors.toList()); } }

@PostConstruct在 Bean 初始化后自动计算相似度矩阵并缓存,避免每次推荐请求都重算。recommendForUser遍历用户已评分的电影,累加相似电影的得分,最后排序取 TopN。这里有个性能隐患:如果物品数量上万,双重循环计算相似度会非常慢。实际部署时我会加一个阈值,只对共同评分用户数超过 3 的物品对计算相似度,并且把相似度矩阵持久化到 Redis,重启时直接加载。

4. 避坑与排查:毕业设计里最容易翻车的五个点

4.1 评分矩阵稀疏导致推荐结果为空

现象:用户只评了 2 部电影,调用推荐接口返回空列表。原因是 ItemCF 需要用户有足够的历史评分才能找到相似物品,评分太少时相似度计算覆盖不到。解决办法是加冷启动兜底:当用户评分少于 5 条时,直接返回按平均评分排序的热门电影列表,同时在前端提示「评分越多推荐越准」。

4.2 SpringBoot 版本太高导致 MyBatis-Plus 不兼容

现象:项目启动报NoSuchMethodError或ClassNotFoundException,堆栈指向 MyBatis-Plus 的内部类。原因是 SpringBoot 3.x 默认用 Jakarta EE 替代了 Java EE,而老版本 MyBatis-Plus 还在用javax.*包。解决办法是把 SpringBoot 降到 2.7.x,或者升级 MyBatis-Plus 到 3.5.3 以上版本。我一般建议毕业设计用 SpringBoot 2.7.18,稳定且资料多。

4.3 相似度矩阵计算超时导致接口 504

现象:推荐接口第一次调用要等十几秒,前端超时。原因是@PostConstruct里同步计算全量相似度,数据量大时阻塞了主线程。解决办法是把相似度计算改成异步任务,用@Async注解加线程池,启动后后台慢慢算,算完前推荐接口先返回热门列表。或者更简单——离线用 Python 算好相似度矩阵,存到数据库或 Redis,Java 只负责读取。

4.4 数据库评分数据重复插入

现象:同一用户对同一电影有多条评分记录,导致推荐结果异常。原因是批量导入时没有去重,或者用户重复提交评分。解决办法是建表时加UNIQUE KEY uk_user_movie (user_id, movie_id),插入时用INSERT IGNORE或ON DUPLICATE KEY UPDATE。如果已经产生脏数据,用DELETE t1 FROM rating t1 INNER JOIN rating t2 WHERE t1.id > t2.id AND t1.user_id = t2.user_id AND t1.movie_id = t2.movie_id清理。

4.5 前端跨域请求被拦截

现象:Vue 前端调用推荐接口报 CORS 错误,浏览器控制台显示Access-Control-Allow-Origin缺失。原因是 SpringBoot 默认不允许跨域。解决办法是在 Controller 上加@CrossOrigin注解,或者全局配置WebMvcConfigurer的addCorsMappings。如果前端打包后放进 SpringBoot 的static目录,同源访问就不会有这个问题,这也是我推荐的部署方式。

5. 进阶技巧:用评估指标证明你的推荐系统有效

5.1 离线评估的四个核心指标

答辩时老师一定会问「你怎么证明推荐效果是好的」。光说「我觉得推荐挺准的」没有说服力,要用指标说话。最常用的四个指标是:

指标含义计算方式毕业设计参考值
Precision@K前 K 个推荐中用户实际喜欢的比例命中数 / K0.15-0.30
Recall@K用户喜欢的电影中被推荐出来的比例命中数 / 用户喜欢总数0.10-0.25
Coverage推荐系统能覆盖多少比例的电影被推荐过的电影数 / 总电影数0.40-0.70
RMSE预测评分与真实评分的均方根误差√(Σ(pred-actual)²/N)0.85-1.10

Precision 和 Recall 通常此消彼长,K 值越大 Precision 越低但 Recall 越高。毕业设计里我一般取 K=10,然后画一条 Precision-Recall 曲线,展示不同 K 值下的权衡。Coverage 反映推荐多样性,如果 Coverage 低于 0.3,说明系统只推热门电影,需要检查热门惩罚系数是否生效。

5.2 用留一法做训练集和测试集划分

评估的第一步是划分数据。推荐系统不能用随机划分,因为会泄露未来信息。正确做法是留一法(Leave-One-Out):对每个用户,把他最后一条评分作为测试集,其余作为训练集。这样模拟的是「用历史行为预测未来行为」的真实场景。

# 留一法划分:每个用户最后一条评分作测试 ratings_sorted = ratings.sort_values(['user_id', 'timestamp']) test = ratings_sorted.groupby('user_id').tail(1) train = ratings_sorted.drop(test.index) # 在训练集上计算相似度,在测试集上评估 # 对每个测试样本,用训练集预测得分,计算 RMSE

这个划分方式有个坑:如果用户只有一条评分,留一法会导致训练集为空。解决办法是过滤掉评分少于 5 条的用户,或者对这类用户单独处理。实际跑 MovieLens-1M 时,过滤后大约保留 95% 的用户,足够评估用了。

5.3 我踩过的评估翻车经历

第一次做推荐系统评估时,我直接用全部数据算相似度,然后在同一批数据上评估 Precision,结果跑出 0.6 的漂亮数字。答辩时老师问了一句「你这是不是过拟合了」,我才意识到问题——用测试集算相似度再评估测试集,相当于考试前偷看了答案。后来改成严格在训练集上算相似度,Precision 掉到 0.22,但这才是真实水平。这件事让我养成了一个习惯:任何评估代码写完,先检查有没有数据泄露,确认训练集和测试集完全隔离再跑指标。希望帮到你。

本文还有配套的精品资源,点击获取