资讯详情

Python基于Hadoop的电影推荐系统实战:从MapReduce到ItemCF

📅 2026/10/12 21:53:58 | 华诺云谱 👁 阅读
Python基于Hadoop的电影推荐系统实战:从MapReduce到ItemCF
简介这套基于Hadoop的电影推荐系统Python实现专为计算机、电子信息、数学等专业学生打造可满足课程设计、期末大作业或毕业设计需求。压缩包共包含10个文件包括4个Python源码脚本、CSV评分数据、用户与电影信息数据集以及Markdown说明文档整体仅2.49MB便于下载保存和本地二次开发。整个项目采用参数化编程参数易调整、注释详细并附带运行结果已在实际环境中测试通过。当前已有231人学习/下载读者可借此掌握Hadoop平台上的文件操作与数据处理技能理解从原始数据读取、MapReduce任务执行到推荐结果输出的完整链路。基于Windows 10、Hadoop 2.8.3、Python 3、VSCode和MySQL 8.0的配置实践也为后续大数据项目开发提供了可直接复用的参考并降低了环境搭建门槛整套源码结构清晰、模块划分明确是一份适合新手从理论走向实战的入门范例。1. 用Python基于Hadoop跑电影推荐系统它到底是什么又适合谁python实现的基于Hadoop的电影推荐系统说的其实是一个很具体的东西用Python脚本通过Hadoop Streaming跑MapReduce任务在分布式环境里完成影视数据的存储、相似度计算和最终推荐。这个项目最常见的载体是课程设计和毕业设计数据源通常是公开的MovieLens电影评分数据输出是给每个用户推荐10部没看过的电影。一个反直觉的事实是单看数据量这个项目其实用不上Hadoop。十万条评分记录一台普通笔记本用pandas几分钟就能算完。但课程设计要的不是“算完”而是完整走一遍大数据链路数据进HDFS、脚本分布式执行、结果从集群取回。这套链路才是项目真正值钱的部分也是面试官会追问的部分。文章从Hadoop环境搭建开始把MapReduce的代码实现、参数设置和踩坑记录完整讲一遍适合正在做Hadoop课程设计、准备大数据方向面试或者想把推荐系统真正跑在分布式环境里的从业者。2. Hadoop伪分布式搭建与MovieLens数据上HDFS第一关不过后面全是坑2.1 伪分布式要过的三关JDK、SSH免密和配置文件hadoop伪分布式搭建是课程设计最主流的选择一台Ubuntu虚拟机就能完成不用买集群。常见的安装与配置流程里最容易让新手卡住的是三关JDK、SSH免密和四个配置文件。第一关是JDK。整个Hadoop运行在JVM上2.x和3.x版本都要求Java 8。下载JDK后把解压路径写进/etc/profile然后要让hadoop-env.sh里的JAVA_HOME指向同一个路径。这里有个常见的翻车点终端里java -version能出来但启动HDFS时报找不到Java多半是hadoop-env.sh里的JAVA_HOME还是默认的占位符没有改成实际绝对路径。第二关是SSH免密。伪分布式也需要namenode通过SSH连到本机启动datanode所以ssh localhost要能直接登录。做法是ssh-keygen -t rsa生成密钥再把~/.ssh/id_rsa.pub追加到~/.ssh/authorized_keys里。如果免密不生效优先检查.ssh目录权限是否过大authorized_keys权限必须是600。第三关是配置文件。伪分布式最少要改四个文件取值如下表配置文件配置项推荐取值说明core-site.xmlfs.defaultFShdfs://localhost:9000默认文件系统伪分布式指向本机hdfs-site.xmldfs.replication1单机只有一个datanode副本设1hdfs-site.xmldfs.namenode.name.dir/opt/hadoop/data/namenamenode元数据目录先mkdir后再格式化hdfs-site.xmldfs.datanode.data.dir/opt/hadoop/data/datadatanode数据块目录yarn-site.xmlyarn.nodemanager.aux-servicesmapreduce_shuffleMapReduce跑在YARN上必须配mapred-site.xmlmapreduce.framework.nameyarn把MR任务提交到YARN配置修改后先执行hdfs namenode -format格式化再依次启动start-dfs.sh和start-yarn.sh。不要每次改配置都重新格式化格式化会清空元数据我见过同学一天格式化五次最后数据目录全废。启动后用jps检查进程能看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager这五个进程齐了伪分布式才算立住。2.2 用Python清洗MovieLens评分数据把CSV变成MapReduce可读的TSV数据准备比想象中重要。MovieLens的ratings.csv虽然叫CSV但如果直接按逗号split遇到字段里的引号就会错位。我建议先用Python的csv模块做一个转换把评分数据变成Hadoop最友好的TAB分隔格式userId\tmovieId\trating三列不要表头。# prepare_ratings.py # 输入 MovieLens ratings.csv输出 Hadoop 友好的 userIdTABmovieIdTABrating import csv reader csv.reader(open(ratings.csv, r, encodingutf-8)) next(reader, None) # 跳掉表头 out open(ratings.tsv, w, encodingutf-8) for row in reader: if len(row) 4: continue # 脏数据行直接跳过 user_id, movie_id, rating row[0], row[1], row[2] out.write(%s\t%s\t%s\n % (user_id, movie_id, rating)) out.close() line_count sum(1 for _ in open(ratings.tsv)) print(ratings.tsv done, lines:, line_count)这段代码的逻辑很直白先用csv模块正确解析引号再重新拼成TAB分隔的定长三列。注意输出文件不要写表头MapReduce脚本里每一行都按数据行处理表头只会成为脏数据。len(row) 4的判断用来过滤残行这个习惯救过我很多次原始数据集里偶尔会有空行或字段缺失。清洗完成后把文件送进HDFShdfs dfs -mkdir -p /movie/input hdfs dfs -put ratings.tsv /movie/input/ratings.tsv hdfs dfs -ls /movie/input-mkdir -p会自动创建多级目录-put把本地文件复制到HDFS不是移动本地文件还在-ls用来确认文件真实存在。如果-put报错大概率是前面的配置文件有问题或namenode没起来回到2.1检查jps进程。2.3 用HDFS命令检查数据完整性文件块、副本数与目录规划数据上传后不要急着写算法先确认它在HDFS上是完整且健康的。我会用两个命令做检查hdfs fsck /movie/input/ratings.tsv -files -blocks hdfs dfs -count /movie/inputfsck能看到文件被切成了几个block每个block的副本数是多少。伪分布式下dfs.replication设为1所以每个block应该只有一份副本。如果日志里出现Missing replica基本是配置没生效或者datanode没起来。-count输出文件数和目录占用确认数据量级和你本地的行数对得上。HDFS目录规划直接影响后期迭代。我会固定一套路径中间结果和最终结果分开避免反复删除覆盖HDFS路径存放内容由谁产出/movie/input清洗后的ratings.tsv本地上传/movie/pairs电影对输出Job1/movie/count电影对聚合计数Job2/movie/sim相似度矩阵sim.txt本地脚本回传/movie/result最终推荐结果Job3这套路径也是项目文档说明的目录骨架。写文档的时候照这个结构画一张表数据流向一句话就能讲清楚答辩PPT也不用临时编词。伪分布式环境里HDFS磁盘空间非常有限建议每个Job跑完后把不用的中间路径删掉hdfs dfs -rm -r /movie/pairs这类操作要养成习惯。3. 用Hadoop Streaming写Python MapReduce从物品共现矩阵到Top-N推荐3.1 先想清楚算法再写代码为什么用ItemCF而不是UserCF推荐算法选型不需要花哨。协同过滤里两大门派UserCF是“和你口味相似的用户喜欢什么”ItemCF是“和你喜欢的电影相似的是什么”。电影推荐场景里ItemCF的理由更充分用户的兴趣在变但一部电影的相似电影是相对稳定的而且电影数量通常远小于用户数量物品之间的相似度矩阵更容易算、更好维护。在Hadoop课程设计里选ItemCF还有一个现实好处它天然贴合MapReduce的分组-聚合模型。整个计算可以切成三个任务Job1把用户看过的电影两两配对产出“电影A-电影B”的组合对。Job2统计每个电影对的共现次数再结合每部电影的出现次数算出相似度矩阵。Job3把相似度矩阵和用户评分结合计算候选电影的加权得分按用户输出Top10。这三个任务正好对应Hadoop的Map阶段和Reduce阶段分组交给shuffle聚合写在reducer里。下面按Job逐个写代码所有脚本都是纯Python通过Hadoop Streaming接入。3.2 Job1从用户评分到电影共现对Job1的map阶段非常轻读一行评分数据按TAB切出userId和movieId输出以userId为key、movieId为value让shuffle把同一个用户的所有记录送到同一个reducer。# phase1_mapper.py # 输入: user_id\tmovie_id\trating # 输出: user_id\tmovie_id import sys for line in sys.stdin: line line.strip() if not line: continue parts line.split(\t) if len(parts) ! 3: continue user_id, movie_id parts[0], parts[1] print(%s\t%s % (user_id, movie_id))注意print输出的每一行都会成为下一阶段的输入所以这个脚本里绝对不能print调试信息。想打印日志要写到sys.stderr。Hadoop Streaming会把mapper的stdout当作数据通道把stderr当作日志通道很多新手把日志打到stdout结果下一阶段收到一堆以DEBUG开头的脏数据。reducer端才是Job1的核心一个用户看过的电影列表聚齐后两两组合生成电影对同时输出每个电影自己的自对后面算相似度时会用到。# phase1_reducer.py # 输入: user_id\tmovie_id # 输出: movie_a\tmovie_b 含 movie_a\tmovie_a 自对 import sys from itertools import combinations current_user None movies [] def emit_pairs(movie_list): # 先输出每个电影的自对后面用来统计电影出现次数 for m in movie_list: print(%s\t%s % (m, m)) # 再输出有序组合避免 (A,B) 和 (B,A) 重复计算 for m1, m2 in combinations(sorted(movie_list), 2): print(%s\t%s % (m1, m2)) for line in sys.stdin: parts line.strip().split(\t) if len(parts) ! 2: continue user_id, movie_id parts[0], parts[1] if current_user ! user_id: if current_user is not None: emit_pairs(movies) current_user user_id movies [] movies.append(movie_id) if current_user is not None: emit_pairs(movies)combinations(sorted(movie_list), 2)保证了电影对有序同一个用户看过{A, B}两个电影时只输出A-B不会同时输出B-A。自对(movie, movie)是这批数据里最巧妙的一环一部电影被多少个用户看过完全可以从自对的统计次数里得到正好作为相似度计算的分母。提交Job1的命令hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -files phase1_mapper.py,phase1_reducer.py \ -mapper python3 phase1_mapper.py \ -reducer python3 phase1_reducer.py \ -input /movie/input/ratings.tsv \ -output /movie/pairs-files把两个本地脚本分发到每个容器的工作目录-mapper和-reducer指定执行命令注意一定要写python3写成python在某些系统会调到Python2导致语法报错-output目录必须事先不存在否则Hadoop直接拒绝执行。3.3 Job2聚合共现次数并生成相似度矩阵Job1的输出里同一个电影对会重复出现多次因为不同用户都可能看过这两个电影。Job2负责把这些重复行聚合成一个计数mapper直接复用系统的cat命令reducer做累加。# phase2_reducer.py # 输入: movie_a\tmovie_b # 输出: movie_a\tmovie_b\tcount import sys current_pair None count 0 for line in sys.stdin: parts line.strip().split(\t) if len(parts) ! 2: continue pair (parts[0], parts[1]) if current_pair ! pair: if current_pair is not None: print(%s\t%s\t%d % (current_pair[0], current_pair[1], count)) current_pair pair count 0 count 1 if current_pair is not None: print(%s\t%s\t%d % (current_pair[0], current_pair[1], count))提交命令hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -mapper cat \ -reducer python3 phase2_reducer.py \ -input /movie/pairs \ -output /movie/count这里-mapper cat直接用系统命令说明mapper不一定非要Python脚本Hadoop Streaming允许任何可执行程序。接下来把聚合结果取回本地用Python脚本直接计算相似度矩阵。这一步我坚持在本地做因为MovieLens小数据集的共现矩阵完全装得进内存没有必要再用MapReduce重写一遍而且全局计数这种操作硬套MR反而要处理跨任务共享状态的问题。hdfs dfs -getmerge /movie/count ./pair_count.tsv# build_sim.py # 输入: pair_count.tsv, 格式 movie_a\tmovie_b\tcount # 输出: sim.txt, 格式 movie_a\tmovie_b:sim,movie_c:sim,... import sys import math from collections import defaultdict MIN_CNT 2 # 共同看过该电影对的用户数下限 MIN_SIM 0.1 # 相似度下限过滤掉噪声 K 30 # 每个电影最多保留的相似邻居数 pair_cnt defaultdict(dict) self_cnt defaultdict(int) for line in open(sys.argv[1]): a, b, c line.strip().split(\t) c int(c) if a b: self_cnt[a] c else: pair_cnt[a][b] c sim defaultdict(list) for a in pair_cnt: neighbors [] for b, c in pair_cnt[a].items(): if c MIN_CNT: continue # 余弦相似度共现次数 / sqrt(电影A出现次数 * 电影B出现次数) s c / math.sqrt(self_cnt.get(a, 0) * self_cnt.get(b, 0) 1e-6) if s MIN_SIM: neighbors.append((s, b)) neighbors.sort(reverseTrue) sim[a] neighbors[:K] with open(sim.txt, w) as out: for a, neighbors in sim.items(): line a \t ,.join(%s:%.4f % (b, s) for s, b in neighbors) out.write(line \n)相似度计算用的是余弦公式count(A,B) / sqrt(count(A,A) * count(B,B))。分母里count(A,A)就是看过电影A的人数对热门电影做了天然惩罚避免《肖申克的救赎》这种大众片霸占所有推荐位。MIN_CNT、MIN_SIM、K这三个参数先用2、0.1、30跑一版第4章会详细展开怎么调。把sim.txt传回HDFS供Job3使用hdfs dfs -mkdir -p /movie/sim hdfs dfs -put sim.txt /movie/sim/sim.txt3.4 Job3加载相似度矩阵生成每个用户的Top-N推荐列表Job3是整个系统的最后一环。map阶段读原始评分数据过滤掉那些在相似度矩阵里没有邻居的冷门电影reduce阶段加载sim.txt对每个用户的所有评分做加权累加产出Top10。# phase3_mapper.py # 输入: user_id\tmovie_id\trating # 输出: 过滤后的 user_id\tmovie_id\trating import sys SIM_FILE sim.txt sim_items set() for line in open(SIM_FILE): a line.strip().split(\t)[0] sim_items.add(a) for line in sys.stdin: parts line.strip().split(\t) if len(parts) ! 3: continue if parts[1] in sim_items: print(%s\t%s\t%s % (parts[0], parts[1], parts[2]))注意-files参数会把sim.txt复制到每个task的工作目录所以脚本里直接用相对路径open(sim.txt)就能读到。如果不加-files脚本在集群节点上找不到这个文件会报FileNotFoundError这个错我在第一次跑Job3时踩过。reducer端的逻辑是推荐算法的收口# phase3_reducer.py # 输入: user_id\tmovie_id\trating # 输出: user_id\tmovie1:score,movie2:score,... import sys from collections import defaultdict TOP_N 10 SIM_FILE sim.txt sim {} for line in open(SIM_FILE): a, rest line.strip().split(\t) mapping {} for item in rest.split(,): b, s item.split(:) mapping[b] float(s) sim[a] mapping current_user None history set() rating_map {} scores defaultdict(float) def flush(): global current_user, history, rating_map, scores if current_user is None: return for movie, rating in rating_map.items(): for nb, s in sim.get(movie, {}).items(): if nb not in history: scores[nb] rating * s top sorted(scores.items(), keylambda x: x[1], reverseTrue)[:TOP_N] if top: out ,.join(%s:%.4f % (m, s) for m, s in top) print(%s\t%s % (current_user, out)) current_user None history set() rating_map {} scores defaultdict(float) for line in sys.stdin: parts line.strip().split(\t) if len(parts) ! 3: continue user, movie, rating parts[0], parts[1], parts[2] if user ! current_user: flush() current_user user history.add(movie) rating_map[movie] float(rating) flush()打分公式是经典ItemCF用户对电影B的预测分等于用户所有已评分电影A的评分乘以A与B的相似度累加求和。if nb not in history保证推荐结果不会出现用户已经看过的电影这个是用户体验的底线。最后用float转一下评分再排序避免字符串排序带来的数字错乱。提交Job3hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -files phase3_mapper.py,phase3_reducer.py,sim.txt \ -mapper python3 phase3_mapper.py \ -reducer python3 phase3_reducer.py \ -input /movie/input/ratings.tsv \ -output /movie/result跑完后查看结果hdfs dfs -ls /movie/result hdfs dfs -cat /movie/result/part-00000 | head -20如果一切顺利能看到形如用户ID\t电影1:分数,电影2:分数,...的输出行。到这一步一个基于Hadoop的电影推荐系统核心链路已经闭环了。4. 相似度度量、K值和TopN怎么定让推荐结果可解释的调参套路4.1 三种相似度度量的取舍共现次数、余弦相似度、Jaccard把算法跑通只是第一步参数和度量选型才是让项目“能答辩”的关键。相似度有三种常见度量它们的差异直接决定推荐效果度量计算公式特点适用场景共现次数count(A,B)直观但偏置大热门电影天然占优快速验证链路时用余弦相似度count(A,B) / sqrt(count(A,A) * count(B,B))惩罚热门电影ItemCF默认选择本项目默认Jaccard相似度count(A,B) / (count(A,A) count(B,B) - count(A,B))只看交集占比数值更温和行为数据极稀疏时调相似度不是玄学但要先看数据稀疏度。MovieLens 1M数据集里一部热门电影可能被几千人看过冷门电影只有个位数共现次数直接拿来做相似度推荐结果会被热门片刷屏冷门好片永远出不来。余弦相似度的分母把“看过这部片的人数”拉进来对热门做了平衡。Jaccard在稀疏场景更稳因为它的分母是并集大小不会被出现次数差距带偏。课程设计阶段用余弦相似度即可理由也容易讲公式简单、效果稳定、答辩时一眼能看懂。不要为了显得高级用复杂的Embedding方案徒增解释成本。4.2 三个必调参数MIN_CNT、MIN_SIM和K值build_sim.py里的三个参数是第一优先级的调试对象它们在文档说明里也应该是单独一节。MIN_CNT是共现次数下限。默认2意思是至少两个用户共同看过这两部电影这对才算有效。如果设成1任何一部冷门电影的偶然共现都会被算进去生成的相似度矩阵会混入大量随机关联。数据量较小或行为很稀疏时可以降到1但如果数据量正常建议保持2以上。MIN_SIM是相似度阈值。默认0.1低于这个值的邻居直接丢弃。这个参数的直觉是相似度低于0.1的电影对即使推荐出去用户也不会觉得“像”还会占用计算和存储资源。调参时先跑一版统计一下sim.txt里相似度的分布我见过很多项目把阈值设到0.3结果每个电影只剩两三个邻居推荐列表全是冷门片降到0.1才是合理区间。K值决定每个电影保留多少相似邻居。默认30对应的是sim.txt里每行的长度。K值太小比如10推荐结果集中在少数老片上多样性差K值太大比如50边际收益明显下降内存和网络开销却线性上涨。在10000部电影、K30的规模下sim.txt的大小只有几百KB到几MB课程设计完全扛得住。下面是经验参数速查参数推荐范围效果倾向MIN_CNT13越小覆盖越全越大噪声越少MIN_SIM0.050.2越小召回越多越大越精准K2050越小结果越集中越大越多样4.3 TopN到底取10还是20要精确率还是召回率推荐结果输出多少条要根据评估指标来定。TOP_N10时用户看到的列表短命中率通常会高一些精确率好看TOP_N20或50时覆盖面变大召回率会上升但用户感知里“准”的程度会下降。课程设计答辩时老师常问的一句话是“你这个推荐系统准确率是多少”如果你只说“我们推荐10部”这个问题其实没有闭合。正确做法是先切出一份测试集在TOP_N10、20、50三档分别计算精确率和召回率把结果整理成表格。这个过程放到第6章具体讲先记住结论默认TOP_N10评估时三档都要跑。4.4 把参数变成运行参数一条命令跑多组实验手动改代码里的参数再逐个重跑效率太低也容易忘记哪组参数对应哪个结果。我在项目里会把三个参数改成从命令行读取# build_sim.py 参数化改造示例 # 用法: python3 build_sim.py pair_count.tsv 2 0.1 30 import sys MIN_CNT int(sys.argv[2]) MIN_SIM float(sys.argv[3]) K int(sys.argv[4])改造后就能写成脚本批量实验python3 build_sim.py pair_count.tsv 2 0.1 30 mv sim.txt sim_cnt2_sim01_k30.txt python3 build_sim.py pair_count.tsv 1 0.05 50 mv sim.txt sim_cnt1_sim005_k50.txt每组实验产物用参数命名后面看结果文件就知道当时跑的是什么配置。Job3的TOP_N也可以同样从sys.argv读取。文档说明里记录一张参数实验表每组参数对应的样本数、召回率、精确率这样的课程设计报告在老师眼里和流水账完全两个级别。5. 避坑Hadoop伪分布式跑Python推荐时的5个踩坑现场5.1 subprocess failed with code 1脚本本地能跑Hadoop上一跑就挂现象Job提交后Task失败日志里出现PipeMapRed.waitOutputThreads(): subprocess failed with code 1。原因Python脚本在处理某行数据时抛了异常进程以非0码退出。最常见的是line.split(\t)后没有判断字段数量空行或脏数据导致数组越界。解决所有解析输入行的代码都先strip()再判空split()后检查长度再取下标。改完不要直接提交Hadoop先用管道模拟输入echo -e 1\t2\t3 | python3 phase1_mapper.py这一步能在30秒内发现90%的脚本问题。另外把异常信息写到stderrHadoop会把它带进日志里yarn logs -applicationId能看到具体报错。5.2 CSV引号导致字段错位推荐结果全是乱码现象清洗后的ratings.tsv里某些行的movieId变成了两段或者评分列出现了非数字。原因直接对CSV做split(,)遇到带引号的字段就把引号内的逗号也切开了。解决清洗步骤老老实实用Python的csv模块读取而不是手动split。转换完成后做一个列数校验任何一行字段数不是3就打印出来人工看。这个坑的特点是隐蔽大部分行正常只有少量行错位跑进Hadoop后变成脏数据排查起来非常费时间。5.3 Name node is in safe mode伪分布式最常撞上的墙现象执行hdfs dfs -put时提示Cannot create file ... Name node is in safe mode。原因namenode格式化后或异常重启后进入安全模式等待datanode上报数据块。最常见诱因是磁盘空间不足或者上次关机时HDFS没有正常停止。解决先查磁盘df -h确认dfs.datanode.data.dir所在分区没有写满然后强制离开安全模式hdfs dfsadmin -safemode leave如果每次重启都进安全模式检查hdfs-site.xml里dfs.replication是否设成了1副本数为3时单节点永远满足不了条件也会反复进安全模式。5.4 topN被字典序坑了9排在10前面现象推荐结果里分数最高的电影排在后面doc里的顺序看起来不对劲比如9.5分的排在0.8分的前面。原因reducer里对元组排序时评分是字符串而不是数值。字符串比较是一位一位比的9 10是True。解决排序前显式转成float。在我的phase3_reducer代码里sorted(scores.items(), keylambda x: x[1], reverseTrue)能正常工作是因为scores字典的值在累加时就是浮点数。如果你自己写了别的版本检查排序的key是不是数值类型。这个坑最坑的地方在于它不报错只会让结果在视觉上很奇怪。5.5 -file把相似度矩阵发给了每个task内存悄悄爆掉现象Job3跑起来后每个container的内存占用明显升高任务变慢甚至出现Container OOM。原因-files sim.txt会把完整矩阵复制到每个task的工作目录而每个mapper/reducer进程又会把它整个读进内存。电影数量大或K值高时这个文件的尺寸会失控。解决先把K值降下来sim.txt的规模约等于“电影数乘以K”K30时一两千部电影没问题K100就会膨胀好几倍。如果矩阵实在太大把相似度存到HBase在reducer里按需查询而不是全量加载。Hadoop Streaming不是银弹中间数据大到单机放不下时说明这个方案已经不适合用纯文件分发来支撑了。6. 验证推荐结果与落库从离线指标到可演示的课程设计6.1 先写评估脚本再调参用精确率和召回率说话推荐链路搭好后第一件事不是调参而是写评估脚本。把清洗前的ratings.csv按时间排序每个用户取最后20%的评分作为测试集其余作为训练集。训练集跑完推荐后用测试集计算Top10的精确率和召回率# eval_topn.py # 用法: python3 eval_topn.py test.tsv result.tsv import sys from collections import defaultdict test defaultdict(set) with open(sys.argv[1]) as f: for line in f: user, movie, _ line.strip().split(\t) test[user].add(movie) hit rec_cnt test_cnt 0 for line in open(sys.argv[2]): parts line.strip().split(\t) if len(parts) ! 2: continue user, movies_str parts[0], parts[1] recs [m.split(:)[0] for m in movies_str.split(,)] if user not in test: continue test_cnt len(test[user]) rec_cnt len(recs) hit len(test[user] set(recs)) precision hit / rec_cnt if rec_cnt else 0 recall hit / test_cnt if test_cnt else 0 f1 2 * precision * recall / (precision recall 1e-9) print(precision10%.4f recall10%.4f f1%.4f % (precision, recall, f1))6.2 把推荐结果写回MySQL网页演示的基本功推荐结果在HDFS里是一堆part文件演示给老师看要落库。常见做法是hdfs dfs -getmerge /movie/result result.tsv然后用Python把结果写进MySQL网页端直接查库展示。这个步骤代码量不大但对“完整项目”的观感提升很明显——HDFS里的文件只存在于命令行世界MySQL里的数据才能被网页和接口使用。6.3 再往前走一步HA和更大的数据集伪分布式跑通是及格线想做出区分度可以往两个方向走。一是升级成HA模式集成Zookeeper做自动故障切换这是hadoop和zookeeper整合实战的经验能让答辩时多一段“高可用”的谈资二是把数据集从ml-latest-small换成ml-25m把离线计算迁移到Spark推荐逻辑不变但架构从“能跑”变成“能扩展”。我复盘自己做过的那一版最后悔的不是算法调得不好而是没有先写评估脚本就去调参导致答辩被问“推荐质量怎么证明”时只能含糊带过。现在我做这类项目第一件事永远是切好训练集和测试集把评估脚本跑到能输出指标再动算法。这个习惯帮我避开了很多看似跑通、实则没效果的局面。希望这个顺序也能帮到你少走一段弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑