Entraîner des modèles avec PyTorch

Effectué

PyTorch est une infrastructure de Machine Learning couramment utilisée pour l’entraînement des modèles Deep Learning. Dans Azure Databricks, PyTorch est préinstallé dans les clusters ML.

Notes

Dans cette unité, les extraits de code sont fournis à titre d’exemples pour mettre en évidence les points clés. Au cours de l’exercice figurant plus loin dans ce module, vous aurez l’occasion d’exécuter le code d’un exemple complet et opérationnel.

Définir un réseau PyTorch

Dans PyTorch, les modèles sont basés sur un réseau que vous définissez. Le réseau se compose de plusieurs couches, chacune avec des entrées et des sorties spécifiées. De plus, le travail définit une fonction de transfert qui applique des fonctions à chaque couche à mesure que les données sont transmises via le réseau.

L’exemple de code suivant définit un réseau.

import torch
import torch.nn as nn
import torch.nn.functional as F

class MyNet(nn.Module):
    def __init__(self):
        super(MyNet, self).__init__()
        self.layer1 = nn.Linear(4, 5)
        self.layer2 = nn.Linear(5, 5)
        self.layer3 = nn.Linear(5, 3)

    def forward(self, x):
        layer1_output = torch.relu(self.layer1(x))
        layer2_output = torch.relu(self.layer2(layer1_output))
        y = self.layer3(layer2_output)
        return y

Bien que le code puisse sembler complexe au premier abord, cette classe définit un réseau relativement simple comportant trois couches :

  • Une couche d’entrée qui accepte quatre valeurs d’entrée et génère cinq valeurs de sortie pour la couche suivante.
  • Une couche qui accepte cinq entrées et génère cinq sorties.
  • Une couche de sortie finale qui accepte cinq entrées et génère trois sorties.

La fonction de transfert applique les couches aux données d’entrée (x), en passant la sortie de chaque couche à la suivante et, pour finir, en retournant la sortie de la dernière couche (qui contient le vecteur de prédiction d’étiquette, y). Une fonction d’activation d’unité linéaire régularisée (ReLU) est appliquée aux sorties des couches 1 et 2 pour limiter les valeurs de sortie à des nombres positifs.

Notes

Selon le type de critère de perte utilisé, vous pouvez choisir d’appliquer une fonction d’activation telle que log_softmax à la valeur de retour pour la forcer à figurer dans la plage comprise entre 0 et 1. Toutefois, certains critères de perte (comme CrossEntropyLoss, qui est couramment utilisé pour la classification multiclasse) appliquent automatiquement une fonction appropriée.

Pour créer un modèle pour l’entraînement, il vous suffit de créer une instance de la classe de réseau comme suit :

myModel = MyNet()

Préparer les données pour la modélisation

Les couches PyTorch fonctionnent sur des données qui sont mises en forme en tant que tenseurs, c’est-à-dire des structures de type matrice. Il existe différentes fonctions pour convertir d’autres formats de données courants en tenseurs, et vous pouvez définir un chargeur de données PyTorch pour lire les tenseurs de données dans un modèle à des fins d’entraînement ou d’inférence.

Comme pour la plupart des techniques de Machine Learning supervisées, vous devez définir des jeux de données distincts pour l’entraînement et la validation. Cette séparation vous permet de vérifier que le modèle effectue des prédictions justes lorsqu’il est présenté avec des données sur lesquelles il n’a pas été entraîné.

Le code suivant définit deux chargeurs de données : l’un pour l’entraînement et l’autre pour les tests. Dans cet exemple, les données sources de chaque chargeur sont supposées être un tableau Numpy de valeurs de caractéristiques et un tableau Numpy des valeurs d’étiquette correspondantes.

# Create a dataset and loader for the training data and labels
train_x = torch.Tensor(x_train).float()
train_y = torch.Tensor(y_train).long()
train_ds = td.TensorDataset(train_x,train_y)
train_loader = td.DataLoader(train_ds, batch_size=20,
    shuffle=False, num_workers=1)

# Create a dataset and loader for the test data and labels
test_x = torch.Tensor(x_test).float()
test_y = torch.Tensor(y_test).long()
test_ds = td.TensorDataset(test_x,test_y)
test_loader = td.DataLoader(test_ds, batch_size=20,
    shuffle=False, num_workers=1)

Dans cet exemple, les chargeurs divisent les données en lots de 30, qui sont passés à la fonction de transfert pendant l’entraînement ou l’inférence.

Choisir un critère de perte et un algorithme d’optimiseur

Le modèle est entraîné en alimentant les données d’entraînement dans le réseau, en mesurant la perte (la différence agrégée entre les valeurs prédites et les valeurs réelles) et en optimisant le réseau grâce à l’ajustement des pondérations et des équilibres afin de réduire la perte. Les détails spécifiques de la façon dont la perte est calculée et réduite sont régis par le critère de perte et l’algorithme d’optimiseur que vous choisissez.

Critères de perte

PyTorch prend en charge plusieurs fonctions de critères de perte, notamment (parmi beaucoup d’autres) :

  • cross_entropy : fonction qui mesure la différence agrégée entre les valeurs prédites et les valeurs réelles pour plusieurs variables (généralement utilisée pour mesurer la perte relative aux probabilités de classe dans la classification multiclasse).
  • binary_cross_entropy : fonction qui mesure la différence entre les probabilités prédites et réelles (généralement utilisée afin de mesurer la perte pour les probabilités de classe dans la classification binaire).
  • mse_loss : fonction qui mesure la perte d’erreur quadratique moyenne pour les valeurs numériques prédites et réelles (généralement utilisée pour la régression).

Pour spécifier le critère de perte que vous souhaitez utiliser lors de l’entraînement de votre modèle, vous créez une instance de la fonction appropriée, comme suit :

import torch.nn as nn

loss_criteria = nn.CrossEntropyLoss

Conseil

Pour plus d’informations sur les critères de perte disponibles dans PyTorch, consultez Fonctions de perte dans la documentation PyTorch.

Algorithmes d’optimiseur

Après avoir calculé la perte, un optimiseur est utilisé pour déterminer la meilleure façon d’ajuster les pondérations et les équilibres afin de la réduire. Les optimiseurs sont des implémentations spécifiques d’une approche de descente de gradient pour réduire une fonction. Les optimiseurs disponibles dans PyTorch sont (entre autres) les suivants :

Pour entraîner un modèle à l’aide de l’un de ces algorithmes, vous devez créer une instance de l’optimiseur et définir tous les paramètres requis. Les paramètres spécifiques varient en fonction de l’optimiseur choisi, mais la plupart d’entre eux vous obligent à spécifier un taux d’entraînement qui régit la taille des ajustements effectués avec chaque optimisation.

Le code suivant crée une instance de l’optimiseur Adam.

import torch.optim as opt

learning_rate = 0.001
optimizer = opt.Adam(model.parameters(), lr=learning_rate)

Conseil

Pour plus d’informations sur les optimiseurs disponibles dans PyTorch, consultez Algorithmes dans la documentation PyTorch.

Créer des fonctions d’entraînement et de test

Une fois que vous avez défini un réseau et préparé des données pour celui-ci, vous pouvez utiliser les données pour entraîner et tester un modèle en transmettant les données d’entraînement via le réseau, en calculant la perte, en optimisant les pondérations et les tendances du réseau et en validant les performances du réseau avec les données de test. Il est courant de définir une fonction qui transmet des données via le réseau pour entraîner le modèle avec les données d’entraînement, et une fonction distincte pour tester le modèle avec les données de test.

Créer une fonction d’entraînement

L’exemple suivant montre une fonction permettant d’entraîner un modèle.

def train(model, data_loader, optimizer):

    # Use GPU if available, otherwise CPU
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)
    
    # Set the model to training mode (to enable backpropagation)
    model.train()
    train_loss = 0
    
    # Feed the batches of data forward through the network
    for batch, tensor in enumerate(data_loader):
        data, target = tensor # Specify features and labels in a tensor
        optimizer.zero_grad() # Reset optimizer state
        out = model(data) # Pass the data through the network
        loss = loss_criteria(out, target) # Calculate the loss
        train_loss += loss.item() # Keep a running total of loss for each batch

        # backpropagate adjustments to weights/bias
        loss.backward()
        optimizer.step()

    #Return average loss for all batches
    avg_loss = train_loss / (batch+1)
    print('Training set: Average loss: {:.6f}'.format(avg_loss))
    return avg_loss

L’exemple suivant montre une fonction permettant de tester le modèle.

def test(model, data_loader):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)
    # Switch the model to evaluation mode (so we don't backpropagate)
    model.eval()
    test_loss = 0
    correct = 0

    # Pass the data through with no gradient computation
    with torch.no_grad():
        batch_count = 0
        for batch, tensor in enumerate(data_loader):
            batch_count += 1
            data, target = tensor
            # Get the predictions
            out = model(data)

            # calculate the loss
            test_loss += loss_criteria(out, target).item()

            # Calculate the accuracy
            _, predicted = torch.max(out.data, 1)
            correct += torch.sum(target==predicted).item()
            
    # Calculate the average loss and total accuracy for all batches
    avg_loss = test_loss/batch_count
    print('Validation set: Average loss: {:.6f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        avg_loss, correct, len(data_loader.dataset),
        100. * correct / len(data_loader.dataset)))
    return avg_loss

Entraîner le modèle sur plusieurs époques

Pour entraîner un modèle Deep Learning, vous exécutez généralement plusieurs fois la fonction d’entraînement (appelée époques), dans le but de réduire la perte calculée à partir des données d’entraînement pour chaque époque. Vous pouvez utiliser votre fonction de test pour valider que la perte des données de test (sur lesquelles le modèle n’a pas été entraîné) se réduit également en accord avec la perte d’entraînement. En d’autres termes, vous vérifiez que l’entraînement du modèle ne produit pas un modèle surajusté aux données d’entraînement.

Conseil

Vous n’avez pas besoin d’exécuter la fonction de test pour chaque époque. Vous pouvez choisir de l’exécuter toutes les deux époques, ou une fois à la fin. Toutefois, il peut être utile de tester le modèle au moment où il est entraîné pour déterminer après combien d’époques un modèle commence à être surajusté.

Le code suivant entraîne un modèle sur 50 époques.

epochs = 50
for epoch in range(1, epochs + 1):

    # print the epoch number
    print('Epoch: {}'.format(epoch))
    
    # Feed training data into the model to optimize the weights
    train_loss = train(model, train_loader, optimizer)
    print(train_loss)
    
    # Feed the test data into the model to check its performance
    test_loss = test(model, test_loader)
    print(test_loss)

Enregistrer l’état d’un modèle entraîné

Une fois que vous avez correctement entraîné un modèle, vous pouvez enregistrer ses pondérations et ses tendances comme suit :

model_file = '/dbfs/my_model.pkl'
torch.save(model.state_dict(), model_file)

Pour charger et utiliser le modèle ultérieurement, créez une instance de la classe de réseau sur laquelle le modèle est basé, puis chargez les pondérations et les tendances enregistrées.

model = myNet()
model.load_state_dict(torch.load(model_file))