Tensores en PyTorch
Contents
Tensores en PyTorch#
Introducción#
Los tensores son similares a los ndarrays de NumPy
, excepto que los tensores pueden ejecutarse en GPU u otros aceleradores de hardware. De hecho, los tensores y las matrices NumPy a menudo pueden compartir la misma memoria subyacente, lo que elimina la necesidad de copiar datos (consulte Puente con NumPy). Los tensores también están optimizados para la diferenciación automática.
En esta lección aprenderemos la manera de manipular tensores en PyTorch.
Importar Torch#
import torch
import numpy as np
print("Using torch", torch.__version__)
Using torch 1.11.0+cu102
Inicializa un Tensor#
Directamente de los datos#
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
x_data
tensor([[1, 2],
[3, 4]])
A partir de arreglos Numpy#
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
x_np
tensor([[1, 2],
[3, 4]])
Desde otro tensor
# semilla para reproductibilidad
torch.manual_seed(100)
x_ones = torch.ones_like(x_data) # retiene las propiedades de x_data
print(f"Tensor Unos: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float)# sobre escribe le tipo de dato
print(f"Tensor Aleatorio: \n {x_rand} \n")
Tensor Unos:
tensor([[1, 1],
[1, 1]])
Tensor Aleatorio:
tensor([[0.1117, 0.8158],
[0.2626, 0.4839]])
Con valores aleatorios y constantes#
shape = (2,3,)
rand_tensor = torch.rand(shape) # Uniform[0,1]
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
print(f"Tensor Aleatorio: \n {rand_tensor} \n")
print(f"Tensor Unos: \n {ones_tensor} \n")
print(f"Tensor Ceros: \n {zeros_tensor}")
Tensor Aleatorio:
tensor([[0.6765, 0.7539, 0.2627],
[0.0428, 0.2080, 0.1180]])
Tensor Unos:
tensor([[1., 1., 1.],
[1., 1., 1.]])
Tensor Ceros:
tensor([[0., 0., 0.],
[0., 0., 0.]])
tensor = torch.rand(3,4)
print(f"Forma del tensor: {tensor.shape}")
print(f"Tipo de datos del tensor: {tensor.dtype}")
print(f"Dispositivo en donde está el tensor: {tensor.device}")
Forma del tensor: torch.Size([3, 4])
Tipo de datos del tensor: torch.float32
Dispositivo en donde está el tensor: cpu
Operaciones sobre Tensores#
Indexación y rebanado (slicing) al estilo Numpy#
tensor = torch.ones(4, 4)
print('Primera fila: ', tensor[0])
print('Primera columna: ', tensor[:,0])
print('Ultima columna: ', tensor[...,-1])
# modifica columna 1 del tensor
tensor[:,1] = 0
print(tensor)
Primera fila: tensor([1., 1., 1., 1.])
Primera columna: tensor([1., 1., 1., 1.])
Ultima columna: tensor([1., 1., 1., 1.])
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])
Concatenaciones de tensores#
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)
tensor([[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.]])
Operaciones aritméticas#
# Multiplicación matricial
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)
y3 = torch.rand_like(tensor)
torch.matmul(tensor, tensor.T, out=y3)
# Multiplicación elemento a elemento
z1 = tensor * tensor
z2 = tensor.mul(tensor)
z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out = z3)
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])
Agregación#
agg = tensor.sum()
agg_item = agg.item()
print('agg = ', agg)
print('agg_item = ', agg_item)
print('type(gg_item):', type(agg_item))
agg = tensor(12.)
agg_item = 12.0
type(gg_item): <class 'float'>
Operaciones in place#
print(tensor, '\n')
tensor.add_(5)
print(tensor)
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])
tensor([[6., 5., 6., 6.],
[6., 5., 6., 6.],
[6., 5., 6., 6.],
[6., 5., 6., 6.]])
Puente con Numpy#
t = torch.ones(5)
print(f't: {t}')
n = t.numpy()
print(f'n: {n}')
# el cambio se refleja en el arreglo numpy
t.add_(1)
print(f't: {t}')
print(f'n: {n}')
t: tensor([1., 1., 1., 1., 1.])
n: [1. 1. 1. 1. 1.]
t: tensor([2., 2., 2., 2., 2.])
n: [2. 2. 2. 2. 2.]
Cambio de forma de los tensores#
x = torch.arange(6)
print('x:', x)
x: tensor([0, 1, 2, 3, 4, 5])
x.view(2, 3)
tensor([[0, 1, 2],
[3, 4, 5]])
# aplanando 4 imágenes
batch_size = 4
n_rows = 2
n_columns = 2
x = torch.rand(batch_size, n_rows, n_columns)
print(x)
tensor([[[0.6211, 0.5530],
[0.6896, 0.3687]],
[[0.9053, 0.8356],
[0.3039, 0.6726]],
[[0.5740, 0.9233],
[0.9178, 0.7590]],
[[0.7775, 0.6179],
[0.3379, 0.2170]]])
x = x.view(batch_size, n_rows*n_columns)
print(x)
tensor([[0.6211, 0.5530, 0.6896, 0.3687],
[0.9053, 0.8356, 0.3039, 0.6726],
[0.5740, 0.9233, 0.9178, 0.7590],
[0.7775, 0.6179, 0.3379, 0.2170]])
x = x.permute(1,0)
print(x)
tensor([[0.6211, 0.9053, 0.5740, 0.7775],
[0.5530, 0.8356, 0.9233, 0.6179],
[0.6896, 0.3039, 0.9178, 0.3379],
[0.3687, 0.6726, 0.7590, 0.2170]])
Grafo de cálculo dinámico y retropropagación(backpropagation)#
Una de las principales razones para usar PyTorch en proyectos de aprendizaje profundo es que podemos obtener automáticamente gradientes/derivadas de las funciones
que definamos. Principalmente usaremos PyTorch para implementar redes neuronales, y ellas son solamente funciones sofisticadas. Si usamos matrices de peso en nuestra función que queremos aprender, entonces se llaman parámetros o simplemente pesos.
Si nuestra red neuronal tuviera un solo parámetro, hablaríamos de tomar la derivada, pero verá que siempre tendremos múltiples variables; en ese caso hablamos de gradientes. Es un término más general.
Dada una entrada \(\mathbf{x}\), definimos nuestra función manipulando esa entrada, generalmente mediante multiplicaciones de matrices con matrices de peso(weight) y sumas con los llamados vectores de sesgo(bias). A medida que manipulamos nuestra entrada, estamos creando automáticamente un grafo computacional. Este grafo muestra cómo llegar a nuestra salida a partir de nuestra entrada.
PyTorch es un marco definido por ejecución; esto significa que podemos simplemente hacer nuestras manipulaciones, y PyTorch hará un seguimiento de ese grafo por nosotros. Por lo tanto, creamos un grafo de cálculo dinámico en el camino.
Ejemplo#
Primero creamos un ejemplo de tensor simple y luego le agregaremos el grafo de cálculo dinámico.
x = torch.ones((3,))
print(x.requires_grad)
False
x.requires_grad = True
print(x.requires_grad)
True
Vamos a crear la siguiente función $\( y = \frac{1}{|x|} \sum_i [(x_i+2)^2 + 3], \)$
para luego calcular su gradiente \(\frac{\partial y}{\partial x}\) en el punto \([1, 2, 3]\). En la expresión, \(|x|\) será el tamaño(forma) de \(x\).
x = torch.arange(3, dtype=torch.float32, requires_grad=True) # solamente puede usar floats para calcular gradientes
print('x: ', x)
x: tensor([0., 1., 2.], requires_grad=True)
Ahora construimos el grafo de la función
a = x + 2
b = a**2
c = b + 3
y = c.mean()
print('y = ', y)
y = tensor(12.6667, grad_fn=<MeanBackward0>)
Cálculo del gradiente#
y.backward()
x.grad() contiene el gradiente \(\frac{\partial y}{\partial x}\) en el punto \(x = [1, 2, 3]\)
print(x.grad)
tensor([1.3333, 2.0000, 2.6667])
El cálculo se realizó de la siguiente forma
Entonces, si \(x = [1, 2, 3]\) se obtiene \(\frac{\partial y}{\partial x} = [4/3, 2, 8/3]\).
Soporte para GPU#
gpu_avail = torch.cuda.is_available()
print(f"¿Hay GPU disponible? {gpu_avail}")
¿Hay GPU disponible? False
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
print("Device", device)
Device cpu
x = torch.zeros(2, 3)
x = x.to(device)
print("X", x)
X tensor([[0., 0., 0.],
[0., 0., 0.]])
x.device
device(type='cpu')