Comment évaluer les performances des modèles PyTorch
Concevoir un modèle d'apprentissage profond est parfois un art. Il y a beaucoup de points de décision à prendre et il n’est pas facile de dire lequel est le meilleur. Une façon d’élaborer une conception consiste à procéder par essais et erreurs et à évaluer le résultat sur des données réelles. Par conséquent, il est important de disposer d’une méthode scientifique pour évaluer les performances de votre réseau neuronal et de vos modèles d’apprentissage profond. En fait, c’est aussi la même méthode pour comparer tout type de modèles de machine learning sur un usage particulier.
Dans cet article, vous découvrirez le flux de travail reçu pour évaluer de manière robuste les performances du modèle. Dans les exemples, nous utiliserons PyTorch pour construire nos modèles, mais la méthode peut également être appliquée à d'autres modèles. Après avoir terminé cet article, vous saurez :
- Comment évaluer un modèle PyTorch à l'aide d'un ensemble de données de vérification
- Comment évaluer un modèle PyTorch avec une validation croisée k-fold
Démarrez votre projet avec mon livre Deep Learning with PyTorch. Il fournit des tutoriels d'auto-apprentissage avec du code fonctionnel.
Aperçu
Ce chapitre est composé de quatre parties ; ils sont:
- Évaluation empirique des modèles
- Fractionnement des données
- Entraîner un modèle PyTorch avec validation
- Validation croisée k-fold
Évaluation empirique des modèles
Lors de la conception et de la configuration d’un modèle d’apprentissage profond à partir de zéro, de nombreuses décisions doivent être prises. Cela inclut des décisions de conception telles que le nombre de couches à utiliser dans un modèle d'apprentissage en profondeur, la taille de chaque couche et le type de couches ou de fonctions d'activation à utiliser. Il peut également s'agir du choix de la fonction de perte, de l'algorithme d'optimisation, du nombre d'époques à entraîner et de l'interprétation des résultats du modèle. Heureusement, vous pouvez parfois copier la structure des réseaux d’autres personnes. Parfois, vous pouvez simplement faire votre choix en utilisant quelques heuristiques. Pour savoir si vous avez fait un bon choix ou non, le meilleur moyen est de comparer plusieurs alternatives en les évaluant empiriquement avec des données réelles.
L’apprentissage profond est souvent utilisé pour des problèmes comportant de très grands ensembles de données. Cela représente des dizaines de milliers ou des centaines de milliers d’échantillons de données. Cela fournit de nombreuses données pour les tests. Mais vous devez disposer d’une stratégie de test robuste pour estimer les performances de votre modèle sur des données invisibles. Sur cette base, vous pouvez disposer d'une métrique à comparer entre différentes configurations de modèles.
Fractionnement des données
Si vous disposez d’un ensemble de données de dizaines de milliers d’échantillons, voire plus, vous n’avez pas toujours besoin de tout donner à votre modèle pour l’entraînement. Cela augmentera inutilement la complexité et allongera la durée de la formation. Plus n’est pas toujours mieux. Vous n’obtiendrez peut-être pas le meilleur résultat.
Lorsque vous disposez d'une grande quantité de données, vous devez en prendre une partie comme ensemble d'entraînement qui est introduit dans le modèle pour l'entraînement. Une autre partie est conservée comme ensemble de test pour ne pas participer à la formation, mais vérifiée avec un modèle entraîné ou partiellement entraîné à titre d'évaluation. Cette étape est généralement appelée « répartition train-test ».
Considérons l'ensemble de données sur le diabète des Indiens Pima. Vous pouvez charger les données en utilisant NumPy :
import numpy as np
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
Il existe 768 échantillons de données. Ce n'est pas beaucoup mais cela suffit à démontrer la rupture. Considérons les premiers 66 % comme l’ensemble d’entraînement et les autres comme l’ensemble de test. Le moyen le plus simple de procéder consiste à découper un tableau :
# find the boundary at 66% of total samples
count = len(data)
n_train = int(count * 0.66)
# split the data at the boundary
train_data = data[:n_train]
test_data = data[n_train:]
Le choix de 66 % est arbitraire, mais vous ne voulez pas que l'ensemble d'entraînement soit trop petit. Parfois, vous pouvez utiliser une répartition entre 70 % et 30 %. Mais si l'ensemble de données est énorme, vous pouvez même utiliser une répartition de 30 à 70 % si 30 % des données d'entraînement sont suffisamment volumineuses.
Si vous divisez les données de cette manière, vous suggérez que les ensembles de données soient mélangés afin que l'ensemble d'entraînement et l'ensemble de test soient également diversifiés. Si vous constatez que l'ensemble de données d'origine est trié et que vous effectuez l'ensemble de test uniquement à la fin, vous constaterez peut-être que toutes les données de test appartiennent à la même classe ou portent la même valeur dans l'une des entités d'entrée. Ce n’est pas idéal.
Bien sûr, vous pouvez appeler np.random.shuffle(data)
avant la scission pour éviter cela. Mais de nombreux ingénieurs en apprentissage automatique utilisent généralement scikit-learn pour cela. Voir cet exemple :
import numpy as np
from sklearn.model_selection import train_test_split
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
train_data, test_data = train_test_split(data, test_size=0.33)
Mais le plus souvent, cela se fait après avoir séparé la fonctionnalité d’entrée et les étiquettes de sortie. Notez que cette fonction de scikit-learn peut fonctionner non seulement sur les tableaux NumPy mais aussi sur les tenseurs PyTorch :
import numpy as np
import torch
from sklearn.model_selection import train_test_split
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
Entraîner un modèle PyTorch avec validation
Revoyons le code pour créer et entraîner un modèle d'apprentissage profond sur cet ensemble de données :
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
...
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 50 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(Xtrain) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
bar.set_postfix(
loss=float(loss)
)
Dans ce code, un lot est extrait de l'ensemble d'entraînement à chaque itération et envoyé au modèle lors de la passe avant. Ensuite, vous calculez le gradient dans la passe arrière et mettez à jour les poids.
Bien que, dans ce cas, vous ayez utilisé l'entropie croisée binaire comme métrique de perte dans la boucle d'entraînement, vous êtes peut-être davantage préoccupé par la précision de la prédiction. Le calcul de la précision est facile. Vous arrondissez le résultat (dans la plage de 0 à 1) à l'entier le plus proche afin d'obtenir une valeur binaire de 0 ou 1. Ensuite, vous comptez le pourcentage de correspondance de votre prédiction avec l'étiquette ; cela vous donne la précision.
Mais quelle est votre prédiction ? Il s'agit de y_pred
ci-dessus, qui est la prédiction de votre modèle actuel sur X_batch
. Ajouter de la précision à la boucle d'entraînement devient ceci :
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress, with accuracy
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss)
acc=float(acc)
)
Cependant, le X_batch
et le y_batch
sont utilisés par l'optimiseur, et l'optimiseur affinera votre modèle afin qu'il puisse prédire y_batch
à partir de X_batch
. Et maintenant, vous utilisez la précision pour vérifier si y_pred
correspond à y_batch
. C'est comme tricher, car si votre modèle se souvient d'une manière ou d'une autre de la solution, il peut simplement vous rapporter le y_pred
et obtenir une précision parfaite sans réellement déduire y_pred
de X_batch
.
En effet, un modèle d’apprentissage profond peut être si compliqué que vous ne pouvez pas savoir si votre modèle se souvient simplement de la réponse ou s’il déduit la réponse. Par conséquent, la meilleure façon n'est pas de calculer la précision à partir de X_batch
ou de quoi que ce soit de X_train
mais à partir de quelque chose d'autre : votre ensemble de test. Ajoutons une mesure de précision après chaque époque en utilisant X_test
:
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate model at end of epoch
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
acc = float(acc)
print(f"End of {epoch}, accuracy {acc}")
Dans ce cas, le acc
dans la boucle for interne n'est qu'une métrique montrant la progression. Pas beaucoup de différence dans l'affichage de la métrique de perte, sauf qu'elle n'est pas impliquée dans l'algorithme de descente de gradient. Et vous vous attendez à ce que la précision s’améliore à mesure que la métrique de perte s’améliore également.
Dans la boucle for externe, à la fin de chaque époque, vous calculez la précision à partir de X_test
. Le flux de travail est similaire : vous donnez l'ensemble de test au modèle et demandez sa prédiction, puis comptez le nombre de résultats correspondants avec les étiquettes de votre ensemble de test. Mais cette précision est celle dont vous devez vous soucier. Cela devrait s'améliorer au fur et à mesure que l'entraînement progresse, mais si vous ne le voyez pas s'améliorer (c'est-à-dire augmenter la précision) ou même se détériorer, vous devez interrompre l'entraînement car il semble commencer à surajuster. Le surapprentissage se produit lorsque le modèle a commencé à se souvenir de l'ensemble d'entraînement plutôt que d'apprendre à en déduire la prédiction. Un signe en est que la précision de l’ensemble d’entraînement ne cesse d’augmenter tandis que la précision de l’ensemble de test diminue.
Voici le code complet pour implémenter tout ce qui précède, du fractionnement des données à la validation à l'aide de l'ensemble de test :
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
from sklearn.model_selection import train_test_split
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 50 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(X_train) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar: #, disable=True) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate model at end of epoch
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
acc = float(acc)
print(f"End of {epoch}, accuracy {acc}")
Le code ci-dessus imprimera ce qui suit :
End of 0, accuracy 0.5787401795387268
End of 1, accuracy 0.6102362275123596
End of 2, accuracy 0.6220472455024719
End of 3, accuracy 0.6220472455024719
End of 4, accuracy 0.6299212574958801
End of 5, accuracy 0.6377952694892883
End of 6, accuracy 0.6496062874794006
End of 7, accuracy 0.6535432934761047
End of 8, accuracy 0.665354311466217
End of 9, accuracy 0.6614173054695129
End of 10, accuracy 0.665354311466217
End of 11, accuracy 0.665354311466217
End of 12, accuracy 0.665354311466217
End of 13, accuracy 0.665354311466217
End of 14, accuracy 0.665354311466217
End of 15, accuracy 0.6732283234596252
End of 16, accuracy 0.6771653294563293
End of 17, accuracy 0.6811023354530334
End of 18, accuracy 0.6850393414497375
End of 19, accuracy 0.6889764070510864
End of 20, accuracy 0.6850393414497375
End of 21, accuracy 0.6889764070510864
End of 22, accuracy 0.6889764070510864
End of 23, accuracy 0.6889764070510864
End of 24, accuracy 0.6889764070510864
End of 25, accuracy 0.6850393414497375
End of 26, accuracy 0.6811023354530334
End of 27, accuracy 0.6771653294563293
End of 28, accuracy 0.6771653294563293
End of 29, accuracy 0.6692913174629211
End of 30, accuracy 0.6732283234596252
End of 31, accuracy 0.6692913174629211
End of 32, accuracy 0.6692913174629211
End of 33, accuracy 0.6732283234596252
End of 34, accuracy 0.6771653294563293
End of 35, accuracy 0.6811023354530334
End of 36, accuracy 0.6811023354530334
End of 37, accuracy 0.6811023354530334
End of 38, accuracy 0.6811023354530334
End of 39, accuracy 0.6811023354530334
End of 40, accuracy 0.6811023354530334
End of 41, accuracy 0.6771653294563293
End of 42, accuracy 0.6771653294563293
End of 43, accuracy 0.6771653294563293
End of 44, accuracy 0.6771653294563293
End of 45, accuracy 0.6771653294563293
End of 46, accuracy 0.6771653294563293
End of 47, accuracy 0.6732283234596252
End of 48, accuracy 0.6732283234596252
End of 49, accuracy 0.6732283234596252
Validation croisée k-fold
Dans l'exemple ci-dessus, vous avez calculé la précision à partir de l'ensemble de test. Il est utilisé comme score pour le modèle au fur et à mesure de votre progression dans la formation. Vous souhaitez vous arrêter au point où ce score est à son maximum. En fait, en comparant simplement le score de cet ensemble de tests, vous savez que votre modèle fonctionne mieux après l'époque 21 et commence à être surajusté par la suite. Est-ce vrai ?
Si vous avez construit deux modèles de conceptions différentes, devriez-vous simplement comparer la précision de ces modèles sur le même ensemble de tests et affirmer que l’un est meilleur que l’autre ?
En fait, vous pouvez affirmer que l'ensemble de test n'est pas suffisamment représentatif même après avoir mélangé votre ensemble de données avant d'extraire l'ensemble de test. Vous pouvez également affirmer que, par hasard, un modèle correspond mieux à cet ensemble de tests particulier, mais pas toujours mieux. Pour présenter un argument plus solide sur le modèle qui est le meilleur, indépendamment de la sélection de l'ensemble de tests, vous pouvez essayer plusieurs ensembles de tests et faire la moyenne de la précision.
C'est ce que fait une validation croisée k-fold. C'est un progrès que de décider quelle conception fonctionne le mieux. Il fonctionne en répétant le processus de formation à partir de zéro plusieurs fois $k$, chacun avec une composition différente des ensembles de formation et de test. Pour cette raison, vous aurez $k$modèles et $k$scores de précision de leur ensemble de tests respectif. Vous n’êtes pas seulement intéressé par la précision moyenne mais aussi par l’écart type. L'écart type indique si le score de précision est cohérent ou si un ensemble de tests est particulièrement bon ou mauvais dans un modèle.
Étant donné que la validation croisée k-fold entraîne le modèle à partir de zéro plusieurs fois, il est préférable d'enrouler la boucle d'entraînement dans une fonction :
def model_train(X_train, y_train, X_test, y_test):
# create new model
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 25 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(X_train) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate accuracy at end of training
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
return float(acc)
Le code ci-dessus n'imprime délibérément rien (avec disable=True
dans tqdm
) pour garder l'écran moins encombré.
Également à partir de scikit-learn, vous disposez d'une fonction de validation croisée k-fold. Vous pouvez l'utiliser pour produire une estimation robuste de la précision du modèle :
from sklearn.model_selection import StratifiedKFold
# define 5-fold cross validation test harness
kfold = StratifiedKFold(n_splits=5, shuffle=True)
cv_scores = []
for train, test in kfold.split(X, y):
# create model, train, and get accuracy
acc = model_train(X[train], y[train], X[test], y[test])
print("Accuracy: %.2f" % acc)
cv_scores.append(acc)
# evaluate the model
print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100))
L'exécution de ceci imprime :
Accuracy: 0.64
Accuracy: 0.67
Accuracy: 0.68
Accuracy: 0.63
Accuracy: 0.59
64.05% (+/- 3.30%)
Dans scikit-learn, il existe plusieurs fonctions de validation croisée k-fold, et celle utilisée ici est k-fold stratifiée. Il suppose que y
sont des étiquettes de classe et prend en compte leurs valeurs de manière à fournir une représentation de classe équilibrée dans les divisions.
Le code ci-dessus utilisait $k=5$ou 5 fractionnements. Cela signifie diviser l'ensemble de données en cinq parties égales, en choisir une comme ensemble de test et combiner le reste dans un ensemble d'entraînement. Il existe cinq façons de procéder, donc la boucle for ci-dessus aura cinq itérations. À chaque itération, vous appelez la fonction model_train()
et obtenez le score de précision en retour. Ensuite, vous l'enregistrez dans une liste, qui sera utilisée pour calculer la moyenne et l'écart type à la fin.
L'objet kfold
vous renverra les indices. Par conséquent, vous n'avez pas besoin d'exécuter la répartition train-test à l'avance, mais utilisez les indices fournis pour extraire l'ensemble d'entraînement et l'ensemble de test à la volée lorsque vous appelez la fonction model_train()
.
Le résultat ci-dessus montre que le modèle est moyennement bon, avec une précision moyenne de 64 %. Et ce score est stable puisque l'écart type est à 3%. Cela signifie que la plupart du temps, vous vous attendez à ce que la précision du modèle soit comprise entre 61 % et 67 %. Vous pouvez essayer de modifier le modèle ci-dessus, par exemple en ajoutant ou en supprimant une couche, et voir l'ampleur du changement dans la moyenne et l'écart type. Vous pouvez également essayer d'augmenter le nombre d'époques utilisées dans l'entraînement et observer le résultat.
La moyenne et l'écart type de la validation croisée k-fold sont ce que vous devez utiliser pour comparer la conception d'un modèle.
En reliant le tout ensemble, vous trouverez ci-dessous le code complet pour la validation croisée k-fold :
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
from sklearn.model_selection import StratifiedKFold
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
def model_train(X_train, y_train, X_test, y_test):
# create new model
model = nn.Sequential(
nn.Linear(8, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 1),
nn.Sigmoid()
)
# loss function and optimizer
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)
n_epochs = 25 # number of epochs to run
batch_size = 10 # size of each batch
batches_per_epoch = len(X_train) // batch_size
for epoch in range(n_epochs):
with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar:
bar.set_description(f"Epoch {epoch}")
for i in bar:
# take a batch
start = i * batch_size
X_batch = X_train[start:start+batch_size]
y_batch = y_train[start:start+batch_size]
# forward pass
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
# backward pass
optimizer.zero_grad()
loss.backward()
# update weights
optimizer.step()
# print progress
acc = (y_pred.round() == y_batch).float().mean()
bar.set_postfix(
loss=float(loss),
acc=float(acc)
)
# evaluate accuracy at end of training
y_pred = model(X_test)
acc = (y_pred.round() == y_test).float().mean()
return float(acc)
# define 5-fold cross validation test harness
kfold = StratifiedKFold(n_splits=5, shuffle=True)
cv_scores = []
for train, test in kfold.split(X, y):
# create model, train, and get accuracy
acc = model_train(X[train], y[train], X[test], y[test])
print("Accuracy: %.2f" % acc)
cv_scores.append(acc)
# evaluate the model
print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100))
Résumé
Dans cet article, vous avez découvert l'importance de disposer d'un moyen robuste pour estimer les performances de vos modèles d'apprentissage profond sur des données invisibles, et vous avez appris comment le faire. Vous avez vu :
- Comment diviser les données en ensembles de formation et de test à l'aide de scikit-learn
- Comment effectuer une validation croisée k-fold à l'aide de scikit-learn
- Comment modifier la boucle de formation dans un modèle PyTorch pour intégrer la validation des ensembles de tests et la validation croisée