Fonksiyonel programlamada (özellikle Scala ve Scala API'leri) azaltma ve katlamaLeft / katlama arasındaki fark nedir?


Yanıtlar:


261

katla - sol

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

katla karşılaştır

Ş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.

Sonuç

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.

Ekstra: Spark API Hakkındaki Görüşüm

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.


2
Bu yüzden foldLeftiçeren Leftadında ve neden denilen bir yöntem de vardır fold.
kiritsuku

1
@Cloudtech Bu, spesifikasyonu dahilinde değil, tek iş parçacıklı uygulamasının bir tesadüfidir. 4 çekirdekli makinemde, eklemeyi denersem .par, (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)her seferinde farklı sonuçlar alıyorum.
samthebest

2
@AlexDean bilgisayar bilimi bağlamında, hayır gerçekten bir kimliğe ihtiyaç duymaz çünkü boş koleksiyonlar sadece istisnalar atma eğilimindedir. Ancak, koleksiyon boşken kimlik öğesi döndürülürse matematiksel olarak daha zariftir (ve koleksiyonlar bunu yaparsa daha zarif olur). Matematikte "bir istisna atmak" diye bir şey yoktur.
samthebest

3
@samthebest: Değişebilirlik konusunda emin misin? github.com/apache/spark/blob/… "Değişmeli olmayan işlevler için sonuç, dağıtılmamış bir koleksiyona uygulanan katlamanın sonucundan farklı olabilir" diyor.
Make42

1
@ Make42 Bu doğru, kişi kendi 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.
samthebest

10

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


Biraz ileri geri gittikten sonra, haklı olduğuna inanıyoruz. Birleştirme sırası ilk gelen ilk hizmettir. 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.
samthebest

3

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ı:

foldRDD için yapısı

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 .


Etimolojinin kafa karıştırıcı olduğunu ve programlama literatürünün resmi tanımlarda eksik olduğunu kabul etmeliyim. Ben bunu söylemek güvenli olduğunu düşünüyorum 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.
samthebest

Tanımsız katlama sırası, bölümlemeyle ilgili değildir. Bir runJob uygulamasının doğrudan bir sonucudur.

AH! Maalesef amacınızın ne olduğunu çözemedim, ancak 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?
samthebest

Düzenleyemiyorum veya kaldıramıyorum - böyle bir seçenek yok. Ödüllendirebilirim ama sanırım sadece bir dikkatten epey puan alıyorsun, yanılıyor muyum? Ödül vermemi istediğini onaylarsan, bunu önümüzdeki 24 saat içinde yaparım. Düzeltmeleriniz için teşekkürler ve bir yöntem için özür dilerim, ancak tüm uyarıları görmezden geliyorsunuz, bu büyük bir şey ve cevap her yerde alıntılandı.

1
Kaygıyı açıkça belirten ilk kişi olduğu için @Mishael Rosenthal'a ödüllendirmeye ne dersiniz? Noktalarla ilgilenmiyorum, sadece SEO ve organizasyon için SO kullanmayı seviyorum.
samthebest

2

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.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.