资讯动态

基于Spark的混合推荐系统实战:从协同过滤到实时推荐

发布时间:2026/10/22 12:21:37 来源:尧图企业网站定制
简介这是一份基于Spark的电影推荐系统设计与实现的完整课题资料面向大数据、推荐系统方向的毕业设计与课程设计人员也可供对协同过滤、内容推荐算法感兴趣的开发者参考。文档以论文形式系统梳理了课题背景、国内外研究现状、Spark与MongoDB等关键技术并重点介绍了基于人口统计学、基于内容、基于协同过滤三类推荐算法的原理与设计思路。系统分析部分涵盖可行性与功能需求设计部分细化各推荐算法流程实现部分展示用户登录注册、个性化推荐、电影搜索、用户评分等核心功能测试部分给出功能与性能验证结果目录结构从绪论到致谢完整清晰可直接作为相关课题的参考蓝本。资源包内仅含单个DOCX文档大小约7.46MB内容以论文正文为主共一个文件便于查阅和编辑。目前已有207人学习下载适合需要快速掌握电影推荐系统整体架构与研究思路的学生或开发者。1. 这个项目不只是论文一套能从登录跑到推荐的完整系统电影推荐系统这类课题网上源码不少但多数是单机跑个 Python 脚本调一下 surprise 库就完事谈不上“系统”。这份资源不一样——它是一份带完整论文的工程实现后端用 Spark 做分布式计算MongoDB 存业务数据Redis 做缓存Elasticsearch 参与搜索前端还有 AngularJS 页面。换句话说它不是 toy demo而是一条能沿着“用户登录 → 评分 → 离线统计 → 实时推荐 → 详情页相似推荐”跑通的完整链路。我拆这份项目时最大的感受是它的核心不在协同过滤那几行 ALS 代码而在“三种推荐算法怎么分工”。基于人口统计学的离线推荐管冷启动和热门榜基于内容的实时推荐管用户当下行为协同过滤管个性化。单一算法都有各自的坑混合起来才是个能用的推荐系统。这套设计思路比代码本身更值得读。适合读这份资源的人有三类做大数据课程设计或毕业设计的学生想把 Spark 推荐从“会调 API”推进到“能搭完整服务”的后端开发以及想找一个可复现的推荐系统基线工程、准备加功能的算法工程师。下文我会把技术选型、模块结构、核心实现和运行步骤一层层拆开最后给出我的改造建议。2. 整体架构拆解Spark 计算、MongoDB 存储、三套算法分工2.1 技术栈全景与数据流向这套系统的技术选型围绕一个核心约束单机环境下模拟大数据处理流程同时保证每一层都有真实组件在跑。先看全景计算引擎Spark负责离线批量统计和实时流处理存储层MongoDB 存用户、评分、电影和推荐结果Redis 缓存热点数据Elasticsearch 做电影搜索后端Spring Maven提供登录、评分、推荐结果的 REST 接口前端AngularJS 2负责页面渲染和交互集群形态伪分布式1 个 Master 3 个 Worker 用虚拟机模拟数据流向是这样的用户在前端页面产生评分、点击、搜索行为这些行为写入 MongoDB 的业务表离线推荐任务用 Spark SQL 周期性从 MongoDB 读取评分表跑统计和模型训练把结果写回 MongoDB 的推荐结果表实时推荐则通过 Spark Streaming 消费行为流结合用户最近的评分记录做基于内容的相似推荐前端通过 Spring 后端接口读取 MongoDB 里的推荐结果展示给用户。这里有一个值得注意的设计决定MongoDB 同时承担“业务数据库”和“推荐结果数据库”两个角色。好的一面是技术栈干净不用在 HBase 和 MySQL 之间做数据同步坏的一面是推荐结果表要按用户维度设计好索引否则前端高频读取时会有性能压力。实际工程里我会把推荐结果再拷一层到 Redis这个后面改造章节细说。2.2 离线推荐模块基于人口统计学的四类统计任务离线推荐是这套系统的“底座”它解决的核心问题是冷启动新用户没有任何行为记录时推荐什么答案是给所有人推一样的榜单——历史热门、近期热门、高分电影、各类型 Top10。四个统计任务分别这样设计历史热门影片的统计直接用 Spark SQL 从 MongoDB 的 ratings 表读取评分记录按电影 ID 分组统计评分人数取数量最多的前 N 部。这个逻辑简单粗暴但“评分人数多 热门”这个假设在大多数场景下成立。近期热门影片的统计在历史热门的基础上加了时间窗口。实现方式是定义一个 UDF把评分时间戳转换成“是否在近一个月内”的布尔标记然后只统计窗口内的评分。这个 UDF 是理解 Spark SQL 自定义函数的好例子。电影平均评分的统计按电影 ID 聚合所有评分计算平均值写入 AverageMovies 数据集。这个结果会用在详情页展示电影得分也会作为离线推荐的一个排序因子。各级别影片 Top10 的统计这是最复杂的一个。电影的 genres 字段是“动作|冒险|科幻”这种按竖线分隔的字符串需要先用 UDF 按|切分成数组再用explode展开成多行最后按每个类别分别计算评分 Top10。核心代码见下一章。2.3 实时推荐与详情页推荐基于内容的相似匹配实时推荐模块用的是 Spark Streaming它接收用户的行为流比如刚给某部电影打了高分然后做两件事一是把行为写入 MongoDB 作为后续离线训练的输入二是立刻基于这部电影的内容标签找相似电影推给用户。这里的“相似”不是协同过滤算出来的用户相似而是电影本身属性类型、导演、主演的相似度用的是基于内容的推荐算法。详情页推荐是实时推荐的“懒加载版本”用户点进某部电影详情页时后端才去计算这部电影的相似电影并返回。好处是不需要预先为每个用户算好结果存储开销小坏处是首次访问详情页会有计算延迟。在 Spark 环境下这个计算可以做成一次性的批处理任务把每部电影的相似 Top10 预先算好存起来而不是实时算。2.4 协同过滤模块ALS 模型的定位论文里提到协同过滤算法也提到 Spark MLlib 里的 ALS。在这套系统里协同过滤不是唯一的推荐来源而是作为离线个性化推荐的补充。它的任务是从历史的“用户-电影-评分”矩阵中学习两个隐因子矩阵——用户特征矩阵和电影特征矩阵然后用矩阵乘法预测用户对未看过电影的评分。但 ALS 有两个天然痛点论文里也明确说了数据稀疏和冷启动。冷启动已经靠人口统计学推荐兜底了数据稀疏则依赖我们输入 ALS 的数据质量。常见做法是把评分人数少于阈值的电影过滤掉再把评分矩阵做归一化让模型更容易收敛。这块我在改造章节会给出具体参数建议。3. 核心实现逐段拆解从 MongoDB 读取到推荐结果落地3.1 Spark SQL 读取 MongoDB连接器与 DataFrame离线推荐的第一步是把 MongoDB 中的数据加载成 Spark DataFrame。项目里没有用自定义读取器而是直接用了官方 MongoDB Spark Connector。加载评分表和电影表的写法如下import org.apache.spark.sql.SparkSession val spark SparkSession.builder() .appName(OfflineRecommender) .master(local[*]) // 伪分布式环境下这里也可以是 spark://hadoop102:7077 .config(spark.mongodb.input.uri, mongodb://hadoop102:27017/movie.ratings) .config(spark.mongodb.output.uri, mongodb://hadoop102:27017/movie.result) .getOrCreate() val ratingsDF spark.read.format(mongo) .option(uri, mongodb://hadoop102:27017/movie.ratings) .load() .select(uid, mid, score, timestamp) ratingsDF.show(5)注意select里只取了四个字段uid用户 ID、mid电影 ID、score评分、timestamp评分时间戳。之所以要显式限定字段是因为 MongoDB 的文档结构往往比关系表松散early 筛选能显著减少 Spark 在 shuffle 阶段的数据量。伪分布式环境下资源本来就紧张这个习惯能帮你少踩不少 OOM 的坑。3.2 UDF 实现电影类别切分与近期热门统计基于人口统计学的推荐里两个核心统计逻辑都需要自定义 UDF。第一个是把电影的 genres 字符串切分成数组第二个是判断评分时间是否落在近一个月窗口内。这段代码集中展示 UDF 的注册和用法import org.apache.spark.sql.functions._ import org.apache.spark.sql.expressions.UserDefinedFunction // 按 | 切分电影类别如 Action|Adventure|Sci-Fi - [Action, Adventure, Sci-Fi] val splitGenres: UserDefinedFunction udf((genres: String) { if (genres null || genres.trim.isEmpty) Array.empty[String] else genres.split(\\|).map(_.trim) }) // 判断评分时间是否在近30天内 val isRecent: UserDefinedFunction udf((ts: Long) { val thirtyDaysAgo System.currentTimeMillis() - 30L * 24 * 3600 * 1000 ts thirtyDaysAgo }) // 各类型电影 Top10 统计 val genreTopMovies ratingsDF .join(moviesDF, ratingsDF(mid) moviesDF(mid)) .withColumn(genre, explode(splitGenres(moviesDF(genres)))) .groupBy(genre, mid) .agg(avg(score).as(avg_score), count(mid).as(score_count)) .withColumn(rank, row_number().over(Window.partitionBy(genre).orderBy(desc(avg_score), desc(score_count)))) .filter(col(rank) 10) .drop(rank)genreTopMovies这段逻辑的关键点在explode它把“一部电影属于三个类别”这一行数据展开成三行每个类别一行这样后续 groupBy 才能按类别独立排序。窗口函数row_number() over partitionBy负责在每个类别内部取评分最高的 10 部。如果你的数据里有一些只有一两人评分的电影建议在agg之后加一个过滤条件只保留score_count 10的影片否则“平均分第一名”容易被单条高分评价刷上去。近期热门统计更简单对 ratings 表用 UDF 过滤近一个月的评分然后按 mid 聚合计数取 Top N 写入 RateMoreRecentlyMovies 集合。3.3 ALS 协同过滤训练与个性化推荐ALS 是 Spark MLlib 里最成熟的协同过滤实现。代码不长但调参直接影响推荐质量import org.apache.spark.ml.recommendation.ALS import org.apache.spark.ml.evaluation.RegressionEvaluator // 划分训练集和测试集测试集用于评估模型误差 val Array(training, test) ratingsDF.randomSplit(Array(0.8, 0.2)) val als new ALS() .setRank(20) // 隐因子数量越大表达能力越强但过拟合风险也越高 .setMaxIter(10) // 迭代次数日志里会显示每次迭代的 loss .setRegParam(0.1) // 正则化参数控制过拟合 .setUserCol(uid) .setItemCol(mid) .setRatingCol(score) .setColdStartStrategy(drop) // 预测时遇到未见过的用户/电影直接丢弃而非报错 val model als.fit(training) // 为每个用户生成 Top20 推荐 val userRecs model.recommendForAllUsers(20) userRecs.write.mode(overwrite).format(mongo) .option(uri, mongodb://hadoop102:27017/movie.UserRecs) .save()setColdStartStrategy(drop)是必须强调的一个配置。ALS 模型在预测时如果遇到训练集里没出现过的用户或电影默认会抛异常。线上系统里每天都有新用户新电影所以必须设置成drop或nan丢弃无法预测的条目而不是让整个批次失败。rank 参数常见取值范围是 10 到 50regParam 是 0.01 到 0.1调参时优先观察测试集上的 RMSE均方根误差。3.4 基于内容的相似度计算标签向量与余弦相似度基于内容的推荐核心是把电影转成向量然后算向量相似度。这里用的是最简单的标签映射法把电影的 genres、主演、导演拼接成文本分词后映射成 TF-IDF 向量再算两两之间的余弦相似度。import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer} import org.apache.spark.ml.linalg.SparseVector // 构造电影内容描述列 val movieContent moviesDF .withColumn(content, concat_ws( , col(genres), col(actors), col(directors))) // 分词 TF-IDF得到每部电影的特征向量 val tokenizer new Tokenizer().setInputCol(content).setOutputCol(words) val hashingTF new HashingTF().setInputCol(words).setOutputCol(rawFeatures).setNumFeatures(1000) val idf new IDF().setInputCol(rawFeatures).setOutputCol(features) val pipeline new Pipeline().setStages(Array(tokenizer, hashingTF, idf)) val featureModel pipeline.fit(movieContent) val movieFeatures featureModel.transform(movieContent) movieFeatures.select(mid, features).show(5)这里使用了HashingTF而不是CountVectorizer主要考虑是 HashingTF 不需要预先构建词典适合跑批任务缺点是哈希碰撞会引入噪声setNumFeatures设得越大碰撞概率越低。算相似度时对每个电影向量和其余所有向量做点积并归一化就能得到余弦相似度。如果电影数量很大更聪明的做法是先用 ALS 电影因子向量代替 TF-IDF 向量因为 ALS 训练出的隐因子已经隐含了“同类型电影向量接近”的特性。3.5 后端接口与前端调用的衔接推荐结果算完后展示层还需要后端接口。Spring 后端提供了四类核心接口登录注册、获取首页推荐、详情页相似推荐、提交评分。前端 AngularJS 通过 HTTP 调用这些接口渲染推荐列表RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private MongoTemplate mongoTemplate; // 首页推荐读取离线推荐表按用户返回混合结果 GetMapping(/home/{uid}) public ListRecommendation home(PathVariable String uid) { Query query new Query(Criteria.where(uid).is(uid)); ListUserRecs userRecs mongoTemplate.find(query, UserRecs.class, UserRecs); return userRecs.stream() .flatMap(rec - rec.getRecs().stream()) .sorted((a, b) - b.getScore().compareTo(a.getScore())) .limit(20) .collect(Collectors.toList()); } // 提交评分写入 MongoDB同时作为实时推荐的触发信号 PostMapping(/rate) public Result rate(RequestBody RatingRequest request) { mongoTemplate.save(request, ratings); return Result.success(); } }前端接到recommendations数组后按分数从高到低渲染成卡片列表。这个接口的写法比较直白但它把分层关系讲清楚了前端只认 userId不关心推荐结果的来源是离线统计还是协同过滤后端通过对UserRecs聚合实现多路推荐融合。如果你想加一路“热门兜底”在接口里把GenresTopMovies也读出来混进去就行。4. 环境搭建与程序运行伪分布式 Spark 集群怎么跑起来4.1 虚拟机模拟四节点 Spark 集群论文的实际运行环境是单机伪分布式一台物理机上用 VMware 开三台 CentOS 虚拟机搭一个四节点 Spark 集群1 Master 3 Worker三台机器的主机名分别设计为 hadoop102、hadoop103、hadoop104。之所以从 hadoop102 开始编号是为了给 Namenode 留出 101 的位置如果是生产环境你还会加一个 105 做备用 Master。搭建步骤如下在三台虚拟机上分别安装 JDK 并配置JAVA_HOME配置 SSH 免密登录三台机器两两互信修改/etc/hosts把三个主机名映射到固定 IP在 Spark 的conf/slaves文件里填写三个 Worker 主机名修改spark-env.sh指定SPARK_MASTER_HOSThadoop102、SPARK_MASTER_PORT7077完成后在 Master 节点执行start-all.sh浏览器访问hadoop102:8080Worker 列表里能看到三个节点说明集群搭好了。4.2 MongoDB 集合设计与启动MongoDB 在 Windows 本机运行负责存业务表和推荐结果表。数据库里设计了两类集合一类是原始数据——User用户、Ratings评分、Movies电影另一类是推荐结果——AverageMovies、GenresTopMovies、RateMoreMovies、RateMoreRecentlyMovies、UserRecs。启动方式最简单命令行直接跑mongod --dbpath /data/db --port 27017 --bind_ip 0.0.0.0要注意的是--bind_ip 0.0.0.0否则虚拟机里的 Spark 应用连不上 Windows 宿主机上的 MongoDB。这是伪分布式环境里的常见坑后面避坑章节还会细说。业务表创建之后用mongoimport把 ratings.dat 和 movies.dat 导入即可。4.3 Redis 与 Elasticsearch 配置Redis 在 Linux 虚拟机里运行主要给实时推荐模块做缓存缓存用户最近评分记录和热门推荐结果。配置项只需要改两处bind 0.0.0.0允许远程访问maxmemory设一个合理上限如 2GB避免缓存无限增长把内存吃光。Elasticsearch 的作用是搜索。电影搜索功能没有走 MongoDB 的模糊查询而是把电影标题、类别、主演信息同步到 ES用 ES 的倒排索引做全文检索。配置上需要注意 ES 的network.host要改成虚拟机能访问的 IPdiscovery.type设为single-node否则单机模式会因组播发现失败而启动异常。4.4 系统启动顺序与验证整个系统启动依赖顺序很关键错了会出现“Spark 任务提交成功但读写数据失败”的怪问题# 1. 启动 MongoDBWindows 宿主机 mongod --dbpath /data/db --port 27017 --bind_ip 0.0.0.0 # 2. 启动 Redishadoop103 redis-server /etc/redis/redis.conf # 3. 启动 Elasticsearchhadoop104 elasticsearch -d # 4. 启动 Spark 集群hadoop102 Master start-all.sh # 5. 启动后端 Spring 服务WindowsIDEA 里直接 Run mvn spring-boot:run # 6. 前端 AngularJS 通过 Nginx 或开发服务器访问 localhost:8088启动完成后验证路径浏览器访问localhost:8088能看到登录页登录后首页能看到“热门推荐”“近期热门”“高分电影”三个榜单点进任何一部电影详情页能看到相似电影给任意电影打 5 分后隔几秒刷新首页实时推荐区应该出现与刚评分电影类型相近的影片。这四条验证过了系统链路就算通了。4.5 推荐任务的手动触发离线推荐任务如果用spark-submit提交可以做成 jar 单独跑spark-submit \ --class com.movie.recommender.OfflineRecommender \ --master spark://hadoop102:7077 \ --executor-memory 1G \ movie-recommender-1.0.jar--executor-memory在伪分布式环境下不要超过 2G。三台虚拟机共享一台物理机的内存给多了反而会因为物理内存不足引起整机 swapSpark 任务会越跑越慢最后表现为 Web UI 上任务卡在 SCHEDULED 状态不动。5. 常见问题避坑伪分布式环境下的血泪经验5.1 Spark 任务报错找不到 MongoDB 驱动类现象提交离线推荐任务后任务在读取 MongoDB 阶段抛出ClassNotFoundException: com.mongodb.MongoClient或类似驱动类缺失错误。原因Spark 集群的 Executor 节点没有加载 MongoDB Connector 的依赖 jar。本地 IDEA 运行时依赖在 classpath 里没问题但spark-submit提交到集群后Executor 的 classpath 是独立的。解决打包时把依赖打进 fat jar或者提交任务时用--packages org.mongodb.spark:mongo-spark-connector_2.11指定依赖。从那以后我每次提交 Spark 任务都强制检查一遍--packages列表和代码里 import 的包一一对应。5.2 Executor 频繁 OOM任务卡死在 SCHEDULED 状态现象离线统计任务在跑大表聚合时报 Executor OOM日志里有java.lang.OutOfMemoryError, Web UI 里看到多个 Executor 处于 FAILED 状态。原因伪分布式集群的内存总量有限三台 Worker 共享物理内存。默认配置下 Spark 会为每个 Executor 分配物理内存的很大比例多个 Executor 叠加直接吃满内存。另外读取 MongoDB 时如果没有提前 select 字段DataFrame 会带着大量冗余字段参加 shuffle内存压力更大。解决第一spark-env.sh里把SPARK_WORKER_MEMORY降到 2G提交任务时--executor-memory 1G第二所有 Spark SQL 读取后的第一步就是select精简字段第三对特别大的 ratings 表先按 uid 做一次repartition再聚合。这条优化在真实集群同样适用只是真实集群阈值可以放宽。5.3 UDF 在集群模式下抛序列化异常现象本地跑没问题spark-submit提交到集群后报Task not serializable或org.apache.spark.SparkException: Task not serializable错误栈指向代码里定义的 UDF。原因如果 UDF 内部引用了外部对象比如配置类实例、SparkSession 以外的全局变量这个外部对象必须可序列化否则闭包无法在 Executor 间传输。本地模式下 Driver 和 Executor 在同一 JVM问题不会暴露。解决UDF 内部不要引用外部非序列化对象把所有依赖封装成Serializable案例类或者在 UDF 内部直接创建所需对象。我的习惯是任何 UDF 先问自己“如果它在另一台机器上执行需要带什么东西”只带字符串和基本类型就是最安全的。5.4 Elasticsearch 启动后外部无法访问现象ES 在本机启动正常curl localhost:9200有响应但在另一台机器上访问hadoop104:9200超时或拒绝连接。原因Elasticsearch 默认只绑定127.0.0.1虚拟机上的 Spark 应用当然访问不到。解决修改elasticsearch.yml设置network.host: 0.0.0.0同时设置discovery.type: single-node否则单节点会因为 bootstrap checks 拒绝启动。改完重启 ES 后用curl http://hadoop104:9200验证。5.5 前端页面能打开但登录后一直跳回登录页现象localhost:8088 的页面加载正常输入用户名密码后接口也能返回成功但页面刷新后又回到登录页。原因AngularJS 前端把登录状态存到了浏览器 localStorage但后端接口跨域或 Cookie 配置了Secure属性导致 Cookie 没写进去。Spring 后端返回登录态时如果没有正确配置跨域头浏览器会拦截写入。解决后端在 WebMvcConfigurer 里配置 CorsRegistry允许前端域名跨域请求带上 Cookie同时确认登录接口的响应头包含Access-Control-Allow-Credentials: true。如果用了 Nginx 代理前端还要检查代理配置是否把 Cookie 头透传了。6. 二次开发改造思路把“能跑的推荐”变成“好用的推荐”读完源码、跑通链路之后真正的价值在于改造成符合自己场景的系统。我会从四个层面动刀按投入产出比排序。第一层推荐结果的加权融合。当前首页接口直接合并 UserRecs 排序本质上还是单一算法输出轮流展示。更稳的做法是给每路推荐打分ALS 结果乘 0.5基于内容的结果乘 0.3热门榜乘 0.2做加权取 TopN。这几个权重可以先用固定值跑一周看各路的点击转化率再调。代码改动只在 RecommendController 里加一个融合函数不动底层。第二层给 ALS 加一个数据预处理。当前 ratings 表是原始评分直接进模型数据稀疏时会训练出大量零向量。建议训练前过滤掉评分人数少于 20 的电影对 score 做标准化到 0-1 区间。能明显提升收敛速度和推荐精度。第三层实时推荐的触发源扩展。当前实时推荐只响应“评分”事件。改造方向是把“点击详情页”“搜索电影”也作为行为流输入对点击行为降权即可。改动工作量集中在行为日志接入和权重映射。第四层把热点数据推进 Redis。详情页相似推荐和首页热门榜都可以做成定时任务预热进 Redis前端接口只读缓存降低 MongoDB 读压力。改造点是在离线任务结束后加一步写 Redis 的操作。验证改造效果的方法不需要上线 A/B准备两组用户一组走老逻辑一组走新逻辑各推 50 部电影比较两组用户的点击率和评分数量。这个比较可以在 MongoDB 里直接用聚合查询完成不用额外搭数据平台。这套工程我前前后后拆过两次每次都被同一个问题卡住伪分布式环境下只要集群一重启MongoDB 里的推荐结果表和 Redis 缓存就不同步。从那以后我每次改完离线任务都强制走一遍“清空结果表 → 重跑离线批 → 刷新 Redis 预热”的流程确认三份数据一致才交给前端验收。做推荐系统数据一致性和算法效果一样影响体验——希望这篇拆解能帮你少走我来回折腾的那几步路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑