web-dev-qa-db-fra.com

Expliquez la fonctionnalité d'agrégation dans Spark

Je cherche une meilleure explication de la fonctionnalité d'agrégation disponible via spark en python.

L'exemple que j'ai est le suivant (en utilisant pyspark de Spark version 1.2.0)

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])))

Sortie:

(10, 4)

J'obtiens le résultat attendu (10,4) qui est la somme de 1+2+3+4 et 4 éléments. Si je change la valeur initiale transmise à la fonction d'agrégation en (1,0) de (0,0) J'obtiens le résultat suivant

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])))

Sortie:

(19, 4)

La valeur augmente de 9. Si je la change en (2,0), la valeur passe à (28,4) etc.

Quelqu'un peut-il m'expliquer comment cette valeur est calculée? Je m'attendais à ce que la valeur augmente de 1 et non de 9, je m'attendais à voir (11,4) à la place je vois (19,4).

46
ab_tech_sp

Je n'ai pas assez de points de réputation pour commenter la réponse précédente de Maasg. En fait, la valeur zéro doit être "neutre" envers le seqop, ce qui signifie qu'elle n'interfère pas avec le résultat du seqop, comme 0 vers add, ou 1 vers *;

Vous ne devez JAMAIS essayer avec des valeurs non neutres car elles peuvent être appliquées à des heures arbitraires. Ce comportement n'est pas uniquement lié au nombre de partitions.

J'ai essayé la même expérience que celle indiquée dans la question. avec 1 partition, la valeur zéro a été appliquée 3 fois. avec 2 partitions, 6 fois. avec 3 partitions, 9 fois et cela continuera.

15
John Knight

Je n'étais pas complètement convaincu de la réponse acceptée, et la réponse de JohnKnight a aidé, voici donc mon point de vue:

Tout d'abord, expliquons greg () dans mes propres mots:

Prototype :

agrégat (zeroValue, seqOp, combOp)

Description :

aggregate() vous permet de prendre un RDD et de générer une valeur unique qui est d'un type différent de ce qui était stocké dans le RDD d'origine.

Paramètres :

  1. zeroValue: La valeur d'initialisation, pour votre résultat, au format souhaité.
  2. seqOp: l'opération que vous souhaitez appliquer aux enregistrements RDD. S'exécute une fois pour chaque enregistrement d'une partition.
  3. combOp: définit comment les objets résultants (un pour chaque partition) sont combinés.

Exemple :

Calculez la somme d'une liste et la longueur de cette liste. Renvoie le résultat dans une paire de (sum, length).

Dans un Spark Shell, j'ai d'abord créé une liste avec 4 éléments, avec 2 partitions:

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

alors j'ai défini mon seqOp:

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

et mon 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]) )

puis j'ai agrégé:

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

Comme vous pouvez le voir, j'ai donné des noms descriptifs à mes variables, mais permettez-moi de l'expliquer davantage:

La première partition a la sous-liste [1, 2]. Nous appliquerons le seqOp à chaque élément de cette liste et cela produira un résultat local, une paire de (sum, length), Qui reflétera le résultat localement, uniquement dans cette première partition.

Commençons donc: local_result Est initialisé au paramètre zeroValue avec lequel nous avons fourni la fonction aggregate(), c'est-à-dire (0, 0) et list_element Est le premier élément de la liste, à savoir 1. En conséquence, c'est ce qui se passe:

0 + 1 = 1
0 + 1 = 1

Maintenant, le résultat local est (1, 1), ce qui signifie que jusqu'à présent, pour la 1ère partition, après avoir traité uniquement le premier élément, la somme est 1 et la longueur 1. Remarquez que local_result Obtient mis à jour de (0, 0) à (1, 1).

1 + 2 = 3
1 + 1 = 2

et maintenant le résultat local est (3, 2), qui sera le résultat final de la 1ère partition, car ce ne sont pas d'autres éléments dans la sous-liste de la 1ère partition.

En faisant de même pour la 2ème partition, nous obtenons (7, 2).

Maintenant, nous appliquons le combOp à chaque résultat local, afin que nous puissions former le résultat global final, comme ceci: (3,2) + (7,2) = (10, 4)


Exemple décrit dans la "figure":

            (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)

Inspiré par ce grand exemple .


Alors maintenant, si le zeroValue n'est pas (0, 0), mais (1, 0), on s'attendrait à obtenir (8 + 4, 2 + 2) = (12, 4), ce qui ne expliquez ce que vous vivez. Même si nous modifions le nombre de partitions de mon exemple, je ne pourrai plus l'obtenir.

La clé ici est la réponse de JohnKnight, qui déclare que le zeroValue est non seulement analogue au nombre de partitions, mais peut être appliqué plus de fois que prévu.

82
gsamaras

Aggregate vous permet de transformer et de combiner les valeurs du RDD à volonté.

Il utilise deux fonctions:

Le premier transforme et ajoute les éléments de la collection d'origine [T] dans un agrégat local [U] et prend la forme: (U, T) => U. Vous pouvez le voir comme un pli et donc il nécessite également un zéro pour cette opération. Cette opération est appliquée localement à chaque partition en parallèle.

Voici où réside la clé de la question: La seule valeur qui devrait être utilisée ici est la valeur ZERO pour l'opération de réduction. Cette opération est exécutée localement sur chaque partition, par conséquent, l'ajout de quoi que ce soit à cette valeur nulle s'ajoutera au résultat multiplié par le nombre de partitions du RDD.

La deuxième opération prend 2 valeurs du type de résultat de l'opération précédente [U] et les combine en une seule valeur. Cette opération réduira les résultats partiels de chaque partition et produira le total réel.

Par exemple: étant donné un RDD de chaînes:

val rdd:RDD[String] = ???

Disons que vous voulez agréger la longueur des chaînes dans ce RDD, vous feriez donc:

1) La première opération transformera les chaînes en taille (int) et accumulera les valeurs de taille.

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

2) fournir le ZÉRO pour l'opération d'addition (0)

val ZERO = 0

3) une opération pour additionner deux entiers:

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

Mettre tous ensemble:

rdd.aggregate(ZERO, stringSizeCummulator, add)

Alors, pourquoi le ZERO est-il nécessaire? Lorsque la fonction de cummulateur est appliquée au premier élément d'une partition, il n'y a pas de total cumulé. ZERO est utilisé ici.

Par exemple. Mon RDD est: - Partition 1: ["Jump", "over"] - Partition 2: ["the", "wall"]

Il en résultera:

P1:

  1. stringSizeCummulator (ZERO, "Jump") = 4
  2. stringSizeCummulator (4, "over") = 8

P2:

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

Réduire: ajouter (P1, P2) = 15

29
maasg

Vous pouvez utiliser le code suivant (en scala) pour voir précisément ce que fait aggregate. Il construit un arbre de toutes les opérations d'addition et de fusion:

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)

Et puis, dans le Shell:

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

Nous avons donc ces 3 partitions: [4], [1,2] et [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)))

Vous pouvez représenter le résultat sous forme d'arbre:

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

Vous pouvez voir qu'un premier élément zéro est créé sur le nœud du pilote (à gauche de l'arborescence), puis les résultats de toutes les partitions sont fusionnés un par un. Vous voyez également que si vous remplacez 0 par 1 comme vous l'avez fait dans votre question, cela ajoutera 1 à chaque résultat sur chaque partition, et ajoutera également 1 à la valeur initiale sur le pilote. Ainsi, le nombre total de fois où la valeur zéro que vous donnez est utilisée est:

number of partitions + 1.

Donc, dans votre cas, le résultat de

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

sera:

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

L'implémentation de aggregate est assez simple. Il est défini dans RDD.scala, ligne 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
}
1
lovasoa

Excellentes explications, cela m'a vraiment aidé à comprendre le fonctionnement en dessous de la fonction d'agrégation. J'ai joué avec lui pendant un certain temps et j'ai découvert comme ci-dessous.

  • si vous utilisez l'acc as as (0,0), cela ne changera pas le résultat de la sortie de la fonction.

  • si l'accumulateur initial est changé, il traitera le résultat quelque chose comme ci-dessous

[somme des éléments RDD + valeur initiale acc * Nombre de partitions RDD + valeur initiale acc]

pour la question ici, je suggère de vérifier les partitions car le nombre de partitions devrait être de 8 selon ma compréhension car chaque fois que nous traiterons l'opération seq sur une partition de RDD, cela commencera par la somme initiale du résultat acc et aussi quand il va faire le peigne Op il utilisera à nouveau la valeur initiale acc une fois.

par exemple Liste (1,2,3,4) & acc (1,0)

Obtenez les partitions dans scala by RDD.partitions.size

si les partitions sont 2 et que le nombre d'éléments est 4 alors => [10 + 1 * 2 + 1] => (13,4)

si la partition est 4 et le nombre d'éléments est 4 alors => [10 + 1 * 4 + 1] => (15,4)

J'espère que cela vous aide, vous pouvez vérifier ici pour des explications. Merci.

1
iSingh

Pour les personnes qui recherchent Scala Code équivalent pour l'exemple ci-dessus - le voici. Même logique, même entrée/résultat.

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)

Je vais expliquer le concept de l'opération d'agrégation dans Spark comme suit:

Définition de la fonction d'agrégation

**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 représente le nombre de partitions disponibles dans notre cluster Spark.

Par conséquent, le rdd est distribué en 4 partitions comme:

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

nous divisons l'énoncé du problème en deux parties: la première partie du problème consiste à agréger le nombre total de fleurs cueillies dans chaque quadrant; c'est l'agrégation de séquences intra-partition

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

La deuxième partie du problème consiste à additionner ces agrégats individuels à travers les partitions; c'est l'agrégation inter-partition.

36 + 75 + 108 + 65 = 284

La somme, stockée dans un RDD peut en outre être utilisée et traitée pour tout type de transformation ou autre action

Donc, le code devient comme:

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

Explication: (0) - est l'accumulateur Le premier + est la somme intra-partition, en ajoutant le nombre total de fleurs cueilli par chaque cueilleur dans chaque quadrant du jardin. Le second + est la somme inter-partition, qui agrège les sommes totales de chaque quadrant.

Cas 1:

Supposons, si nous devons réduire les fonctions après la valeur initiale. Que se passerait-il si la valeur initiale n'était pas nulle ??. Si c'était 4, par exemple:

Le nombre serait ajouté à chaque agrégat intra-partition, ainsi qu'à l'agrégat inter-partition:

Le premier calcul serait donc:

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

Voici le calcul d'agrégation inter-partition avec la valeur initiale de 5:

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

Donc, pour en venir à votre requête: la somme peut être calculée en fonction du nombre de partitions sur lesquelles les données rdd sont distribuées. je pensais que vos données sont distribuées comme ci-dessous et c'est pourquoi vous avez le résultat comme (19, 4). Ainsi, lorsque vous effectuez une opération d'agrégation, spécifiez le nombre de valeurs de partition:

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)
)

résultat:

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)

Explication: Comme vos données sont réparties en 8 partitions, le résultat est similaire (en utilisant la logique expliquée ci-dessus)

ajout intra-partition:

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

calcul inter-partition:

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

Merci

0
Suresh Gudimetla

J'essaie de nombreuses expériences sur cette question. Il est préférable de définir le nombre de partitions pour l'agrégat. le seqOp traitera chaque partition et appliquera la valeur initiale, de plus, combOp appliquera également la valeur initiale lors de la combinaison de toutes les partitions. Je présente donc le format de cette question:

final result = sum(list) + num_Of_Partitions * initial_Value + 1
0
W.Sen

Merci à gsamaras.

Mon graphique est comme ci-dessous, enter image description here

0
ryh