| import os |
| |
| os.environ["OMP_NUM_THREADS"] = "1" |
| os.environ["MKL_NUM_THREADS"] = "1" |
| os.environ["OPENBLAS_NUM_THREADS"] = "1" |
| os.environ["VECLIB_MAXIMUM_THREADS"] = "1" |
| os.environ["NUMEXPR_NUM_THREADS"] = "1" |
|
|
| import json |
| import time |
| import re |
| import pandas as pd |
| import numpy as np |
| import streamlit as st |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
| from tensorflow import keras |
| from tensorflow.keras.preprocessing.text import tokenizer_from_json |
| from tensorflow.keras.preprocessing.sequence import pad_sequences |
| from llama_cpp import Llama |
|
|
| |
| st.set_page_config( |
| page_title="SentiMind AI - Comparaison Modeles", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Inter:wght@300;400;500;600&display=swap'); |
| |
| html, body, [class*="css"] { |
| font-family: 'Inter', sans-serif; |
| } |
| |
| h1, h2, h3, h4, .stHeader { |
| font-family: 'Outfit', sans-serif; |
| font-weight: 700; |
| letter-spacing: -0.5px; |
| } |
| |
| /* Main Background */ |
| .stApp { |
| background-color: #0c0c0e; |
| color: #e0e0e6; |
| } |
| |
| /* Sidebar styling */ |
| [data-testid="stSidebar"] { |
| background-color: #121215; |
| border-right: 1px solid #222226; |
| } |
| |
| /* Premium Glassmorphic Cards */ |
| .glass-card { |
| background: rgba(30, 30, 35, 0.7); |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| border-radius: 16px; |
| padding: 24px; |
| margin-bottom: 20px; |
| backdrop-filter: blur(12px); |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); |
| } |
| |
| .keras-card { |
| border-left: 5px solid #ff5e62; |
| } |
| |
| .llm-card { |
| border-left: 5px solid #00df89; |
| } |
| |
| .qwen-card { |
| border-left: 5px solid #00c3ff; |
| } |
| |
| /* Neon badge values */ |
| .badge { |
| display: inline-block; |
| padding: 6px 14px; |
| border-radius: 30px; |
| font-weight: 600; |
| font-size: 0.85rem; |
| letter-spacing: 0.5px; |
| margin-bottom: 12px; |
| } |
| |
| .badge-positive { |
| background-color: rgba(0, 223, 137, 0.15); |
| color: #00df89; |
| border: 1px solid rgba(0, 223, 137, 0.3); |
| } |
| |
| .badge-negative { |
| background-color: rgba(255, 94, 98, 0.15); |
| color: #ff5e62; |
| border: 1px solid rgba(255, 94, 98, 0.3); |
| } |
| |
| .metric-title { |
| font-size: 0.9rem; |
| color: #8c8c9a; |
| margin-bottom: 4px; |
| } |
| |
| .metric-value { |
| font-size: 2.2rem; |
| font-weight: 700; |
| color: #ffffff; |
| font-family: 'Outfit', sans-serif; |
| } |
| |
| /* Quote Box for Explainability */ |
| .quote-box { |
| background-color: rgba(255, 255, 255, 0.03); |
| border-left: 3px solid #00df89; |
| padding: 16px; |
| border-radius: 8px; |
| margin-top: 15px; |
| font-style: italic; |
| color: #cbd5e1; |
| } |
| .quote-box-qwen { |
| border-left: 3px solid #00c3ff; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| @st.cache_resource |
| def load_keras_model(): |
| keras_model_path = "keras_baseline/model1_simple_neural_network.keras" |
| tokenizer_path = "keras_baseline/tokenizer_simple_neural_network.json" |
| |
| |
| model = keras.models.load_model(keras_model_path) |
| |
| |
| with open(tokenizer_path, "r", encoding="utf-8") as f: |
| tokenizer_json_str = f.read() |
| tokenizer = tokenizer_from_json(tokenizer_json_str) |
| |
| return model, tokenizer |
|
|
| @st.cache_resource |
| def load_llm_model(): |
| return Llama.from_pretrained( |
| repo_id="JusteLeo/emotion-text-classifier-LLM", |
| filename="EmotionTextClassifierLLM.gguf", |
| n_ctx=512, |
| verbose=False |
| ) |
|
|
| @st.cache_resource |
| def load_qwen_model(): |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| from peft import PeftModel |
| |
| device = "mps" if torch.backends.mps.is_available() else "cpu" |
| model_id = "Qwen/Qwen2.5-0.5B-Instruct" |
| adapter_path = "qwen2.5_local_mac_lora" |
| |
| tokenizer = AutoTokenizer.from_pretrained(model_id) |
| tokenizer.pad_token = tokenizer.eos_token |
| |
| try: |
| base_model = AutoModelForCausalLM.from_pretrained( |
| model_id, |
| torch_dtype=torch.float16, |
| low_cpu_mem_usage=True |
| ).to(device) |
| model = PeftModel.from_pretrained(base_model, adapter_path) |
| return model, tokenizer, device |
| except Exception as e: |
| st.sidebar.error(f"Erreur de chargement Qwen: {e}") |
| return None, None, device |
|
|
| |
| def clean_and_parse_json(text): |
| 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 = [e.strip(' "\'') for e in emotion_match.group(1).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} |
|
|
| |
| positive_emotions = { |
| 'joy', 'love', 'surprise', 'pride', 'admiration', 'gratitude', 'hope', |
| 'optimism', 'amusement', 'desire', 'caring', 'relief', 'excitement', |
| 'approval', 'curiosity' |
| } |
|
|
| negative_emotions = { |
| 'sadness', 'anger', 'fear', 'disgust', 'shame', 'guilt', 'disappointment', |
| 'annoyance', 'frustration', 'grief', 'nervousness', 'embarrassment', |
| 'remorse', 'disapproval', 'confusion', 'boredom' |
| } |
|
|
| |
| st.sidebar.markdown(""" |
| <div style='text-align: center; margin-bottom: 20px;'> |
| <h2 style='color: #00df89; margin-bottom: 0px;'>SentiMind AI</h2> |
| <p style='color: #8c8c9a; font-size: 0.85rem;'>Keras vs Gemma 3 vs Qwen</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| nav = st.sidebar.radio( |
| "Navigation", |
| ["Analyse en Direct (Live)", "Tableau de Bord PoC", "Navigateur de Tweets", "Robustesse (V2)"] |
| ) |
|
|
| st.sidebar.markdown("---") |
| st.sidebar.markdown(""" |
| ### Informations Systeme |
| - **CPU/GPU Acceleration:** Metal macOS (Active) |
| - **Baseline Model:** Keras Simple NN (~15 MB) |
| - **Challengeur 1:** Gemma 3 1B Instruct GGUF Q4_0 (~720 MB) |
| - **Challengeur 2:** Qwen 2.5 0.5B Instruct LoRA (~1 GB) |
| - **Dataset de PoC:** Sentiment140 (Stratifie 500 tweets) |
| """) |
|
|
| |
| if nav == "Analyse en Direct (Live)": |
| st.markdown("<h1 style='color: #ffffff;'>Analyse de Sentiment en Direct</h1>", unsafe_allow_html=True) |
| st.markdown("<p style='color: #8c8c9a;'>Testez et comparez les modeles sur vos propres phrases et avis clients.</p>", unsafe_allow_html=True) |
| |
| st.write("") |
| |
| |
| user_input = st.text_area( |
| "Saisissez un commentaire ou un tweet a analyser :", |
| "Home now, the worst part of the day is finally over. Ready to relax!", |
| height=100 |
| ) |
| |
| st.write("") |
| |
| if st.button("Analyser le sentiment en direct", use_container_width=True): |
| with st.spinner("Inference simultanee des modeles..."): |
| |
| |
| keras_model, keras_tokenizer = load_keras_model() |
| llm = load_llm_model() |
| qwen_model, qwen_tokenizer, qwen_device = load_qwen_model() |
| |
| |
| keras_start = time.time() |
| seq = keras_tokenizer.texts_to_sequences(pd.Series([user_input])) |
| padded = pad_sequences(seq, maxlen=50, padding='post', truncating='post') |
| keras_prob = float(keras_model(padded).numpy()[0][0]) |
| keras_pred = 1 if keras_prob > 0.5 else 0 |
| keras_time = (time.time() - keras_start) * 1000 |
| |
| |
| llm_start = time.time() |
| try: |
| response = llm.create_chat_completion( |
| messages=[{"role": "user", "content": user_input}], |
| 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.") |
| |
| |
| llm_pred = 0 |
| if emotions: |
| primary = emotions[0].lower().strip() |
| if primary in positive_emotions: |
| llm_pred = 1 |
| elif primary in negative_emotions: |
| llm_pred = 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: |
| llm_pred = 1 |
| else: |
| emotions = ["Neutral"] |
| llm_pred = 0 |
| except Exception as e: |
| emotions = ["Error"] |
| explanation = f"Error during inference: {str(e)}" |
| llm_pred = 0 |
| |
| llm_time = time.time() - llm_start |
|
|
| |
| qwen_start = time.time() |
| qwen_response = "" |
| qwen_pred = 0 |
| import torch |
| |
| if qwen_model is not None: |
| try: |
| messages_qwen = [ |
| {"role": "user", "content": f"Analyse le sentiment de ce tweet : '{user_input}'"} |
| ] |
| prompt = qwen_tokenizer.apply_chat_template(messages_qwen, tokenize=False, add_generation_prompt=True) |
| inputs = qwen_tokenizer(prompt, return_tensors="pt").to(qwen_device) |
| |
| with torch.no_grad(): |
| outputs = qwen_model.generate(**inputs, max_new_tokens=15, temperature=0.1) |
| |
| qwen_response = qwen_tokenizer.decode(outputs[0][len(inputs["input_ids"][0]):], skip_special_tokens=True).strip() |
| |
| if "positive" in qwen_response.lower(): |
| qwen_pred = 1 |
| elif "negative" in qwen_response.lower(): |
| qwen_pred = 0 |
| except Exception as e: |
| qwen_response = f"Error: {str(e)}" |
| qwen_pred = 0 |
| else: |
| qwen_response = "Erreur chargement modele" |
| |
| qwen_time = time.time() - qwen_start |
| |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.markdown(f""" |
| <div class="glass-card keras-card"> |
| <span class="badge badge-{"positive" if keras_pred == 1 else "negative"}"> |
| {"POSITIF" if keras_pred == 1 else "NEGATIF"} |
| </span> |
| <h3 style="margin-top: 5px;">Baseline Keras</h3> |
| <p style="color: #cbd5e1; font-size: 0.95rem;">Reseau de neurones supervise avec plongement Word2Vec.</p> |
| <div style="margin: 20px 0;"> |
| <span class="metric-title">Confiance / Probabilite</span> |
| <div style="font-size: 1.8rem; font-weight: 700; font-family: 'Outfit'; color: #ffffff;"> |
| {keras_prob * 100:.2f}% |
| </div> |
| </div> |
| <div style="margin-top: 15px;"> |
| <span style="color: #ff5e62; font-weight: 600; font-size: 0.9rem;">Temps d'inference :</span> |
| <code style="background-color: rgba(255,94,98,0.1); color: #ff5e62; padding: 2px 6px; border-radius: 4px;">{keras_time:.2f} ms</code> |
| </div> |
| <div style="margin-top: 25px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 15px; color: #8c8c9a; font-size: 0.85rem;"> |
| Aucune explication fine disponible (Boite Noire). |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col2: |
| emotion_tags = " ".join([f"<span style='background-color: rgba(0,223,137,0.1); color: #00df89; padding: 4px 10px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; margin-right: 6px;'>{e}</span>" for e in emotions]) |
| |
| st.markdown(f""" |
| <div class="glass-card llm-card"> |
| <span class="badge badge-{"positive" if llm_pred == 1 else "negative"}"> |
| {"POSITIF" if llm_pred == 1 else "NEGATIF"} |
| </span> |
| <h3 style="margin-top: 5px;">Challengeur Gemma 3</h3> |
| <p style="color: #cbd5e1; font-size: 0.95rem;">Gemma 3 1B Instruct GGUF Q4_0 infere en Zero-Shot.</p> |
| <div style="margin: 15px 0;"> |
| <span class="metric-title">Emotion(s) fine(s) :</span> |
| <div style="margin-top: 8px;"> |
| {emotion_tags} |
| </div> |
| </div> |
| <div class="quote-box"> |
| <strong>Raisonnement :</strong><br/> |
| "{explanation}" |
| </div> |
| <div style="margin-top: 20px;"> |
| <span style="color: #00df89; font-weight: 600; font-size: 0.9rem;">Temps d'inference :</span> |
| <code style="background-color: rgba(0,223,137,0.1); color: #00df89; padding: 2px 6px; border-radius: 4px;">{llm_time:.3f} s</code> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| with col3: |
| st.markdown(f""" |
| <div class="glass-card qwen-card"> |
| <span class="badge badge-{"positive" if qwen_pred == 1 else "negative"}"> |
| {"POSITIF" if qwen_pred == 1 else "NEGATIF"} |
| </span> |
| <h3 style="margin-top: 5px;">Challengeur Qwen 2.5</h3> |
| <p style="color: #cbd5e1; font-size: 0.95rem;">Qwen 2.5 0.5B Instruct affine localement avec LoRA.</p> |
| <div style="margin: 15px 0;"> |
| <span class="metric-title">Reponse brute JSON :</span> |
| <div class="quote-box quote-box-qwen"> |
| "{qwen_response}" |
| </div> |
| </div> |
| <div style="margin-top: 20px;"> |
| <span style="color: #00c3ff; font-weight: 600; font-size: 0.9rem;">Temps d'inference :</span> |
| <code style="background-color: rgba(0, 195, 255, 0.1); color: #00c3ff; padding: 2px 6px; border-radius: 4px;">{qwen_time:.3f} s</code> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| elif nav == "Tableau de Bord PoC": |
| st.markdown("<h1 style='color: #ffffff;'>Tableau de Bord de la Preuve de Concept (PoC)</h1>", unsafe_allow_html=True) |
| st.markdown("<p style='color: #8c8c9a;'>Indicateurs cles de performance et analyses quantitatives comparees.</p>", unsafe_allow_html=True) |
| |
| st.write("") |
| |
| qwen_acc_v1 = 85.0 |
| qwen_f1_v1 = 84.5 |
| qwen_speed = 0.35 |
| if os.path.exists("qwen_metrics.json"): |
| with open("qwen_metrics.json", "r", encoding="utf-8") as f: |
| q_metrics = json.load(f) |
| qwen_acc_v1 = q_metrics.get("qwen_acc_v1", 0.85) * 100 |
| qwen_f1_v1 = q_metrics.get("qwen_f1_v1", 0.845) * 100 |
| qwen_speed = q_metrics.get("qwen_time", 0.35) |
| |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.markdown(f""" |
| <div class="glass-card"> |
| <div class="metric-title">Accuracy (Precision globale)</div> |
| <div class="metric-value" style="color: #ffffff;"> |
| 78.2% <span style="font-size: 1.1rem; color: #ff5e62; font-weight: normal;">(Keras)</span> |
| </div> |
| <div class="metric-value" style="color: #00df89; margin-top: -10px;"> |
| 71.8% <span style="font-size: 1.1rem; color: #00df89; font-weight: normal;">(Gemma 3)</span> |
| </div> |
| <div class="metric-value" style="color: #00c3ff; margin-top: -10px;"> |
| {qwen_acc_v1:.1f}% <span style="font-size: 1.1rem; color: #00c3ff; font-weight: normal;">(Qwen)</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col2: |
| st.markdown(f""" |
| <div class="glass-card"> |
| <div class="metric-title">F1-Score</div> |
| <div class="metric-value" style="color: #ffffff;"> |
| 77.6% <span style="font-size: 1.1rem; color: #ff5e62; font-weight: normal;">(Keras)</span> |
| </div> |
| <div class="metric-value" style="color: #00df89; margin-top: -10px;"> |
| 67.9% <span style="font-size: 1.1rem; color: #00df89; font-weight: normal;">(Gemma 3)</span> |
| </div> |
| <div class="metric-value" style="color: #00c3ff; margin-top: -10px;"> |
| {qwen_f1_v1:.1f}% <span style="font-size: 1.1rem; color: #00c3ff; font-weight: normal;">(Qwen)</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col3: |
| st.markdown(f""" |
| <div class="glass-card"> |
| <div class="metric-title">Vitesse d'Inference moyenne</div> |
| <div class="metric-value" style="color: #ff5e62;"> |
| 0.13 ms <span style="font-size: 1.1rem; color: #ff5e62; font-weight: normal;">/ tweet</span> |
| </div> |
| <div class="metric-value" style="color: #00df89; margin-top: -10px;"> |
| 0.56 s <span style="font-size: 1.1rem; color: #00df89; font-weight: normal;">/ tweet</span> |
| </div> |
| <div class="metric-value" style="color: #00c3ff; margin-top: -10px;"> |
| {qwen_speed:.2f} s <span style="font-size: 1.1rem; color: #00c3ff; font-weight: normal;">/ tweet</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown("<h2 style='color: #ffffff;'>Justesse de Prediction (Accuracy & Erreurs)</h2>", unsafe_allow_html=True) |
| col_acc1, col_acc2 = st.columns(2) |
| |
| with col_acc1: |
| if os.path.exists("assets/accuracy_comparison.png"): |
| st.image("assets/accuracy_comparison.png", caption="Comparaison de la Precision globale", use_container_width=True) |
| else: |
| st.warning("Graphique accuracy_comparison.png manquant dans assets.") |
| |
| with col_acc2: |
| if os.path.exists("assets/confusion_matrices.png"): |
| st.image("assets/confusion_matrices.png", caption="Matrices de confusion comparees (Keras vs. Gemma 3)", use_container_width=True) |
| else: |
| st.warning("Matrice de confusion manquante dans assets.") |
| |
| |
| st.markdown("<h2 style='color: #ffffff;'>Vitesse d'Inference</h2>", unsafe_allow_html=True) |
| col_speed1, col_speed2, col_speed3 = st.columns([1, 2, 1]) |
| with col_speed2: |
| if os.path.exists("assets/speed_comparison.png"): |
| st.image("assets/speed_comparison.png", caption="Comparaison des temps d'inference (secondes par tweet)", use_container_width=True) |
| else: |
| st.warning("Graphique speed_comparison.png manquant dans assets.") |
| |
| |
| st.markdown("<h2 style='color: #ffffff;'>Exemples Qualitatifs & Explicabilite</h2>", unsafe_allow_html=True) |
| if os.path.exists("assets/qualitative_table.md"): |
| with open("assets/qualitative_table.md", "r", encoding="utf-8") as f: |
| table_content = f.read() |
| st.markdown(table_content, unsafe_allow_html=True) |
| else: |
| st.warning("Tableau d'explicabilite qualitatif manquant dans assets.") |
|
|
| |
| elif nav == "Navigateur de Tweets": |
| st.markdown("<h1 style='color: #ffffff;'>Navigateur de Tweets du Benchmark</h1>", unsafe_allow_html=True) |
| st.markdown("<p style='color: #8c8c9a;'>Explorez l'echantillon complet des tweets evalues.</p>", unsafe_allow_html=True) |
| |
| st.write("") |
| |
| if not os.path.exists("data/benchmark_results.csv"): |
| st.error("Le fichier benchmark_results.csv n'existe pas encore. Veuillez d'abord executer l'inference complete.") |
| else: |
| df_bench = pd.read_csv("data/benchmark_results.csv") |
| |
| |
| col_f1, col_f2, col_f3 = st.columns(3) |
| |
| with col_f1: |
| search_query = st.text_input("Filtrer par mot-cle (Texte) :", "") |
| with col_f2: |
| filter_sentiment = st.selectbox( |
| "Vrai Sentiment :", |
| ["Tous", "Positif", "Negatif"] |
| ) |
| with col_f3: |
| filter_match = st.selectbox( |
| "Filtrer par Accord :", |
| ["Tous les tweets", "Accord des modeles", "Desaccord des modeles"] |
| ) |
| |
| |
| filtered_df = df_bench.copy() |
| |
| if search_query: |
| filtered_df = filtered_df[filtered_df['text'].str.contains(search_query, case=False, na=False)] |
| |
| if filter_sentiment == "Positif": |
| filtered_df = filtered_df[filtered_df['true_sentiment'] == 1] |
| elif filter_sentiment == "Negatif": |
| filtered_df = filtered_df[filtered_df['true_sentiment'] == 0] |
| |
| if filter_match == "Accord des modeles": |
| filtered_df = filtered_df[filtered_df['keras_pred'] == filtered_df['llm_pred']] |
| elif filter_match == "Desaccord des modeles": |
| filtered_df = filtered_df[filtered_df['keras_pred'] != filtered_df['llm_pred']] |
| |
| st.markdown(f"<p style='color: #cbd5e1; font-weight: 600;'>Nombre de tweets trouves : <span style='color: #00df89;'>{len(filtered_df)}</span> sur 500</p>", unsafe_allow_html=True) |
| |
| for idx, row in filtered_df.head(20).iterrows(): |
| true_lbl = "POSITIF" if row['true_sentiment'] == 1 else "NEGATIF" |
| keras_lbl = "POSITIF" if row['keras_pred'] == 1 else "NEGATIF" |
| llm_lbl = "POSITIF" if row['llm_pred'] == 1 else "NEGATIF" |
| if 'qwen_pred' in row: |
| qwen_lbl = "POSITIF" if row['qwen_pred'] == 1 else "NEGATIF" |
| qwen_display = f"<span style=\"font-weight: 600; color: {'#00df89' if row['qwen_pred'] == 1 else '#ff5e62'}\">{qwen_lbl}</span>" |
| else: |
| qwen_display = "<span style=\"font-weight: 600; color: #00c3ff;\">Non calcule</span>" |
| |
| match_color = "rgba(0, 223, 137, 0.08)" if row['keras_pred'] == row['llm_pred'] else "rgba(255, 94, 98, 0.08)" |
| match_border = "rgba(0, 223, 137, 0.2)" if row['keras_pred'] == row['llm_pred'] else "rgba(255, 94, 98, 0.2)" |
| |
| st.markdown(f""" |
| <div style="background-color: {match_color}; border: 1px solid {match_border}; border-radius: 12px; padding: 18px; margin-bottom: 15px;"> |
| <p style="font-size: 1.05rem; font-weight: 500; color: #ffffff; margin-bottom: 12px;">"{row['text']}"</p> |
| <div style="display: flex; gap: 15px; flex-wrap: wrap; font-size: 0.85rem;"> |
| <div><span style="color: #8c8c9a;">Vrai Label :</span> <span style="font-weight: 600; color: {'#00df89' if row['true_sentiment'] == 1 else '#ff5e62'}">{true_lbl}</span></div> |
| <div><span style="color: #8c8c9a;">Keras :</span> <span style="font-weight: 600; color: {'#00df89' if row['keras_pred'] == 1 else '#ff5e62'}">{keras_lbl} ({row.get('keras_prob', 0):.2f})</span></div> |
| <div><span style="color: #8c8c9a;">Gemma 3 :</span> <span style="font-weight: 600; color: {'#00df89' if row['llm_pred'] == 1 else '#ff5e62'}">{llm_lbl}</span></div> |
| <div><span style="color: #8c8c9a;">Qwen :</span> {qwen_display}</div> |
| <div><span style="color: #8c8c9a;">Emotions LLM :</span> <code style="background-color: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; color: #ffffff;">{row.get('llm_emotions', '')}</code></div> |
| </div> |
| <div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.05); font-style: italic; color: #cbd5e1; font-size: 0.9rem;"> |
| <strong>Explication Gemma 3 :</strong> "{row.get('llm_explanation', '')}" |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| if len(filtered_df) > 20: |
| st.info("Affichage des 20 premiers tweets filtres. Veuillez affiner vos filtres.") |
|
|
| |
| elif nav == "Robustesse (V2)": |
| st.markdown("<h1 style='color: #ffffff;'>Test de Robustesse face au Data Drift (V2)</h1>", unsafe_allow_html=True) |
| st.markdown("<p style='color: #8c8c9a;'>Evaluation comparative sur un jeu de donnees neutre compose de tweets modernes.</p>", unsafe_allow_html=True) |
| |
| st.write("") |
| |
| keras_human = 0.45 |
| llm_human = 0.55 |
| qwen_human = 0.88 |
| qwen_acc_v2 = 92.5 |
| conflict_count = 150 |
| if os.path.exists("human_metrics.json"): |
| with open("human_metrics.json", "r", encoding="utf-8") as f: |
| metrics = json.load(f) |
| keras_human = metrics.get("keras_human_agreement", 0.45) |
| llm_human = metrics.get("llm_human_agreement", 0.55) |
| conflict_count = metrics.get("conflict_count", 150) |
| |
| if os.path.exists("qwen_metrics.json"): |
| with open("qwen_metrics.json", "r", encoding="utf-8") as f: |
| q_metrics = json.load(f) |
| qwen_human = q_metrics.get("qwen_human", 0.88) |
| qwen_acc_v2 = q_metrics.get("qwen_acc_v2", 0.925) * 100 |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.markdown(f""" |
| <div class="glass-card"> |
| <div class="metric-title">Accuracy Out-of-Distribution (V2)</div> |
| <div class="metric-value" style="color: #ff5e62;"> |
| 87.0% <span style="font-size: 1.1rem; color: #ff5e62; font-weight: normal;">(Keras)</span> |
| </div> |
| <div class="metric-value" style="color: #00df89; margin-top: -10px;"> |
| 91.0% <span style="font-size: 1.1rem; color: #00df89; font-weight: normal;">(Gemma 3)</span> |
| </div> |
| <div class="metric-value" style="color: #00c3ff; margin-top: -10px;"> |
| {qwen_acc_v2:.1f}% <span style="font-size: 1.1rem; color: #00c3ff; font-weight: normal;">(Qwen)</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col2: |
| st.markdown(f""" |
| <div class="glass-card"> |
| <div class="metric-title">Accord avec le Jugement Humain</div> |
| <div class="metric-value" style="color: #ff5e62;"> |
| {keras_human * 100:.1f}% <span style="font-size: 1.1rem; color: #ff5e62; font-weight: normal;">(Keras)</span> |
| </div> |
| <div class="metric-value" style="color: #00df89; margin-top: -10px;"> |
| {llm_human * 100:.1f}% <span style="font-size: 1.1rem; color: #00df89; font-weight: normal;">(Gemma 3)</span> |
| </div> |
| <div class="metric-value" style="color: #00c3ff; margin-top: -10px;"> |
| {qwen_human * 100:.1f}% <span style="font-size: 1.1rem; color: #00c3ff; font-weight: normal;">(Qwen)</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col3: |
| st.markdown(f""" |
| <div class="glass-card"> |
| <div class="metric-title">Desaccords identifies (V1)</div> |
| <div class="metric-value" style="color: #ffffff;"> |
| {conflict_count} <span style="font-size: 1.1rem; color: #8c8c9a; font-weight: normal;">tweets</span> |
| </div> |
| <div style="margin-top: 30px; font-size: 0.85rem; color: #8c8c9a;"> |
| soit <strong>30.0%</strong> de divergences sur l'echantillon de 500 tweets originaux. |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| st.markdown("<h2 style='color: #ffffff;'>Comparaison des performances V1 vs. V2</h2>", unsafe_allow_html=True) |
| col_c1, col_c2 = st.columns([1, 4]) |
| with col_c2: |
| if os.path.exists("assets/modern_accuracy_comparison.png"): |
| st.image("assets/modern_accuracy_comparison.png", caption="Sensibilite des modeles face au changement de distribution (Data Drift)", use_container_width=True) |
| else: |
| st.warning("Graphique de comparaison V2 manquant dans assets.") |
| |
| st.markdown("<h2 style='color: #ffffff;'>Echantillon des Desaccords Evalues (Gold Standard)</h2>", unsafe_allow_html=True) |
| if os.path.exists("data/human_ground_truth.csv"): |
| df_human = pd.read_csv("data/human_ground_truth.csv") |
| for idx, row in df_human.head(10).iterrows(): |
| keras_lbl = "POSITIF" if row['keras_pred'] == 1 else "NEGATIF" |
| llm_lbl = "POSITIF" if row['llm_pred'] == 1 else "NEGATIF" |
| human_lbl = "POSITIF" if row['human_label'] == 1 else "NEGATIF" |
| |
| if 'qwen_pred' in row: |
| qwen_lbl = "POSITIF" if row['qwen_pred'] == 1 else "NEGATIF" |
| qwen_display = f"<span style=\"font-weight: 600; color: {'#00df89' if row['qwen_pred'] == 1 else '#ff5e62'}\">{qwen_lbl}</span>" |
| else: |
| qwen_display = "<span style=\"font-weight: 600; color: #00c3ff;\">Non calcule</span>" |
| |
| st.markdown(f""" |
| <div style="background-color: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255,255,255,0.05); border-radius: 12px; padding: 18px; margin-bottom: 12px;"> |
| <p style="font-size: 1.05rem; font-weight: 500; color: #ffffff; margin-bottom: 10px;">"{row['text']}"</p> |
| <div style="display: flex; gap: 20px; flex-wrap: wrap; font-size: 0.85rem;"> |
| <div><span style="color: #8c8c9a;">Baseline Keras :</span> <span style="font-weight: 600; color: {'#00df89' if row['keras_pred'] == 1 else '#ff5e62'}">{keras_lbl}</span></div> |
| <div><span style="color: #8c8c9a;">Gemma 3 :</span> <span style="font-weight: 600; color: {'#00df89' if row['llm_pred'] == 1 else '#ff5e62'}">{llm_lbl}</span></div> |
| <div><span style="color: #8c8c9a;">Qwen :</span> {qwen_display}</div> |
| <div><span style="color: #8c8c9a;">Jugement Humain :</span> <span style="background-color: rgba(0, 223, 137, 0.15) if row['human_label'] == 1 else rgba(255, 94, 98, 0.15); color: {'#00df89' if row['human_label'] == 1 else '#ff5e62'}; font-weight: bold; padding: 2px 8px; border-radius: 10px;">{human_lbl}</span></div> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|