Pytorch
TensorFlow

#
Résumé de textes

Dans cette section, nous allons voir comment les transformers peuvent être utilisés pour condenser de longs documents en résumés, une tâche connue sous le nom de résumé de texte. Il s’agit de l’une des tâches de NLP les plus difficiles car elle requiert une série de capacités, telles que la compréhension de longs passages et la génération d’un texte cohérent qui capture les sujets principaux d’un document. Cependant, lorsqu’il est bien fait, le résumé de texte est un outil puissant qui peut accélérer divers processus commerciaux en soulageant les experts du domaine de la lecture détaillée de longs documents.

Bien qu’il existe déjà plusieurs modèles finetunés pour le résumé sur le Hub, la plupart d’entre eux ne sont adaptés qu’aux documents en anglais. Ainsi, pour ajouter une touche d’originalité à cette section, nous allons entraîner un modèle bilingue pour l’anglais et l’espagnol. À la fin de cette section, vous disposerez d’un modèle capable de résumer les commentaires des clients comme celui présenté ici :

Comme nous allons le voir, ces résumés sont concis car ils sont appris à partir des titres que les clients fournissent dans leurs commentaires sur les produits. Commençons par constituer un corpus bilingue approprié pour cette tâche.

##
Préparation d’un corpus multilingue

Nous allons utiliser le Multilingual Amazon Reviews Corpus pour créer notre résumeur bilingue. Ce corpus est constitué de critiques de produits Amazon en six langues et est généralement utilisé pour évaluer les classifieurs multilingues. Cependant, comme chaque critique est accompagnée d’un titre court, nous pouvons utiliser les titres comme résumés cibles pour l’apprentissage de notre modèle ! Pour commencer, téléchargeons les sous-ensembles anglais et espagnols depuis le Hub :

    from datasets import load_dataset

    spanish_dataset = load_dataset("amazon_reviews_multi", "es")
    english_dataset = load_dataset("amazon_reviews_multi", "en")
    english_dataset

    DatasetDict({
        train: Dataset({
            features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'],
            num_rows: 200000
        })
        validation: Dataset({
            features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'],
            num_rows: 5000
        })
        test: Dataset({
            features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'],
            num_rows: 5000
        })
    })

Comme vous pouvez le voir, pour chaque langue, il y a 200 000 critiques pour la partie entraînement et 5 000 critiques pour chacune des parties validation et test. Les informations qui nous intéressent sont contenues dans les colonnes `review_body` et `review_title`. Voyons quelques exemples en créant une fonction simple qui prend un échantillon aléatoire de l’ensemble d’entraînement avec les techniques apprises au chapitre 5 :

    def show_samples(dataset, num_samples=3, seed=42):
        sample = dataset["train"].shuffle(seed=seed).select(range(num_samples))
        for example in sample:
            print(f"\n'>> Title: {example['review_title']}'")
            print(f"'>> Review: {example['review_body']}'")

    show_samples(english_dataset)

    '>> Title: Worked in front position, not rear'
    # Travaillé en position avant, pas arrière
    '>> Review: 3 stars because these are not rear brakes as stated in the item description. At least the mount adapter only worked on the front fork of the bike that I got it for.'
    # 3 étoiles car ce ne sont pas des freins arrière comme indiqué dans la description de l'article. Au moins, l'adaptateur de montage ne fonctionnait que sur la fourche avant du vélo pour lequel je l'ai acheté.

    '>> Title: meh'
    '>> Review: Does it’s job and it’s gorgeous but mine is falling apart, I had to basically put it together again with hot glue'
    # Il fait son travail et il est magnifique mais le mien est en train de tomber en morceaux, j'ai dû le recoller avec de la colle chaude.

    '>> Title: Can\'t beat these for the money'
    # On ne peut pas faire mieux pour le prix
    '>> Review: Bought this for handling miscellaneous aircraft parts and hanger "stuff" that I needed to organize; it really fit the bill. The unit arrived quickly, was well packaged and arrived intact (always a good sign). There are five wall mounts-- three on the top and two on the bottom. I wanted to mount it on the wall, so all I had to do was to remove the top two layers of plastic drawers, as well as the bottom corner drawers, place it when I wanted and mark it; I then used some of the new plastic screw in wall anchors (the 50 pound variety) and it easily mounted to the wall. Some have remarked that they wanted dividers for the drawers, and that they made those. Good idea. My application was that I needed something that I can see the contents at about eye level, so I wanted the fuller-sized drawers. I also like that these are the new plastic that doesn\'t get brittle and split like my older plastic drawers did. I like the all-plastic construction. It\'s heavy duty enough to hold metal parts, but being made of plastic it\'s not as heavy as a metal frame, so you can easily mount it to the wall and still load it up with heavy stuff, or light stuff. No problem there. For the money, you can\'t beat it. Best one of these I\'ve bought to date-- and I\'ve been using some version of these for over forty years.'
    # Je l'ai acheté pour manipuler diverses pièces d'avion et des "trucs" de hangar que je devais organiser ; il a vraiment fait l'affaire. L'unité est arrivée rapidement, était bien emballée et est arrivée intacte (toujours un bon signe). Il y a cinq supports muraux - trois sur le dessus et deux sur le dessous. Je voulais le monter sur le mur, alors tout ce que j'ai eu à faire était d'enlever les deux couches supérieures de tiroirs en plastique, ainsi que les tiroirs d'angle inférieurs, de le placer où je voulais et de le marquer ; j'ai ensuite utilisé quelques-uns des nouveaux ancrages muraux à vis en plastique (la variété de 50 livres) et il s'est facilement monté sur le mur. Certains ont fait remarquer qu'ils voulaient des séparateurs pour les tiroirs, et qu'ils les ont fabriqués. Bonne idée. Pour ma part, j'avais besoin de quelque chose dont je pouvais voir le contenu à hauteur des yeux, et je voulais donc des tiroirs plus grands. J'aime aussi le fait qu'il s'agisse du nouveau plastique qui ne se fragilise pas et ne se fend pas comme mes anciens tiroirs en plastique. J'aime la construction entièrement en plastique. Elle est suffisamment résistante pour contenir des pièces métalliques, mais étant en plastique, elle n'est pas aussi lourde qu'un cadre métallique, ce qui permet de la fixer facilement au mur et de la charger d'objets lourds ou légers. Aucun problème. Pour le prix, c'est imbattable. C'est le meilleur que j'ai acheté à ce jour, et j'utilise des versions de ce type depuis plus de quarante ans.

> ✏️ Essayez ! Changez la graine aléatoire dans la commande `Dataset.shuffle()` pour explorer d’autres critiques dans le corpus. Si vous parlez espagnol, jetez un coup d’œil à certaines des critiques dans `spanish_dataset` pour voir si les titres semblent aussi être des résumés raisonnables.

Cet échantillon montre la diversité des critiques que l’on trouve généralement en ligne, allant du positif au négatif (et tout ce qui se trouve entre les deux !). Bien que l’exemple avec le titre « meh » ne soit pas très informatif, les autres titres semblent être des résumés décents des critiques. Entraîner un modèle de résumé sur l’ensemble des 400 000 avis prendrait beaucoup trop de temps sur un seul GPU, nous allons donc nous concentrer sur la génération de résumés pour un seul domaine de produits. Pour avoir une idée des domaines parmi lesquels nous pouvons choisir, convertissons `english_dataset` en `pandas.DataFrame` et calculons le nombre d’avis par catégorie de produits :

    english_dataset.set_format("pandas")
    english_df = english_dataset["train"][:]
    # Afficher le compte des 20 premiers produits
    english_df["product_category"].value_counts()[:20]

    home                      17679   # maison
    apparel                   15951   # vêtements
    wireless                  15717   # sans fil
    other                     13418   # autres
    beauty                    12091   # beauté
    drugstore                 11730   # pharmacie
    kitchen                   10382   # cuisine
    toy                        8745   # jouets
    sports                     8277   # sports
    automotive                 7506   # automobile
    lawn_and_garden            7327   # pelouse_et_jardin
    home_improvement           7136   # amélioration_de_la_maison
    pet_products               7082   # produits_pour_animaux_de_compagnie
    digital_ebook_purchase     6749   # achat_de_livres_numériques
    pc                         6401   # ordinateur_personnel
    electronics                6186   # électronique
    office_product             5521   # produits_de_bureau
    shoes                      5197   # chaussures
    grocery                    4730   # épicerie
    book                       3756   # livre
    Name: product_category, dtype: int64

Les produits les plus populaires du jeu de données anglais concernent les articles ménagers, les vêtements et l’électronique sans fil. Pour rester dans le thème d’Amazon, nous allons nous concentrer sur le résumé des critiques de livres. Après tout, c’est la raison d’être de l’entreprise ! Nous pouvons voir deux catégories de produits qui correspondent à nos besoins (`book` et `digital_ebook_purchase`). Nous allons donc filtrer les jeux de données dans les deux langues pour ces produits uniquement. Comme nous l’avons vu dans le chapitre 5, la fonction `Dataset.filter()` nous permet de découper un jeu de données de manière très efficace. Nous pouvons donc définir une fonction simple pour le faire :

    def filter_books(example):
        return (
            example["product_category"] == "book"
            or example["product_category"] == "digital_ebook_purchase"
        )

Maintenant, lorsque nous appliquons cette fonction à `english_dataset` et `spanish_dataset`, le résultat ne contient que les lignes impliquant les catégories de livres. Avant d’appliquer le filtre, changeons le format de `english_dataset` de `"pandas"` à `"arrow"` :

    english_dataset.reset_format()

Nous pouvons ensuite appliquer la fonction de filtrage et, à titre de vérification, inspecter un échantillon de critiques pour voir si elles portent bien sur des livres :

    spanish_books = spanish_dataset.filter(filter_books)
    english_books = english_dataset.filter(filter_books)
    show_samples(english_books)

    '>> Title: I\'m dissapointed.'
    # Je suis déçu
    '>> Review: I guess I had higher expectations for this book from the reviews. I really thought I\'d at least like it. The plot idea was great. I loved Ash but, it just didnt go anywhere. Most of the book was about their radio show and talking to callers. I wanted the author to dig deeper so we could really get to know the characters. All we know about Grace is that she is attractive looking, Latino and is kind of a brat. I\'m dissapointed.'
    # Je suppose que j'avais de plus grandes attentes pour ce livre d'après les critiques. Je pensais vraiment que j'allais au moins l'aimer. L'idée de l'intrigue était géniale. J'aimais Ash, mais ça n'allait nulle part. La plus grande partie du livre était consacrée à leur émission de radio et aux conversations avec les auditeurs. Je voulais que l'auteur creuse plus profondément pour que nous puissions vraiment connaître les personnages. Tout ce que nous savons de Grace, c'est qu'elle est séduisante, qu'elle est latino et qu'elle est une sorte de garce. Je suis déçue.

    '>> Title: Good art, good price, poor design'
    # Un bon art, un bon prix, un mauvais design
    '>> Review: I had gotten the DC Vintage calendar the past two years, but it was on backorder forever this year and I saw they had shrunk the dimensions for no good reason. This one has good art choices but the design has the fold going through the picture, so it\'s less aesthetically pleasing, especially if you want to keep a picture to hang. For the price, a good calendar'
    # J'ai eu le calendrier DC Vintage ces deux dernières années, mais il était en rupture de stock pour toujours cette année et j'ai vu qu'ils avaient réduit les dimensions sans raison valable. Celui-ci a de bons choix artistiques mais le design a le pli qui traverse l'image, donc c'est moins esthétique, surtout si vous voulez garder une image à accrocher. Pour le prix, c'est un bon calendrier.

    '>> Title: Helpful'
    # Utile
    '>> Review: Nearly all the tips useful and. I consider myself an intermediate to advanced user of OneNote. I would highly recommend.'
    # Presque tous les conseils sont utiles et. Je me considère comme un utilisateur intermédiaire à avancé de OneNote. Je le recommande vivement.

D’accord, nous pouvons voir que les critiques ne concernent pas strictement les livres et peuvent se référer à des choses comme des calendriers et des applications électroniques telles que OneNote. Néanmoins, le domaine semble approprié pour entraîner un modèle de résumé. Avant de regarder les différents modèles qui conviennent à cette tâche, nous avons une dernière préparation de données à faire : combiner les critiques anglaises et espagnoles en un seul objet `DatasetDict`. 🤗 Datasets fournit une fonction pratique `concatenate_datasets()` qui (comme son nom l’indique) va empiler deux objets `Dataset` l’un sur l’autre. Ainsi, pour créer notre jeu de données bilingue, nous allons boucler sur chaque division, concaténer les jeux de données pour cette division, et mélanger le résultat pour s’assurer que notre modèle ne s’adapte pas trop à une seule langue :

    from datasets import concatenate_datasets, DatasetDict

    books_dataset = DatasetDict()

    for split in english_books.keys():
        books_dataset[split] = concatenate_datasets(
            [english_books[split], spanish_books[split]]
        )
        books_dataset[split] = books_dataset[split].shuffle(seed=42)

    # Quelques exemples
    show_samples(books_dataset)

    '>> Title: Easy to follow!!!!'
    # Facile à suivre!!!!
    '>> Review: I loved The dash diet weight loss Solution. Never hungry. I would recommend this diet. Also the menus are well rounded. Try it. Has lots of the information need thanks.'
    # J'ai adoré The dash diet weight loss Solution. Jamais faim. Je recommande ce régime. Les menus sont également bien arrondis. Essayez-le. Il contient beaucoup d'informations, merci.

    '>> Title: PARCIALMENTE DAÑADO'
    # PARTIELLEMENT ENDOMMAGÉ
    '>> Review: Me llegó el día que tocaba, junto a otros libros que pedí, pero la caja llegó en mal estado lo cual dañó las esquinas de los libros porque venían sin protección (forro).'
    # Il est arrivé le jour prévu, avec d'autres livres que j'avais commandés, mais la boîte est arrivée en mauvais état, ce qui a endommagé les coins des livres car ils étaient livrés sans protection (doublure).

    '>> Title: no lo he podido descargar'
    # Je n'ai pas pu le télécharger
    '>> Review: igual que el anterior'
    # même chose que ci-dessus

Cela ressemble certainement à un mélange de critiques anglaises et espagnoles ! Maintenant que nous avons un corpus d’entraînement, une dernière chose à vérifier est la distribution des mots dans les critiques et leurs titres. Ceci est particulièrement important pour les tâches de résumé, où les résumés de référence courts dans les données peuvent biaiser le modèle pour qu’il ne produise qu’un ou deux mots dans les résumés générés. Les graphiques ci-dessous montrent les distributions de mots, et nous pouvons voir que les titres sont fortement biaisés vers seulement 1 ou 2 mots :

Pour y remédier, nous allons filtrer les exemples avec des titres très courts afin que notre modèle puisse produire des résumés plus intéressants. Puisque nous avons affaire à des textes anglais et espagnols, nous pouvons utiliser une heuristique grossière pour séparer les titres sur les espaces blancs, puis utiliser notre fidèle méthode `Dataset.filter()` comme suit :

    books_dataset = books_dataset.filter(lambda x: len(x["review_title"].split()) > 2)

Maintenant que nous avons préparé notre corpus, voyons quelques transformers possibles que l’on pourrait finetuné dessus !

##
Modèles pour le résumé de texte

Si vous y pensez, le résumé de texte est une tâche similaire à la traduction automatique. Nous avons un corps de texte, comme une critique, que nous aimerions « traduire » en une version plus courte qui capture les caractéristiques saillantes de l’entrée. En conséquence, la plupart des transformers pour le résumé adoptent l’architecture encodeur-décodeur que nous avons rencontrée pour la première fois dans le chapitre 1, bien qu’il y ait quelques exceptions comme la famille de modèles GPT qui peut également être utilisée pour le résumé dans des contextes peu complexes. Le tableau suivant présente quelques modèles pré-entraînés populaires qui peuvent être finetunés pour le résumé.

Transformers | Description | Multilingue ?
--- | --- | ---
GPT-2 | Bien qu’il soit entraîné comme un modèle de langage autorégressif, vous pouvez faire en sorte que le GPT-2 génère des résumés en ajoutant `TL;DR` à la fin du texte d’entrée. | ❌
PEGASUS | Utilise un objectif de pré-entraînement pour prédire les phrases masquées dans les textes à plusieurs phrases. Cet objectif de pré-entraînement est plus proche du résumé que de la modélisation du langage standard et obtient des scores élevés sur des benchmarks populaires. | ❌
T5 | Une architecture universelle de transformer qui formule toutes les tâches dans un cadre texte à texte. Par exemple, le format d’entrée du modèle pour résumer un document est `summarize: ARTICLE`. | ❌
mT5 | Une version multilingue de T5, pré-entraînée sur le corpus multilingue Common Crawl (mC4), couvrant 101 langues. | ✅
BART | Une architecture de transformer avec une pile d’encodeurs et de décodeurs entraînés pour reconstruire l’entrée corrompue qui combine les schémas de pré-entraînement de BERT et GPT-2. | ❌
mBART-50 | Une version multilingue de BART, pré-entraînée sur 50 langues. | ✅

Comme vous pouvez le voir dans ce tableau, la majorité des transformers pour le résumé (et en fait la plupart des tâches de NLP) sont monolingues. C’est une bonne chose si votre tâche se déroule dans une langue « à haute ressource » comme l’anglais ou l’allemand, mais moins pour les milliers d’autres langues utilisées dans le monde. Heureusement, il existe une catégorie de transformers multilingues, comme mT5 et mBART, qui viennent à la rescousse. Ces modèles sont pré-entraînés en utilisant la modélisation du langage mais avec une particularité : au lieu d’être entraîné sur un corpus d’une seule langue, ils sont entraînés conjointement sur des textes dans plus de 50 langues !

Nous allons nous concentrer sur mT5, une architecture intéressante basée sur T5 qui a été pré-entraînée dans un cadre texte à texte. Dans T5, chaque tâche de NLP est formulée en termes d’un préfixe de prompt comme `summarize:` qui conditionne le modèle à adapter le texte généré au prompt. Comme le montre la figure ci-dessous, cela rend le T5 extrêmement polyvalent car vous pouvez résoudre de nombreuses tâches avec un seul modèle !

mT5 n’utilise pas de préfixes mais partage une grande partie de la polyvalence de T5 et a l’avantage d’être multilingue. Maintenant que nous avons choisi un modèle, voyons comment préparer nos données pour l’entraînement.

> ✏️ Essayez ! Une fois que vous aurez terminé cette section, comparez le mT5 à mBART en finetunant ce dernier avec les mêmes techniques. Pour des points bonus, vous pouvez aussi essayer de finetuner le T5 uniquement sur les critiques anglaises. Puisque le T5 a un préfixe spécial, vous devrez ajouter `summarize:` aux entrées dans les étapes de prétraitement ci-dessous.

##
Prétraitement des données

Notre prochaine tâche est de tokeniser et d’encoder nos critiques et leurs titres. Comme d’habitude, nous commençons par charger le tokenizer associé au checkpoint du modèle pré-entraîné. Nous utiliserons `mt5-small` comme checkpoint afin de pouvoir finetuner le modèle en un temps raisonnable :

    from transformers import AutoTokenizer

    model_checkpoint = "google/mt5-small"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

> 💡 Aux premiers stades de vos projets de NLP, une bonne pratique consiste à entraîner une classe de « petits » modèles sur un petit échantillon de données. Cela vous permet de déboguer et d’itérer plus rapidement vers un flux de travail de bout en bout. Une fois que vous avez confiance dans les résultats, vous pouvez toujours faire évoluer le modèle en changeant simplement le checkpoint du modèle !

Testons le tokenizer de mT5 sur un petit exemple :

    inputs = tokenizer(
        "I loved reading the Hunger Games!"
    )  # J'ai adoré lire les Hunger Games !
    inputs

    {'input_ids': [336, 259, 28387, 11807, 287, 62893, 295, 12507, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}

Ici nous pouvons voir les familiers `input_ids` et `attention_mask` que nous avons rencontrés dans nos premières expériences de finetuning au chapitre 3. Décodons ces identifiants d’entrée avec la fonction `convert_ids_to_tokens()` du tokenizer pour voir à quel type de tokenizer nous avons affaire :

    tokenizer.convert_ids_to_tokens(inputs.input_ids)

    ['▁I', '▁', 'loved', '▁reading', '▁the', '▁Hung', 'er', '▁Games', '</s>']

Le caractère Unicode spécial `▁` et le token de fin de séquence `</s>` indiquent que nous avons affaire au tokenizer de SentencePiece, qui est basé sur l’algorithme de segmentation Unigram discuté dans le chapitre 6. Unigram est particulièrement utile pour les corpus multilingues car il permet à SentencePiece d’être agnostique vis-à-vis des accents, de la ponctuation et du fait que de nombreuses langues, comme le japonais, n’ont pas de caractères d’espacement.

Pour tokeniser notre corpus, nous devons faire face à une subtilité associée au résumé : comme nos étiquettes sont également du texte, il est possible qu’elles dépassent la taille maximale du contexte du modèle. Cela signifie que nous devons appliquer une troncature à la fois aux critiques et à leurs titres pour nous assurer de ne pas transmettre des entrées trop longues à notre modèle. Les tokenizers de 🤗 Transformers fournissent une fonction très pratique `as_target_tokenizer()` qui vous permet de tokeniser les étiquettes en parallèle avec les entrées. Ceci est typiquement fait en utilisant un gestionnaire de contexte à l’intérieur d’une fonction de prétraitement qui encode d’abord les entrées, et ensuite encode les étiquettes comme une colonne séparée. Voici un exemple d’une telle fonction pour mT5 :

    max_input_length = 512
    max_target_length = 30

    def preprocess_function(examples):
        model_inputs = tokenizer(
            examples["review_body"],
            max_length=max_input_length,
            truncation=True,
        )
        labels = tokenizer(
            examples["review_title"], max_length=max_target_length, truncation=True
        )
        model_inputs["labels"] = labels["input_ids"]
        return model_inputs

Parcourons ce code pour comprendre ce qui se passe. La première chose que nous avons faite est de définir des valeurs pour `max_input_length` et `max_target_length`, qui fixent les limites supérieures de la longueur des commentaires et des titres. Comme le corps de la critique est généralement beaucoup plus long que le titre, nous avons mis ces valeurs à l’échelle en conséquence. Ensuite, dans la `preprocess_function()` elle-même, nous pouvons voir que les commentaires sont d’abord tokenizés, suivis par les titres avec `as_target_tokenizer()`.

Avec la fonction `preprocess_function()`, il est alors simple de tokeniser l’ensemble du corpus en utilisant la fonction pratique `Dataset.map()` que nous avons largement utilisée dans ce cours :

    tokenized_datasets = books_dataset.map(preprocess_function, batched=True)

Maintenant que le corpus a été prétraité, examinons certaines métriques couramment utilisées pour le résumé. Comme nous allons le voir, il n’existe pas de solution miracle pour mesurer la qualité d’un texte généré par une machine.

> 💡 Vous avez peut-être remarqué que nous avons utilisé `batched=True` dans notre fonction `Dataset.map()` ci-dessus. Cela permet de coder les exemples par lots de 1 000 (par défaut) et d’utiliser les capacités de multithreading des tokenizers rapides de 🤗 Transformers. Lorsque cela est possible, essayez d’utiliser `batched=True` pour tirer le meilleur parti de votre prétraitement !

##
Métriques pour le résumé de texte

Par rapport à la plupart des autres tâches que nous avons abordées dans ce cours, la mesure des performances des tâches de génération de texte comme le résumé ou la traduction n’est pas aussi simple. Par exemple, pour une critique telle que « J’ai adoré lire les Hunger Games », il existe plusieurs résumés valides, comme « J’ai adoré Hunger Games » ou « Hunger Games est une excellente lecture ». Il est clair que l’application d’une sorte de correspondance exacte entre le résumé généré et l’étiquette n’est pas une bonne solution. En effet, même les humains auraient de mauvais résultats avec une telle mesure, car nous avons tous notre propre style d’écriture.

Pour le résumé, l’une des métriques les plus couramment utilisées est le score ROUGE (abréviation de Recall-Oriented Understudy for Gisting Evaluation). L’idée de base de cette métrique est de comparer un résumé généré avec un ensemble de résumés de référence qui sont généralement créés par des humains. Pour être plus précis, supposons que nous voulions comparer les deux résumés suivants :

    generated_summary = "I absolutely loved reading the Hunger Games"
    # "J'ai absolument adoré lire les Hunger Games"
    reference_summary = "I loved reading the Hunger Games"
    # "J'ai adoré lire les Hunger Games"

Une façon de les comparer pourrait être de compter le nombre de mots qui se chevauchent, qui dans ce cas serait de 6. Cependant, cette méthode est un peu grossière, c’est pourquoi ROUGE se base sur le calcul des scores de précision et de rappel pour le chevauchement.

> 🙋 Ne vous inquiétez pas si c’est la première fois que vous entendez parler de précision et de rappel. Nous allons parcourir ensemble quelques exemples explicites pour que tout soit clair. Ces métriques sont généralement rencontrées dans les tâches de classification, donc si vous voulez comprendre comment la précision et le rappel sont définis dans ce contexte, nous vous recommandons de consulter les guides de `scikit-learn`.

Pour ROUGE, le rappel mesure la proportion du résumé de référence qui est capturée par le résumé généré. Si nous ne faisons que comparer des mots, le rappel peut être calculé selon la formule suivante : R e c a l l=N o m b r e d e m o t s q u i s e c h e v a u c h e n t N o m b r e t o t a l d e m o t s d a n s l e r e ˊ s u m e ˊ d e r e ˊ f e r e n c e \mathrm{Recall} = \frac{\mathrm{Nombre\,de\,mots\,qui\,se\,chevauchent}}{\mathrm{Nombre\, total\, de\, mots\, dans\, le\, résumé\, de\, réference}} Recall=Nombre total de mots dans le r e ˊ sum e ˊ de r e ˊ ference Nombre de mots qui se chevauchent​

Pour notre exemple simple ci-dessus, cette formule donne un rappel parfait de 6/6 = 1, c’est-à-dire que tous les mots du résumé de référence ont été produits par le modèle. Cela peut sembler génial, mais imaginez que le résumé généré ait été « J’ai vraiment aimé lire les Hunger Games toute la nuit ». Le rappel serait également parfait, mais le résumé serait sans doute moins bon puisqu’il serait verbeux. Pour traiter ces scénarios, nous calculons également la précision, qui dans le contexte de ROUGE, mesure la proportion du résumé généré qui est pertinente : P r e c i s i o n=N o m b r e d e m o t s q u i s e c h e v a u c h e n t N o m b r e t o t a l d e m o t s d a n s l e r e ˊ s u m e ˊ g e ˊ n e ˊ r e ˊ \mathrm{Precision} = \frac{\mathrm{Nombre\,de\,mots\,qui\,se\,chevauchent}}{\mathrm{Nombre\, total\, de\, mots\, dans\, le\, résumé\, généré}} Precision=Nombre total de mots dans le r e ˊ sum e ˊ g e ˊ n e ˊ r e ˊ Nombre de mots qui se chevauchent​

En appliquant cela à notre résumé verbeux, on obtient une précision de 6/10 = 0,6, ce qui est considérablement moins bon que la précision de 6/7 = 0,86 obtenue par notre résumé plus court. En pratique, la précision et le rappel sont généralement calculés, puis le score F1 (la moyenne harmonique de la précision et du rappel) est indiqué. Nous pouvons le faire facilement dans 🤗 Datasets en installant d’abord le package `rouge_score` :

    !pip install rouge_score

et ensuite charger la métrique ROUGE comme suit :

    import evaluate

    rouge_score = evaluate.load("rouge")

Ensuite, nous pouvons utiliser la fonction `rouge_score.compute()` pour calculer toutes les métriques en une seule fois :

    scores = rouge_score.compute(
        predictions=[generated_summary], references=[reference_summary]
    )
    scores

    {'rouge1': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92)),
     'rouge2': AggregateScore(low=Score(precision=0.67, recall=0.8, fmeasure=0.73), mid=Score(precision=0.67, recall=0.8, fmeasure=0.73), high=Score(precision=0.67, recall=0.8, fmeasure=0.73)),
     'rougeL': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92)),
     'rougeLsum': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92))}

Whoa, il y a pas mal d’informations dans cette sortie. Qu’est-ce que ça veut dire ? Tout d’abord, 🤗 Datasets calcule des intervalles de confiance pour la précision, le rappel et le score F1. Ce sont les attributs `low`, `mid`, et `high` que vous pouvez voir ici. De plus, 🤗 Datasets calcule une variété de scores ROUGE qui sont basés sur différents types de granularité du texte lors de la comparaison des résumés générés et de référence. La variante `rouge1` est le chevauchement des unigrammes. C’est juste une façon fantaisiste de dire le chevauchement des mots et c’est exactement la métrique dont nous avons discuté ci-dessus. Pour vérifier cela, nous allons extraire la valeur `mid` de nos scores :

    scores["rouge1"].mid

    Score(precision=0.86, recall=1.0, fmeasure=0.92)

Super, les chiffres de précision et de rappel correspondent ! Maintenant, qu’en est-il des autres scores ROUGE ? `rouge2` mesure le chevauchement entre les bigrammes (chevauchement des paires de mots), tandis que `rougeL` et `rougeLsum` mesurent les plus longues séquences de mots correspondants en recherchant les plus longues sous-souches communes dans les résumés générés et de référence. Le « sum » dans `rougeLsum` fait référence au fait que cette métrique est calculée sur un résumé entier, alors que `rougeL` est calculée comme une moyenne sur des phrases individuelles.

> ✏️ Essayez ! Créez votre propre exemple de résumé généré et de référence et voyez si les scores ROUGE obtenus correspondent à un calcul manuel basé sur les formules de précision et de rappel. Pour des points bonus, divisez le texte en bigrammes et comparez la précision et le rappel pour la métrique `rouge2`.

Nous utiliserons ces scores ROUGE pour suivre les performances de notre modèle, mais avant cela, faisons ce que tout bon praticien de NLP devrait faire : créer une baseline solide, mais simple !

###
Création d’une base de référence solide

Une baseline commune pour le résumé de texte consiste à prendre simplement les trois premières phrases d’un article, souvent appelée la baseline lead-3. Nous pourrions utiliser les points pour tracker les limites des phrases mais cela échouera avec des acronymes comme « U.S. » ou « U.N. ». Nous allons donc utiliser la bibliothèque `nltk`, qui inclut un meilleur algorithme pour gérer ces cas. Vous pouvez installer le package en utilisant `pip` comme suit :

    !pip install nltk

puis téléchargez les règles de ponctuation :

    import nltk

    nltk.download("punkt")

Ensuite, nous importons le tokenizer de `nltk` et créons une fonction simple pour extraire les trois premières phrases d’une critique. La convention dans le résumé de texte est de séparer chaque résumé avec une nouvelle ligne, donc nous allons également inclure ceci et tester le tout sur un exemple d’entraînement :

    from nltk.tokenize import sent_tokenize

    def three_sentence_summary(text):
        return "\n".join(sent_tokenize(text)[:3])

    print(three_sentence_summary(books_dataset["train"][1]["review_body"]))

    'I grew up reading Koontz, and years ago, I stopped,convinced i had "outgrown" him.'
    # J'ai grandi en lisant Koontz, et il y a des années, j'ai arrêté, convaincu que je l'avais "dépassé"
    'Still,when a friend was looking for something suspenseful too read, I suggested Koontz.'
    # "Pourtant, quand une amie cherchait un livre à suspense, je lui ai suggéré Koontz."
    'She found Strangers.'
    # Elle a trouvé Strangers.

Cela semble fonctionner, alors implémentons maintenant une fonction qui extrait ces résumés d’un jeu de données et calcule les scores ROUGE pour la ligne de base :

    def evaluate_baseline(dataset, metric):
        summaries = [three_sentence_summary(text) for text in dataset["review_body"]]
        return metric.compute(predictions=summaries, references=dataset["review_title"])

Nous pouvons ensuite utiliser cette fonction pour calculer les scores ROUGE sur l’ensemble de validation et les embellir un peu en utilisant Pandas :

    import pandas as pd

    score = evaluate_baseline(books_dataset["validation"], rouge_score)
    rouge_names = ["rouge1", "rouge2", "rougeL", "rougeLsum"]
    rouge_dict = dict((rn, round(score[rn].mid.fmeasure * 100, 2)) for rn in rouge_names)
    rouge_dict

    {'rouge1': 16.74, 'rouge2': 8.83, 'rougeL': 15.6, 'rougeLsum': 15.96}

Nous pouvons voir que le score de `rouge2` est significativement plus bas que le reste. Ceci reflète probablement le fait que les titres des critiques sont typiquement concis et donc que la baseline lead-3 est trop verbeuse. Maintenant que nous disposons d’une bonne baseline, concentrons-nous sur le finetuning du mT5 !

##
<i> Finetuning </i> de mT5 avec l’API Trainer

Le finetuning d’un modèle pour le résumé est très similaire aux autres tâches que nous avons couvertes dans ce chapitre. La première chose à faire est de charger le modèle pré-entraîné à partir du checkpoint `mt5-small`. Puisque la compression est une tâche de séquence à séquence, nous pouvons charger le modèle avec la classe `AutoModelForSeq2SeqLM`, qui téléchargera automatiquement et mettra en cache les poids :

    from transformers import AutoModelForSeq2SeqLM

    model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

> 💡 Si vous vous demandez pourquoi vous ne voyez aucun avertissement concernant le finetuning du modèle sur une tâche en aval, c’est parce que pour les tâches de séquence à séquence, nous conservons tous les poids du réseau. Comparez cela à notre modèle de classification de texte du chapitre 3 où la tête du modèle pré-entraîné a été remplacée par un réseau initialisé de manière aléatoire.

La prochaine chose que nous devons faire est de nous connecter au Hub. Si vous exécutez ce code dans un notebook, vous pouvez le faire avec la fonction utilitaire suivante :

    from huggingface_hub import notebook_login

    notebook_login()

qui affichera un widget où vous pourrez saisir vos informations d’identification. Vous pouvez également exécuter cette commande dans votre terminal et vous connecter à partir de là :

    huggingface-cli login

Nous aurons besoin de générer des résumés afin de calculer les scores ROUGE pendant l’entraînement. Heureusement, 🤗 Transformers fournit des classes dédiées `Seq2SeqTrainingArguments` et `Seq2SeqTrainer` qui peuvent faire cela pour nous automatiquement ! Pour voir comment cela fonctionne, définissons d’abord les hyperparamètres et autres arguments pour nos expériences :

    from transformers import Seq2SeqTrainingArguments

    batch_size = 8
    num_train_epochs = 8
    # La perte d'entraînement à chaque époque
    logging_steps = len(tokenized_datasets["train"]) // batch_size
    model_name = model_checkpoint.split("/")[-1]

    args = Seq2SeqTrainingArguments(
        output_dir=f"{model_name}-finetuned-amazon-en-es",
        evaluation_strategy="epoch",
        learning_rate=5.6e-5,
        per_device_train_batch_size=batch_size,
        per_device_eval_batch_size=batch_size,
        weight_decay=0.01,
        save_total_limit=3,
        num_train_epochs=num_train_epochs,
        predict_with_generate=True,
        logging_steps=logging_steps,
        push_to_hub=True,
    )

Ici, l’argument `predict_with_generate` a été défini pour indiquer que nous devons générer des résumés pendant l’évaluation afin de pouvoir calculer les scores ROUGE pour chaque époque. Comme discuté au chapitre 1, le décodeur effectue l’inférence en prédisant les tokens un par un, et ceci est implémenté par la méthode `generate()`. Définir `predict_with_generate=True` indique au `Seq2SeqTrainer` d’utiliser cette méthode pour l’évaluation. Nous avons également ajusté certains des hyperparamètres par défaut, comme le taux d’apprentissage, le nombre d’époques, et le taux de décroissance des poids, et nous avons réglé l’option `save_total_limit` pour ne sauvegarder que jusqu’à trois checkpoints pendant l’entraînement. C’est parce que même la plus petite version de mT5 utilise environ 1 Go d’espace disque, et nous pouvons gagner un peu de place en limitant le nombre de copies que nous sauvegardons.

L’argument `push_to_hub=True` nous permettra de pousser le modèle vers le Hub après l’entraînement. Vous trouverez le dépôt sous votre profil utilisateur dans l’emplacement défini par `output_dir`. Notez que vous pouvez spécifier le nom 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/mt5-finetuned-amazon-en-es"` à `Seq2SeqTrainingArguments`.

La prochaine chose que nous devons faire est de fournir à `Seq2SeqTrainer` une fonction `compute_metrics()` afin que nous puissions évaluer notre modèle pendant l’entraînement. Pour le résumé, c’est un peu plus compliqué que de simplement appeler `rouge_score.compute()` sur les prédictions du modèle, puisque nous devons décoder les sorties et les étiquettes en texte avant de pouvoir calculer les scores ROUGE. La fonction suivante fait exactement cela, et utilise également la fonction `sent_tokenize()` de `nltk` pour séparer les phrases du résumé avec des nouvelles lignes :

    import numpy as np

    def compute_metrics(eval_pred):
        predictions, labels = eval_pred
        # Décoder les résumés générés en texte
        decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
        # Remplacer -100 dans les étiquettes car nous ne pouvons pas les décoder
        labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
        # Décoder les résumés de référence en texte
        decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
        # ROUGE attend une nouvelle ligne après chaque phrase
        decoded_preds = ["\n".join(sent_tokenize(pred.strip())) for pred in decoded_preds]
        decoded_labels = ["\n".join(sent_tokenize(label.strip())) for label in decoded_labels]
        # Calcul des scores ROUGE
        result = rouge_score.compute(
            predictions=decoded_preds, references=decoded_labels, use_stemmer=True
        )
        # Extraire les scores médians
        result = {key: value.mid.fmeasure * 100 for key, value in result.items()}
        return {k: round(v, 4) for k, v in result.items()}

Ensuite, nous devons définir un assembleur de données pour notre tâche de séquence à séquence. Comme mT5 est un transformer encodeur-décodeur, une des subtilités de la préparation de nos batchs est que, pendant le décodage, nous devons décaler les étiquettes d’une unité vers la droite. Ceci est nécessaire pour garantir que le décodeur ne voit que les étiquettes de vérité terrain précédentes et non les étiquettes actuelles ou futures, qui seraient faciles à mémoriser pour le modèle. Cela ressemble à la façon dont l’auto-attention masquée est appliquée aux entrées dans une tâche comme la modélisation causale du langage.

Heureusement, 🤗 Transformers fournit un assembleur `DataCollatorForSeq2Seq` qui rembourrera dynamiquement les entrées et les étiquettes pour nous. Pour instancier ce assembleur, nous devons simplement fournir le tokenizer et le modèle :

    from transformers import DataCollatorForSeq2Seq

    data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)

Voyons ce que produit ce assembleur lorsqu’on lui donne un petit batch d’exemples. Tout d’abord, nous devons supprimer les colonnes contenant des chaînes de caractères, car le assembleur ne saura pas comment remplir ces éléments :

    tokenized_datasets = tokenized_datasets.remove_columns(
        books_dataset["train"].column_names
    )

Comme le assembleur attend une liste de `dict`, où chaque `dict` représente un seul exemple du jeu de données, nous devons également mettre les données dans le format attendu avant de les transmettre au assembleur de données :

    features = [tokenized_datasets["train"][i] for i in range(2)]
    data_collator(features)

    {'attention_mask': tensor([[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, 1, 0, 0, 0, 0, 0, 0],
            [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, 1, 1, 1, 1, 1, 1, 1]]), 'input_ids': tensor([[  1494,    259,   8622,    390,    259,    262,   2316,   3435,    955,
                772,    281,    772,   1617,    263,    305,  14701,    260,   1385,
               3031,    259,  24146,    332,   1037,    259,  43906,    305,    336,
                260,      1,      0,      0,      0,      0,      0,      0],
            [   259,  27531,  13483,    259,   7505,    260, 112240,  15192,    305,
              53198,    276,    259,  74060,    263,    260,    459,  25640,    776,
               2119,    336,    259,   2220,    259,  18896,    288,   4906,    288,
               1037,   3931,    260,   7083, 101476,   1143,    260,      1]]), 'labels': tensor([[ 7483,   259,  2364, 15695,     1,  -100],
            [  259, 27531, 13483,   259,  7505,     1]]), 'decoder_input_ids': tensor([[    0,  7483,   259,  2364, 15695,     1],
            [    0,   259, 27531, 13483,   259,  7505]])}

La principale chose à remarquer ici est que le premier exemple est plus long que le second, donc les `input_ids` et `attention_mask` du second exemple ont été complétés sur la droite avec un token `[PAD]` (dont l’identifiant est `0`). De même, nous pouvons voir que les `labels` ont été complétés par des `-100`, pour s’assurer que les tokens de remplissage sont ignorés par la fonction de perte. Et enfin, nous pouvons voir un nouveau `decoder_input_ids` qui a déplacé les étiquettes vers la droite en insérant un token `[PAD]` dans la première entrée.

Nous avons enfin tous les ingrédients dont nous avons besoin pour l’entraînement ! Nous devons maintenant simplement instancier le `Seq2SeqTrainer` avec les arguments :

    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,
    )

et lancer notre course d’entraînement :

    trainer.train()

Pendant l’entraînement, vous devriez voir la perte d’entraînement diminuer et les scores ROUGE augmenter à chaque époque. Une fois l’entraînement terminé, vous pouvez voir les scores ROUGE finaux en exécutant `Trainer.evaluate()` :

    trainer.evaluate()

    {'eval_loss': 3.028524398803711,
     'eval_rouge1': 16.9728,
     'eval_rouge2': 8.2969,
     'eval_rougeL': 16.8366,
     'eval_rougeLsum': 16.851,
     'eval_gen_len': 10.1597,
     'eval_runtime': 6.1054,
     'eval_samples_per_second': 38.982,
     'eval_steps_per_second': 4.914}

D’après les scores, nous pouvons voir que notre modèle a largement surpassé notre baseline lead-3. Bien ! La dernière chose à faire est de pousser les poids du modèle vers le Hub, comme suit :

    trainer.push_to_hub(commit_message="Training complete", tags="summarization")

    'https://huggingface.co/huggingface-course/mt5-finetuned-amazon-en-es/commit/aa0536b829b28e73e1e4b94b8a5aacec420d40e0'

Ceci sauvegardera le checkpoint et les fichiers de configuration dans `output_dir`, avant de télécharger tous les fichiers sur le Hub. En spécifiant l’argument `tags`, nous nous assurons également que le widget sur le Hub sera celui d’un pipeline de résumé au lieu de celui de la génération de texte par défaut associé à l’architecture mT5 (pour plus d’informations sur les balises de modèle, voir la documentation du Hub). La sortie de `trainer.push_to_hub()` est une URL vers le hash du commit Git, donc vous pouvez facilement voir les changements qui ont été faits au dépôt de modèle !

Pour conclure cette section, voyons comment nous pouvons également finetuner mT5 en utilisant les fonctionnalités de bas niveau fournies par 🤗 Accelerate.

##
<i> Finetuning </i> de mT5 avec 🤗 <i> Accelerate </i>

Le finetuning de notre modèle avec 🤗 Accelerate est très similaire à l’exemple de classification de texte que nous avons rencontré dans le chapitre 3. Les principales différences seront la nécessité de générer explicitement nos résumés pendant l’entraînement et de définir comment nous calculons les scores ROUGE (rappelons que le `Seq2SeqTrainer` s’est occupé de la génération pour nous). Voyons comment nous pouvons mettre en œuvre ces deux exigences dans 🤗 Accelerate !

###
Préparer tout pour l’entraînement

La première chose que nous devons faire est de créer un `DataLoader` pour chacun de nos échantillons. Puisque les chargeurs de données PyTorch attendent des batchs de tenseurs, nous devons définir le format à `"torch"` dans nos jeux de données :

    tokenized_datasets.set_format("torch")

Maintenant que nous avons des jeux de données constitués uniquement de tenseurs, la prochaine chose à faire est d’instancier à nouveau le `DataCollatorForSeq2Seq`. Pour cela, nous devons fournir une nouvelle version du modèle, donc chargeons-le à nouveau depuis notre cache :

    model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

Nous pouvons ensuite instancier le assembleur de données et l’utiliser pour définir nos chargeurs de données :

    from torch.utils.data import DataLoader

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

La prochaine chose à faire est de définir l’optimiseur que nous voulons utiliser. Comme dans nos autres exemples, nous allons utiliser `AdamW`, qui fonctionne bien pour la plupart des problèmes :

    from torch.optim import AdamW

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

Enfin, nous introduisons notre modèle, notre optimiseur et nos chargeurs de données dans la méthode `accelerator.prepare()` :

    from accelerate import Accelerator

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

> 🚨 Si vous vous entraînez sur un TPU, vous devrez déplacer tout le code ci-dessus dans une fonction d’entraînement dédiée. Voir le chapitre 3 pour plus de détails.

Maintenant que nous avons préparé nos objets, il reste trois choses à faire :

- définir le planificateur du taux d’apprentissage,
- implémenter une fonction pour post-traiter les résumés pour l’évaluation,
- créer un dépôt sur le Hub vers lequel nous pouvons pousser notre modèle.

Pour le planificateur de taux d’apprentissage, nous utiliserons le planificateur linéaire standard des sections précédentes :

    from transformers import get_scheduler

    num_train_epochs = 10
    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,
    )

Pour le post-traitement, nous avons besoin d’une fonction qui divise les résumés générés en phrases séparées par des nouvelles lignes. C’est le format attendu par la métrique ROUGE et nous pouvons y parvenir avec le bout de code suivant :

    def postprocess_text(preds, labels):
        preds = [pred.strip() for pred in preds]
        labels = [label.strip() for label in labels]

        # ROUGE attend une nouvelle ligne après chaque phrase
        preds = ["\n".join(nltk.sent_tokenize(pred)) for pred in preds]
        labels = ["\n".join(nltk.sent_tokenize(label)) for label in labels]

        return preds, labels

Cela devrait vous sembler familier si vous vous rappelez comment nous avons défini la fonction `compute_metrics()` du `Seq2SeqTrainer`.

Enfin, nous devons créer un dépôt de modèles sur le Hub. Pour cela, nous pouvons utiliser la bibliothèque 🤗 Hub, qui porte le nom approprié. Nous avons juste besoin de définir un nom pour notre dépôt, et la bibliothèque a une fonction utilitaire pour combiner l’identifiant du dépôt avec le profil de l’utilisateur :

    from huggingface_hub import get_full_repo_name

    model_name = "test-bert-finetuned-squad-accelerate"
    repo_name = get_full_repo_name(model_name)
    repo_name

    'lewtun/mt5-finetuned-amazon-en-es-accelerate'

Nous pouvons maintenant utiliser ce nom de dépôt pour cloner une version locale dans notre répertoire de résultats qui stockera les artefacts d’entraînement :

    from huggingface_hub import Repository

    output_dir = "results-mt5-finetuned-squad-accelerate"
    repo = Repository(output_dir, clone_from=repo_name)

Cela nous permettra de pousser les artefacts vers le Hub en appelant la méthode `repo.push_to_hub()` pendant l’entraînement ! Concluons maintenant notre analyse en écrivant la boucle d’entraînement.

###
Boucle d’entraînement

La boucle d’entraînement pour le résumé est assez similaire aux autres exemples 🤗 Accelerate que nous avons rencontrés et est grossièrement divisée en quatre étapes principales :

1. entraîner le modèle en itérant sur tous les exemples dans `train_dataloader` pour chaque époque,
2. générer les résumés du modèle à la fin de chaque époque, en générant d’abord les tokens puis en les décodant (ainsi que les résumés de référence) en texte,
3. calculer les scores ROUGE en utilisant les mêmes techniques que nous avons vues précédemment,
4. sauvegarder les checkpoints et pousser le tout vers le Hub. Ici, nous nous appuyons sur l’argument `blocking=False` de l’objet `Repository` afin de pouvoir pousser les checkpoints par époque de manière asynchrone. Cela nous permet de poursuivre l’entraînement sans avoir à attendre le téléchargement quelque peu lent associé à un modèle de la taille d’1 Go !

Ces étapes peuvent être vues dans le bloc de code suivant :

    from tqdm.auto import tqdm
    import torch
    import numpy as np

    progress_bar = tqdm(range(num_training_steps))

    for epoch in range(num_train_epochs):
        # Entraînement
        model.train()
        for step, batch in enumerate(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 step, batch in enumerate(eval_dataloader):
            with torch.no_grad():
                generated_tokens = accelerator.unwrap_model(model).generate(
                    batch["input_ids"],
                    attention_mask=batch["attention_mask"],
                )

                generated_tokens = accelerator.pad_across_processes(
                    generated_tokens, dim=1, pad_index=tokenizer.pad_token_id
                )
                labels = batch["labels"]

                # Si nous n'avons pas rempli la longueur maximale, nous devons également remplir les étiquettes
                labels = accelerator.pad_across_processes(
                    batch["labels"], dim=1, pad_index=tokenizer.pad_token_id
                )

                generated_tokens = accelerator.gather(generated_tokens).cpu().numpy()
                labels = accelerator.gather(labels).cpu().numpy()

                # Remplacer -100 dans les étiquettes car nous ne pouvons pas les décoder
                labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
                if isinstance(generated_tokens, tuple):
                    generated_tokens = generated_tokens[0]
                decoded_preds = tokenizer.batch_decode(
                    generated_tokens, skip_special_tokens=True
                )
                decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

                decoded_preds, decoded_labels = postprocess_text(
                    decoded_preds, decoded_labels
                )

                rouge_score.add_batch(predictions=decoded_preds, references=decoded_labels)

        # Calculer les métriques
        result = rouge_score.compute()
        # Extract the median ROUGE scores
        result = {key: value.mid.fmeasure * 100 for key, value in result.items()}
        result = {k: round(v, 4) for k, v in result.items()}
        print(f"Epoch {epoch}:", result)

        # 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: {'rouge1': 5.6351, 'rouge2': 1.1625, 'rougeL': 5.4866, 'rougeLsum': 5.5005}
    Epoch 1: {'rouge1': 9.8646, 'rouge2': 3.4106, 'rougeL': 9.9439, 'rougeLsum': 9.9306}
    Epoch 2: {'rouge1': 11.0872, 'rouge2': 3.3273, 'rougeL': 11.0508, 'rougeLsum': 10.9468}
    Epoch 3: {'rouge1': 11.8587, 'rouge2': 4.8167, 'rougeL': 11.7986, 'rougeLsum': 11.7518}
    Epoch 4: {'rouge1': 12.9842, 'rouge2': 5.5887, 'rougeL': 12.7546, 'rougeLsum': 12.7029}
    Epoch 5: {'rouge1': 13.4628, 'rouge2': 6.4598, 'rougeL': 13.312, 'rougeLsum': 13.2913}
    Epoch 6: {'rouge1': 12.9131, 'rouge2': 5.8914, 'rougeL': 12.6896, 'rougeLsum': 12.5701}
    Epoch 7: {'rouge1': 13.3079, 'rouge2': 6.2994, 'rougeL': 13.1536, 'rougeLsum': 13.1194}
    Epoch 8: {'rouge1': 13.96, 'rouge2': 6.5998, 'rougeL': 13.9123, 'rougeLsum': 13.7744}
    Epoch 9: {'rouge1': 14.1192, 'rouge2': 7.0059, 'rougeL': 14.1172, 'rougeLsum': 13.9509}

Et c’est tout ! Une fois que vous l’aurez exécuté, vous aurez un modèle et des résultats assez similaires à ceux que nous avons obtenus avec le `Trainer`.

##
Utilisation de votre modèle <i> finetuné </i>

Une fois que vous avez poussé le modèle vers le Hub, vous pouvez jouer avec lui soit via le widget d’inférence, soit avec un objet `pipeline`, comme suit :

    from transformers import pipeline

    hub_model_id = "huggingface-course/mt5-small-finetuned-amazon-en-es"
    summarizer = pipeline("summarization", model=hub_model_id)

Nous pouvons alimenter notre pipeline avec quelques exemples de l’ensemble de test (que le modèle n’a pas vu) pour avoir une idée de la qualité des résumés. Tout d’abord, implémentons une fonction simple pour afficher ensemble la critique, le titre et le résumé généré :

    def print_summary(idx):
        review = books_dataset["test"][idx]["review_body"]
        title = books_dataset["test"][idx]["review_title"]
        summary = summarizer(books_dataset["test"][idx]["review_body"])[0]["summary_text"]
        print(f"'>>> Review: {review}'")
        print(f"\n'>>> Title: {title}'")
        print(f"\n'>>> Summary: {summary}'")

Examinons l’un des exemples anglais que nous recevons :

    print_summary(100)

    '>>> Review: Nothing special at all about this product... the book is too small and stiff and hard to write in. The huge sticker on the back doesn’t come off and looks super tacky. I would not purchase this again. I could have just bought a journal from the dollar store and it would be basically the same thing. It’s also really expensive for what it is.'
    # Ce produit n'a rien de spécial... le livre est trop petit et rigide et il est difficile d'y écrire. L'énorme autocollant au dos ne se détache pas et a l'air super collant. Je n'achèterai plus jamais ce produit. J'aurais pu simplement acheter un journal dans un magasin à un dollar et ce serait à peu près la même chose. Il est également très cher pour ce qu'il est.

    '>>> Title: Not impressed at all... buy something else'
    # Pas du tout impressionné... achetez autre chose.

    '>>> Summary: Nothing special at all about this product'
    # Rien de spécial à propos de ce produit

Ce n’est pas si mal ! Nous pouvons voir que notre modèle a été capable d’effectuer un résumé abstractif en augmentant certaines parties de la critique avec de nouveaux mots. Et peut-être que l’aspect le plus cool de notre modèle est qu’il est bilingue, donc nous pouvons également générer des résumés de critiques en espagnol :

    print_summary(0)

    '>>> Review: Es una trilogia que se hace muy facil de leer. Me ha gustado, no me esperaba el final para nada'
    # C'est une trilogie qui se lit très facilement. J'ai aimé, je ne m'attendais pas du tout à la fin.

    '>>> Title: Buena literatura para adolescentes'
    # Bonne littérature pour les adolescents

    '>>> Summary: Muy facil de leer'
    # Très facile à lire

Le résumé a été extrait directement de la critique. Néanmoins, cela montre la polyvalence du modèle mT5 et vous a donné un aperçu de ce que c’est que de traiter un corpus multilingue !

Ensuite, nous allons nous intéresser à une tâche un peu plus complexe : entraîner un modèle de langue à partir de zéro.

Update on GitHub