| import os |
| import json |
| import time |
| import re |
| import pandas as pd |
| import numpy as np |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
| from sklearn.metrics import accuracy_score, f1_score, confusion_matrix |
| from llama_cpp import Llama |
|
|
| |
| sns.set_theme(style="darkgrid") |
| plt.rcParams.update({ |
| 'grid.color': '#2c2c2c', |
| 'axes.facecolor': '#121212', |
| 'figure.facecolor': '#0e0e0e', |
| 'text.color': '#e0e0e0', |
| 'axes.labelcolor': '#e0e0e0', |
| 'xtick.color': '#b0b0b0', |
| 'ytick.color': '#b0b0b0', |
| 'font.size': 11 |
| }) |
|
|
| print("=== [Process 2] Evaluating Gemma 3 LLM & Plotting Comparisons ===", flush=True) |
|
|
| |
| if not os.path.exists("data/keras_results.csv"): |
| raise FileNotFoundError("data/keras_results.csv not found! Please run evaluate/evaluate_keras.py first.") |
|
|
| df_sample = pd.read_csv("data/keras_results.csv") |
| print(f"Loaded {len(df_sample)} pre-evaluated tweets from Keras baseline.", flush=True) |
|
|
| |
| print("Loading local Gemma 3 GGUF model via Llama.from_pretrained...", flush=True) |
| llm = Llama.from_pretrained( |
| repo_id="JusteLeo/emotion-text-classifier-LLM", |
| filename="EmotionTextClassifierLLM.gguf", |
| n_ctx=512, |
| verbose=False |
| ) |
| print("Gemma 3 GGUF loaded successfully.", flush=True) |
|
|
| |
| positive_emotions = { |
| 'joy', 'love', 'surprise', 'pride', 'admiration', 'gratitude', 'hope', |
| 'optimism', 'amusement', 'desire', 'caring', 'relief', 'excitement', |
| 'approval', 'caring', 'curiosity' |
| } |
|
|
| negative_emotions = { |
| 'sadness', 'anger', 'fear', 'disgust', 'shame', 'guilt', 'disappointment', |
| 'annoyance', 'frustration', 'grief', 'nervousness', 'embarrassment', |
| 'remorse', 'disapproval', 'confusion', 'boredom' |
| } |
|
|
| def clean_and_parse_json(text): |
| """Robustly cleans and parses JSON from the LLM output.""" |
| cleaned = text.strip() |
| cleaned = re.sub(r"^```(?:json)?", "", cleaned, flags=re.IGNORECASE) |
| cleaned = re.sub(r"```$", "", cleaned).strip() |
| |
| try: |
| data = json.loads(cleaned) |
| return data |
| except Exception: |
| emotions = [] |
| explanation = "Error parsing explanation" |
| |
| emotion_match = re.search(r'"emotions"\s*:\s*\[(.*?)\]', cleaned, re.DOTALL) |
| if emotion_match: |
| emotions_str = emotion_match.group(1) |
| emotions = [e.strip(' "\'') for e in emotions_str.split(',')] |
| |
| explanation_match = re.search(r'"explanation"\s*:\s*"(.*?)"', cleaned, re.DOTALL) |
| if explanation_match: |
| explanation = explanation_match.group(1) |
| |
| return {"emotions": emotions, "explanation": explanation} |
|
|
| llm_emotions = [] |
| llm_explanations = [] |
| llm_predictions = [] |
| llm_times = [] |
|
|
| |
| print("Starting Gemma 3 predictions loop (500 tweets)...", flush=True) |
| for idx, text in enumerate(df_sample['text']): |
| if (idx + 1) % 25 == 0 or idx == 0: |
| print(f"Processing tweet {idx+1}/{len(df_sample)}...", flush=True) |
| |
| start_time = time.time() |
| |
| try: |
| response = llm.create_chat_completion( |
| messages=[{"role": "user", "content": text}], |
| temperature=0.1, |
| max_tokens=80 |
| ) |
| output_content = response["choices"][0]["message"]["content"] |
| parsed = clean_and_parse_json(output_content) |
| |
| emotions = parsed.get("emotions", []) |
| explanation = parsed.get("explanation", "No explanation.") |
| |
| |
| mapped_sentiment = 0 |
| if emotions: |
| primary_emotion = emotions[0].lower().strip() |
| if primary_emotion in positive_emotions: |
| mapped_sentiment = 1 |
| elif primary_emotion in negative_emotions: |
| mapped_sentiment = 0 |
| else: |
| pos_count = sum(1 for e in emotions if e.lower().strip() in positive_emotions) |
| neg_count = sum(1 for e in emotions if e.lower().strip() in negative_emotions) |
| if pos_count > neg_count: |
| mapped_sentiment = 1 |
| else: |
| emotions = ["Neutral"] |
| explanation = "Unable to classify fine emotions." |
| mapped_sentiment = 0 |
| |
| except Exception as e: |
| print(f"Error at index {idx}: {str(e)}", flush=True) |
| emotions = ["Error"] |
| explanation = f"Inference failed with error: {str(e)}" |
| mapped_sentiment = 0 |
| |
| end_time = time.time() |
| |
| llm_emotions.append(", ".join(emotions)) |
| llm_explanations.append(explanation) |
| llm_predictions.append(mapped_sentiment) |
| llm_times.append(end_time - start_time) |
|
|
| df_sample['llm_emotions'] = llm_emotions |
| df_sample['llm_explanation'] = llm_explanations |
| df_sample['llm_pred'] = llm_predictions |
| df_sample['llm_time'] = llm_times |
|
|
| |
| keras_accuracy = accuracy_score(df_sample['true_sentiment'], df_sample['keras_pred']) |
| keras_f1 = f1_score(df_sample['true_sentiment'], df_sample['keras_pred']) |
| keras_avg_time = df_sample['keras_time'].iloc[0] |
|
|
| llm_accuracy = accuracy_score(df_sample['true_sentiment'], df_sample['llm_pred']) |
| llm_f1 = f1_score(df_sample['true_sentiment'], df_sample['llm_pred']) |
| llm_total_time = sum(llm_times) |
| llm_avg_time = np.mean(llm_times) |
|
|
| print(f"\nBaseline Keras Accuracy: {keras_accuracy:.4f}", flush=True) |
| print(f"Baseline Keras F1-Score: {keras_f1:.4f}", flush=True) |
| print(f"Baseline Keras Avg Speed: {keras_avg_time*1000:.2f} ms / tweet", flush=True) |
|
|
| print(f"\nGemma 3 Accuracy: {llm_accuracy:.4f}", flush=True) |
| print(f"Gemma 3 F1-Score: {llm_f1:.4f}", flush=True) |
| print(f"Gemma 3 Avg Speed: {llm_avg_time:.4f} s / tweet", flush=True) |
|
|
| |
| df_sample.to_csv("data/benchmark_results.csv", index=False) |
| print("\nFinal benchmark results saved to data/benchmark_results.csv", flush=True) |
|
|
| |
| print("\n--- Generating Premium Performance Visualizations ---", flush=True) |
| os.makedirs("assets", exist_ok=True) |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(14, 6)) |
|
|
| |
| cm_keras = confusion_matrix(df_sample['true_sentiment'], df_sample['keras_pred']) |
| sns.heatmap(cm_keras, annot=True, fmt='d', cmap='Blues', cbar=False, ax=axes[0], |
| xticklabels=['Négatif', 'Positif'], yticklabels=['Négatif', 'Positif'], |
| annot_kws={"size": 14, "weight": "bold"}) |
| axes[0].set_title("Matrice de Confusion : Baseline Keras", fontsize=14, weight='bold', pad=15) |
| axes[0].set_xlabel("Prédiction", fontsize=12) |
| axes[0].set_ylabel("Vrai Label", fontsize=12) |
|
|
| |
| cm_llm = confusion_matrix(df_sample['true_sentiment'], df_sample['llm_pred']) |
| sns.heatmap(cm_llm, annot=True, fmt='d', cmap='Greens', cbar=False, ax=axes[1], |
| xticklabels=['Négatif', 'Positif'], yticklabels=['Négatif', 'Positif'], |
| annot_kws={"size": 14, "weight": "bold"}) |
| axes[1].set_title("Matrice de Confusion : Challengeur Gemma 3 (LLM)", fontsize=14, weight='bold', pad=15) |
| axes[1].set_xlabel("Prédiction", fontsize=12) |
| axes[1].set_ylabel("Vrai Label", fontsize=12) |
|
|
| plt.tight_layout() |
| plt.savefig("assets/confusion_matrices.png", dpi=300, facecolor='#0e0e0e') |
| plt.close() |
|
|
| |
| fig, ax = plt.subplots(figsize=(8, 6)) |
| models = ['Keras (Baseline)', 'Gemma 3 (LLM)'] |
| accuracies = [keras_accuracy * 100, llm_accuracy * 100] |
| speeds = [keras_avg_time, llm_avg_time] |
|
|
| color_acc = '#00df89' |
| bars = ax.bar(models, accuracies, width=0.5, color=color_acc, alpha=0.85, edgecolor='#00df89', linewidth=1.5) |
| ax.set_ylabel('Précision (Accuracy %)', color=color_acc, fontsize=12, weight='bold') |
| ax.set_ylim(0, 100) |
| ax.tick_params(axis='y', labelcolor=color_acc) |
| ax.set_xticklabels(models, fontsize=12, weight='bold') |
|
|
| for bar in bars: |
| height = bar.get_height() |
| ax.text(bar.get_x() + bar.get_width()/2., height + 2, f'{height:.1f}%', ha='center', va='bottom', color='#00df89', weight='bold', fontsize=12) |
|
|
| plt.title("Comparaison de la Justesse de Prédiction (Accuracy)", fontsize=14, weight='bold', pad=20, color='white') |
| plt.tight_layout() |
| plt.savefig("assets/accuracy_comparison.png", dpi=300, facecolor='#0e0e0e') |
| plt.close() |
|
|
| |
| fig, ax = plt.subplots(figsize=(8, 6)) |
| color_speed = '#ff5e62' |
| bars_time = ax.bar(models, speeds, width=0.5, color=color_speed, alpha=0.85, edgecolor='#ff5e62', linewidth=1.5) |
| ax.set_ylabel("Temps d'inférence moyen par tweet (s)", color=color_speed, fontsize=12, weight='bold') |
| ax.tick_params(axis='y', labelcolor=color_speed) |
| ax.set_xticklabels(models, fontsize=12, weight='bold') |
|
|
| for bar in bars_time: |
| height = bar.get_height() |
| time_text = f'{height*1000:.1f} ms' if height < 0.01 else f'{height:.3f} s' |
| ax.text(bar.get_x() + bar.get_width()/2., height + 0.02 * max(speeds), time_text, ha='center', va='bottom', color='#ff5e62', weight='bold', fontsize=12) |
|
|
| plt.title("Comparaison de la Vitesse d'Inférence", fontsize=14, weight='bold', pad=20, color='white') |
| plt.tight_layout() |
| plt.savefig("assets/speed_comparison.png", dpi=300, facecolor='#0e0e0e') |
| plt.close() |
|
|
| |
| print("\n--- Constructing Qualitative Explainability Table ---", flush=True) |
| ex_happy = df_sample[(df_sample['true_sentiment'] == 1) & (df_sample['llm_pred'] == 1)].head(2) |
| ex_sad = df_sample[(df_sample['true_sentiment'] == 0) & (df_sample['llm_pred'] == 0)].head(2) |
| ex_disagree = df_sample[df_sample['keras_pred'] != df_sample['llm_pred']].head(1) |
|
|
| if len(ex_disagree) == 0: |
| ex_disagree = df_sample.head(1) |
|
|
| qualitative_sample = pd.concat([ex_happy, ex_sad, ex_disagree]).head(5) |
|
|
| qual_markdown = """### Table 1: Analyse Qualitative de l'Explicabilité (Explainability) de Gemma 3 |
| |
| Ce tableau présente 5 exemples de tweets analysés lors de la preuve de concept, confrontant les prédictions binaires de la baseline Keras avec l'analyse d'émotion fine et l'explication formulée par le LLM Gemma 3. |
| |
| | Tweet Original | Vrai Sentiment | Prédiction Keras (Probabilité) | Émotion Fine détectée (Gemma 3) | Explication Générée par Gemma 3 | |
| | :--- | :---: | :---: | :---: | :--- | |
| """ |
|
|
| for _, row in qualitative_sample.iterrows(): |
| true_lbl = "😊 Positif" if row['true_sentiment'] == 1 else "😢 Négatif" |
| keras_lbl = f"Positif ({row['keras_prob']:.2f})" if row['keras_pred'] == 1 else f"Négatif ({row['keras_prob']:.2f})" |
| |
| tweet_txt = row['text'].replace('|', '\\|').replace('\n', ' ') |
| explanation_txt = row['llm_explanation'].replace('|', '\\|').replace('\n', ' ') |
| |
| qual_markdown += f"| \"{tweet_txt}\" | {true_lbl} | {keras_lbl} | **{row['llm_emotions']}** | {explanation_txt} |\n" |
|
|
| with open("assets/qualitative_table.md", "w", encoding="utf-8") as f: |
| f.write(qual_markdown) |
|
|
| print("Visualizations and qualitative table successfully generated in the assets directory!", flush=True) |
| print("=== [Process 2] Finished successfully ===", flush=True) |
|
|