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.