
简介基于Hadoop的商品推荐系统项目源码面向希望学习分布式计算与推荐算法落地的Java开发者、大数据初学者和电商方向研究者。通过这套源码可以直观理解如何把海量用户行为数据放入分布式存储并编写数据处理程序完成浏览记录、购买记录等信息的清洗与转换进而计算物品或用户之间的相似度生成个性化推荐结果。压缩包共含34个文件其中29个Java源文件覆盖数据预处理、协同过滤、相似度计算、推荐输出等关键模块另有5个XML配置文件用于描述依赖和运行参数。整个压缩包仅25KB属于轻量级源码工程方便快速导入开发环境阅读也适合课程设计或毕业设计参考。当前已有193人学习下载体现出该题材具有一定关注度。具体来看工程采用标准Maven目录结构和分层源码组织方式可直接对照代码理解用户协同过滤、物品协同过滤、基于内容推荐等典型算法的实现思路并在此基础上扩展为混合推荐或接入实时计算框架。研读该源码既能掌握Hadoop环境下离线推荐的完整链路也能为后期改进在线推荐模块打下基础。1. 基于hadoop的商品推荐系统课程设计里的常客生产环境的骨架基于hadoop的商品推荐系统是Hadoop课程设计和毕业设计里出镜率最高的一类题目。它要解决的核心问题不是推荐算法多新颖而是把单机就能跑通的协同过滤搬到HDFS加MapReduce的分布式框架上让数据存储和计算过程变成一条可演示、可答辩的完整链路。适合三类人正在选课程设计题目的在校生、想把推荐系统从单机脚本升级为大数据架构的初学者、需要往简历上写分布式项目经验的求职者。这个题目最容易翻车的地方不在算法而在环境搭建和MapReduce作业调试把这两块趟平项目就完成了一大半。2. 商品推荐系统的Hadoop架构存储、计算与ItemCF选型2.1 HDFS、MapReduce、YARN在这个项目里各管哪一段一个商品推荐系统拆到Hadoop生态里职责划分非常清晰HDFS负责存数据MapReduce负责算数据YARN负责给计算分配资源。原始用户行为数据比如“用户A在2024-03-01给商品B打了5分”以CSV或JSON的形式落在HDFS上中间结果比如物品共现矩阵、物品相似度表也全部写回HDFS最终的TopN推荐结果仍然是一张HDFS上的表供后续查询或导出。选Hadoop而不是一台服务器硬扛有三个现实理由。第一课程设计和面试答辩需要体现分布式技术纯单机脚本讲不出数据量大、计算分散的故事。第二数据量一旦到百万级评分单机内存装不下共现矩阵而HDFS天然把文件切块分散存储MapReduce把计算逻辑推到数据所在节点这个“移动计算不移动数据”的设计正好应对。第三HDFS默认保存多份副本推荐系统的训练数据丢了你还有后悔药这一点在课程设计答辩时也是加分项。这里要提前说清楚课程设计用伪分布式就够了。伪分布式是每个进程各跑一台JVM、共享一台机器真实集群是多台机器各跑一个角色。伪分布式能完整走通HDFS上传、MapReduce调度、结果回读的流程对推荐系统这个体量完全够用。做集群反而会给排查问题增加难度新手把伪分布式跑稳比搭三台虚拟机更实际。2.2 协同过滤选UserCF还是ItemCF表格式对比与选择理由商品推荐系统里最常见的算法是协同过滤分为基于用户的UserCF和基于物品的ItemCF。两者的核心区别一句话能说清UserCF找“和我兴趣相似的人”把那些人买过的商品推荐给我ItemCF找“和我买过的商品相似的商品”然后推荐这些相似品。我把两者的关键参数整理成一张表选型时直接对着看对比项UserCFItemCF实时性用户新行为不能立刻反映到推荐结果用户对某物品的新行为能立即影响相关物品推荐冷启动新用户没有历史行为难以找到相似用户新商品没有共现数据难以参与推荐计算开销用户数量大相似用户矩阵规模膨胀快商品数量通常小于用户数量矩阵相对稳定可解释性“和你相似的用户也买过”“买过这个商品的人还买了”适用场景新闻、社区、短视频等用户兴趣变化快的场景电商、视频网站等物品关系稳定的场景商品推荐系统应优先选ItemCF。理由有三个电商场景下商品数量增长比用户数量慢得多物品相似度表可以低频离线刷新比如每天凌晨算一次白天直接用用户的兴趣漂移对商品推荐影响不大你今天对数码产品感兴趣明天买日用品ItemCF都能覆盖从答辩角度讲ItemCF能给出“看了又看”“一起购买”式的推荐理由业务上更好解释。2.3 完整数据流从原始评分表到TopN推荐结果的四道工序确定了ItemCF下面把数据流拆开。推荐系统项目的数据流我一般设计成四道工序每一步的输出都是下一步的输入路径规划好调试时能顺着日志追。第一道数据清洗。原始评分表ratings.csv的字段是user_id、item_id、rating、timestamp清洗要处理缺字段、重复记录、评分超出合法区间比如0到5之外的数据输出干净的评分表到HDFS的/input/ratings_clean目录。第二道构建物品共现矩阵。对每个用户把他评分过的商品两两配对统计所有用户里商品对共同出现的次数输出“商品A:商品B 共现次数”。第三道计算物品相似度。用余弦相似度归一化共现次数公式是C[i][j]除以根号下N_i乘以N_j其中N_i是商品i被多少用户评过分。输出“商品A:商品B 相似度”到HDFS。第四道生成用户TopN推荐。把相似度表和用户评分表做关联对每个用户已评分的商品找出它们的相似商品用评分乘以相似度累加得到预测分排序后取前N个。这套数据流看起来并不复杂但每一步落成MapReduce都有细节。第4章我把每个阶段的代码骨架写出来同时交代清楚参数怎么调、哪些地方容易算错。3. 从零到一搭建Hadoop伪分布式core-site.xml、hdfs-site.xml 与启动自检3.1 版本配对JDK 1.8配合Hadoop 2.x是最省事的组合从零安装Hadoop版本选择比安装动作更重要。我的建议是JDK 1.8配Hadoop 2.10.x这是全网资料最多、踩坑记录最全、课程设计代码兼容性最好的组合。Hadoop 3.x虽然支持了NameNode联邦和纠删码但很多MapReduce老写法在3.x下行为有差异2.x的稳定性和资料密度对新手更友好。操作系统选Ubuntu 20.04或CentOS 7.9都行注意别用Windows直接跑服务端Windows下拿来写代码可以跑NameNode和DataNode会遇到文件权限和本地库问题把精力耗进去不划算。安装前先确认三件事JDK装好并且JAVA_HOME写进了/etc/profilessh localhost免密登录已配置好伪分布式虽然不强制走SSH克隆但start-dfs.sh默认会用ssh免密拉起进程机器内存不低于4GB否则同时跑NameNode、DataNode、ResourceManager和NodeManager会频繁GC甚至OOM。3.2 三个核心配置文件core-site.xml、hdfs-site.xml、mapred-site.xmlHadoop装完需要改配置文件。配置目录在$HADOOP_HOME/etc/hadoop下核心文件就三个core-site.xml、hdfs-site.xml、mapred-site.xml注意这个文件默认叫mapred-site.xml.template要重命名。yarn-site.xml先不配也能跑MapReduce但要跑YARN资源调度就得配我这里建议一步到位配上参考配置如下。core-site.xml设置默认文件系统地址和临时目录。这里最关键的是hadoop.tmp.dir默认值在/tmp下系统一清理你的NameNode元数据就没了项目做着做着DataNode起不来这是最玄学的坑之一!-- core-site.xml默认文件系统指向本地HDFS临时目录避开系统/tmp -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/data/hadoop/tmp/value /property /configurationhdfs-site.xml设置副本数和元数据目录。伪分布式副本数必须设为1不然每个块会尝试复制3份到同一台机器上报错不算还拖慢写入速度。namenode和datanode的目录也显式指定别用默认路径!-- hdfs-site.xml副本数设1数据目录独立管理 -- configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/data/hadoop/name/value /property property namedfs.datanode.data.dir/name value/data/hadoop/data/value /property /configurationmapred-site.xml指定计算框架用YARN!-- mapred-site.xmlMapReduce跑在YARN上而不是独立的MR框架 -- configuration property namemapreduce.framework.name/name valueyarn/value /property /configuration三段配置里有一个共同的动作手动创建/data/hadoop/tmp、/data/hadoop/name、/data/hadoop/data这三个目录并把属主改成当前用户。不创建也能跑但Hadoop在写目录时会因为权限问题报”Permission denied“别偷懒。3.3 格式化与启动先jps看进程再看Web UI和上传回读配置完成后的启动顺序要固定。首次启动先格式化NameNode后续不要再格式化除非你要彻底重建数据目录。格式化命令和执行结果自检放在一起# 格式化NameNode只在首次搭建时执行一次多次执行会导致clusterID不一致 hdfs namenode -format # 启动HDFS和YARN start-dfs.sh start-yarn.sh # 自检一查看Java进程必须看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager jps # 自检二查看数据节点注册情况 hdfs dfsadmin -report进程检查是第一步jps输出里五个进程缺一不可少NameNode看日志少DataNode看clusterID见第5章。第二步是浏览器打开HDFS Web界面确认Hadoop 2.x默认端口500703.x是9870。第三步最关键在HDFS里上传一个测试文件再读回来这个动作能验证FS地址配置、本地目录权限、副本策略全部正常# 上传并回读测试文件 echo hello recommendation test.txt hdfs dfs -mkdir -p /test hdfs dfs -put test.txt /test/ hdfs dfs -cat /test/test.txt看到读出的内容和本地文件一致伪分布式环境才算真正通。很多同学到这一步就以为万事大吉直接开始写代码结果作业一提交就报”Failed to connect to /localhost:9000“回头再查配置白白浪费时间。3.4 把商品数据导入HDFS建目录、传文件、验证块分布环境通了把推荐系统的数据放上来。推荐系统最少需要两张表商品表products.csvitem_id, item_name, price, category评分表ratings.csvuser_id, item_id, rating, timestamp。数据上传前做两件事统一转成UTF-8编码并去掉文件头。文件头会让MapReduce的第一行变成脏数据编码不一致会让中文商品名变成乱码这两个问题第5章会详细说这里先按正确姿势做。# 转码GBK原始文件转UTF-8去掉BOM头 iconv -f GBK -t UTF-8 products_origin.csv products_utf8.csv sed -i 1d products_utf8.csv # 上传到HDFS指定目录 hdfs dfs -mkdir -p /recommend/input hdfs dfs -mkdir -p /recommend/output hdfs dfs -put products_utf8.csv /recommend/input/ hdfs dfs -put ratings_utf8.csv /recommend/input/ # 验证文件已经按块拆分默认块大小128MB小文件只有一个块 hdfs fsck /recommend/input/ratings_utf8.csvfsck这条命令把文件的块分布打出来能看到文件被切成几个块、副本落在哪台机器上。哪怕文件只有几MB也建议跑一次亲眼看到块信息。这个习惯能帮你理解HDFS的存储模型后面遇到小文件优化见第6章时理解会更深。4. 用MapReduce实现ItemCF推荐共现矩阵、相似度计算与TopN生成的三个代码块4.1 构建物品共现矩阵在Reducer里把同一用户的评分物品两两配对第一个MapReduce作业的目标是统计所有商品的共现次数。Mapper读取清洗后的评分表输出用户ID作为key、商品ID和评分作为valueReducer拿到一个用户的所有评分记录后对商品两两配对输出一对共现关系。注意配对前要按商品ID去重避免同一个人重复评分导致同一个共现对被记多次。// Step1构建物品共现矩阵 // Map阶段读入 user_id,item_id,rating输出 user_id, item_id:rating public static class CoOccurrenceMapper extends MapperLongWritable, Text, Text, Text { private Text outKey new Text(); private Text outValue new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] fields value.toString().trim().split(,); if (fields.length 3) return; // 过滤缺字段的脏数据 String userId fields[0]; String itemId fields[1]; outKey.set(userId); outValue.set(itemId : fields[2]); context.write(outKey, outValue); // 同一个用户的所有评分进入同一个Reducer } } // Reduce阶段把同一用户下所有物品两两配对输出 itemA:itemB, 1 public static class CoOccurrenceReducer extends ReducerText, Text, Text, IntWritable { private Text outKey new Text(); private IntWritable one new IntWritable(1); Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { ListString itemList new ArrayListString(); for (Text v : values) { itemList.add(v.toString().split(:)[0]); } // 去重后再配对防止同一用户重复评分导致重复计数 SetString unique new HashSetString(itemList); String[] items unique.toArray(new String[0]); for (int i 0; i items.length; i) { for (int j i 1; j items.length; j) { outKey.set(items[i] : items[j]); context.write(outKey, one); // 共现一次记一条 } } } }这段代码要注意两个细节。第一Mapper的输出value用逗号拼接商品ID和评分Reducer里再拆出来这比自定义Writable类型省事课程设计阶段可读性优先。第二Reducer里用了HashSet去重如果不做这一步同一个用户对某商品评分两次会产生两条一模一样的共现记录后面算相似度时结果会偏大。去重是共现矩阵里最容易漏的逻辑漏了之后推荐结果看起来很合理但数值经不起细抠。4.2 计算物品余弦相似度把共现次数除以商品流行度的平方根第二个作业算相似度。输入的共现表是“itemA:itemB 共现次数”但余弦相似度还需要知道每个商品被多少个用户评分过也就是商品流行度N_i。常见做法是第一个作业的Reducer在输出共现对的同时再把每个商品的出现次数以特殊key写一份或者单独写一个统计作业。我这里用的方式是先把物品流行度表算好然后把这个表放进DistributedCache第二个作业每个Mapper在setup阶段把流行度读进内存map里直接套公式。// Step2计算物品余弦相似度 // DistributedCache里存放流行度文件格式itemId\t被评分用户数 public static class SimilarityMapper extends MapperLongWritable, Text, Text, Text { private MapString, Integer itemCnt new HashMapString, Integer(); Override protected void setup(Context context) throws IOException, InterruptedException { // 从DistributedCache加载物品流行度表 URI[] cacheFiles context.getCacheFiles(); if (cacheFiles ! null) { for (URI uri : cacheFiles) { Path p new Path(uri.toString()); FileSystem fs FileSystem.get(context.getConfiguration()); BufferedReader reader new BufferedReader( new InputStreamReader(fs.open(p), UTF-8)); String line; while ((line reader.readLine()) ! null) { String[] parts line.split(\\t); if (parts.length 2) { itemCnt.put(parts[0], Integer.parseInt(parts[1])); } } reader.close(); } } } Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] fields value.toString().split(\\t); if (fields.length 2) return; String pair fields[0]; double coCnt Double.parseDouble(fields[1]); String[] items pair.split(:); // 余弦相似度 共现次数 / sqrt(商品A流行度 * 商品B流行度) double nA itemCnt.getOrDefault(items[0], 0); double nB itemCnt.getOrDefault(items[1], 0); if (nA 0 || nB 0) return; double sim coCnt / Math.sqrt(nA * nB); context.write(new Text(pair), new Text(String.format(%.4f, sim))); } }相似度计算的关键参数是流行度文件怎么算出来。可以直接在共现矩阵那个作业里加一个Reducer输出也可以单独用一条Hive指令或MapReduce统计。我习惯在同一个作业里用MultipleOutputs把流行度单独写一份文件避免多跑一个作业但代码会复杂一些。用DistributedCache要注意cacheFiles不能超过一定大小课程设计的数据量小完全没问题真正的使用边界是当相似度表上亿条时每个Task都要复制一份内存撑不住那时才需要考虑把相似度表设计成全局共享存储比如HBase。4.3 生成用户TopN推荐相似度加权评分的累加与排序最后一个作业把用户行为数据和相似度表做关联。思路是对每个用户已评分的商品去相似度表里找出和它相似的商品把相似度乘以该用户对原商品的评分作为候选推荐分同一个候选商品可能从多个已评商品累加得分最终按总分排序取TopN。为了让Mapper能直接在内存里查相似度这个作业同样把相似度表放进DistributedCache。// Step3生成用户TopN推荐 // Map阶段读用户评分记录对每个已评商品查找相似商品输出 userId, 候选商品:预测分 public static class RecommendMapper extends MapperLongWritable, Text, Text, Text { private MapString, Double simMap new HashMapString, Double(); Override protected void setup(Context context) throws IOException, InterruptedException { // 加载相似度文件key为itemA:itemBvalue为相似度数值 URI[] cacheFiles context.getCacheFiles(); if (cacheFiles ! null) { for (URI uri : cacheFiles) { Path p new Path(uri.toString()); FileSystem fs FileSystem.get(context.getConfiguration()); BufferedReader reader new BufferedReader( new InputStreamReader(fs.open(p), UTF-8)); String line; while ((line reader.readLine()) ! null) { String[] parts line.split(\\t); if (parts.length 2) { simMap.put(parts[0], Double.parseDouble(parts[1])); } } reader.close(); } } } Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] fields value.toString().trim().split(,); if (fields.length 3) return; String userId fields[0]; String itemId fields[1]; double rating Double.parseDouble(fields[2]); // 遍历相似度表找到与当前商品相关的条目 for (Map.EntryString, Double entry : simMap.entrySet()) { String[] pair entry.getKey().split(:); if (pair[0].equals(itemId)) { context.write(new Text(userId), new Text(pair[1] : entry.getValue() * rating)); } else if (pair[1].equals(itemId)) { context.write(new Text(userId), new Text(pair[0] : entry.getValue() * rating)); } } } } // Reduce阶段按预测分降序取前N个商品 public static class RecommendReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { MapString, Double scoreMap new HashMapString, Double(); for (Text v : values) { String[] parts v.toString().split(:); // 同一个候选商品可能由多个已评商品累加得到求和 scoreMap.merge(parts[0], Double.parseDouble(parts[1]), Double::sum); } // 按分数降序排序 ListMap.EntryString, Double list new ArrayListMap.EntryString, Double(scoreMap.entrySet()); list.sort((a, b) - Double.compare(b.getValue(), a.getValue())); // TopN数量通过Configuration传入默认10 int topN context.getConfiguration().getInt(topN, 10); StringBuilder sb new StringBuilder(); for (int i 0; i Math.min(topN, list.size()); i) { if (i 0) sb.append(,); sb.append(list.get(i).getKey()).append(:).append(list.get(i).getValue()); } context.write(key, new Text(sb.toString())); } }这个阶段有3个参数要重点调。第一个是topN在Driver里用configuration.setInt(topN, 10)设置推荐个数不是越大越好课程设计通常5到10个太多答辩时不好解释。第二个是相似度阈值相似度低于0.01的条目建议直接过滤否则会把大量弱关联商品拉进推荐池分数全是噪声。第三个是Mapper遍历相似度表的方式上面是直接在map里逐条扫描数据量大时效率低可以用分词倒排索引优化课程设计阶段逐条扫足够。跑完三个作业HDFS的/r ecommend/output目录下能看到三步的中间结果。验证推荐质量时把最终结果拿到本地和测试集对比计算召回率这就是第6章的内容。5. 伪分布式项目最常踩的五个坑启动失败、中文乱码、数据倾斜与clusterID冲突5.1 NameNode启动秒退日志报NameNode is not formatted现象执行start-dfs.sh后jps里看不到NameNode进程去logs目录翻hadoop-用户-namenode-主机名.log最后几行出现“NameNode is not formatted”或“java.io.IOException: There appears to be a gap trying to read xxxx bytes”。原因这是最常见的伪分布式启动事故。要么是第一次搭建忘了执行hdfs namenode -format要么是hadoop.tmp.dir指向的目录被系统或人为清掉了元数据丢失NameNode找不到有效镜像要么是改了fs.defaultFS端口新端口下没有对应元数据。我在3.2节特别强调把hadoop.tmp.dir改出/tmp目录就是为了防这一手。解决如果确认是元数据丢失先删掉/data/hadoop/name和/data/hadoop/data目录里的内容重新执行hdfs namenode -format再启动。注意format执行成功会打印“Storage directory ... has been successfully formatted”认准这句话再往下走。如果只是端口换了把新端口的临时目录也一并清理再format不要吝啬删除动作伪分布式没有珍贵数据。5.2 上传中文商品名后文件变成乱码或MapReduce读出问号现象用hdfs dfs -put把含中文商品名的CSV传到HDFSWeb UI里文件名正常但文件内容读出来是一串“???”MapReduce作业把商品名落到结果表里也全是问号。原因终端上传文件时Hadoop的TextInputFormat默认按UTF-8解码但原始CSV是GBK编码。如果文件未转码直接putHDFS只是字节存储Web UI按UTF-8显示时就会乱码MapReduce按UTF-8读时遇到非法字节会替换成问号。这个坑在Windows上生成数据时尤其常见Excel默认存CSV就是GBK。解决上传前统一转码是治本办法。用iconv命令把GBK转成UTF-8同时用sed去掉文件头见3.4节。如果数据已经传上去用hdfs dfs -get下载到本地转码后再覆盖上传。还有一层防护是在代码里给mapper的map方法开头加一行InputFormat.setInputPathFilter之类但更省事的做法是保证源头数据干净别指望代码去兼容脏编码。5.3 MapReduce作业卡在map 100%但reduce一直0%部分reduce任务几十小时不结束现象作业运行到map 100%reduce进度卡在33%或66%点开YARN的Application页面看到某个reduce任务反复重试日志里全是GC停顿或“Container killed by the ApplicationMaster”。原因这是典型的数据倾斜。ItemCF的共现矩阵里头部商品的共现对数量可能是长尾商品的几千倍MapReduce默认按key哈希分区所有热门商品对会涌进同一个reduce那个reduce任务的内存被打爆其他reduce闲得没事干。解决两种手段结合用。第一种是加盐分桶把key拆成“盐值:itemA:itemB”盐值取随机数或itemId的hash对reduce数取模让同一条商品对分散到多个reduce每个reduce只算部分结果最后再合并。第二种是自定义Partitioner按商品ID做两层分区先把爆款商品均匀打散。课程设计阶段我推荐加盐因为改动最小只在写入key时拼一个随机前缀reduce完再把前缀拆掉。5.4 重复格式化NameNode后DataNode无法启动日志报Incompatible clusterIDs现象NameNode能起来但DataNode进程反复退出查看hadoop-用户-datanode-主机名.log报“Incompatible clusterIDs in .../current/VERSIONnamenode clusterIDxxx, datanode clusterIDyyy”。原因伪分布式最典型的二次启动事故。第一次格式化NameNode时生成了一个clusterIDDataNode启动时把这个ID记录在本地的VERSION文件里。之后由于各种原因你重新format了NameNode生成了新的clusterID但DataNode的VERSION还是旧的两边对不上DataNode拒绝注册。解决停止集群后把datanode的数据目录完整删掉也就是/data/hadoop/data目录下的current文件夹再重新start-dfs.sh。DataNode重启时发现本地没有VERSION会重新从NameNode拉取新的clusterID。这里强调一下这个操作会清空已经上传到HDFS的数据所以正式数据要提前备份。这就是为什么我在第3章反复强调“format只做一次”不是玄学是血泪经验。5.5 Windows下用IDEA跑MapReduce作业报跨平台权限或本地库错误现象在Windows上用IDEA写MapReduce直接右键运行main方法报“Permission denied”或“Failed to locate the winutils binary in the Hadoop binaries”作业在本地跑不起来即使能连上HDFS也常报“org.apache.hadoop.security.AccessControlException: Permission denied: userxxx, accessWRITE”。原因Hadoop底层在Windows上需要winutils.exe和hadoop.dll等本地库而且默认使用Unix文件权限模型Windows下代码里的用户信息会映射错。这不是代码问题是Hadoop生态在Windows下的先天性缺陷。解决两个办法。第一在IDEA的VM options里加-Dhadoop.home.dir指向Hadoop解压目录把对应版本的winutils.exe放到$HADOOP_HOME/bin下同时设置环境变量HADOOP_USER_NAMEhdfs手动指定HDFS用户绕过权限检查。第二这也是我最推荐的本地开发、集群执行。在IDEA里只负责把三个MR类写好打成jar包上传到Linux服务器用hadoop jar命令跑。课程设计答辩时老师更认可能真实提交到Hadoop集群的作业而不是本地调试通过的代码。Windows下搭建Hadoop开发环境这件事花时间解决是浪费绕过它才是正解。6. 推荐结果验证召回率计算、演示接口与集群升级路线6.1 用留出法切分训练集与测试集量化推荐质量推荐系统做完了不能只靠肉眼说“推荐得挺合理”要拿数据说话。把评分表按8:2随机切分8成做模型输入2成做测试集。推荐完成后把每个用户测试集里真实有过行为的商品和推荐结果做交集计算两个指标召回率是命中测试集真实商品数除以测试集真实商品总数精确率是命中数除以推荐数。# 按行随机切分数据集 awk -F, BEGIN { srand(42) } rand()0.8 { print train_ratings.csv; next } { print test_ratings.csv } ratings.csv # 统计测试集每个用户的真实商品数和TopN推荐结果做对比 python3 evaluate.py --test test_ratings.csv --recommend rec_result.csv --topn 10召回率在0.1到0.3之间算正常水平ItemCF在课程设计的稀疏数据上召回率不会很高别追求0.5以上那些数字往往是数据泄漏造出来的。答辩时把这个数字讲清楚比数字本身更重要召回率低的原因是评分矩阵稀疏大量商品的共现次数为0相似度表里根本不存在这些商品推荐池天然覆盖不到它们。6.2 把结果做成一个可查询的演示接口最终结果在HDFS上是一张文本表直接展示不够直观。演示时我更推荐把结果导出到MySQL把推荐结果文件下载到本地用LOAD DATA命令导进MySQL再用Spring Boot写一个rest接口参数传userId返回推荐商品列表。这一层不用放在Hadoop集群里跑集群负责离线计算业务接口负责在线读表这也符合真实电商系统的离线推荐架构。6.3 三个值得做的升级方向项目验收后如果想继续深挖有三个方向性价比最高。第一是给MapReduce加Reducer数量参数默认只有一个reducer数据量大时会成为瓶颈设置mapreduce.job.reduces为10或按用户数判断。第二是合并小文件评分表如果切成几十个小文件每个文件一个Map任务启动任务的开销比实际计算还大用CombineTextInputFormat合并输入分片能直观提升作业速度。第三是往Spark方向迁移把三个MapReduce作业换成Spark的groupBy和join算子代码量能缩一半推荐作业从分钟级降到秒级。如果已经有3台机器再从伪分布式升级到真实集群注意部署Zookeeper协调NameNode高可用这是Hadoop集群扩展绕不开的一步。我这些年带过的课程设计项目里凡是老老实实跑完数据流、亲手算过召回率、能讲清每个作业输出的同学答辩都没被问倒过。反而是那些把网上代码抄下来跑通就完事的连自己作业的中间结果都不敢打开看。推荐系统这个题目不怕简单怕的是做完了你还是把它当成黑匣子。希望帮到你。本文还有配套的精品资源点击获取