<i> Finetuner </i> un modèle avec l’API Trainer
La bibliothèque 🤗 Transformers fournit une classe Trainer
pour vous aider à finetuner n’importe lequel des modèles pré-entraînés qu’elle met à disposition sur votre jeu de données. Une fois que vous avez fait tout le travail de prétraitement des données dans la dernière section, il ne vous reste que quelques étapes pour définir le Trainer
. La partie la plus difficile sera probablement de préparer l’environnement pour exécuter Trainer.train()
, car elle fonctionnera très lentement sur un CPU. Si vous n’avez pas de GPU, vous pouvez avoir accès à des GPUs ou TPUs gratuits sur Google Colab.
Les exemples de code ci-dessous supposent que vous avez déjà exécuté les exemples de la section précédente. Voici un bref résumé de ce dont vous avez 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)
Entraînement
La première étape avant de pouvoir définir notre Trainer
est de définir une classe TrainingArguments
qui contiendra tous les hyperparamètres que le Trainer
utilisera pour l’entraînement et l’évaluation. Le seul argument que vous devez fournir est un répertoire où le modèle entraîné sera sauvegardé, ainsi que les checkpoints. Pour tout le reste, vous pouvez laisser les valeurs par défaut, qui devraient fonctionner assez bien pour un finetuning de base.
from transformers import TrainingArguments
training_args = TrainingArguments("test-trainer")
💡 Si vous voulez télécharger automatiquement votre modèle sur le Hub pendant l’entraînement, passez push_to_hub=True
dans le TrainingArguments
. Nous en apprendrons plus à ce sujet au chapitre 4.
La deuxième étape consiste à définir notre modèle. Comme dans le chapitre précédent, nous utiliserons la classe AutoModelForSequenceClassification
, avec deux labels :
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
Vous remarquerez que contrairement au chapitre 2, vous obtenez un message d’avertissement après l’instanciation de ce modèle pré-entraîné. C’est parce que BERT n’a pas été pré-entraîné à la classification de paires de phrases, donc la tête du modèle pré-entraîné a été supprimée et une nouvelle tête adaptée à la classification de séquences a été ajoutée à la place. Les messages d’avertissement indiquent que certains poids n’ont pas été utilisés (ceux correspondant à la tête de pré-entraînement abandonnée) et que d’autres ont été initialisés de manière aléatoire (ceux pour la nouvelle tête). Il conclut en vous encourageant à entraîner le modèle, ce qui est exactement ce que nous allons faire maintenant.
Une fois que nous avons notre modèle, nous pouvons définir un Trainer
en lui passant tous les objets construits jusqu’à présent : le model
, le training_args
, les jeux de données d’entraînement et de validation, notre data_collator
, et notre tokenizer
:
from transformers import Trainer
trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
)
Notez que lorsque vous passez le tokenizer
comme nous l’avons fait ici, le data_collator
par défaut utilisé par le Trainer
sera un DataCollatorWithPadding
comme défini précédemment. Ainsi, vous pouvez sauter la ligne data_collator=data_collator
dans cet appel. Il était quand même important de vous montrer cette partie du traitement dans la section 2 !
Pour finetuner le modèle sur notre jeu de données, il suffit d’appeler la méthode train()
de notre Trainer
:
trainer.train()
Cela lancera le finetuning (qui devrait prendre quelques minutes sur un GPU) et indiquera la perte d’entraînement tous les 500 pas. Cependant, elle ne vous dira pas si votre modèle fonctionne bien (ou mal). Ceci est dû au fait que :
- nous n’avons pas dit au
Trainer
d’évaluer pendant l’entraînement en réglantevaluation_strategy
à soit"steps"
(évaluer chaqueeval_steps
) ou"epoch"
(évaluer à la fin de chaque epoch). - nous n’avons pas fourni au
Trainer
une fonctioncompute_metrics()
pour calculer une métrique pendant ladite évaluation (sinon l’évaluation aurait juste affiché la perte, qui n’est pas un nombre très intuitif).
Evaluation
Voyons comment nous pouvons construire une fonction compute_metrics()
utile et l’utiliser la prochaine fois que nous entraînons. La fonction doit prendre un objet EvalPrediction
(qui est un tuple nommé avec un champ predictions
et un champ label_ids
) et retournera un dictionnaire de chaînes de caractères vers des flottants (les chaînes de caractères étant les noms des métriques retournées, et les flottants leurs valeurs). Pour obtenir des prédictions de notre modèle, nous pouvons utiliser la commande Trainer.predict()
:
predictions = trainer.predict(tokenized_datasets["validation"])
print(predictions.predictions.shape, predictions.label_ids.shape)
(408, 2) (408,)
La sortie de la méthode predict()
est un autre tuple nommé avec trois champs : predictions
, label_ids
, et metrics
. Le champ metrics
contiendra juste la perte sur le jeu de données passé, ainsi que quelques mesures de temps (combien de temps il a fallu pour prédire, au total et en moyenne). Une fois que nous aurons complété notre fonction compute_metrics()
et que nous l’aurons passé au Trainer
, ce champ contiendra également les métriques retournées par compute_metrics()
.
Comme vous pouvez le voir, predictions
est un tableau bidimensionnel de forme 408 x 2 (408 étant le nombre d’éléments dans le jeu de données que nous avons utilisé). Ce sont les logits pour chaque élément du jeu de données que nous avons passé à predict()
(comme vous l’avez vu dans le chapitre précédent, tous les transformers retournent des logits). Pour les transformer en prédictions que nous pouvons comparer à nos étiquettes, nous devons prendre l’indice avec la valeur maximale sur le second axe :
import numpy as np
preds = np.argmax(predictions.predictions, axis=-1)
Nous pouvons maintenant comparer ces preds
aux étiquettes. Pour construire notre fonction compute_metric()
, nous allons nous appuyer sur les métriques de la bibliothèque 🤗 Evaluate. Nous pouvons charger les métriques associées au jeu de données MRPC aussi facilement que nous avons chargé le jeu de données, cette fois avec la fonction evaluate.load()
. L’objet retourné possède une méthode compute()
que nous pouvons utiliser pour effectuer le calcul de la métrique :
import evaluate
metric = evaluate.load("glue", "mrpc")
metric.compute(predictions=preds, references=predictions.label_ids)
{'accuracy': 0.8578431372549019, 'f1': 0.8996539792387542}
Les résultats exacts que vous obtiendrez peuvent varier, car l’initialisation aléatoire de la tête du modèle peut modifier les métriques obtenues. Ici, nous pouvons voir que notre modèle a une précision de 85,78% sur l’ensemble de validation et un score F1 de 89,97. Ce sont les deux métriques utilisées pour évaluer les résultats sur le jeu de données MRPC pour le benchmark GLUE. Le tableau du papier de BERT indique un score F1 de 88,9 pour le modèle de base. Il s’agissait du modèle uncased
alors que nous utilisons actuellement le modèle cased
, ce qui explique le meilleur résultat.
En regroupant le tout, nous obtenons notre fonction compute_metrics()
:
def compute_metrics(eval_preds):
metric = evaluate.load("glue", "mrpc")
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
Et pour le voir utilisé en action pour rapporter les métriques à la fin de chaque époque, voici comment nous définissons un nouveau Trainer
avec cette fonction compute_metrics()
:
training_args = TrainingArguments("test-trainer", evaluation_strategy="epoch")
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)
Notez que nous créons un nouveau TrainingArguments
avec sa evaluation_strategy
définie sur "epoch"
et un nouveau modèle. Sinon, nous ne ferions que continuer l’entraînement du modèle que nous avons déjà entraîné. Pour lancer un nouveau cycle d’entraînement, nous exécutons :
trainer.train()
Cette fois, il indiquera la perte et les mesures de validation à la fin de chaque époque, en plus de la perte d’entraînement. Encore une fois, le score exact de précision/F1 que vous atteignez peut être un peu différent de ce que nous avons trouvé, en raison de l’initialisation aléatoire de la tête du modèle, mais il devrait être dans la même fourchette.
Le Trainer
fonctionnera sur plusieurs GPUs ou TPUs et fournit beaucoup d’options, comme l’entraînement en précision mixte (utilisez fp16 = True
dans vos arguments d’entraînement). Nous passerons en revue tout ce qu’il supporte dans le chapitre 10.
Ceci conclut l’introduction au fine-tuning en utilisant l’API Trainer
. Un exemple d’utilisation pour les tâches de NLP les plus communes es donné dans le chapitre 7, mais pour l’instant regardons comment faire la même chose en PyTorch pur.
✏️ Essayez ! Finetunez un modèle sur le jeu de données GLUE SST-2, en utilisant le traitement des données que vous avez fait dans la section 2.