| """ |
| 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 |
| """ |
|
|
| |
| import argparse |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| from datasets import load_dataset |
| from sklearn.metrics import ( |
| accuracy_score, |
| classification_report, |
| confusion_matrix, |
| precision_recall_fscore_support, |
| ) |
| from transformers import AutoModelForSequenceClassification, AutoTokenizer |
|
|
| |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent |
| MODEL_DIR = PROJECT_ROOT / "model" / "sentiment_model" |
| MAX_LENGTH = 256 |
| 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() |
|
|
|
|
| |
| |
| |
| 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() |
|
|
| |
| for start in range(0, len(texts), batch_size): |
| batch_texts = texts[start:start + batch_size] |
|
|
| |
| |
| |
| |
| |
| inputs = tokenizer( |
| batch_texts, |
| return_tensors="pt", |
| truncation=True, |
| max_length=MAX_LENGTH, |
| padding=True, |
| ).to(device) |
|
|
| |
| with torch.no_grad(): |
| logits = model(**inputs).logits |
|
|
| |
| probabilities = torch.softmax(logits, dim=-1) |
| |
| |
| confidences, predictions = torch.max(probabilities, dim=-1) |
|
|
| |
| all_predictions.extend(predictions.cpu().numpy()) |
| all_confidences.extend(confidences.cpu().numpy()) |
|
|
| |
| done = min(start + batch_size, len(texts)) |
| print(f"\r Progression : {done}/{len(texts)} avis", end="", flush=True) |
|
|
| print() |
| return np.array(all_predictions), np.array(all_confidences) |
|
|
|
|
| |
| |
| |
| 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. |
| """ |
| |
| error_indices = np.where(predictions != labels)[0] |
| if len(error_indices) == 0: |
| print("\nAucune erreur sur cet รฉchantillon !") |
| return |
|
|
| |
| |
| 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) |
|
|
|
|
| |
| |
| |
| def main(): |
| args = parse_args() |
|
|
| |
| if not MODEL_DIR.exists(): |
| sys.exit(f"Erreur : modรจle introuvable dans {MODEL_DIR}. " |
| "Lancez d'abord : python scripts/train.py") |
|
|
| |
| 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 |
|
|
| |
| |
| |
| |
| print("Chargement du test set Allocinรฉ...") |
| test_data = load_dataset("allocine", split="test").shuffle(seed=SEED) |
| |
| 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") |
|
|
| |
| predictions, confidences = predict_in_batches( |
| texts, model, tokenizer, args.batch_size, device |
| ) |
|
|
| |
| precision, recall, f1, _ = precision_recall_fscore_support( |
| labels, predictions, average="binary" |
| ) |
| 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}") |
| |
| |
| print(f" confiance moyenne : {confidences.mean():.4f}") |
|
|
| |
| |
| print("\nRapport par classe :") |
| print(classification_report( |
| labels, predictions, target_names=["nรฉgatif", "positif"], digits=4 |
| )) |
|
|
| |
| 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}") |
|
|
| |
| show_most_confident_errors( |
| texts, labels, predictions, confidences, id2label, args.show_errors |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|