Traduction
Plongeons maintenant dans la traduction. Il s’agit d’une autre tâche de séquence à séquence, ce qui signifie que c’est un problème qui peut être formulé comme le passage d’une séquence à une autre. En ce sens, le problème est assez proche de la tâche de résumé et vous pouvez adapter ce que nous allons voir ici à d’autres problèmes de séquence à séquence tels que :
- Le transfert de style ? c’est-à-dire créer un modèle qui traduit des textes écrits dans un certain style vers un autre (par exemple, du formel au décontracté ou de l’anglais shakespearien à l’anglais moderne).
- La génération de réponse à des questions c’est-à-dire créer un modèle qui génère des réponses à des questions compte tenu d’un contexte.
Si vous disposez d’un corpus de textes suffisamment important en deux langues différentes (ou plus), vous pouvez entraîner un nouveau modèle de traduction à partir de zéro, comme nous le ferons dans la section sur la modélisation causale du langage. Il est toutefois plus rapide de finetuner un modèle de traduction existant, qu’il s’agisse d’un modèle multilingue comme mT5 ou mBART que vous souhaitez adapter à une paire de langues spécifique, ou même d’un modèle spécialisé dans la traduction d’une langue vers une autre que vous souhaitez adapter à votre corpus spécifique.
Dans cette section, nous allons finetuner un modèle Marian pré-entraîné pour traduire de l’anglais au français (puisque de nombreux employés de Hugging Face parlent ces deux langues) sur le jeu de données KDE4 qui est un jeu de données de fichiers localisés pour les applications KDE. Le modèle que nous utiliserons a été pré-entraîné sur un large corpus de textes français et anglais provenant du jeu de données Opus qui contient en fait le jeu de données KDE4. A noter que même si le modèle pré-entraîné que nous utilisons a vu ces données pendant son pré-entraînement, nous verrons que nous pouvons obtenir une meilleure version de ce modèle après un finetuning.
Une fois que nous aurons terminé, nous aurons un modèle capable de faire des prédictions comme celle-ci :
Comme dans les sections précédentes, vous pouvez trouver, télécharger et vérifier les précisions de ce modèle sur le Hub.
Préparation des données
Pour finetuner ou entraîner un modèle de traduction à partir de zéro, nous avons besoin d’un jeu de données adapté à cette tâche. Comme mentionné précédemment, nous utiliserons le jeu de données KDE4 dans cette section. Notez que vous pouvez adapter assez facilement le code pour utiliser vos propres données du moment que vous disposez de paires de phrases dans les deux langues que vous voulez traduire. Reportez-vous au chapitre 5 si vous avez besoin d’un rappel sur la façon de charger vos données personnalisées dans un Dataset
.
Le jeu de données KDE4
Comme d’habitude, nous téléchargeons notre jeu de données en utilisant la fonction load_dataset()
:
from datasets import load_dataset
raw_datasets = load_dataset("kde4", lang1="en", lang2="fr")
Si vous souhaitez travailler avec une autre paire de langues, 92 langues sont disponibles au total pour ce jeu de données. Vous pouvez les voir dans la carte du jeu de données.
Jetons un coup d’œil au jeu de données :
raw_datasets
DatasetDict({
train: Dataset({
features: ['id', 'translation'],
num_rows: 210173
})
})
Nous avons 210 173 paires de phrases. Cependant regroupées dans un seul échantillon. Nous devrons donc créer notre propre jeu de validation. Comme nous l’avons vu dans le chapitre 5, un Dataset
possède une méthode train_test_split()
qui peut nous aider. Nous allons fournir une graine pour la reproductibilité :
split_datasets = raw_datasets["train"].train_test_split(train_size=0.9, seed=20)
split_datasets
DatasetDict({
train: Dataset({
features: ['id', 'translation'],
num_rows: 189155
})
test: Dataset({
features: ['id', 'translation'],
num_rows: 21018
})
})
Nous pouvons renommer la clé test
en validation
comme ceci :
split_datasets["validation"] = split_datasets.pop("test")
Examinons maintenant un élément de ce jeu de données :
split_datasets["train"][1]["translation"]
{'en': 'Default to expanded threads',
'fr': 'Par défaut, développer les fils de discussion'}
Nous obtenons un dictionnaire contenant deux phrases dans la paire de langues qui nous intéresse. Une particularité de ce jeu de données rempli de termes techniques informatiques est qu’ils sont tous entièrement traduits en français. Cependant, les ingénieurs français laissent la plupart des mots spécifiques à l’informatique en anglais lorsqu’ils parlent. Ici, par exemple, le mot « threads » pourrait très bien apparaître dans une phrase française, surtout dans une conversation technique. Mais dans ce jeu de données, il a été traduit en « fils de discussion ». Le modèle pré-entraîné que nous utilisons (qui a été pré-entraîné sur un plus grand corpus de phrases françaises et anglaises) prend l’option de laisser le mot tel quel :
from transformers import pipeline
model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
translator = pipeline("translation", model=model_checkpoint)
translator("Default to expanded threads")
[{'translation_text': 'Par défaut pour les threads élargis'}]
Un autre exemple de ce comportement peut être observé avec le mot « plugin » qui n’est pas officiellement un mot français mais que la plupart des francophones comprendront et ne prendront pas la peine de traduire. Dans le jeu de données KDE4, ce mot a été traduit en français par le plus officiel « module d’extension » :
split_datasets["train"][172]["translation"]
{'en': 'Unable to import %1 using the OFX importer plugin. This file is not the correct format.',
'fr': "Impossible d'importer %1 en utilisant le module d'extension d'importation OFX. Ce fichier n'a pas un format correct."}
Notre modèle pré-entraîné, lui, s’en tient au mot anglais :
translator(
"Unable to import %1 using the OFX importer plugin. This file is not the correct format."
)
[{'translation_text': "Impossible d'importer %1 en utilisant le plugin d'importateur OFX. Ce fichier n'est pas le bon format."}]
Il sera intéressant de voir si notre modèle finetuné tient compte de ces particularités (alerte spoiler : il le fera).
✏️ A votre tour ! Un autre mot anglais souvent utilisé en français est « email ». Trouvez le premier échantillon dans l’échantillon d’entraînement qui utilise ce mot. Comment est-il traduit ? Comment le modèle pré-entraîné traduit-il cette même phrase ?
Traitement des données
Vous devriez maintenant connaître le principe : les textes doivent tous être convertis en ensembles d’ID de tokens pour que le modèle puisse leur donner un sens. Pour cette tâche, nous aurons besoin de tokeniser les entrées et les cibles. Notre première tâche est de créer notre objet tokenizer
. Comme indiqué précédemment, nous utiliserons un modèle pré-entraîné Marian English to French. Si vous essayez ce code avec une autre paire de langues, assurez-vous d’adapter le checkpoint du modèle. L’organisation Helsinki-NLP fournit plus de mille modèles dans plusieurs langues.
from transformers import AutoTokenizer
model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, return_tensors="tf")
Vous pouvez remplacer le model_checkpoint
par un tout autre modèle disponible sur le Hub qui aurait votre préférence, ou par un dossier en local où vous avez sauvegardé un modèle pré-entraîné et un tokenizer.
💡 Si vous utilisez un tokenizer multilingue tel que mBART, mBART-50 ou M2M100, vous devrez définir les codes de langue de vos entrées et cibles dans le tokenizer en définissant tokenizer.src_lang
et tokenizer.tgt_lang
aux bonnes valeurs.
La préparation de nos données est assez simple. Il y a juste une chose à retenir : vous traitez les entrées comme d’habitude, mais pour les cibles, vous devez envelopper le tokenizer dans le gestionnaire de contexte as_target_tokenizer()
.
Un gestionnaire de contexte en Python est introduit avec l’instruction with
et est utile lorsque vous avez deux opérations liées à exécuter en paire. L’exemple le plus courant est lorsque vous écrivez ou lisez un fichier, ce qui est souvent fait dans une instruction comme :
with open(file_path) as f:
content = f.read()
Ici, les deux opérations connexes qui sont exécutées en paire sont les actions d’ouverture et de fermeture du fichier. L’objet correspondant au fichier ouvert f
n’existe qu’à l’intérieur du bloc indenté sous le with
. L’ouverture se produit avant ce bloc et la fermeture à la fin du bloc.
Dans le cas présent, le gestionnaire de contexte as_target_tokenizer()
va définir le tokenizer dans la langue de sortie (ici, le français) avant l’exécution du bloc indenté, puis le redéfinir dans la langue d’entrée (ici, l’anglais).
Ainsi, le prétraitement d’un échantillon ressemble à ceci :
en_sentence = split_datasets["train"][1]["translation"]["en"]
fr_sentence = split_datasets["train"][1]["translation"]["fr"]
inputs = tokenizer(en_sentence)
with tokenizer.as_target_tokenizer():
targets = tokenizer(fr_sentence)
Si nous oublions de tokeniser les cibles dans le gestionnaire de contexte, elles seront tokenisées par le tokenizer d’entrée, ce qui dans le cas d’un modèle Marian, ne va pas du tout bien se passer :
wrong_targets = tokenizer(fr_sentence)
print(tokenizer.convert_ids_to_tokens(wrong_targets["input_ids"]))
print(tokenizer.convert_ids_to_tokens(targets["input_ids"]))
['▁Par', '▁dé', 'f', 'aut', ',', '▁dé', 've', 'lop', 'per', '▁les', '▁fil', 's', '▁de', '▁discussion', '</s>']
['▁Par', '▁défaut', ',', '▁développer', '▁les', '▁fils', '▁de', '▁discussion', '</s>']
Comme on peut le voir, utiliser le tokenizer anglais pour prétraiter une phrase française donne un batch de tokens plus important, puisque le tokenizer ne connaît aucun mot français (sauf ceux qui apparaissent aussi en anglais, comme « discussion »).
Les inputs
et les targets
sont des dictionnaires avec nos clés habituelles (identifiants d’entrée, masque d’attention, etc.). La dernière étape est de définir une clé "labels"
dans les entrées. Nous faisons cela dans la fonction de prétraitement que nous allons appliquer sur les jeux de données :
max_input_length = 128
max_target_length = 128
def preprocess_function(examples):
inputs = [ex["en"] for ex in examples["translation"]]
targets = [ex["fr"] for ex in examples["translation"]]
model_inputs = tokenizer(inputs, max_length=max_input_length, truncation=True)
# Configurer le tokenizer pour les cibles.
with tokenizer.as_target_tokenizer():
labels = tokenizer(targets, max_length=max_target_length, truncation=True)
model_inputs["labels"] = labels["input_ids"]
return model_inputs
Notez que nous avons fixé des longueurs maximales similaires pour nos entrées et nos sorties. Comme les textes que nous traitons semblent assez courts, nous utilisons 128.
💡 Si vous utilisez un modèle T5 (plus précisément, un des checkpoints t5-xxx
), le modèle s’attendra à ce que les entrées aient un préfixe indiquant la tâche à accomplir, comme translate: English to French:
.
⚠️ Nous ne faisons pas attention au masque d’attention des cibles car le modèle ne s’y attend pas. Au lieu de cela, les étiquettes correspondant à un token de padding doivent être mises à -100
afin qu’elles soient ignorées dans le calcul de la perte. Cela sera fait par notre assembleur de données plus tard puisque nous appliquons le padding dynamique, mais si vous utilisez le padding ici, vous devriez adapter la fonction de prétraitement pour mettre toutes les étiquettes qui correspondent au token de padding à -100
.
Nous pouvons maintenant appliquer ce prétraitement en une seule fois sur toutes les échantillons de notre jeu de données :
tokenized_datasets = split_datasets.map(
preprocess_function,
batched=True,
remove_columns=split_datasets["train"].column_names,
)
Maintenant que les données ont été prétraitées, nous sommes prêts à finetuner notre modèle pré-entraîné !
<i> Finetuner </i> le modèle avec l’API Trainer
Le code actuel utilisant Trainer
sera le même que précédemment, avec juste un petit changement : nous utilisons ici Seq2SeqTrainer
qui est une sous-classe de Trainer
qui nous permet de traiter correctement l’évaluation, en utilisant la méthode generate()
pour prédire les sorties à partir des entrées. Nous y reviendrons plus en détail lorsque nous parlerons du calcul de la métrique.
Tout d’abord, nous avons besoin d’un modèle à finetuner. Nous allons utiliser l’API habituelle AutoModel
:
from transformers import AutoModelForSeq2SeqLM
model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)
Notez que cette fois-ci, nous utilisons un modèle qui a été entraîné sur une tâche de traduction et qui peut déjà être utilisé, donc il n’y a pas d’avertissement concernant les poids manquants ou ceux nouvellement initialisés.
Assemblage des données
Nous aurons besoin d’un assembleur de données pour gérer le rembourrage pour la mise en batchs dynamique. Ici, nous ne pouvons pas simplement utiliser un DataCollatorWithPadding
comme dans le chapitre 3 car cela ne rembourre que les entrées (identifiants d’entrée, masque d’attention, et token de type identifiants). Nos étiquettes doivent également être rembourrées à la longueur maximale rencontrée dans les étiquettes. Et, comme mentionné précédemment, la valeur de remplissage utilisée pour remplir les étiquettes doit être -100
et non le token de padding du tokenizer afin de s’assurer que ces valeurs soient ignorées dans le calcul de la perte.
Tout ceci est réalisé par un DataCollatorForSeq2Seq
. Comme le DataCollatorWithPadding
, il prend le tokenizer
utilisé pour prétraiter les entrées, mais également le model
. C’est parce que cet assembleur de données est également responsable de la préparation des identifiants d’entrée du décodeur, qui sont des versions décalées des étiquettes avec un token spécial au début. Comme ce décalage est effectué de manière légèrement différente selon les architectures, le DataCollatorForSeq2Seq
a besoin de connaître l’objet model
:
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)
Pour le tester sur quelques échantillons, nous l’appelons simplement sur une liste d’exemples de notre échantillon d’entrainement tokénisé :
batch = data_collator([tokenized_datasets["train"][i] for i in range(1, 3)])
batch.keys()
dict_keys(['attention_mask', 'input_ids', 'labels', 'decoder_input_ids'])
Nous pouvons vérifier que nos étiquettes ont été rembourrées à la longueur maximale du batch, en utilisant -100
:
batch["labels"]
tensor([[ 577, 5891, 2, 3184, 16, 2542, 5, 1710, 0, -100,
-100, -100, -100, -100, -100, -100],
[ 1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, 817,
550, 7032, 5821, 7907, 12649, 0]])
Nous pouvons aussi jeter un coup d’œil aux identifiants d’entrée du décodeur, pour voir qu’il s’agit de versions décalées des étiquettes :
batch["decoder_input_ids"]
tensor([[59513, 577, 5891, 2, 3184, 16, 2542, 5, 1710, 0,
59513, 59513, 59513, 59513, 59513, 59513],
[59513, 1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124,
817, 550, 7032, 5821, 7907, 12649]])
Voici les étiquettes des premier et deuxième éléments de notre jeu de données :
for i in range(1, 3):
print(tokenized_datasets["train"][i]["labels"])
[577, 5891, 2, 3184, 16, 2542, 5, 1710, 0]
[1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, 817, 550, 7032, 5821, 7907, 12649, 0]
Nous allons transmettre ce data_collator
au Seq2SeqTrainer
. Ensuite, jetons un coup d’oeil à la métrique.
Métriques
La fonctionnalité que Seq2SeqTrainer
ajoute à sa superclasse Trainer
est la possibilité d’utiliser la méthode generate()
pendant l’évaluation ou la prédiction. Pendant l’entraînement, le modèle utilisera les decoder_input_ids
avec un masque d’attention assurant qu’il n’utilise pas les tokens après le token qu’il essaie de prédire, pour accélérer l’entraînement. Pendant l’inférence, nous ne pourrons pas les utiliser puisque nous n’aurons pas d’étiquettes. Ainsi c’est une bonne idée d’évaluer notre modèle avec la même configuration.
Comme nous l’avons vu dans le chapitre 1, le décodeur effectue l’inférence en prédisant les tokens un par un. C’est quelque chose qui est implémenté en coulisses dans 🤗 Transformers par la méthode generate()
. Le Seq2SeqTrainer
nous laissera utiliser cette méthode pour l’évaluation si nous indiquons predict_with_generate=True
.
La métrique traditionnelle utilisée pour la traduction est le score BLEU, introduit dans un article de 2002 par Kishore Papineni et al. Le score BLEU évalue dans quelle mesure les traductions sont proches de leurs étiquettes. Il ne mesure pas l’intelligibilité ou l’exactitude grammaticale des résultats générés par le modèle, mais utilise des règles statistiques pour garantir que tous les mots des résultats générés apparaissent également dans les cibles. En outre, il existe des règles qui pénalisent les répétitions des mêmes mots s’ils ne sont pas également répétés dans les cibles (pour éviter que le modèle ne produise des phrases telles que « the the the the the the the ») et les phrases produites qui sont plus courtes que celles des cibles (pour éviter que le modèle ne produise des phrases telles que « the »).
L’une des faiblesses de BLEU est qu’il s’attend à ce que le texte soit déjà tokenisé, ce qui rend difficile la comparaison des scores entre les modèles qui utilisent différents tokenizers. Par conséquent, la mesure la plus couramment utilisée aujourd’hui pour évaluer les modèles de traduction est SacreBLEU qui remédie à cette faiblesse (et à d’autres) en standardisant l’étape de tokenisation. Pour utiliser cette métrique, nous devons d’abord installer la bibliothèque SacreBLEU :
!pip install sacrebleu
Nous pouvons ensuite charger ce score via evaluate.load()
comme nous l’avons fait dans le chapitre 3 :
import evaluate
metric = evaluate.load("sacrebleu")
Cette métrique prend des textes comme entrées et cibles. Elle est conçue pour accepter plusieurs cibles acceptables car il y a souvent plusieurs traductions possibles d’une même phrase. Le jeu de données que nous utilisons n’en fournit qu’une seule, mais en NLP, il n’est pas rare de trouver des jeux de données ayant plusieurs phrases comme étiquettes. Ainsi, les prédictions doivent être une liste de phrases mais les références doivent être une liste de listes de phrases.
Essayons un exemple :
predictions = [
"This plugin lets you translate web pages between several languages automatically."
]
references = [
[
"This plugin allows you to automatically translate web pages between several languages."
]
]
metric.compute(predictions=predictions, references=references)
{'score': 46.750469682990165,
'counts': [11, 6, 4, 3],
'totals': [12, 11, 10, 9],
'precisions': [91.67, 54.54, 40.0, 33.33],
'bp': 0.9200444146293233,
'sys_len': 12,
'ref_len': 13}
Cela donne un score BLEU de 46.75, ce qui est plutôt bon. A titre de comparaison, le Transformer original dans l’article Attention Is All You Need a obtenu un score BLEU de 41.8 sur une tâche de traduction similaire entre l’anglais et le français ! (Pour plus d’informations sur les métriques individuelles, comme counts
et bp
, voir le dépôt SacreBLEU. D’autre part, si nous essayons avec les deux mauvais types de prédictions (répétitions ou prédiction trop courte) qui sortent souvent des modèles de traduction, nous obtiendrons des scores BLEU plutôt mauvais :
predictions = ["This This This This"]
references = [
[
"This plugin allows you to automatically translate web pages between several languages."
]
]
metric.compute(predictions=predictions, references=references)
{'score': 1.683602693167689,
'counts': [1, 0, 0, 0],
'totals': [4, 3, 2, 1],
'precisions': [25.0, 16.67, 12.5, 12.5],
'bp': 0.10539922456186433,
'sys_len': 4,
'ref_len': 13}
predictions = ["This plugin"]
references = [
[
"This plugin allows you to automatically translate web pages between several languages."
]
]
metric.compute(predictions=predictions, references=references)
{'score': 0.0,
'counts': [2, 1, 0, 0],
'totals': [2, 1, 0, 0],
'precisions': [100.0, 100.0, 0.0, 0.0],
'bp': 0.004086771438464067,
'sys_len': 2,
'ref_len': 13}
Le score peut aller de 0 à 100. Plus il est élevé, mieux c’est.
Pour passer des sorties du modèle aux textes utilisables par la métrique, nous allons utiliser la méthode tokenizer.batch_decode()
. Nous devons juste nettoyer tous les -100
dans les étiquettes. Le tokenizer fera automatiquement la même chose pour le token de padding :
import numpy as np
def compute_metrics(eval_preds):
preds, labels = eval_preds
# Dans le cas où le modèle retourne plus que les logits de prédiction
if isinstance(preds, tuple):
preds = preds[0]
decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)
# Remplacer les -100 dans les étiquettes car nous ne pouvons pas les décoder
labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
# Quelques post-traitements simples
decoded_preds = [pred.strip() for pred in decoded_preds]
decoded_labels = [[label.strip()] for label in decoded_labels]
result = metric.compute(predictions=decoded_preds, references=decoded_labels)
return {"bleu": result["score"]}
Maintenant que c’est fait, nous sommes prêts à finetuner notre modèle !
<i> Finetuner </i> le modèle
La première étape consiste à se connecter à Hugging Face, afin de pouvoir télécharger vos résultats sur le Hub. Il y a une fonction pratique pour vous aider à le faire dans un notebook :
from huggingface_hub import notebook_login
notebook_login()
Cela affichera un widget où vous pourrez entrer vos identifiants de connexion à Hugging Face.
Si vous ne travaillez pas dans un notebook, tapez simplement la ligne suivante dans votre terminal :
huggingface-cli login
Une fois ceci fait, nous pouvons définir notre Seq2SeqTrainingArguments
. Comme pour le Trainer
, nous utilisons une sous-classe de TrainingArguments
qui contient quelques champs supplémentaires :
from transformers import Seq2SeqTrainingArguments
args = Seq2SeqTrainingArguments(
f"marian-finetuned-kde4-en-to-fr",
evaluation_strategy="no",
save_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=32,
per_device_eval_batch_size=64,
weight_decay=0.01,
save_total_limit=3,
num_train_epochs=3,
predict_with_generate=True,
fp16=True,
push_to_hub=True,
)
En dehors des hyperparamètres habituels (comme le taux d’apprentissage, le nombre d’époques, la taille des batchs et une le taux de décroissance des poids), voici quelques changements par rapport à ce que nous avons vu dans les sections précédentes :
- Nous ne définissons pas d’évaluation car elle prend du temps. Nous allons juste évaluer une fois notre modèle avant l’entraînement et après.
- Nous avons mis
fp16=True
, ce qui accélère l’entraînement sur les GPUs modernes. - Nous définissons
predict_with_generate=True
, comme discuté ci-dessus. - Nous utilisons
push_to_hub=True
pour télécharger le modèle sur le Hub à la fin de chaque époque.
Notez que vous pouvez spécifier le nom complet du dépôt vers lequel vous voulez pousser avec l’argument hub_model_id
(en particulier, vous devrez utiliser cet argument pour pousser vers une organisation). Par exemple, lorsque nous avons poussé le modèle vers l’organisation huggingface-course
, nous avons ajouté hub_model_id="huggingface-course/marian-finetuned-kde4-en-to-fr"
à Seq2SeqTrainingArguments
. Par défaut, le dépôt utilisé sera dans votre espace et nommé d’après le répertoire de sortie que vous avez défini. Dans notre cas ce sera "sgugger/marian-finetuned-kde4-en-to-fr"
(qui est le modèle que nous avons lié au début de cette section).
💡 Si le répertoire de sortie que vous utilisez existe déjà, il doit être un clone local du dépôt vers lequel vous voulez pousser. S’il ne l’est pas, vous obtiendrez une erreur lors de la définition de votre Seq2SeqTrainer
et devrez définir un nouveau nom.
Enfin, nous passons tout au Seq2SeqTrainer
:
from transformers import Seq2SeqTrainer
trainer = Seq2SeqTrainer(
model,
args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)
Avant d’entraîner, nous allons d’abord regarder le score obtenu par notre modèle, pour vérifier que nous n’aggravons pas les choses avec notre finetuning. Cette commande va prendre un peu de temps, vous pouvez donc prendre un café pendant qu’elle s’exécute :
trainer.evaluate(max_length=max_target_length)
{'eval_loss': 1.6964408159255981,
'eval_bleu': 39.26865061007616,
'eval_runtime': 965.8884,
'eval_samples_per_second': 21.76,
'eval_steps_per_second': 0.341}
Un score BLEU de 39 n’est pas trop mauvais, ce qui reflète le fait que notre modèle est déjà bon pour traduire des phrases anglaises en phrases françaises.
Vient ensuite l’entraînement, qui prendra également un peu de temps :
trainer.train()
Notez que pendant l’entraînement, chaque fois que le modèle est sauvegardé (ici, à chaque époque), il est téléchargé sur le Hub en arrière-plan. De cette façon, vous serez en mesure de reprendre votre entraînement sur une autre machine si nécessaire.
Une fois l’entraînement terminé, nous évaluons à nouveau notre modèle. Avec un peu de chance, nous verrons une amélioration du score BLEU !
trainer.evaluate(max_length=max_target_length)
{'eval_loss': 0.8558505773544312,
'eval_bleu': 52.94161337775576,
'eval_runtime': 714.2576,
'eval_samples_per_second': 29.426,
'eval_steps_per_second': 0.461,
'epoch': 3.0}
C’est une amélioration de près de 14 points, ce qui est formidable.
Enfin, nous utilisons la méthode push_to_hub()
pour nous assurer que nous téléchargeons la dernière version du modèle. Trainer
rédige également une carte de modèle avec tous les résultats de l’évaluation et la télécharge. Cette carte de modèle contient des métadonnées qui aident le Hub à choisir le widget pour l’inférence. Habituellement, il n’y a pas besoin de dire quoi que ce soit car il peut inférer le bon widget à partir de la classe du modèle, mais dans ce cas, la même classe de modèle peut être utilisée pour toutes sortes de problèmes de séquence à séquence. Ainsi nous spécifions que c’est un modèle de traduction :
trainer.push_to_hub(tags="translation", commit_message="Training complete")
Cette commande renvoie l’URL du commit qu’elle vient de faire, si vous voulez l’inspecter :
'https://huggingface.co/sgugger/marian-finetuned-kde4-en-to-fr/commit/3601d621e3baae2bc63d3311452535f8f58f6ef3'
À ce stade, vous pouvez utiliser le widget d’inférence sur le Hub pour tester votre modèle et le partager avec vos amis. Vous avez réussi à finetuner un modèle sur une tâche de traduction. Félicitations !
Si vous souhaitez vous plonger un peu plus profondément dans la boucle d’entraînement, nous allons maintenant vous montrer comment faire la même chose en utilisant 🤗 Accelerate.
Une boucle d’entraînement personnalisée
Jetons maintenant un coup d’œil à la boucle d’entraînement complète afin que vous puissiez facilement personnaliser les parties dont vous avez besoin. Elle ressemblera beaucoup à ce que nous avons fait dans la section 2 et dans le chapitre 3.
Préparer le tout pour l’entraînement
Vous avez vu tout cela plusieurs fois maintenant, donc nous allons passer en revue le code assez rapidement. D’abord, nous allons construire le DataLoader
à partir de nos jeux de données, après avoir configuré les jeux de données au format "torch"
pour obtenir les tenseurs PyTorch :
from torch.utils.data import DataLoader
tokenized_datasets.set_format("torch")
train_dataloader = DataLoader(
tokenized_datasets["train"],
shuffle=True,
collate_fn=data_collator,
batch_size=8,
)
eval_dataloader = DataLoader(
tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)
Ensuite, nous réinstantifions notre modèle pour nous assurer que nous ne poursuivons pas le finetuning précédent et que nous repartons du modèle pré-entraîné :
model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)
Nous aurons alors besoin d’un optimiseur :
from transformers import AdamW
optimizer = AdamW(model.parameters(), lr=2e-5)
Une fois que nous avons tous ces objets, nous pouvons les envoyer à la méthode accelerator.prepare()
. Rappelez-vous que si vous voulez entraîner sur des TPUs dans un notebook de Colab, vous devez déplacer tout ce code dans une fonction d’entraînement et ne devrait pas exécuter une cellule qui instancie un Accelerator
.
from accelerate import Accelerator
accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
model, optimizer, train_dataloader, eval_dataloader
)
Maintenant que nous avons envoyé notre train_dataloader
à accelerator.prepare()
, nous pouvons utiliser sa longueur pour calculer le nombre d’étapes d’entraînement. Rappelez-vous que nous devrions toujours faire cela après avoir préparé le chargeur de données car cette méthode va changer la longueur du DataLoader
. Nous utilisons un programme linéaire classique du taux d’apprentissage à 0 :
from transformers import get_scheduler
num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
Enfin, pour pousser notre modèle vers le Hub, nous aurons besoin de créer un objet Repository
dans un dossier de travail. Tout d’abord, connectez-vous au Hub si vous n’êtes pas déjà connecté. Nous déterminerons le nom du dépôt à partir de l’identifiant du modèle que nous voulons donner à notre modèle (n’hésitez pas à remplacer le repo_name
par votre propre choix, il doit juste contenir votre nom d’utilisateur, ce que fait la fonction get_full_repo_name()
) :
from huggingface_hub import Repository, get_full_repo_name
model_name = "marian-finetuned-kde4-en-to-fr-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/marian-finetuned-kde4-en-to-fr-accelerate'
Ensuite, nous pouvons cloner ce dépôt dans un dossier local. S’il existe déjà, ce dossier local doit être un clone du dépôt avec lequel nous travaillons :
output_dir = "marian-finetuned-kde4-en-to-fr-accelerate"
repo = Repository(output_dir, clone_from=repo_name)
Nous pouvons maintenant télécharger tout ce que nous sauvegardons dans output_dir
en appelant la méthode repo.push_to_hub()
. Cela nous aidera à télécharger les modèles intermédiaires à la fin de chaque époque.
Boucle d’entraînement
Nous sommes maintenant prêts à écrire la boucle d’entraînement complète. Pour simplifier sa partie évaluation, nous définissons cette fonction postprocess()
qui prend les prédictions et les étiquettes et les convertit en listes de chaînes de caractères que notre objet metric
attend :
def postprocess(predictions, labels):
predictions = predictions.cpu().numpy()
labels = labels.cpu().numpy()
decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
# Remplace -100 dans les étiquettes car nous ne pouvons pas les décoder
labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
# Quelques post-traitements simples
decoded_preds = [pred.strip() for pred in decoded_preds]
decoded_labels = [[label.strip()] for label in decoded_labels]
return decoded_preds, decoded_labels
La boucle d’entraînement ressemble beaucoup à celles de la section 2 et du chapitre 3, avec quelques différences dans la partie évaluation. Donc concentrons-nous sur cela !
La première chose à noter est que nous utilisons la méthode generate()
pour calculer les prédictions. C’est une méthode sur notre modèle de base et non pas le modèle enveloppé créé dans la méthode prepare()
. C’est pourquoi nous déballons d’abord le modèle, puis nous appelons cette méthode.
La deuxième chose est que, comme avec la classification de token, deux processus peuvent avoir rembourrés les entrées et les étiquettes à des formes différentes. Ainsi nous utilisons accelerator.pad_across_processes()
pour rendre les prédictions et les étiquettes de la même forme avant d’appeler la méthode gather()
. Si nous ne faisons pas cela, l’évaluation va soit se tromper, soit se bloquer pour toujours.
from tqdm.auto import tqdm
import torch
progress_bar = tqdm(range(num_training_steps))
for epoch in range(num_train_epochs):
# Entraînement
model.train()
for batch in train_dataloader:
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
# Evaluation
model.eval()
for batch in tqdm(eval_dataloader):
with torch.no_grad():
generated_tokens = accelerator.unwrap_model(model).generate(
batch["input_ids"],
attention_mask=batch["attention_mask"],
max_length=128,
)
labels = batch["labels"]
# Nécessaire pour rembourrer les prédictions et les étiquettes à rassembler
generated_tokens = accelerator.pad_across_processes(
generated_tokens, dim=1, pad_index=tokenizer.pad_token_id
)
labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)
predictions_gathered = accelerator.gather(generated_tokens)
labels_gathered = accelerator.gather(labels)
decoded_preds, decoded_labels = postprocess(predictions_gathered, labels_gathered)
metric.add_batch(predictions=decoded_preds, references=decoded_labels)
results = metric.compute()
print(f"epoch {epoch}, BLEU score: {results['score']:.2f}")
# Sauvegarder et télécharger
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
if accelerator.is_main_process:
tokenizer.save_pretrained(output_dir)
repo.push_to_hub(
commit_message=f"Training in progress epoch {epoch}", blocking=False
)
epoch 0, BLEU score: 53.47
epoch 1, BLEU score: 54.24
epoch 2, BLEU score: 54.44
Une fois que c’est fait, vous devriez avoir un modèle qui a des résultats assez similaires à celui entraîné avec Seq2SeqTrainer
. Vous pouvez vérifier celui que nous avons entraîné en utilisant ce code sur huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate. Et si vous voulez tester des modifications de la boucle d’entraînement, vous pouvez les mettre en œuvre directement en modifiant le code ci-dessus !
Utilisation du modèle <i> finetuné </i>
Nous vous avons déjà montré comment vous pouvez utiliser le modèle que nous avons finetuné sur le Hub avec le widget d’inférence. Pour l’utiliser localement dans un pipeline
, nous devons juste spécifier l’identifiant de modèle approprié :
from transformers import pipeline
# Remplacez ceci par votre propre checkpoint
model_checkpoint = "huggingface-course/marian-finetuned-kde4-en-to-fr"
translator = pipeline("translation", model=model_checkpoint)
translator("Default to expanded threads")
[{'translation_text': 'Par défaut, développer les fils de discussion'}]
Comme prévu, notre modèle pré-entraîné a adapté ses connaissances au corpus sur lequel nous l’avons finetuné. Et au lieu de laisser le mot anglais « threads », le modèle le traduit maintenant par la version française officielle. Il en va de même pour « plugin » :
translator(
"Unable to import %1 using the OFX importer plugin. This file is not the correct format."
)
[{'translation_text': "Impossible d'importer %1 en utilisant le module externe d'importation OFX. Ce fichier n'est pas le bon format."}]
Un autre excellent exemple d’adaptation au domaine !
✏️ A votre tour ! Que retourne le modèle sur l’échantillon avec le mot « email » que vous avez identifié plus tôt ?