多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Hadoop图书推荐系统课程设计:从HDFS到MapReduce的ItemCF实战

Hadoop图书推荐系统课程设计:从HDFS到MapReduce的ItemCF实战 简介这份资源是山东大学大数据课程设计的完整项目包围绕基于Hadoop实现的图书推荐系统展开适合大数据专业学生、准备课程设计或毕业设计的学习者参考。项目采用Apriori算法完成频繁项集挖掘与图书推荐配套源代码、实验报告与数据库脚本代码含注释新手也能理解整体流程。压缩包共78个文件约20.11MB以50个class编译文件、17个java源文件为主另含xml配置、properties参数文件、sql建表脚本、md说明与doc实验报告覆盖从数据存储到推荐计算的主要环节。目前已有507人学习下载。读者可据此获得一套可直接部署运行的高分项目方案结合实验报告理解算法原理与系统结构并参考目录组织方式完成自己的课程设计或期末大作业。1. 从课程设计到能跑通的图书推荐Hadoop 这套组合拳到底解决什么问题图书推荐系统听起来像是个“老掉牙”的题目但把它放到 Hadoop 上做课程设计很多同学第一反应是数据量又不大为什么非得用 Hadoop我当初也这么想过直到把某高校图书馆公开的 30 万条借阅记录、8 万条图书元数据和 2 万条用户画像丢进单机 MySQL 跑协同过滤光是一个用户-物品相似度矩阵就把内存撑爆了。Hadoop 在这里的价值不是“为了用而用”而是把推荐算法里最吃资源的几步——物品共现统计、相似度计算、TopN 推荐生成——拆成可水平扩展的 MapReduce 作业让课程设计从“跑个 demo”变成“能处理真实规模数据”的工程练习。这篇笔记面向正在做 Hadoop 课程设计、或者想拿图书推荐系统练手大数据全链路的同学。我会按“数据怎么进 HDFS → 离线统计怎么算 → 推荐结果怎么落库 → 怎么验证效果”的顺序把每个环节的代码、参数和踩过的坑讲清楚。你不需要先成为 Hadoop 专家但至少要有一台能跑伪分布式或三节点集群的机器以及 Java、MySQL 的基本操作能力。下面所有内容都围绕一个目标让你在课程设计答辩时能指着屏幕说清楚每一层数据是怎么流动的而不是只会说“我调了个库”。2. 图书推荐系统的数据底座HDFS 目录规划与 MySQL 表结构设计2.1 为什么推荐系统的输入层要拆成三份数据图书推荐的核心输入通常有三类用户行为数据借阅、收藏、评分、图书元数据书名、作者、分类、ISBN、用户属性院系、年级、性别。很多课程设计把这三类数据一股脑塞进一个 CSV结果在 MapReduce 里做 join 时字段错位调试半天。我一般会在 HDFS 上按业务域分目录存放既方便后续多作业复用也符合数仓分层的基本习惯。一个可落地的 HDFS 目录结构如下# 在 HDFS 上创建推荐系统专用目录 hdfs dfs -mkdir -p /bookrec/raw/behavior # 原始借阅行为 hdfs dfs -mkdir -p /bookrec/raw/book # 图书元数据 hdfs dfs -mkdir -p /bookrec/raw/user # 用户属性 hdfs dfs -mkdir -p /bookrec/etl/behavior_clean # 清洗后行为 hdfs dfs -mkdir -p /bookrec/etl/book_clean # 清洗后图书 hdfs dfs -mkdir -p /bookrec/output/similarity # 物品相似度 hdfs dfs -mkdir -p /bookrec/output/recommend # 最终推荐结果这段命令的逻辑是按“原始层 → 清洗层 → 输出层”划分。raw目录只做归档不做任何修改etl目录存放经过缺失值过滤、格式统一后的数据output目录给下游作业和导出程序读取。参数上唯一需要注意的是副本数课程设计环境通常用默认的 3 副本即可如果机器少可以临时改成 2命令是hdfs dfs -setrep -w 2 /bookrec/raw/behavior但生产环境不建议低于 3。2.2 MySQL 侧的表结构推荐结果怎么存才方便查Hadoop 负责算MySQL 负责查。课程设计答辩时老师通常会让你现场演示“给某个用户推荐几本书”如果每次都要跑一遍 MapReduce等待时间会让你很尴尬。所以我会把最终推荐结果预先导出到 MySQL表结构设计如下-- 用户推荐结果表每个用户存 TopN 推荐 CREATE TABLE user_recommend ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(32) NOT NULL COMMENT 用户编号, book_id VARCHAR(32) NOT NULL COMMENT 图书编号, score DOUBLE NOT NULL COMMENT 推荐分数, rank_no INT NOT NULL COMMENT 排名从1开始, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id), KEY idx_user_rank (user_id, rank_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书相似度表用于解释推荐理由 CREATE TABLE book_similarity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, book_id_a VARCHAR(32) NOT NULL, book_id_b VARCHAR(32) NOT NULL, similarity DOUBLE NOT NULL, cooccurrence INT DEFAULT 0 COMMENT 共现次数, UNIQUE KEY uk_book_pair (book_id_a, book_id_b) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;user_recommend表用(user_id, rank_no)做联合索引是因为查询场景几乎都是“取某用户的前 N 条”这个索引能直接命中。book_similarity表存双向相似度还是单向取决于你的算法实现——如果是对称相似度如余弦存单向即可查询时用UNION或应用层处理如果是非对称的如条件概率必须存双向。我一般推荐课程设计用余弦相似度存单向省一半存储。提示MySQL 建表时字符集统一用utf8mb4避免图书标题里的特殊符号如数学公式中的希腊字母插入失败。这个坑我在三个学生的课程设计里都见过。3. 用 MapReduce 实现物品协同过滤从共现矩阵到 TopN 推荐3.1 物品协同过滤的两阶段拆解图书推荐最常用的算法是物品协同过滤ItemCF因为它比用户协同过滤更稳定——图书的数量通常远小于用户数量而且图书之间的相似度不会因为某个用户的行为突变而剧烈波动。ItemCF 的核心分两步第一步统计物品共现矩阵第二步根据共现矩阵计算相似度并生成推荐。在 Hadoop 上这两步分别对应两个 MapReduce 作业。第一个作业的输入是清洗后的借阅行为输出是book_id_a, book_id_b, cooccurrence三元组第二个作业的输入是共现矩阵和用户行为输出是user_id, book_id, score。很多课程设计把两步合并成一个作业结果 reducer 里既要算相似度又要算推荐分逻辑耦合严重调试时根本不知道错在哪一步。我强烈建议拆开虽然多跑一轮作业但每个作业的职责单一出问题容易定位。3.2 共现矩阵统计作业的完整代码与参数说明先看第一个作业的 Mapper。它的任务是对每个用户把他借阅过的图书两两组合输出book_a:book_b, 1。public class CooccurrenceMapper extends MapperLongWritable, Text, Text, IntWritable { private Text pairKey new Text(); private final static IntWritable ONE new IntWritable(1); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 输入格式user_id \t book_id1,book_id2,book_id3 String line value.toString().trim(); if (line.isEmpty()) return; String[] parts line.split(\t); if (parts.length 2) return; String[] books parts[1].split(,); // 两两组合注意去重和排序保证 ab 避免重复对 for (int i 0; i books.length; i) { for (int j i 1; j books.length; j) { String a books[i].compareTo(books[j]) 0 ? books[i] : books[j]; String b books[i].compareTo(books[j]) 0 ? books[j] : books[i]; pairKey.set(a : b); context.write(pairKey, ONE); } } } }这段代码的关键点有三个。第一输入格式假设每个用户的行为已经按用户聚合这需要前一个 ETL 作业完成不能直接把原始借阅流水丢进来。第二a和b的排序是为了保证(book1,book2)和(book2,book1)被当成同一对否则共现矩阵会出现重复计数。第三如果某个用户借了 50 本书两两组合就是 1225 对数据量会膨胀所以实际项目中通常会对热门用户做截断比如只取最近 20 本。课程设计数据量小可以不做截断但你要知道这个边界。Reducer 直接对相同 key 的 value 求和即可public class CooccurrenceReducer extends ReducerText, IntWritable, Text, IntWritable { private IntWritable result new IntWritable(); Override protected void reduce(Text key, IterableIntWritable values, Context context) throws IOException, InterruptedException { int sum 0; for (IntWritable val : values) { sum val.get(); } result.set(sum); context.write(key, result); } }驱动类里需要设置job.setCombinerClass(CooccurrenceReducer.class)因为共现计数是求和操作Combiner 能大幅减少 shuffle 数据量。这个优化在课程设计里经常被忽略但加上之后作业运行时间通常能缩短 30% 以上。3.3 相似度计算与推荐生成第二个作业怎么写第二个作业的 Mapper 要处理两类输入共现矩阵和用户行为。我一般用MultipleInputs来区分共现矩阵走一个 Mapper用户行为走另一个 Mapper然后在 Reducer 里做 join。但更简单的做法是分两步先单独跑一个作业算相似度再用相似度表和用户行为生成推荐。课程设计时间紧的话我推荐后者逻辑清晰虽然多一次 I/O。相似度计算公式用余弦// 在 Reducer 中计算余弦相似度 // 输入book_a:book_b \t cooccurrence // 需要额外加载每个图书的流行度被借阅次数 double similarity cooccurrence / Math.sqrt(popularityA * popularityB);这里popularityA和popularityB需要从另一个作业的输出中读取。常见做法是在 Driver 里用job.addCacheFile()把流行度文件分发到每个节点然后在 setup 方法里加载到内存。注意如果图书数量超过 10 万这个 HashMap 会占用较大内存需要设置合理的 JVM 堆大小比如-Xmx2g。推荐生成阶段对每个用户遍历他借过的书从相似度表中取出相似图书按相似度加权求和最后排除已借图书取 TopN。这一步的 Reducer 输出直接写入 HDFS格式为user_id \t book_id:score,book_id:score方便后续导出到 MySQL。注意相似度计算时如果分母为 0某本书从未被借阅要跳过该对否则会得到 NaN。这个异常在 Java 里不会抛错但会污染整个结果导出到 MySQL 时变成 NULL排查起来很费时间。4. 避坑与排查课程设计里最容易翻车的五个地方4.1 伪分布式环境跑 MapReduce 卡在 Map 100% Reduce 0%现象作业进度条一直停在 Map 100%Reduce 阶段迟迟不开始最后超时失败。原因通常是 YARN 的容器内存不足或者 Reduce 任务等待资源。课程设计常用的虚拟机只给了 2GB 内存而默认的mapreduce.map.memory.mb是 1024mapreduce.reduce.memory.mb也是 1024加上 ApplicationMaster 的 1024总共需要 3GB 以上。解决方法是修改yarn-site.xml中的yarn.nodemanager.resource.memory-mb为 2048同时把 Map 和 Reduce 的内存降到 512命令是-D mapreduce.map.memory.mb512 -D mapreduce.reduce.memory.mb512。4.2 中文图书标题在 HDFS 上显示乱码现象用hdfs dfs -cat查看清洗后的图书数据中文书名变成???或乱码。原因是原始 CSV 文件是 GBK 编码而 Hadoop 默认按 UTF-8 读取。解决方法是在 ETL 阶段用iconv转码或者在 Java 里指定InputStreamReader的字符集为 GBK。我一般会在上传前用file -i检查编码然后用iconv -f GBK -t UTF-8转换后再hdfs dfs -put。4.3 推荐结果里出现用户已经借过的书现象给用户推荐的 TopN 里有他上个月刚借过的书。原因是推荐生成阶段没有做已借过滤。解决方法是在 Reducer 里维护一个用户已借图书的 Set输出前检查if (!borrowedSet.contains(bookId))。这个 Set 可以从用户行为数据中提前加载但要注意内存占用如果单个用户借阅记录超过 1000 条建议用 Bloom Filter 替代。4.4 MySQL 导出时LOAD DATA LOCAL INFILE报错现象用LOAD DATA LOCAL INFILE把 HDFS 结果导入 MySQL 时提示The used command is not allowed with this MySQL version。原因是 MySQL 8.0 默认关闭了local_infile。解决方法是登录 MySQL 后执行SET GLOBAL local_infile 1;同时在连接字符串里加上allowLoadLocalInfiletrue。如果用的是 JDBC还需要在 URL 后追加allowLoadLocalInfiletrue。4.5 相似度矩阵过大导致 OOM现象第二个 MapReduce 作业在 Reduce 阶段报java.lang.OutOfMemoryError: Java heap space。原因是某个热门图书与其他图书的共现对过多Reducer 一次性加载所有相似度到内存。解决方法是设置mapreduce.reduce.memory.mb2048并调整mapreduce.reduce.java.opts-Xmx1536m同时在代码里对相似度做阈值过滤比如只保留相似度大于 0.1 的对能减少 70% 以上的数据量。5. 从离线推荐到效果验证A/B 测试与离线指标怎么落地课程设计答辩时老师最常问的一句话是“你怎么证明你的推荐是有效的”如果你只回答“我跑出来了结果”分数不会高。你需要准备至少一个离线指标和一个在线验证思路。离线指标我推荐用 Hit Rate命中率把用户最近一次借阅作为测试集用之前的行为训练模型看推荐列表里是否包含测试集里的书。计算方式如下# 离线评估计算 TopN 推荐的命中率 def hit_rate(recommend_dict, test_dict, N10): hits 0 total 0 for user, books in test_dict.items(): if user not in recommend_dict: continue rec_set set(recommend_dict[user][:N]) test_set set(books) if rec_set test_set: hits 1 total 1 return hits / total if total 0 else 0.0这段 Python 脚本读取两个字典recommend_dict是推荐结果test_dict是测试集。N取 10 是课程设计的常见值因为推荐列表太长没有展示意义。计算逻辑是只要推荐的前 N 本里有任意一本在测试集中就算命中。这个指标简单直观适合在答辩时展示。在线验证的思路是把用户随机分成两组一组看原始推荐一组看你的推荐统计两组的借阅转化率。课程设计没有真实流量可以用历史数据模拟——比如取最后一个月的数据做“在线”评估前几个月做训练。这种时间切分比随机切分更接近真实场景因为推荐系统本质上是在预测未来行为。提示离线指标高不代表线上效果好因为存在“信息茧房”问题——推荐越准用户越只看同类书多样性下降。课程设计里可以额外算一个覆盖率指标推荐结果中不同图书的数量占总图书数量的比例。覆盖率低于 10% 说明推荐过于集中答辩时主动提这一点老师会觉得你考虑得全面。最后说一个我自己的习惯每次跑完 MapReduce我都会用hdfs dfs -get把结果拉到本地用head -100看一眼原始输出而不是直接导入 MySQL。因为 MapReduce 的输出格式是key \t value如果 Reducer 里不小心多输出了一个制表符导入时就会字段错位。这个检查花不了两分钟但能省掉半小时的排查时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表