स्पार्क और स्केलिंग जैसे स्काला और रूपरेखा दोनों क्यों हैं reduce
और foldLeft
? तो फिर क्या अंतर है reduce
और fold
?
स्पार्क और स्केलिंग जैसे स्काला और रूपरेखा दोनों क्यों हैं reduce
और foldLeft
? तो फिर क्या अंतर है reduce
और fold
?
जवाबों:
इस विषय से संबंधित किसी भी अन्य स्टैकओवरफ्लो उत्तर में स्पष्ट रूप से उल्लेख नहीं किया गया एक बड़ा अंतर यह है कि 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
स्पार्क में इस शब्द का इस्तेमाल पूरी तरह से छोड़ दिया गया तो भ्रम से बचा जा सकेगा । कम से कम स्पार्क उनके प्रलेखन में एक नोट है:
यह स्कैला जैसी कार्यात्मक भाषाओं में गैर-वितरित संग्रहों के लिए कार्यान्वित गुना संचालन से कुछ अलग व्यवहार करता है।
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)
यह करने के लिए च की जरूरत नहीं होगी।
अगर मैं गलत नहीं हूं, भले ही स्पार्क एपीआई को इसकी आवश्यकता नहीं है, फिर भी एफ को कम्यूटेटिव होने के लिए फोल्ड की आवश्यकता होती है। क्योंकि जिस क्रम में विभाजन एकत्र किए जाएंगे, वह सुनिश्चित नहीं है। निम्न कोड में उदाहरण के लिए केवल पहला प्रिंट आउट छाँटा गया है:
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
अपाचे स्पार्क में समान नहीं है, 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
।
निष्कर्ष: fold
RDD पर विखंडू के आदेश पर निर्भर नहीं किया जा सकता है और कम्यूटिटी और एसोसिएटिविटी की आवश्यकता है ।
fold
पर RDD
रों वास्तव में वास्तव में बस के रूप में एक ही है reduce
, लेकिन (मैं अपने जवाब को नवीनीकृत किया है और भी अधिक स्पष्ट होना) इस सम्मान नहीं करता जड़ गणितीय मतभेद। हालांकि मैं असहमत हूं कि हमें वास्तव में कम्यूटेटिविटी की जरूरत है, बशर्ते किसी को भरोसा हो कि उनका पार्टनर जो भी कर रहा है, वह ऑर्डर को संरक्षित कर रहा है।
runJob
कोड के माध्यम से पढ़ने पर मुझे लगता है कि वास्तव में यह एक कार्य समाप्त होने के अनुसार संयोजन करता है, विभाजन का क्रम नहीं। यह इस महत्वपूर्ण विवरण है जो सब कुछ जगह में आता है। मैंने अपने उत्तर को फिर से संपादित किया है और इस प्रकार आपने जो गलती की है उसे ठीक किया। जब से हम अब समझौते में हैं, कृपया आप या तो अपना इनाम हटा सकते हैं?
स्कैडिंग के लिए एक अन्य अंतर हैडोप में कॉम्बीनेर्स का उपयोग है।
कल्पना करें कि आपका ऑपरेशन कम्यूटेटिव मोनॉइड है, इसे कम करने के साथ मैप डेटा पर भी फेरबदल करने के बजाय सभी डेटा को रिड्यूस करने के लिए लागू किया जाएगा। फोल्डेफ्ट के साथ ऐसा नहीं है।
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 }
}
स्कैलिंग में अपने संचालन को मोनॉयड के रूप में परिभाषित करने के लिए हमेशा अच्छा अभ्यास है।