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("""
Keras vs Gemma 3 vs Qwen
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"""Reseau de neurones supervise avec plongement Word2Vec.
{keras_time:.2f} ms
Gemma 3 1B Instruct GGUF Q4_0 infere en Zero-Shot.
{llm_time:.3f} s
Qwen 2.5 0.5B Instruct affine localement avec LoRA.
{qwen_time:.3f} s
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"""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']}"
{row.get('llm_emotions', '')}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""""{row['text']}"