関数型プログラミング(特にScalaとScala API)でのreduceとfoldLeft / foldの違いは?


回答:


260

縮小対折りたたみ左

このトピックに関連する他のスタックオーバーフローの回答で明確に言及されていない大きな大きな違いはreduce可換モノイド、つまり可換性と連想性の両方を備えた演算を指定する必要があることです。これは、操作を並列化できることを意味します。

この区別は、ビッグデータ/ MPP /分散コンピューティングにとって非常に重要であり、reduceそれが存在する理由のすべてです。コレクションは細かく分割でき、はreduce各チャンクreduceを操作できます。その後、は各チャンクの結果を操作できます。実際、チャンクのレベルは1レベル深く停止する必要はありません。各チャンクも切り刻むことができました。これが、CPUの数が無制限の場合、リスト内の整数の合計がO(log N)になる理由です。

あなただけの署名を見れば理由はないreduceあなたとあなたができることはすべてを達成することができますので、存在しては、reducefoldLeft。の機能はの機能foldLeftよりも優れていますreduce

ただし、を並列化することはできないfoldLeftため、その実行時間は常にO(N)です(可換モノイドをフィードする場合でも)。これは、演算が可換モノイドではないと想定されているため、累積値は一連の連続した集計によって計算されるためです。

foldLeft可換性や結合性は想定していません。コレクションを切り分ける機能を提供するのは連想性であり、順序は重要ではないため(つまり、各チャンクからの各結果をどの順序で集約するかは関係ありません)、累積を容易にする交換性です。厳密に言えば、たとえば分散ソートアルゴリズムなどの並列化には交換性は必要ありません。チャンクに順序を付ける必要がないため、ロジックが簡単になります。

Sparkのドキュメントを見ると、reduce具体的には「...可換および結合二項演算子」と書かれています。

http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD

これreduceは、単なる特殊なケースではない証明ですfoldLeft

scala> val intParList: ParSeq[Int] = (1 to 100000).map(_ => scala.util.Random.nextInt()).par

scala> timeMany(1000, intParList.reduce(_ + _))
Took 462.395867 milli seconds

scala> timeMany(1000, intParList.foldLeft(0)(_ + _))
Took 2589.363031 milli seconds

縮小対折りたたみ

ここで、FP /数学のルーツに少し近づき、説明するのが少し難しくなります。Reduceは、秩序のないコレクション(マルチセット)を扱うMapReduceパラダイムの一部として正式に定義されています。Foldは再帰に関して正式に定義されており(カタモフィズムを参照)、コレクションの構造/シーケンスを想定しています。

fold(厳密な)Map Reduceプログラミングモデルではfoldチャンクに順序がfoldなく、連想性ではなく連想性のみが必要であるため定義できないため、Scaldingにはメソッドはありません。

簡単に言えば、reduce累積の順序なしで機能し、累積foldの順序が必要であり、ゼロの値を必要とするのはその累積の順序であり、それらを区別するゼロの値の存在ではありません。厳密に言えreduce 、空のコレクションで動作するはずです。そのゼロ値は、任意の値を取得xしてから解決することで推定できるためですが、x op y = x非可換演算では、左と右の異なるゼロ値が存在する可能性があるため、動作しません(すなわちx op y != y op x)。もちろん、Scalaはこのゼロの値が何であるかを計算する必要はありません。そのため、いくつかの数学(おそらく計算不可能)を実行する必要があるため、例外をスローします。

プログラミングの明らかな違いはシグネチャだけなので、(語源学ではよくあることですが)この元の数学的意味が失われているようです。結果は、MapReduceの元の意味を保持するのではなくreduce、の同義語になりましたfold。現在、これらの用語はしばしば互換的に使用され、ほとんどの実装で同じように動作します(空のコレクションは無視されます)。奇妙さは、Sparkのように、これから取り上げる特異性によって悪化します。

したがって、Sparkにがありますfoldが、サブ結果(パーティションごとに1つ)が(書き込み時に)結合される順序は、タスクが完了する順序と同じであり、したがって非決定的です。をfold使用していることを指摘してくれた@CafeFeedに感謝しrunJobます。さらに混乱を持っていないスパークによって作成されたtreeReduceが、何をtreeFold

結論

空ではないシーケンスに適用された場合でも、適用された場合でも違いがreduceありfoldます。前者は、任意の順序でコレクションのMapReduceプログラミングパラダイム(http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf)の一部として定義されており、演算子は連想的で確定的な結果を提供します。後者は、カモモーフィズムの観点から定義されており、コレクションにシーケンスの概念がある(またはリンクリストのように再帰的に定義される)必要があるため、可換演算子は必要ありません。

実際に起因するプログラミングのunmathematical性質のために、reduceそしてfoldどちらかが正しく(Scalaの中など)または誤っ(スパークのように)、同じように動作する傾向があります。

Extra:Spark APIに関する私の意見

私の意見ではfold、Sparkで用語の使用が完全に削除された場合、混乱は回避されます。少なくともSparkのドキュメントには注記があります。

これは、Scalaのような関数型言語の非分散型コレクションに実装された折りたたみ操作とは多少動作が異なります。


2
そのfoldLeftためLeft、その名前にが含まれ、と呼ばれるメソッドもありfoldます。
キリツク2014

1
@Cloudtechこれは、仕様の範囲内ではなく、シングルスレッド実装の偶然です。4コアマシンでを追加しようとすると.par(List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)毎回異なる結果が得られます。
samthebest 2014

2
コンピュータサイエンスのコンテキストでは@AlexDean。空のコレクションは例外をスローする傾向があるため、IDは実際には必要ありません。ただし、コレクションが空の場合にアイデンティティー要素が返されると、数学的にはエレガントになります(コレクションがこれを行うとよりエレガントになります)。数学では「例外を投げる」ことは存在しません。
samthebest 2014年

3
@samthebest:可換性について確信がありますか?github.com/apache/spark/blob/…は、「可換ではない関数の結果は、非分散コレクションに適用されたフォールドの結果とは異なる場合がある」と述べています。
Make42 2016年

1
@ Make42正解ですが、次のように独自のreallyFoldヒモを書くこともできますrdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)
samthebest 2016年

10

Spark APIがそれを要求していなくても、私が間違っていなければ、foldはfが可換であることも要求します。パーティションが集約される順序は保証されていないためです。たとえば、次のコードでは、最初の出力のみがソートされます。

import org.apache.spark.{SparkConf, SparkContext}

object FoldExample extends App{

  val conf = new SparkConf()
    .setMaster("local[*]")
    .setAppName("Simple Application")
  implicit val sc = new SparkContext(conf)

  val range = ('a' to 'z').map(_.toString)
  val rdd = sc.parallelize(range)

  println(range.reduce(_ + _))
  println(rdd.reduce(_ + _))
  println(rdd.fold("")(_ + _))
}  

プリントアウト:

abcdefghijklmnopqrstuvwxyz

abcghituvjklmwxyzqrsdefnop

defghinopjklmqrstuvabcwxyz


何度か行った後、私たちはあなたが正しいと信じています。組み合わせる順序は先着順です。sc.makeRDD(0 to 9, 2).mapPartitions(it => { java.lang.Thread.sleep(new java.util.Random().nextInt(1000)); it } ).map(_.toString).fold("")(_ + _)2+コアで数回実行すると、ランダム(パーティションごと)の順序で生成されることがわかります。私はそれに応じて私の答えを更新しました。
samthebest 2016年

3

foldApache Sparkの場合は、fold非分散コレクションの場合とは異なります。実際、確定的な結果を生成するには可換関数が必要です。

これは、Scalaのような関数型言語の非分散型コレクションに実装された折りたたみ操作とは多少動作が異なります。この折りたたみ操作は、パーティションに個別に適用し、定義された順序で各要素に順に折りたたむのではなく、それらの結果を最終結果に折りたたむことができます。可換ではない関数の場合、結果は非分散コレクションに適用されるフォールドの結果と異なる場合があります。

これMishael Rosenthalによって示され、Make42 のコメントで示唆されています。

観察された動作はHashPartitioner、実際にparallelizeはシャッフルせず、を使用しない場合に関連していることが示唆されていますHashPartitioner

import org.apache.spark.sql.SparkSession

/* Note: standalone (non-local) mode */
val master = "spark://...:7077"  

val spark = SparkSession.builder.master(master).getOrCreate()

/* Note: deterministic order */
val rdd = sc.parallelize(Seq("a", "b", "c", "d"), 4).sortBy(identity[String])
require(rdd.collect.sliding(2).forall { case Array(x, y) => x < y })

/* Note: all posible permutations */
require(Seq.fill(1000)(rdd.fold("")(_ + _)).toSet.size == 24)

説明:

foldRDDの構造

def fold(zeroValue: T)(op: (T, T) => T): T = withScope {
  var jobResult: T
  val cleanOp: (T, T) => T
  val foldPartition = Iterator[T] => T
  val mergeResult: (Int, T) => Unit
  sc.runJob(this, foldPartition, mergeResult)
  jobResult
}

RDDの構造とreduce同じです。

def reduce(f: (T, T) => T): T = withScope {
  val cleanF: (T, T) => T
  val reducePartition: Iterator[T] => Option[T]
  var jobResult: Option[T]
  val mergeResult =  (Int, Option[T]) => Unit
  sc.runJob(this, reducePartition, mergeResult)
  jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}

where runJobは、パーティションの順序を無視して実行され、結果として可換関数が必要になります。

foldPartitionおよびreducePartitionによって処理され、効果的に(継承と委任によって)reduceLeftおよびによって実装さfoldLeftTraversableOnceます。

結論:foldRDDでは、チャンクの順序に依存できず、交換性と結合性が必要です。


語源が紛らわしく、プログラミング文献が正式な定義に欠けていることを認めざるを得ません。foldon RDDは確かにとまったく同じであると言っても安全だと思いますreduceが、これは根の数学的違いを尊重していません(私は答えをさらに明確にするために更新しました)。パーティショナーが何をしていても自信があるという前提で交換性が本当に必要であることに私は同意しませんが、それは秩序を守っています。
samthebest 2016

未定義の折りたたみの順序は、パーティショニングとは関係ありません。これは、runJob実装の直接的な結果です。

ああ!申し訳ありませんが、ポイントを理解できませんでしたが、runJobコードを読んだところ、パーティションの順序ではなく、タスクがいつ終了したかに応じて結合が行われていることがわかりました。すべてが所定の場所に収まるようにするのは、この重要な詳細です。私はもう一度回答を編集して、指摘された間違いを修正しました。現在、合意に達しているため、賞金を削除していただけませんか?
samthebest 2016年

編集も削除もできません-そのようなオプションはありません。受賞はできますが、注意だけでもかなりのポイントを獲得できると思いますが、間違いですか?あなたが私に報酬を与えたいと思うなら、私はそれを24時間以内に行います。メソッドの訂正と申し訳ありませんが、すべての警告を無視したように見えました。これは大きなことであり、回答は至る所に引用されています。

1
@Mishael Rosenthalが懸念を明確に述べた最初の人物だったので、あなたにそれを授与してはどうでしょう。私はポイントには興味がなく、SEOと組織にSOを使用するのが好きです。
samthebest 2016年

2

Scaldingのもう1つの違いは、Hadoopでのコンバイナの使用です。

操作が可換モノイドであり、reduce使用して、reduceにすべてのデータをシャッフル/ソートするのではなく、reduceをマップ側に適用するとします。foldLeftこのケースではありません。

pipe.groupBy('product) {
   _.reduce('price -> 'total){ (sum: Double, price: Double) => sum + price }
   // reduce is .mapReduceMap in disguise
}

pipe.groupBy('product) {
   _.foldLeft('price -> 'total)(0.0){ (sum: Double, price: Double) => sum + price }
}

Scaldingでオペレーションをモノイドとして定義することは常に良い習慣です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.