import numpy as npComplément - niveau intermédiaire¶
Lorsque l’on a parlé de programmation vectorielle, on a vu que l’on pouvait écrire quelque chose comme ceci :
X = np.linspace(0, 2 * np.pi)
Y = np.cos(X) + np.sin(X) + 2Je vous fais remarquer que dans cette dernière ligne on combine :
deux tableaux de mêmes tailles - quand on ajoute
np.cos(X)avecnp.sin(X);un tableau avec un scalaire - quand on ajoute
2au résultat.
En fait, le broadcasting est ce qui permet :
d’unifier le sens de ces deux opérations ;
de donner du sens à des cas plus généraux, où on fait des opérations entre des tableaux qui ont des tailles différentes, mais assez semblables pour que l’on puisse tout de même les combiner.
Exemples en 2D¶
Nous allons commencer par quelques exemples simples, avant de généraliser le mécanisme. Pour commencer, nous nous donnons un tableau de base :
a = 100 * np.ones((3, 5), dtype=np.int32)
print(a)[[100 100 100 100 100]
[100 100 100 100 100]
[100 100 100 100 100]]
Je vais illustrer le broadcasting avec l’opération +, mais bien entendu ce mécanisme est à l’œuvre dès que vous faites des opérations entre deux tableaux qui n’ont pas les mêmes dimensions.
Pour commencer, je vais donc ajouter à mon tableau de base un scalaire :
Broadcasting entre les dimensions (3, 5) et (1,)¶
print(a)[[100 100 100 100 100]
[100 100 100 100 100]
[100 100 100 100 100]]
b = 3
print(b)3
Lorsque j’ajoute ces deux tableaux, c’est comme si j’avais ajouté à a la différence :
# pour élaborer c
c = a + b
print(c)[[103 103 103 103 103]
[103 103 103 103 103]
[103 103 103 103 103]]
# c'est comme si j'avais
# ajouté à a ce terme-ci
print(c - a)[[3 3 3 3 3]
[3 3 3 3 3]
[3 3 3 3 3]]
C’est un premier cas particulier de broadcasting dans sa version extrême.
Le scalaire b, qui est en l’occurrence considéré comme un tableau dont le shape vaut (1,), est dupliqué dans les deux directions jusqu’à obtenir ce tableau uniforme de taille (5, 3) et qui contient un 3 partout.
Et c’est ce tableau, qui est maintenant de la même taille que a, qui est ajouté à a.
Je précise que cette explication est du domaine du modèle pédagogique ; je ne dis pas que l’implémentation va réellement allouer un second tableau, bien évidemment on peut optimiser pour éviter cette construction inutile.
Broadcasting (3, 5) et (5,)¶
Voyons maintenant un cas un peu moins évident. Je peux ajouter à mon tableau de base une ligne, c’est-à-dire un tableau de taille (5, ). Voyons cela :
print(a)[[100 100 100 100 100]
[100 100 100 100 100]
[100 100 100 100 100]]
b = np.arange(1, 6)
print(b)[1 2 3 4 5]
b.shape(5,)Ici encore, je peux ajouter les deux termes :
# je peux ici encore
# ajouter les tableaux
c = a + b
print(c)[[101 102 103 104 105]
[101 102 103 104 105]
[101 102 103 104 105]]
# et c'est comme si j'avais
# ajouté à a ce terme-ci
print(c - a)[[1 2 3 4 5]
[1 2 3 4 5]
[1 2 3 4 5]]
Avec le même point de vue que tout à l’heure, on peut se dire qu’on a d’abord transformé (broadcasté) le tableau b :
depuis la dimension (5,)
vers la dimension (3, 5)
# départ
print(b)[1 2 3 4 5]
# arrivée
print(c - a)[[1 2 3 4 5]
[1 2 3 4 5]
[1 2 3 4 5]]
Vous commencez à mieux voir comment ça fonctionne ; s’il existe une direction dans laquelle on peut “tirer” les données pour faire coincider les formes, on peut faire du broadcasting. Et ça marche dans toutes les directions, comme on va le voir tout de suite.
Broadcasting (3, 5) et (3, 1)¶
Au lieu d’ajouter à a une ligne, on peut lui ajouter une colonne, pourvu qu’elle ait la même taille que les colonnes de a :
print(a)[[100 100 100 100 100]
[100 100 100 100 100]
[100 100 100 100 100]]
b = np.arange(1, 4).reshape(3, 1)
print(b)[[1]
[2]
[3]]
Voyons comment se passe le broadcasting dans ce cas-là :
c = a + b
print(c)[[101 101 101 101 101]
[102 102 102 102 102]
[103 103 103 103 103]]
print(c - a)[[1 1 1 1 1]
[2 2 2 2 2]
[3 3 3 3 3]]
Vous voyez que tout se passe exactement de la même façon que lorsqu’on avait ajouté une simple ligne, on a cette fois “tiré” la colonne dans la direction des lignes, pour passer :
depuis la dimension (3, 1)
vers la dimension (3, 5)
# départ
print(b)[[1]
[2]
[3]]
# arrivée
print(c - a)[[1 1 1 1 1]
[2 2 2 2 2]
[3 3 3 3 3]]
Broadcasting (3, 1) et (1, 5)¶
Nous avons maintenant tous les éléments en main pour comprendre un exemple plus intéressant, où les deux tableaux ont des formes pas vraiment compatibles à première vue :
col = np.arange(1, 4).reshape((3, 1))
print(col)[[1]
[2]
[3]]
line = 100 * np.arange(1, 6)
print(line)[100 200 300 400 500]
Grâce au broadcasting, on peut additionner ces deux tableaux pour obtenir ceci :
m = col + line
print(m)[[101 201 301 401 501]
[102 202 302 402 502]
[103 203 303 403 503]]
Remarquez qu’ici les deux entrées ont été étirées pour atteindre une dimension commune.
Et donc pour illustrer le broadcasting dans ce cas, tout se passe comme si on avait :
transformé la colonne (3, 1)
en tableau (3, 5)
print(col)[[1]
[2]
[3]]
print(col + np.zeros(5, dtype=np.int64))[[1 1 1 1 1]
[2 2 2 2 2]
[3 3 3 3 3]]
et transformé la ligne (1, 5)
en tableau (3, 5)
print(line)[100 200 300 400 500]
print(line + np.zeros(3, dtype=np.int64).reshape((3, 1)))[[100 200 300 400 500]
[100 200 300 400 500]
[100 200 300 400 500]]
avant d’additionner terme à terme ces deux tableaux 3 x 5.
En dimensions supérieures¶
Pour savoir si deux tableaux peuvent être compatibles via broadcasting, il faut comparer leurs formes. Je commence par vous donner des exemples. Ici encore quand on mentionne l’addition, cela vaut pour n’importe quel opérateur binaire.
Exemples de dimensions compatibles¶
A 15 x 3 x 5
B 15 x 1 x 5
A+B 15 x 3 x 5Cas de l’ajout d’un scalaire :
A 15 x 3 x 5
B 1
A+B 15 x 3 x 5A 15 x 3 x 5
B 3 x 5
A+B 15 x 3 x 5A 15 x 3 x 5
B 3 x 1
A+B 15 x 3 x 5Exemples de dimensions non compatibles¶
Deux lignes de longueurs différentes :
A 3
B 4Un cas plus douteux :
A 2 x 1
B 8 x 4 x 3Comme vous le voyez sur tous ces exemples :
on peut ajouter A et B lorsqu’il existe une dimension C qui “étire” à la fois celle de A et celle de B ;
on le voit sur le dernier exemple, mais on ne peut broadcaster que de 1 vers ; lorsque divise , on ne peut pas broadcaster de vers , comme on pourrait peut-être l’imaginer.
Comme c’est un cours de Python, plutôt que de formaliser ça sous une forme mathématique - je vous le laisse en exercice - je vais vous proposer plutôt une fonction Python qui détermine si deux tuples sont des shape compatibles de ce point de vue.
# le module broadcasting n'est pas standard
# c'est moi qui l'ai écrit pour illustrer le cours
from broadcasting import compatible, compatible2# on peut dupliquer selon un axe
compatible((15, 3, 5), (15, 1, 5))(15, 3, 5)# ou selon deux axes
compatible((15, 3, 5), (5,))(15, 3, 5)# c'est bien clair que non
compatible((2,), (3,))False# on ne peut pas passer de 2 à 4
compatible((1, 2), (2, 4))False