Aller au contenu principal

Module 6 — Classification de texte et analyse de sentiment

Le module 3 a posé une référence TF-IDF plus régression logistique. Le module 5 a montré la puissance des plongements contextuels. Ce module joint les deux : on affine un encodeur préentraîné pour prédire la note (une à cinq étoiles) d'un avis, on mesure honnêtement le résultat, et on décide si le supplément de coût par rapport à TF-IDF se justifie.

Le corpus d'avis n'est pas équilibré, et il ne le sera jamais

Comme dans tout corpus d'avis réels, la distribution est fortement biaisée vers les extrêmes.

NoteProportion typiqueInterprétation
5 étoiles45 %Client conquis, tout va bien
4 étoiles20 %Client satisfait avec réserve
3 étoiles10 %Client mitigé, difficile à interpréter
2 étoiles10 %Client déçu
1 étoile15 %Client furieux

Une exactitude de 45 % s'atteint donc en répondant systématiquement « 5 étoiles ». C'est le score qu'un modèle indolent produirait, et c'est en dessous de ce plancher que tout modèle sérieux doit être évalué. La F1 macro-moyennée (moyenne des F1 par classe) et la matrice de confusion sont les métriques honnêtes à regarder.

Affiner un encodeur en quelques dizaines de lignes

Affiner (fine-tune) un encodeur préentraîné consiste à reprendre ses poids appris sur un grand corpus général, à ajouter une tête de classification et à entraîner l'ensemble sur nos données étiquetées pour quelques époques seulement. Le taux d'apprentissage est faible (de l'ordre de 2×1052 \times 10^{-5}), pour ne pas effacer les représentations préapprises.

from datasets import load_dataset
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
import evaluate, numpy as np

dataset = load_dataset("csv", data_files={"train": "avis_train.csv",
"test": "avis_test.csv"})
tok = AutoTokenizer.from_pretrained("camembert-base")

def encoder(exemple):
return tok(exemple["texte"], truncation=True, max_length=256)

dataset = dataset.map(encoder, batched=True)
dataset = dataset.rename_column("etoiles", "labels")

modele = AutoModelForSequenceClassification.from_pretrained(
"camembert-base", num_labels=5)

f1 = evaluate.load("f1")
def metriques(eval_pred):
logits, y = eval_pred
preds = np.argmax(logits, axis=-1)
return f1.compute(predictions=preds, references=y, average="macro")

args = TrainingArguments(
output_dir="./sortie",
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
num_train_epochs=3,
learning_rate=2e-5,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1",
)

trainer = Trainer(model=modele, args=args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
compute_metrics=metriques)
trainer.train()

En trois époques sur dix mille avis, l'exécution prend une vingtaine de minutes sur un GPU T4 gratuit de Colab. Les points à surveiller pendant l'entraînement : la perte de validation qui remonte (signal de surapprentissage, il faut réduire les époques), et l'exactitude globale qui monte alors que la F1 macro stagne (signal que le modèle apprend à répondre « 5 étoiles » plus efficacement, sans mieux traiter les classes minoritaires).

Corriger le déséquilibre : trois leviers, un seul qui marche vraiment

Sur-échantillonner les classes minoritaires (dupliquer les avis 1 et 2 étoiles) fait apprendre le modèle par cœur : les avis dupliqués passent plusieurs fois par époque, et le modèle finit par mémoriser leurs formulations. La F1 augmente sur le jeu d'entraînement, chute sur le jeu de test.

Sous-échantillonner la classe majoritaire (jeter des avis 5 étoiles) réduit le déséquilibre mais fait perdre au modèle des exemples utiles. Sur des petits corpus, c'est souvent le moins mauvais choix ; sur les grands, c'est un gaspillage.

Pondérer la perte par l'inverse de la fréquence de chaque classe est la seule solution qui n'introduit pas de nouveau biais. Chaque erreur sur une classe rare coûte plus cher, chaque erreur sur la classe fréquente coûte moins.

import torch, torch.nn as nn

comptes = np.bincount(dataset["train"]["labels"])
poids = torch.tensor(len(dataset["train"]) / (5 * comptes),
dtype=torch.float32)

class TrainerPondere(Trainer):
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
labels = inputs.pop("labels")
sortie = model(**inputs)
perte = nn.CrossEntropyLoss(weight=poids.to(sortie.logits.device))(
sortie.logits, labels)
return (perte, sortie) if return_outputs else perte

Lire une matrice de confusion à cinq classes

Sur les cinq étoiles, les erreurs ne se distribuent pas au hasard : elles se concentrent sur la diagonale et ses voisines immédiates. Un avis 4 étoiles confondu avec un avis 5 étoiles est un désaccord de tempérament, presque acceptable. Un avis 1 étoile classé 5 étoiles est un vrai contresens.

from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, preds, normalize="true")
# Ligne = vrai, colonne = prédit ; chaque ligne somme à 1

Sur notre corpus, une matrice typique après affinage montre :

  • 5 étoiles bien classées à 85 % (car majoritaire et cohérente)
  • 1 étoile à 75 % (vocabulaire de colère fort et reconnaissable)
  • 3 étoiles à 40 % seulement (nuances difficiles, souvent confondues avec 2 ou 4)
  • Presque aucune confusion 1 contre 5 : c'est le vrai bon signal

Regrouper les classes en deux paquets (négatif : 1-2, positif : 4-5, en jetant les 3) donne souvent une meilleure valeur métier que d'obstiner sur cinq niveaux.

Choisir la granularité en fonction de l'usage aval

Si le modèle sert à alerter sur les avis négatifs, une classification binaire à trois classes est suffisante et bien plus fiable. Si le score doit alimenter un tableau de bord fin, garder les cinq niveaux et présenter la matrice de confusion à qui consomme les résultats, pour qu'il sache la nature exacte des erreurs.

Comparaison honnête avec la référence TF-IDF

Sur le même jeu de test, sans triche :

ModèleExactitudeF1 macroTemps d'entraînementLatence par avis
Toujours « 5 étoiles »45 %12 %00 ms
TF-IDF + logistique60 %52 %10 s1 ms
CamemBERT affiné68 %62 %20 min (GPU)15 ms (GPU)

Le gain absolu de CamemBERT sur TF-IDF est de 8 points d'exactitude et 10 points de F1 macro. Le coût est cent fois le temps d'entraînement et quinze fois la latence, plus la dépendance à un GPU. Pour un flux de mille avis par jour, TF-IDF fait probablement l'affaire ; pour un million d'avis par jour dans une application temps réel, l'écart en euros justifie l'affinage.

La comparaison doit être faite sur le même partitionnement

Un article qui prétend qu'un Transformer bat TF-IDF sans donner le protocole exact de découpe et la même graine aléatoire n'est pas comparable. La règle : mêmes indices d'entraînement, mêmes indices de test, mêmes prétraitements. Sinon, ce que l'on mesure inclut l'aléa du partitionnement.

En résumé

  • Sur un corpus d'avis, la classe majoritaire à 45 % fixe le plancher ; toute évaluation doit se lire au-dessus, avec F1 macro et matrice de confusion.
  • Affiner un encodeur prend une trentaine de lignes avec Trainer ; taux d'apprentissage de l'ordre de 2×1052 \times 10^{-5}, trois époques suffisent souvent.
  • La pondération de la perte est la seule des trois techniques (sur-, sous-échantillonnage, pondération) qui n'introduit pas de biais dans le corpus vu par le modèle.
  • Le gain d'un encodeur affiné sur TF-IDF est de l'ordre de 8 points d'exactitude, au prix d'un facteur cent en entraînement et quinze en latence ; le choix est un arbitrage économique, pas technique.

Module suivant : au lieu de classer un avis entier, on va identifier les entités qu'il mentionne — noms de produits, lieux, services — avec l'étiquetage BIO.