Pytorch
TensorFlow

#
Débogage du pipeline d’entraînement

Vous avez écrit un magnifique script pour entraîner ou finetuner un modèle sur une tâche donnée en suivant consciencieusement les conseils du chapitre 7. Mais lorsque vous lancez la commande `model.fit()`, quelque chose d’horrible se produit : vous obtenez une erreur 😱 ! Ou pire, tout semble aller bien et l’entraînement se déroule sans erreur mais le modèle résultant est mauvais. Dans cette section, nous allons vous montrer ce que vous pouvez faire pour déboguer ce genre de problèmes.

##
Déboguer le pipeline d’entraînement

Le problème lorsque vous rencontrez une erreur dans `trainer.train()` est qu’elle peut provenir de plusieurs sources, car la fonction `Trainer` assemble généralement des batchs de choses. Elle convertit les jeux de données en chargeurs de données donc le problème pourrait être quelque chose d’erroné dans votre jeu de données, ou un problème en essayant de regrouper les éléments des jeux de données ensemble. Ensuite, elle prend un batch de données et le transmet au modèle, le problème peut donc se situer dans le code du modèle. Après cela, elle calcule les gradients et effectue l’étape d’optimisation, le problème peut donc également se situer dans votre optimiseur. Et même si tout se passe bien pendant l’entraînement, quelque chose peut encore mal tourner pendant l’évaluation si votre métrique pose problème.

La meilleure façon de déboguer une erreur qui survient dans `trainer.train()` est de passer manuellement en revue tout le pipeline pour voir où les choses se sont mal passées. L’erreur est alors souvent très facile à résoudre.

Pour le démontrer, nous utiliserons le script suivant qui tente de finetuner un modèle DistilBERT sur le jeu de données MNLI :

    from datasets import load_dataset
    import evaluate
    from transformers import (
        AutoTokenizer,
        AutoModelForSequenceClassification,
        TrainingArguments,
        Trainer,
    )

    raw_datasets = load_dataset("glue", "mnli")

    model_checkpoint = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

    def preprocess_function(examples):
        return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

    tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
    model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)

    args = TrainingArguments(
        f"distilbert-finetuned-mnli",
        evaluation_strategy="epoch",
        save_strategy="epoch",
        learning_rate=2e-5,
        num_train_epochs=3,
        weight_decay=0.01,
    )

    metric = evaluate.load("glue", "mnli")

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        return metric.compute(predictions=predictions, references=labels)

    trainer = Trainer(
        model,
        args,
        train_dataset=raw_datasets["train"],
        eval_dataset=raw_datasets["validation_matched"],
        compute_metrics=compute_metrics,
    )
    trainer.train()

Si vous essayez de l’exécuter, vous serez confronté à une erreur plutôt cryptique :

    'ValueError: You have to specify either input_ids or inputs_embeds'

###
Vérifiez vos données

Cela va sans dire, mais si vos données sont corrompues, le `Trainer` ne sera pas capable de former des batchs et encore moins d’entraîner votre modèle. Donc, tout d’abord, vous devez jeter un coup d’oeil à ce qui se trouve dans votre jeu d’entraînement.

Pour éviter d’innombrables heures passées à essayer de corriger quelque chose qui n’est pas la source du bug, nous vous recommandons d’utiliser `trainer.train_dataset` pour vos vérifications et rien d’autre. Faisons donc cela ici :

    trainer.train_dataset[0]

    {'hypothesis': 'Product and geography are what make cream skimming work. ',
     'idx': 0,
     'label': 1,
     'premise': 'Conceptually cream skimming has two basic dimensions - product and geography.'}

Vous remarquez quelque chose d’anormal ? Ceci, en conjonction avec le message d’erreur sur les `input_ids` manquants, devrait vous faire réaliser que ce sont des textes et non des nombres que le modèle peut comprendre. Ici, l’erreur originale est très trompeuse parce que le `Trainer` enlève automatiquement les colonnes qui ne correspondent pas à la signature du modèle (c’est-à-dire, les arguments attendus par le modèle). Cela signifie qu’ici, tout, sauf les étiquettes, a été éliminé. Il n’y avait donc aucun problème à créer des batchs et à les envoyer ensuite au modèle, qui s’est plaint à son tour de ne pas avoir reçu les bons arguments.

Pourquoi les données n’ont-elles pas été traitées ? Nous avons utilisé la méthode `Dataset.map()` sur les jeux de données pour appliquer le tokenizer sur chaque échantillon. Mais si vous regardez attentivement le code, vous verrez que nous avons fait une erreur en passant les ensembles d’entraînement et d’évaluation au `Trainer`. Au lieu d’utiliser `tokenized_datasets` ici, nous avons utilisé `raw_datasets` 🤦. Alors corrigeons ça !

    from datasets import load_dataset
    import evaluate
    from transformers import (
        AutoTokenizer,
        AutoModelForSequenceClassification,
        TrainingArguments,
        Trainer,
    )

    raw_datasets = load_dataset("glue", "mnli")

    model_checkpoint = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

    def preprocess_function(examples):
        return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

    tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
    model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)

    args = TrainingArguments(
        f"distilbert-finetuned-mnli",
        evaluation_strategy="epoch",
        save_strategy="epoch",
        learning_rate=2e-5,
        num_train_epochs=3,
        weight_decay=0.01,
    )

    metric = evaluate.load("glue", "mnli")

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        return metric.compute(predictions=predictions, references=labels)

    trainer = Trainer(
        model,
        args,
        train_dataset=tokenized_datasets["train"],
        eval_dataset=tokenized_datasets["validation_matched"],
        compute_metrics=compute_metrics,
    )
    trainer.train()

Ce nouveau code donnera maintenant une erreur différente (c’est un progrès !) :

    'ValueError: expected sequence of length 43 at dim 1 (got 37)'

En regardant le traceback, nous pouvons voir que l’erreur se produit dans l’étape de collationnement des données :

    ~/git/transformers/src/transformers/data/data_collator.py in torch_default_data_collator(features)
        105                 batch[k] = torch.stack([f[k] for f in features])
        106             else:
    --> 107                 batch[k] = torch.tensor([f[k] for f in features])
        108
        109     return batch

Donc, nous devrions passer à cela. Mais avant finissons d’inspecter nos données, pour être sûrs à 100% qu’elles sont correctes.

Une chose que vous devriez toujours faire lorsque vous déboguez une session d’entraînement est de jeter un coup d’oeil aux entrées décodées de votre modèle. Nous ne pouvons pas donner un sens aux chiffres que nous lui fournissons directement, nous devons donc examiner ce que ces chiffres représentent. Dans le domaine de la vision par ordinateur cela signifie regarder les images décodées des pixels que vous passez, dans le domaine de la parole cela signifie écouter les échantillons audio décodés, et pour notre exemple de NLP cela signifie utiliser notre tokenizer pour décoder les entrées :

    tokenizer.decode(trainer.train_dataset[0]["input_ids"])

    '[CLS] conceptually cream skimming has two basic dimensions - product and geography. [SEP] product and geography are what make cream skimming work. [SEP]'

Cela semble correct. Vous devriez faire cela pour toutes les clés dans les entrées :

    trainer.train_dataset[0].keys()

    dict_keys(['attention_mask', 'hypothesis', 'idx', 'input_ids', 'label', 'premise'])

Notez que les clés qui ne correspondent pas à des entrées acceptées par le modèle seront automatiquement écartées, donc ici nous ne garderons que `input_ids`, `attention_mask`, et `label` (qui sera renommé `labels`). Pour revérifier la signature du modèle, vous pouvez imprimer la classe de votre modèle, puis aller consulter sa documentation :

    type(trainer.model)

    transformers.models.distilbert.modeling_distilbert.DistilBertForSequenceClassification

Donc dans notre cas, nous pouvons vérifier les paramètres acceptés sur cette page. Le `Trainer` va également enregistrer les colonnes qu’il rejette.

Nous avons vérifié que les identifiants d’entrée sont corrects en les décodant. Ensuite, il y a le `attention_mask` :

    tokenizer.decode(trainer.train_dataset[0]["attention_mask"])

    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Comme nous n’avons pas appliqué de padding dans notre prétraitement, cela semble parfaitement naturel. Pour être sûr qu’il n’y a pas de problème avec ce masque d’attention, vérifions qu’il est de la même longueur que nos identifiants d’entrée :

    len(trainer.train_dataset[0]["attention_mask"]) == len(
        trainer.train_dataset[0]["input_ids"]
    )

    True

C’est bien ! Enfin, vérifions notre étiquette :

    trainer.train_dataset[0]["label"]

    1

Comme les identifiants d’entrée, c’est un nombre qui n’a pas vraiment de sens en soi. Comme nous l’avons vu précédemment, la correspondance entre les entiers et les noms d’étiquettes est stockée dans l’attribut `names` de la caractéristique correspondante du jeu de données :

    trainer.train_dataset.features["label"].names

    ['entailment', 'neutral', 'contradiction']

Donc `1` signifie `neutral`, ce qui signifie que les deux phrases que nous avons vues ci-dessus ne sont pas en contradiction : la première n’implique pas la seconde. Cela semble correct !

Nous n’avons pas de token de type identifiant ici puisque DistilBERT ne les attend pas. Si vous en avez dans votre modèle, vous devriez également vous assurer qu’ils correspondent correctement à l’endroit où se trouvent la première et la deuxième phrase dans l’entrée.

✏️ A votre tour ! Vérifiez que tout semble correct avec le deuxième élément du jeu de données d’entraînement.

Ici nous ne vérifions que le jeu d’entraînement. Vous devez bien sûr vérifier de la même façon les jeux de validation et de test.

Maintenant que nous savons que nos jeux de données sont bons, il est temps de vérifier l’étape suivante du pipeline d’entraînement.

###
Des jeux de données aux chargeurs de données

La prochaine chose qui peut mal tourner dans le pipeline d’entraînement est lorsque le `Trainer` essaie de former des batchs à partir du jeu d’entraînement ou de validation. Une fois que vous êtes sûr que les jeux de données du `Trainer` sont corrects, vous pouvez essayer de former manuellement un batch en exécutant ce qui suit (remplacez `train` par `eval` pour le dataloader de validation) :

    for batch in trainer.get_train_dataloader():
        break

Ce code crée le dataloader d’entraînement puis le parcourt en s’arrêtant à la première itération. Si le code s’exécute sans erreur, vous avez le premier batch d’entraînement que vous pouvez inspecter, et si le code se trompe, vous êtes sûr que le problème se situe dans le dataloader, comme c’est le cas ici :

    ~/git/transformers/src/transformers/data/data_collator.py in torch_default_data_collator(features)
        105                 batch[k] = torch.stack([f[k] for f in features])
        106             else:
    --> 107                 batch[k] = torch.tensor([f[k] for f in features])
        108
        109     return batch

    ValueError: expected sequence of length 45 at dim 1 (got 76)

L’inspection de la dernière image du traceback devrait suffire à vous donner un indice mais creusons un peu plus. La plupart des problèmes lors de la création d’un batch sont dus à l’assemblage des exemples en un seul batch. La première chose à vérifier en cas de doute est le `collate_fn` utilisé par votre `DataLoader` :

    data_collator = trainer.get_train_dataloader().collate_fn
    data_collator

    <function transformers.data.data_collator.default_data_collator(features: List[InputDataClass], return_tensors='pt') -> Dict[str, Any]>

C’est donc `default_data_collator`, mais ce n’est pas ce que nous voulons dans ce cas. Nous voulons rembourrer nos exemples à la phrase la plus longue du batch, ce qui est fait par `DataCollatorWithPadding`. Et cette assembleur de données est censé être utilisé par défaut par le `Trainer`, alors pourquoi n’est-il pas utilisé ici ?

La réponse est que nous n’avons pas passé le `tokenizer` au `Trainer`, donc il ne pouvait pas créer le `DataCollatorWithPadding` que nous voulons. En pratique, il ne faut jamais hésiter à transmettre explicitement l’assembleur de données que l’on veut utiliser pour être sûr d’éviter ce genre d’erreurs. Adaptons notre code pour faire exactement cela :

    from datasets import load_dataset
    import evaluate
    from transformers import (
        AutoTokenizer,
        AutoModelForSequenceClassification,
        DataCollatorWithPadding,
        TrainingArguments,
        Trainer,
    )

    raw_datasets = load_dataset("glue", "mnli")

    model_checkpoint = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

    def preprocess_function(examples):
        return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

    tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
    model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)

    args = TrainingArguments(
        f"distilbert-finetuned-mnli",
        evaluation_strategy="epoch",
        save_strategy="epoch",
        learning_rate=2e-5,
        num_train_epochs=3,
        weight_decay=0.01,
    )

    metric = evaluate.load("glue", "mnli")

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        return metric.compute(predictions=predictions, references=labels)

    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

    trainer = Trainer(
        model,
        args,
        train_dataset=tokenized_datasets["train"],
        eval_dataset=tokenized_datasets["validation_matched"],
        compute_metrics=compute_metrics,
        data_collator=data_collator,
        tokenizer=tokenizer,
    )
    trainer.train()

La bonne nouvelle ? Nous n’avons plus la même erreur qu’avant, ce qui est un progrès certain. La mauvaise nouvelle ? Nous obtenons une erreur CUDA infâme à la place :

    RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`

C’est une mauvaise chose car les erreurs CUDA sont extrêmement difficiles à déboguer en général. Nous verrons dans une minute comment résoudre ce problème mais terminons d’abord notre analyse de la création de batchs.

Si vous êtes sûr que votre collecteur de données est le bon, vous devriez essayer de l’appliquer sur quelques échantillons de votre jeu de données :

    data_collator = trainer.get_train_dataloader().collate_fn
    batch = data_collator([trainer.train_dataset[i] for i in range(4)])

Ce code échouera parce que le `train_dataset` contient des colonnes de type string que le `Trainer` supprime habituellement. Vous pouvez les supprimer manuellement ou si vous voulez reproduire exactement ce que le `Trainer` fait en coulisse, vous pouvez appeler la méthode `Trainer._remove_unused_columns()` qui fait cela :

    data_collator = trainer.get_train_dataloader().collate_fn
    actual_train_set = trainer._remove_unused_columns(trainer.train_dataset)
    batch = data_collator([actual_train_set[i] for i in range(4)])

Vous devriez alors être en mesure de déboguer manuellement ce qui se passe dans le collecteur de données si l’erreur persiste.

Maintenant que nous avons débogué le processus de création de batch, il est temps d’en passer un dans le modèle !

###
Passage par le modèle

Vous devriez être en mesure d’obtenir un batch en exécutant la commande suivante :

    for batch in trainer.get_train_dataloader():
        break

Si vous exécutez ce code dans un notebook, vous risquez d’obtenir une erreur CUDA similaire à celle que nous avons vue précédemment, auquel cas vous devrez redémarrer votre notebook et réexécuter le dernier extrait sans la ligne `trainer.train()`. C’est la deuxième chose la plus ennuyeuse à propos des erreurs CUDA : elles cassent irrémédiablement votre noyau. La première plus ennuyeuse est le fait qu’elles sont difficiles à déboguer.

Comment cela se fait-il ? Cela tient à la façon dont les GPUs fonctionnent. Ils sont extrêmement efficaces pour exécuter un batch d’opérations en parallèle, mais l’inconvénient est que lorsque l’une de ces instructions entraîne une erreur, vous ne le savez pas immédiatement. Ce n’est que lorsque le programme appelle une synchronisation des multiples processus sur le GPU qu’il réalise que quelque chose s’est mal passé, de sorte que l’erreur est en fait mentionnée à un endroit qui n’a rien à voir avec ce qui l’a créée. Par exemple, si nous regardons notre traceback précédent, l’erreur a été soulevée pendant la passe arrière, mais nous verrons dans une minute qu’elle provient en fait de quelque chose dans la passe avant.

Alors comment déboguer ces erreurs ? La réponse est simple : nous ne le faisons pas. À moins que votre erreur CUDA ne soit une erreur out-of-memory (ce qui signifie qu’il n’y a pas assez de mémoire dans votre GPU), vous devez toujours revenir au CPU pour la déboguer.

Pour faire cela dans notre cas, nous devons juste remettre le modèle sur le CPU et l’appeler sur notre batch. Le batch retourné par le `DataLoader` n’a pas encore été déplacé sur le GPU :

    outputs = trainer.model.cpu()(**batch)

    ~/.pyenv/versions/3.7.9/envs/base/lib/python3.7/site-packages/torch/nn/functional.py in nll_loss(input, target, weight, size_average, ignore_index, reduce, reduction)
       2386         )
       2387     if dim == 2:
    -> 2388         ret = torch._C._nn.nll_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
       2389     elif dim == 4:
       2390         ret = torch._C._nn.nll_loss2d(input, target, weight, _Reduction.get_enum(reduction), ignore_index)

    IndexError: Target 2 is out of bounds.

L’image devient plus claire. Au lieu d’avoir une erreur CUDA, nous avons maintenant une `IndexError` dans le calcul de la perte (donc rien à voir avec la passe arrière comme nous l’avons dit plus tôt). Plus précisément, nous pouvons voir que c’est la cible 2 qui crée l’erreur, donc c’est un bon moment pour vérifier le nombre de labels de notre modèle :

    trainer.model.config.num_labels

    2

Avec deux étiquettes, seuls les 0 et les 1 sont autorisés comme cibles, mais d’après le message d’erreur, nous avons obtenu un 2. Obtenir un 2 est en fait normal : si nous nous souvenons des noms des étiquettes que nous avons extraits plus tôt, il y en avait trois, donc nous avons les indices 0, 1 et 2 dans notre jeu de données. Le problème est que nous n’avons pas indiqué cela à notre modèle, qui aurait dû être créé avec trois étiquettes. Alors, corrigeons cela !

    from datasets import load_dataset
    import evaluate
    from transformers import (
        AutoTokenizer,
        AutoModelForSequenceClassification,
        DataCollatorWithPadding,
        TrainingArguments,
        Trainer,
    )

    raw_datasets = load_dataset("glue", "mnli")

    model_checkpoint = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

    def preprocess_function(examples):
        return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

    tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
    model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)

    args = TrainingArguments(
        f"distilbert-finetuned-mnli",
        evaluation_strategy="epoch",
        save_strategy="epoch",
        learning_rate=2e-5,
        num_train_epochs=3,
        weight_decay=0.01,
    )

    metric = evaluate.load("glue", "mnli")

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        return metric.compute(predictions=predictions, references=labels)

    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

    trainer = Trainer(
        model,
        args,
        train_dataset=tokenized_datasets["train"],
        eval_dataset=tokenized_datasets["validation_matched"],
        compute_metrics=compute_metrics,
        data_collator=data_collator,
        tokenizer=tokenizer,
    )

Nous n’incluons pas encore la ligne `trainer.train()` pour prendre le temps de vérifier que tout se passe bien. Si nous passons un batch à notre modèle, il fonctionne maintenant sans erreur !

    for batch in trainer.get_train_dataloader():
        break

    outputs = trainer.model.cpu()(**batch)

L’étape suivante consiste alors à revenir au GPU et à vérifier que tout fonctionne encore :

    import torch

    device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
    batch = {k: v.to(device) for k, v in batch.items()}

    outputs = trainer.model.to(device)(**batch)

Si vous obtenez toujours une erreur, assurez-vous de redémarrer votre notebook et d’exécuter uniquement la dernière version du script.

###
Exécution d’une étape d’optimisation

Maintenant que nous savons que nous pouvons construire des batchs qui passent réellement par le modèle, nous sommes prêts pour l’étape suivante du pipeline d’entraînement : calculer les gradients et effectuer une étape d’optimisation.

La première partie est juste une question d’appel de la méthode `backward()` sur la perte :

    loss = outputs.loss
    loss.backward()

Il est plutôt rare d’obtenir une erreur à ce stade, mais si vous en obtenez une, assurez-vous de retourner au CPU pour obtenir un message d’erreur utile.

Pour effectuer l’étape d’optimisation, il suffit de créer le `optimizer` et d’appeler sa méthode `step()` :

    trainer.create_optimizer()
    trainer.optimizer.step()

Encore une fois, si vous utilisez l’optimiseur par défaut dans le `Trainer`, vous ne devriez pas avoir d’erreur à ce stade, mais si vous avez un optimiseur personnalisé, il pourrait y avoir quelques problèmes à déboguer ici. N’oubliez pas de revenir au CPU si vous obtenez une erreur CUDA bizarre à ce stade. En parlant d’erreurs CUDA, nous avons mentionné précédemment un cas particulier. Voyons cela maintenant.

###
Gérer les erreurs <i> CUDA out of memory </i>

Chaque fois que vous obtenez un message d’erreur qui commence par `RuntimeError : CUDA out of memory`, cela indique que vous êtes à court de mémoire GPU. Cela n’est pas directement lié à votre code et peut arriver avec un script qui fonctionne parfaitement bien. Cette erreur signifie que vous avez essayé de mettre trop de choses dans la mémoire interne de votre GPU et que cela a entraîné une erreur. Comme pour d’autres erreurs CUDA, vous devrez redémarrer votre noyau pour être en mesure d’exécuter à nouveau votre entraînement.

Pour résoudre ce problème, il suffit d’utiliser moins d’espace GPU, ce qui est souvent plus facile à dire qu’à faire. Tout d’abord, assurez-vous que vous n’avez pas deux modèles sur le GPU en même temps (sauf si cela est nécessaire pour votre problème, bien sûr). Ensuite, vous devriez probablement réduire la taille de votre batch car elle affecte directement les tailles de toutes les sorties intermédiaires du modèle et leurs gradients. Si le problème persiste, envisagez d’utiliser une version plus petite de votre modèle.

Dans la prochaine partie du cours, nous examinerons des techniques plus avancées qui peuvent vous aider à réduire votre empreinte mémoire et vous permettre de finetuner les plus grands modèles.

###
Évaluation du modèle

Maintenant que nous avons résolu tous les problèmes liés à notre code, tout est parfait et l’entraînement devrait se dérouler sans problème, n’est-ce pas ? Pas si vite ! Si vous exécutez la commande `trainer.train()`, tout aura l’air bien au début, mais après un moment vous obtiendrez ce qui suit :

    # Cela prendra beaucoup de temps et se soldera par une erreur, vous ne devriez donc pas utiliser cette cellule.
    trainer.train()

    TypeError: only size-1 arrays can be converted to Python scalars

Vous réaliserez que cette erreur apparaît pendant la phase d’évaluation, donc c’est la dernière chose que nous aurons besoin de déboguer.

Vous pouvez exécuter la boucle d’évaluation du `Trainer` indépendamment de l’entraînement comme ceci :

    trainer.evaluate()

    TypeError: only size-1 arrays can be converted to Python scalars

💡 Vous devriez toujours vous assurer que vous pouvez exécuter `trainer.evaluate()` avant de lancer `trainer.train()`, pour éviter de gaspiller beaucoup de ressources de calcul avant de tomber sur une erreur.

Avant de tenter de déboguer un problème dans la boucle d’évaluation, vous devez d’abord vous assurer que vous avez examiné les données, que vous êtes en mesure de former un batch correctement et que vous pouvez exécuter votre modèle sur ces données. Nous avons effectué toutes ces étapes, et le code suivant peut donc être exécuté sans erreur :

    for batch in trainer.get_eval_dataloader():
        break

    batch = {k: v.to(device) for k, v in batch.items()}

    with torch.no_grad():
        outputs = trainer.model(**batch)

L’erreur survient plus tard, à la fin de la phase d’évaluation, et si nous regardons le traceback, nous voyons ceci :

    ~/git/datasets/src/datasets/metric.py in add_batch(self, predictions, references)
        431         """
        432         batch = {"predictions": predictions, "references": references}
    --> 433         batch = self.info.features.encode_batch(batch)
        434         if self.writer is None:
        435             self._init_writer()

Cela nous indique que l’erreur provient du module `datasets/metric.py` donc c’est un problème avec notre fonction `compute_metrics()`. Elle prend un tuple avec les logits et les labels sous forme de tableaux NumPy, alors essayons de lui fournir cela :

    predictions = outputs.logits.cpu().numpy()
    labels = batch["labels"].cpu().numpy()

    compute_metrics((predictions, labels))

    TypeError: only size-1 arrays can be converted to Python scalars

Nous obtenons la même erreur, donc le problème vient bien de cette fonction. Si on regarde son code, on voit qu’elle transmet simplement les `predictions` et les `labels` à `metric.compute()`. Y a-t-il donc un problème avec cette méthode ? Pas vraiment. Jetons un coup d’oeil rapide aux formes :

    predictions.shape, labels.shape

    ((8, 3), (8,))

Nos prédictions sont toujours des logits et non les prédictions réelles, c’est pourquoi la métrique retourne cette erreur (quelque peu obscure). La correction est assez simple, il suffit d’ajouter un argmax dans la fonction `compute_metrics()` :

    import numpy as np

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        predictions = np.argmax(predictions, axis=1)
        return metric.compute(predictions=predictions, references=labels)

    compute_metrics((predictions, labels))

    {'accuracy': 0.625}

Maintenant notre erreur est corrigée ! C’était la dernière, donc notre script va maintenant entraîner un modèle correctement.

Pour référence, voici le script complètement corrigé :

    import numpy as np
    from datasets import load_dataset
    import evaluate
    from transformers import (
        AutoTokenizer,
        AutoModelForSequenceClassification,
        DataCollatorWithPadding,
        TrainingArguments,
        Trainer,
    )

    raw_datasets = load_dataset("glue", "mnli")

    model_checkpoint = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

    def preprocess_function(examples):
        return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

    tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
    model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)

    args = TrainingArguments(
        f"distilbert-finetuned-mnli",
        evaluation_strategy="epoch",
        save_strategy="epoch",
        learning_rate=2e-5,
        num_train_epochs=3,
        weight_decay=0.01,
    )

    metric = evaluate.load("glue", "mnli")

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        predictions = np.argmax(predictions, axis=1)
        return metric.compute(predictions=predictions, references=labels)

    data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

    trainer = Trainer(
        model,
        args,
        train_dataset=tokenized_datasets["train"],
        eval_dataset=tokenized_datasets["validation_matched"],
        compute_metrics=compute_metrics,
        data_collator=data_collator,
        tokenizer=tokenizer,
    )
    trainer.train()

Dans ce cas, il n’y a plus de problème, et notre script va finetuner un modèle qui devrait donner des résultats raisonnables. Mais que faire lorsque l’entraînement se déroule sans erreur et que le modèle entraîné n’est pas du tout performant ? C’est la partie la plus difficile de l’apprentissage automatique et nous allons vous montrer quelques techniques qui peuvent vous aider.

💡 Si vous utilisez une boucle d’entraînement manuelle, les mêmes étapes s’appliquent pour déboguer votre pipeline d’entraînement, mais il est plus facile de les séparer. Assurez-vous cependant de ne pas avoir oublié le `model.eval()` ou le `model.train()` aux bons endroits, ou le `zero_grad()` à chaque étape !

##
Déboguer les erreurs silencieuses pendant l’entraînement

Que peut-on faire pour déboguer un entraînement qui se termine sans erreur mais qui ne donne pas de bons résultats ? Nous allons vous donner quelques pistes ici, mais sachez que ce type de débogage est la partie la plus difficile de l’apprentissage automatique et qu’il n’y a pas de réponse magique.

###
Vérifiez vos données (encore !)

Votre modèle n’apprendra quelque chose que s’il est réellement possible d’apprendre quelque chose de vos données. Si un bug corrompt les données ou si les étiquettes sont attribuées de manière aléatoire, il est très probable que vous n’obtiendrez aucun entraînement de modèle sur votre jeu de données. Commencez donc toujours par revérifier vos entrées et étiquettes décodées, et posez-vous les questions suivantes :

- les données décodées sont-elles compréhensibles ?
- êtes-vous d’accord avec les étiquettes ?
- y a-t-il une étiquette qui est plus courante que les autres ?
- quelle devrait être la perte/métrique si le modèle prédisait une réponse aléatoire/toujours la même réponse ?

⚠️ Si vous effectuez un entraînement distribué, imprimez des échantillons de votre ensemble de données dans chaque processus et vérifiez par trois fois que vous obtenez la même chose. Un bug courant consiste à avoir une source d’aléa dans la création des données qui fait que chaque processus a une version différente du jeu de données.

Après avoir examiné vos données, examinez quelques-unes des prédictions du modèle. Si votre modèle produit des tokens, essayez aussi de les décoder ! Si le modèle prédit toujours la même chose, cela peut être dû au fait que votre jeu de données est biaisé en faveur d’une catégorie (pour les problèmes de classification). Des techniques telles que le suréchantillonnage des classes rares peuvent aider. D’autre part, cela peut également être dû à des problèmes d’entraînement tels que de mauvais réglages des hyperparamètres.

Si la perte/la métrique que vous obtenez sur votre modèle initial avant entraînement est très différente de la perte/la métrique à laquelle vous vous attendez pour des prédictions aléatoires, vérifiez la façon dont votre perte ou votre métrique est calculée. Il y a probablement un bug. Si vous utilisez plusieurs pertes que vous ajoutez à la fin, assurez-vous qu’elles sont de la même échelle.

Lorsque vous êtes sûr que vos données sont parfaites, vous pouvez voir si le modèle est capable de s’entraîner sur elles grâce à un test simple.

###
Surentraînement du modèle sur un seul batch

Le surentraînement est généralement une chose que nous essayons d’éviter lors de l’entraînement car cela signifie que le modèle n’apprend pas à reconnaître les caractéristiques générales que nous voulons qu’il reconnaisse et se contente de mémoriser les échantillons d’entraînement. Cependant, essayer d’entraîner votre modèle sur un batch encore et encore est un bon test pour vérifier si le problème tel que vous l’avez formulé peut être résolu par le modèle que vous essayez d’entraîner. Cela vous aidera également à voir si votre taux d’apprentissage initial est trop élevé.

Une fois que vous avez défini votre `modèle`, c’est très facile. Il suffit de prendre un batch de données d’entraînement, puis de le traiter comme votre jeu de données entier que vous finetunez sur un grand nombre d’époques :

    for batch in trainer.get_train_dataloader():
        break

    batch = {k: v.to(device) for k, v in batch.items()}
    trainer.create_optimizer()

    for _ in range(20):
        outputs = trainer.model(**batch)
        loss = outputs.loss
        loss.backward()
        trainer.optimizer.step()
        trainer.optimizer.zero_grad()

💡 Si vos données d’entraînement ne sont pas équilibrées, veillez à créer un batch de données d’entraînement contenant toutes les étiquettes.

Le modèle résultant devrait avoir des résultats proches de la perfection sur le même `batch`. Calculons la métrique sur les prédictions résultantes :

    with torch.no_grad():
        outputs = trainer.model(**batch)
    preds = outputs.logits
    labels = batch["labels"]

    compute_metrics((preds.cpu().numpy(), labels.cpu().numpy()))

    {'accuracy': 1.0}

100% de précision, voilà un bel exemple de surentraînement (ce qui signifie que si vous essayez votre modèle sur n’importe quelle autre phrase, il vous donnera très probablement une mauvaise réponse) !

Si vous ne parvenez pas à ce que votre modèle obtienne des résultats parfaits comme celui-ci, cela signifie qu’il y a quelque chose qui ne va pas dans la façon dont vous avez formulé le problème ou dans vos données. Vous devez donc y remédier. Ce n’est que lorsque vous parviendrez à passer le test de surentraînement que vous pourrez être sûr que votre modèle peut réellement apprendre quelque chose.

⚠️ Vous devrez recréer votre modèle et votre `Trainer` après ce test, car le modèle obtenu ne sera probablement pas capable de récupérer et d’apprendre quelque chose d’utile sur votre jeu de données complet.

###
Ne réglez rien tant que vous n’avez pas une première ligne de base

Le réglage des hyperparamètres est toujours considéré comme la partie la plus difficile de l’apprentissage automatique mais c’est juste la dernière étape pour vous aider à gagner un peu sur la métrique. La plupart du temps, les hyperparamètres par défaut du `Trainer` fonctionneront très bien pour vous donner de bons résultats. Donc ne vous lancez pas dans une recherche d’hyperparamètres longue et coûteuse jusqu’à ce que vous ayez quelque chose qui batte la ligne de base que vous avez sur votre jeu de données.

Une fois que vous avez un modèle suffisamment bon, vous pouvez commencer à le finetuner un peu. N’essayez pas de lancer un millier d’exécutions avec différents hyperparamètres mais comparez quelques exécutions avec différentes valeurs pour un hyperparamètre afin de vous faire une idée de celui qui a le plus d’impact.

Si vous modifiez le modèle lui-même, restez simple et n’essayez rien que vous ne puissiez raisonnablement justifier. Veillez toujours à revenir au test de surentraînement pour vérifier que votre modification n’a pas eu de conséquences inattendues.

###
Demander de l’aide

Nous espérons que vous avez trouvé dans cette section des conseils qui vous ont aidé à résoudre votre problème. Si ce n’est pas le cas, n’oubliez pas que vous pouvez toujours demander de l’aide à la communauté sur le forum.

Voici quelques ressources (en anglais) supplémentaires qui peuvent s’avérer utiles :

- La reproductibilité comme vecteur des meilleures pratiques d’ingénierie par Joel Grus
- Liste de contrôle pour le débogage des réseaux de neurones par Cecelia Shao
- Comment tester unitairement le code d’apprentissage automatique par Chase Roberts
- Une recette pour entraîner les réseaux de neurones par Andrej Karpathy

Bien sûr, tous les problèmes rencontrés lors de l’entraînement ne sont pas forcément de votre faute ! Si vous rencontrez quelque chose dans la bibliothèque 🤗 Transformers ou 🤗 Datasets qui ne semble pas correct, vous avez peut-être trouver un bug. Vous devez absolument nous en parler pour qu’on puisse le corriger. Dans la section suivante, nous allons vous expliquer exactement comment faire.

< > Update on GitHub