Clase 4: Model Training
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
Lossde 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?
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:
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.
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.
Corresponde al proceso de Holdout pero repetido \(K\) veces. No es tan utilizado en Deep Learning debido a los altos costos computacionales.
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:
👀
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.
Una capa en Pytorch
torch.nn.atributos:
.weight.data y .bias.data (para los pesos y bias respectivos).## 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)
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.shapeSuperMLP(
(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)
)
)
=================================================================
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
=================================================================
Loss Function
Loss Function a utilizar le llamaremos criterion.Optimizer
optimizer y se importa desde torch.optim.lr (learning rate).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
model.train().
optimizer.zero_grad().logits = super_model(X). Esto permite calcular los Logits y las variables intermedias necesarias para el Backward Pass.loss = criterion(logits, y).loss.backward(). Esto calcula los gradientes y los acumula en cada parámetro del modelo.optimizer. Y llamaremos al proceso de actualizar pesos como optimizer.step().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
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.val_logits = super_model(X_val). Esto permite calcular los Logits para poder calcular el loss de Validación.val_loss = criterion(val_logits, y_val).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.
🤓
Las métricas presentadas son las básicas para clasificación y regresión. Existen otras más específicas según el campo:
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()Es importante ser capaz de identificar el momento exacto en el cual el momento comienza su overfitting. Para ello se utiliza el “Checkpointing”.
Checkpoint
EarlyStopping
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.
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].
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)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).
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
DataLoader en la GPU..compute()).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.
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.
🗞️ 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.
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().
🤓 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.
\[\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.
\[ \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} \]
