在Spark UDF中训练LogisticRegression的技术问题咨询
Spark Scala代码运行疑问解答
运行代码
import org.apache.spark.sql.SparkSession import org.apache.spark.ml.classification._ object Hello { def main(args: Array[String]) = { val getLabel1Probability = udf((param1: Double, labeledEntries: Seq[Array[Double]]) => { val trainingData = labeledEntries.map(entry => (org.apache.spark.ml.linalg.Vectors.dense(entry(0)), entry(1))).toList.toDF("features", "label") val regression = new LogisticRegression() val fittingModel = regression.fit(trainingData) val prediction = fittingModel.predictProbability(org.apache.spark.ml.linalg.Vectors.dense(param1)) val probability = prediction.toArray(1) probability }) val df = Seq((1.0, Seq(Array(1.0, 0), Array(2.0, 1))), (3.0, Seq(Array(1.0, 0), Array(2.0, 1)))).toDF("Param1", "LabeledEntries") val dfWithLabel1Probability = df.withColumn( "Label1Probability", getLabel1Probability( $"Param1", $"LabeledEntries" ) ) display(dfWithLabel1Probability) } } Hello.main(Array())
上述代码在Databricks多节点集群(DBR 13.2,Spark 3.4.0,Scala 2.12)中运行成功,能正常展示dfWithLabel1Probability的数据。
疑问解答
1. UDF内创建DataFrame未触发NullPointerException的原因
在标准Spark环境中,UDF内创建DataFrame确实可能因_sqlContext为空触发NPE,但Databricks notebook环境做了特殊优化:
- Databricks会在集群节点上预初始化Spark上下文相关实例,包括
SQLContext,UDF执行时可直接复用这些已初始化的对象,因此不会出现空指针。 - 这种行为不具有不确定性,是Databricks环境的固有特性,但在标准Spark集群(如自建YARN集群)中运行相同代码,大概率会触发NPE——因为Worker节点没有预初始化的上下文,UDF内无法获取有效的
SQLContext创建DataFrame。
2. 基于DataFrame列数据训练LogisticRegression的替代方案
UDF内训练模型违背Spark设计原则,不仅可能引发上下文问题,还会导致每个Worker重复训练模型,效率极低,且无法利用Spark分布式计算能力。针对百万行数据场景,推荐以下两种方案:
方案一:全局训练+批量预测(适用于所有样本共享同一训练数据集的场景)
如果所有行的LabeledEntries是相同的训练数据,直接在Driver端全局训练模型,再用UDF或内置预测API批量处理所有Param1:
// 提取全局训练数据(假设所有行的LabeledEntries一致) val globalTrainingData = df.select($"LabeledEntries").first().getAs[Seq[Array[Double]]](0) val trainingDF = globalTrainingData.map(entry => (org.apache.spark.ml.linalg.Vectors.dense(entry(0)), entry(1))).toList.toDF("features", "label") // 全局训练模型 val lr = new LogisticRegression() val model = lr.fit(trainingDF) // 定义预测UDF(仅做预测,不训练) val predictProbUdf = udf((param: Double) => { val vec = org.apache.spark.ml.linalg.Vectors.dense(param) model.predictProbability(vec).toArray(1) }) // 批量预测 val resultDF = df.withColumn("Label1Probability", predictProbUdf($"Param1")) display(resultDF)
方案二:分组训练+分布式预测(适用于每行有独立训练数据集的场景)
如果每行的LabeledEntries是独立训练数据,可使用mapGroups实现分布式训练,避免将所有数据加载到Driver内存:
import org.apache.spark.sql.Row import org.apache.spark.sql.types._ // 定义结果Schema val resultSchema = StructType(df.schema.fields ++ Array(StructField("Label1Probability", DoubleType))) // 生成唯一标识后转换为RDD处理 val resultRDD = df.withColumn("id", monotonically_increasing_id()) .rdd .map(row => (row.getLong(2), (row.getDouble(0), row.getAs[Seq[Array[Double]]](1)))) .mapGroups { case (id, iter) => val (param1, labeledEntries) = iter.next() // 分布式训练模型 val trainingData = labeledEntries.map(entry => (org.apache.spark.ml.linalg.Vectors.dense(entry(0)), entry(1))) val trainingDF = spark.createDataFrame(trainingData).toDF("features", "label") val lr = new LogisticRegression() val model = lr.fit(trainingDF) // 预测概率 val prob = model.predictProbability(org.apache.spark.ml.linalg.Vectors.dense(param1)).toArray(1) // 返回结果行 Row(id, param1, labeledEntries, prob) } // 转换回DataFrame val resultDF = spark.createDataFrame(resultRDD, resultSchema) display(resultDF)
该方案将训练任务分发到各个Worker节点执行,适合百万行级别的数据处理。
内容的提问来源于stack exchange,提问作者Juan Carlos Blanco Martínez
相关产品推荐
相关产品推荐

