TICS-579-Deep Learning

Clase 4: Model Training

Alfonso Tobar-Arancibia

Entrenamiento de la Red

A diferencia de un Modelo de Machine Learning, las Redes Neuronales se entrenan de manera progresiva (se espera una mejora en cada Epoch). Si nuestra Arquitectura es apropiada nosotros deberíamos esperar que el Loss de nuestra red siempre disminuya. ¿Por qué?

💡 Dado que el entrenamiento es progresivo, el modelo puede retomar su entrenamiento desde un set de pesos dados.

¿Siempre buscamos la Red que tenga el mejor Loss de Entrenamiento? ¿Cuál es la diferencia entre el Loss y el Rendimento del Modelo?

Al igual que en los modelos de Machine Learning debemos evitar a toda costa el Overfitting. ¿Qué es el overfitting?

Entrenamiento de la Red

Bias-Variance Tradeoff (Dilema Sesgo-Varianza)

Probablemente el concepto más importante para determinar si un modelo tiene potencial o no. Corresponden a dos tipos de errores que pueden sufrir los modelos de ML.

Bias

Corresponde al sesgo, y tiene que ver con la diferencia entre el valor real y el valor predicho. Bajo sesgo implica una mejor predicción.

Variance

Corresponde a la varianza y tiene que ver con la dispersión de los valores predichos. Baja Varianza implica un modelo más estable y menos flexible.

En general hay que buscar el equilibrio entre ambos tipos de errores:

  • Alto Sesgo y baja Varianza: Underfitting.
  • Bajo Sesgo y Alta Varianza: Overfitting.

Model Validation

Validación Cruzada

Se refiere al proceso de entrenar un modelo en una cierta porción de los datos, pero validar sus rendimiento y capacidad de generalización en un set de datos no vistos por el modelo al momento de entrenar.

¿Qué es la Generalización?

Los dos métodos más populares que se usan en Machine Learning son Holdout y K-Fold. Más métodos se pueden encontrar en los docs de Scikit-Learn.

Debido a los volúmenes de datos utilizados, el esquema de validación más utilizado es el Holdout.

Model Validation: Holdout

Train

Corresponde a la porción de utilizado para que el modelo aprenda.

Validation

Corresponde a la porción de datos no vistos por el modelo durante el entrenamiento. Se utiliza para medir el nivel de generalización del modelo.

Test

Se utiliza para evaluar reportando una métrica de diseño del Modelo.

A diferencia de un modelo de Machine Learning el proceso de validación del modelo se realiza en conjunto con el entrenamiento. Es decir, se entrena y valida el modelo Epoch a Epoch.

Model Validation: K-Fold

Corresponde al proceso de Holdout pero repetido \(K\) veces. No es tan utilizado en Deep Learning debido a los altos costos computacionales.

¿Qué es Pytorch?

Pytorch

Es una librería de manipulación de Tensores especializada en Deep Learning. Provee principalmente, manipulación de tensores (igual que Numpy, pero en GPU), además de Autograd (calcula derivadas de manera automática).

Para poder comenzar a utilizarlo se requieren normalmente 3 imports:

import torch
import torch.nn as nn
import torch.nn.functional as F

👀

  • torch es donde se encuentran la mayoría de funciones básicas para manipular tensores.
  • torch.nn es donde se encuentran los módulos necesarios para poder crear redes neuronales (neural networks). Cada módulo es una clase en Python.
  • torch.nn.functional es donde se encontrarán utility functions además de versiones funcionales de elementos de torch.nn.

👀 Una versión funcional es capaz de replicar la operación de un módulo pero no tiene la capacidad de almacenar los parámetros aprendidos.

Pytorch: Modelo

Una capa en Pytorch

  • Son elementos importados desde torch.nn.
  • Estos módulos deben ser instanciados para luego ser utilizados.
  • Cada capa tiene guarda sus parámetros como atributos:
    • .weight.data y .bias.data (para los pesos y bias respectivos).
    • Ojo: Pytorch utiliza los parámetros de manera transpuesta a como lo aprendimos en clases.
## Ejemplo de un capa de parámetros en Pytorch
## Proyecta desde 4 dimensiones a 12 dimensiones
fc1 = nn.Linear(in_features = 4, out_features=12)

## Forward Pass:
## Calcula las activaciones de la capa
fc1(X)

Output: Activaciones de la capa

tensor([[ 0.2620, -0.5313, 0.3907, …, -0.0451, -1.2113, -2.9476], [-1.2096, -0.2586, -0.1372, …, 0.1342, -1.1130, -1.4764], [-1.7676, -0.3754, -0.3786, …, 0.2964, -1.2234, -1.6176], …, [-2.1432, -0.3500, -0.5168, …, 0.3048, -1.1075, -1.1932], [-2.6030, 0.0033, -0.6494, …, 0.8202, -1.6117, -1.0077], [ 0.1126, -0.4532, 0.3573, …, 0.0660, -1.0688, -2.4856]], grad_fn=)

:::
:::{.column}

::: {.callout-tip style="font-size: 90%;" icon=false appearance="default"} 
## Un clase en Pytorch permite crear redes más complejas y poseen 2 métodos principales:
  * Todas las clases deben heredar de `nn.Module`.

* `__init__()`:
  * Se inicializan los módulos con `super().__init__()`.
  * Se definen las capas de la red como atributos de la clase.
    * self.nombre_de_la_capa = nn.Capa(...)
    * self.funcion = nn.Funcion(...)
* `forward()`:
  * Define como se conectan las capas en el Forward Pass.

:::

```{.python style="font-size: 110%;"}
class MLP(nn.Module):
  def __init__(self, in_features, out_features):
    super().__init__()
    
    ## Defincición de capas
    self.fc1 = nn.Linear(in_features, out_features)

  def forward(self,x):
    x = self.fc1(x)
    return x

model = MLP(in_features=4, out_features = 12)
model(X)

Pytorch: Crear modelos más complejos

class MLP2(nn.Module):
  def __init__(self, in_features, out_features):
    super().__init__()
    self.fc1 = nn.Linear(in_features, out_features)
    self.relu = nn.ReLU(inplace = True)
    self.fc2 = nn.Linear(out_features, 1)

  def forward(self,x):
    x = self.fc1(x)
    x = self.relu(x)
    x = self.fc2(x)
    return x

class SuperMLP(nn.Module):
  def __init__(self):
    super().__init__()
    self.mlp1 = MLP(in_features=4, out_features=12)
    self.mlp2 = MLP2(in_features=12, out_features=8)
  def forward(self, x):
    x = self.mlp1(x)
    x = self.mlp2(x)
    return x

super_model = SuperMLP()
logits = super_model(X)
logits.shape

Es posible combinar distintos nn.Module en un sólo modelo.

super_model
SuperMLP(
  (mlp1): MLP(
    (fc1): Linear(in_features=4, out_features=12, bias=True)
  )
  (mlp2): MLP2(
    (fc1): Linear(in_features=12, out_features=8, bias=True)
    (relu): ReLU()
    (fc2): Linear(in_features=8, out_features=1, bias=True)
  )
)

Pytorch: Visualización del Modelo

super_model
SuperMLP(
  (mlp1): MLP(
    (fc1): Linear(in_features=4, out_features=12, bias=True)
  )
  (mlp2): MLP2(
    (fc1): Linear(in_features=12, out_features=8, bias=True)
    (relu): ReLU()
    (fc2): Linear(in_features=8, out_features=1, bias=True)
  )
)
from torchinfo import summary
summary(super_model)
=================================================================
Layer (type:depth-idx)                   Param #
=================================================================
SuperMLP                                 --
├─MLP: 1-1                               --
│    └─Linear: 2-1                       60
├─MLP2: 1-2                              --
│    └─Linear: 2-2                       104
│    └─ReLU: 2-3                         --
│    └─Linear: 2-4                       9
=================================================================
Total params: 173
Trainable params: 173
Non-trainable params: 0
=================================================================

Pytorch: Optimizador y Loss Function

Loss Function

  • El Loss Function a utilizar le llamaremos criterion.
criterion = nn.BCEWithLogitsLoss()

Optimizer

  • El Optimizador lo llamaremos optimizer y se importa desde torch.optim.
  • Además el optimizador debe ser instanciado junto con los parámetros del modelo y la tasa de aprendizaje lr (learning rate).
optimizer = torch.optim.Adam(super_model.parameters(), lr = 3e-4)

Pytorch: Training Loop

loss_history = []
for e in range(epochs):
  # Definición del modo del Modelo
  super_model.train()
  optimizer.zero_grad()

  # Forward Pass
  logits = super_model(X)
  loss = criterion(logits, y)
  ## Calcula los gradientes (Backward Pass)
  loss.backward()

  # Actualización de los Pesos
  optimizer.step()

  # Log del Modelo
  loss_history.append(loss.item())

Elementos clave del Training Loop

  • Pytorch requiere fijar el Modo de la Red. Para entrenar se utiliza model.train().
    • Pytorch tiene la costumbre de Acumular Gradientes. Por lo tanto, antes de cada Loop, se deben reiniciar los gradientes a cero utilizando optimizer.zero_grad().
  • El Forward Pass lo llamaremos con logits = super_model(X). Esto permite calcular los Logits y las variables intermedias necesarias para el Backward Pass.
  • El error/loss de la red lo calculamos con loss = criterion(logits, y).
  • El cálculo de gradientes lo llamaremos loss.backward(). Esto calcula los gradientes y los acumula en cada parámetro del modelo.
  • El optimizador lo llamaremos optimizer. Y llamaremos al proceso de actualizar pesos como optimizer.step().

Pytorch: Validation Loop

loss_history = []
val_loss_history = []
for e in range(epochs):
  # Definición del modo del Modelo
  super_model.train()
  optimizer.zero_grad()

  # Forward Pass
  logits = super_model(X)
  loss = criterion(logits, y)
  ## Calcula los gradientes (Backward Pass)
  loss.backward()

  # Actualización de los Pesos
  optimizer.step()

  # Log de Entrenamiento
  loss_history.append(loss.item())

  # Validation Loop
  super_model.eval()

  # Evita cálculo de gradientes
  with torch.no_grad():
    val_logits = super_model(X_val)
    val_loss = criterion(val_logits, y_val)
    # Log de Validación
    val_loss_history.append(val_loss.item())

Elementos clave del Validation Loop

  • Se debe fijar el modo del modelo a evaluación con model.eval(). No es necesario calcular gradientes en Validación ya que no hay actualización de parámetros.
    • with torch.no_grad(): desactiva el cálculo de gradientes.
  • El Forward Pass lo llamaremos con val_logits = super_model(X_val). Esto permite calcular los Logits para poder calcular el loss de Validación.
  • El error/loss de la red lo calculamos como val_loss = criterion(val_logits, y_val).

Pytorch: Model Evaluation

Nos referimos a la evaluación del modelo como la medición de la performance esperada por nuestro modelo. La Evaluación del Modelo se realiza en torno a una métrica definida a priori por el modelador. La métrica a utilizar está íntimamente ligada al problema a resolver.

Clasificación

  • \(Accuracy = \frac{1}{m} \sum_{i = 1}^m 1\{y_i = \hat{y_i}\}\)
  • \(Precision = \frac{TP}{TP + FP}\)
  • \(Recall = \frac{TP}{TP + FN}\)
  • \(F1-Score = 2 \cdot \frac{Precision \cdot Recall}{Precision + Recall}\)

Regresión

  • \(RMSE = \frac{1}{m} \sum_{i=1}^m (y_i-\hat{y_i})^2\)
  • \(MAE = \frac{1}{m} \sum_{i=1}^m |y_i - \hat{y_i}|\)
  • \(MAPE = 100 \cdot \frac{1}{m} \sum_{i=1}^m \frac{|y_i-\hat{y_i}|}{max(\epsilon,y_i)}\)
  • \(SMAPE = \frac{2}{m} \sum_{i=1}^2 \frac{|y_i - \hat{y_i} |}{max(|y_i + \hat{y_i}|,\epsilon)}\)

🤓

Las métricas presentadas son las básicas para clasificación y regresión. Existen otras más específicas según el campo:

  • IoU en segmentación semántica,
  • MAP@k en recomendación,
  • BLEU o ROUGE en NLP, etc.
  • Una lista extensa puede consultarse en Torchmetrics .

Pytorch: Training-Validation Loop + Evaluación

loss_history, val_loss_history = [], []
train_metric_history, val_metric_history = [], []
for e in range():
  train_metric = BinaryAccuracy()
  val_metric = BinaryAccuracy()

  ## Training Loop
  super_model.train()
  optimizer.zero_grad()
  logits = super_model(X)
  loss = criterion(logits, y)
  loss.backward()
  optimizer.step()
  loss_history.append(loss.item())
  acc = train_metric(logits, y)
  train_metric_history.append(acc)
  tr_acc = train_metric.compute()

  # Validation Loop
  super_model.eval()
  with torch.no_grad():
    val_logits = super_model(X_val)
    val_loss = criterion(val_logits, y_val)
    val_loss_history.append(val_loss.item())
    acc = train_metric(val_logits, y_val)
    val_metric_history.append(acc)
    val_acc = test_metric.compute()

Elementos clave de este loop

  • Adicional al cálculo de Loss se calcula alguna métrica de interés para el problema.
  • Se pueden loguear cuantas métricas se deseen.

Monitoreo de un Modelo: Validation Curve

Es importante ser capaz de identificar el momento exacto en el cual el momento comienza su overfitting. Para ello se utiliza el “Checkpointing”.

Checkpoint

  • Corresponde a un snapshot del modelo a un cierto punto. En la práctica se almacenan los parámetros del mejor modelo y del último Epoch.

EarlyStopping

  • Teoricamente, una vez que la red Neuronal alcanza el punto de Overfitting ya no tiene sentido seguir el entrenamiento. Por lo tanto es posible detener el entrenamiento bajo una cierta condición.

Entrenamiento Eficiente: GPU

La principal ventaja de frameworks como Pytorch es su ejecución en GPU, la cual ofrece una enorme capacidad de cómputo por la gran cantidad de núcleos.

🤓 Las GPUs usan CUDA (una variante compleja de C++), por lo que los errores suelen ser crípticos. Por ello se recomienda desarrollar en CPU y pasar a GPU solo cuando el código ya funcione correctamente y sea necesario entrenar.

## Permite automáticamente reconocer si es que existe GPU en el sistema 
## y de existir lo asigna como el dispositivo de entrenamiento.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

🖥️ El código anterior es particularmente útil para plataformas como Google Colab donde se permite activar o desactivar el uso de GPU.

## Fija el dispositivo de entrenamiento a CPU
device = torch.device("cpu")

Entrenamiento Eficiente: Pytorch Dataset

Pytorch posee varias estrategias para hacer más eficiente el entrenamiento de Redes Neuronales. Dentro de las estrategias más comunes está el entrenamiento en GPU en Mini Batches.

Pytorch Dataset

Corresponde a una clase que hereda de torch.utils.data.Dataset, la cual define la forma en que se cargan los datos. Su principal ventaja es que permite realizar una carga perezosa (lazy loading), es decir, los datos se leen únicamente en el momento en que son requeridos.

from torch.utils.data import Dataset

class ExampleDataset(Dataset):
  def __init__(self, x,y):
    ## Convertimos a numpy arrays
    self.X = x.to_numpy()
    self.y = y.values

  def __len__(self):
    return len(self.X)

  def __getitem__(self, idx):
    return dict(
        ## Tranformarmos cada índice en Tensor de Floats.
        x=torch.from_numpy(self.X)[idx].float(),
        y=torch.from_numpy(self.y)[idx].float()
        )

train_data = ExampleDataset(X_train, y_train)
val_data = ExampleDataset(X_val, y_val)

__init__: Inicializa el dataset solicitando los datos requeridos.

__len__: Define como se calcula la cantidad de muestras del dataset.

__getitem__: Define qué devuelve el elemento \(i\) del dataset. Es decir train_data[i] o val_data[i].

Entrenamiento Eficiente: Pytorch Dataloader

Pytorch Dataloader

Corresponde a una clase que permite cargar los datos por batch. Además permite paralelizar la carga de datos utilizando múltiples workers y controlar aspectos como qué hacer con el último batch (si es que no es completo) o si es necesario mezclar los datos (shuffling). La instancia del Dataloader también es lazy.

from torch.utils.data import DataLoader
train_loader=DataLoader(train_data, batch_size=32, num_workers=10, pin_memory=True, drop_last=True, shuffle=True)
val_loader=DataLoader(val_data, batch_size=32, num_workers=10, pin_memory=True, shuffle=False)
  • batch_size: Tamaño del mini-batch.
  • num_workers: Número de procesos a utilizar para cargar los datos.
  • pin_memory: Si es True, las GPU destinan un espacio especial en memoria para acelerar la transferencia de datos.
  • drop_last: Si es True, se descarta el último batch si no es completo.
  • shuffle: Si es True, se mezclan los datos en cada epoch.

Para entrenar en GPU es necesario que el modelo y los datos estén GPU.

Para pasar el modelo a la GPU se utiliza super_model.to(device).

Para pasar los datos a la GPU se utiliza X.to(device) o y.to(device).

Pytorch: Training-Validation Loop + Dataloader

  epoch_loss=dict(train=[], val=[])
  epoch_metric=dict(train=[], val=[])
  super_model.to(device)
  for e in range(epochs):
    train_metric, val_metric = BinaryAccuracy(), BinaryAccuracy()
    batch_loss=dict(train=[], val=[])

    model.train()
    for batch in train_loader:
      X, y = batch["x"].to(device), batch["y"].to(device)
      optimizer.zero_grad()
      logits = super_model(X)
      loss = criterion(logits, y)
      loss.backward()
      acc = train_metric(logits, y)
      optimizer.step()
      batch_loss["train"].append(loss.item())

    tr_acc = train_metric.compute()
    train_epoch_loss = np.mean(batch_loss["train"])
    epoch_metric["train"].append(tr_acc)

    model.eval()
    with torch.no_grad():
      for batch in val_loader:
        X, y = batch["x"].to(device), batch["y"].to(device)
        logits = super_model(X)
        loss = criterion(logits, y)
        batch_loss["val"].append(loss.item())
        acc = val_metric(logits, y)

      val_acc = val_metric.compute()
      val_epoch_loss = np.mean(batch_loss["val"])
      epoch_metric["val"].append(val_acc)

    epoch_loss["train"].append(train_epoch_loss)
    epoch_loss["val"].append(val_epoch_loss)

👀 Detalles clave de este loop

  • Tanto para el Training Loop como para el Validation Loop se itera sobre el DataLoader en la GPU.
  • Se definen métricas dentro de la Epoch. Las cuales serán utilizadas para calcular por batch y por epoch (utilizando .compute()).
  • Se definen métricas separadas para entrenamiento y validación.
  • Normalmente se loguean el Loss y una o más métricas por Epoch para construir las curvas de validación.

Detach

En ocasiones no es posible convertir un tensor a un valor numérico (float, int, etc) si es que el tensor requiere gradientes. En estos casos es necesario “desconectar” el tensor del grafo de cómputo utilizando .detach() y luego conviertiendolo a Numpy.

data.detach().numpy()

Categorical Variables: One Hot Encoding

Hasta ahora hemos asumido que las variables de entrada son numéricas. Pero en la práctica es muy común encontrarse con variables categóricas. En Deep Learning existen dos técnicas para lidiar con este tipo de variables: One-Hot-Encoding y el uso de Embeddings.

📋 One-Hot-Encoding

  • Permite una representación dispersa (sparse) de las variables categóricas. Consiste en crear una columna por cada categoría, y asignar un 1 o un 0 dependiendo si la instancia pertenece o no a dicha categoría.

  • Es una representación estática sin parámetros entrenables asociados.

  • Genera tantas columnas/features nuevas como categorías existan en la variable original. Por lo tanto, puede generar problemas de dimensionalidad si existen muchas categorías.

Categorical Variables: Embeddings

🗞️ Embeddings

  • Un embedding es una representación numérica de un objeto (palabra, imagen, nodo, etc.) en un espacio vectorial de menor dimensión que captura sus características y relaciones de manera útil para un modelo de machine learning.

  • En lugar de trabajar con las categorías crudas, los embeddings convierten esos datos en vectores de números reales. De esta forma, objetos similares quedan representados por vectores cercanos.

  • Los vectores son aprendidos por el modelo durante el entrenamiento, de tal manera que la representación aprendida es la optima para el problema específico a resolver. Es decir, agrega parámetros entrenables al modelo.

Aplicación en Pytorch

One Hot Encoding

Se puede utilizar directamente en Pytorch utilizando F.one_hot(). En general esto se hace fuera del modelo y no es una capa entrenable.

Embedding

Este caso sí es una capa entrenable y se aplica en Pytorch como una capa más del modelo utilizando nn.Embedding().

nn.Embedding(num_embeddings, embedding_dim)
  • num_embeddings: Corresponde al número de categórías.
  • embedding_dim: El número de dimensiones en el cual se quiere representar.

🤓 Projection Layer

En algunos casos se agrega una Projection Layer (capa lineal) en las variables numéricas a una nueva dimensión (Esto ya que el Embedding lleva a las variables categóricas a una dimensión diferente). Esto permite que el modelo aprenda una representación conjunta de las variables numéricas y categóricas. Luego las capas de proyección y embedding se concatenan y se pasan a las capas siguientes.

Embedding: Ejemplo

\[e=Emb(X)\] \[\phi1 = e \cdot W1\] \[Z = \phi_1 = + 1_m b^T\] \[p = \sigma(Z)\] \[L = \frac{-1}{m}\left[y^T log(p) + (1-y)^T log(1-p)\right]\]

🤓 No haremos la derivación completa, sólo nos enfocaremos en el Gradiente del Embedding.

Embedding: Ejemplo

\[\frac{\partial L}{\partial Z} = \frac{1}{m} \cdot [p - y]\] \[\frac{\partial L}{\partial \phi_1} = \frac{1}{m} \cdot [p - y]\] \[\frac{\partial L}{\partial e} = \frac{\partial L}{\partial \phi_1} \cdot \frac{\partial \phi_1}{\partial e}= \frac{1}{m}[p-y] \cdot W_1^T \] \[\frac{\partial}{\partial E} = \frac{\partial L}{\partial e} \cdot \frac{\partial e}{\partial E} = \frac{1}{m} [p-y] \cdot W_1^T \cdot \frac{\partial e}{\partial E}\]

¿Cuánto vale \(\frac{\partial e}{\partial E}\)?

\[\frac{\partial e}{\partial E} = S^T\]

Donde \(S\) es es la matriz One-Hot-Encoding de tamaño \(m \times C\) donde C es el número de categorías.

Embedding: Ejemplo Numérico

\[ e = \begin{bmatrix} 0.1000 & 0.2000 & 0.3000 \\ 0.4000 & 0.0000 & -0.1000 \\ 0.2000 & -0.2000 & 0.1000 \\ 0.4000 & 0.0000 & -0.1000 \end{bmatrix} \]

Embedding: Ejemplo Numérico

\[ \phi_1 = \begin{bmatrix} 0.0300 \\ 0.1900 \\ 0.1600 \\ 0.1900 \end{bmatrix} \]

\[ Z = \begin{bmatrix} 0.1300 \\ 0.2900 \\ 0.2600 \\ 0.2900 \end{bmatrix} \]

\[ p = \begin{bmatrix} 0.5325 \\ 0.5720 \\ 0.5646 \\ 0.5720 \end{bmatrix} \]

\[ \begin{align} \frac{\partial L}{\partial E} &= \frac{1}{m} S^T \cdot [p - y] \cdot W_1^T\\ &= \frac{1}{4} \cdot \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{bmatrix} \cdot \left[\begin{bmatrix} 0.5325 \\ 0.5720 \\ 0.5646 \\ 0.5720 \end{bmatrix} - \begin{bmatrix} 1.0 \\ 0.0 \\ 1.0 \\ 0.0 \end{bmatrix} \right] \cdot \begin{bmatrix} 0.5 & -0.25 & 0.1 \end{bmatrix}\\ &=\begin{bmatrix} -0.0584 & 0.0292 & -0.0117 \\ 0.0000 & -0.0000 & 0.0000 \\ 0.0000 & -0.0000 & 0.0000 \\ 0.1430 & -0.0715 & 0.0286 \\ -0.0544 & 0.0272 & -0.0109 \end{bmatrix} \end{align} \]

🎊 Estamos listos para entrenar Redes Neuronales 🎊