NLP Course documentation

Un entraînement complet

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Un entraînement complet

Ask a Question

Maintenant nous allons voir comment obtenir les mêmes résultats que dans la dernière section sans utiliser la classe Trainer. Encore une fois, nous supposons que vous avez fait le traitement des données dans la section 2. Voici un court résumé couvrant tout ce dont vous aurez besoin :

from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)


def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=True)


tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

Préparer l’entraînement

Avant d’écrire réellement notre boucle d’entraînement, nous devons définir quelques objets. Les premiers sont les dataloaders que nous utiliserons pour itérer sur les batchs. Mais avant de pouvoir définir ces chargeurs de données, nous devons appliquer un peu de post-traitement à nos tokenized_datasets, pour prendre soin de certaines choses que le Trainer fait pour nous automatiquement. Spécifiquement, nous devons :

  • supprimer les colonnes correspondant aux valeurs que le modèle n’attend pas (comme les colonnes sentence1 et sentence2),
  • renommer la colonne label en labels (parce que le modèle s’attend à ce que l’argument soit nommé labels),
  • définir le format des jeux de données pour qu’ils retournent des tenseurs PyTorch au lieu de listes.

Notre tokenized_datasets a une méthode pour chacune de ces étapes :

tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")
tokenized_datasets["train"].column_names

Nous pouvons alors vérifier que le résultat ne comporte que des colonnes que notre modèle acceptera :

["attention_mask", "input_ids", "labels", "token_type_ids"]

Maintenant que cela est fait, nous pouvons facilement définir nos dataloaders :

from torch.utils.data import DataLoader

train_dataloader = DataLoader(
    tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator
)

Pour vérifier rapidement qu’il n’y a pas d’erreur dans le traitement des données, nous pouvons inspecter un batch comme celui-ci :

for batch in train_dataloader:
    break
{k: v.shape for k, v in batch.items()}
{'attention_mask': torch.Size([8, 65]),
 'input_ids': torch.Size([8, 65]),
 'labels': torch.Size([8]),
 'token_type_ids': torch.Size([8, 65])}

Notez que les formes réelles seront probablement légèrement différentes pour vous puisque nous avons défini shuffle=True pour le chargeur de données d’entraînement et que nous paddons à la longueur maximale dans le batch.

Maintenant que nous en avons terminé avec le prétraitement des données (un objectif satisfaisant mais difficile à atteindre pour tout praticien d’apprentissage automatique), passons au modèle. Nous l’instancions exactement comme nous l’avons fait dans la section précédente :

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

Pour s’assurer que tout se passera bien pendant l’entraînement, nous transmettons notre batch à ce modèle :

outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])

Tous les modèles 🤗 Transformers renvoient la perte lorsque les labels sont fournis. Nous obtenons également les logits (deux pour chaque entrée de notre batch, donc un tenseur de taille 8 x 2).

Nous sommes presque prêts à écrire notre boucle d’entraînement ! Il nous manque juste deux choses : un optimiseur et un planificateur de taux d’apprentissage. Puisque nous essayons de reproduire à la main ce que fait la fonction Trainer, utilisons les mêmes paramètres par défaut. L’optimiseur utilisé par Trainer est AdamW, qui est le même qu’Adam, mais avec une torsion pour la régularisation par décroissance de poids (voir Decoupled Weight Decay Regularization par Ilya Loshchilov et Frank Hutter) :

from transformers import AdamW

optimizer = AdamW(model.parameters(), lr=5e-5)

Enfin, le planificateur du taux d’apprentissage utilisé par défaut est juste une décroissance linéaire de la valeur maximale (5e-5) à 0. Pour le définir correctement, nous devons connaître le nombre d’étapes d’entraînement que nous prendrons, qui est le nombre d’époques que nous voulons exécuter multiplié par le nombre de batch d’entraînement (qui est la longueur de notre dataloader d’entraînement). Le Trainer utilise trois époques par défaut, nous allons donc suivre ça :

from transformers import get_scheduler

num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)
print(num_training_steps)
1377

La boucle d’entraînement

Une dernière chose : nous voulons utiliser le GPU si nous en avons un (sur un CPU, l’entraînement peut prendre plusieurs heures au lieu de quelques minutes). Pour ce faire, nous définissons un device sur lequel nous allons placer notre modèle et nos batchs :

import torch

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
device
device(type='cuda')

Nous sommes maintenant prêts à entraîner ! Pour avoir une idée du moment où l’entraînement sera terminé, nous ajoutons une barre de progression sur le nombre d’étapes d’entraînement, en utilisant la bibliothèque tqdm :

from tqdm.auto import tqdm

progress_bar = tqdm(range(num_training_steps))

model.train()
for epoch in range(num_epochs):
    for batch in train_dataloader:
        batch = {k: v.to(device) for k, v in batch.items()}
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

Vous pouvez voir que le cœur de la boucle d’entraînement ressemble beaucoup à celui de l’introduction. Nous n’avons pas demandé de rapport, donc cette boucle d’entraînement ne nous dira rien sur les résultats du modèle. Pour cela, nous devons ajouter une boucle d’évaluation.

La boucle d’évaluation

Comme nous l’avons fait précédemment, nous allons utiliser une métrique fournie par la bibliothèque 🤗 Evaluate. Nous avons déjà vu la méthode metric.compute(), mais les métriques peuvent en fait accumuler des batchs pour nous au fur et à mesure que nous parcourons la boucle de prédiction avec la méthode add_batch(). Une fois que nous avons accumulé tous les batchs, nous pouvons obtenir le résultat final avec metric.compute(). Voici comment implémenter tout cela dans une boucle d’évaluation :

import evaluate

metric = evaluate.load("glue", "mrpc")
model.eval()
for batch in eval_dataloader:
    batch = {k: v.to(device) for k, v in batch.items()}
    with torch.no_grad():
        outputs = model(**batch)

    logits = outputs.logits
    predictions = torch.argmax(logits, dim=-1)
    metric.add_batch(predictions=predictions, references=batch["labels"])

metric.compute()
{'accuracy': 0.8431372549019608, 'f1': 0.8907849829351535}

Une fois encore, vos résultats seront légèrement différents en raison du caractère aléatoire de l’initialisation de la tête du modèle et du mélange des données, mais ils devraient se situer dans la même fourchette.

✏️ Essayez Modifiez la boucle d’entraînement précédente pour finetuner votre modèle sur le jeu de données SST-2.

Optimisez votre boucle d’entraînement avec 🤗 <i> Accelerate </i>

La boucle d’entraînement que nous avons définie précédemment fonctionne bien sur un seul CPU ou GPU. Mais en utilisant la bibliothèque 🤗 Accelerate, il suffit de quelques ajustements pour permettre un entraînement distribué sur plusieurs GPUs ou TPUs. En partant de la création des dataloaders d’entraînement et de validation, voici à quoi ressemble notre boucle d’entraînement manuel :

from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)

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

num_epochs = 3
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

progress_bar = tqdm(range(num_training_steps))

model.train()
for epoch in range(num_epochs):
    for batch in train_dataloader:
        batch = {k: v.to(device) for k, v in batch.items()}
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

Et voici les changements :

+ from accelerate import Accelerator
  from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler

+ accelerator = Accelerator()

  model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
  optimizer = AdamW(model.parameters(), lr=3e-5)

- device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
- model.to(device)

+ train_dataloader, eval_dataloader, model, optimizer = accelerator.prepare(
+     train_dataloader, eval_dataloader, model, optimizer
+ )

  num_epochs = 3
  num_training_steps = num_epochs * len(train_dataloader)
  lr_scheduler = get_scheduler(
      "linear",
      optimizer=optimizer,
      num_warmup_steps=0,
      num_training_steps=num_training_steps
  )

  progress_bar = tqdm(range(num_training_steps))

  model.train()
  for epoch in range(num_epochs):
      for batch in train_dataloader:
-         batch = {k: v.to(device) for k, v in batch.items()}
          outputs = model(**batch)
          loss = outputs.loss
-         loss.backward()
+         accelerator.backward(loss)

          optimizer.step()
          lr_scheduler.step()
          optimizer.zero_grad()
          progress_bar.update(1)

La première ligne à ajouter est la ligne d’importation. La deuxième ligne instancie un objet Accelerator qui va regarder l’environnement et initialiser la bonne configuration distribuée. 🤗 Accelerate gère le placement des périphériques pour vous, donc vous pouvez enlever les lignes qui placent le modèle sur le périphérique (ou, si vous préférez, les changer pour utiliser accelerator.device au lieu de device).

Ensuite, le gros du travail est fait dans la ligne qui envoie les dataloaders, le modèle, et l’optimiseur à accelerator.prepare(). Cela va envelopper ces objets dans le conteneur approprié pour s’assurer que votre entraînement distribué fonctionne comme prévu. Les changements restants à faire sont la suppression de la ligne qui met le batch sur le device (encore une fois, si vous voulez le garder, vous pouvez juste le changer pour utiliser accelerator.device) et le remplacement de loss.backward() par accelerator.backward(loss).

⚠️ Afin de bénéficier de la rapidité offerte par les TPUs du Cloud, nous vous recommandons de rembourrer vos échantillons à une longueur fixe avec les arguments `padding="max_length"` et `max_length` du tokenizer.

Si vous souhaitez faire un copier-coller pour jouer, voici à quoi ressemble la boucle d’entraînement complète avec 🤗 Accelerate :

from accelerate import Accelerator
from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler

accelerator = Accelerator()

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)

train_dl, eval_dl, model, optimizer = accelerator.prepare(
    train_dataloader, eval_dataloader, model, optimizer
)

num_epochs = 3
num_training_steps = num_epochs * len(train_dl)
lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

progress_bar = tqdm(range(num_training_steps))

model.train()
for epoch in range(num_epochs):
    for batch in train_dl:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

En plaçant ceci dans un script train.py, cela sera exécutable sur n’importe quel type d’installation distribuée. Pour l’essayer dans votre installation distribuée, exécutez la commande :

accelerate config

qui vous demandera de répondre à quelques questions et enregistrera vos réponses dans un fichier de configuration utilisé par cette commande :

accelerate launch train.py

qui lancera l’entraînement distribué.

Si vous voulez essayer ceci dans un notebook (par exemple, pour le tester avec des TPUs sur Colab), collez simplement le code dans une training_function() et lancez une dernière cellule avec :

from accelerate import notebook_launcher

notebook_launcher(training_function)

Vous trouverez d’autres exemples dans le dépôt d’🤗 Accelerate.