Scala와 Spark 및 Scalding과 같은 프레임 워크에 reduce
및 foldLeft
? 그렇다면 reduce
과 의 차이점은 무엇 fold
입니까?
Scala와 Spark 및 Scalding과 같은 프레임 워크에 reduce
및 foldLeft
? 그렇다면 reduce
과 의 차이점은 무엇 fold
입니까?
답변:
이 주제와 관련된 다른 stackoverflow 답변에서 명확하게 언급되지 않은 큰 차이점 reduce
은 commutative monoid , 즉 commutative 및 associative 연산을 제공해야한다는 것 입니다. 이는 작업을 병렬화 할 수 있음을 의미합니다.
이러한 구분은 빅 데이터 / MPP / 분산 컴퓨팅에있어 매우 중요하며, 그 이유 reduce
가 모두 존재합니다. 컬렉션은 잘게 쪼개 질 수 있고 reduce
각 청크에서 reduce
작동 할 수 있습니다. 그런 다음 각 청크 의 결과에 대해 작동 할 수 있습니다. 사실 청킹 수준은 한 수준 깊이에서 멈출 필요가 없습니다. 우리는 각 덩어리도자를 수 있습니다. 이것이 무한한 수의 CPU가 주어지면 목록의 정수 합계가 O (log N) 인 이유입니다.
방금 서명을 보면 대한 이유가 없다 reduce
당신과 당신이 할 수있는 모든 것을 달성 할 수 있기 때문에 존재하는 reduce
와 함께 foldLeft
. 의 기능이의 기능 foldLeft
보다 큽니다 reduce
.
그러나 를 병렬화 할 수 없으므로 foldLeft
런타임은 항상 O (N)입니다 (교류 형 모노 이드를 공급하더라도). 이는 연산이 교환 적 모노 이드 가 아니라고 가정하고 누적 된 값이 일련의 순차적 집계에 의해 계산되기 때문입니다.
foldLeft
commutativity 또는 associativity를 가정하지 않습니다. 컬렉션을 다듬을 수있는 능력을 부여하는 것은 연관성이고 순서가 중요하지 않기 때문에 누적을 쉽게 만드는 것은 교환 성입니다 (따라서 각 청크에서 각 결과를 집계 할 순서는 중요하지 않습니다). 엄밀히 말하면, 병렬화에는 commutativity가 필요하지 않습니다 (예 : 분산 정렬 알고리즘). 청크에 순서를 지정할 필요가 없기 때문에 논리를 더 쉽게 만듭니다.
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 / 수학적 뿌리에 조금 더 가까워지고 설명하기가 조금 더 까다로워지는 곳입니다. Reduce는 순서없는 컬렉션 (다중 집합)을 다루는 MapReduce 패러다임의 일부로 공식적으로 정의되고, Fold는 재귀 측면에서 공식적으로 정의되며 (카타 모피 즘 참조) 따라서 컬렉션에 대한 구조 / 시퀀스를 가정합니다.
더 없습니다 fold
때문에 우리가 정의 할 수 없습니다 프로그래밍 모델을 줄이려면 (엄격한)지도 아래 끓는에서 방법 fold
덩어리가 순서를 가지고 있지 않기 때문에 fold
유일한 연관성이 아니라 교환 법칙이 필요가.
간단히 말해서, reduce
누적 순서없이 작동하고, 누적 순서가 fold
필요하며,이를 구별하는 0 값의 존재가 아니라 0 값을 필요로하는 것은 누적 순서입니다. 엄밀히 말하면 빈 컬렉션에서 작동 reduce
해야 합니다. 왜냐하면 0 값은 임의의 값을 취한 다음를 x
풀어서 추론 할 수 있기 때문 x op y = x
입니다. (예 x op y != y op x
). 물론 스칼라는 (아마도 계산할 수없는) 수학을해야하기 때문에이 제로 값이 무엇인지 알아 내려고하지 않습니다. 그래서 그냥 예외를 던집니다.
프로그래밍의 유일한 명백한 차이점은 서명이기 때문에이 원래의 수학적 의미가 상실된 것 같습니다 (어원학의 경우가 많습니다). 그 결과 MapReduce의 원래 의미를 보존하기보다 reduce
의 동의어가되었습니다 fold
. 이제 이러한 용어는 종종 같은 의미로 사용되며 대부분의 구현에서 동일하게 작동합니다 (빈 컬렉션 무시). 이상 함은 Spark와 같은 특이성에 의해 악화됩니다.
따라서 Spark 에는 이 fold
있지만 하위 결과 (각 파티션에 대해 하나씩)가 결합되는 순서 (작성 당시)는 작업이 완료되는 순서와 동일하므로 비 결정적입니다. 지적에 대한 @CafeFeed 덕분에 fold
사용 runJob
후 코드를 읽고, 나는 그것이 비 결정적이다 것을 깨달았다. 또한 혼란은 스파크가없는 의해 생성 treeReduce
되지만를 treeFold
.
사이에 차이가 reduce
와 fold
도 비어 있지 않은 시퀀스에 적용이. 전자는 임의의 순서 ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf )를 사용하여 컬렉션에 대한 MapReduce 프로그래밍 패러다임의 일부로 정의되며 연산자는 결정 론적 결과를 제공하기 위해 연관됩니다. 후자는 catomorphisms 관점에서 정의되며 컬렉션이 시퀀스 개념을 가져야하며 (또는 연결된 목록처럼 재귀 적으로 정의 됨) 교환 연산자가 필요하지 않습니다.
실제로 때문에 프로그래밍의 unmathematical 특성, reduce
및 fold
하나 제대로 (스칼라처럼) 또는 잘못 (불꽃처럼), 같은 방식으로 행동하는 경향이있다.
내 의견은 fold
Spark에서 용어 사용 이 완전히 삭제되면 혼란을 피할 수 있다는 것 입니다. 적어도 spark는 문서에 메모가 있습니다.
이것은 Scala와 같은 기능적 언어에서 분산되지 않은 컬렉션에 대해 구현 된 접기 작업과는 다소 다르게 작동합니다.
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)
. 출퇴근하는 데 f가 필요하지 않습니다.
내가 착각하지 않았다면 Spark API에 필요하지 않더라도 fold는 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
Apache Spark에서는 fold
분산되지 않은 컬렉션 과 동일 하지 않습니다. 실제로 결정 론적 결과를 생성 하려면 교환 함수 가 필요 합니다.
이것은 Scala와 같은 기능적 언어에서 분산되지 않은 컬렉션에 대해 구현 된 접기 작업과는 다소 다르게 작동합니다. 이 접기 작업은 파티션에 개별적으로 적용한 다음 정의 된 순서대로 각 요소에 순차적으로 접기를 적용하는 대신 결과를 최종 결과로 접을 수 있습니다. 교환되지 않는 함수의 경우 결과가 분산되지 않은 컬렉션에 적용된 접기의 결과와 다를 수 있습니다.
이 표시되었습니다 에 의해 미사 엘 로젠탈 에 의해 제안 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)
설명 :
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
on RDD는 청크의 순서에 의존 할 수 없으며 commutativity 및 associativity가 필요합니다 .
fold
on RDD
s가 실제로 reduce
. 나는 그들의 파티오가하는 일이 무엇이든 자신감을 가지고 있다면 우리가 정말로 commutativity 가 필요하다는 것에 동의하지 않지만 , 그것은 질서를 보존하는 것입니다.
runJob
코드 를 읽어 보니 실제로 파티션 순서가 아니라 작업이 완료 될 때 결합이 수행된다는 것을 알 수 있습니다. 모든 것을 제자리에 놓는 것은이 핵심 세부 사항입니다. 나는 내 대답을 다시 편집 하여 지적한 실수를 수정했습니다. 우리가 지금 합의 했으니 현상금을 제거 할 수 있습니까?
Scalding의 또 다른 차이점은 Hadoop에서 결합기를 사용한다는 것입니다.
당신의 작업이 교환 모노이 드라고 상상해보십시오. reduce 는 모든 데이터를 리듀서로 셔플 링 / 정렬하는 대신 맵 측에도 적용됩니다. 함께 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 }
}
항상 Scalding에서 작업을 monoid로 정의하는 것이 좋습니다.