Classification de commentaires avec Camembert sans prise de tête : les fondamentaux 🇫🇷¶

Xiaoou WANG

Motivation¶

Camembert a été publié en juin 2020. Cependant force est de constater que l’emploi de Bert en français (Il s’agit plutôt de Roberta pour Camembert, voir 10 questions rapides sur Bert) n’est pas encore une tendance. Nous pensons que cela est en partie dû au manque de tutoriels sur l’emploi des modèles pré-entraînés.

Ceci est le deuxième d’une série de 10 tutoriels sur Camembert. Dans ce tuto nous allons voir notamment comment utiliser Camembert sans fine-tuning, ce dernier présupposant des connaissances relativement plus poussées.

Nous commençons par installer transformers et sentencepiece, deux packages nécessaires à l’usage de Camembert. Quelques autres packages courants en machine learning ont aussi été importés.

[47]:
# !pip install transformers
# !pip install sentencepiece

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score
import torch
import transformers as ppb
import warnings
warnings.filterwarnings('ignore')

Données¶

Ensuite nous importons les données. Il s’agit d’un petit jeu de données que j’ai trouvé ici. Cela vous permettra notamment de répliquer ce tutoriel sur votre propre ordinateur, tant l’usage de GPU est peu nécessaire.

La structure du dataframe est simple. Il y a une colonne commentaire avec quelques autres colonnes annotant la classe de ce commentaire. J’ai filtré les autres classes en ne gardant que temps pour utiliser une simple régression logistique sur les données. 1 signifie que le commentaire est lié à des problèmes de temps d’attente et 0 non.

[48]:
url="https://raw.githubusercontent.com/nlpinfrench/nlpinfrench.github.io/master/source/labeled_data.csv"
df = pd.read_csv(url,header=1,names = ['a','review','b','c','temps','e'])
# Report the number of sentences.
print('Number of sentences: {:,}\n'.format(df.shape[0]))
# remove unuseful columns
df = df[["review","temps"]]
# Display 5 random rows from the data.
df.sample(5)
Number of sentences: 322

[48]:
review temps
10 accueil moyen... 0
273 .trop d'attente.insupportable trop de stress 1
84 la boutique 0
81 pas trop de monde contrairement Ă  d'autres bou... 0
276 1h30 d’attente pour un service irrespectueux. 1

Modèle et Tokenizer¶

Ensuite nous allons importer le modèle, le tokenizer et les weights pré-entraînés. Les weights sont comme les word embeddings. Deux choses à noter :

  1. Bert a son propre tokenizer avec un vocabulaire fixe. Il est donc inutile de tokénisez vous-même.

  2. Ces weights sont issus du modèle à l’état “brut”.

En pratique vous devez fine-tuner Camembert sur des données à vous afin d’avoir des weights propres à chaque tâche et corpus.

Par contre, le but de ce tuto n’est pas de fine-tuner le modèle mais de vous montrer que vous pouvez utiliser Bert avec les choses que vous connaissez déjà en word embeddings. Il s’agit d’une séance de “désintimidation” :D

[9]:
# load model, tokenizer and weights
camembert, tokenizer, weights = (ppb.CamembertModel, ppb.CamembertTokenizer, 'camembert-base')

# Load pretrained model/tokenizer
tokenizer = tokenizer.from_pretrained(weights)
model = camembert.from_pretrained(weights)

Bert ne sait que tokéniser des phrases de longueur maximale de 512 tokens. Ici nous allons simplement enlever les commentaires trop longs.

[51]:
# see if there are length > 512
max_len = 0
for i,sent in enumerate(df["review"]):
    # Tokenize the text and add `[CLS]` and `[SEP]` tokens.
    input_ids = tokenizer.encode(sent, add_special_tokens=True)
    if len(input_ids) > 512:
        print("annoying review at", i,"with length",
              len(input_ids))
    # Update the maximum sentence length.
    max_len = max(max_len, len(input_ids))

print('Max sentence length: ', max_len)
annoying review at 314 with length 556
Max sentence length:  556
[52]:
# remove > 512 sentence
df.drop([314],inplace=True)

Padding¶

Maintenant que la phrase la plus longue enlevée, nous faisons un padding de 472 tokens pour homogénéiser la longueur de phrases. Cela rendra l’entraînement plus simple. Nous indiquons aussi où se trouve les paddings avec np.where pour que Bert sache traiter les tokens de padding.

[35]:
tokenized = df['review'].apply((lambda x: tokenizer.encode(x, add_special_tokens=True)))
max_len = 0
for i in tokenized.values:
    if len(i) > max_len:
        max_len = len(i)

padded = np.array([i + [0]*(max_len-len(i)) for i in tokenized.values])
np.array(padded).shape
[35]:
(321, 472)
[36]:
attention_mask = np.where(padded != 0, 1, 0)
attention_mask.shape
[36]:
(321, 472)

Utiliser l’encodeur (encoder)¶

Enfin nous transformer les tokens en tensor pour les passer dans le fameux transformer. Seule la dernière couche est conservée pour faire la classification.

[38]:
input_ids = torch.tensor(padded)
attention_mask = torch.tensor(attention_mask)

with torch.no_grad():
    last_hidden_states = model(input_ids, attention_mask=attention_mask)

Entraîner un modèle logistique¶

Comme nous avons seulement besoin du premier token (CLS qui signifie classification) pour le modèle logistique, nous faisons en slice avec [:,0,:].

[39]:
features = last_hidden_states[0][:,0,:].numpy()
labels = df.temps
labels

Que l’entraînement commence ! Nous commençons par faire un split train/test avec Scikit-Learn. Ensuite nous utilisons Grid Search pour essayer de trouver le meilleur paramètre. Finalement on entraîne le modèle.

[41]:
train_features, test_features, train_labels, test_labels = train_test_split(features, labels)
[42]:
# Grid search
parameters = {'C': np.linspace(0.0001, 100, 20)}
grid_search = GridSearchCV(LogisticRegression(), parameters)
grid_search.fit(train_features, train_labels)

print('best parameters: ', grid_search.best_params_)
print('best scrores: ', grid_search.best_score_)
best parameters:  {'C': 5.263252631578947}
best scrores:  0.8625
[44]:
lr_clf = LogisticRegression(C=grid_search.best_params_['C'])
lr_clf.fit(train_features, train_labels)

[44]:
LogisticRegression(C=5.263252631578947, class_weight=None, dual=False,
                   fit_intercept=True, intercept_scaling=1, l1_ratio=None,
                   max_iter=100, multi_class='auto', n_jobs=None, penalty='l2',
                   random_state=None, solver='lbfgs', tol=0.0001, verbose=0,
                   warm_start=False)

Résultats et conclusions¶

Nous arrivons donc à une précision de 91.36%. Si nous avions utilisé un classifieur aléatoire la précision aurait été autour de 60%.

Voilà ! Bravo d’avoir lu jusqu’ici. Nous espérons que vous avez vu que finalement Bert n’était pas si difficile à comprendre. Il s’agit juste d’un encoder auquel on ajoute un algorithme de classification.

La vraie force de Bert réside dans ses possibilités de fine-tuning. A bientôt donc pour un cas pratique en classification de documents :D

[45]:
lr_clf.score(test_features, test_labels)
[45]:
0.9135802469135802
[54]:
from sklearn.dummy import DummyClassifier
clf = DummyClassifier()

scores = cross_val_score(clf, train_features, train_labels)
print("Dummy classifier score: %0.3f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
Dummy classifier score: 0.621 (+/- 0.15)