なぜScalaの火花や火傷のようなフレームワークには、両方を持っているかreduce
とfoldLeft
?それでは違い何reduce
とはfold
?
なぜScalaの火花や火傷のようなフレームワークには、両方を持っているかreduce
とfoldLeft
?それでは違い何reduce
とはfold
?
回答:
このトピックに関連する他のスタックオーバーフローの回答で明確に言及されていない大きな大きな違いはreduce
、可換モノイド、つまり可換性と連想性の両方を備えた演算を指定する必要があることです。これは、操作を並列化できることを意味します。
この区別は、ビッグデータ/ MPP /分散コンピューティングにとって非常に重要であり、reduce
それが存在する理由のすべてです。コレクションは細かく分割でき、はreduce
各チャンクreduce
を操作できます。その後、は各チャンクの結果を操作できます。実際、チャンクのレベルは1レベル深く停止する必要はありません。各チャンクも切り刻むことができました。これが、CPUの数が無制限の場合、リスト内の整数の合計がO(log N)になる理由です。
あなただけの署名を見れば理由はないreduce
あなたとあなたができることはすべてを達成することができますので、存在しては、reduce
とfoldLeft
。の機能はの機能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の中など)または誤っ(スパークのように)、同じように動作する傾向があります。
私の意見ではfold
、Sparkで用語の使用が完全に削除された場合、混乱は回避されます。少なくともSparkのドキュメントには注記があります。
これは、Scalaのような関数型言語の非分散型コレクションに実装された折りたたみ操作とは多少動作が異なります。
foldLeft
ためLeft
、その名前にが含まれ、と呼ばれるメソッドもありfold
ます。
.par
、(List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)
毎回異なる結果が得られます。
reallyFold
ヒモを書くこともできますrdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)
。
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+コアで数回実行すると、ランダム(パーティションごと)の順序で生成されることがわかります。私はそれに応じて私の答えを更新しました。
fold
Apache 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)
説明:
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
およびによって実装さfoldLeft
れTraversableOnce
ます。
結論:fold
RDDでは、チャンクの順序に依存できず、交換性と結合性が必要です。
fold
on RDD
は確かにとまったく同じであると言っても安全だと思いますreduce
が、これは根の数学的違いを尊重していません(私は答えをさらに明確にするために更新しました)。パーティショナーが何をしていても自信があるという前提で交換性が本当に必要であることに私は同意しませんが、それは秩序を守っています。
runJob
コードを読んだところ、パーティションの順序ではなく、タスクがいつ終了したかに応じて結合が行われていることがわかりました。すべてが所定の場所に収まるようにするのは、この重要な詳細です。私はもう一度回答を編集して、指摘された間違いを修正しました。現在、合意に達しているため、賞金を削除していただけませんか?
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でオペレーションをモノイドとして定義することは常に良い習慣です。