簡體   English   中英

解釋 Spark 中的聚合功能(使用 Python 和 Scala)

[英]Explain the aggregate functionality in Spark (with Python and Scala)

我正在尋找對通過 python 中的 spark 可用的聚合功能的更好解釋。

我的例子如下(使用來自 Spark 1.2.0 版本的 pyspark)

sc.parallelize([1,2,3,4]).aggregate(
  (0, 0),
  (lambda acc, value: (acc[0] + value, acc[1] + 1)),
  (lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1])))

輸出:

(10, 4)

我得到了預期的結果(10,4) ,它是1+2+3+4和 4 個元素的總和。 如果我將傳遞給聚合函數的初始值從(0,0)更改為(1,0)(0,0)得到以下結果

sc.parallelize([1,2,3,4]).aggregate(
    (1, 0),
    (lambda acc, value: (acc[0] + value, acc[1] + 1)),
    (lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1])))

輸出:

(19, 4)

該值增加 9。如果我將其更改為(2,0) ,則該值將變為(28,4)等等。

有人可以向我解釋這個值是如何計算的嗎? 我預計值會上升 1 而不是 9,預計會看到(11,4)而我看到的是(19,4)

對接受的答案並不完全信服,JohnKnight 的回答有所幫助,所以這是我的觀點:

首先,讓我們用我自己的話解釋aggregate()

原型

聚合(零值,seqOp,combOp)

說明

aggregate()允許您獲取一個 RDD 並生成一個與原始 RDD 中存儲的類型不同的單個值。

參數

  1. zeroValue :結果的初始化值,采用所需格式。
  2. seqOp :要應用於 RDD 記錄的操作。 為分區中的每條記錄運行一次。
  3. combOp :定義結果對象(每個分區一個)如何組合。

示例

計算列表的總和和該列表的長度。 (sum, length)對的形式返回結果。

在 Spark shell 中,我首先創建了一個包含 4 個元素和 2 個分區的列表:

listRDD = sc.parallelize([1,2,3,4], 2)

然后我定義了我的seqOp

seqOp = (lambda local_result, list_element: (local_result[0] + list_element, local_result[1] + 1) )

和我的combOp

combOp = (lambda some_local_result, another_local_result: (some_local_result[0] + another_local_result[0], some_local_result[1] + another_local_result[1]) )

然后我匯總:

listRDD.aggregate( (0, 0), seqOp, combOp)
Out[8]: (10, 4)

如您所見,我為變量指定了描述性名稱,但讓我進一步解釋一下:

第一個分區具有子列表 [1, 2]。 我們將 seqOp 應用於該列表的每個元素,這將產生一個本地結果,一對(sum, length) ,它將在本地反映結果,僅在第一個分區中。

所以,讓我們開始吧: local_result被初始化為我們提供給aggregate()zeroValue參數,即 (0, 0) 和list_element是列表的第一個元素,即 1。結果是這樣的:

0 + 1 = 1
0 + 1 = 1

現在,本地結果是 (1, 1),這意味着,到目前為止,對於第一個分區,僅處理第一個元素后,總和為 1,長度為 1。注意, local_result從 (0, 0),到 (1, 1)。

1 + 2 = 3
1 + 1 = 2

現在本地結果是 (3, 2),這將是第一個分區的最終結果,因為它們不是第一個分區的子列表中的其他元素。

對第二個分區做同樣的事情,我們得到 (7, 2)。

現在我們將 combOp 應用於每個局部結果,以便我們可以形成最終的全局結果,如下所示: (3,2) + (7,2) = (10, 4)


“圖”中描述的示例:

            (0, 0) <-- zeroValue

[1, 2]                  [3, 4]

0 + 1 = 1               0 + 3 = 3
0 + 1 = 1               0 + 1 = 1

1 + 2 = 3               3 + 4 = 7
1 + 1 = 2               1 + 1 = 2       
    |                       |
    v                       v
  (3, 2)                  (7, 2)
      \                    / 
       \                  /
        \                /
         \              /
          \            /
           \          / 
           ------------
           |  combOp  |
           ------------
                |
                v
             (10, 4)

靈感來自這個偉大的例子


所以現在如果zeroValue不是 (0, 0),而是 (1, 0),人們會期望得到 (8 + 4, 2 + 2) = (12, 4),這並不能解釋你的經歷。 即使我們更改了示例的分區數,我也無法再次獲得它。

這里的關鍵是 JohnKnight 的回答,其中指出zeroValue不僅類似於分區數,而且可能比您預期的應用更多次。

使用 Scala 進行解釋

Aggregate 允許您隨意轉換和組合 RDD 的值。

它使用兩個函數:

第一個將原始集合 [T] 的元素轉換並添加到本地聚合 [U] 中並采用以下形式:(U,T) => U。您可以將其視為折疊,因此它也需要一個零對於那個操作。 此操作在本地並行應用於每個分區。

這就是問題的關鍵所在:這里應該使用的唯一值是歸約運算的零值。 此操作在每個分區本地執行,因此,向該零值添加任何內容都將添加到結果乘以 RDD 的分區數。

第二個操作取前一個操作 [U] 的結果類型的 2 個值,並將其合並為一個值。 此操作將減少每個分區的部分結果並產生實際總數。

例如:給定一個字符串 RDD:

val rdd:RDD[String] = ???

假設您想要聚合該 RDD 中字符串的長度,因此您可以執行以下操作:

  1. 第一個操作將字符串轉換為 size (int) 並累積 size 的值。

    val stringSizeCummulator: (Int, String) => Int = (total, string) => total + string.lenght`

  2. 為加法運算提供零 (0)

    零值 = 0

  3. 將兩個整數相加的操作:

    val add: (Int, Int) => Int = _ + _

把它們放在一起:

rdd.aggregate(ZERO, stringSizeCummulator, add)

使用 Spark 2.4 及更高版本

rdd.aggregate(ZERO)(stringAccumulator,add)

那么,為什么需要零呢? 當 cummulator 函數應用於分區的第一個元素時,沒有運行總計。 此處使用零。

例如。 我的RDD是:

  • 第 1 部分:[“跳”、“過”]
  • 第 2 部分:["the", "wall"]

這將導致:

P1:

  1. stringSizeCummulator(零,“跳躍”)= 4
  2. stringSizeCummulator(4, "over") = 8

P2:

  1. stringSizeCummulator(ZERO, "the") = 3
  2. stringSizeCummulator(3, "wall") = 7

減少:添加(P1,P2)= 15

我沒有足夠的聲望點來評論 Maasg 的先前回答。 實際上,零值對於 seqop 應該是“中性的”,這意味着它不會干擾 seqop 結果,例如 0 表示添加,或 1 表示 *;

您永遠不應該嘗試使用非中性值,因為它可能會應用於任意時間。 此行為不僅與分區數有關。

我嘗試了與問題中所述相同的實驗。 對於 1 個分區,零值應用了 3 次。 帶2個分區,6次。 有 3 個分區,9 次,這將繼續。

您可以使用以下代碼(在 Scala 中)來准確查看aggregate正在做什么。 它構建了一個包含所有加法和合並操作的樹:

sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

val zero : Tree[Int] = Leaf(0)
val rdd = sc.parallelize(1 to 4).repartition(3)

然后,在外殼中:

scala> rdd.glom().collect()
res5: Array[Array[Int]] = Array(Array(4), Array(1, 2), Array(3))

所以,我們有這 3 個分區:[4]、[1,2] 和 [3]。

scala> rdd.aggregate(zero)((l,r)=>Branch(l, Leaf(r)), (l,r)=>Branch(l,r))
res11: Tree[Int] = Branch(Branch(Branch(Leaf(0),Branch(Leaf(0),Leaf(4))),Branch(Leaf(0),Leaf(3))),Branch(Branch(Leaf(0),Leaf(1)),Leaf(2)))

您可以將結果表示為一棵樹:

+
| \__________________
+                    +
| \________          | \
+          +         +   2
| \        | \       | \         
0  +       0  3      0  1
   | \
   0  4

您可以看到在驅動程序節點(樹的左側)上創建了第一個零元素,然后將所有分區的結果一一合並。 您還會看到,如果您像在問題中一樣將 0 替換為 1,它將為每個分區上的每個結果加 1,並在驅動程序的初始值上加 1。 因此,您給出的值被使用的總次數是:

number of partitions + 1

所以,在你的情況下,結果

aggregate(
  (X, Y),
  (lambda acc, value: (acc[0] + value, acc[1] + 1)),
  (lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1])))

將:

(sum(elements) + (num_partitions + 1)*X, count(elements) + (num_partitions + 1)*Y)

aggregate的實現非常簡單。 它在RDD.scala 第 1107 行中定義:

  def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U = withScope {
    // Clone the zero value since we will also be serializing it as part of tasks
    var jobResult = Utils.clone(zeroValue, sc.env.serializer.newInstance())
    val cleanSeqOp = sc.clean(seqOp)
    val cleanCombOp = sc.clean(combOp)
    val aggregatePartition = (it: Iterator[T]) => it.aggregate(zeroValue)(cleanSeqOp, cleanCombOp)
    val mergeResult = (index: Int, taskResult: U) => jobResult = combOp(jobResult, taskResult)
    sc.runJob(this, aggregatePartition, mergeResult)
    jobResult
}

很好的解釋,它真的幫助我理解了聚合函數的底層工作。 我玩了一段時間,發現如下。

  • 如果您使用 acc 作為 (0,0) 那么它不會改變函數的輸出結果。

  • 如果初始累加器發生變化,那么它將處理結果如下

[RDD元素總和+acc初始值*RDD分區數+acc初始值]

對於這里的問題,我建議檢查分區,因為根據我的理解,分區數應為 8,因為每次我們在 RDD 的分區上處理 seq 操作時,它將以 acc 結果的初始總和開始,並且何時它將執行組合操作,它將再次使用 acc 初始值一次。

例如 List (1,2,3,4) & acc (1,0)

通過 RDD.partitions.size 獲取 scala 中的分區

如果分區是 2 & 元素數是 4 那么 => [ 10 + 1 * 2 + 1 ] => (13,4)

如果分區是 4 & 元素數是 4 那么 => [ 10 + 1 * 4 + 1 ] => (15,4)

希望這會有所幫助,您可以在此處查看解釋。 謝謝。

感謝 gsamaras。

我的視圖如下, 在此處輸入圖片說明

對於為上述示例尋找 Scala 等效代碼的人 - 在這里。 相同的邏輯,相同的輸入/結果。

scala> val listRDD = sc.parallelize(List(1,2,3,4), 2)
listRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:21

scala> listRDD.collect()
res7: Array[Int] = Array(1, 2, 3, 4)

scala> listRDD.aggregate((0,0))((acc, value) => (acc._1+value,acc._2+1),(acc1,acc2) => (acc1._1+acc2._1,acc1._2+acc2._2))
res10: (Int, Int) = (10,4)

我嘗試了很多關於這個問題的實驗。 最好為聚合設置分區數。 seqOp 將處理每個分區並應用初始值,此外,當組合所有分區時,combOp 也會應用初始值。 所以,我提出這個問題的格式:

final result = sum(list) + num_Of_Partitions * initial_Value + 1

我將解釋Spark中Aggregate操作的概念如下:

聚合函數的定義

**def aggregate** (initial value)(an intra-partition sequence operation)(an inter-partition combination operation)

val flowers = sc.parallelize(List(11, 12, 13, 24, 25, 26, 35, 36, 37, 24, 25, 16), 4) --> 4 表示我們的 Spark 集群中可用的分區數.

因此,rdd 被分配到 4 個分區中:

11, 12, 13
24, 25, 26
35, 36, 37
24, 25, 16

我們將問題陳述分為兩部分:第一部分是匯總每個象限采摘的花朵總數; 這就是分區內序列聚合

11+12+13 = 36
24+25+26 = 75
35+36+37 = 108
24+25 +16 = 65

問題的第二部分是對跨分區的這些單獨的聚合求和; 這就是分區間聚合。

36 + 75 + 108 + 65 = 284

存儲在 RDD 中的總和可以進一步用於任何類型的轉換或其他操作

所以代碼變成了這樣:

val sum = flowers.aggregate(0)((acc, value) => (acc + value), (x,y) => (x+y))val sum = flowers.aggregate(0)(_+_, _+_)
Answer: 284

解釋: (0) - 是累加器 第一個+是分區內和,將花園每個象限中每個采摘者采摘的花朵總數相加。 第二個+是分區間總和,它匯總了每個象限的總和。

情況1:

假設,如果我們需要在初始值之后減少函數。 如果初始值不為零會發生什么??。 如果是 4,例如:

該數字將添加到每個分區內聚合中,也會添加到分區間聚合中:

所以第一個計算是:

11+12+13 = 36 + 5 = 41
24+25+26 = 75 + 5 = 80
35+36+37 = 108 + 5 = 113
24+25 +16 = 65 + 5 = 70

下面是初始值為5的分區間聚合計算:

partition1 + partition2 + partition3+ partition4 + 5 = 41 + 80 + 113 + 70 = 309

因此,進入您的查詢:可以根據 rdd 數據分布的分區數計算總和。 我認為您的數據分布如下,這就是為什么結果為 (19, 4)。 因此,在進行聚合操作時,請特定於分區值的數量:

val list = sc.parallelize(List(1,2,3,4))
val list2 = list.glom().collect
val res12 = list.aggregate((1,0))(
      (acc, value) => (acc._1 + value, acc._2 + 1),
      (acc1, acc2) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)

結果:

list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at command-472682101230301:1
list2: Array[Array[Int]] = Array(Array(), Array(1), Array(), Array(2), Array(), Array(3), Array(), Array(4))
res12: (Int, Int) = (19,4)

說明:由於您的數據分布在 8 個分區中,結果就像(通過使用上面解釋的邏輯)

分區內添加:

0+1=1
1+1=2
0+1=1
2+1=3
0+1=1
3+1=4
0+1=1
4+1=5

total=18

分區間計算:

18+1 (1+2+1+3+1+4+1+5+1) = 19

謝謝

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM