资讯动态

基于Hadoop与Spark的电影用户性别预测分类流水线实战

发布时间:2026/10/14 14:32:35 来源:尧图企业网站定制
简介这份资源是基于KNN算法的Hadoop电影网站用户性别预测实现程序面向具备一定Java与Hadoop基础、希望实践大数据分类建模的学习者与开发者可用于课程设计、毕业设计或大数据实验场景。压缩包共78个文件约5.86MB包含28个java源码、30个class编译文件、5个jar包以及classpath、project、properties等工程配置另有dat数据文件与readme说明覆盖数据预处理、KNN数据计算与模型评价等完整环节。数据预处理阶段可打包成jar在Hadoop集群运行数据目录需上传至运行目录并修改文件路径KNN计算阶段改为本地目录直接运行即可因数据量较大作者实测运行约两小时。目前已有3037人学习下载读者可借此掌握从数据join、评分与用户数据拆分到KNN分类预测的全流程实现思路并参考现成工程结构与jar包快速复现实验、排查路径与运行配置问题。1. 电影网站用户性别预测从 Hadoop 日志到可落地的分类流水线电影网站上用户注册时性别是选填项运营侧却要靠性别做推荐冷启动和广告定向。真实场景里一个日活几十万的站点用户行为日志每天增量在 GB 级别单机 pandas 读一遍就爆内存。这个标题要解决的就是把散落在 HDFS 上的用户行为日志用 Hadoop 做清洗和特征聚合再喂给分类模型预测缺失性别。它适合有 Java 或 Python 基础、正在做课程设计或公司内数据平台建设的工程师。核心链路是 HDFS 存原始日志、MapReduce 或 Spark 做特征、模型做二分类。下面按「数据怎么进、特征怎么算、模型怎么训、坑怎么排」四段推进每一步都给可复现的命令和参数。2. 数据落 HDFS 与特征口径先把性别预测的输入定义清楚2.1 为什么性别预测不能直接拿原始日志开跑电影网站的用户行为日志通常长这样一行一条 JSON字段包括 user_id、movie_id、actionclick/collect/rate、timestamp、rating、device。性别标签只存在于注册表里且大量为空。直接拿原始日志做训练有两个致命问题一是同一用户的行为条数差异极大重度用户几千条、轻度用户几条模型会被高频用户带偏二是原始日志里没有聚合特征单条点击记录对性别几乎没有区分度。常见做法是先把行为按 user_id 聚合生成每个用户的统计画像再和注册表的性别标签 join。这一步在 Hadoop 上做好处是数据量再涨也不用改架构。特征口径要在动手前定死否则后面模型效果无法归因。我一般会定这几类行为频次点击、收藏、评分的次数、行为偏好各类型电影占比、时间习惯活跃时段分布、评分倾向平均分、方差。性别差异往往藏在偏好和评分倾向里频次类特征区分度反而有限。提示特征口径一旦确定写进配置或常量类不要散落在各个 Job 里否则换一批数据就对不上。2.2 用 MapReduce 做用户行为聚合的最小可跑示例假设原始日志在/movie/raw/下格式是每行一个 JSON。先写 Mapper 解析出 user_id 和关键行为字段Reducer 按 user_id 聚合。下面是最小可跑的 Java 代码骨架跑通后再加特征。// UserBehaviorMapper.java public class UserBehaviorMapper extends MapperLongWritable, Text, Text, Text { private ObjectMapper jsonMapper new ObjectMapper(); // Jackson private Text outKey new Text(); private Text outValue new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString().trim(); if (line.isEmpty()) return; try { JsonNode node jsonMapper.readTree(line); String userId node.path(user_id).asText(); String action node.path(action).asText(); String movieId node.path(movie_id).asText(); // 输出格式user_id - action:movieId outKey.set(userId); outValue.set(action : movieId); context.write(outKey, outValue); } catch (Exception e) { // 脏数据计数不要直接抛异常中断整个 Job context.getCounter(PARSE, BAD_JSON).increment(1); } } }// UserBehaviorReducer.java public class UserBehaviorReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { int click 0, collect 0, rate 0; SetString movieSet new HashSet(); for (Text v : values) { String[] parts v.toString().split(:, 2); String action parts[0]; String movieId parts.length 1 ? parts[1] : ; movieSet.add(movieId); switch (action) { case click: click; break; case collect: collect; break; case rate: rate; break; default: break; } } // 输出user_id \t click,collect,rate,distinct_movies String result click , collect , rate , movieSet.size(); context.write(key, new Text(result)); } }逻辑说明Mapper 只做解析和字段抽取不做任何聚合保证可并行Reducer 按 user_id 拿到该用户所有行为统计各动作次数和去重电影数。参数说明context.getCounter用来统计脏数据量跑完在 Job 日志里能看到方便判断数据质量movieSet用 HashSet 去重如果单用户行为条数极大比如超过十万要考虑用 HyperLogLog 近似去重否则 Reducer 内存会吃紧。打包提交命令# 编译打包 mvn clean package -DskipTests # 提交到 YARN hadoop jar user-profile.jar com.example.UserBehaviorJob \ -D mapreduce.job.reduces8 \ /movie/raw/ /movie/feature/user_behavior/mapreduce.job.reduces按数据量调一般 1GB 数据给 4 到 8 个 Reducer。输出目录不能预先存在否则 Job 直接失败这是新手最常踩的坑之一。2.3 把注册表 join 进来生成带标签的训练集聚合完行为特征后需要和注册表 join 拿到性别标签。注册表通常不大几十万到几百万行可以走分布式缓存DistributedCache在 Map 端完成 join避免 Reduce 端 shuffle。// LabelJoinMapper.java public class LabelJoinMapper extends MapperLongWritable, Text, Text, Text { private MapString, String userGender new HashMap(); Override protected void setup(Context context) throws IOException { // 从分布式缓存读取注册表 URI[] files DistributedCache.getCacheFiles(context.getConfiguration()); for (URI file : files) { try (BufferedReader br new BufferedReader( new InputStreamReader(new FileInputStream(file.getPath())))) { String line; while ((line br.readLine()) ! null) { String[] parts line.split(\t); if (parts.length 2) { userGender.put(parts[0], parts[1]); // user_id - gender } } } } } Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 输入是上一步的输出user_id \t click,collect,rate,distinct_movies String[] parts value.toString().split(\t); if (parts.length 2) return; String userId parts[0]; String gender userGender.get(userId); if (gender null || gender.isEmpty()) return; // 无标签样本丢弃 context.write(new Text(gender), new Text(parts[1])); } }逻辑说明setup 阶段把注册表加载进内存map 阶段对每条行为特征查表打标签。参数说明注册表通过-files参数分发命令里写-files hdfs:///movie/raw/register.tsv#register.tsv。注意性别标签只保留 M/F 两类其他值如空、未知在 setup 里就过滤掉否则模型会多出无意义类别。这一步输出就是训练集的原始格式后续可以拉到本地或直接在 Spark 里读。3. 特征工程与模型训练从 HDFS 特征到性别分类器3.1 在 Spark 里做特征向量化和类别平衡Hadoop 聚合出的特征是数值型但直接丢给逻辑回归效果一般因为各特征量纲差异大且性别样本往往不平衡男性用户可能占七成。常见做法是在 Spark 里做标准化和重采样。下面用 PySpark 写一段可跑的脚本。from pyspark.sql import SparkSession from pyspark.ml.feature import VectorAssembler, StandardScaler from pyspark.ml.classification import LogisticRegression from pyspark.ml.evaluation import BinaryClassificationEvaluator from pyspark.sql.functions import col, when spark SparkSession.builder \ .appName(GenderPredict) \ .config(spark.sql.shuffle.partitions, 200) \ .getOrCreate() # 读取 HDFS 上的特征格式gender \t click,collect,rate,distinct_movies df spark.read.csv(hdfs:///movie/feature/labeled/, sep\t) \ .toDF(gender, features_raw) # 拆分特征列 from pyspark.sql.functions import split df df.withColumn(feat_arr, split(col(features_raw), ,)) for i, name in enumerate([click, collect, rate, distinct_movies]): df df.withColumn(name, col(feat_arr).getItem(i).cast(double)) # 标签编码M - 1.0, F - 0.0 df df.withColumn(label, when(col(gender) M, 1.0).otherwise(0.0)) # 向量化 assembler VectorAssembler( inputCols[click, collect, rate, distinct_movies], outputColfeatures_raw_vec ) df assembler.transform(df) # 标准化 scaler StandardScaler(inputColfeatures_raw_vec, outputColfeatures, withMeanTrue, withStdTrue) scalerModel scaler.fit(df) df scalerModel.transform(df) # 类别平衡对少数类过采样 major df.filter(col(label) 1.0) minor df.filter(col(label) 0.0) ratio major.count() / max(minor.count(), 1) minor_over minor.sample(withReplacementTrue, fractionratio, seed42) balanced major.union(minor_over) # 训练 train, test balanced.randomSplit([0.8, 0.2], seed42) lr LogisticRegression(maxIter100, regParam0.01, elasticNetParam0.0) model lr.fit(train) # 评估 predictions model.transform(test) evaluator BinaryClassificationEvaluator(metricNameareaUnderROC) auc evaluator.evaluate(predictions) print(AUC , auc)逻辑说明先拆分 CSV 特征列并转 double再做标签编码和向量化标准化消除量纲影响过采样缓解类别不平衡最后训练逻辑回归并输出 AUC。参数说明regParam控制正则强度特征少时 0.01 到 0.1 都合理elasticNetParam为 0 表示纯 L2 正则spark.sql.shuffle.partitions默认 200小数据可以降到 20 减少调度开销。AUC 低于 0.6 说明特征区分度不够要回去补特征而不是调模型参数。3.2 特征重要性怎么看别让模型当黑匣子逻辑回归的系数可以直接解释。训练完打印model.coefficients对应四个特征的权重。如果某个特征权重接近零说明它对性别预测没贡献可以考虑删掉。更直观的做法是用随机森林看特征重要性。from pyspark.ml.classification import RandomForestClassifier rf RandomForestClassifier(numTrees100, maxDepth5, seed42) rfModel rf.fit(train) print(Feature importances:, rfModel.featureImportances)featureImportances返回一个稀疏向量索引对应输入特征顺序。我一般会把结果和业务直觉对照如果「收藏次数」权重远高于「点击次数」说明收藏行为更能区分性别后续可以围绕收藏做更细的特征比如收藏电影的类型分布。这一步是血泪经验——不看特征重要性直接调参等于蒙眼开车。3.3 模型持久化与离线打分训练好的模型要存下来供后续批量打分。Spark 模型用save写到 HDFS加载时用对应类的load。# 保存 model.save(hdfs:///movie/model/gender_lr/) # 加载并打分 from pyspark.ml.classification import LogisticRegressionModel loaded LogisticRegressionModel.load(hdfs:///movie/model/gender_lr/) scored loaded.transform(df) scored.select(user_id, prediction, probability).write \ .mode(overwrite).parquet(hdfs:///movie/output/gender_pred/)参数说明保存路径不要预先创建Spark 会自己建目录打分结果存 parquet 比 CSV 省空间且带 schema。离线打分适合每天跑一次批任务把预测性别回写到用户画像表。如果要做实时得换在线特征存储和模型服务那是另一个话题。4. 避坑与排查Hadoop 性别预测流水线最常见的 5 个翻车点4.1 现象Job 卡在 Map 100% Reduce 0% 不动原因数据倾斜。某个 user_id 行为条数极多所有记录落到同一个 Reducer其他 Reducer 早早跑完它还在慢慢磨。解决在 Mapper 输出 key 上加随机后缀打散比如user_id _ rand(0,9)Reducer 里再去掉后缀聚合或者改用 Spark 的repartition加盐。判断方法看 Job 计数器里各 Reduce 的输入记录数差一个数量级就是倾斜。4.2 现象AUC 一直在 0.5 附近模型等于瞎猜原因特征里混入了 user_id 或 movie_id 这类高基数 ID 特征模型学到的是记忆而不是泛化或者标签 join 时 user_id 格式不一致一边是字符串一边是数字导致大量样本标签为空被丢弃。解决检查 join 后的样本量如果比预期少很多先排查格式特征里坚决不放原始 ID只放聚合统计量。4.3 现象提交 Job 报错 Could not find or load main class原因打包时没有把依赖打进去或者hadoop jar指定的类名和实际包路径不一致。解决用 Maven Shade 插件打 fat jar命令里类名写全限定名比如com.example.UserBehaviorJob。另外注意 Hadoop 集群的 Java 版本要和编译版本一致本地用 JDK 17 编译、集群是 JDK 8跑起来会报 UnsupportedClassVersionError。4.4 现象输出目录已存在导致 Job 直接失败原因Hadoop 默认不允许覆盖已存在的输出目录这是保护机制。解决提交前先hadoop fs -rm -r /movie/feature/user_behavior/或者在代码里用FileOutputFormat.setOutputPath前先判断并删除。不要图省事改成允许覆盖容易误删上游数据。4.5 现象Spark 读 HDFS 特征时报 NumberFormatException原因Hadoop 聚合输出里混入了脏数据比如某行只有 user_id 没有特征或者特征列里有空字符串。解决在 Spark 读取后用filter过滤掉特征列数为 0 或转换失败的行同时回到 MapReduce 阶段加计数器统计脏数据比例。如果脏数据超过 5%要先修数据源而不是在模型侧硬扛。5. 进阶技巧用性别预测概率做用户画像的软标签硬标签直接预测 M/F在性别缺失率高的场景下信息损失大更好的做法是输出概率把「男性概率 0.73」作为软标签写进画像。推荐系统里可以用这个概率做加权而不是二值切分。具体操作是在打分后不取prediction而是从probability向量里取正类分量。from pyspark.sql.functions import udf from pyspark.sql.types import DoubleType from pyspark.ml.linalg import VectorUDT # 提取正类概率 extract_prob udf(lambda v: float(v[1]), DoubleType()) scored scored.withColumn(male_prob, extract_prob(probability)) scored.select(user_id, male_prob).write \ .mode(overwrite).parquet(hdfs:///movie/output/gender_soft/)验证软标签是否靠谱可以按male_prob分桶看每桶里真实性别为 M 的比例是否单调递增。如果 0.5 到 0.6 桶的真实男性比例反而低于 0.4 到 0.5 桶说明模型排序能力有问题要回去检查特征和采样。这个验证方法比单看 AUC 更能暴露问题。另一个技巧是把性别预测和电影类型偏好做交叉特征。比如「男性概率 × 动作片点击占比」这类交叉特征在树模型里往往比原始特征更有区分度。我一般会在 Spark 里用VectorAssembler把交叉项拼进去再训一版随机森林对比 AUC。如果提升不到 0.02就不值得增加线上复杂度。最后说个习惯每次跑完流水线把 AUC、特征重要性、脏数据比例三个指标记到一张表里按天对比。模型效果突然掉先看脏数据比例是不是涨了再看特征分布是不是漂了。这个习惯帮我省过好几次通宵排查。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑