Cómo hacer una cuadrícula de hiperparámetros de búsqueda para modelos PyTorch
Los "pesos" de una red neuronal se denominan "parámetros" en el código PyTorch y el optimizador los ajusta durante el entrenamiento. Por el contrario, los hiperparámetros son los parámetros de una red neuronal que están fijados por diseño y no ajustados mediante entrenamiento. Algunos ejemplos son el número de capas ocultas y la elección de funciones de activación. La optimización de hiperparámetros es una gran parte del aprendizaje profundo. La razón es que las redes neuronales son muy difíciles de configurar y es necesario establecer muchos parámetros. Además de eso, los modelos individuales pueden tardar mucho en entrenarse.
En esta publicación, descubrirá cómo utilizar la capacidad de búsqueda en cuadrícula de la biblioteca de aprendizaje automático Python scikit-learn para ajustar los hiperparámetros de los modelos de aprendizaje profundo de PyTorch. Después de leer este post, sabrás:
- Cómo empaquetar modelos de PyTorch para usarlos en scikit-learn y cómo usar la búsqueda en cuadrícula
- Cómo realizar una búsqueda en cuadrícula de parámetros comunes de redes neuronales, como la tasa de aprendizaje, la tasa de abandono, las épocas y la cantidad de neuronas
- Cómo definir sus propios experimentos de ajuste de hiperparámetros en sus propios proyectos
Pon en marcha tu proyecto con mi libro Aprendizaje profundo con PyTorch. Proporciona tutoriales de autoaprendizaje con código de trabajo.
Descripción general
En esta publicación, verá cómo utilizar la capacidad de búsqueda en cuadrícula de scikit-learn con un conjunto de ejemplos que puede copiar y pegar en su propio proyecto como punto de partida. A continuación se muestra una lista de los temas que vamos a cubrir:
- Cómo utilizar modelos de PyTorch en scikit-learn
- Cómo utilizar la búsqueda de cuadrícula en scikit-learn
- Cómo ajustar el tamaño del lote y las épocas de entrenamiento
- Cómo ajustar los algoritmos de optimización
- Cómo ajustar la tasa de aprendizaje y el impulso
- Cómo ajustar la inicialización del peso de la red
- Cómo sintonizar las funciones de activación
- Cómo ajustar la regularización del abandono
- Cómo ajustar la cantidad de neuronas en la capa oculta
Cómo utilizar modelos de PyTorch en scikit-learn
Los modelos de PyTorch se pueden usar en scikit-learn si se envuelven con skorch. Esto es para aprovechar la naturaleza de escritura pato de Python para hacer que el modelo PyTorch proporcione una API similar a la del modelo scikit-learn, de modo que todo en scikit-learn pueda funcionar. En skorch, existen NeuralNetClassifier
para redes neuronales de clasificación y NeuralNetRegressor
para redes neuronales de regresión. Es posible que deba ejecutar el siguiente comando para instalar el módulo.
pip install skorch
Para usar estos contenedores, debe definir su modelo PyTorch como una clase usando nn.Module
, luego pasar el nombre de la clase al argumento module
al construir el Clase NeuralNetClassifier
. Por ejemplo:
class MyClassifier(nn.Module):
def __init__(self):
super().__init__()
...
def forward(self, x):
...
return x
# create the skorch wrapper
model = NeuralNetClassifier(
module=MyClassifier
)
El constructor de la clase NeuralNetClassifier
puede tomar argumentos predeterminados que se pasan a las llamadas a model.fit()
(la forma de invocar un bucle de entrenamiento en modelos de scikit-learn ), como el número de épocas y el tamaño del lote. Por ejemplo:
model = NeuralNetClassifier(
module=MyClassifier,
max_epochs=150,
batch_size=10
)
El constructor de la clase NeuralNetClassifier
también puede tomar nuevos argumentos que se pueden pasar al constructor de su clase de modelo, pero debe anteponerle module__
(con dos guiones bajos). Estos nuevos argumentos pueden llevar un valor predeterminado en el constructor, pero serán anulados cuando el contenedor cree una instancia del modelo. Por ejemplo:
import torch.nn as nn
from skorch import NeuralNetClassifier
class SonarClassifier(nn.Module):
def __init__(self, n_layers=3):
super().__init__()
self.layers = []
self.acts = []
for i in range(n_layers):
self.layers.append(nn.Linear(60, 60))
self.acts.append(nn.ReLU())
self.add_module(f"layer{i}", self.layers[-1])
self.add_module(f"act{i}", self.acts[-1])
self.output = nn.Linear(60, 1)
def forward(self, x):
for layer, act in zip(self.layers, self.acts):
x = act(layer(x))
x = self.output(x)
return x
model = NeuralNetClassifier(
module=SonarClassifier,
max_epochs=150,
batch_size=10,
module__n_layers=2
)
Puede verificar el resultado inicializando un modelo e imprimiéndolo:
print(model.initialize())
En este ejemplo, deberías ver:
<class 'skorch.classifier.NeuralNetClassifier'>[initialized](
module_=SonarClassifier(
(layer0): Linear(in_features=60, out_features=60, bias=True)
(act0): ReLU()
(layer1): Linear(in_features=60, out_features=60, bias=True)
(act1): ReLU()
(output): Linear(in_features=60, out_features=1, bias=True)
),
)
Cómo utilizar la búsqueda de cuadrícula en scikit-learn
La búsqueda de cuadrícula es una técnica de optimización de hiperparámetros del modelo. Simplemente agota todas las combinaciones de hiperparámetros y encuentra la que dio la mejor puntuación. En scikit-learn, esta técnica se proporciona en la clase GridSearchCV
. Al construir esta clase, debe proporcionar un diccionario de hiperparámetros para evaluar en el argumento param_grid
. Este es un mapa del nombre del parámetro del modelo y una serie de valores para probar.
De forma predeterminada, la precisión es la puntuación optimizada, pero se pueden especificar otras puntuaciones en el argumento de puntuación del constructor GridSearchCV
. El proceso GridSearchCV
construirá y evaluará un modelo para cada combinación de parámetros. La validación cruzada se utiliza para evaluar cada modelo individual y se utiliza el valor predeterminado de validación cruzada triple, aunque puede anular esto especificando el argumento cv en el constructor GridSearchCV
.
A continuación se muestra un ejemplo de cómo definir una búsqueda de cuadrícula simple:
param_grid = {
'epochs': [10,20,30]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, Y)
Al establecer el argumento n_jobs
en el constructor GridSearchCV
en $-1$, el proceso utilizará todos los núcleos de su máquina. De lo contrario, el proceso de búsqueda en la red solo se ejecutará en un solo subproceso, lo cual es más lento en las CPU de múltiples núcleos.
Una vez completado, puede acceder al resultado de la búsqueda de cuadrícula en el objeto de resultado devuelto por grid.fit()
. El miembro best_score_
proporciona acceso a la mejor puntuación observada durante el procedimiento de optimización, y best_params_
describe la combinación de parámetros que lograron los mejores resultados. Puede obtener más información sobre la clase GridSearchCV
en la documentación de la API scikit-learn.
Inicie su proyecto con mi libro Aprendizaje profundo con PyTorch. Proporciona tutoriales de autoaprendizaje con código de trabajo.
Descripción del problema
Ahora que sabe cómo usar modelos PyTorch con scikit-learn y cómo usar la búsqueda de cuadrícula en scikit-learn, veamos algunos ejemplos.
Todos los ejemplos se demostrarán en un pequeño conjunto de datos de aprendizaje automático estándar llamado conjunto de datos de clasificación de inicio de diabetes de los indios Pima. Este es un pequeño conjunto de datos con todos los atributos numéricos con el que es fácil trabajar.
A medida que avance con los ejemplos de esta publicación, agregará los mejores parámetros. Esta no es la mejor manera de realizar búsquedas en cuadrículas porque los parámetros pueden interactuar, pero es buena para fines de demostración.
Cómo ajustar el tamaño del lote y el número de épocas
En este primer ejemplo simple, verá cómo ajustar el tamaño del lote y la cantidad de épocas utilizadas al ajustar la red.
El tamaño del lote en el descenso de gradiente iterativo es la cantidad de patrones que se muestran a la red antes de que se actualicen los pesos. También es una optimización en el entrenamiento de la red, definiendo cuántos patrones leer a la vez y mantener en la memoria.
La cantidad de épocas es la cantidad de veces que todo el conjunto de datos de entrenamiento se muestra a la red durante el entrenamiento. Algunas redes son sensibles al tamaño del lote, como las redes neuronales recurrentes LSTM y las redes neuronales convolucionales.
Aquí evaluará un conjunto de diferentes tamaños de minibatch de 10 a 100 en pasos de 20.
La lista completa de códigos se proporciona a continuación:
import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = nn.ReLU()
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
def forward(self, x):
x = self.act(self.layer(x))
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
optimizer=optim.Adam,
verbose=False
)
# define the grid search parameters
param_grid = {
'batch_size': [10, 20, 40, 60, 80, 100],
'max_epochs': [10, 50, 100]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado:
Best: 0.714844 using {'batch_size': 10, 'max_epochs': 100}
0.665365 (0.020505) with: {'batch_size': 10, 'max_epochs': 10}
0.588542 (0.168055) with: {'batch_size': 10, 'max_epochs': 50}
0.714844 (0.032369) with: {'batch_size': 10, 'max_epochs': 100}
0.671875 (0.022326) with: {'batch_size': 20, 'max_epochs': 10}
0.696615 (0.008027) with: {'batch_size': 20, 'max_epochs': 50}
0.714844 (0.019918) with: {'batch_size': 20, 'max_epochs': 100}
0.666667 (0.009744) with: {'batch_size': 40, 'max_epochs': 10}
0.687500 (0.033603) with: {'batch_size': 40, 'max_epochs': 50}
0.707031 (0.024910) with: {'batch_size': 40, 'max_epochs': 100}
0.667969 (0.014616) with: {'batch_size': 60, 'max_epochs': 10}
0.694010 (0.036966) with: {'batch_size': 60, 'max_epochs': 50}
0.694010 (0.042473) with: {'batch_size': 60, 'max_epochs': 100}
0.670573 (0.023939) with: {'batch_size': 80, 'max_epochs': 10}
0.674479 (0.020752) with: {'batch_size': 80, 'max_epochs': 50}
0.703125 (0.026107) with: {'batch_size': 80, 'max_epochs': 100}
0.680990 (0.014382) with: {'batch_size': 100, 'max_epochs': 10}
0.670573 (0.013279) with: {'batch_size': 100, 'max_epochs': 50}
0.687500 (0.017758) with: {'batch_size': 100, 'max_epochs': 100}
Puede ver que el tamaño de lote de 10 y 100 épocas logró el mejor resultado de aproximadamente un 71% de precisión (pero también debe tener en cuenta la desviación estándar de la precisión).
Cómo ajustar el algoritmo de optimización del entrenamiento
Toda biblioteca de aprendizaje profundo debe ofrecer una variedad de algoritmos de optimización. PyTorch no es una excepción.
En este ejemplo, ajustará el algoritmo de optimización utilizado para entrenar la red, cada uno con parámetros predeterminados.
Este es un ejemplo extraño porque a menudo elegirá un enfoque a priori y en su lugar se concentrará en ajustar sus parámetros a su problema (consulte el siguiente ejemplo).
Aquí, evaluará el conjunto de algoritmos de optimización disponibles en PyTorch.
La lista completa de códigos se proporciona a continuación:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = nn.ReLU()
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
def forward(self, x):
x = self.act(self.layer(x))
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
max_epochs=100,
batch_size=10,
verbose=False
)
# define the grid search parameters
param_grid = {
'optimizer': [optim.SGD, optim.RMSprop, optim.Adagrad, optim.Adadelta,
optim.Adam, optim.Adamax, optim.NAdam],
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado:
Best: 0.721354 using {'optimizer': <class 'torch.optim.adamax.Adamax'>}
0.674479 (0.036828) with: {'optimizer': <class 'torch.optim.sgd.SGD'>}
0.700521 (0.043303) with: {'optimizer': <class 'torch.optim.rmsprop.RMSprop'>}
0.682292 (0.027126) with: {'optimizer': <class 'torch.optim.adagrad.Adagrad'>}
0.572917 (0.051560) with: {'optimizer': <class 'torch.optim.adadelta.Adadelta'>}
0.714844 (0.030758) with: {'optimizer': <class 'torch.optim.adam.Adam'>}
0.721354 (0.019225) with: {'optimizer': <class 'torch.optim.adamax.Adamax'>}
0.709635 (0.024360) with: {'optimizer': <class 'torch.optim.nadam.NAdam'>}
Los resultados sugieren que el algoritmo de optimización Adamax es el mejor con una puntuación de aproximadamente el 72% de precisión.
Vale la pena mencionar que GridSearchCV
recreará su modelo con frecuencia para que cada prueba sea independiente. La razón por la que se puede hacer es por el contenedor NeuralNetClassifier
, que conoce el nombre de la clase para su modelo PyTorch y crea una instancia para usted cuando lo solicite.
Cómo ajustar la tasa de aprendizaje y el impulso
Es común preseleccionar un algoritmo de optimización para entrenar su red y ajustar sus parámetros.
Con diferencia, el algoritmo de optimización más común es el antiguo Descenso de gradiente estocástico (SGD) porque se comprende muy bien. En este ejemplo, verá cómo optimizar la tasa de aprendizaje de SGD y los parámetros de impulso.
La tasa de aprendizaje controla cuánto se debe actualizar el peso al final de cada lote, y el impulso controla cuánto se debe permitir que la actualización anterior influya en la actualización del peso actual.
Probará un conjunto de tasas de aprendizaje estándar pequeñas y valores de impulso de 0,2 a 0,8 en pasos de 0,2, así como 0,9 (porque puede ser un valor popular en la práctica). En PyTorch, la forma de establecer la tasa de aprendizaje y el impulso es la siguiente:
optimizer = optim.SGD(lr=0.001, momentum=0.9)
En el contenedor skorch, podrá enrutar los parámetros al optimizador con el prefijo optimizer__
.
Generalmente, es una buena idea incluir también la cantidad de épocas en una optimización como esta, ya que existe una dependencia entre la cantidad de aprendizaje por lote (tasa de aprendizaje), la cantidad de actualizaciones por época (tamaño de lote) y la cantidad. de épocas.
La lista completa de códigos se proporciona a continuación:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = nn.ReLU()
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
def forward(self, x):
x = self.act(self.layer(x))
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
optimizer=optim.SGD,
max_epochs=100,
batch_size=10,
verbose=False
)
# define the grid search parameters
param_grid = {
'optimizer__lr': [0.001, 0.01, 0.1, 0.2, 0.3],
'optimizer__momentum': [0.0, 0.2, 0.4, 0.6, 0.8, 0.9],
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado.
Best: 0.682292 using {'optimizer__lr': 0.001, 'optimizer__momentum': 0.9}
0.648438 (0.016877) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.0}
0.671875 (0.017758) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.2}
0.674479 (0.022402) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.4}
0.677083 (0.011201) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.6}
0.679688 (0.027621) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.8}
0.682292 (0.026557) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.9}
0.671875 (0.019918) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.0}
0.648438 (0.024910) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.2}
0.546875 (0.143454) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.4}
0.567708 (0.153668) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.6}
0.552083 (0.141790) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.8}
0.451823 (0.144561) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.9}
0.348958 (0.001841) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.0}
0.450521 (0.142719) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.2}
0.450521 (0.142719) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.4}
0.450521 (0.142719) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.6}
0.348958 (0.001841) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.8}
0.348958 (0.001841) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.9}
0.444010 (0.136265) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.0}
0.450521 (0.142719) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.2}
0.348958 (0.001841) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.4}
0.552083 (0.141790) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.6}
0.549479 (0.142719) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.8}
0.651042 (0.001841) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.9}
0.552083 (0.141790) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.0}
0.348958 (0.001841) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.2}
0.450521 (0.142719) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.4}
0.552083 (0.141790) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.6}
0.450521 (0.142719) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.8}
0.450521 (0.142719) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.9}
Puede ver que, con SGD, los mejores resultados se lograron utilizando una tasa de aprendizaje de 0,001 y un impulso de 0,9 con una precisión de aproximadamente el 68 %.
Cómo ajustar la inicialización del peso de la red
La inicialización del peso de la red neuronal solía ser simple: use pequeños valores aleatorios.
Ahora hay un conjunto de técnicas diferentes para elegir. Puede obtener una lista detallada en la documentación torch.nn.init
.
En este ejemplo, observará cómo ajustar la selección de inicialización del peso de la red evaluando todas las técnicas disponibles.
Utilizará el mismo método de inicialización de peso en cada capa. Idealmente, puede ser mejor utilizar diferentes esquemas de inicialización de peso según la función de activación utilizada en cada capa. En el siguiente ejemplo, utilizará un rectificador para la capa oculta. Utilice sigmoide para la capa de salida porque las predicciones son binarias. La inicialización del peso está implícita en los modelos de PyTorch. Por lo tanto, debe escribir su propia lógica para inicializar el peso, después de crear la capa pero antes de usarla. Modifiquemos PyTorch de la siguiente manera:
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self, weight_init=torch.nn.init.xavier_uniform_):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = nn.ReLU()
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
# manually init weights
weight_init(self.layer.weight)
weight_init(self.output.weight)
def forward(self, x):
x = self.act(self.layer(x))
x = self.prob(self.output(x))
return x
Se agrega un argumento weight_init
a la clase PimaClassifier
y espera uno de los inicializadores de torch.nn.init
. En GridSearchCV
, debe usar el prefijo module__
para hacer que NeuralNetClassifier
enrute el parámetro al constructor de clases del modelo.
La lista completa de códigos se proporciona a continuación:
import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self, weight_init=init.xavier_uniform_):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = nn.ReLU()
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
# manually init weights
weight_init(self.layer.weight)
weight_init(self.output.weight)
def forward(self, x):
x = self.act(self.layer(x))
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
optimizer=optim.Adamax,
max_epochs=100,
batch_size=10,
verbose=False
)
# define the grid search parameters
param_grid = {
'module__weight_init': [init.uniform_, init.normal_, init.zeros_,
init.xavier_normal_, init.xavier_uniform_,
init.kaiming_normal_, init.kaiming_uniform_]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado.
Best: 0.697917 using {'module__weight_init': <function kaiming_uniform_ at 0x112020c10>}
0.348958 (0.001841) with: {'module__weight_init': <function uniform_ at 0x1120204c0>}
0.602865 (0.061708) with: {'module__weight_init': <function normal_ at 0x112020550>}
0.652344 (0.003189) with: {'module__weight_init': <function zeros_ at 0x112020820>}
0.691406 (0.030758) with: {'module__weight_init': <function xavier_normal_ at 0x112020af0>}
0.592448 (0.171589) with: {'module__weight_init': <function xavier_uniform_ at 0x112020a60>}
0.563802 (0.152971) with: {'module__weight_init': <function kaiming_normal_ at 0x112020ca0>}
0.697917 (0.013279) with: {'module__weight_init': <function kaiming_uniform_ at 0x112020c10>}
Los mejores resultados se lograron con un esquema de inicialización de peso uniforme de He logrando un rendimiento de aproximadamente el 70%.
Cómo sintonizar la función de activación neuronal
La función de activación controla la no linealidad de las neuronas individuales y cuándo disparar.
Generalmente, la función de activación del rectificador es la más popular. Sin embargo, solían ser las funciones sigmoidea y tanh, y estas funciones aún pueden ser más adecuadas para diferentes problemas.
En este ejemplo, evaluará algunas de las funciones de activación disponibles en PyTorch. Solo usará estas funciones en la capa oculta, ya que se requiere una función de activación sigmoidea en la salida para el problema de clasificación binaria. Similar al ejemplo anterior, este es un argumento para el constructor de clases del modelo y usará el prefijo module__
para la cuadrícula de parámetros GridSearchCV
.
Generalmente, es una buena idea preparar los datos según el rango de las diferentes funciones de transferencia, lo cual no hará en este caso.
La lista completa de códigos se proporciona a continuación:
import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self, activation=nn.ReLU):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = activation()
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
# manually init weights
init.kaiming_uniform_(self.layer.weight)
init.kaiming_uniform_(self.output.weight)
def forward(self, x):
x = self.act(self.layer(x))
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
optimizer=optim.Adamax,
max_epochs=100,
batch_size=10,
verbose=False
)
# define the grid search parameters
param_grid = {
'module__activation': [nn.Identity, nn.ReLU, nn.ELU, nn.ReLU6,
nn.GELU, nn.Softplus, nn.Softsign, nn.Tanh,
nn.Sigmoid, nn.Hardsigmoid]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado.
Best: 0.699219 using {'module__activation': <class 'torch.nn.modules.activation.ReLU'>}
0.687500 (0.025315) with: {'module__activation': <class 'torch.nn.modules.linear.Identity'>}
0.699219 (0.011049) with: {'module__activation': <class 'torch.nn.modules.activation.ReLU'>}
0.674479 (0.035849) with: {'module__activation': <class 'torch.nn.modules.activation.ELU'>}
0.621094 (0.063549) with: {'module__activation': <class 'torch.nn.modules.activation.ReLU6'>}
0.674479 (0.017566) with: {'module__activation': <class 'torch.nn.modules.activation.GELU'>}
0.558594 (0.149189) with: {'module__activation': <class 'torch.nn.modules.activation.Softplus'>}
0.675781 (0.014616) with: {'module__activation': <class 'torch.nn.modules.activation.Softsign'>}
0.619792 (0.018688) with: {'module__activation': <class 'torch.nn.modules.activation.Tanh'>}
0.643229 (0.019225) with: {'module__activation': <class 'torch.nn.modules.activation.Sigmoid'>}
0.636719 (0.022326) with: {'module__activation': <class 'torch.nn.modules.activation.Hardsigmoid'>}
Muestra que la función de activación ReLU logró los mejores resultados con una precisión de aproximadamente el 70%.
Cómo ajustar la regularización del abandono escolar
En este ejemplo, observará cómo ajustar la tasa de abandono para la regularización en un esfuerzo por limitar el sobreajuste y mejorar la capacidad del modelo para generalizar.
Para obtener mejores resultados, es mejor combinar el abandono con una restricción de peso, como la restricción de norma máxima, que se implementa en la función de pase hacia adelante.
Esto implica ajustar tanto el porcentaje de abandono como la restricción de peso. Probaremos porcentajes de abandono entre 0,0 y 0,9 (1,0 no tiene sentido) y valores de restricción de peso MaxNorm entre 0 y 5.
La lista completa de códigos se proporciona a continuación.
import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
# PyTorch classifier
class PimaClassifier(nn.Module):
def __init__(self, dropout_rate=0.5, weight_constraint=1.0):
super().__init__()
self.layer = nn.Linear(8, 12)
self.act = nn.ReLU()
self.dropout = nn.Dropout(dropout_rate)
self.output = nn.Linear(12, 1)
self.prob = nn.Sigmoid()
self.weight_constraint = weight_constraint
# manually init weights
init.kaiming_uniform_(self.layer.weight)
init.kaiming_uniform_(self.output.weight)
def forward(self, x):
# maxnorm weight before actual forward pass
with torch.no_grad():
norm = self.layer.weight.norm(2, dim=0, keepdim=True).clamp(min=self.weight_constraint / 2)
desired = torch.clamp(norm, max=self.weight_constraint)
self.layer.weight *= (desired / norm)
# actual forward pass
x = self.act(self.layer(x))
x = self.dropout(x)
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
optimizer=optim.Adamax,
max_epochs=100,
batch_size=10,
verbose=False
)
# define the grid search parameters
param_grid = {
'module__weight_constraint': [1.0, 2.0, 3.0, 4.0, 5.0],
'module__dropout_rate': [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado.
Best: 0.701823 using {'module__dropout_rate': 0.1, 'module__weight_constraint': 2.0}
0.669271 (0.015073) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 1.0}
0.692708 (0.035132) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 2.0}
0.589844 (0.170180) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 3.0}
0.561198 (0.151131) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 4.0}
0.688802 (0.021710) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 5.0}
0.697917 (0.009744) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 1.0}
0.701823 (0.016367) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 2.0}
0.694010 (0.010253) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 3.0}
0.686198 (0.025976) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 4.0}
0.679688 (0.026107) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 5.0}
0.701823 (0.029635) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 1.0}
0.682292 (0.014731) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 2.0}
0.701823 (0.009744) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 3.0}
0.701823 (0.026557) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 4.0}
0.687500 (0.015947) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 5.0}
0.686198 (0.006639) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 1.0}
0.656250 (0.006379) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 2.0}
0.565104 (0.155608) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 3.0}
0.700521 (0.028940) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 4.0}
0.669271 (0.012890) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 5.0}
0.661458 (0.018688) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 1.0}
0.669271 (0.017566) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 2.0}
0.652344 (0.006379) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 3.0}
0.680990 (0.037783) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 4.0}
0.692708 (0.042112) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 5.0}
0.666667 (0.006639) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 1.0}
0.652344 (0.011500) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 2.0}
0.662760 (0.007366) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 3.0}
0.558594 (0.146610) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 4.0}
0.552083 (0.141826) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 5.0}
0.548177 (0.141826) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 1.0}
0.653646 (0.013279) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 2.0}
0.661458 (0.008027) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 3.0}
0.553385 (0.142719) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 4.0}
0.669271 (0.035132) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 5.0}
0.662760 (0.015733) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 1.0}
0.636719 (0.024910) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 2.0}
0.550781 (0.146818) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 3.0}
0.537760 (0.140094) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 4.0}
0.542969 (0.138144) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 5.0}
0.565104 (0.148654) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 1.0}
0.657552 (0.008027) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 2.0}
0.428385 (0.111418) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 3.0}
0.549479 (0.142719) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 4.0}
0.648438 (0.005524) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 5.0}
0.540365 (0.136861) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 1.0}
0.605469 (0.053083) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 2.0}
0.553385 (0.139948) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 3.0}
0.549479 (0.142719) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 4.0}
0.595052 (0.075566) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 5.0}
Puede ver que la tasa de abandono del 10 % y la restricción de peso de 2,0 dieron como resultado la mejor precisión de aproximadamente el 70 %.
Cómo ajustar el número de neuronas en la capa oculta
La cantidad de neuronas en una capa es un parámetro importante a ajustar. Generalmente el número de neuronas en una capa controla la capacidad de representación de la red, al menos en ese punto de la topología.
Generalmente, una red de una sola capa lo suficientemente grande puede aproximarse a cualquier otra red neuronal, debido al teorema de aproximación universal.
En este ejemplo, observará cómo ajustar la cantidad de neuronas en una sola capa oculta. probará valores del 1 al 30 en pasos de 5.
Una red más grande requiere más entrenamiento y lo ideal es que al menos el tamaño del lote y el número de épocas se optimicen con el número de neuronas.
La lista completa de códigos se proporciona a continuación.
import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV
# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
class PimaClassifier(nn.Module):
def __init__(self, n_neurons=12):
super().__init__()
self.layer = nn.Linear(8, n_neurons)
self.act = nn.ReLU()
self.dropout = nn.Dropout(0.1)
self.output = nn.Linear(n_neurons, 1)
self.prob = nn.Sigmoid()
self.weight_constraint = 2.0
# manually init weights
init.kaiming_uniform_(self.layer.weight)
init.kaiming_uniform_(self.output.weight)
def forward(self, x):
# maxnorm weight before actual forward pass
with torch.no_grad():
norm = self.layer.weight.norm(2, dim=0, keepdim=True).clamp(min=self.weight_constraint / 2)
desired = torch.clamp(norm, max=self.weight_constraint)
self.layer.weight *= (desired / norm)
# actual forward pass
x = self.act(self.layer(x))
x = self.dropout(x)
x = self.prob(self.output(x))
return x
# create model with skorch
model = NeuralNetClassifier(
PimaClassifier,
criterion=nn.BCELoss,
optimizer=optim.Adamax,
max_epochs=100,
batch_size=10,
verbose=False
)
# define the grid search parameters
param_grid = {
'module__n_neurons': [1, 5, 10, 15, 20, 25, 30]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)
# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("%f (%f) with: %r" % (mean, stdev, param))
La ejecución de este ejemplo produce el siguiente resultado.
Best: 0.708333 using {'module__n_neurons': 30}
0.654948 (0.003683) with: {'module__n_neurons': 1}
0.666667 (0.023073) with: {'module__n_neurons': 5}
0.694010 (0.014382) with: {'module__n_neurons': 10}
0.682292 (0.014382) with: {'module__n_neurons': 15}
0.707031 (0.028705) with: {'module__n_neurons': 20}
0.703125 (0.030758) with: {'module__n_neurons': 25}
0.708333 (0.015733) with: {'module__n_neurons': 30}
Puede ver que los mejores resultados se lograron con una red con 30 neuronas en la capa oculta con una precisión de aproximadamente el 71%.
Consejos para la optimización de hiperparámetros
Esta sección enumera algunos consejos útiles a considerar al ajustar los hiperparámetros de su red neuronal.
- Validación cruzada $k$-Fold. Puede ver que los resultados de los ejemplos de esta publicación muestran cierta variación. Se utilizó una validación cruzada predeterminada de 3, pero quizás $k=5$o $k=10$serían más estables. Elija cuidadosamente su configuración de validación cruzada para garantizar que sus resultados sean estables.
- Revise toda la cuadrícula. No se centre únicamente en el mejor resultado, revise toda la tabla de resultados y busque tendencias que respalden las decisiones de configuración. Por supuesto, habrá más combinaciones y llevará más tiempo evaluarlas.
- Paralelizar. Utilice todos sus núcleos si puede, las redes neuronales tardan en entrenarse y, a menudo, queremos probar muchos parámetros diferentes. Considere ejecutarlo en una plataforma en la nube, como AWS.
- Utilice una muestra de su conjunto de datos. Debido a que las redes tardan en entrenarse, intente entrenarlas en una muestra más pequeña de su conjunto de datos de entrenamiento, solo para tener una idea de las direcciones generales de los parámetros en lugar de las configuraciones óptimas.
- Comience con rejillas gruesas. Comience con cuadrículas de grano grueso y amplíe las cuadrículas de grano más fino una vez que pueda reducir el alcance.
- No transferir resultados. Los resultados generalmente son específicos del problema. Intente evitar configuraciones favoritas en cada nuevo problema que vea. Es poco probable que los resultados óptimos que descubra en un problema se transfieran a su próximo proyecto. En su lugar, busque tendencias más amplias, como el número de capas o las relaciones entre parámetros.
- La reproducibilidad es un problema. Aunque configuramos la semilla para el generador de números aleatorios en NumPy, los resultados no son 100% reproducibles. La reproducibilidad cuando se busca en cuadrículas en modelos PyTorch ajustados implica más de lo que se presenta en esta publicación.
Lectura adicional
Esta sección proporciona más recursos sobre el tema si desea profundizar más.
- documentación de skorch
- torch.nn de PyTorch
- GridSearchCV de scikit-learn
Resumen
En esta publicación, descubrió cómo puede ajustar los hiperparámetros de sus redes de aprendizaje profundo en Python usando PyTorch y scikit-learn.
Específicamente, aprendió:
- Cómo empaquetar modelos de PyTorch para usarlos en scikit-learn y cómo usar la búsqueda en cuadrícula.
- Cómo realizar una búsqueda en cuadrícula de un conjunto de diferentes parámetros de redes neuronales estándar para modelos PyTorch.
- Cómo diseñar sus propios experimentos de optimización de hiperparámetros.