Broadcasting#

Le broadcasting est un mĂ©canisme utilisĂ© par PyTorch pour traiter les tenseurs lors d’opĂ©rations arithmĂ©tiques non Ă©videntes.

Par exemple, il est clair qu’on ne peut pas additionner une matrice \(3 \times 3\) avec une matrice \(4 \times 2\), ce qui entraĂźnerait une erreur. En revanche, ajouter un scalaire Ă  une matrice \(3 \times 3\) ou un vecteur de taille \(3\) Ă  une matrice \(3 \times 3\) est possible, mĂȘme si la logique n’est pas toujours Ă©vidente.

Le broadcasting de PyTorch repose sur des rĂšgles simples Ă  connaĂźtre lors de la manipulation de tenseurs.

RĂšgles de broadcasting#

Pour que deux tenseurs soient broadcastables, ils doivent respecter les rĂšgles suivantes :

  • Chaque tenseur doit avoir au moins une dimension.

  • En itĂ©rant sur les tailles des dimensions (en commençant par la derniĂšre), les tailles doivent ĂȘtre Ă©gales, ou l’une d’elles doit ĂȘtre 1, ou l’une d’elles doit ĂȘtre absente.

Utilisons des exemples pour clarifier :

import torch

# Deux tenseurs de la mĂȘme taille sont toujours broadcastables
x=torch.zeros(5,7,3)
y=torch.zeros(5,7,3)

# Les deux tenseurs suivants ne sont pas broadcastables car x n'a pas au moins une dimension
x=torch.zeros((0,))
y=torch.zeros(2,2)

# On aligne les dimensions visuellement pour voir si les tenseurs sont broadcastables
# En partant de la droite,
# 1. x et y ont la mĂȘme taille et sont de taille 1
# 2. y est de taille 1
# 3. x et y ont la mĂȘme taille
# 4. la dimension de y n'existe pas
# Les deux tenseurs sont donc broadcastables
x=torch.zeros(5,3,4,1)
y=torch.zeros(  3,1,1)

# A l'inverse, ces deux tenseurs ne sont pas broadcastables car 3. x et y n'ont pas la mĂȘme taille
x=torch.zeros(5,2,4,1)
y=torch.zeros(  3,1,1)

Maintenant que nous savons comment identifier deux tenseurs broadcastables, dĂ©finissons les rĂšgles appliquĂ©es lors de l’opĂ©ration entre eux.

Les rĂšgles sont :

  • RĂšgle 1 : Si le nombre de dimensions de x et y diffĂšre, ajoutez 1 au dĂ©but des dimensions du tenseur ayant le moins de dimensions pour les aligner.

  • RĂšgle 2 : Pour chaque taille de dimension, la taille rĂ©sultante est le maximum des tailles de x et y.

Le tenseur dont la taille est modifiée sera dupliqué autant que nécessaire pour correspondre.

Note : Si deux tenseurs ne sont pas broadcastables, leur addition entraĂźnera une erreur. Cependant, dans de nombreux cas, le broadcasting fonctionnera mais ne produira pas le rĂ©sultat souhaitĂ©. C’est pourquoi il est crucial de maĂźtriser ces rĂšgles.

Reprenons nos deux exemples :

Ajouter un scalaire Ă  une matrice \(3 \times 3\) :

x=torch.randn(3,3)
y=torch.tensor(1)
print("x : " ,x)
print("y : " ,y)
print("x+y : " ,x+y)
print("x+y shape : ",(x+y).shape)
# Le tenseur y est broadcastĂ© pour avoir la mĂȘme taille que x, il se transforme en tenseur de 1 de taille 3x3
x :  tensor([[ 0.6092, -0.6887,  0.3060],
        [ 1.3496,  1.7739, -0.4011],
        [-0.8876,  0.7196, -0.3810]])
y :  tensor(1)
x+y :  tensor([[1.6092, 0.3113, 1.3060],
        [2.3496, 2.7739, 0.5989],
        [0.1124, 1.7196, 0.6190]])
x+y shape :  torch.Size([3, 3])

Ajouter un vecteur de taille \(3\) Ă  une matrice \(3 \times 3\) :

x=torch.randn(3,3)
y=torch.tensor([1,2,3]) # tenseur de taille 3
print("x : " ,x)
print("y : " ,y)
print("x+y : " ,x+y)
print("x+y shape : ",(x+y).shape)
# Le tenseur y est broadcastĂ© pour avoir la mĂȘme taille que x, il se transforme en tenseur de 1 de taille 3x3
x :  tensor([[ 0.9929, -0.1435,  1.5740],
        [ 1.2143,  1.3366,  0.6415],
        [-0.2718,  0.3497, -0.2650]])
y :  tensor([1, 2, 3])
x+y :  tensor([[1.9929, 1.8565, 4.5740],
        [2.2143, 3.3366, 3.6415],
        [0.7282, 2.3497, 2.7350]])
x+y shape :  torch.Size([3, 3])

Examinons maintenant d’autres exemples plus complexes :

x=torch.zeros(5,3,4,1)
y=torch.zeros(  3,1,1)
print("x+y shape : ",(x+y).shape)
# Le tenseur y a été étendu en taille 1x3x1x1 (rÚgle 1) puis dupliqué en taille 5x3x4x1 (rÚgle 2)
x+y shape :  torch.Size([5, 3, 4, 1])
x=torch.empty(1)
y=torch.empty(3,1,7)
print("x+y shape : ",(x+y).shape)
# Le tenseur y a été étendu en taille 1x1x1 (rÚgle 1) puis dupliqué en taille 3x1x7 (rÚgle 2)
x+y shape :  torch.Size([3, 1, 7])
x=torch.empty(5,2,4,1)
y=torch.empty(3,1,1)
print("x+y shape : ",(x+y).shape)
# L'opération n'est pas possible car les tenseurs ne sont pas broadcastables (dimension 3 en partant de la fin ne correspond pas)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[18], line 3
      1 x=torch.empty(5,2,4,1)
      2 y=torch.empty(3,1,1)
----> 3 print("x+y shape : ",(x+y).shape)
      4 # L'opération n'est pas possible car les tenseurs ne sont pas broadcastables (dimension 3 en partant de la fin ne correspond pas)

RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 1

Autres points à considérer#

Comparaison avec des scalaires#

On n’y pense pas toujours, mais cela permet d’effectuer des comparaisons de maniùre simple.

a = torch.tensor([10., 0, -4])
print(a > 0)
print(a==0)
tensor([ True, False, False])
tensor([False,  True, False])

On peut aussi comparer deux tenseurs entre eux :

a=torch.tensor([1,2,3])
b=torch.tensor([4,2,6])
# Comparaison élément par élément
print(a==b)
# Comparaison élément par élément et égalité pour tous les éléments
print((a==b).all())
# Comparaison élément par élément et égalité pour au moins un élément
print((a==b).any())
# Comparaison avec supérieur ou égal
print(a>=b)
tensor([False,  True, False])
tensor(False)
tensor(True)
tensor([False,  True, False])

Cela peut ĂȘtre trĂšs utile pour crĂ©er des masques Ă  partir d’un seuil, par exemple, ou vĂ©rifier que deux opĂ©rations sont Ă©quivalentes.

Utilisation de unsqueeze()#

Nous avons vu prĂ©cĂ©demment qu’il est possible de broadcaster un tenseur de taille \(3\) vers une matrice de taille \(3 \times 3\). Le broadcasting de PyTorch le transforme automatiquement en taille \(1 \times 3\) pour effectuer l’opĂ©ration. Cependant, on peut vouloir rĂ©aliser l’opĂ©ration dans l’autre sens, c’est-Ă -dire ajouter un tenseur \(3 \times 1\) Ă  une matrice de taille \(3 \times 3\).

Dans ce cas, il faut remplacer la rùgle 1 manuellement à l’aide de la fonction unsqueeze(), qui permet d’ajouter une dimension.

x=torch.tensor([1,2,3])
y=torch.randn(3,3)
print("y : ",y )
print("x+y : ",x+y) 

x=x.unsqueeze(1)
print("x shape : ",x.shape)
print("x+y : ",x+y)
y :  tensor([[ 1.3517,  1.1880,  0.4483],
        [ 0.5137, -0.5406, -0.1412],
        [-0.0108,  1.3757,  0.6112]])
x+y :  tensor([[2.3517, 3.1880, 3.4483],
        [1.5137, 1.4594, 2.8588],
        [0.9892, 3.3757, 3.6112]])
x shape :  torch.Size([3, 1])
x+y :  tensor([[2.3517, 2.1880, 1.4483],
        [2.5137, 1.4594, 1.8588],
        [2.9892, 4.3757, 3.6112]])

Comme vous pouvez le voir, nous avons pu contourner les rÚgles de PyTorch pour obtenir le résultat souhaité.

Note :

  • La rĂšgle 1 de PyTorch Ă©quivaut Ă  appliquer x.unsqueeze(0) jusqu’à ce que le nombre de dimensions soit le mĂȘme.

  • Il est possible de remplacer unsqueeze() par None de la maniĂšre suivante :

x=torch.tensor([1,2,3])
# La premiÚre opération est l'équivalent de unsqueeze(0) et la seconde de unsqueeze(1)
x[None].shape,x[...,None].shape
(torch.Size([1, 3]), torch.Size([3, 1]))

Utilisation de keepdim#

Les fonctions de PyTorch qui rĂ©duisent la taille d’un tenseur selon une dimension (torch.sum pour sommer selon une dimension, torch.mean pour calculer la moyenne, etc.) ont un paramĂštre intĂ©ressant Ă  utiliser dans certains cas.

Ces opĂ©rations modifient la dimension du tenseur et suppriment automatiquement la dimension sur laquelle l’opĂ©ration a Ă©tĂ© effectuĂ©e.

x=torch.randn(3,4,5)
print(x.shape)
x=x.sum(dim=1) # somme sur la dimension 1
print(x.shape)
torch.Size([3, 4, 5])
torch.Size([3, 5])

Si vous souhaitez conserver la dimension sur laquelle la somme est effectuĂ©e, vous pouvez utiliser l’argument keepdim=True.

x=torch.randn(3,4,5)
print(x.shape)
x=x.sum(dim=1,keepdim=True) # somme sur la dimension 1
print(x.shape)
torch.Size([3, 4, 5])
torch.Size([3, 1, 5])

Cela peut ĂȘtre trĂšs utile pour Ă©viter les erreurs avec les dimensions. Examinons un cas oĂč cela impacte le broadcasting.

x=torch.randn(3,4,5)
y=torch.randn(1,1,1)
x_sum=x.sum(dim=1)
x_sum_keepdim=x.sum(dim=1,keepdim=True)
print("Les deux opérations sont elles équivalentes ? :",(x_sum+y==x_sum_keepdim+y).all().item())
Les deux opérations sont elles équivalentes ? : False

Voici ce qui s’est passĂ© :

  • Dans le premier cas, x_sum a une taille de \(3 \times 5\). La rĂšgle 1 le transforme en \(1 \times 3 \times 5\), et la rĂšgle 2 transforme y en \(1 \times 3 \times 5\).

  • Dans le second cas, x_sum_keepdim a une taille de \(3 \times 1 \times 5\), et la rĂšgle 2 transforme y en \(1 \times 3 \times 5\).

Notation d’Einstein#

Cette partie n’est pas directement liĂ©e au broadcasting, mais il est important de la connaĂźtre.

Pour multiplier des matrices dans PyTorch, nous avons utilisĂ© l’opĂ©rateur @ (ou torch.matmul) jusqu’ici. Il existe une autre mĂ©thode pour effectuer des multiplications matricielles avec la Einstein Summation (torch.einsum).

Il s’agit d’une notation compacte pour exprimer des produits et des sommes, par exemple : ik,kj -> ij Le cĂŽtĂ© gauche reprĂ©sente les dimensions des entrĂ©es, sĂ©parĂ©es par des virgules. Ici, nous avons deux tenseurs avec chacun deux dimensions (i,k et k,j). Le cĂŽtĂ© droit reprĂ©sente les dimensions du rĂ©sultat, soit un tenseur de dimensions i,j.

Les rùgles de la notation d’Einstein sont :

  • Les indices rĂ©pĂ©tĂ©s Ă  gauche sont implicitement sommĂ©s s’ils n’apparaissent pas Ă  droite.

  • Chaque indice peut apparaĂźtre au maximum deux fois Ă  gauche.

  • Les indices non rĂ©pĂ©tĂ©s Ă  gauche doivent apparaĂźtre Ă  droite.

On peut l’utiliser pour diverses opĂ©rations :

torch.einsum('ij->ji', a)

renvoie la transposée de la matrice a.

Alors que

torch.einsum('bi,ij,bj->b', a, b, c)

renvoie un vecteur de taille b oĂč la k-iĂšme coordonnĂ©e est la somme de \(a[k,i]⋅b[i,j]⋅c[k,j]\). Cette notation est particuliĂšrement pratique lorsque vous manipulez des batchs avec plusieurs dimensions. Par exemple, si vous avez deux lots de matrices et que vous voulez calculer le produit matriciel par batch, vous pouvez utiliser :

torch.einsum('bik,bkj->bij', a, b)

C’est une mĂ©thode pratique pour effectuer des multiplications matricielles dans PyTorch. De plus, c’est trĂšs rapide et souvent la maniĂšre la plus efficace pour rĂ©aliser des opĂ©rations personnalisĂ©es dans PyTorch.