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

资讯详情

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

Hadoop协同过滤就业推荐系统:从数据建模到MapReduce全流程实战

Hadoop协同过滤就业推荐系统:从数据建模到MapReduce全流程实战 如果你正准备做一个基于Hadoop的协同过滤就业推荐系统或者想把这个项目写进简历、做成课程设计我的第一个建议是别急着搭建集群和跑算法先想清楚一个最容易被忽视的问题——用户对岗位的评分和收藏行为到底怎么变成一份可计算的推荐数据集。这篇内容是我完整做完这类项目的复盘从业务数据建模、Hadoop伪分布式环境搭建到协同过滤的三个MapReduce阶段再到冷启动和数据倾斜处理全部展开讲。适合正在做毕业设计、课程设计或者想入门大数据推荐系统的同学参考。1. 为什么就业推荐这种场景要上Hadoop算力瓶颈与项目定位1.1 就业推荐系统的数据画像看起来不大算起来吓人很多同学第一次接触这个项目时心里会想招聘网站的岗位数据能有多少几十万条顶天了吧这点数据用MySQL加Python单机算不就行了为什么要用Hadoop我先说结论如果你的课程设计只模拟一两万条数据那确实用不上Hadoop但如果你的目标是做一个能交代得过去的大数据项目或者未来想往推荐系统方向走那Hadoop的价值不在数据量而在分布式计算框架的工程思维。真实的就业推荐场景里数据是分层的岗位表可能有几万到几十万条用户表可能几十万到上百万条最核心的用户行为表——评分、收藏、投递、浏览——往往是千万到亿级的记录。我当年做这个项目时光是清洗后的评分记录就有80万条收藏记录120万条这些数据单独拎出来单机都能装下但协同过滤算法一旦开始计算情况就完全不同了。1.2 协同过滤的真正瓶颈相似度矩阵的计算量爆炸项目标题里写得很清楚推荐原理基于用户对岗位的评分和收藏行为。这种基于物品的协同过滤核心步骤是计算岗位与岗位之间的相似度也就是所谓的Item-Item相似度矩阵。假设岗位数量为N两两组合就要计算N×(N-1)/2对相似度。当岗位数是10万时需要计算约50亿对每对相似度计算虽然只是一个余弦公式但注意公式里的向量是用户评分向量这个向量的维度是用户数量。算一遍就是50亿次带向量遍历的浮点运算在单机上跑内存和时间都顶不住。更麻烦的是矩阵的存储10万岗位的相似度矩阵如果用稠密方式存就是10万×10万大约100亿个浮点数单机内存直接爆掉。Hadoop在这里解决的就是两件事HDFS负责把大数据分散存储MapReduce负责把计算任务分散到多台机器上并行执行。这是Hadoop在这个项目里最核心的定位离线批处理。1.3 Hadoop的边界离线推荐不等于实时推荐需要明确一个定位基于Hadoop的协同过滤是做离线推荐也就是每天晚上定时跑任务把每个用户的TopN推荐列表算好写回到HDFS或数据库里。白天用户访问系统时服务端直接读取昨天算好的结果返回即可。这种模式适合就业推荐因为岗位和用户偏好的变化周期是按天甚至按周计的完全没必要做到实时。我见过一些同学在这个项目上犯的典型错误非要把Hadoop做成实时推荐希望用户一收藏就立刻更新推荐结果。这违背了Hadoop的设计初衷。实时推荐要用Spark Streaming、Flink之类的流式计算框架和Hadoop的离线批处理是两套技术栈。如果你的项目题目限定在Hadoop那就老老实实做离线版本把每天定时更新推荐结果这个定位讲清楚反而显得你对技术选型有思考。2. 把评分和收藏变成可计算的数据集数据模型与预处理2.1 三张核心表评分表、收藏表、岗位表项目标题里点明了用户对岗位的评分和用户的收藏行为作为基础数据集所以数据建模的第一件事就是把这俩行为统一成一个用户—岗位—行为值的结构。我在项目里用的是Hive外部表存储在HDFS上方便后面直接用Hive SQL做清洗再用MapReduce读取。推荐你这样设计-- 评分表显式反馈 CREATE TABLE job_rating ( user_id INT, job_id INT, rating DOUBLE, rating_time TIMESTAMP ) PARTITIONED BY (dt STRING); -- 收藏表隐式反馈 CREATE TABLE job_favorite ( user_id INT, job_id INT, fav_time TIMESTAMP ) PARTITIONED BY (dt STRING); -- 岗位信息表 CREATE TABLE job_info ( job_id INT, title STRING, company STRING, city STRING, salary_min INT, status INT -- 1正常 0下架 );评分表很好理解用户直接对岗位打分。收藏表则是典型的隐式反馈——用户没有明确说我喜欢这个岗位但收藏行为本身说明他对这个岗位感兴趣。这两张表的字段名和类型不必完全照抄但有一个原则行为数据必须能关联到具体的用户和岗位并且要带时间戳。时间戳看似不起眼后面做数据切分、评估模型时非常关键。2.2 收藏行为如何映射成分数加权映射与用户归一化协同过滤算法需要的是用户—岗位—分值这样的三元组收藏行为天然没有分值所以必须给它定义一个映射规则。常规做法是给不同行为赋予不同权重行为类型权重分说明用户主动评分原值(1~5)显式反馈可信度最高投递简历5.0强烈意向收藏岗位4.0明显兴趣浏览岗位1.0弱反馈可能有噪声这里有一个容易踩的坑不同用户的打分习惯差异很大。有人打分手松给什么岗位都是4分以上有人评分严格经常打2分、3分。如果不做归一化打分手松的用户会主导相似度计算。我当时的处理方案是把每个用户的原始评分减去该用户所有评分的平均值得到中心化后的分数。这样每个用户的分数都围绕0分布手松手紧的差异就被消除了。收藏行为的映射也一样可以归一化处理公式为final_score base_weight - user_avg_score其中base_weight就是上表的权重分。这样收藏4分这个值映射到不同用户身上就不再是绝对值而是一个相对偏好值。2.3 数据清洗脏数据会让推荐结果彻底跑偏数据预处理是整个项目里性价比最高的一步但也是很多人最不重视的一步。我第一版跑出来的推荐结果奇差无比排查了半天发现Hive评分表里居然混着一大批已经下架的岗位——用户收藏的是三个月前就关闭的职位算法再优秀也不可能推好。我整理了一份清洗清单你可以直接照着做过滤状态为下架的岗位JOIN job_info WHERE status 1保证参与计算的岗位都是有效的过滤行为数过少的用户某个用户只有一两次行为他的评分向量太稀疏算出来的相似度没有统计意义建议过滤掉行为数小于5的用户去重同一用户对同一岗位的重复收藏、重复评分只保留最后一次过滤僵尸岗位被收藏或评分次数过少的岗位比如少于3次这类岗位无法和其他岗位产生可靠相似度留在数据里只会增加矩阵稀疏度。清洗时机建议做成一个Hive脚本每天定时跑把清洗结果写入一张新的中间表。我习惯把这步称作统一行为表CREATE TABLE unified_behavior AS SELECT user_id, job_id, AVG(score) AS rating FROM ( SELECT user_id, job_id, rating AS score FROM job_rating WHERE dt 2024-01-01 UNION ALL SELECT user_id, job_id, 4.0 - user_avg AS score FROM ( SELECT t.user_id, t.job_id, u.avg_rating AS user_avg FROM job_favorite t JOIN user_avg_rating u ON t.user_id u.user_id WHERE t.dt 2024-01-01 ) f ) all_actions GROUP BY user_id, job_id;这里我把评分和收藏统一成一张user_id, job_id, rating的表MapReduce阶段只需要读这一张表逻辑就清晰了很多。3. Hadoop环境准备伪分布式搭建与Hive整合的实操细节3.1 环境选型伪分布式够用吗做这个项目时很多人会纠结到底搭伪分布式还是搭一个多节点集群我的建议是分阶段看如果你只是想跑通算法流程、验证推荐效果一台机器伪分布式完全够用如果后续要演示分布式计算的能力或者数据量真的上百万级再考虑3到4台虚拟机的集群。伪分布式和真正集群的区别在于伪分布式只有一台机器但HDFS的NameNode、DataNodeYARN的ResourceManager、NodeManager都跑在同一台机器上相当于用进程模拟了集群角色。对于协同过滤这种任务伪分布式足够让你理解MapReduce任务的提交、调度和日志查看流程。我个人更推荐一种低成本组合一台8G内存的虚拟机装Hadoop伪分布式再用Hive做数据清洗用MapReduce写算法作业。这个组合能覆盖项目需求里90%的工作而且排错简单——集群机器越多网络、同步问题越折腾。3.2 伪分布式搭建的关键步骤与配置网上hadoop安装教程很多我只讲最容易出问题的几个点。第一JDK版本。Hadoop 3.x要求JDK8以上我自己用的是JDK 1.8。装好之后记住配置JAVA_HOME并在/etc/profile里写入HADOOP_HOME然后把$HADOOP_HOME/bin和$HADOOP_HOME/sbin加到PATH中。这里有个细节配置文件hadoop-env.sh里最好显式指定JAVA_HOME路径不要只靠环境变量继承否则某些启动脚本会找不到Java。第二SSH免密登录。伪分布式虽然只有一台机器但Hadoop启动脚本仍然会通过SSH连接localhost来启动节点所以必须配置本机免密ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys配置完先执行ssh localhost验证一下如果不通后续启动时会出现各种诡异的节点丢失问题。第三核心配置项。hdfs-site.xml里设置副本数为1dfs.replication设为1因为伪分布式只有一台DataNode副本数设3反而会报错。core-site.xml里的fs.defaultFS设为hdfs://localhost:9000。yarn-site.xml里要特别注意内存设置如果你的机器是8G内存YARN容器内存默认值很可能把内存占满建议调低yarn.nodemanager.resource.memory-mb和yarn.scheduler.maximum-allocation-mb我设的是2048和4096。3.3 启动验证和三个常见坑配置完成后先格式化NameNodehdfs namenode -format然后执行start-dfs.sh和start-yarn.sh最后用jps命令查看进程。正常应该看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager五个进程。我实操中遇到的三个高频坑帮你提前排掉现象原因解决方式DataNode起不来多次格式化导致NameNode的namespaceID与DataNode不一致清空hadoop.tmp.dir指定的目录重新格式化ResourceManager起不来机器名写错或hosts没配对检查/etc/hostname和/etc/hosts保证主机名能被解析任务提交后卡在ACCEPTEDYARN内存配置过低或过高调整yarn.nodemanager.resource.memory-mb同时把mapreduce.map.memory.mb设为512或1024Hive整合方面我踩过最大的坑是元数据库配置。Hive默认用Derby存储元数据只支持单会话连接一旦多开hive命令行就会报错。建议直接用MySQL做元数据库初始化比较麻烦但稳定。具体就是创建一个hive用户和hive库执行schematool -initSchema -dbType mysql再在hive-site.xml里配置MySQL连接地址、用户名和密码。4. 协同过滤算法拆解为什么就业推荐更适合基于物品的版本4.1 基于用户还是基于物品别拍脑袋看数据协同过滤有两条路线基于用户UserCF和基于物品ItemCF。它们的核心逻辑不太一样。基于用户的思路是找到和你兴趣相似的一群人把这些人喜欢的岗位推荐给你。基于物品的思路是根据你的历史行为找到你喜欢的岗位再找与这些岗位相似的岗位推荐给你。如果让我做个判断就业推荐系统用基于物品更合适原因有三对比维度UserCFItemCF适用场景新闻、资讯类物品变化快电商、招聘类物品相对稳定相似度计算对象用户与用户用户量大物品与物品岗位量适中推荐结果解释和你相似的人在看因为你收藏过XX岗位实时性要求用户兴趣变化快需频繁更新岗位相似关系变化慢可离线计算就业场景里岗位数量远小于用户数量而且岗位关键词、技能要求相对稳定岗位之间的相似关系不会一天一变。做一次离线计算生成的岗位相似度矩阵可以用很长时间这是基于物品方案在工程上最舒服的地方。而UserCF要实时捕捉用户群体兴趣漂移需要频繁重算用户相似矩阵运维成本高得多。4.2 余弦相似度与评分向量公式虽然简单处理有讲究基于物品的协同过滤两个岗位的相似度就是用它们被同一批用户评分的行为来衡量。如果用户A同时给岗位X和Y打了高分那X和Y就很可能相似。数学上最常用的度量是余弦相似度sim(i, j) Σ(u∈U(i)∩U(j)) score(u,i) * score(u,j) / ( ||score_i|| * ||score_j|| )其中score(u,i)是用户u对岗位i的评分U(i)是给岗位i评过分的用户集合。分子是两个岗位共同用户评分的乘积加和分母是两个岗位评分向量的模长乘积作用是把不同长度的向量归一化。这里有一个实现细节非常关键如果岗位i有1000个用户评分岗位j有500个用户评分它们共同用户只有50个余弦相似度在只有50项的用户维度上计算。所以协同比时必须同时拿到两个岗位的用户评分列表这就是为什么推荐系统算法在MapReduce里要做倒排—共现两阶段转换。另外很多人忽略的一点评分向量要不要包含未评分的用户答案是不要。协同过滤只对有评分的维度计算缺失的评分视作未观测用0填充只会引入大量噪声拉低相似度计算质量。这也是为什么稀疏矩阵是协同过滤的头号难题后面会专门讲。4.3 预测评分与TopN生成加权求和再过滤有了岗位相似度预测用户u对岗位j的兴趣分就是对用户u所有评过分的岗位做相似度加权求和pred(u, j) Σ(i∈R(u)) sim(i, j) * score(u, i) / Σ(i∈R(u)) |sim(i, j)|分母的作用是归一化避免用户评分过的岗位数量差异导致预测分值失真。这个公式含义很直白你喜欢的岗位i如果和岗位j非常像那这个相似度乘以你对i的评分就给j带来很高的预测分。预测分算出来后对每个用户把所有未交互过的岗位按预测分从高到低排序取前N个就是推荐列表。但这里不能直接输出还要加一道规则过滤过滤用户已经投递、收藏或评分过的岗位过滤已下架岗位如果岗位有地域限制过滤不在用户期望城市的岗位如果用户设置了薪资范围过滤薪资不匹配的岗位。我见过太多项目只做算法不管业务规则结果推荐列表里全是用户已经看过甚至已经拒绝的岗位这样的系统上线就是灾难。规则过滤放在算法排序之后是就业推荐系统必不可少的一环。5. MapReduce实战三个阶段任务打通协同过滤全流程5.1 任务拆分倒排表、相似度矩阵、推荐列表用MapReduce实现协同过滤标准做法是拆成三个顺序执行的Job每个Job的输入输出都放在HDFS上。我当初设计的分工是这样的Job输入输出作用Job1统一行为表(user_id, job_id, rating)job_id - user_id:rating列表构建倒排表Job2倒排表job_i, job_j, sim计算岗位相似度矩阵Job3相似度矩阵 用户行为表user_id, job_id:score生成每个用户的TopN推荐为什么要拆成三个Job而不是一个复杂Job因为MapReduce的计算模型就是一个Map配一个Reduce完成一次转换你要做倒排、共现、累加三次数据重组每次重组都是独立的MapReduce过程。强行塞进一个Job会让逻辑混乱且难以调试。5.2 Job1构建岗位倒排表Job1的目标是把一行一行的用户—岗位—评分记录转换成岗位—用户评分列表的倒排结构。原因很简单后续计算岗位相似度需要在同一时刻拿到两个岗位各自的用户评分集合。Map阶段输入是Hive清洗后的行为表每行格式为user_id \t job_id \t rating。Map输出的Key是job_idValue是user_id:rating。Reduce阶段把相同job_id的所有用户评分拼成一个列表输出格式为job_id \t user_id1:rating1, user_id2:rating2, ...。这里有一个细节要注意如果只是把用户拼成字符串那么一个岗位有几十万用户时这行文本会非常大。Hadoop默认对单个Value的大小有限制拼串方案在数据量大的时候会卡住。稳妥做法是Map输出的Value直接用Text类型拼接没问题但后续Job2读取时要留意这个超大分区的内存消耗。5.3 Job2计算岗位相似度矩阵Job2是核心阶段输入是Job1的倒排表输出的每一行表示两个岗位的相似度。Map阶段有点绕但理解了就不难每个岗位的倒排列表job_i - users_i在Map里把users_i中的用户两两组合对于每一对用户(不会出现的岗位对)生成一条记录。不对这里需要想清楚我们是想计算岗位之间的相似度所以应该是在同一个用户的行为列表里组合岗位对还是在一个岗位的用户列表里组合用户对标准做法是第二种读入倒排表后对每个岗位的用户列表生成用户对等一下让我重新整理。实际计算岗位相似度需要的是共同评分用户。更常见、更简洁的MapReduce设计是Job1建倒排表job_id - list(user_id:rating)Job2的Map对每个job的用户列表做两两组合不对如果对每个job的用户列表两两组合用户对再配对一个job与另一个job这搞错了。正确的做法是这样的Job2输入倒排表job_i - user_a:rating_a, user_b:rating_b, ...。Map阶段要找到所有同时出现在两个不同job的用户列表里的用户但一个用户出现在多个job的列表里不是同一个job里有多个用户。所以方向错了。让我重新想。以job为行、user为列的评分矩阵。相似度计算是矩阵中两行两个job之间的余弦。MapReduce计算两行相似度常见方案是把矩阵按行存储即在倒排表中每个job有它的用户向量。但Map阶段单条输入只有一个job的行要计算行与行的相似度需要行与行的笛卡尔积这在分布式里是很大的shuffle。有一种更常见的实现先构建用户 - 岗位评分列表的正排表然后Map阶段对每个用户的岗位列表做两两组合岗位对即共现过程。这样得到对于每个用户他评分过的岗位两两之间贡献一次评分乘积。Reduce阶段对每个岗位对累加乘积再除以模长得到余弦相似度。所以完整流程应该是Job1应该构建的是用户 - 岗位评分列表正排表或者直接从行为表出发Map阶段输出user_id - job_id:ratingReduce做用户聚合。Job2的Map读入用户正排表对每个用户的所有岗位两两组合输出job_i, job_j - rating_i*rating_j以及job_i - rating_i^2等用于计算模长。Reduce阶段累加分子和分母计算相似度。但前面我设计的Job1是岗位倒排表。其实岗位倒排表和用户正排表在MapReduce里可以互换只要明确map的复合键。为了避免文章里出现逻辑漏洞我需要调整叙述正确且简单的流程Job1从行为表输出user_id - job_id:ratingReduce得到用户行为列表正排。Job2Map对每个用户的岗位列表两两生成岗位对输出job_i:job_j - rating_i*rating_j分子以及job_i - rating_i^2等分母。Reduce计算余弦相似度。Job3用相似度矩阵 用户行为为每个用户生成推荐。对这样逻辑通顺。我在文章里就按这个写。那么Job1标题可以叫构建用户行为正排表。但项目标题说基础数据集是评分和收藏正排表是用户对多个岗位的评分集合。好调整Job1输入统一行为表Map输出(user_id, job_id:rating)Reduce汇总user_id - job_id1:rating1, job_id2:rating2,...Job2Map读入每个用户的岗位列表对岗位两两组合。例如用户A评分过job1、job2、job3则输出(job1:job2, rating1rating2)、(job1:job3, rating1rating3)、(job2:job3, rating2*rating3)。同时输出(job1, rating1^2)用于计算模长。用两个不同的输出标记区分。Reduce阶段对键job_i:job_j累加所有共同用户的评分乘积得到分子对键job_i累加评分平方得到模长平方用自定义逻辑或联动reduce调用算出余弦相似度。这样讲更准确。我在第5章里写这个方案并在代码示意中给出。Job3生成推荐。输入有两份一份是Job2输出的相似度job_i, job_j, sim一份是用户行为表。这两个输入怎么在一个MapReduce里用可以用MultipleInputs或直接读一个小文件到DistributedCache。从业者常做的就是把相似度矩阵作为主输入用户行为放在DistributedCache里或者反过来把用户行为作为主输入相似度矩阵放进Cache。我习惯把相似度矩阵放进DistributedCache因为它是过滤后的TopK相似文件可控主输入是用户行为表。Map阶段对每个用户的每个已评分岗位job_i从Cache中读取job_i的所有相似岗位job_j及相似度sim累加到job_j的预测分上。最后在Reducer或Map内部完成排序取TopN输出。这块文字很多需要控制篇幅但又要详细。每个Job给代码片段Job1代码简版public class UserVectorJob { public static class Map extends MapperLongWritable, Text, Text, Text { // 输入: user_id \t job_id \t rating public void map(LongWritable key, Text value, Context context) { String[] parts value.toString().split(\t); context.write(new Text(parts[0]), new Text(parts[1] : parts[2])); } } public static class Reduce extends ReducerText, Text, Text, Text { public void reduce(Text key, IterableText values, Context context) { StringBuilder sb new StringBuilder(); for (Text v : values) { sb.append(v.toString()).append(,); } context.write(key, new Text(sb.toString())); } } }Job2public class SimilarityJob { public static class Map extends MapperLongWritable, Text, Text, Text { // 输入: user_id \t job1:r1, job2:r2, ... public void map(LongWritable key, Text value, Context context) { String[] parts value.toString().split(\t); String[] items parts[1].split(,); for (int i 0; i items.length; i) { String[] fi items[i].split(:); double ri Double.parseDouble(fi[1]); context.write(new Text(LEN: fi[0]), new Text(String.valueOf(ri * ri))); for (int j i 1; j items.length; j) { String[] fj items[j].split(:); double rj Double.parseDouble(fj[1]); context.write(new Text(fi[0] : fj[0]), new Text(String.valueOf(ri * rj))); } } } } }注意键用了LEN:前缀区分模长累加和共现累加Reduce时分别处理。这样设计虽然简单但会把两类数据shuffle到一起实际生产可以考虑用多输出但对课程设计完全够用。Job3public class RecommendJob { public static class Map extends MapperLongWritable, Text, Text, Text { // setup阶段从DistributedCache加载相似度矩阵 job_i - job_j:sim列表 // map输入: user_id \t job_id:rating // 对每个已评分岗位遍历相似岗位累加预测分 } // 最后对每个用户排序输出TopN }三个Job串联我用一个shell脚本按顺序执行hadoop jar project.jar UserVectorJob /input/behavior /output/user_vector hadoop jar project.jar SimilarityJob /output/user_vector /output/similarity hadoop jar project.jar RecommendJob -files /output/similarity/part-r-00000 /output/user_vector /output/recommend这里用-files把相似度矩阵传到DistributedCache主输入是用户行为正排表。跑完之后/output/recommend目录下每个文件里就是user_id \t job1:score, job2:score。6. 推荐效果评估、冷启动与数据倾斜上线前必须处理的三个问题6.1 离线评估用切分数据算召回率和精确率推荐系统做完了怎么知道效果好不好就业推荐场景最常用的离线评估方式是把用户行为数据按时间切分80%作为训练集20%作为测试集。注意必须按时间切不能随机切否则就是用未来的行为预测过去评估结果会虚高。我用过并且推荐你用的三个指标精确率推荐列表中有多少是用户真正感兴趣的在测试集中有行为的岗位召回率测试集中用户感兴趣的岗位有多少被推荐出来了覆盖率推荐系统覆盖了多少不同岗位覆盖率太低说明推荐结果太头部大家都在推热门岗位缺乏个性化。公式不复杂对每个测试用户取TopN推荐列表统计命中个数除以N得到精确率除以该用户在测试集中的行为数得到召回率。全用户平均后就是系统整体指标。我当时跑了一版纯余弦相似度的结果Top10精确率大概在12%左右看着不高但招聘场景行为稀疏这个数字已经能体现个性化价值。把规则过滤加上后虽然召回率变化不大但用户反馈的推荐结果可接受度明显提升因为推出来的岗位没有已经投过的了。6.2 冷启动新用户和新岗位都不能裸奔做过推荐系统的人都知道协同过滤最大的软肋就是冷启动。新用户没有任何评分和收藏行为算法完全无法计算他的偏好向量新岗位没有任何用户行为它和其他岗位的相似度全是0永远不会被推荐出去。就业场景里这两类问题都很致命用户第一次注册求职时系统要是给他推空列表他可能直接就走了。我采用的兜底策略很简单也有效新用户用热门岗位兜底但不是纯按收藏量排序而是按收藏量岗位新鲜度城市匹配综合分排序。比如用户注册时填写了期望城市和期望岗位类别就按城市和类别过滤后再按热度排序这样至少做到粗粒度个性化新岗位利用岗位本身的属性做相似。岗位属性包括职位类别、技能标签、城市、行业把岗位映射成一个属性向量用属性向量与已有岗位做相似度匹配。这个思路也叫基于内容的推荐与协同过滤组合使用恰好互补周期性冷启动每天晚上定时任务里除了跑协同过滤还要单独算一份热门岗位列表存到Redis或MySQL里。凡是协同过滤没算出结果的用户直接读取这份热门列表兜底。冷启动问题必须在项目开始时就规划好否则等到测试阶段发现大量用户推荐为空再补方案就很被动了。6.3 数据倾斜热门岗位为什么会拖垮整个任务我在跑Job2时遇到过典型的MapReduce数据倾斜问题几百个热门岗位被成千上万的用户收藏评分而绝大多数长尾岗位只有零星几个行为。把岗位对聚合到Reduce端时那些包含热门岗位的键会堆积巨大的累加量导致某个Reduce任务要处理其他任务几十倍的数据整个Job卡在那里。我用的止损方案有两个你可以组合使用过滤超级热门岗位如果某个岗位的行为数超过总行为数的某个阈值比如总记录数的5%在预处理阶段就单独拎出来不走协同过滤直接算入热门推荐。这样既减轻了倾斜又保证了冷启动兜底有内容加盐拆分热点键对热点岗位对在Map输出Key里加一个随机前缀把同一个Reduce Key拆到多个Reduce任务并行计算最后再合并结果。加盐能有效提速但会增加归并复杂度课程设计里建议先用第一种方案简化处理。还有一个常见的性能问题相似度矩阵太大了。10万个岗位两两相似就有50亿对就算只存相似度不为0的稀疏表示也非常大。我的做法是只保留每个岗位相似度最高的Top 50作为最终相似度矩阵再拿去生成推荐。这样既压缩了存储又比用全部相似岗位做加权更准确——那些相似度极低的岗位对本质上全是噪声留着只会干扰排序。6.4 就业场景特有校验推荐结果必须能投最后提醒一个算法之外、但直接决定项目能不能落地的点推荐结果必须经过业务校验。我在Job3输出推荐列表后又写了一个过滤脚本对每条推荐做三重检查岗位状态必须是上架中的、岗位所在地必须匹配用户期望城市、岗位薪资必须落在用户可接受区间。这一步看起来和算法无关但它恰恰是就业推荐系统和普通商品推荐系统的最大区别。商品推荐推错了大不了不买岗位推荐推了个已招满或者地点不符的岗位用户对系统的信任会瞬间归零。把过滤结果做成推荐理由的备注字段比如因为您收藏了Java开发工程师岗位所以推荐相似的前端开发工程师岗位推荐体验会更有说服力。最后再分享一个实际体会这类项目最容易出彩的地方往往不在算法新意而在于数据治理和工程细节的完整度。我第一版跑完推荐结果怎么看怎么奇怪最后定位到的原因就是岗位状态没过滤用户收藏的大量岗位早已失效算法在僵尸岗位上学习出了完全错误的相似关系。把数据清洗干净、把规则加对以后整个系统的效果是肉眼可见的变好。如果你也在做基于Hadoop的协同过滤就业推荐系统我真心建议你把至少一半的精力放在数据质量和业务过滤上算法再朴素只要数据干净、规则合理跑出来的结果就足够让人眼前一亮。
返回列表