From dbba37a4077844083e89f8ce39374a85e233187a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Mureau?= Date: Fri, 4 Jul 2025 22:49:22 +0200 Subject: [PATCH] FastAPI + Streamlit are configured. Only remains the email sending features after 3 wrong predictions reports --- __pycache__/functions.cpython-312.pyc | Bin 0 -> 3950 bytes .../api/__pycache__/main.cpython-312.pyc | Bin 4867 -> 6599 bytes deployment/api/app.py | 85 ++++++++++++ deployment/api/main.py | 124 +++++++++++------- functions.py | 72 ++++++++++ 5 files changed, 233 insertions(+), 48 deletions(-) create mode 100644 __pycache__/functions.cpython-312.pyc create mode 100644 deployment/api/app.py create mode 100644 functions.py diff --git a/__pycache__/functions.cpython-312.pyc b/__pycache__/functions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb3a9cb46a27491560f45d3833ed31bec676e712 GIT binary patch literal 3950 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 literal 4867 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 diff --git a/deployment/api/app.py b/deployment/api/app.py new file mode 100644 index 0000000..e5b5991 --- /dev/null +++ b/deployment/api/app.py @@ -0,0 +1,85 @@ +import streamlit as st +import requests + +st.title("Analyse de Sentiment - Air Paradis") + +tweet = st.text_area("Entrez votre tweet ici :", height=150) + +# Initialisation des états +if "prediction_result" not in st.session_state: + st.session_state.prediction_result = None + +if "show_report_button" not in st.session_state: + st.session_state.show_report_button = False + +if "report_sent" not in st.session_state: + st.session_state.report_sent = False + +if "last_report_sent" not in st.session_state: + st.session_state.last_report_sent = False + +def reset_state(): + st.session_state.prediction_result = None + st.session_state.show_report_button = False + st.session_state.report_sent = False + st.session_state.last_report_sent = False + +# Bouton d'analyse +if st.button("Analyser le sentiment"): + if not tweet.strip(): + st.warning("Veuillez entrer un tweet non vide.") + reset_state() + else: + try: + response = requests.post( + "http://localhost:8000/predict", + json={"tweet": tweet} + ) + if response.status_code == 200: + data = response.json() + st.session_state.prediction_result = data + st.session_state.show_report_button = True + st.session_state.report_sent = False + st.session_state.last_report_sent = False + else: + st.error(f"Erreur API : {response.status_code} - {response.text}") + reset_state() + except Exception as e: + st.error(f"Erreur lors de la requête vers l'API : {e}") + reset_state() + +# Affichage du résultat +if st.session_state.prediction_result: + sentiment = st.session_state.prediction_result["sentiment"].capitalize() + prob = st.session_state.prediction_result["probability"] + st.success(f"Sentiment : {sentiment}") + st.info(f"Probabilité : {prob:.2f}") + +# Gestion du bouton signalement +if st.session_state.show_report_button and not st.session_state.report_sent: + if st.button("Signaler une mauvaise prédiction"): + try: + response = requests.post( + "http://localhost:8000/report_error", + json={"tweet": tweet, "prediction": sentiment} + ) + if response.status_code == 200: + data = response.json() + # Mettre à jour les états + st.session_state.show_report_button = False + st.session_state.report_sent = True + st.session_state.last_report_sent = data.get("report_sent", False) + + # Forcer un re-run + st.rerun() + else: + st.error(f"Erreur API : {response.status_code} - {response.text}") + except Exception as e: + st.error(f"Erreur lors de la requête vers l'API : {e}") + +# Message si signalement déjà fait +elif st.session_state.report_sent: + st.success("✅ Merci pour votre signalement.") + + if st.session_state.last_report_sent: + st.info("📩 Un rapport a été envoyé à l'administrateur du site.") \ No newline at end of file diff --git a/deployment/api/main.py b/deployment/api/main.py index ad79695..fd63145 100644 --- a/deployment/api/main.py +++ b/deployment/api/main.py @@ -1,113 +1,141 @@ import os -import dill +import sys import pickle from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from pydantic import BaseModel import mlflow from mlflow.tracking import MlflowClient -import mlflow.keras +from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.sequence import pad_sequences -# Charger les variables d'environnement -load_dotenv(dotenv_path='../../.env') +# 🔁 Import de la fonction de prétraitement depuis ../../functions.py +PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) +sys.path.append(PARENT_DIR) +from functions import preprocess_tweet -#%% +# 🔐 Charger les variables d'environnement (.env à deux niveaux au-dessus) +load_dotenv(dotenv_path=os.path.join(PARENT_DIR, ".env")) + +# 🔗 Configurer MLflow mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI") if mlflow_tracking_uri: mlflow.set_tracking_uri(mlflow_tracking_uri) else: - print("⚠️ Attention : MLFLOW_TRACKING_URI non défini dans .env") + print("⚠️ MLFLOW_TRACKING_URI non défini dans .env") -# Modèle et étape par défaut +# 📌 Nom du modèle et stage MODEL_NAME = "SentimentAnalysisLSTM" STAGE = "Production" +# 🧾 Structures des requêtes POST class TweetRequest(BaseModel): tweet: str -def get_latest_version(client, model_name, stage): - versions = client.search_model_versions(f"name='{model_name}'") - filtered = [v for v in versions if v.current_stage == stage] - if not filtered: - return None - latest = max(filtered, key=lambda v: int(v.version)) - return latest +class ReportRequest(BaseModel): + tweet: str + prediction: str -def load_model_and_artifacts(model_name="SentimentAnalysisLSTM", stage="Production"): +# 🧠 Stockage des signalements d’erreurs +error_reports = {} + +# 🔍 Fonction pour charger modèle + tokenizer +def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE): client = MlflowClient() versions = client.get_latest_versions(model_name, [stage]) if not versions: raise RuntimeError(f"Aucune version du modèle '{model_name}' en stage '{stage}'") - version = versions[0] - run_id = version.run_id - if run_id is None: - raise RuntimeError("'run_id' introuvable.") - # Récupérer les tags du modèle pour trouver embedding_type - model_version_info = client.get_model_version(model_name, version.version) + version_info = versions[0] + run_id = version_info.run_id + if not run_id: + raise RuntimeError("Run ID introuvable.") + + # Extraire le tag d'embedding + model_version_info = client.get_model_version(model_name, version_info.version) embedding_type = model_version_info.tags.get("embedding_type", "glove").lower() + # Déterminer les chemins des artefacts local_dir = "./downloaded_artifacts" os.makedirs(local_dir, exist_ok=True) - base_path = "local_artifacts/logs" + base_path = "local_artifacts" + tokenizer_file = "tokenizer.pickle" - if embedding_type == "glove": + if "glove" in embedding_type: keras_file = "final_model_LSTM_GloVe-300d-Fige.keras" - tokenizer_file = "tokenizer.pickle" - preprocess_file = "preprocess_function.dill" - elif embedding_type == "word2vec": - keras_file = "final_model_LSTM_Word2Vec.keras" - tokenizer_file = "tokenizer_word2vec.pickle" - preprocess_file = "preprocess_function_word2vec.dill" + elif "word2vec" in embedding_type: + keras_file = "final_model_LSTM_Word2Vec-Fige.keras" else: - raise ValueError(f"Type d'embedding inconnu: {embedding_type}") + raise ValueError(f"Embedding type inconnu : {embedding_type}") def dl(path): return client.download_artifacts(run_id=run_id, path=f"{base_path}/{path}", dst_path=local_dir) + # 🔽 Télécharger les artefacts model_path = dl(keras_file) tokenizer_path = dl(tokenizer_file) - preprocess_path = dl(preprocess_file) - model = mlflow.keras.load_model(model_path) + # 📦 Chargement + model = load_model(model_path) with open(tokenizer_path, "rb") as f: tokenizer = pickle.load(f) - with open(preprocess_path, "rb") as f: - preprocess_fn = dill.load(f) - - return model, tokenizer, preprocess_fn, embedding_type + return model, tokenizer, embedding_type +# 🚀 Lancer l'API app = FastAPI(title="API Prédiction Sentiment - Air Paradis") try: - model, tokenizer, preprocess_tweet = load_model_and_artifacts() + model, tokenizer, embedding_type = load_model_and_artifacts() except Exception as e: - print(f"❌ Erreur lors du chargement des artefacts : {e}") - model, tokenizer, preprocess_tweet = None, None, None + print(f"\n❌ Erreur lors du chargement des artefacts : {e}") + model, tokenizer = None, None @app.get("/") def root(): - return {"message": "API Prédiction Sentiment Air Paradis - OK"} + return {"message": "API Sentiment - Air Paradis - en ligne"} @app.post("/predict") def predict_sentiment(request: TweetRequest): - if model is None or tokenizer is None or preprocess_tweet is None: - raise HTTPException(status_code=503, detail="Modèle ou artefacts non disponibles.") + if model is None or tokenizer is None: + raise HTTPException(status_code=503, detail="Modèle ou artefacts indisponibles.") + + try: + processed_tweet = preprocess_tweet(request.tweet) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur de prétraitement : {e}") - processed_tweet = preprocess_tweet(request.tweet) if not processed_tweet.strip(): raise HTTPException(status_code=400, detail="Tweet vide ou invalide après prétraitement.") - sequence = tokenizer.texts_to_sequences([processed_tweet]) - sequence_padded = pad_sequences(sequence, maxlen=100) # Ajuster maxlen si besoin - try: + sequence = tokenizer.texts_to_sequences([processed_tweet]) + sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1]) prediction_prob = model.predict(sequence_padded)[0][0] sentiment = "positif" if prediction_prob >= 0.5 else "negatif" except Exception as e: - raise HTTPException(status_code=500, detail=f"Erreur lors de l'inférence : {e}") + raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {e}") - return {"sentiment": sentiment, "probability": float(prediction_prob)} + return { + "sentiment": sentiment, + "probability": float(prediction_prob) + } + +@app.post("/report_error") +def report_error(request: ReportRequest): + tweet = request.tweet.strip() + prediction = request.prediction.strip().lower() + + if not tweet or not prediction: + raise HTTPException(status_code=400, detail="Tweet ou prédiction manquants.") + + # Ajouter ou mettre à jour le signalement du tweet + error_reports[tweet] = prediction + + report_sent = False + # Condition : à chaque multiple de 3 signalements totaux + if len(error_reports) % 3 == 0: + report_sent = True + + return {"report_sent": report_sent} \ No newline at end of file diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..e97040b --- /dev/null +++ b/functions.py @@ -0,0 +1,72 @@ +import re +import html +import numpy as np +import pandas as pd +from multiprocessing import Pool + +# NLTK +import nltk +from nltk.corpus import stopwords +from nltk.tokenize import word_tokenize +from nltk.stem import WordNetLemmatizer + +# Assure que les ressources nécessaires sont disponibles +try: + stopwords.words('english') + word_tokenize("test") + WordNetLemmatizer().lemmatize("testing") +except LookupError: + nltk.download('punkt') + nltk.download('punkt_tab') + nltk.download('stopwords') + nltk.download('wordnet') + nltk.download('omw-1.4') # Pour les lemmes WordNet en anglais + +# Convertir les labels : 0 pour négatif, 1 pour tout le reste +def convert_sentiment_label(df): + converted_target_data = df.copy() + converted_target_data['target'] = converted_target_data['target'].apply(lambda x: 0 if x == 0 else 1) + return converted_target_data + +# Réduction de la taille du dataset pour équilibrer +def downsample_data(df, n_samples=50000, random_state=42): + negative_samples = df[df['target'] == 0].sample(n=n_samples, random_state=random_state) + positive_samples = df[df['target'] == 1].sample(n=n_samples, random_state=random_state) + downsampled_data = pd.concat([negative_samples, positive_samples]) + return downsampled_data + +# Fonction principale de prétraitement +def preprocess_tweet(tweet): + if not isinstance(tweet, str): + return "" + + tweet = tweet.lower() + tweet = re.sub(r'https?://\S+|www\.\S+', '', tweet) + tweet = re.sub(r'@\w+', '', tweet) + tweet = re.sub(r'#(\w+)', r'# \1', tweet) + tweet = html.unescape(tweet) + tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet) + + tokens = word_tokenize(tweet) + + lemmatizer = WordNetLemmatizer() + tokens = [lemmatizer.lemmatize(token) for token in tokens] + + stop_words = set(stopwords.words('english')) + important_words = {'no', 'not', 'nor', 'neither', 'never', 'nobody', 'none', 'nothing', 'nowhere'} + stop_words = stop_words - important_words + tokens = [token for token in tokens if token not in stop_words] + + return ' '.join(tokens) + +# Traitement en parallèle sur un DataFrame +def process_in_parallel(df, func, n_jobs=4): + df_split = np.array_split(df, n_jobs) + with Pool(n_jobs) as pool: + df = pd.concat(pool.map(func, df_split)) + return df + +# Application du prétraitement sur une partition +def apply_preprocessing(df_part): + df_part['processed_text'] = df_part['text'].apply(preprocess_tweet) + return df_part