Pytorch
TensorFlow

#
Derrière le pipeline

Il s'agit de la première section dont le contenu est légèrement différent selon que vous utilisez PyTorch ou TensorFlow. Cliquez sur le bouton situé au-dessus du titre pour sélectionner la plateforme que vous préférez !

Commençons par un exemple complet en regardant ce qui s’est passé en coulisses lorsque nous avons exécuté le code suivant dans le chapitre 1 :

    from transformers import pipeline

    classifier = pipeline("sentiment-analysis")
    classifier(
        [
            "I've been waiting for a HuggingFace course my whole life.",
            # J'ai attendu un cours de HuggingFace toute ma vie.
            "I hate this so much!",  # Je déteste tellement ça !
        ]
    )

la sortie :

    [{'label': 'POSITIVE', 'score': 0.9598047137260437},
     {'label': 'NEGATIVE', 'score': 0.9994558095932007}]

Comme nous l’avons vu dans le chapitre 1, ce pipeline regroupe trois étapes : le prétraitement, le passage des entrées dans le modèle et le post-traitement.

Passons rapidement en revue chacun de ces éléments.

##
Prétraitement avec un <i> tokenizer </i>

Comme d’autres réseaux de neurones, les transformers ne peuvent pas traiter directement le texte brut, donc la première étape de notre pipeline est de convertir les entrées textuelles en nombres afin que le modèle puisse les comprendre. Pour ce faire, nous utilisons un tokenizer, qui sera responsable de :

- diviser l’entrée en mots, sous-mots, ou symboles (comme la ponctuation) qui sont appelés tokens,
- associer chaque token à un nombre entier,
- ajouter des entrées supplémentaires qui peuvent être utiles au modèle.

Tout ce prétraitement doit être effectué exactement de la même manière que celui appliqué lors du pré-entraînement du modèle. Nous devons donc d’abord télécharger ces informations depuis le Hub. Pour ce faire, nous utilisons la classe `AutoTokenizer` et sa méthode `from_pretrained()`. En utilisant le nom du checkpoint de notre modèle, elle va automatiquement récupérer les données associées au tokenizer du modèle et les mettre en cache (afin qu’elles ne soient téléchargées que la première fois que vous exécutez le code ci-dessous).

Puisque le checkpoint par défaut du pipeline `sentiment-analysis` (analyse de sentiment) est `distilbert-base-uncased-finetuned-sst-2-english` (vous pouvez voir la carte de ce modèle ici), nous exécutons ce qui suit :

    from transformers import AutoTokenizer

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)

Une fois que nous avons le tokenizer nous pouvons lui passer directement nos phrases et obtenir un dictionnaire prêt à être donné à notre modèle ! La seule chose qui reste à faire est de convertir en tenseurs la liste des identifiants d’entrée.

Vous pouvez utiliser 🤗 Transformers sans avoir à vous soucier du framework utilisé comme backend. Il peut s’agir de PyTorch, de TensorFlow ou de Flax pour certains modèles. Cependant, les transformers n’acceptent que les tenseurs en entrée. Si c’est la première fois que vous entendez parler de tenseurs, vous pouvez les considérer comme des tableaux NumPy. Un tableau NumPy peut être un scalaire (0D), un vecteur (1D), une matrice (2D), ou avoir davantage de dimensions. Les tenseurs des autres frameworks d’apprentissage machine se comportent de manière similaire et sont généralement aussi simples à instancier que les tableaux NumPy.

Pour spécifier le type de tenseurs que nous voulons récupérer (PyTorch, TensorFlow, ou simplement NumPy), nous utilisons l’argument `return_tensors` :

    raw_inputs = [
        "I've been waiting for a HuggingFace course my whole life.",
        # J'ai attendu un cours de HuggingFace toute ma vie.
        "I hate this so much!",  # Je déteste tellement ça !
    ]
    inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
    print(inputs)

Ne vous préoccupez pas encore du remplissage (padding) et de la troncature, nous les expliquerons plus tard. Les principales choses à retenir ici sont que vous pouvez passer une phrase ou une liste de phrases, ainsi que spécifier le type de tenseurs que vous voulez récupérer (si aucun type n’est passé, par défaut vous obtiendrez une liste de listes comme résultat).

Voici à quoi ressemblent les résultats sous forme de tenseurs PyTorch :

    {
        'input_ids': tensor([
            [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172, 2607,  2026,  2878,  2166,  1012,   102],
            [  101,  1045,  5223,  2023,  2061,  2172,   999,   102,     0,     0,     0,     0,     0,     0,     0,     0]
        ]),
        '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, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        ])
    }

La sortie elle-même est un dictionnaire contenant deux clés : `input_ids` et `attention_mask`. `input_ids` contient deux lignes d’entiers (une pour chaque phrase) qui sont les identifiants uniques des tokens dans chaque phrase. Nous expliquerons ce qu’est l’`attention_mask` plus tard dans ce chapitre.

##
Passage au modèle

Nous pouvons télécharger notre modèle pré-entraîné de la même manière que nous l’avons fait avec notre tokenizer. 🤗 Transformers fournit une classe `AutoModel` qui possède également une méthode `from_pretrained()` :

    from transformers import AutoModel

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModel.from_pretrained(checkpoint)

Dans cet extrait de code, nous avons téléchargé le même checkpoint que nous avons utilisé dans notre pipeline auparavant (il devrait en fait avoir déjà été mis en cache) et instancié un modèle avec lui.

Cette architecture ne contient que le module de transformer de base : étant donné certaines entrées, il produit ce que nous appellerons des états cachés, également connus sous le nom de caractéristiques. Pour chaque entrée du modèle, nous récupérons un vecteur en grande dimension représentant la compréhension contextuelle de cette entrée par le transformer.

Si cela ne fait pas sens, ne vous inquiétez pas. Nous expliquons tout plus tard.

Bien que ces états cachés puissent être utiles en eux-mêmes, ils sont généralement les entrées d’une autre partie du modèle, connue sous le nom de tête. Dans le chapitre 1, les différentes tâches auraient pu être réalisées avec la même architecture mais en ayant chacune d’elles une tête différente.

###
Un vecteur de grande dimension ?

Le vecteur produit en sortie par le transformer est généralement de grande dimension. Il a généralement trois dimensions :

- la taille du lot : le nombre de séquences traitées à la fois (2 dans notre exemple),
- la longueur de la séquence : la longueur de la représentation numérique de la séquence (16 dans notre exemple),
- la taille cachée : la dimension du vecteur de chaque entrée du modèle.

On dit qu’il est de « grande dimension » en raison de la dernière valeur. La taille cachée peut être très grande (généralement 768 pour les petits modèles et pour les grands modèles cela peut atteindre 3072 voire plus).

Nous pouvons le constater si nous alimentons notre modèle avec les entrées que nous avons prétraitées :

    outputs = model(**inputs)
    print(outputs.last_hidden_state.shape)

    torch.Size([2, 16, 768])

Notez que les sorties des modèles de la bibliothèque 🤗 Transformers se comportent comme des `namedtuples` ou des dictionnaires. Vous pouvez accéder aux éléments par attributs (comme nous l’avons fait), par clé (`outputs["last_hidden_state"]`), ou même par l’index si vous savez exactement où se trouve la chose que vous cherchez (`outputs[0]`).

###
Les têtes des modèles : donner du sens aux chiffres

Les têtes des modèles prennent en entrée le vecteur de grande dimension des états cachés et le projettent sur une autre dimension. Elles sont généralement composées d’une ou de quelques couches linéaires :

La sortie du transformer est envoyée directement à la tête du modèle pour être traitée. Dans ce diagramme, le modèle est représenté par sa couche d’enchâssement et les couches suivantes. La couche d’enchâssement convertit chaque identifiant d’entrée dans l’entrée tokenisée en un vecteur qui représente le token associé. Les couches suivantes manipulent ces vecteurs en utilisant le mécanisme d’attention pour produire la représentation finale des phrases. Il existe de nombreuses architectures différentes disponibles dans la bibliothèque 🤗 Transformers, chacune étant conçue autour de la prise en charge d’une tâche spécifique. En voici une liste non exhaustive :

- `*Model` (récupérer les états cachés)
- `*ForCausalLM`
- `*ForMaskedLM`
- `*ForMultipleChoice`
- `*ForQuestionAnswering`
- `*ForSequenceClassification`
- `*ForTokenClassification`
- et autres 🤗

Pour notre exemple, nous avons besoin d’un modèle avec une tête de classification de séquence (pour pouvoir classer les phrases comme positives ou négatives). Donc, nous n’utilisons pas réellement la classe `AutoModel` mais plutôt `AutoModelForSequenceClassification` :

    from transformers import AutoModelForSequenceClassification

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
    outputs = model(**inputs)

Maintenant, si nous examinons la forme de nos entrées, la dimensionnalité est beaucoup plus faible. La tête du modèle prend en entrée les vecteurs de grande dimension que nous avons vus précédemment et elle produit des vecteurs contenant deux valeurs (une par étiquette) :

    print(outputs.logits.shape)

    torch.Size([2, 2])

Comme nous n’avons que deux phrases et deux étiquettes, le résultat que nous obtenons est de forme 2 x 2

##
Post-traitement de la sortie

Les valeurs que nous obtenons en sortie de notre modèle n’ont pas nécessairement de sens en elles-mêmes. Jetons-y un coup d’œil :

    print(outputs.logits)

    tensor([[-1.5607,  1.6123],
            [ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

Notre modèle a prédit `[-1.5607, 1.6123]` pour la première phrase et `[ 4.1692, -3.3464]` pour la seconde. Ce ne sont pas des probabilités mais des logits, les scores bruts, non normalisés, produits par la dernière couche du modèle. Pour être convertis en probabilités, ils doivent passer par une couche SoftMax (tous les modèles de la bibliothèque 🤗 Transformers sortent les logits car la fonction de perte de l’entraînement fusionne généralement la dernière fonction d’activation, comme la SoftMax, avec la fonction de perte réelle, comme l’entropie croisée) :

    import torch

    predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
    print(predictions)

    tensor([[4.0195e-02, 9.5980e-01],
            [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

Maintenant nous pouvons voir que le modèle a prédit `[0.0402, 0.9598]` pour la première phrase et `[0.9995, 0.0005]` pour la seconde. Ce sont des scores de probabilité reconnaissables.

Pour obtenir les étiquettes correspondant à chaque position, nous pouvons inspecter l’attribut `id2label` de la configuration du modèle (plus de détails dans la section suivante) :

    model.config.id2label

    {0: 'NEGATIVE', 1: 'POSITIVE'}

Nous pouvons maintenant conclure que le modèle a prédit ce qui suit :

- première phrase : NEGATIVE: 0.0402, POSITIVE: 0.9598
- deuxième phrase : NEGATIVE: 0.9995, POSITIVE: 0.0005

Nous avons reproduit avec succès les trois étapes du pipeline : prétraitement avec les tokenizers, passage des entrées dans le modèle et post-traitement ! Prenons maintenant le temps de nous plonger plus profondément dans chacune de ces étapes.

✏️ Essayez ! Choisissez deux (ou plus) textes de votre choix (en anglais) et faites-les passer par le pipeline `sentiment-analysis`. Reproduisez ensuite vous-même les étapes vues ici et vérifiez que vous obtenez les mêmes résultats !