Neden Scala ve Spark ve Scalding gibi çerçevelerde hem reduceve hem de var foldLeft? Öyleyse reduceve arasındaki fark foldnedir?
Neden Scala ve Spark ve Scalding gibi çerçevelerde hem reduceve hem de var foldLeft? Öyleyse reduceve arasındaki fark foldnedir?
Yanıtlar:
Bu konuyla ilgili başka hiçbir yığın aşımı yanıtında açıkça belirtilmeyen büyük bir fark, değişmeli bir monoid , yani hem değişmeli hem de ilişkisel bir işlem reduceverilmesi gerektiğidir . Bu, işlemin paralelleştirilebileceği anlamına gelir.
Bu ayrım, Büyük Veri / MPP / dağıtılmış bilgi işlem için ve reducevar olmasının tüm nedeni için çok önemlidir . Koleksiyon parçalanabilir ve reducekutu her bir yığın üzerinde çalışabilir, daha sonra reducekutu her bir parçanın sonuçları üzerinde çalışabilir - aslında yığın seviyesinin bir seviye derinliğinde durması gerekmez. Her parçayı da doğrayabiliriz. Bu nedenle, sonsuz sayıda CPU verildiğinde, listedeki tam sayıların toplamının O (log N) olması gerekir.
Sadece imzalar bakarsak için hiçbir neden yoktur reduceBirlikte elinizden geleni elde edebilirsiniz, çünkü varlığını reducebir ile foldLeft. İşlevselliği, işlevselliğinden foldLeftdaha büyüktür reduce.
Ancak a'yı paralelleştiremezsiniz foldLeft, bu nedenle çalışma zamanı her zaman O (N) olur (değişmeli bir monoidle besleseniz bile). Bunun nedeni, işlemin değişmeli bir monoid olmadığı varsayılması ve bu nedenle birikmiş değerin bir dizi sıralı toplama tarafından hesaplanacak olmasıdır.
foldLeftdeğişme veya ilişkisellik varsaymaz. Koleksiyonun parçalanmasını sağlayan şey ilişkilendirilebilirliktir ve biriktirmeyi kolaylaştıran değişme özelliğidir, çünkü sıra önemli değildir (bu nedenle, her bir parçadan elde edilen sonuçların her birini hangi sırayla bir araya getirmenin önemi yoktur). Paralelleştirme için, örneğin dağıtılmış sıralama algoritmaları için kesinlikle değişme gerekli değildir, sadece mantığı kolaylaştırır çünkü parçalarınıza bir sıralama vermeniz gerekmez.
Spark belgelerine bir göz atarsanız, reduceözellikle "... değişmeli ve ilişkisel ikili operatör" diyor.
http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD
İşte reducesadece özel bir durum OLMAYAN kanıtı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
Şimdi burası FP / matematiksel köklere biraz daha yaklaşıyor ve açıklaması biraz daha zor. İndirgeme, sırasız koleksiyonlarla (çoklu kümeler) ilgilenen MapReduce paradigmasının bir parçası olarak resmi olarak tanımlanır, Fold resmi olarak özyineleme (katamorfizma bakın) açısından tanımlanır ve bu nedenle koleksiyonlar için bir yapı / sıra varsayar.
foldScalding'de bir yöntem yoktur , çünkü (katı) Map Reduce programlama modeli altında tanımlayamayız foldçünkü parçaların bir sıralaması yoktur ve foldsadece ilişkilendirilebilirlik gerektirir, komütatiflik gerektirir.
Basitçe ifade reduceetmek gerekirse, bir kümülâsyon sırası olmadan çalışır, bir kümülasyon sırası foldgerektirir ve onları ayıran sıfır değerinin varlığını DEĞİL, sıfır değerini gerektiren bu birikim sırasıdır. Kesin olarak konuşursak reduce , boş bir koleksiyon üzerinde çalışmalıdır, çünkü sıfır değeri, rastgele bir değer xalıp sonra çözerek çıkarılabilir x op y = x, ancak bu, farklı bir sol ve sağ sıfır değeri olabileceğinden, değişmeli olmayan bir işlemle çalışmaz. (yani x op y != y op x). Elbette Scala, bu sıfır değerinin ne olduğunu hesaplama zahmetine girmez, çünkü bu biraz matematik yapmayı gerektirir (muhtemelen hesaplanamaz), bu yüzden sadece bir istisna atar.
Görünüşe göre (etimolojide çoğu zaman olduğu gibi) bu orijinal matematiksel anlam kaybolmuştur, çünkü programlamadaki tek bariz fark imzadır. Sonuç, MapReduce'tan orijinal anlamını korumak yerine reducebunun eşanlamlısı haline geldi fold. Şimdi bu terimler genellikle birbirinin yerine kullanılır ve çoğu uygulamada aynı şekilde davranır (boş koleksiyonları göz ardı ederek). Tuhaflık, Spark'ta olduğu gibi şimdi ele alacağımız tuhaflıklar tarafından daha da kötüleştirilir.
Kıvılcım Yani gelmez bir var fold, ancak alt sonuçları (her bölüm için bir tane) (yazma anda) kombine sırası görevlerini tamamladıktan hangi sıra ile aynıdır - ve böylece olmayan deterministik. @CafeFeed'e bu foldkullanımları işaret ettiği için teşekkürler runJob, kodu okuduktan sonra bunun deterministik olmadığını anladım. Daha fazla kafa karışıklığı, Spark'ın bir treeReduceama hayır treeFold.
Boş olmayan diziler arasında reduceve foldhatta uygulandığında bile bir fark vardır . İlki, rasgele sırayla ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ) koleksiyonlar üzerindeki MapReduce programlama paradigmasının bir parçası olarak tanımlanmıştır ve operatörlerin, deterministik sonuçlar vermek için ilişkilendirilebilir. İkincisi, katomorfizmler açısından tanımlanır ve koleksiyonların bir dizi kavramına sahip olmasını gerektirir (veya bağlantılı listeler gibi yinelemeli olarak tanımlanır), bu nedenle değişmeli operatörler gerektirmez.
Uygulamada bağlı programlama unmathematical doğasına, reduceve foldya doğru (Scala) ya da yanlış (Spark gibi), aynı şekilde davranır eğilimindedir.
Benim fikrim, Spark'ta terimin kullanımı foldtamamen bırakılırsa karışıklığın önleneceği yönünde . En azından kıvılcımın belgelerinde bir not var:
Bu, Scala gibi işlevsel dillerde dağıtılmamış koleksiyonlar için uygulanan katlama işlemlerinden biraz farklı davranır.
foldLeftiçeren Leftadında ve neden denilen bir yöntem de vardır fold.
.par, (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)her seferinde farklı sonuçlar alıyorum.
reallyFoldpezevengini yazabilir , çünkü rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)bu işe gidip gelmek için f'ye ihtiyaç duymaz.
Yanılmıyorsam, Spark API gerektirmese bile, katlama ayrıca f'nin değişmeli olmasını gerektirir. Çünkü bölümlerin toplanacağı sıra garanti edilememiştir. Örneğin aşağıdaki kodda yalnızca ilk çıktı sıralanır:
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("")(_ + _))
}
Çıktı:
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("")(_ + _)Birkaç kez 2+ çekirdek ile çalıştırırsanız , rastgele (bölümleme açısından) düzen ürettiğini göreceksiniz. Cevabımı buna göre güncelledim.
foldApache Spark'ta folddağıtılmamış koleksiyonlarla aynı değildir. Aslında deterministik sonuçlar üretmek için değişmeli fonksiyon gerektirir :
Bu, Scala gibi işlevsel dillerde dağıtılmamış koleksiyonlar için uygulanan katlama işlemlerinden biraz farklı davranır. Bu katlama işlemi, bölümlere ayrı ayrı uygulanabilir ve daha sonra, katlamayı her bir öğeye sırayla belirli bir sırayla uygulamak yerine, bu sonuçları nihai sonuca katlayabilir. Değişmeli olmayan işlevler için sonuç, dağıtılmamış bir koleksiyona uygulanan katlamanın sonucundan farklı olabilir.
Bu gösterilmiştir tarafından Mishael Rosenthal ve önerdiği Make42 içinde onun comment .
Gözlemlenen davranışın, HashPartitionergerçekte parallelizekarıştırılmadığı ve kullanılmadığı zamanlarla ilgili olduğu öne sürülmüştürHashPartitioner .
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)
Açıklandı:
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'nin yapısıylareduce aynıdır :
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"))
}
burada runJobbölüm sırasına bakılmaksızın gerçekleştirilir ve değişmeli fonksiyona ihtiyaç duyar.
foldPartitionve reducePartitionişleme sırası açısından eşdeğerdir ve etkili bir şekilde (miras ve yetkilendirme yoluyla) tarafından reduceLeftve foldLeftüzerinde uygulanır TraversableOnce.
Sonuç: foldRDD, yığın sırasına bağlı olamaz ve değişme ve birleşme gerektirir .
foldüzerinde RDDs gerçekten gerçekten sadece aynı olduğunu reduce, ancak bu (ben bile daha anlaşılır olması için benim cevap güncelledik) kök matematiksel farklılıklarına saygı etmez. Taraftarının yaptığı her ne olursa olsun kendinden emin olması koşuluyla, gerçekten değişime ihtiyacımız olduğu konusunda hemfikir olmasam da, düzeni korumaktır.
runJobkodu okuduktan sonra , birleştirmeyi gerçekten de bir görevin bittiği zamana göre yaptığını görüyorum, bölümlerin sırasına göre DEĞİL. Her şeyin yerine oturmasını sağlayan bu anahtar ayrıntıdır. Cevabımı tekrar düzenledim ve böylece işaret ettiğiniz hatayı düzelttim. Şimdi anlaştığımız için lütfen ödülünüzü kaldırır mısınız?
Haşlama için diğer bir fark, Hadoop'ta birleştiricilerin kullanılmasıdır.
İşleminizin değişmeli monoid olduğunu hayal edin, azaltma ile tüm verileri indirgeyicilere karıştırmak / sıralamak yerine harita tarafında da uygulanacaktır. FoldLeft ile durum böyle değil.
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'de işlemlerinizi monoid olarak tanımlamak her zaman iyi bir uygulamadır.