View file src/colab/camembert.py - Download

# -*- coding: utf-8 -*-
"""camembert.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/12yCLlBU7ZA7WilCeBpl7kogXwLKMJetS

Hands-on CamemBERT: Une Introduction au Modèle CamemBERT

Benjamin Muller, Nathan Godey, Roman Castagné

Jul 6, 2022

link : https://camembert-model.fr/posts/tutorial/

La première étape est l’installation et l’importation des librairies utilisées dans la suite de l’atelier. Certaines librairies (torch, numpy, sklearn, …) sont pré-installées dans l’environnement de Google Colab, nous n’avons donc pas besoin de nous en occuper.
"""

!pip install transformers
!pip install plotly==5.8.0
!pip install pyyaml==5.4.1
!pip install datasets
!pip install pytorch-lightning

from pprint import pprint
import functools

import torch
from torch.utils.data import DataLoader
import torch.nn.functional as F
import pytorch_lightning as pl
from transformers import AutoModelForSequenceClassification, CamembertForMaskedLM, AutoTokenizer, AutoConfig
from datasets import load_dataset
from sklearn.metrics import confusion_matrix, f1_score

import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from tqdm.notebook import tqdm

camembert = CamembertForMaskedLM.from_pretrained('camembert-base')

"""L’architecture de Camembert: le Transformer

Le Transformer est une architecture deep learning introduite en 2017 dans l’article Attention Is All You Need (https://arxiv.org/pdf/1706.03762.pdf). Il permet de traiter des séquences dont les éléments sont fortement inter-dépendants, comme c’est le cas pour les mots d’une phrase par exemple.

Faire des Prédictions avec CamemBERT en mode modèle de langue

Avant d’utiliser le modèle CamemBERT pour la classification de phrases, nous allons étudier les différentes étapes qui permettent de passer d’une phrase (sous la forme d’une chaîne de caractères) à une prédiction.

Le modèle que nous avons téléchargé est pré-entraîné avec une tâche de Masked Language Modelling : certains mots ou sous-mots de la séquence sont masqués et on demande au modèle de les prédire à partir du contexte (les mots non-masqués).

Considérons un exemple simple dans lequel nous voulons compléter 3 phrases dans lesquelles nous avons masqué un mot.

La première étape consiste à tokeniser les chaînes de caractères. Il s’agit de découper les phrases d’entrée en mots et sous-mots (appelés tokens) issus d’un vocabulaire extrait à l’aide de l’algorithme Sentencepiece (article de blog pour plus de détails : https://towardsdatascience.com/byte-pair-encoding-subword-based-tokenization-algorithm-77828a70bee0).

Chaque token est ensuite identifié à l’aide d’un entier correspondant à sa position dans le vocabulaire (input_ids) et un masque indique sur quels tokens l’attention doit se porter dans chaque phrase (attention_mask).
"""

batch_sentences = [
    "Vous savez où est la  la plus proche?",
    "La Seine est un .",
    "Je cherche urgemment un endroit où retirer de l'.",
]

tokenizer = AutoTokenizer.from_pretrained('camembert-base')

# tokenizer_output = tokenizer(
#     batch_sentences
# )

tokenizer_output = tokenizer(
    batch_sentences,
    padding="max_length",
    truncation=True,
    return_tensors="pt"
)

pprint(tokenizer_output, width=150)

# pprint([tokenizer.convert_ids_to_tokens(input_ids) for input_ids in tokenizer_output['input_ids']], width=150)

"""Calculons donc les probabilités issues du modèle à l’aide de la méthode softmax de Pytorch:"""

with torch.no_grad():
    model_output = camembert(**tokenizer_output, output_hidden_states=True)
    model_output

print(model_output.logits.shape) # 3 phrases, 512 tokens (après tokenization + padding), 32005 tokens possibles

def get_probas_from_logits(logits):
    return logits.softmax(-1)


def visualize_mlm_predictions(tokenizer_output, model_output, tokenizer, nb_candidates=10):
    # Decode the tokenized sentences and clean-up the special tokens
    decoded_tokenized_sents = [sent.replace('', '').replace('', ' ') for sent in tokenizer.batch_decode(tokenizer_output.input_ids)]

    # Retrieve the probas at the masked positions
    masked_tokens_mask = tokenizer_output.input_ids == tokenizer.mask_token_id
    batch_mask_probas = get_probas_from_logits(model_output.logits[masked_tokens_mask])

    for sentence, mask_probas in zip(decoded_tokenized_sents, batch_mask_probas):
        # Get top probas and plot them
        top_probas, top_token_ids = mask_probas.topk(nb_candidates, -1)
        top_tokens = tokenizer.convert_ids_to_tokens(top_token_ids)
        bar_chart = px.bar({"tokens": top_tokens[::-1], "probas": list(top_probas)[::-1]},
                        x="probas", y="tokens", orientation='h', title=sentence, width=800)
        bar_chart.show(config={'staticPlot': True})

visualize_mlm_predictions(tokenizer_output, model_output, tokenizer)

"""Représentation de phrases avec Camembert

Comment représenter des phrases ?

Nous avons vu dans la première partie comment CamemBERT est capable de traiter du langage naturel au niveau des mots (ou plus précisément des tokens). Nous allons maintenant voir comment il est possible de traiter le langage naturel au niveau des phrases à l’aide du même modèle.

Pour rappel, CamemBERT associe à chaque token un embedding - un vecteur en haute dimension, ici 768 - dépendant notamment du contexte dans lequel ce token se trouve. Ces embeddings sont en réalité les vecteurs pris en entrée de la couche lm_head, que l’on peut obtenir ainsi:
"""

token_embeddings = model_output.hidden_states[-1]

print(token_embeddings.shape)

"""Comment pouvons-nous représenter une phrase (c’est-à-dire une séquence de tokens) à l’aide de ces embeddings de tokens ?

Deux solutions sont habituellement retenues:

- Représenter la phrase en extrayant l’embedding du premier token, qui dans le cas de CamemBERT est toujours le token \
- Représenter la phrase en réalisant la moyenne des embeddings de tous les tokens de la séquence

Ces deux méthodes sont implémentées dans la cellule suivante:
"""

def take_first_embedding(embeddings, attention_mask=None):
    return embeddings[:, 0]

def average_embeddings(embeddings, attention_mask):
    return (attention_mask[..., None] * embeddings).mean(1)

"""Nous pouvons donc récupérer les représentations pour chacune des méthodes, et vérifier que les tenseurs ont bien la bonne forme en sortie, correspondant à batch_size x hidden_size (un vecteur par phrase dans le batch):"""

first_tok_sentence_representations = take_first_embedding(token_embeddings, tokenizer_output.attention_mask)
avg_sentence_representations = average_embeddings(token_embeddings, tokenizer_output.attention_mask)

first_tok_sentence_representations.shape, avg_sentence_representations.shape

"""Il est possible de mesurer la similarité entre ces deux représentations à l’aide de n’importe quelle distance (euclidienne, absolue, …), mais c’est généralement la cosine-similarity qui est retenue.

Cette similarité peut donner une idée de la similarité sémantique et/ou linguistique entre deux phrases, mais sa fiabilité n’est pas absolue.

Sa valeur, comprise entre -1 et 1, renseigne sur l’écart angulaire entre les deux embeddings. Nous pouvons l’utiliser pour comparer deux à deux nos embeddings issus du batch:
"""

for sent_id_1, sent_id_2 in [[0, 1], [2, 1], [2, 0]]:
    first_tok_similarity_score = F.cosine_similarity(first_tok_sentence_representations[sent_id_1], first_tok_sentence_representations[sent_id_2], dim = -1)
    avg_similarity_score = F.cosine_similarity(avg_sentence_representations[sent_id_1], avg_sentence_representations[sent_id_2], dim = -1)

    print(f"{batch_sentences[sent_id_1]}    vs.    {batch_sentences[sent_id_2]}")
    print(f"Score (first_tok) : {first_tok_similarity_score}")
    print(f"Score (average) : {avg_similarity_score}\n")

"""On remarque que l’approche consistant à choisir l’embedding du premier token pour représenter une phrase ne permet pas de différencier les niveaux de similarité entre les phrases. Pour l’inférence, on préférera donc faire la moyenne des embeddings, qui donne une plus grande similarité entre les phrases les plus proches sémantiquement.

Application à des messages de chats

Maintenant que nous avons pris en main le modèle CamemBERT, nous allons voir comment l’utiliser sur des données réelles.

Pour cet atelier, nous allons utiliser le split français du dataset MIAM, qui regroupe des messages extraits d’une plateforme de chat, accompagnés de labels décrivant une intention propre au message.

Ce dataset est disponible sur HuggingFace, et on peut le télécharger grâce à la fonction load_dataset:
"""

dataset = load_dataset("miam", "loria")
dataset

"""Il est possible de convertir ce dataset en pandas.DataFrame:


"""

pd_dataset = {split_name: split_data.to_pandas() for split_name, split_data in dataset.items()}
pd_dataset["validation"]

"""Explorons ce dataset. Dans un premier temps, on peut étudier la répartition des labels:"""

nb_labels = len(pd_dataset["train"]["Label"].unique())
print(f"Le dataset comprend {nb_labels} labels.")

ax = pd_dataset["train"]["Label"].hist(density=True, bins=nb_labels+1)
ax.set_xlabel("Label ID")
ax.set_ylabel("Fréquence")
ax.set_title("Répartition des labels dans le dataset MIAM (train split)")
ax.figure.show()

"""Ce dataset est donc assez déséquilibré. On peut ensuite s’intéresser à la longueur des chaînes de caractères et vérifier qu’elles sont adaptées à l’utilisation de CamemBERT:

"""

pd_dataset["train"]["len_utt"] = pd_dataset["train"]["Utterance"].apply(lambda x: len(x))
ax = pd_dataset["train"]["len_utt"].hist(density=True, bins=50)
ax.set_xlabel("Longueur")
ax.set_ylabel("Fréquence")
ax.set_title("Nombre de caractères par phrase")
ax.figure.show()

"""Notamment, aucune phrase ne comporte plus de 512 caractères, et donc aucune phrase ne comportera plus de 512 tokens:


"""

print((pd_dataset["train"]["len_utt"] > 512).any())

"""Utilisons maintenant CamemBERT afin d’obtenir une représentation vectorielle de chacun des messages! Pour cela, nous allons utiliser le DataLoader de PyTorch dans lequel nous pouvons tokenizer les messages grâce à une collate_fn:"""

def tokenize_batch(samples, tokenizer):
    text = [sample["Utterance"] for sample in samples]
    labels = torch.tensor([sample["Label"] for sample in samples])
    str_labels = [sample["Dialogue_Act"] for sample in samples]
    # The tokenizer handles
    # - Tokenization (amazing right?)
    # - Padding (adding empty tokens so that each example has the same length)
    # - Truncation (cutting samples that are too long)
    # - Special tokens (in CamemBERT, each sentence ends with a special token )
    # - Attention mask (a binary vector which tells the model which tokens to look at. For instance it will not compute anything if the token is a padding token)
    tokens = tokenizer(text, padding="longest", return_tensors="pt")

    return {"input_ids": tokens.input_ids, "attention_mask": tokens.attention_mask, "labels": labels, "str_labels": str_labels, "sentences": text}

"""Récupérons les trois splits du dataset, qui nous seront utiles dans la seconde partie de l’atelier.

Nous pouvons maintenant créer un DataLoader pour l’ensemble de validation sur lequel nous allons travailler:
"""

train_dataset, val_dataset, test_dataset = dataset.values()

val_dataloader = DataLoader(val_dataset, collate_fn=functools.partial(tokenize_batch, tokenizer=tokenizer), batch_size=16)
next(iter(val_dataloader))

"""Le DataLoader récupère des batchs de 16 phrases avec leurs labels, les tokenise, et renvoie un dictionnaire contenant tout le nécéssaire pour CamemBERT.

Nous allons itérer dans ce DataLoader afin de récupérer une représentation pour chacune des phrases du dataset:
"""

sentences = []
labels = []
str_labels = []
all_representations = torch.Tensor()

with torch.no_grad():
    for tokenized_batch in tqdm(val_dataloader):
        model_output = camembert(
            input_ids = tokenized_batch["input_ids"],
            attention_mask = tokenized_batch["attention_mask"],
            output_hidden_states=True
        )
        batch_representations = average_embeddings(model_output["hidden_states"][-1], tokenized_batch["attention_mask"])
        sentences.extend(tokenized_batch["sentences"])
        labels.extend(tokenized_batch["labels"])
        str_labels.extend(tokenized_batch["str_labels"])
        all_representations = torch.cat((all_representations, batch_representations), 0)

"""L’inférence a pris un peu plus de 3 minutes. C’est long, n’est-ce pas ? C’est normal: nous n’avons pas utilisé le GPU mais le CPU pour réaliser l’inférence sur ces 59 batchs!

Pour utiliser le GPU, il suffit d’utiliser la méthode .cuda() sur les tenseurs et le modèle afin de transférer les poids sur cet accélérateur matériel:
"""

camembert = camembert.cuda()

sentences = []
labels = []
str_labels = []
all_representations = torch.tensor([], device='cuda')

with torch.no_grad():
    for tokenized_batch in tqdm(val_dataloader):
        model_output = camembert(
            input_ids = tokenized_batch["input_ids"].cuda(),
            attention_mask = tokenized_batch["attention_mask"].cuda(),
            output_hidden_states=True
        )
        batch_representations = average_embeddings(model_output["hidden_states"][-1], tokenized_batch["attention_mask"].cuda())
        sentences.extend(tokenized_batch["sentences"])
        labels.extend(tokenized_batch["labels"])
        str_labels.extend(tokenized_batch["str_labels"])
        all_representations = torch.cat((all_representations, batch_representations), 0)

"""Avec le GPU, l’inférence dure 4 secondes: voilà qui est mieux! Il est maintenant temps de visualiser ces représentations et de voir si des clusters apparaissent naturellement.

Nous pourrons notamment voir si les représentations de CamemBERT nous permettent d’attribuer directement le label souhaité aux messages.

Pour cela, commençons par projeter les représentations en deux dimensions à l’aide d’un TSNE.

Nous pouvons maintenant représenter nos phrases en deux dimensions, et associer à l’aide d’une couleur le label correspondant:

"""

from sklearn.manifold import TSNE

tsne = TSNE()
all_representations_2d = tsne.fit_transform(all_representations.cpu())

scatter_plot = px.scatter(x=all_representations_2d[:, 0], y=all_representations_2d[:, 1], color=str_labels)
scatter_plot.show(config={'staticPlot': True})

"""On distingue quelques groupes (ou clusters), mais on se rend bien compte que les représentations du modèle ne permettent pas de classifier correctement les messages selon les labels définis.

Pour découvrir comment finetuner un modèle “général” comme CamemBERT sur cette tâche de classification, rendez-vous dans la deuxième partie (https://camembert-model.fr/posts/tutorial_part2/) de ce tutoriel.
Benjamin Muller
PhD student

Doctorant dans l’équipe ALMAnaCH d’Inria.
"""