ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Hadoop的电影推荐系统:从离线数据到可解释推荐的工程落地

基于Hadoop的电影推荐系统:从离线数据到可解释推荐的工程落地 简介这是一套基于Hadoop实现的电影推荐系统完整项目面向计算机相关专业的毕业设计、期末大作业与课程设计人群也适合想入门大数据推荐算法的开发者。项目含源代码与SQL脚本代码附有注释新手也能看懂部署后即可运行可帮助读者快速搭建一套可演示、可答辩的推荐系统方案。资源包共801个文件约16.27MB以340个js与151个css构成前端页面与交互60个py与47个pyc承载推荐算法与业务逻辑另有9个sql负责数据表与数据初始化配合html、svg、png等静态资源与少量jar、md、pdf说明文件结构完整、层次清晰。目前已有253人学习下载。项目围绕用户、电影与评分数据展开涵盖数据存储、相似度计算与推荐结果展示等环节读者可据此理解Hadoop生态下推荐系统的整体流程并在此基础上做功能扩展或二次开发是兼顾实用性与参考价值的实战型资料。1. 基于 Hadoop 的电影推荐系统从离线数据到可解释推荐的工程落地很多做课程设计或毕业设计的同学一看到「基于 Hadoop 的电影推荐系统」就默认它是个玩具——把 MovieLens 数据往 HDFS 一丢跑个协同过滤输出几个评分预测完事。但真正做过的人知道这套系统里最值钱的部分不是算法本身而是「数据怎么进、特征怎么算、推荐结果怎么存、SQL 怎么查」这条完整链路。它解决的核心问题是当评分数据量从几万条涨到千万条单机内存放不下、Python 脚本跑一次要几个小时的时候怎么用 Hadoop 生态把推荐流程拆成可调度、可复现、可查询的离线任务。适合谁适合正在做 Hadoop 课程设计、需要一套能写进简历的完整项目、或者想理解「推荐系统在分布式环境下到底怎么落地」的工程师。下面我按自己搭过的一套方案把选型、代码、SQL 和踩坑点讲清楚。2. 为什么选 Hadoop 而不是单机 pandas数据规模与任务拆解2.1 电影推荐系统的数据流与 Hadoop 的切入点先把这个系统的数据流说清楚。典型输入是用户-电影-评分三元组常见来源是 MovieLens 的 ratings.csv 和 movies.csv。单机方案用 pandas 读进来做皮尔逊相似度或矩阵分解几百 MB 数据还能撑住。但一旦评分条数到千万级pandas 的read_csv就会吃满内存相似度矩阵是 O(n²) 的直接爆掉。Hadoop 的切入点不是替代算法而是把「数据清洗、用户-物品共现统计、相似度分块计算、推荐结果落库」这几步拆成 MapReduce 或 Spark 任务让每步都能水平扩展。我一般会把整个流程拆成四层原始层HDFS 存 csv、清洗层去重、去空、时间过滤、计算层共现矩阵、相似度、TopN 推荐、服务层MySQL 存结果SQL 查询。Hadoop 负责前三层MySQL 负责最后一层。这样做的理由是HDFS 适合一次写入多次读取的大文件MapReduce/Spark 适合做全量扫描和聚合而 MySQL 适合做点查和范围查。把推荐结果从 HDFS 导出到 MySQL前端或接口层就能用标准 SQL 查「给用户 123 推荐的前 20 部电影」。2.2 伪分布式还是全分布式课程设计的现实选择热搜里「hadoop 伪分布式搭建」和「hadoop 集群搭建」都有人搜说明很多人卡在环境这一步。我的建议很直接如果你只是做课程设计或本地开发用伪分布式就够了一台机器跑 NameNode、DataNode、ResourceManager、NodeManager配置简单调试方便。全分布式适合数据量真的超过单机磁盘或需要多节点并行的时候但课程设计阶段没必要给自己加网络和权限的麻烦。伪分布式的最小配置我一般这么改# core-site.xml configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configuration # hdfs-site.xml configuration property namedfs.replication/name value1/value /property /configuration # mapred-site.xml configuration property namemapreduce.framework.name/name valueyarn/value /property /configuration # yarn-site.xml configuration property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property /configuration逻辑说明fs.defaultFS指向本地 9000 端口dfs.replication设为 1 是因为伪分布式只有一个 DataNode设 3 会一直报副本不足。mapreduce.framework.name设为 yarn 表示用 YARN 调度 MapReduce 任务。参数怎么改如果 9000 被占用换成 8020 或 9001如果内存小在yarn-site.xml里加yarn.nodemanager.resource.memory-mb限制容器内存。格式化 HDFS 和启动的命令hdfs namenode -format start-dfs.sh start-yarn.sh jpsjps应该看到 NameNode、DataNode、ResourceManager、NodeManager、SecondaryNameNode 五个进程。少一个就去对应日志里找原因常见的是端口冲突或 JAVA_HOME 没配。2.3 用 MapReduce 算共现矩阵Mapper 与 Reducer 的职责划分推荐系统里最核心的一步是算物品共现——同一用户看过哪些电影组合。用 MapReduce 做这件事很直观Mapper 读一行评分记录输出keyuserId, valuemovieIdReducer 拿到一个用户看过的所有电影列表两两组合输出keymovieA:movieB, value1。这样每个物品对就被计数一次。# mapper.py import sys for line in sys.stdin: line line.strip() if not line or line.startswith(userId): continue fields line.split(,) if len(fields) 3: continue user_id, movie_id, rating fields[0], fields[1], fields[2] try: if float(rating) 3.5: # 只保留正面评分 print(f{user_id}\t{movie_id}) except ValueError: continue# reducer.py import sys from itertools import combinations current_user None movies [] for line in sys.stdin: user_id, movie_id line.strip().split(\t) if current_user user_id: movies.append(movie_id) else: if current_user is not None: for a, b in combinations(sorted(set(movies)), 2): print(f{a}:{b}\t1) current_user user_id movies [movie_id] if current_user is not None: for a, b in combinations(sorted(set(movies)), 2): print(f{a}:{b}\t1)逻辑说明Mapper 过滤掉评分低于 3.5 的记录只保留用户真正喜欢的电影这样共现矩阵更有意义。Reducer 用combinations生成两两组合sorted(set(movies))去重并排序避免同一对重复输出。参数说明评分阈值 3.5 可以调MovieLens 是 0.5 到 5.0 的评分3.5 以上算正面如果数据里评分是 1 到 5 的整数改成 4 也行。跑这个任务的命令hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -files mapper.py,reducer.py \ -mapper python3 mapper.py \ -reducer python3 reducer.py \ -input /movie/ratings.csv \ -output /movie/cooccurrence注意-files会把脚本分发到各个节点-input和-output都是 HDFS 路径。输出目录不能已存在否则任务直接失败。3. 从共现矩阵到 TopN 推荐相似度计算与结果导出3.1 用 SQL 做相似度排序Hive 还是 MySQL共现矩阵算完后下一步是算物品相似度。常见做法是用余弦相似度或 Jaccard 相似度公式不复杂但要对每个物品对做归一化。这里有个选择用 Hive SQL 在 Hadoop 上算还是导出到 MySQL 用 SQL 算。我的经验是如果共现矩阵有几十万对Hive 更合适因为数据还在 HDFS 上不用来回搬如果只有几万对导出到 MySQL 用 SQL 算更快因为 MySQL 的索引和排序更成熟。用 Hive 建表并算相似度的 SQL-- 建共现表 CREATE TABLE IF NOT EXISTS cooccurrence ( pair STRING, cnt INT ) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t; -- 加载数据 LOAD DATA INPATH /movie/cooccurrence/part-* INTO TABLE cooccurrence; -- 拆分物品对并算相似度 CREATE TABLE item_similarity AS SELECT split(pair, :)[0] AS item_a, split(pair, :)[1] AS item_b, cnt, cnt / sqrt(sum(cnt) OVER (PARTITION BY split(pair, :)[0])) AS sim FROM cooccurrence;逻辑说明split(pair, :)[0]取出物品对里的第一个物品[1]取第二个。sum(cnt) OVER (PARTITION BY ...)是窗口函数算每个物品的总共现次数用来做归一化。参数说明如果相似度要更准可以换成cnt / (sqrt(cnt_a) * sqrt(cnt_b))但需要额外 join 一次物品总计数表。3.2 生成用户 TopN 推荐并写入 MySQL有了物品相似度给用户推荐就是找到用户看过的电影根据相似度找到最相似的未看电影按相似度排序取前 N。这一步可以用 Hive SQL 做也可以导出到 MySQL 用 SQL 做。我一般会在 Hive 里生成推荐结果然后导出到 MySQL 供查询。-- 用户看过的电影 CREATE TABLE user_watched AS SELECT DISTINCT user_id, movie_id FROM ratings WHERE rating 3.5; -- 生成推荐 CREATE TABLE user_recommend AS SELECT uw.user_id, s.item_b AS recommended_movie, SUM(s.sim) AS score FROM user_watched uw JOIN item_similarity s ON uw.movie_id s.item_a LEFT JOIN user_watched uw2 ON uw.user_id uw2.user_id AND s.item_b uw2.movie_id WHERE uw2.movie_id IS NULL GROUP BY uw.user_id, s.item_b ORDER BY score DESC LIMIT 20;逻辑说明LEFT JOIN user_watched uw2加WHERE uw2.movie_id IS NULL是为了排除用户已经看过的电影。SUM(s.sim)把用户看过的所有电影对推荐电影的相似度加起来作为推荐分数。LIMIT 20取前 20 条。参数说明LIMIT可以改成 10 或 50看前端需要多少rating 3.5和前面 Mapper 的阈值保持一致。导出到 MySQL 的命令sqoop export \ --connect jdbc:mysql://localhost:3306/movie_rec \ --username root \ --password yourpassword \ --table user_recommend \ --export-dir /user/hive/warehouse/user_recommend \ --input-fields-terminated-by \001注意 Hive 默认分隔符是\001Sqoop 导出时要对应上否则字段会错位。3.3 MySQL 表设计与查询 SQLMySQL 这边建两张表就够了一张存推荐结果一张存电影元数据。CREATE TABLE user_recommend ( user_id INT, recommended_movie INT, score DOUBLE, PRIMARY KEY (user_id, recommended_movie) ); CREATE TABLE movies ( movie_id INT PRIMARY KEY, title VARCHAR(255), genres VARCHAR(255) ); -- 查询某个用户的推荐结果 SELECT m.title, r.score FROM user_recommend r JOIN movies m ON r.recommended_movie m.movie_id WHERE r.user_id 123 ORDER BY r.score DESC LIMIT 20;逻辑说明user_recommend用联合主键避免重复推荐movies存电影标题和类型。查询时 join 两张表按分数降序取前 20。参数说明如果推荐结果要分页加OFFSET如果要按类型过滤在WHERE里加m.genres LIKE %Action%。4. 避坑与排查Hadoop 电影推荐系统最常见的 5 个翻车点4.1 伪分布式启动后 jps 少进程现象start-dfs.sh和start-yarn.sh都执行了但jps只看到部分进程比如没有 DataNode 或 NodeManager。原因最常见的是多次hdfs namenode -format导致 clusterID 不一致或者端口被占用。解决先看logs目录下对应进程的日志如果是 clusterID 不一致删掉dfs/data和dfs/name重新格式化如果是端口冲突改core-site.xml和hdfs-site.xml里的端口。4.2 Streaming 任务报 Python 找不到现象hadoop jar hadoop-streaming.jar提交后任务失败日志里写python3: command not found。原因各节点没有 Python3或者 PATH 不一致。解决在-mapper和-reducer里用绝对路径比如/usr/bin/python3或者用-files分发一个 shell 脚本脚本里写#!/usr/bin/env python3。更稳的做法是在提交命令里加-D mapreduce.map.env.PATH/usr/bin。4.3 Hive 加载数据后字段错位现象LOAD DATA后SELECT *看到字段串在一起或者全是 NULL。原因建表时FIELDS TERMINATED BY和实际文件分隔符不一致。解决先用head看 HDFS 上文件的实际分隔符如果是\t就写FIELDS TERMINATED BY \t如果是\001就写\001。注意 Hive 里\001要写成\001不能直接写\001。4.4 Sqoop 导出时主键冲突现象sqoop export报Duplicate entry for key PRIMARY。原因MySQL 表里已经有相同主键的记录Sqoop 默认是 insert 模式。解决加--update-key user_id,recommended_movie --update-mode allowinsert让 Sqoop 做 upsert或者导出前先TRUNCATE TABLE。我一般会在 Sqoop 命令前加一个清表步骤避免脏数据。4.5 推荐结果全是热门电影现象给每个用户推荐的电影都差不多全是评分人数最多的那几部。原因共现矩阵没有做热门惩罚热门电影和任何电影都容易共现相似度被高估。解决在相似度公式里除以物品流行度的对数比如sim cnt / log(1 popularity)或者在推荐分数里加一个惩罚项。这个坑很隐蔽不特意查的话推荐结果看起来「能用」但实际没有个性化。5. 进阶技巧用 ItemCF 的加权相似度提升推荐多样性前面用的是简单共现计数实际做的时候我会加两个改进一是用评分加权二是做热门惩罚。评分加权的意思是用户给 5 分和给 3 分对共现的贡献不一样5 分应该权重更高。热门惩罚的意思是热门电影和冷门电影共现一次比两部冷门电影共现一次更「不值钱」。改进后的相似度公式可以写成-- 加权共现 CREATE TABLE weighted_cooccurrence AS SELECT a.movie_id AS item_a, b.movie_id AS item_b, SUM(a.rating * b.rating) AS weighted_cnt FROM ratings a JOIN ratings b ON a.user_id b.user_id AND a.movie_id b.movie_id WHERE a.rating 3.5 AND b.rating 3.5 GROUP BY a.movie_id, b.movie_id; -- 热门惩罚后的相似度 CREATE TABLE item_similarity_v2 AS SELECT w.item_a, w.item_b, w.weighted_cnt / (LOG(1 pa.pop) * LOG(1 pb.pop)) AS sim FROM weighted_cooccurrence w JOIN (SELECT movie_id, COUNT(*) AS pop FROM ratings GROUP BY movie_id) pa ON w.item_a pa.movie_id JOIN (SELECT movie_id, COUNT(*) AS pop FROM ratings GROUP BY movie_id) pb ON w.item_b pb.movie_id;逻辑说明a.rating * b.rating让高分组合权重更大a.movie_id b.movie_id避免重复对。LOG(1 pop)做热门惩罚流行度越高分母越大相似度越低。参数说明LOG的底数用自然对数或 10 都行影响的是绝对数值不影响排序如果觉得惩罚太狠可以把LOG换成SQRT。验证推荐效果的方法留出最近 20% 的评分做测试集看推荐结果里有多少落在测试集里算命中率。如果命中率低于随机推荐说明相似度算错了或者数据有问题。我一般会先跑一个小数据集比如 10 万条评分确认流程通了再上全量。最后说个血泪经验Hadoop 这套东西环境配置和日志排查占的时间比写算法多得多。别一上来就追求全分布式和高大上算法先把伪分布式跑通用 Streaming 写最简单的 Mapper 和 Reducer确认数据能进能出再逐步加相似度加权和热门惩罚。SQL 那边也是先把推荐结果导进 MySQL用一条 join 查出来再考虑分页和过滤。希望帮到你。本文还有配套的精品资源点击获取
返回列表