01. Fundamentos de PyTorch y Descenso de Gradiente¶
Este primer tutorial cubre los siguientes temas:
- Introducciones a los tensores PyTorch
- Operaciones con tensores
- Introducción a los gradientes tensoriales
- Interoperabilidad entre PyTorch y Numpy
Instalación de PyTorch¶
Nota: Antes de ejecutar cualquier código en este cuaderno, deberías haber pasado por los pasos de instalación de PyTorch.
Si estás ejecutando en Google Colab, no es necesario instalar ninguna librería (Google Colab tiene PyTorch y otras bibliotecas preinstaladas).
Como alternativa, también se puede instalar directamente el requirments.txt localizado el repositorio de este notebook.
Una vez instalado, se importa PyTorch y se comprueba la versión utilizada.
import torch
import numpy as np
torch.__version__
'2.0.1'
Introducción a los tensores con Pytorch¶
Tensores¶
En esencia, PyTorch es una biblioteca para procesar tensores. Un tensor es un número, vector, matriz o cualquier array de n dimensiones (también denominados simplemente tensores).
Para empezar de forma sencilla, se creará un tensor con un solo número.
# Escalares en PyTorch
# ======================================================================================
t1 = torch.tensor(4.)
t1
tensor(4.)
t1.ndim
0
4. es una abreviatura de 4.0. Se utiliza para indicar a Python (y PyTorch) que desea crear un número de coma flotante. Se puede verificar esto comprobando el atributo dtype de nuestro tensor.
# Tipo de dato de un tensor
# ======================================================================================
t1.dtype
torch.float32
Creación de vectores
# Vector de 1 dimensión
# ======================================================================================
t2 = torch.tensor([1., 2, 3, 4])
t2_int = torch.tensor([1, 2, 3, 4])
print(t2)
print(t2.dtype)
print(t2_int)
print(t2_int.dtype)
tensor([1., 2., 3., 4.]) torch.float32 tensor([1, 2, 3, 4]) torch.int64
Como se puede ver, todos los números del tensor tienen el mismo tipo.
La dimensión ahora es 1.
t2.ndim
1
# Matrices
# ======================================================================================
t3 = torch.tensor([[5., 6],
[7, 8],
[9, 10]])
t3
tensor([[ 5., 6.],
[ 7., 8.],
[ 9., 10.]])
t3.ndim
2
Los tensores propiamente dichos son aquellos arrays de 3 dimensiones o más, aunque se suele hablar de tensores siempre que son de objeto tensor. Los tensores son la estructura de datos básica en PyTorch.
Los tensores son similares a los arrays de NumPy, pero pueden ser usados en GPUs para acelerar los cálculos, como se verá más adelante.
# Tensor tridimensional
# ======================================================================================
t4 = torch.tensor([
[[11, 12, 13, 10],
[11, 12, 13, 10],
[13, 14, 15, 10]],
[[15, 16, 17, 10],
[11, 12, 13, 10],
[17, 18, 19., 10]]])
t4
tensor([[[11., 12., 13., 10.],
[11., 12., 13., 10.],
[13., 14., 15., 10.]],
[[15., 16., 17., 10.],
[11., 12., 13., 10.],
[17., 18., 19., 10.]]])
Los tensores pueden tener cualquier número de dimensiones y diferentes longitudes a lo largo de cada dimensión. Se puede inspeccionar la longitud a lo largo de cada dimensión usando la propiedad .shape de un tensor.
Al igual que pasa con numpy, no es posible crear tensores con una dimensionalidad incompatible.
# Tensor con dimensiones imcompatibles
# ======================================================================================
# t5 = torch.tensor([[5., 6, 11],
# [7, 8],
# [9, 10]])
El ValueError se debe a que las longitudes de las filas [5., 6, 11] y [7, 8] no coinciden.
Tensores aleatorios¶
Los modelos de aprendizaje automático como las redes neuronales manipulan y buscan patrones dentro de los tensores. Cuando se construyen modelos de aprendizaje automático con PyTorch, es raro que se creen tensores a mano.
Sin embargo, un modelo de aprendizaje automático a menudo comienza con grandes tensores de números aleatorios (weights and biases) que posteriormente se ajustan estos números aleatorios a medida que trabaja a través de los datos para representarlos mejor.
Para crear tensores con números aleatorios entre [0,1] se utiliza la funicón torch.rand () pasando el parámetro size.
# Tensor con valores aleatorios de dimensiones (3, 4)
# ======================================================================================
tensor_aleatorio = torch.rand(size=(3, 4))
# Se imprime por pantalla
tensor_aleatorio, tensor_aleatorio.dtype, tensor_aleatorio.shape, tensor_aleatorio.ndim
(tensor([[0.8955, 0.7819, 0.9867, 0.6980],
[0.8123, 0.1524, 0.3739, 0.8194],
[0.0155, 0.1711, 0.2164, 0.8552]]),
torch.float32,
torch.Size([3, 4]),
2)
Operaciones con tensores¶
En el aprendizaje profundo, los datos (imágenes, texto, video, audio, estructuras de proteínas, etc.) se representan como tensores.
Para codificar una red neuronal, es necesario realizar operaciones básicas entre tensores:
- Suma
- Resta
- Multiplicación (elemento a elemento)
- División
- Multiplicación de matrices
Comencemos con algunas de las operaciones fundamentales, suma (+), resta (-), multiplicación (*).
Funcionan tal como piensas que lo harían, como en numpy.
# Suma
# ======================================================================================
tensor = torch.tensor([1, 2, 3])
tensor + 10
tensor([11, 12, 13])
# Multiplicación por un escalar
# ======================================================================================
tensor * 10
tensor([10, 20, 30])
# Resta
# ======================================================================================
tensor = tensor - 10
tensor
tensor([-9, -8, -7])
Multiplicación de matrices¶
Una de las operaciones más comunes en los algoritmos de aprendizaje automático y aprendizaje profundo (como las redes neuronales) es la [multiplicación de matrices](https://www.mathsisfun.com/algebra/matrix-multiplying.html).
PyTorch implementa la funcionalidad de multiplicación de matrices en el método torch.matmul().
Las dos reglas principales para la multiplicación de matrices a recordar son:
- Las dimensiones internas deben coincidir:
(3, 2) @ (3, 2)no funcionará(2, 3) @ (3, 2)funcionará(3, 2) @ (2, 3)funcionará
- La matriz resultante tiene la forma de las dimensiones externas:
(2, 3) @ (3, 2)->(2, 2)(3, 2) @ (2, 3)->(3, 3)
Nota: "
@" en Python es el símbolo para la multiplicación de matrices.Recurso: Puede ver todas las reglas para la multiplicación de matrices usando
torch.matmul()en la documentación de PyTorch.
tensor = torch.tensor([1, 2, 3])
tensor.shape
torch.Size([3])
La diferencia entre la multiplicación elemento a elemento y la multiplicación de matrices es la adición de valores.
Para nuestra variable tensor con valores [1, 2, 3]:
| Operación | Cálculo | Código |
|---|---|---|
| Multiplicación elemento a elemento | [1*1, 2*2, 3*3] = [1, 4, 9] |
tensor * tensor |
| Multiplicación de matrices | [1*1 + 2*2 + 3*3] = [14] |
tensor.matmul(tensor) |
# Multiplicación elemento a elemento de tensores
# ======================================================================================
tensor * tensor
tensor([1, 4, 9])
# Multiplicación matricial con el operador @
# ======================================================================================
tensor @ tensor
tensor(14)
# Multiplicación matricial con el método matmul
# ======================================================================================
torch.matmul(tensor, tensor)
tensor(14)
Cálculo de los valores maximo, minimo, media y suma de un tensor¶
# Se crea un tensor
tensor = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
tensor.dtype
torch.int64
# Cálculo de los valores maximo, minimo, media y suma de un tensor
# ======================================================================================
x = torch.tensor([1,2,1,3,1,2], dtype=torch.float32) # para calcular la media hay que convertir a float
print(f"Min: {x.min()}")
print(f"Max: {x.max()}")
print(f"Media: {x.mean()}")
print(f"Suma: {x.sum()}")
Min: 1.0 Max: 3.0 Media: 1.6666666269302368 Suma: 10.0
Min/Max posicional¶
También se puede encontrar el índice de un tensor donde ocurre el máximo o el mínimo con [`torch.argmax()`](https://pytorch.org/docs/stable/generated/torch.argmax.html) y [`torch.argmin()`](https://pytorch.org/docs/stable/generated/torch.argmin.html) respectivamente.
Esto es útil en caso de que sólo se quiera la posición donde está el valor más alto (o más bajo) y no el valor en sí (lo veremos en una sección posterior cuando usemos la función de activación softmax).
# Obtención del índice del valor máximo y mínimo de un tensor
# ======================================================================================
# Se crea un tensor secuencial
tensor = torch.arange(10, 100, 10)
print(f"Tensor: {tensor}")
# Se devuelve el índice del valor máximo y mínimo
print(f"Index where max value occurs: {tensor.argmax()}")
print(f"Index where min value occurs: {tensor.argmin()}")
Tensor: tensor([10, 20, 30, 40, 50, 60, 70, 80, 90]) Index where max value occurs: 8 Index where min value occurs: 0
Ejercicio Hasta ahora se han cubierto algunos métodos de tensor, pero hay muchos más en la documentación de
torch.Tensor, se recomienda revisar la web y repasar cualquier función o método que llame la atención.
Otras funciones tensoriales¶
El módulo `torch` también contiene muchas funciones para crear y manipular tensores.
# Crear un tensor con un valor fijo para cada elemento
# ======================================================================================
tensor = torch.full((3, 2), 42)
tensor
tensor([[42, 42],
[42, 42],
[42, 42]])
Para aplicar funciones matemáticas a un tensor, hay que realizarlo a través de una función de torch. En la documentación de PyTorch, se pueden encontrar muchas otras funciones matematicas. Se recomienda dedicar un tiempo a revisar la documentación.
# Seno de un tensor
# ======================================================================================
tensor_sin = torch.sin(tensor)
tensor_sin
tensor([[-0.9165, -0.9165],
[-0.9165, -0.9165],
[-0.9165, -0.9165]])
Reorganización, apilamiento y permutación¶
Frecuentemente se quiere reorganizar o cambiar las dimensiones de los tensores sin cambiar los valores que contienen.
Para hacerlo, algunos métodos populares son:
| Método | Descripción |
|---|---|
torch.reshape(input, shape) |
Reorganiza input a shape (si es compatible), también se puede usar torch.Tensor.reshape(). |
torch.Tensor.view(shape) |
Devuelve una vista del tensor original en una shape diferente pero comparte los mismos datos que el tensor original. |
torch.stack(tensors, dim=0) |
Concatena una secuencia de tensors a lo largo de una nueva dimensión (dim), todos los tensors deben tener el mismo tamaño. |
torch.permute(input, dims) |
Devuelve una vista del input original con sus dimensiones permutadas (reorganizadas) a dims. |
redes neuronales) se tratan de manipular tensores de alguna manera. Y debido a las reglas de la multiplicación de matrices, si hay incompatibilidades de forma, se producirán errores. Estos métodos te ayudan a asegurarte de que los elementos correctos de tus tensores se mezclan con los elementos correctos de otros tensores.
# Se crea un tensor de 1 dimensión con los valores del 1 al 7
# ======================================================================================
x = torch.arange(1., 8.)
x, x.shape
(tensor([1., 2., 3., 4., 5., 6., 7.]), torch.Size([7]))
Añadimos una dimensión extra con torch.reshape().
# Se añade una dimension extra al tensor x
# ======================================================================================
x_reshaped = x.reshape(1, 7)
x_reshaped, x_reshaped.shape
(tensor([[1., 2., 3., 4., 5., 6., 7.]]), torch.Size([1, 7]))
Con view se puede hacer lo mismo, pero sin crear una copia del tensor original.
# Con el método view
# ======================================================================================
z = x.view(1, 7)
z, z.shape
(tensor([[1., 2., 3., 4., 5., 6., 7.]]), torch.Size([1, 7]))
# Cambiamos un elemento y se cambia en los dos tensores
# ======================================================================================
z[:, 0] = 5
z, x
(tensor([[5., 2., 3., 4., 5., 6., 7.]]), tensor([5., 2., 3., 4., 5., 6., 7.]))
Los tensores pueden ser cambiados de dimensiones, pero la cantidad de elementos debe ser la misma. Y, por lo tanto, las dimensiones deben de ser compatibles.
Por ejemplo, un tensor de dimensión (10, 10, 3) tiene 300 elementos. Lo podríamos cambiár a la diemensión (30, 10), pero no a otra incompatible como (4, 10, 10)
# Se crea un tensor
# ======================================================================================
x = torch.randn(10, 10, 3)
x.shape
torch.Size([10, 10, 3])
# Cambiamos la dimesión de un tensor
# ======================================================================================
y = x.reshape(30,10)
print(y.shape)
y = x.reshape(3,10,10)
print(y.shape)
# Dimensiones incompatibles
# y = x.reshape(4,10,10)
# print(y.shape)
torch.Size([30, 10]) torch.Size([3, 10, 10])
Si se utiliza el -1 como valor de una dimensión, esta se calculará automáticamente para que la cantidad de elementos sea la misma.
# Equivale a poner -1 en la dimensión que se quiere calcular automáticamente
y = x.reshape(3,-1,10)
print(y.shape)
torch.Size([3, 10, 10])
Se pueede obtener más información sobre las operaciones de tensor aquí: https://pytorch.org/docs/stable/torch.html. Se recomienda experimentar 5-10 funciones y operaciones de tensor para familiarizarse con la librería.
Para apilar tensores se utiliza la función torch.stack().
# Apilar tensores - horizontalmente
# ======================================================================================
x = torch.tensor([1, 2, 3, 4])
x_stacked = torch.stack([x, x, x, x], dim=0)
x_stacked
tensor([[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4]])
# Apilar tensores - verticalmente
# ======================================================================================
x = torch.tensor([1, 2, 3, 4])
x_stacked = torch.stack([x, x, x, x], dim=1)
x_stacked
tensor([[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[4, 4, 4, 4]])
También se puede reordenar el orden de los valores de los ejes con torch.permute(input, dims), donde el input se convierte en una vista con nuevos dims.
# Permutar tensor
# Se crea un tensor con una forma específica
x_original = torch.rand(size=(224, 224, 3))
# Se permuta el tensor
x_permuted = x_original.permute(2, 0, 1) # shifts axis 0->1, 1->2, 2->0
print(f"Inicial: {x_original.shape}")
print(f"Final: {x_permuted.shape}")
Inicial: torch.Size([224, 224, 3]) Final: torch.Size([3, 224, 224])
Nota: Debido a que permutar devuelve una vista (comparte los mismos datos que el original), los valores en el tensor permutado serán los mismos que el tensor original y si cambias los valores en la vista, cambiará los valores del original.
Tensores y Gradientes¶
Una de las propiedades más importantes de PyTorch es que se pueden calcular los gradientes, o derivadas, de los tensores. Esto es muy útil para el entrenamiento de redes neuronales, ya que se puede calcular el error de la red y ajustar los pesos para minimizar el error con el algoritmo del descenso del gradiente.
Como se verá más adelante, el algoritmo del descenso del gradiente es el que se utiliza para ajustar los pesos de la red. Este algoritmo consiste en calcular el gradiente de la función de error con respecto a los pesos, y actualizar los pesos en la dirección opuesta al gradiente, ya que en esa dirección la función de error decrece más rápidamente.
Por este motivo, es muy importante que la librería utilizada permita calcular de forma eficiente los gradientes de un tensor. A continuación mostramos un ejemplo.
# Creación de tensores
# ======================================================================================
x = torch.tensor(3.)
w = torch.tensor(4., requires_grad=True)
b = torch.tensor(5., requires_grad=True)
x, w, b
(tensor(3.), tensor(4., requires_grad=True), tensor(5., requires_grad=True))
Se han creado tres tensores: x, w y b, todos son simplemente números. w y b tienen un parámetro adicional requires_grad establecido en True. Ahora se verá su importante función.
Ahora se creará un nuevo tensor y combinando estos tensores.
# Operación aritmetica
# ======================================================================================
y = w * x**2 + b
y
tensor(41., grad_fn=<AddBackward0>)
Como era de esperar, y es un tensor con el valor $4 * 3^2 + 5 = 41$.
Lo que hace único a PyTorch es que podemos calcular automáticamente la derivada de y!
Los tensores que tienen requires_grad establecido en True, es decir, w y b. Esta característica de PyTorch se llama_autograd_ (gradientes automáticos).
Para calcular las derivadas, podemos invocar el método .backward en nuestro resultado y.
# Calculamos las derivadas
# ======================================================================================
y.backward()
Las derivadas de y con respecto a los tensores de entrada se almacenan en la propiedad .grad de los respectivos tensores. Se puede observar que x tiene una derivada igual a None porque no se ha incluido el parámetro requires_grad en su definición.
# Obtención de las derivadas
# ======================================================================================
print('dy/dx:', x.grad)
print('dy/dw:', w.grad)
print('dy/db:', b.grad)
dy/dx: None dy/dw: tensor(9.) dy/db: tensor(1.)
Como era de esperar, dy/dw tiene el mismo valor que x, es decir, 3, y dy/db tiene el valor 1.
Se presenta a continuación un segundo ejemplo:
$y=2*x^2$
Donde,
- dy/dx = 4x
- como x=3, dy/dx = 4*3 = 12
# Otro ejemplo
# ======================================================================================
x = torch.tensor(3., requires_grad=True)
y = 2*x**2
y.backward()
x.grad
tensor(12.)
Esta propiedad de los tensores es muy útil para la implementación de redes neuronales, ya que permite definir el tamaño de las capas de forma dinámica, en función de los datos de entrada.
Interoperabilidad con Numpy¶
Numpy es una popular biblioteca de código abierto que se utiliza para la computación matemática y científica en Python. Permite operaciones eficientes en grandes arrays multidimensionales y tiene un vasto ecosistema de bibliotecas de soporte, que incluyen:
Pandas para E/S de archivos y análisis de datos Matplotlib para trazado y visualización
- OpenCV para procesamiento de imágenes y videos
Una pregunta que surge a menudo es por qué necesitamos una biblioteca como PyTorch, ya que Numpy ya proporciona estructuras de datos y utilidades para trabajar con datos numéricos multidimensionales.
Hay dos razones principales:
- Autograd: la capacidad de calcular gradientes automáticamente para operaciones de tensor es esencial para entrenar modelos de aprendizaje profundo.
- Compatibilidad con GPU: al trabajar con conjuntos de datos masivos y modelos grandes, las operaciones de tensor de PyTorch se pueden realizar de manera eficiente utilizando una Unidad de procesamiento de gráficos (GPU). Los cálculos que normalmente pueden llevar horas se pueden completar en minutos usando GPU.
Aprovecharemos ampliamente estas dos funciones de PyTorch en esta serie de tutoriales.
En lugar de reinventar la rueda, PyTorch interactúa bien con Numpy para aprovechar su ecosistema existente de herramientas y bibliotecas.
Así es como se crea una matriz en Numpy:
# Creación de un array de numpy
# ======================================================================================
x = np.array([[1, 2], [3, 4.]])
x
array([[1., 2.],
[3., 4.]])
Se puede convertir una matriz Numpy en un tensor PyTorch de forma muy sencilla usando torch.from_numpy.
# Cambio de numpy a tensor
# ======================================================================================
y = torch.from_numpy(x)
y
tensor([[1., 2.],
[3., 4.]], dtype=torch.float64)
Se verifica que la matriz numpy y el tensor de antorcha tengan tipos de datos similares.
x.dtype, y.dtype
(dtype('float64'), torch.float64)
También se pueda hacer el paso contrario: convertir un tensor PyTorch en una matriz Numpy. Para ello se utiliza el método .numpy de un tensor.
# Convertir un tensor de torch a un array de numpy
# ======================================================================================
z = y.numpy()
z
array([[1., 2.],
[3., 4.]])
La interoperabilidad entre PyTorch y Numpy es esencial porque la mayoría de los conjuntos de datos con los que trabajará probablemente se leerán y preprocesarán como matrices de Numpy.
Información de sesión¶
import session_info
session_info.show(html=False)
----- numpy 1.25.2 session_info 1.0.0 torch 2.0.1 ----- IPython 8.14.0 jupyter_client 8.3.0 jupyter_core 5.3.1 jupyterlab 4.0.5 notebook 7.0.3 ----- Python 3.10.12 (main, Jul 5 2023, 15:34:07) [Clang 14.0.6 ] macOS-10.16-x86_64-i386-64bit ----- Session information updated at 2023-09-02 11:33