尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Hadoop豆瓣图书推荐系统:从爬虫到MapReduce实战

SpringBoot+Hadoop豆瓣图书推荐系统:从爬虫到MapReduce实战 简介一份基于Hadoop的豆瓣电子图书推荐系统毕业设计论文资源面向计算机相关专业学生和需要完成推荐系统、大数据方向毕设的开发者。内容从课题背景、需求分析到系统设计、数据库设计与测试完整梳理了基于SpringBootMySQL协同过滤算法的实现思路并以Hadoop处理海量书籍与用户数据适合用作论文框架参考或项目启动蓝本。包体共1个docx文件约5.24MB内含完整毕业论文正文包括摘要、目录、关键技术介绍、功能模块设计、页面截图与测试章节等结构清晰可直接翻阅。目前已有110人学习下载。资源不仅能帮助快速理解电子图书推荐系统的整体架构还可借鉴其管理员功能、个人中心、豆瓣高分管理等模块设计以及协同过滤推荐的具体落地方式对撰写开题报告、任务书和毕业答辩都有实用价值。1. 从毕设选题到落地系统这个标题到底在讲什么每年毕业季都能看到一批「基于 SpringBoot 的 XX 管理系统」而这一套带着 Hadoop 的电子图书推荐系统显然是冲着两个目标去的一是把增删改查的 CRUD 项目升级成有算法、有大数据框架的系统二是给论文里增加「分布式计算」「离线推荐」这些能让答辩老师点头的技术点。说白了这是一个把 SpringBoot、Hadoop 和推荐算法三者拼在一起跑通一条从数据采集、离线计算到在线推荐的完整链路。它适合正在选毕设题目、又不想做纯管理系统的同学也适合想了解推荐系统怎么从单机搬到分布式环境的开发者。这个系统的核心逻辑并不复杂先用爬虫把豆瓣的图书信息和用户评价拿下来存进 MySQL再定期用 Hadoop 的 MapReduce 做一次离线推荐计算把结果写回数据库或文件最后 SpringBoot 对外提供 REST 接口前端拿到接口数据展示「猜你喜欢」「相似图书」。难点不在任何一个单独环节而在组件之间的衔接——Hadoop 的版本、SpringBoot 的依赖、评分数据的稀疏度每一个都能让你在深夜改配置改到怀疑人生。下面按实际落地顺序把每一层讲透。2. 豆瓣图书数据从哪来爬虫的字段设计与清洗策略想做推荐系统第一件事不是写算法而是攒数据。豆瓣没有开放官方图书评分 API常见做法是自己写爬虫抓取图书详情页和用户评分页。别一上来就想着抓全站几万本书够用了重点是字段要对齐后续推荐算法的输入格式。2.1 需要哪些字段从「推荐」倒推数据表结构推荐系统的输入无非三类用户标识、物品标识、用户对物品的反馈。放在图书场景下就是 user_id、book_id、rating 三元组。但论文里不能只有这三列评审老师会追问「冷启动怎么办」「新书怎么推荐」所以数据库里还得有图书的元数据字段和用户的历史行为字段。我一般用四张表表名核心字段作用bookbook_id、title、author、publisher、pub_date、rating_avg、tags图书元数据做内容过滤和展示useruser_id、nickname、regist_date用户基础信息ratinguser_id、book_id、rating、create_time评分主表推荐算法的核心输入user_behavioruser_id、book_id、action_type、create_time浏览、收藏等隐式反馈补稀疏评分设计表结构时注意一个坑rating 表一定要有 create_time 字段后续做时间衰减、评估冷启动都要用到。version 字段MyBatis 乐观锁也建议加上因为离线推荐任务和用户实时评分写入会发生并发冲突。2.2 爬虫怎么写用 HttpClient Jsoup 控制节奏地抓取爬豆瓣图书页面技术上很简单难在频率控制。豆瓣对单 IP 的请求频率很敏感短时间高频请求会直接返回 418 或封禁。我建议单机单线程每抓一页 sleep 1~3 秒数据量控制在几千本。下面是一段可用的爬虫核心代码抓取豆瓣图书 top250 列表页并解析出图书信息// 豆瓣图书爬虫核心逻辑简化版 // 依赖org.apache.httpcomponents.client5:httpclient5、org.jsoup:jsoup public void crawlBooks(int startPage) throws Exception { String url https://book.douban.com/top250?start startPage; // 1. 构造请求带上浏览器标识避免被简单反爬策略拦截 HttpGet request new HttpGet(url); request.setHeader(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); request.setHeader(Referer, https://book.douban.com/); // 2. 发送请求拿到 HTML String html; try (CloseableHttpClient client HttpClients.createDefault(); CloseableHttpResponse response client.execute(request)) { html EntityUtils.toString(response.getEntity(), UTF-8); } // 3. 用 Jsoup 解析 HTML抽取图书条目 Document doc Jsoup.parse(html); Elements items doc.select(tr.item); for (Element item : items) { // 标题a 标签的 title 属性 String title item.select(a[title]).attr(title); // 评分span classrating_nums 的文本 String ratingStr item.select(span.rating_nums).text(); // 图书详情页链接后续可以二次请求拿简介、作者、出版社 String detailUrl item.select(a[title]).attr(href); Book book new Book(); book.setTitle(title); book.setRatingAvg(Double.parseDouble(ratingStr)); book.setDetailUrl(detailUrl); // 入库操作省略注意按 title 去重 bookMapper.insertOrUpdate(book); } }这段代码的逻辑分三步构造请求、解析 HTML、入库。参数说明startPage 是豆瓣列表页的分页参数每页 25 本User-Agent 必须设置否则豆瓣会返回 302 重定向到一个验证页面sleep 逻辑我在实际代码里放在 for 循环外每页抓完后Thread.sleep(1500)如果你用的是 WebMagic 这类框架可以配置 site.setSleepTime(1500)。注意insertOrUpdate要用 title 做业务去重因为同一本书可能出现在多个榜单里。爬完列表页后还需要二次请求 detailUrl 拿内容简介、作者、出版社这些元数据用于后续的基于内容的冷启动推荐。这个二次请求的解析逻辑类似用 Jsoup 选择.related_info或#content节点即可不再赘述。数据量建议用户 500 左右、图书 3000 左右、评分 10 万条以上这个量级既能让算法看出效果又不会让伪分布式 Hadoop 跑得太吃力。3. 推荐算法怎么选协同过滤与 Slope One 的落地实现数据到位后要选择推荐算法。在图书评分场景下基于物品的协同过滤Item-Based CF几乎是默认选项用户数增长快但物品图书数量相对稳定item-based 的相似度矩阵可以离线算好线上查询时延迟低。基于用户的协同过滤在这个场景会有一个明显问题——新用户没有历史评分相似用户根本算不出来。3.1 为什么是 Slope One简单、可解释、适合毕设工作量Slope One 是 item-based CF 里最朴素的一种核心思想是「用物品之间的平均评分偏差来预测未知评分」。它比计算余弦相似度更直观如果书 A 的平均分是 4.5书 B 的平均分是 4.0那么一个给 A 打了 5 分的用户对 B 的预测就是5 (4.0 - 4.5) 4.5。选择 Slope One 有三个理由第一实现代码短一个类就能写完训练和预测论文里贴代码不占篇幅第二每次推荐都能解释「因为你看过《活着》而你给《活着》打了 5 分《许三观卖血记》平均比《活着》低 0.3 分所以预测你也会喜欢」——这种可解释性比深度学习黑匣子更适合本科毕设第三它天然适合 MapReduce 化改造后面章节会细说。当然 Slope One 的短板也很明显它假设物品偏差是全局一致的没考虑用户群体的差异。所以实做时我会加两个修正一是引入评分次数作为权重只有共同评分人数超过阈值的物品对才参与预测二是做时间衰减越早的评分权重越低。3.2 单机版 Slope One 实现先跑通再上分布式任何算法都应该先在单机上用本地数据跑通再搬上 Hadoop。原因很简单分布式环境排错成本高一次堆栈日志可能让你找一晚上。下面是单机版的核心实现数据量在 10 万条以内跑起来毫无压力// Slope One 单机实现训练 预测核心逻辑 // 数据格式ListString[]每行 [userId, bookId, rating] public class SlopeOneRecommender { // 存储每个物品被评分的总分和次数用于计算平均分 private MapString, Double itemSum new HashMap(); private MapString, Integer itemCnt new HashMap(); // 存储物品对之间的偏差dev[i][j] sum(r_u(i) - r_u(j)) / count private MapString, MapString, Double dev new HashMap(); // 存储物品对被共同评分的次数 private MapString, MapString, Integer freq new HashMap(); public void train(ListString[] data) { // 第一遍统计每本书的评分总分和次数 for (String[] row : data) { String userId row[0], bookId row[1]; double rating Double.parseDouble(row[2]); itemSum.merge(bookId, rating, Double::sum); itemCnt.merge(bookId, 1, Integer::sum); } // 第二遍按用户分组计算物品两两之间的偏差 MapString, ListString[] userGroups data.stream() .collect(Collectors.groupingBy(row - row[0])); for (ListString[] rows : userGroups.values()) { for (int i 0; i rows.size(); i) { String bookI rows.get(i)[1]; double ratingI Double.parseDouble(rows.get(i)[2]); for (int j i 1; j rows.size(); j) { String bookJ rows.get(j)[1]; double ratingJ Double.parseDouble(rows.get(j)[2]); updateDiff(bookI, bookJ, ratingI - ratingJ); updateDiff(bookJ, bookI, ratingJ - ratingI); } } } // 第三遍求平均偏差除以共同评分次数 for (String bookI : dev.keySet()) { for (String bookJ : dev.get(bookI).keySet()) { int cnt freq.get(bookI).get(bookJ); dev.get(bookI).put(bookJ, dev.get(bookI).get(bookJ) / cnt); } } } // 预测用户对某本书的打分综合所有该用户评过分的书来计算 public double predict(String userId, String bookId, MapString, Double userRatings) { double total 0, weightSum 0; for (Map.EntryString, Double entry : userRatings.entrySet()) { String ratedBook entry.getKey(); double rating entry.getValue(); if (ratedBook.equals(bookId) || !dev.containsKey(bookId)) continue; if (!dev.get(bookId).containsKey(ratedBook)) continue; int cnt freq.get(bookId).get(ratedBook); if (cnt 2) continue; // 共同评分数太少偏差不可信 total (dev.get(bookId).get(ratedBook) rating) * cnt; weightSum cnt; } return weightSum 0 ? -1 : total / weightSum; } private void updateDiff(String bookI, String bookJ, double diff) { dev.computeIfAbsent(bookI, k - new HashMap()).merge(bookJ, diff, Double::sum); freq.computeIfAbsent(bookI, k - new HashMap()).merge(bookJ, 1, Integer::sum); } }参数说明cnt 2这个阈值是去噪的关键如果一本书只有一个人评过和另一本书的偏差毫无统计意义weightSum做加权平均让共同评分多的物品对在预测中占更大话语权。训练时按用户分组是关键步骤——Slope One 的所有偏差都来自「同一个用户对两本书的评分差」所以先按 userId 分组再在组内做两两组合。单机跑通后需要把训练结果序列化到本地文件或 MySQL供后续 SpringBoot 在线推荐调用。这一步不要偷懒直接每次启动都重新训练数据量大时启动会慢到让人崩溃。我一般是把dev和freq两个 Map 直接序列化成 JSON 文件SpringBoot 启动时加载到内存。3.3 冷启动兜底热门榜与内容匹配协同过滤天然无法处理两类用户没评过分的新用户和没有任何用户行为的新书。我的处理策略是双通道对新用户推荐全局热门图书 TopN对新书用图书的 tags 字段和用户历史评分图书的 tags 做 Jaccard 相似度匹配。这样既保证了召回率又能在论文里写一段「混合推荐策略」的改进点。需要强调的是混用策略的触发条件要写清楚用户评分记录数小于 5 条时走热门榜图书在推荐结果中从未出现且发布时间小于 30 天时走内容匹配。这两个阈值不是拍脑袋是在测试集上试过之后定的论文里可以放一张不同阈值下推荐准确率的对比表。4. 把离线计算搬上 HadoopMapReduce 作业的设计与提交单机 Slope One 在 10 万条数据下表现不错但论文里如果只有单机实现Hadoop 就没戏份了。这一章讲怎么把 Slope One 拆成 MapReduce 作业以及伪分布式环境下部署、提交的完整姿势。这也是全篇最容易翻车的地方配置和版本问题会在第 5 章集中排坑。4.1 为什么推荐计算要放到 Hadoop 上跑先回答一个你可能纠结的问题伪分布式 Hadoop 比单机项目慢得多为什么要用它因为论文需要体现「大数据存储与计算」的能力更重要的是理解一个生产场景当评分数据从 10 万涨到 1000 万、涉及上万本书时单机 Slope One 的两两组合复杂度是 O(N²)内存根本放不下。MapReduce 的好处是天然支持数据分片和并行计算把评分数据按用户分片后每台机器只处理一部分用户的评分对最后再聚合。伪分布式虽然只有一台机器但完整的任务调度、中间结果持久化流程和真实集群一致。换句话说Hadoop 在这套系统里的定位是「离线的批量计算引擎」SpringBoot 是「在线服务」两者通过 HDFS 上的结果文件解耦——这点要写进论文的架构图里黑匣子式的大数据组件堆叠是答辩大忌。4.2 把 Slope One 拆成 MapReduce三个作业串成一条链Slope One 的计算过程可以拆成三个 MR 作业每个作业的输入输出都在 HDFS 上作业一DiffCalculator按用户分组输出bookI bookJ devi 差值的累加和 count作业二DiffAggregator聚合作业一的输出对每一对图书求平均偏差得到最终偏差表作业三RecommendationGenerator读取用户历史评分和偏差表生成 TopN 推荐列表。下面给出作业一的 Mapper 和 Reducer 核心代码。这个阶段常见错误是在 Mapper 里尝试「按用户分组」——MapReduce 的分组是由 shuffle 阶段按 key 完成的Mapper 只需要把 userId 作为中间 key同一用户的所有评分记录自然会被分发到同一个 Reducer// 作业一DiffCalculator 核心代码 // 输入格式文本文件每行 userId\tbookId\trating // 输出格式文本文件每行 bookI\tbookJ\t累计偏差\t共同评分次数 public class DiffMapper extends MapperLongWritable, Text, Text, Text { private Text outKey new Text(); private Text outVal new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] fields value.toString().split(\t); if (fields.length 3) return; // 脏数据过滤 String userId fields[0].trim(); String bookId fields[1].trim(); String rating fields[2].trim(); // 用 userId 作为 MapReduce 分组 key outKey.set(userId); // 把 bookId 和 rating 按工具类格式拼装Reducer 侧再解析 outVal.set(bookId , rating); context.write(outKey, outVal); } } public class DiffReducer extends ReducerText, Text, Text, Text { private Text outKey new Text(); private Text outVal new Text(); Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { // 同一个用户下的所有 (bookId, rating) 都会在这里 ListString[] bookRatings new ArrayList(); StringBuilder sb new StringBuilder(); for (Text val : values) { String[] parts val.toString().split(,); bookRatings.add(parts); } // 组内两两组合输出 ((bookI, bookJ), deviation) for (int i 0; i bookRatings.size(); i) { String bookI bookRatings.get(i)[0]; double ratingI Double.parseDouble(bookRatings.get(i)[1]); for (int j i 1; j bookRatings.size(); j) { String bookJ bookRatings.get(j)[0]; double ratingJ Double.parseDouble(bookRatings.get(j)[1]); outKey.set(bookI \t bookJ); outVal.set((ratingI - ratingJ) \t1); context.write(outKey, outVal); } } } }逻辑说明Mapper 做的事情极其简单只是把输入行拆开以 userId 为 key 转发真正的工作在 Reducer——同一个用户的所有评分记录在 reduce 阶段到齐然后做两两遍历。这样设计的好处是map 阶段天然可并行同一用户的数据不会分散到不同机器而导致偏差算错。作业二就是一个标准的求和归约作业三相对独立需要从一个固定目录读偏差表结果文件再按用户生成推荐建议单独实现别和作业一耦合在同一个 Driver 里。4.3 伪分布式搭建与作业提交参数和命令直接抄伪分布式只需要一台 Linux 机器2 核 4G 就能跑内存再小会频繁 GC不需要 HDFS HA、不需要 YARN 高可用。核心配置文件就两个配置文件关键参数说明core-site.xmlfs.defaultFS hdfs://localhost:9000NameNode 地址hdfs-site.xmldfs.replication 1单节点副本太多浪费空间设 1 即可这两个参数是伪分布式最少配置。注意dfs.replication必须设为 1否则向 HDFS 写文件时会因为副本数大于节点数而一直卡在 pending 状态——这是新手最常见的玄学问题之一。作业提交用标准命令# 1. 用 Maven 打包跳过测试 mvn clean package -DskipTests # 2. 启动 HDFS确保 DataNode 和 NameNode 状态正常 start-dfs.sh # 3. 把评分数据上传到 HDFS hdfs dfs -mkdir -p /recommend/input hdfs dfs -put /home/user/ratings.tsv /recommend/input/ # 4. 清理输出目录MR 作业不允许输出目录已存在 hdfs dfs -rm -r -f /recommend/output1 # 5. 提交作业 hadoop jar target/recommend-hadoop-1.0.jar \ com.example.recommend.DiffCalculatorDriver \ /recommend/input /recommend/output1参数说明output1目录必须预先清理MapReduce 的老规矩——输出目录存在就直接报错这个设计让不少第一次跑的人原地崩溃Driver 类路径要写全限定名否则报ClassNotFoundException。如果要跨作业串联作业二输入就是/recommend/output1输出/recommend/output2依此类推。跑完后务必检查_SUCCESS标记文件。HDFS 上只有这个文件存在才代表整个作业成功完成否则即使有输出文件也是残缺的。实际踩坑中经常出现 reducer 崩溃后输出目录里残留半截数据这时候重新提交作业反而报目录已存在先把输出目录删干净再跑。5. 避坑清单Hadoop 与 SpringBoot 整合的 5 个常见问题排查这一章是血泪经验汇总。Hadoop 生态的版本兼容问题多到能单独开一门课挑最影响本项目的 5 个典型问题讲每条都按「现象 → 原因 → 解决」展开。5.1 Windows 下作业卡死报错NativeIO 找不到符号现象在 Windows 本地环境跑 hadoop jar 命令作业提交后一直停留在map 100% reduce 0%YARN 日志里报Failed to locate the winutils binary或NativeIO相关异常。原因Hadoop 的 NativeIO 层依赖 Windows 下的 winutils.exe 提供文件系统操作能力本地开发机不配置 Hadoop 环境变量时JVM 找不到对应的 native 库。解决开发阶段不要在 Windows 直接提交作业推荐用 Docker 跑一个单节点 Hadoop 镜像或者直接在 Linux 虚拟机里开发调试。如果必须在 Windows 上跑需要下载与 Hadoop 版本匹配的 winutils.exe 放入HADOOP_HOME/bin并配置HADOOP_HOME环境变量。我实践后最稳的方案还是 Docker——镜像启动一条命令搞定还能随时重置环境。5.2 SpringBoot 版本装太新启动就报 ClassNotFound现象SpringBoot 3.x 项目引入 Hadoop Client 依赖后启动直接抛java.lang.ClassNotFoundException: javax.xml.bind.JAXBException。原因SpringBoot 3.0 起基于 Jakarta EE 9把原来的javax.*命名空间换成了jakarta.*而 Hadoop 3.x 的客户端依赖仍然是旧版的javax.xml.bind两者冲突。解决锁 SpringBoot 版本在 2.7.x这是当前和 Hadoop 3.x 兼容性最好的版本线。不要追新毕设系统不需要 SpringBoot 3 的新特性。如果已经用了 3.x另一个方向是额外引入jakarta.xml.bind-api做兼容但会遇到更多依赖冲突属于给自己挖坑。5.3 Maven 依赖冲突guava、jackson 反复报错现象pom.xml 引入hadoop-client后项目里原有的 Jackson、Guava 类出现版本冲突报NoSuchMethodError或AbstractMethodError。原因Hadoop 客户端传递依赖里的 guava 和官方 SpringBoot 管理的 guava 版本不一致Jackson 类似。Maven 采用最近声明优先策略导致运行期加载到错误版本。解决统一排除 Hadoop 传递依赖里容易冲突的包用项目父级管理的版本覆盖dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.6/version exclusions exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion /exclusions /dependency逻辑说明排除这两个包后guava 由 SpringBoot 父依赖统一管理jackson-databind 也是 SpringBoot 自带版本。注意 Hadoop 3.3.6 这个版本是我用过的相对稳定的一个它要求 Java 8 或 11和 SpringBoot 2.7.x 默认的 Java 8 正好匹配。如果你的项目用的是 Java 17Hadoop 客户端在反射初始化时会报 IllegalAccess 错误需要升级 Hadoop 到 3.3.6 以上版本。5.4 推荐结果全是热门书明明协同过滤没生效现象接口返回的推荐列表永远是那几本高分书不同用户之间几乎没有差异协同过滤好像失效了。原因评分数据太少达不到cnt 2的阈值predict 方法里的 effective deviation 全被过滤兜底逻辑把热门榜顶了上来。另一个诱因是爬虫只抓了 top 250 高分书用户评分分布严重偏斜低分书没有样本。解决扩大数据采集范围不只抓高分榜单也抓豆瓣读书页面的最新上架和分类浏览页保证评分分布接近真实。同时把cnt 2改为cnt 1观察差异如果推荐效果变好说明阈值设高了如果噪声变大则换一种处理——把评分次数极少的物品对偏差直接置 0而不是跳过。5.5 HDFS 启动正常但作业反复失败磁盘空间告警现象作业提交后 YARN 反复 kill 任务日志里出现disk space is not enough或Local directory is not good。原因伪分布式部署时 HDFS 默认把数据存到/tmp目录而/tmp往往空间小且开机清理。另外默认配置下 NameNode 和 DataNode 的目录都在系统盘日志快速增长把空间吃满。解决在hdfs-site.xml里显式配置dfs.namenode.name.dir和dfs.datanode.data.dir指向数据盘目录例如/data/hadoop/name并且确保有足够磁盘空间。还有一个相关配置是dfs.datanode.du.reserved默认保留 10% 空间实际占用接近上限时 DataNode 会自动停止写入。6. 用评价指标让推荐系统站得住脚验证与对比实验的做法系统跑通只是第一步论文里真正说服答辩老师的是验证环节。推荐系统的验证不能只说「看起来推荐得挺准」要有量化指标和对比实验。这里收一个个人习惯永远先留着测试集不要在整个数据集上训练再重新预测自己那是自欺欺人。6.1 RMSE 评估按用户留出测试集做法是按用户维度切分数据每个用户的评分记录随机留 20% 作为测试集其余 80% 训练。计算预测评分与真实评分的均方根误差# 脚本思路读取推荐结果和测试集比对同一用户同一本书 # 推荐结果文件格式userId bookId predictedRating # 测试集文件格式userId bookId actualRating这一步实操时建议直接写个 Java 方法在 SpringBoot 里运行或者用 Python 脚本处理文本比对。RMSE 的行业参考线基于 Slope One 的评分预测在豆瓣图书数据集上 RMSE 通常落在 0.8~1.1 之间如果超过 1.2说明数据清洗或算法实现有问题。这个数值可以作为你的系统是否正常的判断依据。6.2 对比实验表格证明你不只是做了一道课后题推荐系统论文最稳妥的对比方案是三组热门榜Baseline、纯 Slope One、Slope One 时间衰减 频次加权你的改进方案。结果表格式如下算法RMSETop10 命中率热门榜1.3524.6%Slope One1.0277.8%Slope One 改进版0.9239.4%这个表格展示了改进的价值同时证明了实验不是「开箱即用」而是做了算法优化。Top10 命中率的意思是测试集中用户真实给了高分的书有多少出现在系统推荐的前 10 个位置里。初版跑出来的对比数据往往没那么好看调整方向建议优先试两个一是把直接的评分预测改为「只输出 TopN、不做全量预测」的排序任务用 PrecisionN 作为主指标这个更适合图书推荐场景二是检查测试集切分是否发生了用户穿越——如果同一个用户既有评分在训练集又有同一天的评分在测试集时间上并不合理按时间先后切分得到的指标才有说服力。最后说一下做这类系统的最大教训先把单机项目完整跑通、指标算出来再动 Hadoop 的改造。我见过太多人先搭分布式框架最后连单机推荐效果都说不清楚答辩问答环节直接崩盘。Hadoop 在这套系统里的正确角色是「为了更大的数据集而生」而不是「为了用 Hadoop 而 Hadoop」。把这条主线理清楚从数据到推荐再到评价闭环打通论文的工作量和逻辑自然就立住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表