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.
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 :
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
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 !
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
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 »)
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)
Bonjour, on peut utiliser LabelEncoder de sklearn.preprocessing.