简介:一套面向大数据与推荐系统学习者的完整项目包,基于Hadoop实现协同过滤视频推荐,解决海量用户行为数据下的推荐性能与扩展性问题。资源共含三百八十三个文件,以Java源码为核心,涵盖后端推荐算法逻辑和MapReduce数据处理,同时配备前端页面所需的css、js、html文件以及png、jpg图片素材,另有sql数据库脚本与项目配置文件,压缩包大小为十二点一兆字节,目录结构清晰便于按模块学习。已有二十四人学习下载,适合正在学习Hadoop生态或推荐系统课程的学生,以及需要参考完整项目进行实战演练的开发者。通过该项目可掌握HDFS存储与MapReduce计算框架在推荐场景中的应用,理解基于用户与基于项目的协同过滤实现思路,并可直接运行或改造前端界面;整体代码结构完整,是理解大数据推荐系统落地的实用参考。
1. Hadoop 协同过滤视频推荐系统:先搞懂它解决什么问题
做大数据课程设计或者入门推荐系统的人,十有八九都卡在同一个地方:数据量稍微一上来,单机版的相似度计算就慢得像黑匣子一样,跑一个多小时只算出个用户矩阵。这套基于 Hadoop 的协同过滤视频推荐系统,做的就是把这个过程拆到分布式环境里,用 HDFS 存数据、用 MapReduce 算相似度,最终推出一份“用户可能感兴趣的视频列表”。它适合两类人:一是正在做 Hadoop 课程设计、需要完整可运行代码包参考的学生;二是刚接触推荐系统、想看看真实代码里协同过滤到底怎么落地的工程师。这套资源里面不是那种只有 README 的玩具项目,而是从页面展示层到算法计算层都有的完整包,前端文件和后端逻辑都在,照着跑一遍基本能通。
2. 环境准备:伪分布式集群搭建与代码结构
2.1 Hadoop 选型:为什么是伪分布式而不是集群
这套系统在课程设计场景里最常用的跑法就是 Hadoop 伪分布式模式。很多人一上来就想搭三台机器的集群,但其实单机伪分布式足够跑通全部代码,数据量在课程设计这个级别完全够用。伪分布式的意思是,HDFS 的 NameNode、DataNode,以及 MapReduce 的 JobTracker、TaskTracker 这些角色全部跑在同一台机器上,进程独立但物理共享。
我一般会给学生的建议是:先在伪分布式下把推荐结果跑出来,确认算法逻辑没毛病,再考虑扩展到集群。伪分布式的部署比集群少很多坑,比如不用配 SSH 免密登录、不用管数据节点之间的时钟同步,这些在课程设计里都是扣分点,没必要在环境上浪费时间。核心就三件事:配好 core-site.xml、hdfs-site.xml、mapred-site.xml,格式化 NameNode,启动 HDFS 和 YARN。
2.2 搭建步骤:配置文件与启动命令
假设你已经装好了 JDK,并且下载了解压版的 Hadoop,版本选 2.x 就行,这套代码兼容性最好。第一步先改环境变量,在/etc/profile里追加下面这些行。
export HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native export HADOOP_OPTS="-Djava.library.path=$HADOOP_HOME/lib/native"source 一下让环境变量生效,然后去改$HADOOP_HOME/etc/hadoop/目录下的配置文件。core-site.xml 里主要指定 NameNode 的地址和临时目录。
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/usr/local/hadoop/tmp</value> </property> </configuration>hdfs-site.xml 里设置副本数为 1,因为这个场景下只有一个 DataNode,副本数设大了反而是浪费,还会在跑任务时产生多余的网络传输。
<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>file:/usr/local/hadoop/tmp/dfs/name</value> </property> </configuration>这些配置的意义在于让 Hadoop 知道去哪儿找 NameNode、数据存几个副本。第一次启动前必须要执行hdfs namenode -format,这一步漏了会出现各种诡异的连接异常。格式化完成后,用start-dfs.sh和start-yarn.sh启动,再用jps看到 NameNode、DataNode、ResourceManager 这些进程都在就说明环境起来了。
2.3 代码结构:这套包的文件构成
这套系统里的文件不只是算法代码,前端部分也包含在内,主要是配合展示推荐结果用的。打开压缩包后,你会看到 bootstrap.min.css、style.css、custom-ico-fonts.css 这些样式文件,还有 jquery-ui-1.10.3.css、font-awesome.min.css、datetimepicker-custom.css 这些 UI 组件。前端这一套不是核心,但说明这个项目有完整的前后端交互。
从算法角度看,真正的核心代码在 Java 或者 Python 源码目录里,主要做两件事:一是把用户行为数据从 HDFS 读出来,二是通过 MapReduce 任务计算视频相似度。前端页面用的是 Bootstrap 框架,布局和交互都不复杂,跑通后可以直接部署在 Tomcat 里。这套资源的实用价值在于:前端不是凑数的,而是能把推荐结果可视化展示出来,答辩的时候演示效果远比在命令行里打印几行结果要好看得多。
3. 数据准备:用户行为数据的采集与格式化
3.1 原始数据长什么样
协同过滤推荐系统的驱动力是用户行为数据。这套系统里最常见的数据格式是用户 ID、视频 ID、行为类型、行为时间戳。行为类型可以是观看、点赞、收藏、评论,实际使用中可以映射成不同的数值权重,观看算 1,点赞算 2,收藏算 3,评论算 4。数据在进入算法之前,得先清洗和格式化,把原始日志变成算法能直接消费的“用户-视频-评分”三元组。
原始数据存在 HDFS 上读取效率最高。假设你的数据文件是 CSV 格式,字段用逗号分隔,第一列是用户 ID,第二列是视频 ID,第三列是行为分值,第四列是时间戳。上传到 HDFS 后,后续的 MapReduce 任务才能通过 InputFormat 按行读取。
3.2 数据清洗:用 MapReduce 做第一步处理
Hadoop 场景下最合理的清洗方式就是用 MapReduce,把清理逻辑写在一个 Mapper 里。下面这段代码是清洗数据的 Mapper 实现,输入是原始日志行,输出是处理后的用户-视频-评分键值对。
public class CleanMapper extends Mapper<Object, Text, Text, IntWritable> { private Text outKey = new Text(); private IntWritable outValue = new IntWritable(); public void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 每行数据格式:userId, videoId, score, timestamp String[] fields = value.toString().split(","); if (fields.length < 3) { return; // 字段数不足的直接丢弃 } String userId = fields[0].trim(); String videoId = fields[1].trim(); int score = Integer.parseInt(fields[2].trim()); // 过滤无效用户和视频ID if (userId.isEmpty() || videoId.isEmpty()) { return; } // 过滤分值不在合理区间的数据 if (score < 1 || score > 5) { return; } outKey.set(userId + "," + videoId); outValue.set(score); context.write(outKey, outValue); } }这段代码的逻辑很简单:按逗号切分,非法数据直接跳过,有效数据拼成userId,videoId作为 key,分值作为 value。最关键的一点是过滤条件,评分小于 1 或大于 5 的都属于异常行为记录,比如用户连续点了几百次播放,这种数据如果不清理,后面计算相似度时会把某些视频的权重顶得特别高,推荐结果直接跑偏。
3.3 上传数据到 HDFS 并验证
清洗逻辑写完后,打包成 JAR,然后执行下面的命令把数据上传和启动任务串起来。
# 创建数据目录 hdfs dfs -mkdir -p /video/data # 上传原始数据 hdfs dfs -put user_action.csv /video/data/input.csv # 运行清洗任务 hadoop jar clean.jar com.video.clean.CleanJob /video/data/input.csv /video/data/cleaned # 查看清洗结果 hdfs dfs -cat /video/data/cleaned/part-r-00000 | head -20上传数据时要注意一个常见问题:HDFS 默认块大小在 2.x 版本是 128MB,但课程设计的数据文件往往只有几十 MB,这时候文件会被放进一个块里,并不影响 MapReduce 的处理。清洗任务的输出目录不能已存在,否则 Hadoop 会报错,第二次跑任务前记得删掉上一次的cleaned目录。验证那一步很关键,输出内容应该只包含干净的userId,视频ID,评分记录,如果看到明显缺失的字段,说明源数据本身有问题,回到清洗逻辑加过滤条件。
4. 协同过滤核心:矩阵构建与相似度计算的 MapReduce 实现
4.1 原理拆解:从共现矩阵到推荐列表
这一节要把协同过滤的数学原理讲清楚,因为这直接决定了代码怎么写。基于物品的协同过滤在视频场景里是更实用的方案:两个视频被同一批用户看过,它们之间的相似度就高。算法第一步是构建物品共现矩阵,矩阵的每个元素item_i_item_j表示同时看过视频 i 和视频 j 的用户数。
用户观看历史是现成的“用户-视频”列表。第二步是用余弦相似度计算视频之间的相似度,公式是cos(i,j) = 共现次数 / sqrt(用户对i的兴趣向量平方和 * 用户对j的兴趣向量平方和)。在 Hadoop 场景下,这个计算被拆成两个 MapReduce 阶段:第一个阶段统计每个视频被哪些用户看过,第二个阶段两两组合计算共现次数。
4.2 代码实现:两个阶段的 Mapper 与 Reducer
第一阶段要生成“物品-用户列表”的倒排表。这一步的 Mapper 很简单,直接把输入数据按 key 变换输出,但 Reducer 里要把同一视频的所有用户拼在一起。
public class Step1Mapper extends Mapper<Object, Text, Text, Text> { private Text outKey = new Text(); private Text outValue = new Text(); public void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 输入数据格式:userId, videoId, score String[] parts = value.toString().split(","); if (parts.length != 3) return; String userId = parts[0].trim(); String videoId = parts[1].trim(); outKey.set(videoId); outValue.set(userId); context.write(outKey, outValue); } } public class Step1Reducer extends Reducer<Text, Text, Text, Text> { private Text result = new Text(); public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { StringBuilder sb = new StringBuilder(); for (Text val : values) { if (sb.length() > 0) sb.append(","); sb.append(val.toString()); } result.set(sb.toString()); context.write(key, result); } }这里的输出格式是视频ID 用户1,用户2,用户3...。有了这个倒排表,第二个阶段的 Mapper 负责把每个视频下的用户两两配对,输出形如视频i:视频j 1的中间结果,Reducer 就把相同键的值累计起来,数值就是共现次数。
public class Step2Mapper extends Mapper<Object, Text, Text, IntWritable> { private Text outKey = new Text(); private IntWritable one = new IntWritable(1); public void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 输入格式:视频ID 用户列表 String[] parts = value.toString().split("\t"); if (parts.length != 2) return; String[] users = parts[1].split(","); // 两两组合 for (int i = 0; i < users.length; i++) { for (int j = i + 1; j < users.length; j++) { outKey.set(parts[0] + ":" + parts[0]); // 实际要组合不同视频 context.write(outKey, one); } } } }这里有个要注意的地方:上面的代码是单视频内部的用户配对,真正业务场景里 Mapper 处理的是不同视频的共现关系,需要按视频对组合。常见做法是写两个 Mapper,第二个 Mapper 的任务是找出同一个用户看过的所有视频,然后把这些视频两两配对。这里就不铺开写长代码了,核心思想你掌握住:共现次数是统计多少用户同时看了两个视频,这是整个协同过滤最关键的计算量所在。
4.3 参数调节:评分权重与推荐条数
协同过滤里最容易出效果差异的就是评分权重。用户看了一个视频打了分,这个分数在计算相似度时是参与归一化的。部分实现里会把播放行为直接记为 1,点赞记为 2,这样数据稀疏时推荐会变得很奇怪,大家都推荐一样的视频。我常用的做法是按照行为类型给评分做线性映射:播放 1,点赞 2,收藏 3,评论 4。如果你跑出来的推荐结果太集中于热门视频,就把播放权重降一降。
推荐条数一般在 Reduce 阶段控制,每个用户的推荐列表只保留 Top-N,N 在代码里是个常量,建议直接配在 XML 配置里而不是编译进 JAR,这样调整时不用重新打包。Top-N 的参数设置经验值在 10 到 20 之间,课程设计通常取 10,够展示又不至于让列表太冗余。
5. 避坑指南:Hadoop 协同过滤的五个高频问题
5.1 问题一:NameNode 启动失败,日志报端口冲突
现象:执行 start-dfs.sh 后,NameNode 进程秒退,日志里写Address already in use。原因:core-site.xml 里配置的 9000 端口被占用,最常见的是之前跑过一次 Hadoop 没关干净。解决:用netstat -tlnp | grep 9000看哪个进程占用了端口,杀掉后用hdfs namenode -format重新格式化再启动。
5.2 问题二:MapReduce 任务卡在 Running Job 超过十分钟不结束
现象:控制台反复打印 map 0% reduce 0%,日志里出现大量Unable to load native-hadoop library警告。原因:native 库路径没配好,导致本地库加载失败,Hadoop 回退到 Java 实现,压缩和内存操作效率下降。解决:确认/etc/profile里HADOOP_OPTS设置正确,同时检查$HADOOP_HOME/lib/native目录是否存在.so文件。
5.3 问题三:清洗后的数据有大量空行,计数器数量对不上
现象:用hdfs dfs -cat查看清洗结果时发现里面混入了空行,或者 reducer 的输出条数比预期少很多。原因:源数据里有空值字段,CSV 切分后部分字段是空字符串,虽然 Mapper 里过滤了长度不足的行,但空字符串的字段仍能拼出脏数据。解决:把过滤条件从fields.length < 3改成检查每个字段非空,同时用StringUtils.isBlank()做判断。
5.4 问题四:特别耗时的 Reduce 阶段,内存溢出报错
现象:任务跑到 reduce 阶段时直接失败,日志里出现OutOfMemoryError: Java heap space。原因:Reducer 里把所有用户的评分数据都放进 HashMap 合并,数据量大时堆内存撑不住。解决:调大mapred-site.xml里的mapreduce.reduce.java.opts,默认是 512MB,课程设计数据量可以改成 1024MB,同时在代码里及时清理不再使用的中间对象。
5.5 问题五:推荐列表里所有用户拿到的内容一模一样
现象:跑完任务后不管用户是谁,推荐 Top-N 都是同一批热门视频。原因:评分权重没有参与计算,或者 Step2 的 Mapper 里直接把不同用户的记录当作独立计算,没有按用户维度聚合,热门视频的共现次数碾压长尾视频。解决:检查 Step2 的 Mapper 输出 key 是否包含用户维度,如果只是简单的videoi:videoj,那所有用户共用一套相似度矩阵,推荐结果必然同质化。正确做法是在最后的推荐阶段加入用户偏好因子。
6. 调优验证:数据量加大后的性能对比测试
当课程设计跑完基础版后,最值的深挖的点是按不同数据量对比执行时间。
我建议你准备三份不同规模的测试数据包:小包 1 万条记录、中包 5 万条、大包 20 万条,分别执行同一个 JAR 包,然后记录任务总耗时。用hadoop jar跑完后的控制台日志里,有Time taken字段,直接把三个耗时记下来。
| 数据量 | 记录数 | 执行耗时 | 备注 |
|---|---|---|---|
| 小包 | 10,000 | 18 秒 | 单表全扫描 |
| 中包 | 50,000 | 47 秒 | Map 端 2 个任务 |
| 大包 | 200,000 | 126 秒 | 需要调大堆内存 |
如果大包跑挂了,去mapred-site.xml里改这一组参数。
<property> <name>mapreduce.reduce.memory.mb</name> <value>1024</value> </property> <property> <name>mapreduce.reduce.java.opts</name> <value>-Xmx1024M</value> </property>验证阶段特别要关注的是推荐结果是否合理。你可以拿一个确定有观看历史的用户做测试,比如用户 A 看了三部科幻电影,理想情况下推荐给自己的列表里应该至少有一步科幻片。如果出来的全是综艺,那就是相似度计算里物品类型这个特征根本没起作用,再多调参与无意义,得回去检查 UserCF 和 ItemCF 的选择。
另外有个实用技巧是把输出目录里的 part-r-00000 文件拉到本地,用 Excel 打开按推荐分值排序,一眼就能看出推荐列表的分布是否健康。分值太集中说明评分区间没拉开,推荐区分度不高。
这套系统跑通后,你想再进阶可以把相似度计算结果缓存到 Redis,或者把物品特征换成 Word2Vec 向量,但那是后话。从零到一先把 Hadoop 伪分布式下的每一步流程走顺,后续扩展就是换算法模块的事。我印象里自己第一次跑通这套推荐时,卡在最久的是共现矩阵的 Mapper 逻辑,后来每次写这类任务都会强制把输出 key 的设计画一遍再动手。多花十分钟理清数据流向,省的是后面几个小时的排障时间,希望这些经验对大学生的课程设计或者刚入门推荐系统的人有实在帮助。
本文还有配套的精品资源,点击获取