Pytorch
TensorFlow

#
Réponse aux questions

Il est temps de s’intéresser à la réponse aux questions ! Cette tâche peut prendre plusieurs formes mais celle sur laquelle nous allons nous concentrer dans cette section est appelée réponse aux questions extractives. Il s’agit de poser des questions sur un document et d’identifier les réponses sous forme de « d’étendue de texte » dans le document lui-même.

Nous allons finetuner un modèle BERT sur le jeu de données SQuAD, qui consiste en des questions posées par des crowdworkers sur un ensemble d’articles de Wikipedia. Cela nous donnera un modèle capable de calculer des prédictions comme celui-ci :

Il s’agit d’une présentation du modèle qui a été entraîné à l’aide du code présenté dans cette section et qui a ensuité été téléchargé sur le Hub. Vous pouvez le trouver ici

💡 Les modèles basé que sur l’encodeur comme BERT ont tendance à être excellents pour extraire les réponses à des questions factuelles comme « Qui a inventé l’architecture Transformer ? » mais ne sont pas très performants lorsqu’on leur pose des questions ouvertes comme « Pourquoi le ciel est-il bleu ? ». Dans ces cas plus difficiles, les modèles encodeurs-décodeurs comme le T5 et BART sont généralement utilisés pour synthétiser les informations d’une manière assez similaire au résumé de texte. Si vous êtes intéressé par ce type de réponse aux questions génératives, nous vous recommandons de consulter notre démo basée sur le jeu de données ELI5.

##
Préparation des données

Le jeu de données le plus utilisé comme référence académique pour la réponse extractive aux questions est SQuAD. C’est donc celui que nous utiliserons ici. Il existe également une version plus difficile SQuAD v2, qui comprend des questions sans réponse. Tant que votre propre jeu de données contient une colonne pour les contextes, une colonne pour les questions et une colonne pour les réponses, vous devriez être en mesure d’adapter les étapes ci-dessous.

###
Le jeu de données SQuAD

Comme d’habitude, nous pouvons télécharger et mettre en cache le jeu de données en une seule étape grâce à `load_dataset()` :

    from datasets import load_dataset

    raw_datasets = load_dataset("squad")

Nous pouvons jeter un coup d’œil à cet objet pour en savoir plus sur le jeu de données SQuAD :

    raw_datasets

    DatasetDict({
        train: Dataset({
            features: ['id', 'title', 'context', 'question', 'answers'],
            num_rows: 87599
        })
        validation: Dataset({
            features: ['id', 'title', 'context', 'question', 'answers'],
            num_rows: 10570
        })
    })

On dirait que nous avons tout ce dont nous avons besoin avec les champs `context`, `question` et `answers`. Affichons-les pour le premier élément de notre ensemble d’entraînement :

    print("Context: ", raw_datasets["train"][0]["context"])
    print("Question: ", raw_datasets["train"][0]["question"])
    print("Answer: ", raw_datasets["train"][0]["answers"])

    Context: 'Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.'
    # Sur le plan architectural, l'école a un caractère catholique. Au sommet du dôme doré du bâtiment principal se trouve une statue dorée de la Vierge Marie. Immédiatement devant le bâtiment principal et face à lui, se trouve une statue en cuivre du Christ, les bras levés, avec la légende "Venite Ad Me Omnes". À côté du bâtiment principal se trouve la basilique du Sacré-Cœur. Immédiatement derrière la basilique se trouve la Grotte, un lieu marial de prière et de réflexion. Il s'agit d'une réplique de la grotte de Lourdes, en France, où la Vierge Marie serait apparue à Sainte Bernadette Soubirous en 1858. Au bout de l'allée principale (et dans une ligne directe qui passe par 3 statues et le Dôme d'or), se trouve une statue de pierre simple et moderne de Marie'.
    Question: 'To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?'
    # A qui la Vierge Marie serait-elle apparue en 1858 à Lourdes, en France ?
    Answer: {'text': ['Saint Bernadette Soubirous'], 'answer_start': [515]}

Les champs `context` et `question` sont très simples à utiliser. Le champ `answers` est un peu plus délicat car il compile un dictionnaire avec deux champs qui sont tous deux des listes. C’est le format qui sera attendu par la métrique `squad` lors de l’évaluation. Si vous utilisez vos propres données, vous n’avez pas nécessairement besoin de vous soucier de mettre les réponses dans le même format. Le champ `text` est assez évident et le champ `answer_start` contient l’indice du caractère de départ de chaque réponse dans le contexte.

Pendant l’entraînement, il n’y a qu’une seule réponse possible. Nous pouvons vérifier cela en utilisant la méthode `Dataset.filter()` :

    raw_datasets["train"].filter(lambda x: len(x["answers"]["text"]) != 1)

    Dataset({
        features: ['id', 'title', 'context', 'question', 'answers'],
        num_rows: 0
    })

Pour l’évaluation, cependant, il existe plusieurs réponses possibles pour chaque échantillon, qui peuvent être identiques ou différentes :

    print(raw_datasets["validation"][0]["answers"])
    print(raw_datasets["validation"][2]["answers"])

    {'text': ['Denver Broncos', 'Denver Broncos', 'Denver Broncos'], 'answer_start': [177, 177, 177]}
    {'text': ['Santa Clara, California', "Levi's Stadium", "Levi's Stadium in the San Francisco Bay Area at Santa Clara, California."], 'answer_start': [403, 355, 355]}

Nous ne nous plongerons pas dans le script d’évaluation car tout sera enveloppé pour nous par une métrique de 🤗 Datasets. La version courte est que certaines des questions ont plusieurs réponses possibles, et ce script va comparer une réponse prédite à toutes les réponses acceptables et prendre le meilleur score. Par exemple, si nous regardons l’échantillon de l’indice 2 :

    print(raw_datasets["validation"][2]["context"])
    print(raw_datasets["validation"][2]["question"])

    'Super Bowl 50 was an American football game to determine the champion of the National Football League (NFL) for the 2015 season. The American Football Conference (AFC) champion Denver Broncos defeated the National Football Conference (NFC) champion Carolina Panthers 24–10 to earn their third Super Bowl title. The game was played on February 7, 2016, at Levi\'s Stadium in the San Francisco Bay Area at Santa Clara, California. As this was the 50th Super Bowl, the league emphasized the "golden anniversary" with various gold-themed initiatives, as well as temporarily suspending the tradition of naming each Super Bowl game with Roman numerals (under which the game would have been known as "Super Bowl L"), so that the logo could prominently feature the Arabic numerals 50.'
    # Le Super Bowl 50 était un match de football américain visant à déterminer le champion de la National Football League (NFL) pour la saison 2015. Les Denver Broncos, champions de la Conférence de football américain (AFC), ont battu les Carolina Panthers, champions de la Conférence nationale de football (NFC), 24 à 10, pour remporter leur troisième titre de Super Bowl. Le match s'est déroulé le 7 février 2016 au Levi\'s Stadium, dans la baie de San Francisco, à Santa Clara, en Californie. Comme il s'agissait du 50e Super Bowl, la ligue a mis l'accent sur l'" anniversaire doré " avec diverses initiatives sur le thème de l'or, ainsi qu'en suspendant temporairement la tradition de nommer chaque match du Super Bowl avec des chiffres romains (en vertu de laquelle le match aurait été appelé " Super Bowl L "), afin que le logo puisse mettre en évidence les chiffres arabes 50.''
    'Where did Super Bowl 50 take place?'
    # Où a eu lieu le Super Bowl 50 ?

nous pouvons voir que la réponse peut effectivement être l’une des trois possibilités que nous avons vues précédemment.

###
Traitement des données d'entraînement

Commençons par le prétraitement des données d’entraînement. La partie la plus difficile est de générer des étiquettes pour la réponse à la question, c’est-à-dire les positions de début et de fin des tokens correspondant à la réponse dans le contexte.

Mais ne nous emballons pas. Tout d’abord, à l’aide d’un tokenizer, nous devons convertir le texte d’entrée en identifiants que le modèle peut comprendre :

    from transformers import AutoTokenizer

    model_checkpoint = "bert-base-cased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

Comme mentionné précédemment, nous allons finetuner un modèle BERT, mais vous pouvez utiliser n’importe quel autre type de modèle tant qu’il a un tokenizer rapide implémenté. Vous pouvez voir toutes les architectures qui sont livrées avec un tokenizer rapide dans ce tableau, et pour vérifier que l’objet `tokenizer` que vous utilisez est bien soutenu par 🤗 Tokenizers vous pouvez regarder son attribut `is_fast` :

    tokenizer.is_fast

    True

Nous pouvons transmettre à notre tokenizer la question et le contexte ensemble. Il insérera correctement les tokens spéciaux pour former une phrase comme celle-ci :

    [CLS] question [SEP] context [SEP]

Vérifions à nouveau :

    context = raw_datasets["train"][0]["context"]
    question = raw_datasets["train"][0]["question"]

    inputs = tokenizer(question, context)
    tokenizer.decode(inputs["input_ids"])

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] Architecturally, '
    'the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin '
    'Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms '
    'upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred '
    'Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a '
    'replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette '
    'Soubirous in 1858. At the end of the main drive ( and in a direct line that connects through 3 statues '
    'and the Gold Dome ), is a simple, modern stone statue of Mary. [SEP]'

    '[CLS] A qui la Vierge Marie serait-elle apparue en 1858 à Lourdes en France ? [SEP] Architecturalement, '
    'l école a un caractère catholique. Au sommet du dôme doré du bâtiment principal se trouve une statue dorée de la Vierge '
    'Marie. Immédiatement devant le bâtiment principal et face à lui, se trouve une statue en cuivre du Christ, les bras '
    'levés avec la légende " Venite Ad Me Omnes ". A côté du bâtiment principal se trouve la basilique du Sacré '
    'Cœur. Immédiatement derrière la basilique se trouve la Grotte, un lieu marial de prière et de réflexion. Il s'agit d'une '
    'réplique de la grotte de Lourdes, en France, où la Vierge Marie serait apparue à Sainte Bernadette '
    'Soubirous en 1858. Au bout de l'allée principale ( et en ligne directe qui passe par 3 statues '
    'et le Dôme d'or), se trouve une statue de Marie en pierre, simple et moderne. [SEP]'

Les étiquettes sont l’index des tokens de début et de fin de la réponse. Le modèle sera chargé de prédire dans l’entrée un logit de début et de fin par token, les étiquettes théoriques étant les suivantes :

Dans ce cas, le contexte n’est pas trop long, mais certains des exemples du jeu de données ont des contextes très longs qui dépasseront la longueur maximale que nous avons fixée (qui est de 384 dans ce cas). Comme nous l’avons vu dans le chapitre 6 lorsque nous avons exploré le pipeline de `question-answering`, nous allons traiter les contextes longs en créant plusieurs caractéristiques d’entraînement à partir d’un échantillon de notre jeu de données et avec une fenêtre glissante entre eux.

Pour voir comment cela fonctionne sur notre exemple, nous pouvons limiter la longueur à 100 et utiliser une fenêtre glissante de 50 tokens. Pour rappel, nous utilisons :

- `max_length` pour définir la longueur maximale (ici 100)
- `truncation="only_second"` pour tronquer le contexte (qui est en deuxième position) quand la question avec son contexte est trop longue
- `stride` pour fixer le nombre de tokens se chevauchant entre deux morceaux successifs (ici 50)
- `return_overflowing_tokens=True` pour indiquer au tokenizer que l’on veut les tokens qui débordent

    inputs = tokenizer(
        question,
        context,
        max_length=100,
        truncation="only_second",
        stride=50,
        return_overflowing_tokens=True,
    )

    for ids in inputs["input_ids"]:
        print(tokenizer.decode(ids))

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basi [SEP]'
    '[CLS] A qui la Vierge Marie serait-elle apparue en 1858 à Lourdes en France ? [SEP] Sur le plan architectural, l école a un caractère catholique. Au sommet du dôme doré du bâtiment principal se trouve une statue dorée de la Vierge Marie. Immédiatement devant le bâtiment principal et face à lui, se trouve une statue en cuivre du Christ, les bras levés, avec la légende " Venite Ad Me Omnes ". À côté du bâtiment principal se trouve la basilique du Sacré-Cœur. Immédiatement derrière la basi [SEP]'

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin [SEP]'
    '[CLS] A qui la Vierge Marie serait-elle apparue en 1858 à Lourdes en France ? [SEP] le bâtiment principal et face à lui, une statue en cuivre du Christ aux bras levés avec la légende " Venite Ad Me Omnes ". À côté du bâtiment principal se trouve la basilique du Sacré-Cœur. Immédiatement derrière la basilique se trouve la Grotte, un lieu marial de prière et de réflexion. Il s agit d'une réplique de la grotte de Lourdes, en France, où la Vierge [SEP]'

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP] Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive ( and in a direct line that connects through 3 [SEP]'
    '[CLS] A qui la Vierge Marie serait-elle apparue en 1858 à Lourdes en France ? [SEP] A côté du bâtiment principal se trouve la basilique du Sacré-Cœur. Immédiatement derrière la basilique se trouve la Grotte, un lieu marial de prière et de réflexion. Il s agit d une réplique de la grotte de Lourdes, en France, où la Vierge Marie serait apparue à Sainte Bernadette Soubirous en 1858. Au bout de l allée principale ( et dans une ligne directe qui relie par 3 [SEP]'

    '[CLS] To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France? [SEP]. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive ( and in a direct line that connects through 3 statues and the Gold Dome ), is a simple, modern stone statue of Mary. [SEP]'
    '[CLS] A qui la Vierge Marie est-elle prétendument apparue en 1858 à Lourdes France ? [SEP]. Il s agit d une réplique de la grotte de Lourdes, en France, où la Vierge Marie serait apparue à Sainte Bernadette Soubirous en 1858. Au bout de l allée principale (et dans une ligne directe qui passe par 3 statues et le Dôme d or), se trouve une simple statue de pierre moderne de Marie. [SEP]'

Comme nous pouvons le voir, notre exemple a été divisé en quatre entrées, chacune d’entre elles contenant la question et une partie du contexte. Notez que la réponse à la question (« Bernadette Soubirous ») n’apparaît que dans la troisième et la dernière entrée. Donc en traitant les longs contextes de cette façon, nous allons créer quelques exemples d’entraînement où la réponse n’est pas incluse dans le contexte. Pour ces exemples, les étiquettes seront `start_position = end_position = 0` (donc nous prédisons le token `[CLS]`). Nous définirons également ces étiquettes dans le cas malheureux où la réponse a été tronquée de sorte que nous n’avons que le début (ou la fin) de celle-ci. Pour les exemples où la réponse est entièrement dans le contexte, les étiquettes seront l’index du token où la réponse commence et l’index du token où la réponse se termine.

Le jeu de données nous fournit le caractère de début de la réponse dans le contexte, et en ajoutant la longueur de la réponse, nous pouvons trouver le caractère de fin dans le contexte. Pour faire correspondre ces indices aux tokens, nous devrons utiliser les correspondances offset que nous avons étudiés au chapitre 6. Nous pouvons faire en sorte que notre tokenizer renvoie ces index en passant `return_offsets_mapping=True` :

    inputs = tokenizer(
        question,
        context,
        max_length=100,
        truncation="only_second",
        stride=50,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
    )
    inputs.keys()

    dict_keys(['input_ids', 'token_type_ids', 'attention_mask', 'offset_mapping', 'overflow_to_sample_mapping'])

Comme nous pouvons le voir, nous récupérons les identifiants d’entrée, les tokens de type identifiant, le masque d’attention, ainsi que la correspondance offset dont nous avions besoin et une clé supplémentaire, `overflow_to_sample_mapping`. La valeur correspondante nous sera utile lorsque nous tokeniserons plusieurs textes en même temps (ce que nous devrions faire pour bénéficier du fait que notre tokenizer est en Rust). Puisqu’un échantillon peut donner plusieurs caractéristiques, il fait correspondre chaque caractéristique à l’exemple d’où elle provient. Parce qu’ici nous avons seulement tokenisé un exemple, nous obtenons une liste de `0` :

    inputs["overflow_to_sample_mapping"]

    [0, 0, 0, 0]

Mais si nous tokenisons davantage d’exemples, cela deviendra plus utile :

    inputs = tokenizer(
        raw_datasets["train"][2:6]["question"],
        raw_datasets["train"][2:6]["context"],
        max_length=100,
        truncation="only_second",
        stride=50,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
    )

    print(f"The 4 examples gave {len(inputs['input_ids'])} features.")
    print(f"Here is where each comes from: {inputs['overflow_to_sample_mapping']}.")

    'The 4 examples gave 19 features.'
    'Here is where each comes from: [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3].'

Comme nous pouvons le voir, les trois premiers exemples (aux indices 2, 3 et 4 de l’ensemble d’entraînement) ont chacun donné quatre caractéristiques et le dernier exemple (à l’indice 5 de l’ensemble d’entraînement) a donné 7 caractéristiques.

Ces informations seront utiles pour associer chaque caractéristique obtenue à son étiquette correspondante. Comme mentionné précédemment, ces étiquettes sont :

- `(0, 0)` si la réponse n’est pas dans l’espace correspondant du contexte.
- `(start_position, end_position)` si la réponse est dans l’espace correspondant du contexte, avec `start_position` étant l’index du token (dans les identifiants d’entrée) au début de la réponse et `end_position` étant l’index du token (dans les identifiants d’entrée) où la réponse se termine.

Pour déterminer ce qui est le cas et, le cas échéant, les positions des tokens, nous trouvons d’abord les indices qui commencent et finissent le contexte dans les identifiants d’entrée. Nous pourrions utiliser les tokens de type identifiants pour le faire, mais puisque ceux-ci n’existent pas nécessairement pour tous les modèles (DistilBERT ne les requiert pas par exemple), nous allons plutôt utiliser la méthode `sequence_ids()` du `BatchEncoding` que notre tokenizer retourne.

Une fois que nous avons ces indices de tokens, nous regardons les offsets correspondants, qui sont des tuples de deux entiers représentant l’étendue des caractères dans le contexte original. Nous pouvons ainsi détecter si le morceau de contexte dans cette fonctionnalité commence après la réponse ou se termine avant que la réponse ne commence (dans ce cas, l’étiquette est `(0, 0)`). Si ce n’est pas le cas, nous bouclons pour trouver le premier et le dernier token de la réponse :

    answers = raw_datasets["train"][2:6]["answers"]
    start_positions = []
    end_positions = []

    for i, offset in enumerate(inputs["offset_mapping"]):
        sample_idx = inputs["overflow_to_sample_mapping"][i]
        answer = answers[sample_idx]
        start_char = answer["answer_start"][0]
        end_char = answer["answer_start"][0] + len(answer["text"][0])
        sequence_ids = inputs.sequence_ids(i)

        # Trouver le début et la fin du contexte
        idx = 0
        while sequence_ids[idx] != 1:
            idx += 1
        context_start = idx
        while sequence_ids[idx] == 1:
            idx += 1
        context_end = idx - 1

        # Si la réponse n'est pas entièrement dans le contexte, l'étiquette est (0, 0)
        if offset[context_start][0] > start_char or offset[context_end][1] < end_char:
            start_positions.append(0)
            end_positions.append(0)
        else:
            # Sinon, ce sont les positions de début et de fin du token
            idx = context_start
            while idx <= context_end and offset[idx][0] <= start_char:
                idx += 1
            start_positions.append(idx - 1)

            idx = context_end
            while idx >= context_start and offset[idx][1] >= end_char:
                idx -= 1
            end_positions.append(idx + 1)

    start_positions, end_positions

    ([83, 51, 19, 0, 0, 64, 27, 0, 34, 0, 0, 0, 67, 34, 0, 0, 0, 0, 0],
     [85, 53, 21, 0, 0, 70, 33, 0, 40, 0, 0, 0, 68, 35, 0, 0, 0, 0, 0])

Jetons un coup d’œil à quelques résultats pour vérifier que notre approche est correcte. Pour la première caractéristique, nous trouvons `(83, 85)` comme étiquettes. Comparons alors la réponse théorique avec l’étendue décodée des tokens de 83 à 85 (inclus) :

    idx = 0
    sample_idx = inputs["overflow_to_sample_mapping"][idx]
    answer = answers[sample_idx]["text"][0]

    start = start_positions[idx]
    end = end_positions[idx]
    labeled_answer = tokenizer.decode(inputs["input_ids"][idx][start : end + 1])

    print(f"Theoretical answer: {answer}, labels give: {labeled_answer}")

    'Theoretical answer: the Main Building, labels give: the Main Building'

Cela correspond ! Maintenant vérifions l’index 4, où nous avons mis les étiquettes à `(0, 0)`, signifiant que la réponse n’est pas dans le morceau de contexte de cette caractéristique :

    idx = 4
    sample_idx = inputs["overflow_to_sample_mapping"][idx]
    answer = answers[sample_idx]["text"][0]

    decoded_example = tokenizer.decode(inputs["input_ids"][idx])
    print(f"Theoretical answer: {answer}, decoded example: {decoded_example}")

    'Theoretical answer: a Marian place of prayer and reflection, decoded example: [CLS] What is the Grotto at Notre Dame? [SEP] Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend " Venite Ad Me Omnes ". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grot [SEP]'

En effet, nous ne voyons pas la réponse dans le contexte.

✏️ A votre tour ! En utilisant l’architecture XLNet, le padding est appliqué à gauche et la question et le contexte sont intervertis. Adaptez tout le code que nous venons de voir à l’architecture XLNet (et ajoutez `padding=True`). Soyez conscient que le token `[CLS]` peut ne pas être à la position 0 avec le padding appliqué.

Maintenant que nous avons vu étape par étape comment prétraiter nos données d’entraînement, nous pouvons les regrouper dans une fonction que nous appliquerons à l’ensemble des données d’entraînement. Nous allons rembourrer chaque caractéristique à la longueur maximale que nous avons définie, car la plupart des contextes seront longs (et les échantillons correspondants seront divisés en plusieurs caractéristiques). Il n’y a donc pas de réel avantage à appliquer un rembourrage dynamique ici :

    max_length = 384
    stride = 128

    def preprocess_training_examples(examples):
        questions = [q.strip() for q in examples["question"]]
        inputs = tokenizer(
            questions,
            examples["context"],
            max_length=max_length,
            truncation="only_second",
            stride=stride,
            return_overflowing_tokens=True,
            return_offsets_mapping=True,
            padding="max_length",
        )

        offset_mapping = inputs.pop("offset_mapping")
        sample_map = inputs.pop("overflow_to_sample_mapping")
        answers = examples["answers"]
        start_positions = []
        end_positions = []

        for i, offset in enumerate(offset_mapping):
            sample_idx = sample_map[i]
            answer = answers[sample_idx]
            start_char = answer["answer_start"][0]
            end_char = answer["answer_start"][0] + len(answer["text"][0])
            sequence_ids = inputs.sequence_ids(i)

            # Trouver le début et la fin du contexte
            idx = 0
            while sequence_ids[idx] != 1:
                idx += 1
            context_start = idx
            while sequence_ids[idx] == 1:
                idx += 1
            context_end = idx - 1

            # Si la réponse n'est pas entièrement dans le contexte, l'étiquette est (0, 0)
            if offset[context_start][0] > start_char or offset[context_end][1] < end_char:
                start_positions.append(0)
                end_positions.append(0)
            else:
                # Sinon, ce sont les positions de début et de fin du token
                idx = context_start
                while idx <= context_end and offset[idx][0] <= start_char:
                    idx += 1
                start_positions.append(idx - 1)

                idx = context_end
                while idx >= context_start and offset[idx][1] >= end_char:
                    idx -= 1
                end_positions.append(idx + 1)

        inputs["start_positions"] = start_positions
        inputs["end_positions"] = end_positions
        return inputs

Notez que nous avons défini deux constantes pour déterminer la longueur maximale utilisée ainsi que la longueur de la fenêtre glissante, et que nous avons ajouté un petit nettoyage avant la tokénisation : certaines des questions dans SQuAD ont des espaces supplémentaires au début et à la fin qui n’ajoutent rien (et prennent de la place lors de la tokénisation si vous utilisez un modèle comme RoBERTa), donc nous avons supprimé ces espaces supplémentaires.

Pour appliquer cette fonction à l’ensemble de l’entraînement, nous utilisons la méthode `Dataset.map()` avec le flag `batched=True`. C’est nécessaire ici car nous changeons la longueur du jeu de données (puisqu’un exemple peut donner plusieurs caractéristiques d’entraînement) :

    train_dataset = raw_datasets["train"].map(
        preprocess_training_examples,
        batched=True,
        remove_columns=raw_datasets["train"].column_names,
    )
    len(raw_datasets["train"]), len(train_dataset)

    (87599, 88729)

Comme nous pouvons le voir, le prétraitement a ajouté environ 1 000 caractéristiques. Notre ensemble d’entraînement est maintenant prêt à être utilisé. Passons au prétraitement de l’ensemble de validation !

###
Traitement des données de validation

Le prétraitement des données de validation sera légèrement plus facile car nous n’avons pas besoin de générer des étiquettes (sauf si nous voulons calculer une perte de validation, mais elle ne nous aidera pas vraiment à comprendre la qualité du modèle). Le réel plaisir sera d’interpréter les prédictions du modèle dans des étendues du contexte original. Pour cela, il nous suffit de stocker les correspondances d’offset et un moyen de faire correspondre chaque caractéristique créée à l’exemple original dont elle provient. Puisqu’il y a une colonne identifiant dans le jeu de données original, nous l’utiliserons.

La seule chose que nous allons ajouter ici est un petit nettoyage des correspondances d’offset. Elles contiendront les offsets pour la question et le contexte, mais une fois que nous serons à la phase de post-traitement, nous n’aurons aucun moyen de savoir quelle partie des identifiants d’entrée correspondait au contexte et quelle partie était la question (la méthode `sequence_ids()` que nous avons utilisée n’est disponible que pour la sortie du tokenizer). Donc, nous allons mettre les offsets correspondant à la question à `None` :

    def preprocess_validation_examples(examples):
        questions = [q.strip() for q in examples["question"]]
        inputs = tokenizer(
            questions,
            examples["context"],
            max_length=max_length,
            truncation="only_second",
            stride=stride,
            return_overflowing_tokens=True,
            return_offsets_mapping=True,
            padding="max_length",
        )

        sample_map = inputs.pop("overflow_to_sample_mapping")
        example_ids = []

        for i in range(len(inputs["input_ids"])):
            sample_idx = sample_map[i]
            example_ids.append(examples["id"][sample_idx])

            sequence_ids = inputs.sequence_ids(i)
            offset = inputs["offset_mapping"][i]
            inputs["offset_mapping"][i] = [
                o if sequence_ids[k] == 1 else None for k, o in enumerate(offset)
            ]

        inputs["example_id"] = example_ids
        return inputs

Nous pouvons appliquer cette fonction sur l’ensemble de validation comme précédemment :

    validation_dataset = raw_datasets["validation"].map(
        preprocess_validation_examples,
        batched=True,
        remove_columns=raw_datasets["validation"].column_names,
    )
    len(raw_datasets["validation"]), len(validation_dataset)

    (10570, 10822)

Dans ce cas, nous n’avons ajouté que quelques centaines d’échantillons, il semble donc que les contextes dans l’ensemble de validation soient un peu plus courts.

Maintenant que nous avons prétraité toutes les données, nous pouvons passer à l’entraînement.

##
Finetuner le modèle avec l'API `Trainer`

Le code d’entraînement pour cet exemple ressemblera beaucoup au code des sections précédentes mais le calcul de la métrique avec la fonction `compute_metrics()` sera un défi unique. Puisque nous avons rembourré tous les échantillons à la longueur maximale que nous avons définie, il n’y a pas d’assembleur de données à définir. Ainsi le calcul de la métrique est vraiment la seule chose dont nous devons nous soucier. La partie la plus difficile sera de post-traiter les prédictions du modèle en étendues de texte dans les exemples originaux. Une fois que nous aurons fait cela, la métrique de la bibliothèque 🤗 Datasets fera le gros du travail pour nous.

###
Post-traitement

Le modèle produira des logits pour les positions de début et de fin de la réponse dans les identifiants d’entrée, comme nous l’avons vu lors de notre exploration du pipeline de `question-answering` au chapitre 6. L’étape de post-traitement sera similaire à ce que nous avons fait à ce chapitre là. Voici un rapide rappel des actions que nous avons prises :

- nous avons masqué les logits de début et de fin correspondant aux tokens en dehors du contexte,
- nous avons ensuite converti les logits de début et de fin en probabilités en utilisant une fonction SoftMax,
- nous avons attribué un score à chaque paire `(start_token, end_token)` en prenant le produit des deux probabilités correspondantes,
- nous avons cherché la paire avec le score maximum qui donnait une réponse valide (par exemple, un `start_token` inférieur au `end_token`).

Ici, nous allons modifier légèrement ce processus car nous n’avons pas besoin de calculer les scores réels (juste la réponse prédite). Cela signifie que nous pouvons sauter l’étape de la SoftMax. Pour aller plus vite, nous ne donnerons pas non plus un score à toutes les paires `(start_token, end_token)` possibles, mais seulement celles correspondant aux `n_best` logits les plus élevés (avec `n_best=20`). Puisque nous sautons la SoftMax, les scores seront des scores logi, et seront obtenus en prenant la somme des logits de début et de fin (au lieu du produit, à cause de la règle log⁡(a b)=log⁡(a)+log⁡(b)\log(ab) = \log(a) + \log(b)log(ab)=log(a)+log(b)).

Pour démontrer tout cela, nous aurons besoin d’un certain type de prédictions. Puisque nous n’avons pas encore entraîné notre modèle, nous allons utiliser le modèle par défaut du pipeline de `question-answering` pour générer quelques prédictions sur une petite partie de l’ensemble de validation. Nous pouvons utiliser la même fonction de traitement que précédemment car elle repose sur la constante globale `tokenizer`, nous devons juste changer cet objet par le tokenizer du modèle que nous voulons utiliser temporairement :

    small_eval_set = raw_datasets["validation"].select(range(100))
    trained_checkpoint = "distilbert-base-cased-distilled-squad"

    tokenizer = AutoTokenizer.from_pretrained(trained_checkpoint)
    eval_set = small_eval_set.map(
        preprocess_validation_examples,
        batched=True,
        remove_columns=raw_datasets["validation"].column_names,
    )

Maintenant que le prétraitement est terminé, nous changeons le tokenizer pour celui que nous avons choisi à l’origine :

    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

Nous supprimons ensuite les colonnes de notre `eval_set` qui ne sont pas attendues par le modèle. Nous construisons un batch avec tout de ce petit ensemble de validation et le passons au modèle. Si un GPU est disponible, nous l’utilisons pour aller plus vite :

    import torch
    from transformers import AutoModelForQuestionAnswering

    eval_set_for_model = eval_set.remove_columns(["example_id", "offset_mapping"])
    eval_set_for_model.set_format("torch")

    device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
    batch = {k: eval_set_for_model[k].to(device) for k in eval_set_for_model.column_names}
    trained_model = AutoModelForQuestionAnswering.from_pretrained(trained_checkpoint).to(
        device
    )

    with torch.no_grad():
        outputs = trained_model(**batch)

Puisque `Trainer` nous donne les prédictions sous forme de tableaux NumPy, nous récupérons les logits de début et de fin et les convertissons dans ce format :

    start_logits = outputs.start_logits.cpu().numpy()
    end_logits = outputs.end_logits.cpu().numpy()

Maintenant, nous devons trouver la réponse prédite pour chaque exemple dans notre `small_eval_set`. Un exemple peut avoir été divisé en plusieurs caractéristiques dans `eval_set`, donc la première étape est de faire correspondre chaque exemple dans `small_eval_set` aux caractéristiques correspondantes dans `eval_set` :

    import collections

    example_to_features = collections.defaultdict(list)
    for idx, feature in enumerate(eval_set):
        example_to_features[feature["example_id"]].append(idx)

Avec cela, nous pouvons vraiment nous mettre au travail en bouclant tous les exemples et, pour chaque exemple, toutes les caractéristiques associées. Comme nous l’avons dit précédemment, nous allons regarder les scores logit pour les `n_best` logits de début et logits de fin, en excluant les positions qui donnent :

- une réponse qui ne serait pas dans le contexte
- une réponse avec une longueur négative
- une réponse qui est trop longue (nous limitons les possibilités à `max_answer_length=30`)

Une fois que nous avons toutes les réponses possibles notées pour un exemple, nous choisissons simplement celle qui a le meilleur score logit :

    import numpy as np

    n_best = 20
    max_answer_length = 30
    predicted_answers = []

    for example in small_eval_set:
        example_id = example["id"]
        context = example["context"]
        answers = []

        for feature_index in example_to_features[example_id]:
            start_logit = start_logits[feature_index]
            end_logit = end_logits[feature_index]
            offsets = eval_set["offset_mapping"][feature_index]

            start_indexes = np.argsort(start_logit)[-1 : -n_best - 1 : -1].tolist()
            end_indexes = np.argsort(end_logit)[-1 : -n_best - 1 : -1].tolist()
            for start_index in start_indexes:
                for end_index in end_indexes:
                    # Ignore les réponses qui ne sont pas entièrement dans le contexte
                    if offsets[start_index] is None or offsets[end_index] is None:
                        continue
                    # Ignore les réponses dont la longueur est soit < 0 soit > max_answer_length
                    if (
                        end_index < start_index
                        or end_index - start_index + 1 > max_answer_length
                    ):
                        continue

                    answers.append(
                        {
                            "text": context[offsets[start_index][0] : offsets[end_index][1]],
                            "logit_score": start_logit[start_index] + end_logit[end_index],
                        }
                    )

        best_answer = max(answers, key=lambda x: x["logit_score"])
        predicted_answers.append({"id": example_id, "prediction_text": best_answer["text"]})

Le format final des réponses prédites est celui qui sera attendu par la métrique que nous allons utiliser. Comme d’habitude, nous pouvons la charger à l’aide de la bibliothèque 🤗 Evaluate :

    import evaluate

    metric = evaluate.load("squad")

Cette métrique attend les réponses prédites dans le format que nous avons vu ci-dessus (une liste de dictionnaires avec une clé pour l’identifiant de l’exemple et une clé pour le texte prédit) et les réponses théoriques dans le format ci-dessous (une liste de dictionnaires avec une clé pour l’identifiant de l’exemple et une clé pour les réponses possibles) :

    theoretical_answers = [
        {"id": ex["id"], "answers": ex["answers"]} for ex in small_eval_set
    ]

Nous pouvons maintenant vérifier que nous obtenons des résultats raisonnables en examinant le premier élément des deux listes :

    print(predicted_answers[0])
    print(theoretical_answers[0])

    {'id': '56be4db0acb8001400a502ec', 'prediction_text': 'Denver Broncos'}
    {'id': '56be4db0acb8001400a502ec', 'answers': {'text': ['Denver Broncos', 'Denver Broncos', 'Denver Broncos'], 'answer_start': [177, 177, 177]}}

Pas trop mal ! Voyons maintenant le score que la métrique nous donne :

    metric.compute(predictions=predicted_answers, references=theoretical_answers)

    {'exact_match': 83.0, 'f1': 88.25}

Encore une fois, c’est plutôt bon si l’on considère que, d’après le papier de DistilBERT, finetuné sur SQuAD, ce modèle obtient 79,1 et 86,9 pour ces scores sur l’ensemble du jeu de données.

Maintenant, mettons tout ce que nous venons de faire dans une fonction `compute_metrics()` que nous utiliserons dans le `Trainer`. Normalement, cette fonction `compute_metrics()` reçoit seulement un tuple `eval_preds` avec les logits et les étiquettes. Ici, nous aurons besoin d’un peu plus, car nous devons chercher dans le jeu de données des caractéristiques pour le décalage et dans le jeu de données des exemples pour les contextes originaux. Ainsi nous ne serons pas en mesure d’utiliser cette fonction pour obtenir des résultats d’évaluation standards pendant l’entraînement. Nous ne l’utiliserons qu’à la fin de l’entraînement pour vérifier les résultats.

La fonction `compute_metrics()` regroupe les mêmes étapes que précédemment. Nous ajoutons juste une petite vérification au cas où nous ne trouverions aucune réponse valide (dans ce cas nous prédisons une chaîne vide).

    from tqdm.auto import tqdm

    def compute_metrics(start_logits, end_logits, features, examples):
        example_to_features = collections.defaultdict(list)
        for idx, feature in enumerate(features):
            example_to_features[feature["example_id"]].append(idx)

        predicted_answers = []
        for example in tqdm(examples):
            example_id = example["id"]
            context = example["context"]
            answers = []

            # Parcourir en boucle toutes les fonctionnalités associées à cet exemple
            for feature_index in example_to_features[example_id]:
                start_logit = start_logits[feature_index]
                end_logit = end_logits[feature_index]
                offsets = features[feature_index]["offset_mapping"]

                start_indexes = np.argsort(start_logit)[-1 : -n_best - 1 : -1].tolist()
                end_indexes = np.argsort(end_logit)[-1 : -n_best - 1 : -1].tolist()
                for start_index in start_indexes:
                    for end_index in end_indexes:
                        # Ignore les réponses qui ne sont pas entièrement dans le contexte
                        if offsets[start_index] is None or offsets[end_index] is None:
                            continue
                        # Ignore les réponses dont la longueur est soit < 0, soit > max_answer_length
                        if (
                            end_index < start_index
                            or end_index - start_index + 1 > max_answer_length
                        ):
                            continue

                        answer = {
                            "text": context[offsets[start_index][0] : offsets[end_index][1]],
                            "logit_score": start_logit[start_index] + end_logit[end_index],
                        }
                        answers.append(answer)

            # Sélectionne la réponse avec le meilleur score
            if len(answers) > 0:
                best_answer = max(answers, key=lambda x: x["logit_score"])
                predicted_answers.append(
                    {"id": example_id, "prediction_text": best_answer["text"]}
                )
            else:
                predicted_answers.append({"id": example_id, "prediction_text": ""})

        theoretical_answers = [{"id": ex["id"], "answers": ex["answers"]} for ex in examples]
        return metric.compute(predictions=predicted_answers, references=theoretical_answers)

Nous pouvons vérifier que cela fonctionne sur nos prédictions :

    compute_metrics(start_logits, end_logits, eval_set, small_eval_set)

    {'exact_match': 83.0, 'f1': 88.25}

C’est bien ! Maintenant, utilisons ceci pour finetuner notre modèle.

###
Finetuning du modèle

Nous sommes maintenant prêts à entraîner notre modèle. Créons-le en utilisant la classe `AutoModelForQuestionAnswering` comme précédemment :

    model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

Comme d’habitude, nous recevons un avertissement indiquant que certains poids ne sont pas utilisés (ceux de la tête de pré-entraînement) et que d’autres sont initialisés de manière aléatoire (ceux de la tête de réponse aux questions). Vous devriez être habitué à cela maintenant, mais cela signifie que ce modèle n’est pas encore prêt à être utilisé et qu’il a besoin d’être finetuné. Une bonne chose que nous soyons sur le point de le faire !

Pour pouvoir pousser notre modèle vers le Hub, nous devons nous connecter à Hugging Face. Si vous exécutez ce code dans un notebook, vous pouvez le faire avec la fonction utilitaire suivante, qui affiche un widget où vous pouvez entrer vos identifiants de connexion :

    from huggingface_hub import notebook_login

    notebook_login()

Si vous ne travaillez pas dans un notebook, tapez simplement la ligne suivante dans votre terminal :

    huggingface-cli login

Une fois ceci fait, nous pouvons définir nos `TrainingArguments`. Comme nous l’avons dit lorsque nous avons défini notre fonction pour calculer la métrique, nous ne serons pas en mesure d’avoir une boucle d’évaluation standard à cause de la signature de la fonction `compute_metrics()`. Nous pourrions écrire notre propre sous-classe de `Trainer` pour faire cela (une approche que vous pouvez trouver dans le script d’exemple de réponse aux questions), mais c’est un peu trop long pour cette section. A la place, nous n’évaluerons le modèle qu’à la fin de l’entraînement et nous vous montrerons comment faire une évaluation cela dans le paragraphe « Une boucle d’entraînement personnalisée » ci-dessous.

C’est là que l’API `Trainer` montre ses limites et que la bibliothèque 🤗 Accelerate brille : personnaliser la classe pour un cas d’utilisation spécifique peut être pénible, mais modifier une boucle d’entraînement est facile.

Jetons un coup d’œil à notre `TrainingArguments` :

    from transformers import TrainingArguments

    args = TrainingArguments(
        "bert-finetuned-squad",
        evaluation_strategy="no",
        save_strategy="epoch",
        learning_rate=2e-5,
        num_train_epochs=3,
        weight_decay=0.01,
        fp16=True,
        push_to_hub=True,
    )

Nous avons déjà vu la plupart d’entre eux. Nous définissons quelques hyperparamètres (comme le taux d’apprentissage, le nombre d’époques d’entraînement, un taux de décroissance des poids) et nous indiquons que nous voulons sauvegarder le modèle à la fin de chaque époque, sauter l’évaluation, et télécharger nos résultats vers le Hub. Nous activons également l’entraînement en précision mixte avec `fp16=True`, car cela peut accélérer l’entraînement sur un GPU récent.

Par défaut, le dépôt utilisé sera dans votre espace et nommé après le répertoire de sortie que vous avez défini. Donc dans notre cas il sera dans `"sgugger/bert-finetuned-squad"`. Nous pouvons passer outre en passant un `hub_model_id`, par exemple, pour pousser le modèle dans l’organisation `huggingface_course` nous avons utilisé `hub_model_id= "huggingface_course/bert-finetuned-squad"` (qui est le modèle que nous avons lié au début de cette section).

💡 Si le répertoire de sortie que vous utilisez existe, il doit être un clone local du dépôt vers lequel vous voulez pousser (donc définissez un nouveau nom si vous obtenez une erreur lors de la définition de votre `Trainer`).

Enfin, nous passons tout à la classe `Trainer` et lançons l’entraînement :

    from transformers import Trainer

    trainer = Trainer(
        model=model,
        args=args,
        train_dataset=train_dataset,
        eval_dataset=validation_dataset,
        tokenizer=tokenizer,
    )
    trainer.train()

Notez que pendant l’entraînement, chaque fois que le modèle est sauvegardé (ici, à chaque époque), il est téléchargé sur le Hub en arrière-plan. Ainsi, vous pourrez reprendre votre entraînement sur une autre machine si nécessaire. L’ensemble de l’entraînement prend un certain temps (un peu plus d’une heure sur une Titan RTX), vous pouvez donc prendre un café ou relire les parties du cours qui vous ont semblé plus difficiles pendant qu’il se déroule. Notez également que dès que la première époque est terminée, vous verrez des poids téléchargés sur le Hub et vous pourrez commencer à jouer avec votre modèle sur sa page.

Une fois l’entraînement terminé, nous pouvons enfin évaluer notre modèle (et prier pour ne pas avoir dépensé tout ce temps de calcul pour rien). La méthode `predict()` du `Trainer` retournera un tuple où les premiers éléments seront les prédictions du modèle (ici une paire avec les logits de début et de fin). Nous envoyons ceci à notre fonction `compute_metrics()` :

    predictions, _ = trainer.predict(validation_dataset)
    start_logits, end_logits = predictions
    compute_metrics(start_logits, end_logits, validation_dataset, raw_datasets["validation"])

    {'exact_match': 81.18259224219489, 'f1': 88.67381321905516}

Super ! À titre de comparaison, les scores indiqués dans l’article de BERT pour ce tâche sont de 80,8 et 88,5. Donc nous sommes exactement là où nous devrions être.

Enfin, nous utilisons la méthode `push_to_hub()` pour nous assurer que nous téléchargeons la dernière version du modèle :

    trainer.push_to_hub(commit_message="Training complete")

Cela renvoie l’URL du commit qu’il vient de faire, si vous voulez l’inspecter :

    'https://huggingface.co/sgugger/bert-finetuned-squad/commit/9dcee1fbc25946a6ed4bb32efb1bd71d5fa90b68'

Le `Trainer` rédige également une carte de modèle avec tous les résultats de l’évaluation et la télécharge.

À ce stade, vous pouvez utiliser le widget d’inférence sur le Hub du modèle pour tester le modèle et le partager avec vos amis, votre famille et vos animaux préférés. Vous avez réussi à finetuner un modèle sur une tâche de réponse à une question. Félicitations !

✏️ A votre tour Essayez un autre modèle pour voir s’il est plus performant pour cette tâche !

Si vous voulez plonger un peu plus profondément dans la boucle d’entraînement, nous allons maintenant vous montrer comment faire la même chose en utilisant 🤗 Accelerate.

##
Une boucle d'entraînement personnalisée

Jetons maintenant un coup d’œil à la boucle d’entraînement complète, afin que vous puissiez facilement personnaliser les parties dont vous avez besoin. Elle ressemblera beaucoup à la boucle d’entraînement du chapitre 3, à l’exception de la boucle d’évaluation. Nous serons en mesure d’évaluer le modèle régulièrement puisque nous ne sommes plus contraints par la classe `Trainer`.

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

Tout d’abord, nous devons construire le `DataLoader`s à partir de nos jeux de données. Nous définissons le format de ces jeux de données à `"torch"` et supprimons les colonnes dans le jeu de validation qui ne sont pas utilisées par le modèle. Ensuite, nous pouvons utiliser le `default_data_collator` fourni par 🤗 Transformers comme `collate_fn` et mélanger l’ensemble d’entraînement mais pas celui de validation :

    from torch.utils.data import DataLoader
    from transformers import default_data_collator

    train_dataset.set_format("torch")
    validation_set = validation_dataset.remove_columns(["example_id", "offset_mapping"])
    validation_set.set_format("torch")

    train_dataloader = DataLoader(
        train_dataset,
        shuffle=True,
        collate_fn=default_data_collator,
        batch_size=8,
    )
    eval_dataloader = DataLoader(
        validation_set, collate_fn=default_data_collator, batch_size=8
    )

Ensuite, nous réinstantifions notre modèle afin de nous assurer que nous ne poursuivons pas le finetuning précédent et que nous repartons du modèle BERT pré-entraîné :

    model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

Ensuite, nous aurons besoin d’un optimiseur. Comme d’habitude, nous utilisons le classique `AdamW`, qui est comme Adam mais avec une correction dans la façon dont le taux de décroissance des poids est appliqué :

    from torch.optim import AdamW

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

Une fois que nous avons tous ces objets, nous pouvons les envoyer à la méthode `accelerator.prepare()`. Rappelez-vous que si vous voulez entraîner sur des TPUs dans un notebook Colab, vous devrez déplacer tout ce code dans une fonction d’entraînement, et qui ne devrait pas exécuter une cellule qui instancie un `Accelerator`. Nous pouvons forcer l’entraînement en précision mixte en passant l’argument `fp16=True` à `Accelerator` (ou, si vous exécutez le code comme un script, assurez-vous de remplir la 🤗 Accelerate `config` de manière appropriée).

    from accelerate import Accelerator

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

Comme vous devez le savoir depuis les sections précédentes, nous ne pouvons utiliser la longueur de `train_dataloader` pour calculer le nombre d’étapes d’entraînement qu’après qu’il soit passé par la méthode `accelerator.prepare()`. Nous utilisons le même programme linéaire que dans les sections précédentes :

    from transformers import get_scheduler

    num_train_epochs = 3
    num_update_steps_per_epoch = len(train_dataloader)
    num_training_steps = num_train_epochs * num_update_steps_per_epoch

    lr_scheduler = get_scheduler(
        "linear",
        optimizer=optimizer,
        num_warmup_steps=0,
        num_training_steps=num_training_steps,
    )

Pour pousser notre modèle vers le Hub, nous aurons besoin de créer un objet `Repository` dans un dossier de travail. Tout d’abord, connectez-vous au Hub, si vous n’êtes pas déjà connecté. Nous déterminerons le nom du dépôt à partir de l’identifiant du modèle que nous voulons donner à notre modèle (n’hésitez pas à remplacer le `repo_name` par votre propre choix. Il doit juste contenir votre nom d’utilisateur, ce que fait la fonction `get_full_repo_name()`) :

    from huggingface_hub import Repository, get_full_repo_name

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

    'sgugger/bert-finetuned-squad-accelerate'

Ensuite, nous pouvons cloner ce dépôt dans un dossier local. S’il existe déjà, ce dossier local doit être un clone du dépôt avec lequel nous travaillons :

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

Nous pouvons maintenant télécharger tout ce que nous sauvegardons dans `output_dir` en appelant la méthode `repo.push_to_hub()`. Cela nous aidera à télécharger les modèles intermédiaires à la fin de chaque époque.

##
Boucle d'entraînement

Nous sommes maintenant prêts à écrire la boucle d’entraînement complète. Après avoir défini une barre de progression pour suivre l’évolution de l’entraînement, la boucle comporte trois parties :

- l’entraînement à proprement dit, qui est l’itération classique sur le `train_dataloader`, passage en avant du modèle, puis passage en arrière et étape d’optimisation.
- l’évaluation, dans laquelle nous rassemblons toutes les valeurs pour `start_logits` et `end_logits` avant de les convertir en tableaux NumPy. Une fois la boucle d’évaluation terminée, nous concaténons tous les résultats. Notez que nous devons tronquer car `Accelerator` peut avoir ajouté quelques échantillons à la fin pour s’assurer que nous avons le même nombre d’exemples dans chaque processus.
- sauvegarde et téléchargement, où nous sauvegardons d’abord le modèle et le tokenizer, puis appelons `repo.push_to_hub()`. Comme nous l’avons fait auparavant, nous utilisons l’argument `blocking=False` pour dire à la bibliothèque 🤗 Hub de pousser dans un processus asynchrone. De cette façon, l’entraînement continue normalement et cette (longue) instruction est exécutée en arrière-plan.

Voici le code complet de la boucle d’entraînement :

    from tqdm.auto import tqdm
    import torch

    progress_bar = tqdm(range(num_training_steps))

    for epoch in range(num_train_epochs):
        # Entraînement
        model.train()
        for 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()
        start_logits = []
        end_logits = []
        accelerator.print("Evaluation!")
        for batch in tqdm(eval_dataloader):
            with torch.no_grad():
                outputs = model(**batch)

            start_logits.append(accelerator.gather(outputs.start_logits).cpu().numpy())
            end_logits.append(accelerator.gather(outputs.end_logits).cpu().numpy())

        start_logits = np.concatenate(start_logits)
        end_logits = np.concatenate(end_logits)
        start_logits = start_logits[: len(validation_dataset)]
        end_logits = end_logits[: len(validation_dataset)]

        metrics = compute_metrics(
            start_logits, end_logits, validation_dataset, raw_datasets["validation"]
        )
        print(f"epoch {epoch}:", metrics)

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

Au cas où ce serait la première fois que vous verriez un modèle enregistré avec 🤗 Accelerate, prenons un moment pour inspecter les trois lignes de code qui l’accompagnent :

    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)

La première ligne est explicite : elle indique à tous les processus d’attendre que tout le monde soit à ce stade avant de continuer. C’est pour s’assurer que nous avons le même modèle dans chaque processus avant de sauvegarder. Ensuite, nous prenons le `unwrapped_model`, qui est le modèle de base que nous avons défini. La méthode `accelerator.prepare()` modifie le modèle pour qu’il fonctionne dans l’entraînement distribué. Donc il n’aura plus la méthode `save_pretrained()` car la méthode `accelerator.unwrap_model()` annule cette étape. Enfin, nous appelons `save_pretrained()` mais nous disons à cette méthode d’utiliser `accelerator.save()` au lieu de `torch.save()`.

Une fois ceci fait, vous devriez avoir un modèle qui produit des résultats assez similaires à celui entraîné avec `Trainer`. Vous pouvez vérifier le modèle que nous avons entraîné en utilisant ce code à huggingface-course/bert-finetuned-squad-accelerate. Et si vous voulez tester des modifications de la boucle d’entraînement, vous pouvez les implémenter directement en modifiant le code ci-dessus !

###
Utilisation du modèle finetuné

Nous vous avons déjà montré comment vous pouvez utiliser le modèle que nous avons finetuné sur le Hub avec le widget d’inférence. Pour l’utiliser localement dans un `pipeline`, il suffit de spécifier l’identifiant du modèle :

    from transformers import pipeline

    # Remplacez par votre propre checkpoint
    model_checkpoint = "huggingface-course/bert-finetuned-squad"
    question_answerer = pipeline("question-answering", model=model_checkpoint)

    context = """
    🤗 Transformers is backed by the three most popular deep learning libraries — Jax, PyTorch and TensorFlow — with a seamless integration
    between them. It's straightforward to train your models with one before loading them for inference with the other.
    """
    question = "Which deep learning libraries back 🤗 Transformers?"
    question_answerer(question=question, context=context)

    {'score': 0.9979003071784973,
     'start': 78,
     'end': 105,
     'answer': 'Jax, PyTorch and TensorFlow'}

Super ! Notre modèle fonctionne aussi bien que le modèle par défaut pour ce pipeline !