import os # Configure environment variables to prevent threading conflicts on macOS 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 # Set page config st.set_page_config( page_title="SentiMind AI - Comparaison Modeles", layout="wide", initial_sidebar_state="expanded" ) # Custom premium styling (Dark Glassmorphism) st.markdown(""" """, unsafe_allow_html=True) # ----------------- cached Model Loaders ----------------- @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" # Load model model = keras.models.load_model(keras_model_path) # Load tokenizer 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 # Clean and parse helper 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} # Emotion mappings 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' } # ----------------- Sidebar navigation ----------------- st.sidebar.markdown("""

SentiMind AI

Keras vs Gemma 3 vs Qwen

""", 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) """) # ----------------- Tab 1: Live Analysis ----------------- if nav == "Analyse en Direct (Live)": st.markdown("

Analyse de Sentiment en Direct

", unsafe_allow_html=True) st.markdown("

Testez et comparez les modeles sur vos propres phrases et avis clients.

", unsafe_allow_html=True) st.write("") # Input Area 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..."): # Load models keras_model, keras_tokenizer = load_keras_model() llm = load_llm_model() qwen_model, qwen_tokenizer, qwen_device = load_qwen_model() # --- 1. Keras Inference --- 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 # ms # --- 2. Gemma 3 Inference --- 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.") # Mapped sentiment 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 # s # --- 3. Qwen Inference --- 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 # s # --- RENDER COMPARISON --- col1, col2, col3 = st.columns(3) with col1: st.markdown(f"""
{"POSITIF" if keras_pred == 1 else "NEGATIF"}

Baseline Keras

Reseau de neurones supervise avec plongement Word2Vec.

Confiance / Probabilite
{keras_prob * 100:.2f}%
Temps d'inference : {keras_time:.2f} ms
Aucune explication fine disponible (Boite Noire).
""", unsafe_allow_html=True) with col2: emotion_tags = " ".join([f"{e}" for e in emotions]) st.markdown(f"""
{"POSITIF" if llm_pred == 1 else "NEGATIF"}

Challengeur Gemma 3

Gemma 3 1B Instruct GGUF Q4_0 infere en Zero-Shot.

Emotion(s) fine(s) :
{emotion_tags}
Raisonnement :
"{explanation}"
Temps d'inference : {llm_time:.3f} s
""", unsafe_allow_html=True) with col3: st.markdown(f"""
{"POSITIF" if qwen_pred == 1 else "NEGATIF"}

Challengeur Qwen 2.5

Qwen 2.5 0.5B Instruct affine localement avec LoRA.

Reponse brute JSON :
"{qwen_response}"
Temps d'inference : {qwen_time:.3f} s
""", unsafe_allow_html=True) # ----------------- Tab 2: Analytics Dashboard ----------------- elif nav == "Tableau de Bord PoC": st.markdown("

Tableau de Bord de la Preuve de Concept (PoC)

", unsafe_allow_html=True) st.markdown("

Indicateurs cles de performance et analyses quantitatives comparees.

", 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) # 1. KPI Cards col1, col2, col3 = st.columns(3) with col1: st.markdown(f"""
Accuracy (Precision globale)
78.2% (Keras)
71.8% (Gemma 3)
{qwen_acc_v1:.1f}% (Qwen)
""", unsafe_allow_html=True) with col2: st.markdown(f"""
F1-Score
77.6% (Keras)
67.9% (Gemma 3)
{qwen_f1_v1:.1f}% (Qwen)
""", unsafe_allow_html=True) with col3: st.markdown(f"""
Vitesse d'Inference moyenne
0.13 ms / tweet
0.56 s / tweet
{qwen_speed:.2f} s / tweet
""", unsafe_allow_html=True) # 2. Section: Justesse de Prediction st.markdown("

Justesse de Prediction (Accuracy & Erreurs)

", 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.") # 3. Section: Vitesse d'Inference st.markdown("

Vitesse d'Inference

", 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.") # 3. Qualitative table st.markdown("

Exemples Qualitatifs & Explicabilite

", 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.") # ----------------- Tab 3: Tweet Browser ----------------- elif nav == "Navigateur de Tweets": st.markdown("

Navigateur de Tweets du Benchmark

", unsafe_allow_html=True) st.markdown("

Explorez l'echantillon complet des tweets evalues.

", 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") # Filters row 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"] ) # Apply filters 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"

Nombre de tweets trouves : {len(filtered_df)} sur 500

", 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"{qwen_lbl}" else: qwen_display = "Non calcule" 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"""

"{row['text']}"

Vrai Label : {true_lbl}
Keras : {keras_lbl} ({row.get('keras_prob', 0):.2f})
Gemma 3 : {llm_lbl}
Qwen : {qwen_display}
Emotions LLM : {row.get('llm_emotions', '')}
Explication Gemma 3 : "{row.get('llm_explanation', '')}"
""", unsafe_allow_html=True) if len(filtered_df) > 20: st.info("Affichage des 20 premiers tweets filtres. Veuillez affiner vos filtres.") # ----------------- Tab 4: Robustness (V2) ----------------- elif nav == "Robustesse (V2)": st.markdown("

Test de Robustesse face au Data Drift (V2)

", unsafe_allow_html=True) st.markdown("

Evaluation comparative sur un jeu de donnees neutre compose de tweets modernes.

", 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"""
Accuracy Out-of-Distribution (V2)
87.0% (Keras)
91.0% (Gemma 3)
{qwen_acc_v2:.1f}% (Qwen)
""", unsafe_allow_html=True) with col2: st.markdown(f"""
Accord avec le Jugement Humain
{keras_human * 100:.1f}% (Keras)
{llm_human * 100:.1f}% (Gemma 3)
{qwen_human * 100:.1f}% (Qwen)
""", unsafe_allow_html=True) with col3: st.markdown(f"""
Desaccords identifies (V1)
{conflict_count} tweets
soit 30.0% de divergences sur l'echantillon de 500 tweets originaux.
""", unsafe_allow_html=True) st.markdown("

Comparaison des performances V1 vs. V2

", 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("

Echantillon des Desaccords Evalues (Gold Standard)

", 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"{qwen_lbl}" else: qwen_display = "Non calcule" st.markdown(f"""

"{row['text']}"

Baseline Keras : {keras_lbl}
Gemma 3 : {llm_lbl}
Qwen : {qwen_display}
Jugement Humain : {human_lbl}
""", unsafe_allow_html=True)