Introdución a pytorch, tensores y gradientes

Introducción a PyTorch: Tensores y Gradientes

Fernando Carazo
Septiembre, 2023

En esta serie de artículos, se ofrece una introducción práctica al aprendizaje profundo (deep learning) utilizando PyTorch, una destacada librería de redes neuronales de código abierto desarrollada por el Laboratorio de Investigación de Inteligencia Artificial de Meta (Facebook). Los documentos que componen esta serie, incluido el presente, siguen un enfoque práctico que guiará al lector en la programación de redes neuronales utilizando PyTorch. La forma más efectiva de asimilar el material es ejecutar el código y realizar experimentos con él.

Este primer tutorial aborda los siguientes temas:

  • Introducción a los tensores en PyTorch.

  • Operaciones con tensores.

  • Fundamentos de los gradientes tensoriales.

  • Interoperabilidad entre PyTorch y Numpy.



Logo oficial de PyTorch. Fuente: Wikipedia archive.

Librerías

In [42]:
# Librerías utilizadas en el documento
# ==============================================================================
import torch
import numpy as np

  Warning

La instalación de PyTorch puede variar según el sistema operativo que estés utilizando. Si la instalación no se lleva a cabo correctamente al ejecutar el comando pip install torch, te recomendamos que consultes los pasos de instalación de PyTorch. Si estás utilizando Google Colab, no es necesario instalar ninguna biblioteca adicional, ya que Google Colab ya tiene PyTorch y otras librerías preinstaladas.


Dado que PyTorch es un proyecto en constante desarrollo, es conveniente verificar la versión instalada para asegurar la reproducibilidad de los resultados mostrados.

In [43]:
print(f"Versión de PyTorch: {torch.__version__}")
Versión de PyTorch: 2.0.1+cu118

Tensores con Pytorch

Un tensor es un concepto matemático que generaliza escalares, vectores y matrices hacia dimensiones superiores. Para ilustrarlo, un tensor de orden 0 se reduce a un escalar, mientras que un tensor de orden 1 se convierte en un vector. Cuando llegamos a una matriz de dos dimensiones, estamos ante un tensor de orden 2, y así sucesivamente hasta llegar a n dimensiones. Los tensores tienen una amplia aplicación en distintos ámbitos, desde las matemáticas y la física hasta la informática y el aprendizaje automático.

En el contexto del aprendizaje automático y el aprendizaje profundo, los tensores son la base de datos fundamental que representa las entradas, salidas y el estado interno de una red neuronal.

PyTorch, en su esencia, es una biblioteca especializada en el procesamiento de tensores. Ofrece una amplia variedad de funciones altamente optimizadas para operar eficientemente con estos objetos, incluyendo operaciones cruciales como multiplicaciones de matrices y convoluciones. Además, PyTorch se integra sin problemas con aceleradores de hardware como las GPU, lo que puede impulsar de manera significativa los cálculos necesarios en el aprendizaje profundo. Esta capacidad de PyTorch para trabajar con tensores y aprovechar al máximo el hardware subyacente lo convierte en una herramienta fundamental en el campo del aprendizaje automático y la inteligencia artificial.

  Note

Para aquellos lectores familiarizados con los arrays de Numpy, los tensores de PyTorch comparten similitudes en cuanto a las funcionalidades que ofrecen. Dos características distintivas de los tensores de PyTorch, no presentes en Numpy, son las siguientes: En primer lugar, PyTorch incluye un módulo llamado autograd que proporciona un sistema de diferenciación automática. Este componente resulta esencial para el entrenamiento de modelos de aprendizaje automático, ya que permite calcular gradientes de forma automática durante el proceso de retropropagación. Además, PyTorch está especialmente diseñado para ejecutarse tanto en CPU como en GPU, lo cual brinda una versatilidad fundamental en entornos de computación heterogénea. Como se ilustra más adelante, PyTorch y NumPy son compatibles entre sí, permitiendo una conversión sencilla entre tensores de PyTorch y arrays de NumPy. Esto facilita la integración de código escrito utilizando ambas librerías.

Vease a continuación varios ejemplos de cómo crear tensores en con Pytorch.

Escalares

In [44]:
# Escalares en PyTorch (tensor de orden 0)
# ======================================================================================
t1 = torch.tensor(4.)
print(t1)
print("Orden del tensor:", t1.dim())
tensor(4.)
Orden del tensor: 0

4. es una abreviatura de 4.0. Se emplea para indicar a Python (y PyTorch) que se desea crear un número en formato de coma flotante (float). Esto se puede verificar al revisar el atributo dtype de nuestro tensor.

In [45]:
# Tipo de dato de un tensor
# ======================================================================================
t1.dtype
Out[45]:
torch.float32

Vectores

In [46]:
# Vector de 1 dimensión (tensor de orden 1)
# ======================================================================================
t2 = torch.tensor([1, 2, 3, 4])
print(t2)
print(f"Orden del tensor: {t2.ndim}")
print(f"Forma del tensor: {t2.shape}")
print(f"Tipo de dato del tensor: {t2.dtype}")
tensor([1, 2, 3, 4])
Orden del tensor: 1
Forma del tensor: torch.Size([4])
Tipo de dato del tensor: torch.int64

Todos los elementos de un tensor tienen el mismo tipo. Por esta razón, al crear un tensor que combina valores float y valores int, el tensor resultante adquiere el tipo float.

In [47]:
t2_mix = torch.tensor([1.0, 2, 3, 4])
print(t2_mix)
print(f"Orden del tensor: {t2_mix.ndim}")
print(f"Forma del tensor: {t2_mix.shape}")
print(f"Tipo de dato del tensor: {t2_mix.dtype}")
tensor([1., 2., 3., 4.])
Orden del tensor: 1
Forma del tensor: torch.Size([4])
Tipo de dato del tensor: torch.float32

Matrices

In [48]:
# Matrices 
# ======================================================================================
t3 = torch.tensor([[5., 6], 
                   [7, 8], 
                   [9, 10]])
print(t3)
print(f"Orden del tensor: {t3.ndim}")
print(f"Forma del tensor: {t3.shape}")
print(f"Tipo de dato del tensor: {t3.dtype}")
tensor([[ 5.,  6.],
        [ 7.,  8.],
        [ 9., 10.]])
Orden del tensor: 2
Forma del tensor: torch.Size([3, 2])
Tipo de dato del tensor: torch.float32

Tensor de 3 dimensiones

In [49]:
# 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
Out[49]:
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.]]])

Tensor de orden $n$

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 el atributo .shape. Al igual que pasa con NumPy, no es posible crear tensores con una dimensionalidad incompatible.

In [50]:
# 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.

Generación de Tensores Aleatorios

En el contexto de los modelos de aprendizaje automático, como las redes neuronales, se manipulan y buscan patrones dentro de los tensores. Por lo general, un modelo de aprendizaje automático comienza con tensores de números aleatorios (pesos y bias) que posteriormente se ajustan a medida que procesa los datos de entrenamiento y aprende de ellos.

Para crear tensores con números aleatorios entre [0,1] se utiliza la funicón torch.rand () pasando el parámetro size.

In [51]:
# Tensor con valores aleatorios de dimensiones (3, 4)
# ======================================================================================
tensor_aleatorio = torch.rand(size=(3, 4))
print(tensor_aleatorio)
print(f"Orden del tensor: {tensor_aleatorio.ndim}")
print(f"Forma del tensor: {tensor_aleatorio.shape}")
print(f"Tipo de dato del tensor: {tensor_aleatorio.dtype}")
tensor([[0.4437, 0.7655, 0.8468, 0.9520],
        [0.7143, 0.4448, 0.9016, 0.3687],
        [0.3581, 0.4679, 0.4203, 0.8183]])
Orden del tensor: 2
Forma del tensor: torch.Size([3, 4])
Tipo de dato del tensor: torch.float32

Operaciones con tensores

Para poder crear, entrenar y luego realizar predicciones con una red neuronal, es esencial llevar a cabo operaciones fundamentales entre tensores, que incluyen:

  • Suma
  • Resta
  • Multiplicación (elemento a elemento)
  • División
  • Multiplicación de matrices
In [52]:
# Suma
# ======================================================================================
tensor = torch.tensor([1, 2, 3])
tensor + 10
Out[52]:
tensor([11, 12, 13])
In [53]:
# Multiplicación por un escalar
# ======================================================================================
tensor * 10
Out[53]:
tensor([10, 20, 30])
In [54]:
# Resta
# ======================================================================================
tensor = tensor - 10
tensor
Out[54]:
tensor([-9, -8, -7])

Multiplicación de matrices

La multiplicación de matrices es una de las operaciones más comunes en algoritmos de aprendizaje automático y aprendizaje profundo, como las redes neuronales. En PyTorch, esta funcionalidad se implementa a través del método torch.matmul(). Hay dos reglas principales a tener en cuenta al multiplicar matrices:

  1. Las dimensiones internas deben coincidir:

    • (3, 2) @ (3, 2) no es válido
    • (2, 3) @ (3, 2) es válido
    • (3, 2) @ (2, 3) es válido
  2. La matriz resultante tiene la forma de las dimensiones externas:

    • (2, 3) @ (3, 2) -> (2, 2)
    • (3, 2) @ (2, 3) -> (3, 3)

  Tip

"`@`" en Python es el símbolo para la multiplicación de matrices. Se pueden conultar todas las reglas para la multiplicación de matrices usando `torch.matmul()` [en la documentación de PyTorch](https://pytorch.org/docs/stable/generated/torch.matmul.html).

Es importante diferenciar entre la multiplicación elemento a elemento (element wise) y la multiplicación de matrices. Por ejemplo, para un 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)
In [55]:
tensor = torch.tensor([1, 2, 3])
In [56]:
# Multiplicación elemento a elemento de tensores
# ======================================================================================
tensor = torch.tensor([1, 2, 3])
tensor * tensor
Out[56]:
tensor([1, 4, 9])
In [57]:
# Multiplicación matricial con el operador @
# ======================================================================================
tensor @ tensor
Out[57]:
tensor(14)
In [58]:
# Multiplicación matricial con el método matmul
# ======================================================================================
torch.matmul(tensor, tensor)
Out[58]:
tensor(14)

Cálculo del valor máximo, mínimo, média y suma de un tensor

In [59]:
# Cálculo del valor máximo, mínimo, média 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

Índices de los Valores Máximo y Mínimo

Es posible determinar el índice de un tensor donde se encuentra el valor máximo o mínimo utilizando las funciones torch.argmax() y torch.argmin(). Esta operación resulta útil en situaciones en las que sólo se requiere conocer la posición del valor más alto (o más bajo), y no el valor en sí. Veremos un ejemplo de esto más adelante cuando utilicemos la función de activación softmax.

In [60]:
# Obtención del índice del valor máximo y mínimo de un tensor
# ======================================================================================
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

  Note

PyTorch proporciona una amplia gama de funcionalidades. A continuación, se presentan algunas de las más comúnmente utilizadas, sin embargo, se recomienda explorar la documentación completa de [`torch.Tensor`](https://pytorch.org/docs/stable/tensors.html) para aprovechar al máximo su potencial.

Reorganización, apilamiento y permutación

Frecuentemente, surge la necesidad de reorganizar o modificar las dimensiones de los tensores sin alterar los valores que contienen. Para lograrlo, existen varios métodos

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.

Debido a las restricciones de las reglas de multiplicación de matrices, pueden surgir incompatibilidades de forma que generen errores. Estos métodos permiten asegurar de que los elementos correctos de los tensores se combinan correctamente con los elementos de otros tensores.

In [61]:
# Tensor de 1 dimensión con los valores del 1 al 7
# ======================================================================================
x = torch.arange(1., 8.)
print(x)
print(x.shape)
tensor([1., 2., 3., 4., 5., 6., 7.])
torch.Size([7])

Se añade una dimensión extra con torch.reshape().

In [62]:
x_reshaped = x.reshape(1, 7)
print(x_reshaped)
print(x_reshaped.shape)
tensor([[1., 2., 3., 4., 5., 6., 7.]])
torch.Size([1, 7])

El método view se puede conseguir lo mismo, pero sin crear una copia del tensor original.

In [63]:
x_view_reshaped= x.view(1, 7)
print(x_view_reshaped)
print(x_view_reshaped.shape)
tensor([[1., 2., 3., 4., 5., 6., 7.]])
torch.Size([1, 7])

Dado que se trata de una vista, cualquier modificación realizada en un elemento de esta vista también afectará al tensor al que la vista hace referencia.

In [64]:
x_view_reshaped[:, 0] = 500
print(x_view_reshaped)
print(x)
tensor([[500.,   2.,   3.,   4.,   5.,   6.,   7.]])
tensor([500.,   2.,   3.,   4.,   5.,   6.,   7.])

Los tensores pueden ser redimensionados, siempre y cuando el número total de elementos se mantenga constante. Por ende, las nuevas dimensiones deben ser compatibles. Por ejemplo, un tensor de dimensiones (10, 10, 3) contiene 300 elementos. Es posible reconfigurarlo a (30, 10), pero no sería posible realizar un cambio a una dimensión incompatible, como (4, 10, 10).

In [65]:
x = torch.randn(10, 10, 3)
x.shape
Out[65]:
torch.Size([10, 10, 3])
In [66]:
y = x.reshape(30,10)
print(y.shape)
torch.Size([30, 10])
In [67]:
y = x.reshape(3,10,10)
print(y.shape)
torch.Size([3, 10, 10])
In [68]:
# Dimensiones incompatibles
# y = x.reshape(4,10,10)
# print(y.shape)

Cuando se utiliza el valor -1 en el método reshape, se está especificando a Pytorch que calcule automáticamente el tamaño de esa dimensión en particular. Esto resulta útil cuando se desea remodelar un tensor sin tener que calcular explícitamente el tamaño de una dimensión. Es importante tener en cuenta que solo se puede especificar una dimensión como -1. Si se intenta usar -1 para varias dimensiones, Pytorch no podrá inferir los tamaños correctos y generará un error.

In [69]:
y = x.reshape(3, -1, 10)
print(y.shape)
torch.Size([3, 10, 10])

Para apilar tensores se utiliza la función torch.stack().

In [70]:
# Apilar tensores - horizontalmente
# ======================================================================================
x = torch.tensor([1, 2, 3, 4])
x_stacked = torch.stack([x, x, x, x], dim=0) 
print(x_stacked)
print(x_stacked.shape)
tensor([[1, 2, 3, 4],
        [1, 2, 3, 4],
        [1, 2, 3, 4],
        [1, 2, 3, 4]])
torch.Size([4, 4])
In [71]:
# Apilar tensores - verticalmente
# ======================================================================================
x = torch.tensor([1, 2, 3, 4])
x_stacked = torch.stack([x, x, x, x], dim=1)
print(x_stacked)    
print(x_stacked.shape)
tensor([[1, 1, 1, 1],
        [2, 2, 2, 2],
        [3, 3, 3, 3],
        [4, 4, 4, 4]])
torch.Size([4, 4])

torch.permute(input, dims) es una función en PyTorch que se utiliza para permutar las dimensiones de un tensor. Esto significa que puedes cambiar el orden de las dimensiones de un tensor sin alterar los datos en sí. La función permute devuelve un nuevo tensor con las dimensiones reorganizadas según lo especificado. Por ejemplo, si tienes un tensor tridimensional tensor con forma (a, b, c), puedes usar tensor.permute(x, y, z) para cambiar el orden de las dimensiones a (x, y, z).

In [72]:
# Permutar tensor
# ======================================================================================
x_original = torch.rand(size=(224, 224, 3))
x_permuted = x_original.permute(2, 0, 1) # shifts axis 0->1, 1->2, 2->0
print(f"Shape de x_original: {x_original.shape}")
print(f"Shape de x_permuted: {x_permuted.shape}")
Shape de x_original: torch.Size([224, 224, 3])
Shape de x_permuted: torch.Size([3, 224, 224])

  Note

Dado que la función permute devuelve una vista, los valores en el tensor permutado apuntan a los mismos valores que los del tensor original. Por lo tanto, si se modifican los valores en la vista, también se modificarán en el tensor original.

Tensores y Gradientes

Una de las capacidades más poderosas de PyTorch es su habilidad para calcular gradientes o derivadas de sus tensores. Como se esxplica en siguientes artículos, esta característica es fundamental en el entrenamiento de redes neuronales, ya que permite evaluar el error de la red y ajustar los pesos utilizando el algoritmo del descenso del gradiente.

Para habilitar la funcionalidad de gradientes automáticos (autograd), se emplea el argumento requires_grad al crear un tensor. Cuando se establece requires_grad=True, PyTorch registra cada operación realizada en ese tensor, lo que facilita el cálculo posterior de los gradientes. A continuación, se muestran varios ejemplos de cómo calcular y acceder a los gradientes de un tensor.

Se crean tres tensores: x, w y b, todos ellos con valores de tipo float. Los tensores w y b tienen un parámetro adicional requires_grad establecido en True.

In [73]:
# Creación de tensores
# ======================================================================================
x = torch.tensor(3.)
w = torch.tensor(4., requires_grad=True)
b = torch.tensor(5., requires_grad=True)
print(x)
print(w)
print(b)
tensor(3.)
tensor(4., requires_grad=True)
tensor(5., requires_grad=True)

Ahora se creará un nuevo tensor y combinando estos tensores.

In [74]:
y = w * x**2 + b
y
Out[74]:
tensor(41., grad_fn=<AddBackward0>)

Como era de esperar, y es un tensor con el valor de $4 * 3^2 + 5$, lo que da como resultado 41. Hasta este punto, todo es lo que se espera de una librería de cálculo convencional. Lo que hace a PyTorch especialmente poderoso es su capacidad para calcular automáticamente la derivada de y.

El primer paso para calcular las derivadas es llamar al método backward. Este método no devuelve ninguna salida que se pueda asignar a una variable. En su lugar, su propósito es calcular los gradientes y actualizar los tensores que tienen requires_grad=True.

In [75]:
# Calculo de las derivadas
# ======================================================================================
y.backward()

Después de llamar al método backward de un tensor, los gradientes se almacenan en los tensores que han estado involucrados en el cálculo que ha dado lugar a dicho tensor. En concreto, se almacenan en el atributo .grad.

In [76]:
# 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.)

Se puede observar que x tiene una derivada igual a None, esto se debe a que no se indicó en requires_grad=True en su definición.

Vease un ejemplo más:

$y=2*x^2$

Donde,

  • dy/dx = 4x
  • como x=3, dy/dx = 4*3 = 12
In [77]:
x = torch.tensor(3., requires_grad=True)
y = 2*x**2
y.backward()
x.grad
Out[77]:
tensor(12.)

Interoperabilidad con Numpy

Numpy es una popular librería de código abierto utilizada para la computación matemática y científica en Python. Permite realizar operaciones eficientes en grandes arreglos multidimensionales y cuenta con un amplio ecosistema de librerías que la utilizan como base para expandir sus funcionalidades. El lector puede preguntarse por qué se necesita una biblioteca como PyTorch cuando Numpy ya proporciona estructuras de datos y utilidades para trabajar con datos numéricos multidimensionales. Sin embargo, existen dos razones principales:

  • Autograd: la capacidad de calcular gradientes automáticamente para operaciones con tensores. Además,

  • Compatibilidad con GPU: las operaciones de tensores en PyTorch pueden realizarse de manera eficiente utilizando la Unidad de Procesamiento de Gráficos (GPU).

Estas dos características hacen de PyTorch una librería muy potente para desarrollar modelos de aprendizaje profundo (deep learning). Cabe destacar que PyTorch no reemplaza a Numpy, sino que está altamente integrado con él para aprovechar lo mejor de ambos ecosistemas.

Se puede convertir un array Numpy en un tensor PyTorch de forma muy sencilla usando torch.from_numpy.

In [78]:
# Conversion de numpy a tensores
# ======================================================================================
x_numpy = np.array([[1, 2], [3, 4.]])
x_tensor = torch.from_numpy(x_numpy)
print(x_numpy)
print(x_tensor)
print(f"dtype de x_numpy: {x_numpy.dtype}")
print(f"dtype de x_tensor: {x_tensor.dtype}")
[[1. 2.]
 [3. 4.]]
tensor([[1., 2.],
        [3., 4.]], dtype=torch.float64)
dtype de x_numpy: float64
dtype de x_tensor: torch.float64

También se pueda hacer el proceso contrario: convertir un tensor PyTorch en un array Numpy. Para ello se utiliza el método .numpy del tensor.

In [79]:
# Conversion de tensores a numpy
# ======================================================================================
x_tensor = torch.tensor([[1, 2], [3, 4.]])
x_numpy = x_tensor.numpy()
print(x_tensor)
print(x_numpy)
print(f"dtype de x_tensor: {x_tensor.dtype}")
print(f"dtype de x_numpy: {x_numpy.dtype}")
tensor([[1., 2.],
        [3., 4.]])
[[1. 2.]
 [3. 4.]]
dtype de x_tensor: torch.float32
dtype de x_numpy: float32

Información de sesión

In [80]:
import session_info
session_info.show(html=False)
-----
numpy               1.24.3
session_info        1.0.0
torch               2.0.1+cu118
-----
IPython             8.12.0
jupyter_client      8.1.0
jupyter_core        5.3.0
-----
Python 3.10.10 | packaged by Anaconda, Inc. | (main, Mar 21 2023, 18:39:17) [MSC v.1916 64 bit (AMD64)]
Windows-10-10.0.19045-SP0
-----
Session information updated at 2023-09-19 23:39

¿Cómo citar este documento?

Introducción a PyTorch: Tensores y Gradientes por Fernando Carazo, disponible con licencia Attribution 4.0 International (CC BY 4.0) en https://www.cienciadedatos.net/documentos/pydl01-introduccion-pytorch.html DOI


¿Te ha gustado el artículo? Tu ayuda es importante

Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊


Creative Commons Licence
This work by Fernando Carazo is licensed under a Creative Commons Attribution 4.0 International License.