camembert

Analyse de sentiments avec CamemBERT

L’analyse de sentiments est l’une des applications qu’on rencontre couramment en NLP. En effet, pour beaucoup de business, il est important de connaître l’avis des clients/utilisateurs. Un outil qui permet de déterminer si un produit ou service est plutôt bien vu ou plutôt mal vu, de façon automatique en se basant sur du texte écrit en langage naturel, est forcément une bonne idée. Dans cet article, je vous propose d’implémenter un système d’analyse de sentiments en utilisant le modèle « cousin » de BERT CamemBERT.

Qu’allons-nous faire exactement ?

Le but de cet article est de donner un exemple d’implémentation de modèles de NLP (modèle de classification dans notre cas) basé sur un modèle pré-entraîné type « BERT ».

Nous allons utiliser un jeu de données qui contient des commentaires laissés par les internautes sur Allociné. Pour simplifier les choses, nous n’avons utilisé que les commentaires qui sont soit positifs soit négatifs (pas de commentaire neutre). Notre objectif sera d’entraîner un modèle qui sera capable de prédire la polarité (caractère positif ou négatif) d’un commentaire. Notre modèle ne sera rien de plus qu’un « fine-tuning » de CamemBERT.

Qu’est-ce que CamemBERT ?

CamemBERT est une « version » de RoBERTa pré-entraînée sur un jeu de données francophone. RoBERTa lui-même est une version de BERT pour laquelle, certains hyperparamètres du pré-entraînement ont été modifiés et l’objectif de prédiction de phrase suivante (Next-Sentence Prediction) a été supprimé. CamemBERT hérite donc des avantages de BERT.

Pour l’implémentation de notre modèle d’analyse de sentiments, nous utiliserons la bibliothèque Transformers de HuggingFace ansi que PyTorch.

Implémentation du modèle d’analyse de sentiments

Architecture du modèle

L’architecture de notre modèle est simple. Il s’agit d’un fine-tuning de CamemBERT. C’est-à-dire qu’on va juste ajouter un réseau Feed-Forward et un Softmax à la sortie de CamemBERT.

Architecture du fine-tuning de CamemBERT
Architecture du fine-tuning de CamemBERT

Bibliothèques

Dans notre code nous utiliserons les bibliothèques Python suivantes :

  • PyTorch : On va utiliser la célèbre bibliothèque de deep learning pour l’entraînement de notre modèle.
  • Transformers : Cette bibliothèque permet de télécharger des versions de camemBERT pré-entraîné. Elle offre même un modèle de classification avec CamemBERT tout-fait.
  • Pandas : Pour charger notre jeu de données CSV.
  • Scikit-Learn : Pour évaluer notre modèle grâce aux fonctions de son module metrics.

Nos imports ressemblent donc à ça :

import torch
import seaborn
import pandas as pd
from sklearn import metrics
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
from transformers import CamembertForSequenceClassification, CamembertTokenizer, AdamW

Jeu de données

Le jeu de données est constitué de reviews de films ressemblant à ça :

Aperçu du jeu de données

Les labels sont donc 1 pour un commentaire positif et 0 pour un commentaire négatif.

Encodage du texte

Pour pouvoir passer nos données au modèle, il va falloir qu’elles soient encodées. Encoder, c’est donner une représentation numérique (en vrai vectorielle) au texte exactement comme le fait un embedding classique. La bibliothèque Transformers offre une fonction d’encodage qui nous facilite la vie :

# Chargement du jeu de donnees
dataset = pd.read_csv("reviews_allocine_classification.csv")

reviews = dataset['review'].values.tolist()
sentiments = dataset['sentiment'].values.tolist()

# On charge l'objet "tokenizer"de camemBERT qui va servir a encoder
# 'camebert-base' est la version de camembert qu'on choisit d'utiliser
# 'do_lower_case' à True pour qu'on passe tout en miniscule
TOKENIZER = CamembertTokenizer.from_pretrained(
    'camembert-base',
    do_lower_case=True)

# La fonction batch_encode_plus encode un batch de donnees
encoded_batch = TOKENIZER.batch_encode_plus(reviews,
                                            add_special_tokens=True,
                                            max_length=MAX_LENGTH,
                                            padding=True,
                                            truncation=True,
                                            return_attention_mask = True,
                                            return_tensors = 'pt')

# On transforme la liste des sentiments en tenseur
sentiments = torch.tensor(sentiments)

# On calcule l'indice qui va delimiter nos datasets d'entrainement et de validation
# On utilise 80% du jeu de donnée pour l'entrainement et les 20% restant pour la validation
split_border = int(len(sentiments)*0.8)


train_dataset = TensorDataset(
    encoded_batch['input_ids'][:split_border],
    encoded_batch['attention_mask'][:split_border],
    sentiments[:split_border])
validation_dataset = TensorDataset(
    encoded_batch['input_ids'][split_border:],
    encoded_batch['attention_mask'][split_border:],
    sentiments[split_border:])


batch_size = 32

# On cree les DataLoaders d'entrainement et de validation
# Le dataloader est juste un objet iterable
# On le configure pour iterer le jeu d'entrainement de façon aleatoire et creer les batchs.
train_dataloader = DataLoader(
            train_dataset,
            sampler = RandomSampler(train_dataset),
            batch_size = batch_size)

validation_dataloader = DataLoader(
            validation_dataset,
            sampler = SequentialSampler(validation_dataset),
            batch_size = batch_size)

Chargement du modèle

Cela se fait en une ligne de code grâce à Transformers qui a déjà implémenté le fine-tuning pour nous :

# On la version pre-entrainee de camemBERT 'base'
model = CamembertForSequenceClassification.from_pretrained(
    'camembert-base',
    num_labels = 2)

Hyperparamètres

On utilise l’Adam optimizer avec les paramètres par défaut et on entraîne sur 3 époques.

optimizer = AdamW(model.parameters(),
                  lr = 2e-5, # Learning Rate
                  eps = 1e-8 # Epsilon)
epochs = 3

Entraînement

# On va stocker nos tensors sur mon cpu : je n'ai pas mieux
device = torch.device("cpu")

# Pour enregistrer les stats a chaque epoque
training_stats = []

# Boucle d'entrainement
for epoch in range(0, epochs):
    
    print("")
    print(f'########## Epoch {epoch+1} / {epochs} ##########')
    print('Training...')


    # On initialise la loss pour cette epoque
    total_train_loss = 0

    # On met le modele en mode 'training'
    # Dans ce mode certaines couches du modele agissent differement
    model.train()

    # Pour chaque batch
    for step, batch in enumerate(train_dataloader):

        # On fait un print chaque 40 batchs
        if step % 40 == 0 and not step == 0:
            print(f'  Batch {step}  of 
{len(train_dataloader)}.')
        
        # On recupere les donnees du batch
        input_id = batch[0].to(device)
        attention_mask = batch[1].to(device)
        sentiment = batch[2].to(device)

        # On met le gradient a 0
        model.zero_grad()        

        # On passe la donnee au model et on recupere la loss et le logits (sortie avant fonction d'activation)
        loss, logits = model(input_id, 
                             token_type_ids=None, 
                             attention_mask=attention_mask, 
                             labels=sentiment)

        # On incremente la loss totale
        # .item() donne la valeur numerique de la loss
        total_train_loss += loss.item()

        # Backpropagtion
        loss.backward()

        # On actualise les parametrer grace a l'optimizer
        optimizer.step()

    # On calcule la  loss moyenne sur toute l'epoque
    avg_train_loss = total_train_loss / len(train_dataloader)   

    print("")
    print("  Average training loss: {0:.2f}".format(avg_train_loss))  
    
    # Enregistrement des stats de l'epoque
    training_stats.append(
        {
            'epoch': epoch + 1,
            'Training Loss': avg_train_loss,
        }
    )

print("Model saved!")
torch.save(model.state_dict(), "./sentiments.pt")

Voilà! Notre modèle sera entraîné après quelques minutes (ou quelques heures tout dépend de la puissance de la machine et de la taille du jeu de données). On peut le tester sur un autre jeu de données et voir s’il est assez précis pour aller en production ;-).

Évaluation

On va évaluer notre modèle sur un jeu de données de test nous basant sur le F1-score et la matrice de confusion.

def preprocess(raw_reviews, sentiments=None):
    encoded_batch = TOKENIZER.batch_encode_plus(raw_reviews,
                                                truncation=True,
                                                pad_to_max_length=True,
                                                return_attention_mask=True,
                                                return_tensors = 'pt')
    if sentiments:
        sentiments = torch.tensor(sentiments)
        return encoded_batch['input_ids'], encoded_batch['attention_mask'], sentiments
    return encoded_batch['input_ids'], encoded_batch['attention_mask']

def predict(reviews, model=model):
    with torch.no_grad():
        model.eval()
        input_ids, attention_mask = preprocess(reviews)
        retour = model(input_ids, attention_mask=attention_mask)
        
        return torch.argmax(retour[0], dim=1)


def evaluate(reviews, sentiments):
    predictions = predict(reviews)
    print(metrics.f1_score(sentiments, predictions, average='weighted', zero_division=0))
    seaborn.heatmap(metrics.confusion_matrix(sentiments, predictions))

On obtient un f1-score de : 0.98 et la matrice de confusion suivante :

Conclusion

On a vu, assez rapidement, comment utiliser CamemBERT pour entraîner un modèle d’analyse de sentiments classique. Cet article est juste un exemple qui se veut simple. Pour avoir un modèle robuste il faudra sûrement faire beaucoup de modifications. Le plus gros du travail dans ce genre de problèmes réside dans l’évaluation et l’ajustement des hyperparamètres.

Vous trouverez une version complète du code ici.

Vous voulez publier sur ledatascientist.com ? C’est par ici

5 réflexions sur “Analyse de sentiments avec CamemBERT”

  1. Bonjour,

    Merci pour ce tutoriel.

    J’ai cependant une erreur/incompréhension avec la fonction predict.

    En effet je ne suis pas capable d’interpréter le retour de la fonction « predict ».
    Elle est definit comme suit :

    def predict(review, model=model):
    with torch.no_grad():
    model.eval()

    input_ids, attention_mask = preprocess(review)
    input_ids = input_ids
    attention_mask = attention_mask

    retour = model(input_ids, attention_mask=attention_mask)

    return torch.argmax(retour[0], dim=1)

    Et le resultat de la commande suivante :
    predict(« ce film est nul »)
    est :
    tensor([1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1])

    comment interpréter ce résultat et comment connaitre le taux de certitude ?

    Merci énormément pour l’aide et encore joli tutoriel !

    1. Bonjour Thomas.

      Content que tu aies apprécié ce tuto.

      En fait, la fonction « predict » attend comme 1er paramètre une liste de « reviews ». Cela est fait pour qu’on puisse prédire en « batch ». Si tu veux évaluer le sentiment d’un seul texte il faut que tu fasses : predict(["ce film est nul"]) là tu auras bien une seule valeur en retour.

      Et si tu veux le taux de certitude, il te faut recupérer « retour » (la valeur retournée par « model() »). Elle ressemble à ça : tensor([[ 3.5764, -4.2173]]) ces deux valeurs peuvent être vues comme les dégrés de certitudes pour chacune des deux classes. Tu peux les transformer en « probabilité » avec un softmax.

      J’espère t’avoir aidé. N’hésite pas à poster un autre commentaire, si tu as encore des doutes/incompéhensions/remarques.

      Olivier

  2. Bonjour Olivier,
    Merci pour ce super tuto et le partage sur Collab. Grâce à toi, j’ai fait un bond dans mes approches.
    Une question : pourrais-tu donner un bout de code pour calculer les valeurs et créer une courbe ROC, avec si possible, la courbe de threshold et les courbes optimistic et pessimistic?

    J’ai trouvé une belle matrice de confusion, dont je partage le code ici avec les références :
    cordialement,
    Eric

    #%% Confusion matrix and nice plot
    # https://www.kaggle.com/grfiv4/plot-a-confusion-matrix?rvi=1&scriptVersionId=1078080&cellId=2
    import numpy as np

    def plot_confusion_matrix(cm,
    target_names,
    title=’Confusion matrix’,
    cmap=None,
    normalize=True):
    «  » »
    given a sklearn confusion matrix (cm), make a nice plot

    Arguments
    ———
    cm: confusion matrix from sklearn.metrics.confusion_matrix

    target_names: given classification classes such as [0, 1, 2]
    the class names, for example: [‘high’, ‘medium’, ‘low’]

    title: the text to display at the top of the matrix

    cmap: the gradient of the values displayed from matplotlib.pyplot.cm
    see http://matplotlib.org/examples/color/colormaps_reference.html
    plt.get_cmap(‘jet’) or plt.cm.Blues

    normalize: If False, plot the raw numbers
    If True, plot the proportions

    Usage
    —–
    plot_confusion_matrix(cm = cm, # confusion matrix created by
    # sklearn.metrics.confusion_matrix
    normalize = True, # show proportions
    target_names = y_labels_vals, # list of names of the classes
    title = best_estimator_name) # title of graph

    Citiation
    ———
    http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html

    «  » »
    import matplotlib.pyplot as plt
    import numpy as np
    import itertools

    accuracy = np.trace(cm) / float(np.sum(cm))
    misclass = 1 – accuracy

    if cmap is None:
    cmap = plt.get_cmap(‘Blues’)

    plt.figure(figsize=(8, 6))
    plt.imshow(cm, interpolation=’nearest’, cmap=cmap)
    plt.title(title)
    plt.colorbar()

    if target_names is not None:
    tick_marks = np.arange(len(target_names))
    plt.xticks(tick_marks, target_names, rotation=45)
    plt.yticks(tick_marks, target_names)

    if normalize:
    cm = cm.astype(‘float’) / cm.sum(axis=1)[:, np.newaxis]

    thresh = cm.max() / 1.5 if normalize else cm.max() / 2
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
    if normalize:
    plt.text(j, i, « {:0.4f} ».format(cm[i, j]),
    horizontalalignment= »center »,
    color= »white » if cm[i, j] > thresh else « black »)
    else:
    plt.text(j, i, « {:,} ».format(cm[i, j]),
    horizontalalignment= »center »,
    color= »white » if cm[i, j] > thresh else « black »)

    plt.tight_layout()
    plt.ylabel(‘True label’)
    plt.xlabel(‘Predicted label\naccuracy={:0.4f}; misclass={:0.4f}’.format(accuracy, misclass))
    plt.show()

    #%%
    plot_confusion_matrix(cm = np.array(confusion_matrix),
    normalize = False,
    target_names = [‘Child’, ‘Parent’],
    title = « Confusion matrix\nsous-titre au choix\nnon Normalized »)

    #%%
    plot_confusion_matrix(cm = np.array(confusion_matrix),
    normalize = True,
    target_names = [‘Child’, ‘Parent’],
    title = « Confusion matrix\nsous-titre au choix\nNormalized »)

  3. bonjour,
    comment on peut changer ca si nous avons des labels qui sont de type string?

    # On transforme la liste des sentiments en tenseur
    sentiments = torch.tensor(sentiments)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.