Neden Scala ve Spark ve Scalding gibi çerçevelerde hem reduce
ve hem de var foldLeft
? Öyleyse reduce
ve arasındaki fark fold
nedir?
Neden Scala ve Spark ve Scalding gibi çerçevelerde hem reduce
ve hem de var foldLeft
? Öyleyse reduce
ve arasındaki fark fold
nedir?
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 reduce
verilmesi 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 reduce
var olmasının tüm nedeni için çok önemlidir . Koleksiyon parçalanabilir ve reduce
kutu her bir yığın üzerinde çalışabilir, daha sonra reduce
kutu 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 reduce
Birlikte elinizden geleni elde edebilirsiniz, çünkü varlığını reduce
bir ile foldLeft
. İşlevselliği, işlevselliğinden foldLeft
daha 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.
foldLeft
değ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 reduce
sadece ö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.
fold
Scalding'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 fold
sadece ilişkilendirilebilirlik gerektirir, komütatiflik gerektirir.
Basitçe ifade reduce
etmek gerekirse, bir kümülâsyon sırası olmadan çalışır, bir kümülasyon sırası fold
gerektirir 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 x
alı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 reduce
bunun 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 fold
kullanı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 treeReduce
ama hayır treeFold
.
Boş olmayan diziler arasında reduce
ve fold
hatta 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, reduce
ve fold
ya doğru (Scala) ya da yanlış (Spark gibi), aynı şekilde davranır eğilimindedir.
Benim fikrim, Spark'ta terimin kullanımı fold
tamamen 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.
foldLeft
içeren Left
adı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.
reallyFold
pezevengini 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.
fold
Apache Spark'ta fold
dağı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, HashPartitioner
gerçekte parallelize
karış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 runJob
bölüm sırasına bakılmaksızın gerçekleştirilir ve değişmeli fonksiyona ihtiyaç duyar.
foldPartition
ve reducePartition
işleme sırası açısından eşdeğerdir ve etkili bir şekilde (miras ve yetkilendirme yoluyla) tarafından reduceLeft
ve foldLeft
üzerinde uygulanır TraversableOnce
.
Sonuç: fold
RDD, yığın sırasına bağlı olamaz ve değişme ve birleşme gerektirir .
fold
üzerinde RDD
s 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.
runJob
kodu 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.