कार्यात्मक प्रोग्रामिंग (विशेष रूप से स्काला और स्काला एपीआई) में कमी और तह / मोड़ के बीच अंतर?


96

स्पार्क और स्केलिंग जैसे स्काला और रूपरेखा दोनों क्यों हैं reduceऔर foldLeft? तो फिर क्या अंतर है reduceऔर fold?


इन्हें भी देखें: stackoverflow.com/questions/16111440/scala-fold-vs-foldleft/…
axel22

जवाबों:


260

बनाम तह कम करें

इस विषय से संबंधित किसी भी अन्य स्टैकओवरफ्लो उत्तर में स्पष्ट रूप से उल्लेख नहीं किया गया एक बड़ा अंतर यह है कि reduceएक कम्यूटेटिव मोनॉइड दिया जाना चाहिए , यानी एक ऑपरेशन जो कम्यूटेटिव और एसोसिएटिव दोनों है। इसका मतलब है कि ऑपरेशन को समानांतर किया जा सकता है।

यह अंतर बिग डेटा / एमपीपी / वितरित कंप्यूटिंग के लिए बहुत महत्वपूर्ण है, और पूरे कारण reduceभी मौजूद है। संग्रह को कटा हुआ किया जा सकता है और reduceप्रत्येक कबाड़ पर काम कर सकता है, फिर प्रत्येक भाग reduceके परिणामों पर काम कर सकता है - वास्तव में मंथन के स्तर को एक स्तर गहरा रोकने की आवश्यकता नहीं है। हम प्रत्येक चंक को भी काट सकते थे। यही कारण है कि एक सूची में पूर्णांकों को समाहित करने पर O (log N) होता है, यदि उसे अनंत संख्या में CPU दिया जाता है।

तुम सिर्फ हस्ताक्षर को देखें, तो के लिए कोई कारण नहीं है reduceअस्तित्व के लिए है क्योंकि आप के साथ सब कुछ आप कर सकते हैं प्राप्त कर सकते हैं reduceएक साथ foldLeft। की कार्यक्षमता foldLeftसे अधिक की कार्यक्षमता है reduce

लेकिन आप एक को समानांतर नहीं कर सकते हैं foldLeft, इसलिए इसका रनटाइम हमेशा O (N) होता है (भले ही आप कम्यूटेटिव मोनॉइड में फीड करें)। ऐसा इसलिए है क्योंकि यह माना जाता है कि ऑपरेशन एक कम्यूटेटिव मोनॉइड नहीं है और इसलिए संचयी मूल्य की गणना अनुक्रमिक एकत्रीकरण की एक श्रृंखला द्वारा की जाएगी।

foldLeftकम्यूटेटिविटी को नहीं मानता और न ही एसोसिएटिविटी को। यह सहानुभूति है जो संग्रह को काट देने की क्षमता देता है, और यह कम्यूटेटिविटी है जो संचयी को आसान बनाता है क्योंकि ऑर्डर महत्वपूर्ण नहीं है (इसलिए यह महत्वपूर्ण नहीं है कि प्रत्येक विखंडू से प्रत्येक परिणाम को एकत्रित करने का कौन सा क्रम है)। समानांतर रूप से वितरण के लिए कड़ाई से बोलना कम्यूटेटिविटी आवश्यक नहीं है, उदाहरण के लिए छँटाई गई एल्गोरिदम वितरित, यह सिर्फ तर्क को आसान बनाता है क्योंकि आपको अपने विखंडू को ऑर्डर देने की आवश्यकता नहीं है।

यदि आप 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

बनाम गुना कम करें

अब यह वह जगह है जहां यह एफपी / गणितीय जड़ों के करीब है, और समझाने के लिए थोड़ा पेचीदा है। कम करें को औपचारिक रूप से MapReduce प्रतिमान के भाग के रूप में परिभाषित किया गया है, जो ऑर्डरलेस संग्रह (मल्टीसेट) के साथ संबंधित है, फोल्ड को औपचारिक रूप से पुनरावर्तन के संदर्भ में परिभाषित किया गया है (कैटमोर्फिज़्म देखें) और इस प्रकार संग्रह के लिए एक संरचना या अनुक्रम को मानता है।

foldस्केलिंग में कोई विधि नहीं है क्योंकि (सख्त) मानचित्र प्रोग्रामिंग मॉडल के तहत हम परिभाषित नहीं कर सकते हैं foldक्योंकि विखंडू में ऑर्डर नहीं होता है और foldकेवल समरूपता की आवश्यकता होती है, कम्यूटेटिविटी की नहीं।

सीधे शब्दों में कहें, reduceसंचयन के एक आदेश के बिना काम करता है , संचयी के एक आदेश की foldआवश्यकता है और यह संचयन का क्रम है जो शून्य मान की आवश्यकता है शून्य मान का अस्तित्व नहीं है जो उन्हें अलग करता है। कड़ाई से बोलना एक खाली संग्रह पर काम reduce करना चाहिए , क्योंकि इसका शून्य मान एक मनमाना मूल्य लेने xऔर फिर हल करने के द्वारा घटाया जा सकता है x op y = x, लेकिन यह एक गैर-कम्यूटेटिव ऑपरेशन के साथ काम नहीं करता है क्योंकि एक बाएँ और दाएँ शून्य मान मौजूद हो सकते हैं जो अलग-अलग हैं (यानी x op y != y op x)। बेशक स्काला इस बात को समझने की जहमत नहीं उठाती कि यह शून्य मान क्या है क्योंकि इसके लिए कुछ गणित करने की आवश्यकता होगी (जो कि शायद अविश्वसनीय हैं), इसलिए सिर्फ एक अपवाद फेंकता है।

ऐसा लगता है (जैसा कि अक्सर व्युत्पत्ति में मामला होता है) कि यह मूल गणितीय अर्थ खो गया है, क्योंकि प्रोग्रामिंग में एकमात्र स्पष्ट अंतर हस्ताक्षर है। नतीजा यह है कि MapReduce से मूल अर्थ को संरक्षित करने के बजाय reduceइसका एक पर्याय बन गया है fold। अब इन शर्तों को अक्सर विनिमेय रूप से उपयोग किया जाता है और अधिकांश कार्यान्वयनों (खाली संग्रहों की अनदेखी) में समान व्यवहार करते हैं। स्पार्क की तरह अजीबोगरीब चीज़ों से अजीबता खत्म हो जाती है, जिसे हम अब संबोधित करेंगे।

तो स्पार्क में एक होता है fold, लेकिन वह क्रम जिसमें उप परिणाम (प्रत्येक विभाजन के लिए एक) संयुक्त होते हैं (लेखन के समय) वही क्रम होता है जिसमें कार्य पूर्ण होते हैं - और इस प्रकार गैर-निर्धारक। उस foldउपयोग को इंगित करने के लिए @CafeFeed को धन्यवाद runJob, जो कोड के माध्यम से पढ़ने के बाद मुझे एहसास हुआ कि यह गैर-नियतात्मक है। स्पार्क द्वारा आगे भ्रम पैदा किया जाता है, treeReduceलेकिन नहीं treeFold

निष्कर्ष

गैर-खाली अनुक्रमों पर लागू होने पर भी reduceऔर उनमें अंतर foldहोता है। पूर्व को मनमाने ढंग से ऑर्डर ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ) के साथ संग्रह पर MapReduce प्रोग्रामिंग प्रतिमान के हिस्से के रूप में परिभाषित किया गया है और ऑपरेटरों के मानने के लिए एक होना चाहिए नियतात्मक परिणाम देने के लिए सहयोगी। उत्तरार्द्ध को कैटोमोर्फिम्स के संदर्भ में परिभाषित किया गया है और इसके लिए आवश्यक है कि संग्रह में अनुक्रम की धारणा हो (या पुनरावर्ती रूप से परिभाषित की गई हो, जैसे कि लिंक की गई सूचियाँ), इस प्रकार कम्यूटेटिव ऑपरेटरों की आवश्यकता नहीं होती है।

प्रोग्रामिंग की अदम्य प्रकृति के कारण व्यवहार में है , reduceऔर foldउसी तरह से व्यवहार करते हैं, या तो सही ढंग से (जैसे स्काला में) या गलत तरीके से (स्पार्क की तरह)।

अतिरिक्त: स्पार्क एपीआई पर मेरी राय

मेरी राय है कि अगर foldस्पार्क में इस शब्द का इस्तेमाल पूरी तरह से छोड़ दिया गया तो भ्रम से बचा जा सकेगा । कम से कम स्पार्क उनके प्रलेखन में एक नोट है:

यह स्कैला जैसी कार्यात्मक भाषाओं में गैर-वितरित संग्रहों के लिए कार्यान्वित गुना संचालन से कुछ अलग व्यवहार करता है।


2
यही कारण है कि है foldLeftशामिल Leftअपने नाम में और क्यों वहाँ भी एक विधि कहा जाता है fold
किरित्सुकु

1
@ क्लॉडटेक यह सिंगल थ्रेडेड इम्प्लीमेंटेशन का संयोग है, इसके स्पेसिफिकेशन के भीतर नहीं। मेरे 4-कोर मशीन पर, अगर मैं जोड़ने की कोशिश करता हूं .par, तो (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)मुझे हर बार अलग-अलग परिणाम मिलते हैं।
samthebest

2
कंप्यूटर विज्ञान के संदर्भ में @AlexDean, नहीं, यह वास्तव में एक पहचान की आवश्यकता नहीं है क्योंकि खाली संग्रह केवल अपवादों को फेंक देते हैं। लेकिन यह गणितीय रूप से अधिक सुरुचिपूर्ण है (और संग्रह अधिक होने पर अधिक सुरुचिपूर्ण होगा) यदि संग्रह खाली होने पर पहचान तत्व वापस कर दिया जाता है। गणित में "एक अपवाद फेंकें" मौजूद नहीं है।
samthebest

3
@samthebest: क्या आप कम्यूटिटी के बारे में निश्चित हैं? github.com/apache/spark/blob/… "उन कार्यों के लिए जो प्रशंसनीय नहीं हैं, परिणाम एक गैर-वितरित संग्रह पर लागू गुना से भिन्न हो सकते हैं।"
Make42

1
@ Make42 यह सही है, एक अपने स्वयं के reallyFoldदलाल लिख सकता है , हालांकि, rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)यह करने के लिए च की जरूरत नहीं होगी।
samthebest

10

अगर मैं गलत नहीं हूं, भले ही स्पार्क एपीआई को इसकी आवश्यकता नहीं है, फिर भी एफ को कम्यूटेटिव होने के लिए फोल्ड की आवश्यकता होती है। क्योंकि जिस क्रम में विभाजन एकत्र किए जाएंगे, वह सुनिश्चित नहीं है। निम्न कोड में उदाहरण के लिए केवल पहला प्रिंट आउट छाँटा गया है:

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

3

foldअपाचे स्पार्क में समान नहीं है, foldबल्कि वितरित संग्रह पर। वास्तव में यह नियतात्मक परिणाम उत्पन्न करने के लिए सराहनीय कार्य की आवश्यकता है :

यह स्कैला जैसी कार्यात्मक भाषाओं में गैर-वितरित संग्रहों के लिए कार्यान्वित गुना संचालन से कुछ अलग व्यवहार करता है। यह गुना ऑपरेशन व्यक्तिगत रूप से विभाजन पर लागू किया जा सकता है, और फिर उन परिणामों को अंतिम परिणाम में मोड़ सकता है, बजाय कुछ परिभाषित क्रम में क्रमिक रूप से प्रत्येक तत्व को गुना लागू करने के लिए। ऐसे फ़ंक्शंस के लिए, जो कम्यूटेटिव नहीं हैं, परिणाम गैर-वितरित संग्रह पर लागू गुना से भिन्न हो सकते हैं।

यह दिखाया गया है द्वारा मीशाएल 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)

व्याख्या की:

आरडीडी केfold लिए संरचना

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"))
}

जहां runJobविभाजन के आदेश की अवहेलना की जाती है और कम्यूटेटिव फ़ंक्शन की आवश्यकता होती है।

foldPartitionऔर reducePartitionप्रसंस्करण के आदेश के संदर्भ और प्रभावी ढंग से (विरासत और प्रतिनिधिमंडल द्वारा) द्वारा कार्यान्वित में बराबर हैं reduceLeftऔर foldLeftपर TraversableOnce

निष्कर्ष: foldRDD पर विखंडू के आदेश पर निर्भर नहीं किया जा सकता है और कम्यूटिटी और एसोसिएटिविटी की आवश्यकता है ।


मुझे स्वीकार करना होगा कि व्युत्पत्ति भ्रामक है और प्रोग्रामिंग साहित्य में औपचारिक परिभाषाओं का अभाव है। मुझे लगता है यह कहना है कि सुरक्षित है foldपर RDDरों वास्तव में वास्तव में बस के रूप में एक ही है reduce, लेकिन (मैं अपने जवाब को नवीनीकृत किया है और भी अधिक स्पष्ट होना) इस सम्मान नहीं करता जड़ गणितीय मतभेद। हालांकि मैं असहमत हूं कि हमें वास्तव में कम्यूटेटिविटी की जरूरत है, बशर्ते किसी को भरोसा हो कि उनका पार्टनर जो भी कर रहा है, वह ऑर्डर को संरक्षित कर रहा है।
samthebest

गुना का अपरिभाषित क्रम विभाजन से संबंधित नहीं है। यह एक रनजोब कार्यान्वयन का प्रत्यक्ष परिणाम है।

आह! क्षमा करें, मैं यह नहीं बता सकता कि आपकी बात क्या थी, लेकिन runJobकोड के माध्यम से पढ़ने पर मुझे लगता है कि वास्तव में यह एक कार्य समाप्त होने के अनुसार संयोजन करता है, विभाजन का क्रम नहीं। यह इस महत्वपूर्ण विवरण है जो सब कुछ जगह में आता है। मैंने अपने उत्तर को फिर से संपादित किया है और इस प्रकार आपने जो गलती की है उसे ठीक किया। जब से हम अब समझौते में हैं, कृपया आप या तो अपना इनाम हटा सकते हैं?
samthebest

मैं संपादित या हटा नहीं सकता - ऐसा कोई विकल्प नहीं है। मैं पुरस्कार दे सकता हूं लेकिन मुझे लगता है कि आपको अकेले एक ध्यान से काफी कुछ अंक मिलते हैं, क्या मैं गलत हूं? यदि आप इस बात की पुष्टि करते हैं कि आप मुझे पुरस्कृत करना चाहते हैं तो मैं इसे अगले 24 घंटों में करूंगा। सुधार के लिए धन्यवाद और एक विधि के लिए खेद है, लेकिन ऐसा लग रहा था कि आप सभी चेतावनियों को अनदेखा करते हैं, यह एक बड़ी बात है, और सभी जगह जवाब दिया गया है।

1
कैसे के बारे में आप @Mishael Rosenthal को यह पुरस्कार देते हैं क्योंकि वह इस चिंता को स्पष्ट रूप से बताने वाले पहले व्यक्ति थे। मुझे अंकों में कोई दिलचस्पी नहीं है, मैं सिर्फ एसईओ और संगठन के लिए एसओ का उपयोग करना पसंद करता हूं।
21

2

स्कैडिंग के लिए एक अन्य अंतर हैडोप में कॉम्बीनेर्स का उपयोग है।

कल्पना करें कि आपका ऑपरेशन कम्यूटेटिव मोनॉइड है, इसे कम करने के साथ मैप डेटा पर भी फेरबदल करने के बजाय सभी डेटा को रिड्यूस करने के लिए लागू किया जाएगा। फोल्डेफ्ट के साथ ऐसा नहीं है।

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 }
}

स्कैलिंग में अपने संचालन को मोनॉयड के रूप में परिभाषित करने के लिए हमेशा अच्छा अभ्यास है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.