Los tensores pueden operarse de distintas formas para producir nuevos tensores. Como otras estructuras matemáticas, los tensores permiten operaciones básicas como la suma o el producto. Otras operaciones importantes se pueden hacer con los tensores. Revisamos estas operaciones:
La transposición es una operación que se presenta en tensores de rango mayor o igual a 2. Si bien podemos pensar que un vector (tensor de rango 1) puede transponerse al considerarlo como una columna (vector en vertical) en lugar de un renglón (vector en horizontal). En el caso de las paqueterías con las que operamos la transposición de un vector no surge ningún efecto, sigue siendo un arreglo de números.
import torch
#Vector (tensro rango 1)
x = torch.tensor([1,2,3])
print('Vector {}\nVector transpuesto: {}'.format(x, x.T))
Vector tensor([1, 2, 3]) Vector transpuesto: tensor([1, 2, 3])
En los tensores de mayor rango, la transposición es una permutación que invierte el valor de los índices de un tensor. Es decir, si el tensor $T$ tiene los índices $i_1, i_2,...,i_n$, el vector transpuesto $T^T$ tendrá los índices $i_n, i_{n-1}, ..., i_2, i_1$.
En el caso de las matrices, la tranposición invierte los índices $i,j$ a $j,i$. Así, la transposición de la matriz $A$ tiene como entradas $(A^T)_{i,j} = A_{j,i}$. Es decir,m la transposición de las matrices cambia las columnas por renglones.
#Tensor de rango 2 (matriz)
A = torch.tensor([[0, 5, 1],
[3, 0, 2],
[0, 2, 0],
[5, 3, 1]])
print(A.T)
tensor([[0, 3, 0, 5], [5, 0, 2, 3], [1, 2, 0, 1]])
En tensores de rango 3, los tres índices $i,j,k$ se invierten como $k,j,i$; pot ejemplo, un tensor de tamaño $(2,4,3)$ tendrá una transpuesta de tamaño $(3,4,2)$. De tal forma, que las entradas del tensor transpuesto de grado 3 estarán dadas como: $$(T^T)_{i,j,k} = T_{k,j,i}$$
#Tensor de rango 3
T = torch.tensor([[[1,2,3],[4,5,6],[1,2,3],[1,0.5,2]],
[[7,8,9],[10,11,12],[20,1,0],[0,0,1]]])
print('Tensor transpuesto\n{}'.format(T.T))
print('Tamaño original: {}\nTamaño de tranpuesta: {}'.format(T.size(), T.T.size()))
Tensor transpuesto tensor([[[ 1.0000, 7.0000], [ 4.0000, 10.0000], [ 1.0000, 20.0000], [ 1.0000, 0.0000]], [[ 2.0000, 8.0000], [ 5.0000, 11.0000], [ 2.0000, 1.0000], [ 0.5000, 0.0000]], [[ 3.0000, 9.0000], [ 6.0000, 12.0000], [ 3.0000, 0.0000], [ 2.0000, 1.0000]]]) Tamaño original: torch.Size([2, 4, 3]) Tamaño de tranpuesta: torch.Size([3, 4, 2])
En tensores de mayor rango, la transposición invierte los índices de tal forma que:
$$(T^T)_{i_1,i_2,...,i_{n-1},i_n} = T_{i_n, i_{n-1},...,i_2, i_1}$$Podemos generar un tensro de manera aleatoria (usamos la función torch.rand()) y ver cómo se comportan sus índices:
#Tensor de mayor tango
Trank = torch.rand(2,3,4,5,6,7,8)
print('Tamaño original: {}\nTamaño transpuesto: {}'.format(Trank.size(), Trank.T.size()))
Tamaño original: torch.Size([2, 3, 4, 5, 6, 7, 8]) Tamaño transpuesto: torch.Size([8, 7, 6, 5, 4, 3, 2])
La suma de tensores es una operación sencilla que únicamente consiste en sumar cada una de las entradas de ambos tensores. Por tanto, la suma sólo puede hacerse entre tensores del mismo rango, y de las mismas dimensiones. La suma se da como:
$$(A + B)_{i_1,...,i_n} = A_{i_1,...,i_n} + B_{i_1,...,i_n}$$x = torch.tensor([1,2,1])
y = torch.tensor([1,2,2])
print('Suma de vectores')
print('{}\n + \n{}\n = \n{}'.format(x,y,x+y))
A = torch.tensor([[0, 5, 1],
[3, 0, 2],
[0, 2, 0],
[5, 3, 1]])
B = torch.tensor([[10, 0, 2],
[9, 4, 1],
[8, 1, 0],
[1, 0, 0]])
print('\nSuma de matrices')
print('{}\n + \n{}\n = \n{}'.format(A,B,A+B))
T1 = torch.rand(2,3,4)
T2 = torch.rand(2,3,4)
print('\nSuma de tensores de rango 3')
print('{}\n + \n{}\n = \n{}'.format(T1,T2,T1+T2))
Suma de vectores tensor([1, 2, 1]) + tensor([1, 2, 2]) = tensor([2, 4, 3]) Suma de matrices tensor([[0, 5, 1], [3, 0, 2], [0, 2, 0], [5, 3, 1]]) + tensor([[10, 0, 2], [ 9, 4, 1], [ 8, 1, 0], [ 1, 0, 0]]) = tensor([[10, 5, 3], [12, 4, 3], [ 8, 3, 0], [ 6, 3, 1]]) Suma de tensores de rango 3 tensor([[[0.0457, 0.2114, 0.0302, 0.7899], [0.2373, 0.6150, 0.4963, 0.7592], [0.3869, 0.4044, 0.9591, 0.9765]], [[0.8283, 0.8249, 0.8225, 0.9028], [0.1364, 0.3119, 0.3625, 0.5394], [0.7531, 0.7994, 0.3870, 0.2886]]]) + tensor([[[0.3994, 0.8063, 0.6203, 0.4983], [0.5272, 0.9225, 0.8291, 0.1869], [0.4953, 0.9512, 0.0043, 0.5977]], [[0.4484, 0.6104, 0.0101, 0.8741], [0.9223, 0.8217, 0.9480, 0.2113], [0.8798, 0.6029, 0.6214, 0.3527]]]) = tensor([[[0.4450, 1.0178, 0.6506, 1.2882], [0.7645, 1.5375, 1.3254, 0.9462], [0.8822, 1.3555, 0.9634, 1.5743]], [[1.2767, 1.4354, 0.8327, 1.7769], [1.0587, 1.1336, 1.3106, 0.7507], [1.6329, 1.4023, 1.0084, 0.6413]]])
El producto por un escalar toma un número real $\lambda$ y multiplica cada entrada por este elemento. Por lo que las entradas de un tensor multiplicado por un escalar es de la forma $(\lambda T)_{i_1,...,i_n} = \lambda T_{i_1,...,i_n}$. En este sentido, lo que hace el producto por el escalar es precisamente "escalar" el tensor. En PyTorch, tanto como en Tensorflow y Numpy este producto se hace como:
scalar = a*T
Donde $a$ es un valor numérico, entero o flotante.
#Escalar
a = 2.5
#Productos de tensores
print('{} • {}\n = \n{}\n'.format(a,x, a*x))
print('{} • {}\n = \n{}'.format(a,A, a*A))
2.5 • tensor([1, 2, 1]) = tensor([2.5000, 5.0000, 2.5000]) 2.5 • tensor([[0, 5, 1], [3, 0, 2], [0, 2, 0], [5, 3, 1]]) = tensor([[ 0.0000, 12.5000, 2.5000], [ 7.5000, 0.0000, 5.0000], [ 0.0000, 5.0000, 0.0000], [12.5000, 7.5000, 2.5000]])
El producto punto entre dos vectores $x, y$ de la misma dimensión se calcula como:
$$x^T y = \sum_{i=1}^d x_i y_i$$Este producto punto se realiza de diferentes formas según el lenguaje de programación que estemos usando:
dot = np.dot(x,y)
dot = tf.tensordot(x,y)
scalar = torch.matmul(x,y)
#Producto punto
dot = torch.matmul(x,y)
print('{} • {}\n = \n{}'.format(x,y, dot))
tensor([1, 2, 1]) • tensor([1, 2, 2]) = 7
Los tensores de rango 2 pueden multiplicar a otros tensores de diferente rango. Por ejemplo, se puede realizar el producto entre una matriz $A$ y un vector $x$ donde la dimensiones de las columnas de $A$ deben coincidir con la dimensión de $x$. El resultado de producto es un vector que está determinado como:
$$(Ax)_i = \sum_j A_{i,j} x_j$$En este caso, se utilizan las mismas funciones que en el producto punto. En este caso, tenemos la función torch.matmul().
#Producto matriz con vector
product = torch.matmul(A,x)
print('{} • {}\n = \n{}'.format(A,x, product))
tensor([[0, 5, 1], [3, 0, 2], [0, 2, 0], [5, 3, 1]]) • tensor([1, 2, 1]) = tensor([11, 5, 4, 12])
Asimismo, se puede realizar el producto entre dos matrices que compartan una dimensión. Es decir, la primera matriz tendrá tantas columnas como renglones la segunda. El producto entre matrices está dado como:
$$(AB)_{i,j} = \sum_k A_{i,k} B_{k,j}$$#Producto matriz con vector
rank2_product = torch.matmul(A,B.T)
print('{}\n • \n{}\n = \n{}'.format(A,B.T, rank2_product))
tensor([[0, 5, 1], [3, 0, 2], [0, 2, 0], [5, 3, 1]]) • tensor([[10, 9, 8, 1], [ 0, 4, 1, 0], [ 2, 1, 0, 0]]) = tensor([[ 2, 21, 5, 0], [34, 29, 24, 3], [ 0, 8, 2, 0], [52, 58, 43, 5]])
Los tensores de rango 3 o mayores pueden multiplicarse con otros tensores de menor o igual rango. Por ejemplo, podemos multiplicar un vector de rango 3 por un vector como:
$$(Tx)_{i,j} = \sum_{k} T_{i,j,k} x_k$$Por su parte, también podemos multiplicar el tensor por una matriz, de la siguiente forma:
$$(TA)_{i,j,k} = \sum_{l} T_{i,j,l} A_{l,k}$$#Crea tensores
T = torch.rand(3,3,3)
A = torch.rand(3,2)
x = torch.rand(3,)
#Producto por vector
vector_product = torch.matmul(T,x)
#Producto por matriz
matrix_product = torch.matmul(T,A)
print('Tensor de rango 3 por vector')
print('{}\n • \n{}\n = \n{}'.format(T,x, vector_product))
print('\n\nTensor de rango 3 por matriz')
print('{}\n • \n{}\n = \n{}'.format(T,A, matrix_product))
Tensor de rango 3 por vector tensor([[[0.8923, 0.0408, 0.9621], [0.9718, 0.5970, 0.0816], [0.1249, 0.3443, 0.0333]], [[0.6729, 0.8477, 0.6490], [0.5623, 0.0241, 0.5339], [0.0615, 0.1971, 0.2775]], [[0.3309, 0.1102, 0.6387], [0.8850, 0.8717, 0.8342], [0.7162, 0.9685, 0.1346]]]) • tensor([0.7574, 0.6017, 0.1718]) = tensor([[0.8657, 1.1093, 0.3075], [1.1312, 0.5321, 0.2128], [0.4267, 1.3382, 1.1484]]) Tensor de rango 3 por matriz tensor([[[0.8923, 0.0408, 0.9621], [0.9718, 0.5970, 0.0816], [0.1249, 0.3443, 0.0333]], [[0.6729, 0.8477, 0.6490], [0.5623, 0.0241, 0.5339], [0.0615, 0.1971, 0.2775]], [[0.3309, 0.1102, 0.6387], [0.8850, 0.8717, 0.8342], [0.7162, 0.9685, 0.1346]]]) • tensor([[0.8014, 0.3434], [0.0322, 0.7288], [0.9656, 0.0135]]) = tensor([[[1.6455, 0.3492], [0.8769, 0.7700], [0.1434, 0.2943]], [[1.1933, 0.8576], [0.9670, 0.2179], [0.3235, 0.1685]], [[0.8856, 0.2026], [1.5429, 0.9505], [0.7352, 0.9537]]])
Finalmente, podemos realizar el producto entre tensores de mayor rango. Por ejemplo, entre tensores de rango 3. En todos estos casos, como vemos, se utiliza la función torch.matmul().
#Crea un tensor de rango 3
T2 = torch.rand(3,3,3)
#Producto entre tensores
rank3_product = torch.matmul(T,T2)
print('{}\n • \n{}\n = \n{}'.format(T,T2, rank3_product))
tensor([[[0.8923, 0.0408, 0.9621], [0.9718, 0.5970, 0.0816], [0.1249, 0.3443, 0.0333]], [[0.6729, 0.8477, 0.6490], [0.5623, 0.0241, 0.5339], [0.0615, 0.1971, 0.2775]], [[0.3309, 0.1102, 0.6387], [0.8850, 0.8717, 0.8342], [0.7162, 0.9685, 0.1346]]]) • tensor([[[0.9370, 0.8247, 0.2572], [0.6798, 0.5555, 0.4123], [0.8644, 0.3421, 0.8773]], [[0.4420, 0.4221, 0.0691], [0.1596, 0.2319, 0.4798], [0.1060, 0.8297, 0.2443]], [[0.9635, 0.7712, 0.1022], [0.4589, 0.1896, 0.2119], [0.0781, 0.7673, 0.6103]]]) = tensor([[[1.6956, 1.0878, 1.0905], [1.3870, 1.1611, 0.5678], [0.3799, 0.3057, 0.2033]], [[0.5015, 1.0190, 0.6118], [0.3090, 0.6859, 0.1809], [0.0880, 0.3018, 0.1666]], [[0.4194, 0.7662, 0.4470], [1.3180, 1.4879, 0.7843], [1.1451, 0.8393, 0.3606]]])
El producto de Hadamard es un producto punto a punto, en donde cada entrada de los tensores se multiplican entre si. Es decir, se tiene que:
$$(T \odot U)_{i_1,...,i_n} = T_{i_1,...,i_n}U_{i_1,...,i_n}$$Este producto suele hacerse por medio del operador *.
#Producto punto a punto
had = x*y
print('{}\n o \n{}\n = \n{}'.format(x,y, had))
tensor([0.7574, 0.6017, 0.1718]) o tensor([1, 2, 2]) = tensor([0.7574, 1.2035, 0.3436])
El producto externo es importante para algunas operaciones entre vectores, pues produce una matriz cuyas entradas son productos entre los elementos de ambos vectores. En este caso, el resultado se obtiene como:
$$(x \otimes y)_{i,j} = x_i y_j $$#Producto externo
outer = torch.outer(x,y)
print('{}\n x \n{}\n = \n{}'.format(x,y, outer))
tensor([0.7574, 0.6017, 0.1718]) x tensor([1, 2, 2]) = tensor([[0.7574, 1.5147, 1.5147], [0.6017, 1.2035, 1.2035], [0.1718, 0.3436, 0.3436]])