Comment griller les hyperparamètres de recherche pour les modèles PyTorch
Les « poids » d'un réseau neuronal sont appelés « paramètres » dans le code PyTorch et sont affinés par l'optimiseur pendant l'entraînement. Au contraire, les hyperparamètres sont les paramètres d’un réseau neuronal fixés par conception et non réglés par entraînement. Des exemples en sont le nombre de couches cachées et le choix des fonctions d'activation. L'optimisation des hyperparamètres est une partie importante du deep learning. La raison en est que les réseaux de neurones sont notoirement difficiles à configurer et que de nombreux paramètres doivent être définis. De plus, l’entraînement des modèles individuels peut être très lent.
Dans cet article, vous découvrirez comment utiliser la capacité de recherche de grille de la bibliothèque d'apprentissage automatique Python scikit-learn pour régler les hyperparamètres des modèles d'apprentissage profond PyTorch. Après avoir lu cet article, vous saurez :
- Comment envelopper les modèles PyTorch pour les utiliser dans scikit-learn et comment utiliser la recherche par grille
- Comment rechercher sur une grille les paramètres courants du réseau neuronal, tels que le taux d'apprentissage, le taux d'abandon, les époques et le nombre de neurones
- Comment définir vos propres expériences de réglage d'hyperparamètres sur vos propres projets
Démarrez votre projet avec mon livre Deep Learning with PyTorch. Il fournit des tutoriels d'auto-apprentissage avec du code fonctionnel.
Aperçu
Dans cet article, vous verrez comment utiliser la capacité de recherche de grille scikit-learn avec une suite d'exemples que vous pouvez copier et coller dans votre propre projet comme point de départ. Vous trouverez ci-dessous une liste des sujets que nous allons aborder :
- Comment utiliser les modèles PyTorch dans scikit-learn
- Comment utiliser la recherche par grille dans scikit-learn
- Comment régler la taille des lots et les époques de formation
- Comment régler les algorithmes d'optimisation
- Comment régler le taux et l'élan d'apprentissage
- Comment régler l'initialisation du poids du réseau
- Comment régler les fonctions d'activation
- Comment régler la régularisation des abandons
- Comment régler le nombre de neurones dans la couche cachée
Comment utiliser les modèles PyTorch dans scikit-learn
Les modèles PyTorch peuvent être utilisés dans scikit-learn s'ils sont enveloppés avec skorch. Il s'agit de tirer parti de la nature de typage canard de Python pour que le modèle PyTorch fournisse une API similaire à celle d'un modèle scikit-learn, afin que tout dans scikit-learn puisse fonctionner ensemble. Dans Skorch, il existe NeuralNetClassifier
pour les réseaux de neurones de classification et NeuralNetRegressor
pour les réseaux de neurones de régression. Vous devrez peut-être exécuter la commande suivante pour installer le module.
pip install skorch
Pour utiliser ces wrappers, vous devez définir votre modèle PyTorch en tant que classe à l'aide de nn.Module
, puis transmettre le nom de la classe à l'argument module
lors de la construction du Classe NeuralNetClassifier
. Par exemple:
class MyClassifier(nn.Module):
def __init__(self):
super().__init__()
...
def forward(self, x):
...
return x
# create the skorch wrapper
model = NeuralNetClassifier(
module=MyClassifier
)
Le constructeur de la classe NeuralNetClassifier
peut prendre des arguments par défaut qui sont transmis aux appels à model.fit()
(la manière d'invoquer une boucle d'entraînement dans les modèles scikit-learn ), comme le nombre d'époques et la taille du lot. Par exemple:
model = NeuralNetClassifier(
module=MyClassifier,
max_epochs=150,
batch_size=10
)
Le constructeur de la classe NeuralNetClassifier
peut également prendre de nouveaux arguments qui peuvent être transmis au constructeur de votre classe modèle, mais vous devez le faire précéder de module__
(avec deux traits de soulignement). Ces nouveaux arguments peuvent porter une valeur par défaut dans le constructeur mais ils seront remplacés lorsque le wrapper instanciera le modèle. Par exemple:
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
)
Vous pouvez vérifier le résultat en initialisant un modèle et en l'imprimant :
print(model.initialize())
Dans cet exemple, vous devriez voir :
<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)
),
)
Comment utiliser la recherche par grille dans scikit-learn
La recherche de grille est une technique d'optimisation d'hyperparamètres de modèle. Il suffit d’épuiser toutes les combinaisons d’hyperparamètres et de trouver celle qui a donné le meilleur score. Dans scikit-learn, cette technique est fournie dans la classe GridSearchCV
. Lors de la construction de cette classe, vous devez fournir un dictionnaire d'hyperparamètres à évaluer dans l'argument param_grid
. Il s'agit d'une carte du nom du paramètre du modèle et d'un tableau de valeurs à essayer.
Par défaut, la précision est le score optimisé, mais d'autres scores peuvent être spécifiés dans l'argument score du constructeur GridSearchCV
. Le processus GridSearchCV
construira et évaluera ensuite un modèle pour chaque combinaison de paramètres. La validation croisée est utilisée pour évaluer chaque modèle individuel, et la valeur par défaut d'une validation croisée triple est utilisée, bien que vous puissiez la remplacer en spécifiant l'argument cv au constructeur GridSearchCV
.
Vous trouverez ci-dessous un exemple de définition d'une recherche de grille 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)
En définissant l'argument n_jobs
dans le constructeur GridSearchCV
sur $-1$, le processus utilisera tous les cœurs de votre machine. Sinon, le processus de recherche dans la grille ne s'exécutera que dans un seul thread, ce qui est plus lent dans les processeurs multicœurs.
Une fois terminé, vous pouvez accéder au résultat de la recherche dans la grille dans l'objet résultat renvoyé par grid.fit()
. Le membre best_score_
donne accès au meilleur score observé lors de la procédure d'optimisation, et le membre best_params_
décrit la combinaison de paramètres qui a permis d'obtenir les meilleurs résultats. Vous pouvez en savoir plus sur la classe GridSearchCV
dans la documentation de l'API scikit-learn.
Démarrez votre projet avec mon livre Deep Learning with PyTorch. Il fournit des tutoriels d'auto-apprentissage avec du code fonctionnel.
Description du problème
Maintenant que vous savez comment utiliser les modèles PyTorch avec scikit-learn et comment utiliser la recherche par grille dans scikit-learn, examinons quelques exemples.
Tous les exemples seront démontrés sur un petit ensemble de données d’apprentissage automatique standard appelé ensemble de données de classification de l’apparition du diabète chez les Indiens Pima. Il s'agit d'un petit ensemble de données avec tous les attributs numériques avec lequel il est facile de travailler.
Au fur et à mesure que vous parcourez les exemples de cet article, vous regrouperez les meilleurs paramètres. Ce n'est pas la meilleure façon d'effectuer une recherche sur une grille car les paramètres peuvent interagir, mais c'est une bonne méthode à des fins de démonstration.
Comment régler la taille du lot et le nombre d'époques
Dans ce premier exemple simple, vous examinerez le réglage de la taille du lot et du nombre d'époques utilisés lors de l'ajustement du réseau.
La taille du lot en descente de gradient itérative correspond au nombre de modèles affichés sur le réseau avant la mise à jour des poids. C'est aussi une optimisation dans l'entraînement du réseau, définissant le nombre de modèles à lire à la fois et à conserver en mémoire.
Le nombre d'époques correspond au nombre de fois où l'intégralité de l'ensemble de données d'entraînement est affichée sur le réseau pendant l'entraînement. Certains réseaux sont sensibles à la taille du lot, comme les réseaux de neurones récurrents LSTM et les réseaux de neurones convolutifs.
Ici, vous évaluerez une suite de différentes tailles de mini-lots de 10 à 100 par étapes de 20.
La liste complète des codes est fournie ci-dessous :
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))
L'exécution de cet exemple produit le résultat suivant :
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}
Vous pouvez voir que la taille de lot de 10 et 100 époques a permis d’obtenir le meilleur résultat, avec une précision d’environ 71 % (mais vous devez également prendre en compte l’écart type de la précision).
Comment régler l'algorithme d'optimisation de la formation
Toute bibliothèque d'apprentissage profond doit offrir une variété d'algorithmes d'optimisation. PyTorch ne fait pas exception.
Dans cet exemple, vous ajusterez l'algorithme d'optimisation utilisé pour entraîner le réseau, chacun avec des paramètres par défaut.
Il s'agit d'un exemple étrange car souvent, vous choisirez une approche a priori et vous concentrerez plutôt sur l'ajustement de ses paramètres sur votre problème (voir l'exemple suivant).
Ici, vous évaluerez la suite d'algorithmes d'optimisation disponibles dans PyTorch.
La liste complète des codes est fournie ci-dessous :
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))
L'exécution de cet exemple produit le résultat suivant :
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'>}
Les résultats suggèrent que l'algorithme d'optimisation Adamax est le meilleur avec un score de précision d'environ 72 %.
Il convient de mentionner que GridSearchCV
recréera souvent votre modèle afin que chaque essai soit indépendant. La raison pour laquelle cela peut être fait est due au wrapper NeuralNetClassifier
, qui connaît le nom de la classe de votre modèle PyTorch et en instancie une pour vous sur demande.
Comment régler le taux d'apprentissage et l'élan
Il est courant de présélectionner un algorithme d'optimisation pour entraîner votre réseau et ajuster ses paramètres.
De loin, l’algorithme d’optimisation le plus courant est le vieux Stochastic Gradient Descent (SGD), car il est très bien compris. Dans cet exemple, vous examinerez l'optimisation des paramètres de taux d'apprentissage et d'élan de SGD.
Le taux d'apprentissage contrôle dans quelle mesure il faut mettre à jour le poids à la fin de chaque lot, et l'élan contrôle dans quelle mesure laisser la mise à jour précédente influencer la mise à jour du poids en cours.
Vous essaierez une suite de petits taux d'apprentissage standard et des valeurs d'élan allant de 0,2 à 0,8 par pas de 0,2, ainsi que de 0,9 (car cela peut être une valeur populaire dans la pratique). Dans PyTorch, la manière de définir le taux et l'élan d'apprentissage est la suivante :
optimizer = optim.SGD(lr=0.001, momentum=0.9)
Dans le wrapper skorch, vous pourrez acheminer les paramètres vers l'optimiseur avec le préfixe optimizer__
.
En général, c'est une bonne idée d'inclure également le nombre d'époques dans une optimisation comme celle-ci, car il existe une dépendance entre la quantité d'apprentissage par lot (taux d'apprentissage), le nombre de mises à jour par époque (taille du lot) et le nombre de mises à jour par époque. des époques.
La liste complète des codes est fournie ci-dessous :
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))
L’exécution de cet exemple produit le résultat suivant.
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}
Vous pouvez constater qu'avec SGD, les meilleurs résultats ont été obtenus en utilisant un taux d'apprentissage de 0,001 et un élan de 0,9 avec une précision d'environ 68 %.
Comment régler l'initialisation du poids du réseau
L'initialisation du poids du réseau neuronal était simple : utilisez de petites valeurs aléatoires.
Il existe désormais une suite de techniques différentes parmi lesquelles choisir. Vous pouvez obtenir une liste de blanchisserie à partir de la documentation torch.nn.init
.
Dans cet exemple, vous examinerez la sélection de l'initialisation du poids du réseau en évaluant toutes les techniques disponibles.
Vous utiliserez la même méthode d’initialisation du poids sur chaque couche. Idéalement, il serait peut-être préférable d’utiliser différents schémas d’initialisation de poids en fonction de la fonction d’activation utilisée sur chaque couche. Dans l'exemple ci-dessous, vous utiliserez un redresseur pour la couche cachée. Utilisez sigmoïde pour la couche de sortie car les prédictions sont binaires. L'initialisation du poids est implicite dans les modèles PyTorch. Par conséquent, vous devez écrire votre propre logique pour initialiser le poids, après la création de la couche mais avant son utilisation. Modifions le PyTorch comme suit :
# 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
Un argument weight_init
est ajouté à la classe PimaClassifier
et il attend l'un des initialiseurs de torch.nn.init
. Dans GridSearchCV
, vous devez utiliser le préfixe module__
pour que NeuralNetClassifier
achemine le paramètre vers le constructeur de classe du modèle.
La liste complète des codes est fournie ci-dessous :
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))
L’exécution de cet exemple produit le résultat suivant.
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>}
Les meilleurs résultats ont été obtenus avec un schéma d'initialisation à poids uniforme He, atteignant une performance d'environ 70 %.
Comment régler la fonction d'activation des neurones
La fonction d'activation contrôle la non-linéarité des neurones individuels et le moment où se déclencher.
Généralement, la fonction d’activation du redresseur est la plus populaire. Cependant, il s’agissait auparavant des fonctions sigmoïde et tanh, et ces fonctions peuvent encore être plus adaptées à différents problèmes.
Dans cet exemple, vous évaluerez certaines des fonctions d'activation disponibles dans PyTorch. Vous n'utiliserez ces fonctions que dans la couche cachée, car une fonction d'activation sigmoïde est requise dans la sortie du problème de classification binaire. Semblable à l'exemple précédent, il s'agit d'un argument du constructeur de classe du modèle, et vous utiliserez le préfixe module__
pour la grille de paramètres GridSearchCV
.
En général, il est judicieux de préparer les données en fonction des différentes fonctions de transfert, ce que vous ne ferez pas dans ce cas.
La liste complète des codes est fournie ci-dessous :
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))
L’exécution de cet exemple produit le résultat suivant.
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'>}
Il montre que la fonction d'activation ReLU a obtenu les meilleurs résultats avec une précision d'environ 70 %.
Comment régler la régularisation des abandons
Dans cet exemple, vous examinerez l’ajustement du taux d’abandon pour la régularisation dans le but de limiter le surajustement et d’améliorer la capacité du modèle à généraliser.
Pour de meilleurs résultats, il est préférable de combiner l'abandon avec une contrainte de poids telle que la contrainte de norme maximale, qui est implémentée dans la fonction de passage direct.
Cela implique d’ajuster à la fois le pourcentage d’abandon et la contrainte de poids. Nous essaierons des pourcentages d'abandon compris entre 0,0 et 0,9 (1,0 n'a pas de sens) et des valeurs de contrainte de poids MaxNorm comprises entre 0 et 5.
La liste complète des codes est fournie ci-dessous.
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))
L’exécution de cet exemple produit le résultat suivant.
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}
Vous pouvez voir que le taux d'abandon de 10 % et la contrainte de poids de 2,0 ont abouti à une meilleure précision d'environ 70 %.
Comment régler le nombre de neurones dans la couche cachée
Le nombre de neurones dans une couche est un paramètre important à régler. Généralement, le nombre de neurones dans une couche contrôle la capacité de représentation du réseau, du moins à ce stade de la topologie.
Généralement, un réseau monocouche suffisamment grand peut se rapprocher de n’importe quel autre réseau neuronal, grâce au théorème d’approximation universelle.
Dans cet exemple, vous examinerez le réglage du nombre de neurones dans une seule couche cachée. vous essayerez les valeurs de 1 à 30 par pas de 5.
Un réseau plus grand nécessite plus de formation et au moins la taille du lot et le nombre d'époques devraient idéalement être optimisés avec le nombre de neurones.
La liste complète des codes est fournie ci-dessous.
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))
L’exécution de cet exemple produit le résultat suivant.
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}
Vous pouvez voir que les meilleurs résultats ont été obtenus avec un réseau de 30 neurones dans la couche cachée avec une précision d'environ 71 %.
Conseils pour l’optimisation des hyperparamètres
Cette section répertorie quelques conseils pratiques à prendre en compte lors du réglage des hyperparamètres de votre réseau neuronal.
- $k$-Fold Cross-Validation. Vous pouvez voir que les résultats des exemples de cet article montrent une certaine variance. Une validation croisée par défaut de 3 a été utilisée, mais peut-être que $k=5$ou $k=10$serait plus stable. Choisissez soigneusement votre configuration de validation croisée pour garantir la stabilité de vos résultats.
- Examinez l'ensemble de la grille. Ne vous concentrez pas uniquement sur le meilleur résultat, examinez l'ensemble de la grille de résultats et recherchez des tendances pour prendre en charge les décisions de configuration. Bien sûr, il y aura plus de combinaisons et l’évaluation prendra plus de temps.
- Paralléliser. Utilisez tous vos cœurs si vous le pouvez, les réseaux de neurones sont lents à s'entraîner et nous voulons souvent essayer beaucoup de paramètres différents. Pensez à l'exécuter sur une plate-forme cloud, telle qu'AWS.
- Utilisez un échantillon de votre ensemble de données. Étant donné que les réseaux sont lents à s'entraîner, essayez de les entraîner sur un échantillon plus petit de votre ensemble de données d'entraînement, juste pour avoir une idée des orientations générales des paramètres plutôt que des configurations optimales.
- Commencez avec des grilles grossières. Commencez avec des grilles à gros grain et zoomez sur des grilles à grain plus fin une fois que vous pouvez réduire la portée.
- Ne pas transférer les résultats. Les résultats sont généralement spécifiques au problème. Essayez d'éviter les configurations favorites pour chaque nouveau problème que vous rencontrez. Il est peu probable que les résultats optimaux que vous découvrez sur un problème soient transférés à votre prochain projet. Recherchez plutôt des tendances plus larges, comme le nombre de couches ou les relations entre les paramètres.
- La reproductibilité est un problème. Bien que nous ayons défini la graine du générateur de nombres aléatoires dans NumPy, les résultats ne sont pas reproductibles à 100 %. La reproductibilité lors de la recherche sur grille de modèles PyTorch enveloppés est plus complexe que ce qui est présenté dans cet article.
Lectures complémentaires
Cette section fournit plus de ressources sur le sujet si vous souhaitez approfondir.
- documentation de Skorch
- torch.nn de PyTorch
- GridSearchCV de scikit-learn
Résumé
Dans cet article, vous avez découvert comment régler les hyperparamètres de vos réseaux de deep learning en Python à l'aide de PyTorch et scikit-learn.
Plus précisément, vous avez appris :
- Comment envelopper des modèles PyTorch pour les utiliser dans scikit-learn et comment utiliser la recherche par grille.
- Comment rechercher sur une grille une suite de différents paramètres de réseau neuronal standard pour les modèles PyTorch.
- Comment concevoir vos propres expériences d'optimisation d'hyperparamètres.