Apprentissage

Boucle d’apprentissage

CPU

Grâce à la mise en place de la structure Scenario, nous pouvons écrire une fonction générique qui applique l’algorithme d’optimisation quel que soit l’optimiseur ou le réseau choisi :

def train(S):

  S.model.train()

  for epoch in range(S.epochs):

      print(f"Epoch {epoch+1}/{S.epochs}")

      for i, (x, y) in enumerate(S.train_loader):

          print(">>",f"Batch {i+1}/{len(S.train_loader)} - shape: {x.size()}")
          S.optimizer.zero_grad()
          loss = S.loss_fn(S.model(x), y)
          loss.backward()
          S.optimizer.step()

S = createScenario()
train(S)

Quelques commentaires :

  • L’appel de la fonction train() permet d’activer les couches dédiées à l’apprentissage comme les couches de dropout

  • L’appel de optimizer.zero_grad() permet de remettre à zéro les informations utilisées pour le calcul du gradient

  • L’appel de optimizer.step() applique l’algorithme du gradient

GPU

Nous présentons une version GPU de la boucle d’apprentissage. Au niveau de notre programme, il faut déplacer les données de la mémoire CPU vers celle du GPU en utilisant la fonction to(..). Tout d’abord, on ajoute un paramètre suplémentaire pour gérer la plateforme cible :

S.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Ensuite, on modifie la fonction d’apprentissage, pour qu’elle transfère le modèle et les données vers le GPU :

def trainGPU(S):

  S.model = S.model.to(S.device)
  S.model.train()

  for epoch in range(S.epochs):

      print(f"Epoch {epoch+1}/{S.epochs}")

      for i, (x, y) in enumerate(S.train_loader):
          x = x.to(S.device)
          y = y.to(S.device)

          print(">>",f"Batch {i+1}/{len(S.train_loader)} - shape: {x.size()}")
          S.optimizer.zero_grad()
          loss = S.loss_fn(S.model(x), y)
          loss.backward()
          S.optimizer.step()

Une fois les données transférées sur le GPU, les calculs sont automatiquement effectués sur le GPU, il n’y a pas de modifications supplémentaires à apporter au code.

Gain GPU

Quel gain de performance peut-on espérer ? Question difficile, nous pouvons cependant donner quelques indices :

1- Le gain dépend de l’écart entre la gamme du CPU et celle du GPU :

  • Avec un CPU haut de gamme vs un GPU entrée de gamme : x2-x3

  • Avec un CPU entrée de gamme vs un GPU haut de gamme : x10-x30

2- Le gain dépend aussi de la nature des calculs : s’ils sont massivement parallèles le GPU est roi, s’il y a de nombreux branchements, le CPU peut lui voler la vedette.

Dans les datacenters, les GPU pour le deep learning ne cherchent pas le gain de performance, la dissipation thermique étant difficile à gérer. Avec des modèles géants comme les GPT, la priorité est devenue la quantité de mémoire et sa vitesse.

Loss

Pour monitorer l’apprentissage, nous devons examiner l’évolution de la loss associée à chaque epoch. Pour cela, on peut sommer les loss associées à chaque batch, mais le calcul serait légèrement biaisé à cause du dernier batch de taille différente. Il faut donc mettre en place une moyenne pondérée. En notant \(\mathcal{B}\) l’ensemble des batchs, nous avons :

\[\mathcal{Loss} = \frac{\sum_{b \in \mathcal{B}} Loss_b \cdot |b| } {\sum_{b \in \mathcal{B}} |b|}\]

Il faut maintenant modifier notre fonction d’apprentissage pourqu’elle gère à la fois le set d’entraînement et le set de validation ainsi que les loss associées :

 def train(S):

   S.model = S.model.to(S.device)
   for epoch in range(S.epochs):

       # TRAIN

       trainGlobLoss = 0
       S.model.train()
       for i, (x, y) in enumerate(S.train_batch):
           x = x.to(S.device)
           y = y.to(S.device)

           S.optimizer.zero_grad()
           loss = S.loss_fn(S.model(x), y)
           loss.backward()
           trainGlobLoss += loss.item() * x.size(0)
           S.optimizer.step()

       # VALIDATION

       validGlobLoss = 0
       S.model.eval()
       with torch.no_grad():
           for x, y in S.valid_batch:
               x = x.to(S.device)
               y = y.to(S.device)
               loss = S.loss_fn(S.model(x), y)
               validGlobLoss += loss.item() * x.size(0)

       #normalisation
       trainGlobLoss /= len(S.train_batch.dataset)
       validGlobLoss /= len(S.valid_batch.dataset)

       print(f"{epoch+1}/{S.epochs} - ",trainGlobLoss," - ", validGlobLoss)


 S = createScenario()
 train(S)

Accuracy

L”accuracy correspond au % de bonnes réponses trouvées par le réseau. Cet indicateur est tout aussi important que la Loss.

Par exemple, si on entraîne plusieurs réseaux, le meilleur correspond à celui qui obtient la meilleure accuracy sur le set de validation et non la meilleure Loss. Les courbes de Loss permettent de vérifier que l’apprentissage progresse, mais en aucun cas, elles représentent un indicateur précis de la performance du réseau.

Calcul

En sortie du réseau, nous avons un vecteur de logits de dimension [N,NbClasses], N étant la taille du batch.

  • La première étape consiste à utiliser la fonction predictions = argmax(dim=1) pour trouver pour chaque image la classe prédite. Nous obtenons donc un tenseur de taille [N] contenant les prédictions de chaque image.

  • Ensuite, nous comparons ce vecteur avec celui des catégories de référence, ceci en écrivant predictions == y, nous obtenons un tenseur de booléens.

  • La fonction sum() convertit associe la valeur 1 au booléen True et 0 à False. Le résultat donne le nombre de prédictions correctes dans le batch

  • Il reste à accumuler ce total la variable train_accuracy :

predictions     = logits.argmax(dim=1)
train_accuracy += (predictions == y).sum().item() trainTotal += y.size(0)

Nous aurions pu désactiver le gradient, mais cela n’a pas d’importance à ce niveau.

Tensorboard

Cet outil permet de monitorer en live l’évolution de l’apprentissage.

Installation

Cet outil s’installe comme un package Python standard. Pour ceux qui ont effectué l’installation sur le bureau, il suffit d’écrire :

C:\Users\...\Desktop\Pytorch314\python.exe -m pip install tensorboard

Intégration

from torch.utils.tensorboard import SummaryWriter

def train(S):

   loger = SummaryWriter(S.log_folder)

   for epoch in range(S.epochs):
      ...
      print(...)
      loger.add_scalars("Loss",      {"train": train_GlobLoss, "valid": valid_GlobLoss}, epoch+1)
      loger.add_scalars("Accuracy",  {"train": train_accuracy, "valid": valid_accuracy}, epoch+1)

   loger.close()

Le chemin Losstrain, se lit : ajoute un point au tracé nommé train dans le graphique nommé Loss. Ainsi, ce code construit 2 graphiques :

  • Un graphiqué nommé Loss : la courbe de Loss du train et la courbe de Loss de la validation

  • Un graphique nommé Acc : la courbe d’accuracy du train et la courbe d’accuracy de la validation

Toutes les informations concernant ces tracés sont stockés dans le répertoire C:\log\test1\.

Graphiques

Tensorboard permet de monitorer les courbes en live. Pour des runs qui peuvent durer 2h ou jusqu’à 2 semaines, il est important de pouvoir détecter rapidement si quelque chose disfonctionne et d’arrêter rapidement les frais.

L’affichage de Tensorboard permet de voir comment se comporte l’entraînement. Cependant, il n’est pas optimal pour l’analyse des courbes. En effet, il n’est pas possible de les regrouper par catégories, d’associer une couleur à chaque run… Bref, Tensorboard ne sait pas faire cela.

Ainsi, nous allons faire en sorte que le script exporte aussi les informations au format csv afin de les traiter dans un outil tierce. Pour cela, à chaque run, nous créons une liste Python vide pour stocker les informations :

loger.add_scalars(...)
info.append([epoch+1,train_accuracy,valid_accuracy,train_GlobLoss,valid_GlobLoss])

Une fois le run terminé, nous l’exportons vers un csv grâce à panda :

import pandas as pd

...

columns = ["epoch", "acc/train", "acc/valid", "loss/train", "loss/valid"]
df = pd.DataFrame(info, columns=columns)
df.to_csv( S.CSVname , index = False )

Code final

Nous avons maintenant mis en place une fonction d’apprentissage complète. Elle sert de référence pour la suite de nos projets :

Astuce

Snippet de code pour l’apprentissage sous PyTorch

from torch.utils.tensorboard import SummaryWriter

def train(S):

    loger = SummaryWriter(S.log_folder)
    info = [ ]

    S.model = S.model.to(S.device)
    for epoch in range(S.epochs):

        # TRAIN

        train_GlobLoss = train_accuracy = 0
        S.model.train()
        for x, y in S.train_batch:
            x = x.to(S.device)
            y = y.to(S.device)

            S.optimizer.zero_grad()
            logits = S.model(x)
            loss = S.loss_fn(logits,y)
            loss.backward()
            train_GlobLoss += loss.item() * x.size(0)
            S.optimizer.step()

            # accuracy
            predictions     = logits.argmax(dim=1)
            train_accuracy += (predictions == y).sum().item()

        # VALIDATION

        valid_GlobLoss = valid_accuracy = 0
        S.model.eval()
        with torch.no_grad():
            for x, y in S.valid_batch:
                x = x.to(S.device)
                y = y.to(S.device)
                logits = S.model(x)
                loss = S.loss_fn(logits, y)
                valid_GlobLoss += loss.item() * x.size(0)

                # accuracy
                predictions     = logits.argmax(dim=1)
                valid_accuracy += (predictions == y).sum().item()

        # indicateurs
        nbtrain, nbvalid = len(S.train_batch.dataset), len(S.valid_batch.dataset)
        train_GlobLoss  /= nbtrain
        train_accuracy  /= nbtrain
        valid_GlobLoss  /= nbvalid
        valid_accuracy  /= nbvalid

        print( f"{epoch+1}/{S.epochs} - "
            f"tLoss {train_GlobLoss:.3f}  - vLoss {valid_GlobLoss:.3f} - "
            f"tAcc  {train_accuracy:.3f}  -  vAcc {valid_accuracy:.3f}")

        loger.add_scalars("Loss",      {"train": train_GlobLoss, "valid": valid_GlobLoss}, epoch+1)
        loger.add_scalars("Accuracy",  {"train": train_accuracy, "valid": valid_accuracy}, epoch+1)
        info.append([epoch+1,train_accuracy,valid_accuracy,train_GlobLoss,valid_GlobLoss])

    loger.close()
    # CSV
    columns = ["epoch", "acc/train", "acc/valid", "loss/train", "loss/valid"]
    df = pd.DataFrame(info, columns=columns)
    df.to_csv( S.CSVname , index = False )

S = createScenario()
train(S)