【问题标题】:Applying IndexToString to features vector in Spark将 IndexToString 应用于 Spark 中的特征向量
【发布时间】:2016-02-17 04:43:31
【问题描述】:

上下文:我有一个数据框,其中所有分类值都已使用 StringIndexer 进行索引。

val categoricalColumns = df.schema.collect { case StructField(name, StringType, nullable, meta) => name }    

val categoryIndexers = categoricalColumns.map {
  col => new StringIndexer().setInputCol(col).setOutputCol(s"${col}Indexed") 
}

然后我使用 VectorAssembler 对所有特征列(包括索引的分类列)进行向量化。

val assembler = new VectorAssembler()
    .setInputCols(dfIndexed.columns.diff(List("label") ++ categoricalColumns))
    .setOutputCol("features")

在应用分类器和一些额外的步骤后,我最终得到了一个包含标签、特征和预测的数据框。我想将我的特征向量扩展为单独的列,以便将索引值转换回其原始字符串形式。

val categoryConverters = categoricalColumns.zip(categoryIndexers).map {
colAndIndexer => new IndexToString().setInputCol(s"${colAndIndexer._1}Indexed").setOutputCol(colAndIndexer._1).setLabels(colAndIndexer._2.fit(df).labels)
}

问题:有没有一种简单的方法可以做到这一点,或者是最好的方法以某种方式将预测列附加到测试数据框?

我尝试过的:

val featureSlicers = categoricalColumns.map {
  col => new VectorSlicer().setInputCol("features").setOutputCol(s"${col}Indexed").setNames(Array(s"${col}Indexed"))
}

应用这个给了我我想要的列,但它们是矢量形式(正如它的意思)而不是双精度类型。

编辑: 所需的输出是原始数据框(即分类特征作为字符串而不是索引),并带有一个指示预测标签的附加列(在我的例子中是 0 或 1)。

例如,假设我的分类器的输出看起来像这样:

+-----+---------+----------+
|label| features|prediction|
+-----+---------+----------+
|  1.0|[0.0,3.0]|       1.0|
+-----+---------+----------+

通过在每个特征上应用 VectorSlicer,我会得到:

+-----+---------+----------+-------------+-------------+
|label| features|prediction|statusIndexed|artistIndexed|
+-----+---------+----------+-------------+-------------+
|  1.0|[0.0,3.0]|       1.0|        [0.0]|        [3.0]|
+-----+---------+----------+-------------+-------------+

这很好,但我需要:

+-----+---------+----------+-------------+-------------+
|label| features|prediction|statusIndexed|artistIndexed|
+-----+---------+----------+-------------+-------------+
|  1.0|[0.0,3.0]|       1.0|         0.0 |         3.0 |
+-----+---------+----------+-------------+-------------+

然后才能使用 IndexToString 并将其转换为:

+-----+---------+----------+-------------+-------------+
|label| features|prediction|    status   |    artist   |
+-----+---------+----------+-------------+-------------+
|  1.0|[0.0,3.0]|       1.0|        good |  Pink Floyd |
+-----+---------+----------+-------------+-------------+

甚至:

+-----+----------+-------------+-------------+
|label|prediction|    status   |    artist   |
+-----+----------+-------------+-------------+
|  1.0|       1.0|        good |  Pink Floyd |
+-----+----------+-------------+-------------+

【问题讨论】:

  • 我也有类似的问题。我有一个包含数千列或列的数据集,其中一些是分类的,因此我必须使用 StringIndexerOneHotEncoder 将它们“拆分”为更多列。当我试图理解什么代表组合向量的每个特征时,问题就来了。
  • 有什么理由首先丢弃输入数据?
  • 是的。分类算法需要一个带有“标签”和“特征”列的数据框。其中特征列是向量,不能有字符串。需要明确的是,我仍然拥有包含所有输入数据的原始数据框。
  • 如果您有原始数据,为什么要从那里提取此信息而不是矢量?我的意思是,为什么要麻烦一个小的 UDF?
  • 好奇心,主要是。此外,将一列从一个数据帧附加到另一个数据帧似乎并不像我预期的那么简单。

标签: scala apache-spark apache-spark-ml


【解决方案1】:

嗯,这不是一个非常有用的操作,但应该可以使用列元数据和简单的 UDF 来提取所需的信息。我假设你的数据已经创建了一个类似于这个的管道:

import org.apache.spark.ml.feature.{VectorSlicer, VectorAssembler, StringIndexer}
import org.apache.spark.ml.Pipeline

val df = sc.parallelize(Seq(
  (1L, "a", "foo", 1.0), (2L, "b", "bar", 2.0), (3L, "a", "bar", 3.0)
)).toDF("id", "x1", "x2", "x3")

val featureCols = Array("x1", "x2", "x3")
val featureColsIdx = featureCols.map(c => s"${c}_i")

val indexers = featureCols.map(
  c => new StringIndexer().setInputCol(c).setOutputCol(s"${c}_i")
)

val assembler = new VectorAssembler()
  .setInputCols(featureColsIdx)
  .setOutputCol("features")

val slicer = new VectorSlicer()
  .setInputCol("features")
  .setOutputCol("string_features")
  .setNames(featureColsIdx.init)


val transformed = new Pipeline()
  .setStages(indexers :+ assembler :+ slicer)
  .fit(df)
  .transform(df)

首先我们可以从特征中提取所需的元数据:

val meta = transformed.select($"string_features")
  .schema.fields.head.metadata
  .getMetadata("ml_attr") 
  .getMetadata("attrs")
  .getMetadataArray("nominal")

并将其转换为更易于使用的东西

case class NominalMetadataWrapper(idx: Long, name: String, vals: Array[String])

// In general it could a good idea to make it a broadcast variable
val lookup = meta.map(m => NominalMetadataWrapper(
  m.getLong("idx"), m.getString("name"), m.getStringArray("vals")
))

最后是一个小的 UDF:

import scala.util.Try

val transFeatures = udf((v: Vector) => lookup.map{
  m => Try(m.vals(v(m.idx.toInt).toInt)).toOption
})

transformed.select(transFeatures($"string_features")).

【讨论】:

  • 谢谢!虽然这不是最有用的操作,但知道如何执行此操作会有所帮助。
  • 说实话,我是出于好奇才回答的 :-)
  • 嗨@zero323 我尝试了您在答案中提供的代码,但我得到的特征名称也与特征值混合在一起?例如,如果我有一个功能“供应商”,其索引名称为“vendor_ml_input”,值为“microsoft”、“dell”等,我得到的功能名称为“vendor_ml_input_microsoft”和“vendor_ml_input_dell”,关于我该如何做的任何建议获取功能名称?
  • 对不起,我在 VectorAssembly 链中有 OneHotEncoder,一旦我删除它,这就像你说的那样工作。谢谢
猜你喜欢
  • 1970-01-01
  • 2016-02-25
  • 2022-01-01
  • 1970-01-01
  • 2020-11-08
  • 1970-01-01
  • 2015-06-30
  • 1970-01-01
  • 2020-10-23
相关资源
最近更新 更多