함수형 프로그래밍 (Scala 및 Scala API)에서 reduce와 foldLeft / fold의 차이점은 무엇입니까?
Scala와 Spark 및 Scalding과 같은 프레임 워크에 reduce
및 foldLeft
? 구분 reduce
과 의 차이점은 fold
무엇입니까?
축소 대 foldLeft
이 주제와 관련된 다른 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 개입니다. 엄밀히 말하면 빈 컬렉션에서 작동 reduce
해야 합니다. 왜냐하면 0 값은 임의의 값을 취한 다음을 x
풀서 추론 할 수 있기 때문입니다 x op y = x
. 0 값이 적합한 수 있기 때문에 비 교환 연산에서는 작동하지 않습니다. (예 x op y != y op x
). 물론 스칼라는 (아마도 계산할 수없는) 수학을하기 때문에이 제로 값이 무엇인지 알아 내려고 발음하지 않습니다. 그래서 그냥 예외를 던집니다.
프로그래밍의 유일한 명백한 차이점은 서명이기 때문에 원래의 수학적 의미가 상실된 것입니다 (어원학의 경우가 있음). 그 결과 MapReduce의 원래 의미를 보존하기보다 reduce
의 동의어가 fold
. 이제 사용되는 용어는 종종 같은 의미로 사용 대부분의 구현에서 동일하게 작동합니다 (빈 컬렉션 무시). 이상 함은 Spark와 같은 특이성에 의해 악화됩니다.
스파크 따라서 에는 이 fold
있지만 하위 결과 (각 파티션에 대해 하나씩)가 결합되는 순서 (작성 당시)는 작업이 완료되는 순서와 동일 하므로 비 결정적입니다 . 지적에 대한 @CafeFeed는 fold
사용 runJob
후 코드를 읽고, 나는 그것이 비 결정적이다. 또한 혼란은 생성은 스파크를 통해 생성 treeReduce
됩니다 treeFold
.
결론
사이에 차이가 reduce
와 fold
도 비어 있지 않은 시퀀스에 적용이. 전자는 임의의 순서 ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf )를 사용하여 컬렉션에 대한 MapReduce 프로그래밍 패러다임의 일부로 정의 결과 연산자는 결정 론적 순서를 제공하기 위해 연관 됩니다. 후자는 catomorphism의 관점에서 정의하고 컬렉션이 시퀀스의 개념을 가져야하며 (또는 목록과 같이 재귀로 정의 됨) 교환 연산자가 필요하지 않습니다.
실제로 때문에 프로그래밍의 비 수학적 특성, reduce
및 fold
하나 제대로 (스칼라처럼) 또는 잘못 (불꽃처럼), 같은 방식으로 행동하는 경향이 있습니다.
추가 : Spark API에 대한 나의 의견
내 의견은 fold
Spark에서 용어 사용 이 완전히 삭제되면 혼란을 피할 수 있다는 것 입니다. 적어도 spark는 문서에 메모가 있습니다.
이것은 Scala와 같은 기능적 언어에서 분산되지 않은 컬렉션에 대해 구현 된 접기 작업과는 약간 다르게 작동합니다.
내가 착각하지 않았다면 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
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로 정의하는 것이 좋습니다.
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가 필요합니다 .
'IT' 카테고리의 다른 글
Laravel- 요청시 세션 저장소가 설정되지 않음 (0) | 2020.08.31 |
---|---|
자바 펼쳐에서 딥하는 방법 (0) | 2020.08.31 |
정적 제네릭 메서드 호출 (0) | 2020.08.30 |
전체 커밋이 아닌 하나의 파일에 변경 사항을 선택하는 방법 (0) | 2020.08.30 |
백 스택에서 재개에 대한 조각 (0) | 2020.08.30 |