Difusión (Broadcasting)#
La difusión (broadcasting) es un mecanismo que utiliza PyTorch para manejar tensores durante operaciones aritméticas no evidentes.
Por ejemplo, está claro que no se puede sumar una matriz de \(3 \times 3\) con otra de \(4 \times 2\), ya que esto generaría un error. Sin embargo, es posible sumar un escalar a una matriz \(3 \times 3\) o un vector de tamaño \(3\) a una matriz \(3 \times 3\), aunque la lógica no siempre sea obvia.
El mecanismo de broadcasting de PyTorch se basa en reglas simples que deben conocerse al manipular tensores.
Reglas de broadcasting#
Para que dos tensores sean compatibles con broadcasting (broadcastables), deben cumplir las siguientes reglas:
Cada tensor debe tener al menos una dimensión.
Al comparar las dimensiones (empezando por la última), sus tamaños deben ser iguales, o uno de ellos debe ser \(1\), o una de las dimensiones debe estar ausente.
Utilicemos ejemplos para aclarar:
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)
Ahora que sabemos identificar dos tensores compatibles con broadcasting, definamos las reglas que se aplican durante las operaciones entre ellos:
Las reglas son:
Regla 1: Si el número de dimensiones de \(x\) e \(y\) difiere, se añade un \(1\) al inicio de las dimensiones del tensor con menos dimensiones para alinearlos.
Regla 2: Para cada dimensión, el tamaño resultante es el máximo entre los tamaños de \(x\) e \(y\).
El tensor cuya dimensión se modifica se duplicará las veces necesarias para ajustarse.
Nota: Si dos tensores no son compatibles con broadcasting, su suma generará un error. Sin embargo, en muchos casos, el broadcasting funcionará pero no producirá el resultado esperado. Por eso es crucial dominar estas reglas.
Retomemos los dos ejemplos:
Sumar un escalar a una matriz \(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])
Sumar un vector de tamaño \(3\) a una matriz \(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])
Analicemos ahora otros ejemplos más complejos:
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
Otros aspectos a considerar#
Comparación con escalares#
No siempre lo tenemos en cuenta, pero esto permite realizar comparaciones de manera sencilla.
a = torch.tensor([10., 0, -4])
print(a > 0)
print(a==0)
tensor([ True, False, False])
tensor([False, True, False])
También es posible comparar dos tensores entre sí:
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])
Esto puede ser muy útil para crear máscaras a partir de un umbral, por ejemplo, o para verificar si dos operaciones son equivalentes.
Uso de unsqueeze()#
Anteriormente vimos que es posible aplicar broadcasting a un tensor de tamaño \(3\) hacia una matriz de \(3 \times 3\). PyTorch lo transforma automáticamente a un tamaño \(1 \times 3\) para realizar la operación. Sin embargo, puede que queramos hacer la operación en el otro sentido, es decir, sumar un tensor \(3 \times 1\) a una matriz de \(3 \times 3\).
En este caso, debemos reemplazar manualmente la Regla 1 usando la función unsqueeze(), que permite añadir una dimensión.
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]])
Como pueden ver, pudimos eludir las reglas de PyTorch para obtener el resultado deseado.
Nota:
La Regla 1 de PyTorch equivale a aplicar
x.unsqueeze(0)hasta que el número de dimensiones sea el mismo.Es posible reemplazar
unsqueeze()porNonede la siguiente manera:
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]))
Uso de keepdim#
Las funciones de PyTorch que reducen el tamaño de un tensor a lo largo de una dimensión (como torch.sum para sumar o torch.mean para calcular la media) tienen un parámetro útil en ciertos casos.
Estas operaciones modifican las dimensiones del tensor y eliminan automáticamente la dimensión sobre la cual se realizó la operación.
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 desea conservar la dimensión sobre la cual se realiza la suma, puede usar el argumento 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])
Esto puede ser muy útil para evitar errores con las dimensiones. Analicemos un caso en el que esto afecta al 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
Esto es lo que ocurrió:
En el primer caso,
x_sumtiene un tamaño de \(3 \times 5\). La Regla 1 lo transforma en \(1 \times 3 \times 5\), y la Regla 2 transforma \(y\) en \(1 \times 3 \times 5\).En el segundo caso,
x_sum_keepdimtiene un tamaño de \(3 \times 1 \times 5\), y la Regla 2 transforma \(y\) en \(1 \times 3 \times 5\).
Notación de Einstein#
Esta sección no está directamente relacionada con el broadcasting, pero es importante conocerla.
Para multiplicar matrices en PyTorch, hemos utilizado el operador @ (o torch.matmul) hasta ahora. Sin embargo, existe otro método para realizar multiplicaciones matriciales mediante la suma de Einstein (torch.einsum).
Se trata de una notación compacta para expresar productos y sumas. Por ejemplo: ik,kj -> ij El lado izquierdo representa las dimensiones de las entradas, separadas por comas. Aquí tenemos dos tensores, cada uno con dos dimensiones (i,k y k,j). El lado derecho representa las dimensiones del resultado, es decir, un tensor de dimensiones i,j.
Las reglas de la notación de Einstein son:
Los índices repetidos a la izquierda se suman implícitamente si no aparecen a la derecha.
Cada índice puede aparecer, como máximo, dos veces a la izquierda.
Los índices no repetidos a la izquierda deben aparecer a la derecha.
Se puede utilizar para diversas operaciones:
torch.einsum('ij->ji', a)
devuelve la matriz transpuesta de \(a\).
Mientras que
torch.einsum('bi,ij,bj->b', a, b, c)
devuelve un vector de tamaño \(b\) donde la coordenada \(k\)-ésima es la suma de \(a[k,i] \cdot b[i,j] \cdot c[k,j]\). Esta notación es especialmente práctica cuando se manipulan batches con múltiples dimensiones. Por ejemplo, si tienes dos lotes de matrices y quieres calcular el producto matricial por batch, puedes usar:
torch.einsum('bik,bkj->bij', a, b)
Este es un método práctico para realizar multiplicaciones matriciales en PyTorch. Además, es muy rápido y, a menudo, la forma más eficiente de llevar a cabo operaciones personalizadas en PyTorch.