AvisSense / scripts /evaluate.py
rima ouchefoune
Commentaires pรฉdagogiques dรฉtaillรฉs sur tout le code
762c0d3
Raw
History Blame Contribute Delete
12.2 kB
"""
evaluate.py โ€” ร‰valuation dรฉtaillรฉe du modรจle sauvegardรฉ sur le test set Allocinรฉ.
==================================================================================
CE QUE FAIT CE SCRIPT (ร  lancer APRรˆS train.py) :
1. Recharge le modรจle fine-tunรฉ depuis model/sentiment_model/
2. Prรฉdit le sentiment des avis du split TEST (jamais vus ร  l'entraรฎnement)
3. Affiche : accuracy, precision, recall, F1, rapport par classe,
matrice de confusion
4. Montre les ERREURS LES PLUS CONFIANTES : les avis oรน le modรจle se
trompe en รฉtant trรจs sรปr de lui. C'est l'analyse la plus instructive
du projet : on y trouve l'ironie, les avis mitigรฉs, le vocabulaire
ambigu... -> matiรจre directe pour la section "limites".
POURQUOI UN SCRIPT Sร‰PARร‰ DE train.py ?
- On peut rรฉ-รฉvaluer le modรจle ร  tout moment sans rรฉ-entraรฎner (20 min
รฉconomisรฉes ร  chaque fois).
- On peut รฉvaluer sur plus d'avis que pendant l'entraรฎnement.
- Sรฉparation des responsabilitรฉs : entraรฎner et รฉvaluer sont deux
activitรฉs distinctes du cycle de vie d'un modรจle.
COMMENT LANCER (depuis la racine du projet) :
python scripts/evaluate.py
python scripts/evaluate.py --max-test 5000 --show-errors 10
"""
# โ”€โ”€โ”€ IMPORTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import argparse # Options en ligne de commande
import sys # Sortie propre en cas d'erreur
from pathlib import Path # Chemins portables
import numpy as np # Tableaux numรฉriques (tri des erreurs, masques)
import torch # Exรฉcution du modรจle
from datasets import load_dataset
from sklearn.metrics import (
accuracy_score, # % de bonnes rรฉponses
classification_report, # Rapport precision/recall/F1 PAR classe
confusion_matrix, # Oรน sont les erreurs
precision_recall_fscore_support,
)
from transformers import AutoModelForSequenceClassification, AutoTokenizer
# โ”€โ”€โ”€ CONSTANTES (mรชmes conventions que train.py) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
PROJECT_ROOT = Path(__file__).resolve().parent.parent
MODEL_DIR = PROJECT_ROOT / "model" / "sentiment_model"
MAX_LENGTH = 256 # Mรชme longueur max qu'ร  l'entraรฎnement
SEED = 42
def parse_args():
parser = argparse.ArgumentParser(description="ร‰valuation du modรจle sur le test set")
parser.add_argument("--max-test", type=int, default=2000,
help="Nombre d'avis du test set ร  รฉvaluer (max 20 000)")
parser.add_argument("--batch-size", type=int, default=32,
help="Nombre d'avis prรฉdits en mรชme temps. Plus grand = "
"plus rapide, mais plus de mรฉmoire")
parser.add_argument("--show-errors", type=int, default=5,
help="Nombre d'erreurs les plus confiantes ร  afficher")
return parser.parse_args()
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Prรฉdiction par lots (batch) โ€” bien plus rapide qu'avis par avis
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def predict_in_batches(texts, model, tokenizer, batch_size, device):
"""Prรฉdit le sentiment d'une liste de textes, par lots.
POURQUOI PAR LOTS ? Prรฉdire 2000 avis un par un = 2000 passages dans le
modรจle. Par lots de 32, on n'en fait que 63 : le GPU/CPU calcule les 32
avis EN PARALLรˆLE dans les mรชmes opรฉrations matricielles.
Renvoie deux tableaux numpy alignรฉs avec `texts` :
predictions : la classe prรฉdite (0 ou 1) pour chaque texte
confidences : la probabilitรฉ softmax de la classe prรฉdite
"""
all_predictions = []
all_confidences = []
model.eval() # Mode รฉvaluation : dropout dรฉsactivรฉ (cf. predict.py)
# range(0, N, batch_size) dรฉcoupe la liste en tranches de 32
for start in range(0, len(texts), batch_size):
batch_texts = texts[start:start + batch_size]
# Tokenisation du lot entier d'un coup.
# padding=True : les avis du lot n'ont pas la mรชme longueur -> on
# complรจte les courts avec des tokens <pad> jusqu'ร  la longueur du
# plus long DU LOT (l'attention_mask dira au modรจle de les ignorer).
# .to(device) : envoie les tenseurs sur le GPU si disponible.
inputs = tokenizer(
batch_texts,
return_tensors="pt",
truncation=True,
max_length=MAX_LENGTH,
padding=True,
).to(device)
# Infรฉrence sans calcul de gradients (on ne fait que prรฉdire)
with torch.no_grad():
logits = model(**inputs).logits # forme (batch_size, 2)
# softmax ligne par ligne : chaque avis a ses 2 probabilitรฉs
probabilities = torch.softmax(logits, dim=-1)
# torch.max renvoie EN MรŠME TEMPS la valeur max (= la confiance) et
# son indice (= la classe prรฉdite), pour chaque ligne du lot.
confidences, predictions = torch.max(probabilities, dim=-1)
# .cpu().numpy() : rapatrie les rรฉsultats du GPU vers des tableaux numpy
all_predictions.extend(predictions.cpu().numpy())
all_confidences.extend(confidences.cpu().numpy())
# Barre de progression maison (le \r rรฉรฉcrit la mรชme ligne)
done = min(start + batch_size, len(texts))
print(f"\r Progression : {done}/{len(texts)} avis", end="", flush=True)
print() # Retour ร  la ligne final
return np.array(all_predictions), np.array(all_confidences)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Analyse qualitative : les erreurs les plus confiantes
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def show_most_confident_errors(texts, labels, predictions, confidences,
id2label, n_errors):
"""Affiche les erreurs oรน le modรจle รฉtait le plus sรปr de lui.
POURQUOI C'EST INTร‰RESSANT ? Une erreur ร  51 % de confiance = le modรจle
hรฉsitait, c'est excusable. Une erreur ร  99 % = le modรจle est
SYSTร‰MATIQUEMENT trompรฉ par quelque chose : ironie ("Bravo, 2h de
perdues"), avis mitigรฉ, nรฉgation complexe... Ce sont ces cas qu'on
cite dans la section "limites" du projet.
"""
# np.where renvoie les indices oรน la condition est vraie (les erreurs)
error_indices = np.where(predictions != labels)[0]
if len(error_indices) == 0:
print("\nAucune erreur sur cet รฉchantillon !")
return
# On trie les erreurs par confiance Dร‰CROISSANTE :
# argsort trie en croissant, le signe - inverse l'ordre.
sorted_errors = error_indices[np.argsort(-confidences[error_indices])]
print(f"\nTop {n_errors} erreurs les plus confiantes "
f"({len(error_indices)} erreurs au total) :")
print("-" * 70)
for index in sorted_errors[:n_errors]:
true_label = id2label[int(labels[index])]
predicted_label = id2label[int(predictions[index])]
print(f" Vrai : {true_label:<8} | Prรฉdit : {predicted_label:<8} "
f"| Confiance : {confidences[index]:.2%}")
print(f" ยซ {texts[index][:160]}... ยป")
print("-" * 70)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Programme principal
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def main():
args = parse_args()
# โ”€โ”€ 1. Recharger le modรจle fine-tunรฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if not MODEL_DIR.exists():
sys.exit(f"Erreur : modรจle introuvable dans {MODEL_DIR}. "
"Lancez d'abord : python scripts/train.py")
# "cuda" = GPU NVIDIA ; sinon on calcule sur le processeur
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Matรฉriel : {device} | Modรจle : {MODEL_DIR}")
tokenizer = AutoTokenizer.from_pretrained(str(MODEL_DIR))
model = AutoModelForSequenceClassification.from_pretrained(str(MODEL_DIR)).to(device)
id2label = model.config.id2label # {0: "nรฉgatif", 1: "positif"} (รฉcrit par train.py)
# โ”€โ”€ 2. Charger le test set โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# split="test" : on ne tรฉlรฉcharge QUE le split de test.
# Le test set n'a servi ni ร  entraรฎner ni ร  choisir le meilleur
# checkpoint -> c'est la mesure honnรชte de la gรฉnรฉralisation.
print("Chargement du test set Allocinรฉ...")
test_data = load_dataset("allocine", split="test").shuffle(seed=SEED)
# min(...) : sรฉcuritรฉ si on demande plus d'avis qu'il n'en existe
test_data = test_data.select(range(min(args.max_test, len(test_data))))
texts = test_data["review"]
labels = np.array(test_data["label"])
print(f"ร‰valuation sur {len(texts)} avis de test\n")
# โ”€โ”€ 3. Prรฉdire tous les avis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
predictions, confidences = predict_in_batches(
texts, model, tokenizer, args.batch_size, device
)
# โ”€โ”€ 4. Mรฉtriques globales โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
precision, recall, f1, _ = precision_recall_fscore_support(
labels, predictions, average="binary" # la classe 1 (positif) = rรฉfรฉrence
)
print("\nMรฉtriques sur le test set :")
print(f" accuracy : {accuracy_score(labels, predictions):.4f}")
print(f" precision : {precision:.4f}")
print(f" recall : {recall:.4f}")
print(f" f1 : {f1:.4f}")
# La confiance moyenne donne une idรฉe de la "certitude" gรฉnรฉrale du
# modรจle (attention : softmax est naturellement sur-confiant).
print(f" confiance moyenne : {confidences.mean():.4f}")
# Rapport dรฉtaillรฉ PAR classe : permet de voir si le modรจle est meilleur
# sur les positifs que sur les nรฉgatifs (ou l'inverse).
print("\nRapport par classe :")
print(classification_report(
labels, predictions, target_names=["nรฉgatif", "positif"], digits=4
))
# Matrice de confusion : la diagonale = succรจs, hors-diagonale = erreurs
matrix = confusion_matrix(labels, predictions)
print("Matrice de confusion :")
print(f"{'':>16} | {'prรฉdit nรฉgatif':>15} | {'prรฉdit positif':>15}")
print("-" * 52)
print(f"{'vrai nรฉgatif':>16} | {matrix[0][0]:>15} | {matrix[0][1]:>15}")
print(f"{'vrai positif':>16} | {matrix[1][0]:>15} | {matrix[1][1]:>15}")
# โ”€โ”€ 5. Analyse qualitative des erreurs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
show_most_confident_errors(
texts, labels, predictions, confidences, id2label, args.show_errors
)
if __name__ == "__main__":
main()