Різниця між зменшенням та згортанням ліворуч / згину у функціональному програмуванні (особливо API Scala та Scala)?


96

Чому Scala та такі фреймворки, як Spark та Scalding, мають і те, reduceі інше foldLeft? Тоді в чому різниця між reduceі fold?



Відповіді:


260

зменшити vs foldLeft

Велика різниця, яка не згадується в жодній іншій відповіді стак-потоку, що стосується цієї теми, полягає в тому, що reduceслід надати комунативний моноїд , тобто операцію, яка є одночасно комутативною та асоціативною. Це означає, що операцію можна розпаралелювати.

Ця відмінність дуже важлива для обробки великих даних / MPP / розподілених обчислень, і вся причина, чому вона reduceіснує. Колекцію можна подрібнити, а reduceбаночка може оперувати на кожному шматку, тоді вона reduceможе діяти за результатами кожного шматка - адже рівень збивання не повинен зупинятися на рівні глибоко. Ми могли б також подрібнити кожен шматок. Ось чому підсумовування цілих чисел у списку дорівнює O (журнал N), якщо дано нескінченну кількість ЦП.

Якщо ви просто подивитеся на підписи, немає ніяких причин reduceіснувати, тому що ви можете досягти всього, що можете, з reduceдопомогою foldLeft. Функціональність компанії foldLeftбільше, ніж функціональність reduce.

Але ви не можете паралелізувати a foldLeft, тому час його виконання завжди O (N) (навіть якщо ви подаєте в комутативний моноїд). Це пов’язано з тим, що передбачається, що операція не є комутативним моноїдом, і тому сукупне значення буде обчислюватися низкою послідовних агрегувань.

foldLeftне передбачає комутативності та асоціативності. Саме асоціативність дає можливість подрібнити колекцію, а комутативність робить кумуляцію легкою, оскільки порядок не важливий (тому не має значення, в якому порядку агрегувати кожен з результатів з кожного фрагмента). Строго кажучи, комутативність не потрібна для розпаралелювання, наприклад, алгоритми розподіленого сортування, це просто полегшує логіку, тому що вам не потрібно впорядковувати свої фрагменти.

Якщо ви подивитесь на документацію Spark, 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

зменшити проти кратності

Тепер це дещо ближче до FP / математичних коренів, і трохи складніше для пояснення. Зменшення визначається формально як частина парадигми MapReduce, яка стосується невпорядкованих колекцій (мультимножин), Fold формально визначається з точки зору рекурсії (див. Катаморфізм) і, таким чином, передбачає структуру / послідовність для колекцій.

Немає foldметоду в Scalding, тому що в рамках (суворої) моделі програмування Map Reduce ми не можемо визначити, foldоскільки фрагменти не мають впорядкування і foldвимагають лише асоціативності, а не комутативності.

Простіше кажучи, reduceпрацює без порядку кумуляції, foldвимагає порядку кумуляції, і саме такий порядок кумуляції потребує нульового значення НЕ існування нульового значення, яке їх відрізняє. Власне кажучи, reduce слід працювати над порожньою колекцією, оскільки її нульове значення можна визначити, взявши довільне значення, xа потім вирішити x op y = x, але це не працює з некомутативною операцією, оскільки може існувати ліве і праве нульове значення, які є чіткими (тобто x op y != y op x). Звичайно, Скала не турбується, щоб зрозуміти, що це нульове значення, оскільки для цього потрібно буде робити якусь математику (яка, мабуть, незрозуміла), тому просто створює виняток.

Здається, (як це часто буває в етимології), це початкове математичне значення було втрачено, оскільки єдиною очевидною різницею в програмуванні є підпис. Як результат, це reduceстало синонімом fold, а не зберегти його початкове значення від MapReduce. Зараз ці терміни часто використовуються як взаємозамінні і поводяться однаково в більшості реалізацій (ігноруючи порожні колекції). Дивність посилюється особливостями, як у Spark, про які ми зараз звернемось.

Отже, Spark дійсно має a fold, але порядок об'єднання підрезультатів (по одному для кожного розділу) (на момент написання статті) є таким самим порядком, в якому виконуються завдання - і, отже, недетермінованим. Дякую @CafeFeed за вказівку на те, що foldвикористовує runJob, який, прочитавши код, я зрозумів, що він не детермінований. Подальша плутанина створюється Спарком, treeReduceале його немає treeFold.

Висновок

Існує різниця між reduceі foldнавіть у застосуванні до непорожніх послідовностей. Перший визначається як частина парадигми програмування MapReduce у колекціях з довільним порядком ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ), і слід вважати, що оператори є комутаційними на додаток до того, що вони є асоціативний для отримання детермінованих результатів. Останній визначається з точки зору катоморфізмів і вимагає, щоб колекції мали поняття послідовності (або визначалися рекурсивно, як зв’язані списки), тому не потребують комутативних операторів.

На практиці через не математичну природу програмування, reduceі, foldяк правило, поводяться однаково, або правильно (як у Scala), або неправильно (як у Spark).

Додатково: Моя думка про API Spark

Я вважаю, що плутанини можна було б уникнути, якби використання терміна foldбуло повністю відкинуто в Spark. Принаймні, в документації spark є примітка:

Це поводиться дещо інакше, ніж операції зі складанням, реалізовані для нерозподілених колекцій на функціональних мовах, таких як Scala.


2
Ось чому foldLeftмістить Leftсвоє ім'я і чому також існує метод, який називається fold.
kiritsuku

1
@Cloudtech Це збіг його однопоточної реалізації, а не в її специфікації. Якщо я спробую додати .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)для пересування на це не потрібно буде f.
samthebest

10

Якщо я не помиляюся, навіть якщо API Spark цього не вимагає, складка також вимагає, щоб 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("")(_ + _))
}  

Надрукувати:

а Б В Г Г Д Е Є Ж З И І Ї Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ ью я

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в Apache Spark не те саме, що foldна нерозповсюджених колекціях. Насправді для отримання детермінованих результатів потрібна комутативна функція :

Це поводиться дещо інакше, ніж операції зі складанням, реалізовані для нерозподілених колекцій на функціональних мовах, таких як Scala. Цю операцію згинання можна застосувати до розділів окремо, а потім скласти ці результати до кінцевого результату, а не застосовувати згинання до кожного елемента послідовно в певному визначеному порядку. Для функцій, які не є комутативними, результат може відрізнятися від результатів складок, застосованих до нерозподіленої колекції.

Це було показано на Mishael 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 для RDD

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
}

те саме , що структураreduce для RDD:

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на RDDs дійсно так само, як reduce, але це не поважає кореневих математичних відмінностей (я оновив свою відповідь, щоб бути ще більш зрозумілою). Хоча я не згоден з тим, що нам дійсно потрібна комутативність за умови, що хтось впевнений, що б не робив їхній партьєр, він зберігає порядок.
самбест

Невизначений порядок згортання не пов'язаний з розділенням. Це прямий наслідок реалізації runJob.

А-а! На жаль, я не зміг зрозуміти, про що ви runJobхотіли сказати, але, прочитавши код, я бачу, що він справді виконує комбінування відповідно до того, коли завдання закінчено, А НЕ порядок розділів. Саме ця ключова деталь змушує все вставати на свої місця. Я відредагував мій відповідь знову і , таким чином , виправив помилку ви відзначаєте. Будь ласка, чи можете ви зняти свою суму, оскільки ми зараз домовляємось?
samthebest

Я не можу редагувати або видаляти - такої можливості немає. Я можу нагородити, але я думаю, що ви отримаєте досить багато балів лише за увагу, чи я помиляюся? Якщо ви підтвердите, що хочете, щоб я нагородив, я це зроблю в найближчі 24 години. Дякуємо за виправлення і вибачте за метод, але виглядало так, що ви ігноруєте всі попередження, це велика річ, і відповідь цитується всюди.

1
Як щодо цього ви нагородите його @Mishael Rosenthal, оскільки він першим чітко висловив свою стурбованість. Я не цікавлюсь пунктами, я просто люблю використовувати SO для SEO та організації.
самий найкращий

2

Ще одна відмінність для Scalding - використання комбайнерів у Hadoop.

Уявіть, ваша операція є комутативним моноїдом, з функцією зменшення вона буде застосована на стороні карти також замість перемішування / сортування всіх даних до редукторів. З foldLeft це не так.

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 та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.