web-dev-qa-db-ja.com

配列(つまり、リスト)列をベクターに変換する方法

質問のショートバージョン!

次のスニペットを検討してください(sparkが既にSparkSessionに設定されていると仮定):

from pyspark.sql import Row
source_data = [
    Row(city="Chicago", temperatures=[-1.0, -2.0, -3.0]),
    Row(city="New York", temperatures=[-7.0, -7.0, -5.0]), 
]
df = spark.createDataFrame(source_data)

温度フィールドはフロートのリストであることに注意してください。これらのフロートのリストをMLlibタイプVectorに変換したいのですが、RDDを経由するのではなく、基本的なDataFrame APIを使用してこの変換を表現したいです(JVMからPythonにすべてのデータを送信するため、処理はPythonで行われますが、SparkのCatalystオプティマイザー(yada yada)の利点は得られません)。どうすればいいですか?具体的には:

  1. ストレートキャストを機能させる方法はありますか?詳細(および回避策の試行失敗)については、以下を参照してください?または、私が後にした効果を持つ他の操作はありますか?
  2. 以下に提案する2つの代替ソリューションのうち、どちらがより効率的ですか(UDFとリスト内のアイテムの分解/再組み立て)。それとも、それらのどちらよりも優れている、ほとんどではないがほぼ正しい他の選択肢はありますか?

ストレートキャストが機能しない

これが私が「適切な」ソリューションになると期待するものです。列の型をある型から別の型に変換したいので、キャストを使用する必要があります。ちょっとしたコンテキストとして、別の型にキャストする通常の方法を思い出させてください。

from pyspark.sql import types
df_with_strings = df.select(
    df["city"], 
    df["temperatures"].cast(types.ArrayType(types.StringType()))),
)

今、例えばdf_with_strings.collect()[0]["temperatures"][1]'-7.0'です。しかし、ml Vectorにキャストすると、うまくいきません。

from pyspark.ml.linalg import VectorUDT
df_with_vectors = df.select(df["city"], df["temperatures"].cast(VectorUDT()))

これによりエラーが発生します。

pyspark.sql.utils.AnalysisException: "cannot resolve 'CAST(`temperatures` AS STRUCT<`type`: TINYINT, `size`: INT, `indices`: ARRAY<INT>, `values`: ARRAY<DOUBLE>>)' due to data type mismatch: cannot cast ArrayType(DoubleType,true) to org.Apache.spark.ml.linalg.VectorUDT@3bfc3ba7;;
'Project [city#0, unresolvedalias(cast(temperatures#1 as vector), None)]
+- LogicalRDD [city#0, temperatures#1]
"

うわぁ!これを修正する方法はありますか?

可能な代替案

代替1:VectorAssemblerを使用する

このジョブにほぼ理想的と思われるTransformerがあります: VectorAssembler 。 1つ以上の列を受け取り、それらを単一のベクトルに連結します。残念ながら、Vector列ではなく、Float列とArray列のみを使用するため、以下は機能しません。

from pyspark.ml.feature import VectorAssembler
assembler = VectorAssembler(inputCols=["temperatures"], outputCol="temperature_vector")
df_fail = assembler.transform(df)

このエラーが発生します:

pyspark.sql.utils.IllegalArgumentException: 'Data type ArrayType(DoubleType,true) is not supported.'

私が考えることができる最善の回避策は、リストを複数の列に分解し、VectorAssemblerを使用してそれらすべてを再度収集することです:

from pyspark.ml.feature import VectorAssembler
TEMPERATURE_COUNT = 3
assembler_exploded = VectorAssembler(
    inputCols=["temperatures[{}]".format(i) for i in range(TEMPERATURE_COUNT)], 
    outputCol="temperature_vector"
)
df_exploded = df.select(
    df["city"], 
    *[df["temperatures"][i] for i in range(TEMPERATURE_COUNT)]
)
converted_df = assembler_exploded.transform(df_exploded)
final_df = converted_df.select("city", "temperature_vector")

これは、TEMPERATURE_COUNTが100より大きく、時には1000より大きいことを除いて、理想的だと思われます(別の問題は、配列のサイズが事前にわからない場合、コードがより複雑になることです。 、それは私のデータには当てはまりません。)Sparkは実際にその数の列を持つ中間データセットを生成しますか、またはこれを個々のアイテムが一時的に通過する中間ステップと見なしますか(または実際に行います)これらの列の唯一の使用がベクトルにアセンブルされることがわかったときに、この離れたステップを完全に最適化しますか?)

代替2:UDFを使用する

かなり単純な代替手段は、UDFを使用して変換を行うことです。これにより、実行したいことを1行のコードで非常に直接表現できます。また、膨大な数の列を持つデータセットを作成する必要はありません。ただし、すべてのデータはPythonとJVMの間で交換する必要があり、個々の数値はすべてPythonで処理する必要があります(個々のデータ項目の反復処理で有名です)。これはどのように見えるかです:

from pyspark.ml.linalg import Vectors, VectorUDT
from pyspark.sql.functions import udf
list_to_vector_udf = udf(lambda l: Vectors.dense(l), VectorUDT())
df_with_vectors = df.select(
    df["city"], 
    list_to_vector_udf(df["temperatures"]).alias("temperatures")
)

無視できる発言

このとりとめのない質問の残りのセクションは、答えを見つけようとして私が思いついたいくつかの余分なものです。これを読んでいるほとんどの人はおそらくスキップすることができます。

解決策ではありません:Vectorを使用して開始します

この些細な例では、最初にベクタータイプを使用してデータを作成することができますが、もちろん私のデータは実際にはPythonリストではなく、並列化していますが、代わりにデータから読み取られていますソース。しかし、記録のために、これはどのように見えるかです:

from pyspark.ml.linalg import Vectors
from pyspark.sql import Row
source_data = [
    Row(city="Chicago", temperatures=Vectors.dense([-1.0, -2.0, -3.0])),
    Row(city="New York", temperatures=Vectors.dense([-7.0, -7.0, -5.0])),
]
df = spark.createDataFrame(source_data)

非効率的なソリューション:map()を使用

1つの可能性は、RDD map()メソッドを使用して、リストをVectorに変換することです。これは、UDFの考え方と似ていますが、操作対象のフィールドだけでなく、各行のすべてのフィールドでシリアル化などのコストが発生するため、さらに悪化します。記録のために、このソリューションは次のようになります。

df_with_vectors = df.rdd.map(lambda row: Row(
    city=row["city"], 
    temperatures=Vectors.dense(row["temperatures"])
)).toDF()

キャストの回避策の試行に失敗しました

必死になって、Vectorは4つのフィールドを持つ構造体によって内部的に表されていることに気付きましたが、そのタイプの構造体からの従来のキャストを使用しても機能しません。以下に図を示します(udfを使用して構造体を作成しましたが、udfは重要な部分ではありません)。

from pyspark.ml.linalg import Vectors, VectorUDT
from pyspark.sql.functions import udf
list_to_almost_vector_udf = udf(lambda l: (1, None, None, l), VectorUDT.sqlType())
df_almost_vector = df.select(
    df["city"], 
    list_to_almost_vector_udf(df["temperatures"]).alias("temperatures")
)
df_with_vectors = df_almost_vector.select(
    df_almost_vector["city"], 
    df_almost_vector["temperatures"].cast(VectorUDT())
)

これはエラーを与えます:

pyspark.sql.utils.AnalysisException: "cannot resolve 'CAST(`temperatures` AS STRUCT<`type`: TINYINT, `size`: INT, `indices`: ARRAY<INT>, `values`: ARRAY<DOUBLE>>)' due to data type mismatch: cannot cast StructType(StructField(type,ByteType,false), StructField(size,IntegerType,true), StructField(indices,ArrayType(IntegerType,false),true), StructField(values,ArrayType(DoubleType,false),true)) to org.Apache.spark.ml.linalg.VectorUDT@3bfc3ba7;;
'Project [city#0, unresolvedalias(cast(temperatures#5 as vector), None)]
+- Project [city#0, <lambda>(temperatures#1) AS temperatures#5]
+- LogicalRDD [city#0, temperatures#1]
"
51
Arthur Tacca

個人的にはPython UDFを使用しますが、他のことは一切気にしません。

  • VectorsはネイティブSQL型ではないため、何らかの方法でパフォーマンスのオーバーヘッドが発生します。特に、このプロセスでは、データが最初に 外部タイプから行に変換 、次に 一般的なRowEncoder を使用して行から内部表現に変換される2つのステップが必要です。
  • ダウンストリームML Pipelineは、単純な変換よりもはるかに高価です。さらに、上記のプロセスとは逆のプロセスが必要です

ただし、ここで他のオプションが本当に必要な場合は次のとおりです。

  • Pythonラッパーを使用したScala UDF:

    プロジェクトサイトの指示に従って sbt をインストールします。

    次の構造でScalaパッケージを作成します。

    .
    ├── build.sbt
    └── udfs.scala
    

    build.sbtを編集します(ScalaおよびSparkバージョンを反映するように調整します):

    scalaVersion := "2.11.8"
    
    libraryDependencies ++= Seq(
      "org.Apache.spark" %% "spark-sql" % "2.1.0",
      "org.Apache.spark" %% "spark-mllib" % "2.1.0"
    )
    

    編集udfs.scala

    package com.example.spark.udfs
    
    import org.Apache.spark.sql.functions.udf
    import org.Apache.spark.ml.linalg.DenseVector
    
    object udfs {
      val as_vector = udf((xs: Seq[Double]) => new DenseVector(xs.toArray))
    }
    

    パッケージ:

    sbt package
    

    含む(またはScalaに応じて同等):

    $PROJECT_ROOT/target/scala-2.11/udfs_2.11-0.1-SNAPSHOT.jar
    

    シェルの起動時/アプリケーションの送信時の--driver-class-pathの引数として。

    PySparkでラッパーを定義します。

    from pyspark.sql.column import _to_Java_column, _to_seq, Column
    from pyspark import SparkContext
    
    def as_vector(col):
        sc = SparkContext.getOrCreate()
        f = sc._jvm.com.example.spark.udfs.udfs.as_vector()
        return Column(f.apply(_to_seq(sc, [col], _to_Java_column)))
    

    テスト:

    with_vec = df.withColumn("vector", as_vector("temperatures"))
    with_vec.show()
    
    +--------+------------------+----------------+
    |    city|      temperatures|          vector|
    +--------+------------------+----------------+
    | Chicago|[-1.0, -2.0, -3.0]|[-1.0,-2.0,-3.0]|
    |New York|[-7.0, -7.0, -5.0]|[-7.0,-7.0,-5.0]|
    +--------+------------------+----------------+
    
    with_vec.printSchema()
    
    root
     |-- city: string (nullable = true)
     |-- temperatures: array (nullable = true)
     |    |-- element: double (containsNull = true)
     |-- vector: vector (nullable = true)
    
  • DenseVectorスキーマを反映するデータをJSON形式にダンプし、それを読み返します。

    from pyspark.sql.functions import to_json, from_json, col, struct, lit
    from pyspark.sql.types import StructType, StructField
    from pyspark.ml.linalg import VectorUDT
    
    json_vec = to_json(struct(struct(
        lit(1).alias("type"),  # type 1 is dense, type 0 is sparse
        col("temperatures").alias("values")
    ).alias("v")))
    
    schema = StructType([StructField("v", VectorUDT())])
    
    with_parsed_vector = df.withColumn(
        "parsed_vector", from_json(json_vec, schema).getItem("v")
    )
    
    with_parsed_vector.show()
    
    +--------+------------------+----------------+
    |    city|      temperatures|   parsed_vector|
    +--------+------------------+----------------+
    | Chicago|[-1.0, -2.0, -3.0]|[-1.0,-2.0,-3.0]|
    |New York|[-7.0, -7.0, -5.0]|[-7.0,-7.0,-5.0]|
    +--------+------------------+----------------+
    
    with_parsed_vector.printSchema()
    
    root
     |-- city: string (nullable = true)
     |-- temperatures: array (nullable = true)
     |    |-- element: double (containsNull = true)
     |-- parsed_vector: vector (nullable = true)
    
15
user6910411

私はあなたと同じ問題を抱えていたので、このようにしました。この方法にはRDD変換が含まれるため、パフォーマンスは重要ではありませんが、機能します。

from pyspark.sql import Row
from pyspark.ml.linalg import Vectors

source_data = [
    Row(city="Chicago", temperatures=[-1.0, -2.0, -3.0]),
    Row(city="New York", temperatures=[-7.0, -7.0, -5.0]), 
]
df = spark.createDataFrame(source_data)

city_rdd = df.rdd.map(lambda row:row[0])
temp_rdd = df.rdd.map(lambda row:row[1])
new_df = city_rdd.Zip(temp_rdd.map(lambda x:Vectors.dense(x))).toDF(schema=['city','temperatures'])

new_df

結果は、

DataFrame[city: string, temperatures: vector]
2
GGDammy