Clase 4: Introducción a 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:
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 las versiones funcionales de elementos de torch.nn
.## Permite automáticamente reconocer si es que existe GPU en el sistema y de existir lo asigna.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
¿Cómo definimos los 3 elementos principales de una red?
(Hipótesis, Loss Function y Optimizador)
En Pytorch, cada parte de una red es una clase.
Una vez que un módulo es instanciado, acepta tensores de entrada y devuelve tensores de salida.
De manera análoga, nn.ReLU()
será el módulo que representará una función de activación ReLU.
Pero, ¿Cómo combinamos distintos módulos para crear una sóla arquitectura que represente nuestra Hipótesis?
Para poder crear una Hipótesis en Pytorch podemos combinar cada Módulo entra clase que herede desde nn.Module
.
nn.Module
. Esto permitirá que transformar la clase en Módulos que pueden combinarse para crear Arquitecturas cada vez más complejas.__init__()
class MyNeuralNetwork(nn.Module):
def __init__(self,*):
super().__init__()
self.w1 = nn.Linear(10,32)
self.w2 = nn.Linear(32,64)
self.w3 = nn.Linear(64,1)
self.relu_1= nn.ReLU()
self.relu_2= nn.ReLU()
nn.Module
mediante el super().__init__()
.self
. Esto permite que estos elementos puedan estar disponibles en cualquier método de la clase.self
y luego pueden tener otros parámetros.forward()
class MyNeuralNetwork(nn.Module):
def __init__(self,*):
super().__init__()
self.w1 = nn.Linear(10,32)
self.w2 = nn.Linear(32,64)
self.w3 = nn.Linear(64,1)
self.relu_1= nn.ReLU()
self.relu_2= nn.ReLU()
def forward(self,x):
x = self.w1(x)
x = self.relu_1(x)
x = self.w2(x)
x = self.relu_2(x)
x = self.w3(x)
return x
forward
representa el *forward pass de la red e indica cómo están conectadas las distintas etapas de la red.La nomenclatura utilizada en Pytorch para referirse a la definición de la función de Pérdida es el
criterion
. Es decir, el criterio con el que se mide la pérdida. Más Loss Functions se pueden encontrar acá.
La nomenclatura utilizada en Pytorch para referirse al optimizador a utilizar es
optimizer
. Éste se importa desdetorch.optim
y debe recibir como argumentosmodel.parameters()
y al menos ellearning_rate
. Todos los optimizers pueden encontrarse acá.
Definiremos como Training Loop
al proceso en el cual entrenaremos el modelo.
for e in range(EPOCHS):
## Fijar el modelo en Modo Entrenamiento
model.train()
## Fijar Gradientes en 0
optimizer.zero_grad()
## Forward Pass
preds = model(X)
## Cálculo del Loss (Ojo, primero va la predicción y luego el target). Ver Docs.
loss = criterion(preds, y)
## Cálculo de Gradientes
loss.backward()
## Update Rule
optimizer.step()
.zero_grad()
fijan los gradientes a cero, ya que Pytorch acumula gradientes siempre. Es importante que en cada epoch todos los gradientes acumulados vuelvan a cero para una siguiente optimización..zero_()
.Para generar predicciones basta con generar un
Forward Pass
con el modelo ya entrenado. Dependiendo del modelo, es posible que sea necesario aplicar unpost-procesamiento
.
## Fijar el Modelo en Evaluación.
model.eval()
## Evita que Pytorch calcule Gradientes ya que no es necesario.
with torch.no_grad():
## Cálculo de la salida del modelo (h)
h = model(X)
## Cálculo de Probabilidades (si es que fuera necesario)
y_proba = torch.sigmoid(h)
## Clasificación propiamente tal
y_preds = torch.where(y_proba>=0.5, 1,0)
Para ello Pytorch introduce los conceptos de Dataset y DataLoader para implementar conversión y carga de datos on-the-fly
.
Pytorch necesita crear una clase que herede de
Dataset
y que permita tomar elementos uno a uno y transformarlos en Tensores. Este clase debe tener al menos 3 métodos:__init__
,__len__
y__getitem__
.
Supongamos que nuestros datos iniciales estaban en Numpy.
Cada vez que nosotros llamamos un objeto modelo (que herede de
nn.Module
) este modelo mostrará el modelregistry
. Elregistry
permitirá ver todos los elementos que son parte del modelo. Para que un elemento sea parte del registro, debe haber sido definido comoself.----
.
self.---
debe definirse como un nn.Module y no como un F.---
model.atributo
.Idea:
El Dataloader permitirá ir cargando los datos en memoria en un cierto
batch_size
. La idea es no generar cuellos de botella por falta de memoria disponible.
data = MyDataset(X,y)
train_loader = DataLoader(data, batch_size=32, pin_memory=True,num_workers=12, shuffle=True)
Esto implica que nuestro Training Loop deberá sufrir ciertas modificaciones para ir actualizandose por Batch y no sólo por Epoch.