Redes convolucionales con PyTorch#

Importación de las bibliotecas necesarias#

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as T
from torchvision import datasets
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt

Clasificación en MNIST#

Para demostrar la eficacia de las redes neuronales convolucionales 2D, comenzaremos con el ejemplo clásico de clasificación de los dígitos del 0 al 9 del conjunto de datos MNIST. Retomaremos el código de los cuadernos anteriores, en particular el relacionado con las redes completamente conectadas (fully connected).

Obtención del conjunto de datos y creación de los dataloaders para el entrenamiento#

transform=T.ToTensor() # Pour convertir les éléments en tensor torch directement
dataset = datasets.MNIST(root='./../data', train=True, download=True,transform=transform)
test_dataset = datasets.MNIST(root='./../data', train=False,transform=transform)
plt.imshow(dataset[1][0].permute(1,2,0).numpy(), cmap='gray')
plt.show()
print("Le chiffre sur l'image est un "+str(dataset[1][1]))
../_images/f4a9ef394c4101290b338c61429d10ce6c2203d5ec105a078ee447cc4eeabda3.png
Le chiffre sur l'image est un 0
train_dataset, validation_dataset=torch.utils.data.random_split(dataset, [0.8,0.2])
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
val_loader= DataLoader(validation_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

Creación de una red neuronal convolucional en PyTorch#

class cnn(nn.Module):
  def __init__(self, *args, **kwargs) -> None:
    super().__init__(*args, **kwargs)
    self.conv1=nn.Conv2d(1,8,kernel_size=3,padding=1) # Couche de convolution de 8 filtres
    self.conv2=nn.Conv2d(8,16,kernel_size=3,padding=1) # Couche de convolution de 16 filtres
    self.conv3=nn.Conv2d(16,32,kernel_size=3,padding=1) # Couche de convolution de 32 filtres
    self.pool1=nn.MaxPool2d((2,2)) # Couche de max pooling 
    self.pool2=nn.MaxPool2d((2,2))
    self.fc=nn.Linear(32*7*7,10)
  
  # La fonction forward est la fonction appelée lorsqu'on fait model(x)
  def forward(self,x):
    x=F.relu(self.conv1(x))
    x=self.pool1(x)
    x=F.relu(self.conv2(x))
    x=self.pool2(x)
    x=F.relu(self.conv3(x))
    x=x.view(-1,32*7*7) # Pour convertir la feature map de taille 32x7x7 en taille vecteur de taille 1568
    output=self.fc(x)
    return output
model = cnn()
print(model)
print("Nombre de paramètres", sum(p.numel() for p in model.parameters()))
cnn(
  (conv1): Conv2d(1, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv2): Conv2d(8, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv3): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool1): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), padding=0, dilation=1, ceil_mode=False)
  (pool2): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), padding=0, dilation=1, ceil_mode=False)
  (fc): Linear(in_features=1568, out_features=10, bias=True)
)
Nombre de paramètres 21578

Como puedes observar, el número de parámetros del modelo es considerablemente menor que en una red completamente conectada clásica.

Entrenamiento del modelo#

Definamos la función de pérdida y los hiperparámetros de entrenamiento:

criterion = nn.CrossEntropyLoss()
epochs=5
learning_rate=0.001
optimizer=torch.optim.Adam(model.parameters(),lr=learning_rate)

Ahora, entrenemos el modelo:

for i in range(epochs):
  loss_train=0
  for images, labels in train_loader:
    preds=model(images)
    loss=criterion(preds,labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    loss_train+=loss   
  if i % 1 == 0:
    print(f"step {i} train loss {loss_train/len(train_loader)}")
  loss_val=0    
  for images, labels in val_loader:
    with torch.no_grad(): # permet de ne pas calculer les gradients
      preds=model(images)
      loss=criterion(preds,labels)
      loss_val+=loss 
  if i % 1 == 0:
    print(f"step {i} val loss {loss_val/len(val_loader)}")
step 0 train loss 0.318773478269577
step 0 val loss 0.08984239399433136
step 1 train loss 0.08383051306009293
step 1 val loss 0.0710655003786087
step 2 train loss 0.05604167655110359
step 2 val loss 0.0528845489025116
step 3 train loss 0.04518255963921547
step 3 val loss 0.051780227571725845
step 4 train loss 0.03614392504096031
step 4 val loss 0.0416230633854866

Calculemos la precisión en el conjunto de prueba:

correct = 0
total = 0
for images,labels in test_loader: 
  with torch.no_grad():
    preds=model(images)
    
    _, predicted = torch.max(preds.data, 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()     
test_acc = 100 * correct / total
print("Précision du modèle en phase de test : ",test_acc)
Précision du modèle en phase de test :  98.64

Como puedes ver, se obtiene una precisión de aproximadamente 99%, en comparación con el 97.5% de una red completamente conectada que tiene 10 veces más parámetros.

Softmax e interpretación de los resultados#

Si ejecutamos el modelo con un elemento, obtenemos un vector de este tipo:

images,labels=next(iter(test_loader))

#Isolons un élément 
image,label=images[0].unsqueeze(0),labels[0].unsqueeze(0) # Le unsqueeze permet de garder la dimension batch
pred=model(image)
print(pred)
tensor([[ -9.6179,  -5.1802,  -2.6094,   0.9121, -16.0603,  -8.6510, -30.0099,
          14.4031,  -9.0074,  -2.0431]], grad_fn=<AddmmBackward0>)

Al observar detenidamente los resultados, vemos que el valor del 7.º elemento es el más alto. Por lo tanto, el modelo predijo un 3. Verifiquemos con la etiqueta correspondiente:

print(label)
tensor([7])

En realidad, se trata de un 7. Sin embargo, preferiríamos tener una probabilidad de pertenencia a la clase, ya que esto es más legible y proporciona un grado de confianza fácilmente interpretable.

Para ello, utilizamos la función de activación softmax.

Esta función se define de la siguiente manera: \(\sigma(z)_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}\), donde \(K\) es el número de clases del modelo.

Esta función se denomina softmax porque amplifica el valor máximo mientras reduce los demás. Se utiliza para obtener una distribución de probabilidad, ya que la suma de los valores para las \(K\) clases es igual a 1.

print(F.softmax(pred))
tensor([[3.6964e-11, 3.1266e-09, 4.0884e-08, 1.3833e-06, 5.8867e-14, 9.7215e-11,
         5.1481e-20, 1.0000e+00, 6.8068e-11, 7.2027e-08]],
       grad_fn=<SoftmaxBackward0>)
/tmp/ipykernel_232253/3904022211.py:1: UserWarning: Implicit dimension choice for softmax has been deprecated. Change the call to include dim=X as an argument.
  print(F.softmax(pred))

Ahora, los valores son probabilidades y podemos ver que el 7 se predice con una probabilidad del 99.9%. Por lo tanto, el modelo está muy seguro de su predicción.

Otras aplicaciones#

Las redes convolucionales han destacado por ser particularmente útiles en el campo del procesamiento de imágenes. También se utilizan en áreas como el procesamiento de audio y video, por ejemplo.

Los cuadernos siguientes de este curso muestran aplicaciones de las redes convolucionales en casos más interesantes que MNIST. Se recomienda contar con una GPU o utilizar los cuadernos en Google Colab para obtener tiempos de entrenamiento razonables (aun así, puedes seguir el curso sin entrenar los modelos).