Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND Thierry Parmentelat & Arnaud Legout Inria - UCA
import numpy as np

Complé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) + 2

Je vous fais remarquer que dans cette dernière ligne on combine :

En fait, le broadcasting est ce qui permet :

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 5

Cas de l’ajout d’un scalaire :

A   15 x 3 x 5
B            1
A+B 15 x 3 x 5
A   15 x 3 x 5
B        3 x 5
A+B 15 x 3 x 5
A   15 x 3 x 5
B        3 x 1
A+B 15 x 3 x 5

Exemples de dimensions non compatibles

Deux lignes de longueurs différentes :

A  3
B  4

Un cas plus douteux :

A      2 x 1
B  8 x 4 x 3

Comme vous le voyez sur tous ces exemples :

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