From 365c3cc4d4c73a323aed9edf436dd92a89ee9c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Mureau?= Date: Fri, 11 Jul 2025 15:03:06 +0200 Subject: [PATCH] translation + blog draft --- deployment/api/app.py | 22 +-- deployment/api/email_service.py | 62 ++++---- deployment/api/main.py | 4 +- deployment/api/preprocessing.py | 2 +- deployment/api/tests/test_email_service.py | 4 +- deployment/api/tests/test_main.py | 4 +- docs/blog.md | 162 +++++++++++++++++++++ 7 files changed, 211 insertions(+), 49 deletions(-) create mode 100644 docs/blog.md diff --git a/deployment/api/app.py b/deployment/api/app.py index fc1a68e..99f5838 100644 --- a/deployment/api/app.py +++ b/deployment/api/app.py @@ -9,10 +9,10 @@ st.set_page_config( page_title="Air Paradis - Tweet Prediction", page_icon="✈️", # optionnel, tu peux mettre un emoji ou un fichier image ) -st.title("Analyse de Sentiment - Air Paradis") +st.title("Sentiment Analysis - Air Paradis") -tweet = st.text_area("Entrez votre tweet ici :", height=150) +tweet = st.text_area("Prompt your tweet here :", height=150) # Initialisation des états if "prediction_result" not in st.session_state: @@ -34,9 +34,9 @@ def reset_state(): st.session_state.last_report_sent = False # Bouton d'analyse -if st.button("Analyser le sentiment"): +if st.button("Analyze the sentiment"): if not tweet.strip(): - st.warning("Veuillez entrer un tweet non vide.") + st.warning("Please type a valid tweet.") reset_state() else: try: @@ -54,7 +54,7 @@ if st.button("Analyser le sentiment"): 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}") + st.error(f"Error during API request : {e}") reset_state() # Affichage du résultat @@ -62,11 +62,11 @@ 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é que le tweet soit positif: {prob:.2f}") + st.info(f"Positive tweet probability: {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"): + if st.button("Report a wrong prediction"): try: response = requests.post( f"{API_BASE_URL}/report_error", @@ -86,13 +86,13 @@ if st.session_state.show_report_button and not st.session_state.report_sent: # Forcer un re-run st.rerun() else: - st.error(f"Erreur API : {response.status_code} - {response.text}") + st.error(f"API Error : {response.status_code} - {response.text}") except Exception as e: - st.error(f"Erreur lors de la requête vers l'API : {e}") + st.error(f"Error during API request : {e}") # Message si signalement déjà fait elif st.session_state.report_sent: - st.success("✅ Merci pour votre signalement.") + st.success("✅ Thanks for your report.") if st.session_state.last_report_sent: - st.info("📩 Un rapport a été envoyé à l'administrateur du site.") \ No newline at end of file + st.info("📩 A complete report has been sent to the website administrator.") \ No newline at end of file diff --git a/deployment/api/email_service.py b/deployment/api/email_service.py index 5c66f28..2c2d82c 100644 --- a/deployment/api/email_service.py +++ b/deployment/api/email_service.py @@ -22,7 +22,7 @@ def send_error_report_email(error_reports): return False try: - # Créer le message + # Create message msg = MIMEMultipart() if smtp_from_alias is not None: smtp_from = smtp_from_alias @@ -32,55 +32,55 @@ def send_error_report_email(error_reports): msg['To'] = admin_email msg['Subject'] = f"🚨 Rapport d'erreurs - Prédictions sentiments ({datetime.now().strftime('%d/%m/%Y %H:%M')})" - # Préparer les données JSON + # Prepare JSON data json_data = { - "rapport_genere_le": datetime.now().isoformat(), - "nombre_signalements": len(error_reports), - "signalements": [ + "report_generated_on": datetime.now().isoformat(), + "reports_amount": len(error_reports), + "reports": [ { "id": i, "tweet": tweet, - "prediction_incorrecte": prediction, - "longueur_tweet": len(tweet) + "incorrect_prediction": prediction, + "tweet_length": len(tweet) } for i, (tweet, prediction) in enumerate(error_reports.items(), 1) ], "metadata": { - "modele": "SentimentAnalysisLSTM", - "version_api": "1.0", - "seuil_rapport": 3 + "model": "SentimentAnalysisLSTM", + "api_version": "1.0", + "reporting_threshold": 3 } } - # Corps du message avec JSON intégré + # Message body with embedded JSON body = f""" -Bonjour, +Hello, -Un nouveau rapport d'erreurs a été généré pour l'API de prédiction de sentiments Air Paradis. +A new error report has been generated for the Air Paradis sentiment prediction API. -📊 Nombre total de signalements : {len(error_reports)} -📅 Date du rapport : {datetime.now().strftime('%d/%m/%Y à %H:%M')} +📊 Total number of reports : {len(error_reports)} +📅 Report date : {datetime.now().strftime('%d/%m/%Y à %H:%M')} -📋 Résumé des signalements : +📋 Report summary : """ for i, (tweet, prediction) in enumerate(error_reports.items(), 1): body += f"{i}. Tweet: \"{tweet[:100]}{'...' if len(tweet) > 100 else ''}\"\n" - body += f" Prédiction signalée comme incorrecte: {prediction.upper()}\n\n" + body += f" Prediction reported as incorrect: {prediction.upper()}\n\n" body += """ -📈 Actions recommandées : -- Analyser les tweets signalés pour identifier des patterns -- Considérer un réentraînement du modèle si nécessaire -- Vérifier la qualité des données de training +📈 Recommended actions: +- Analyze reported tweets to identify patterns +- Consider re-training the model if necessary +- Check quality of training data -📊 DONNÉES COMPLÈTES (JSON) : +📊 COMPLETE DATA (JSON) : ═══════════════════════════════════════════════════════════════════════════════ """ - # ✅ Ajouter le JSON directement dans le corps + # ✅ Directly add JSON inside the body json_string = json.dumps(json_data, ensure_ascii=False, indent=2) body += f"```json\n{json_string}\n```" @@ -89,23 +89,23 @@ Un nouveau rapport d'erreurs a été généré pour l'API de prédiction de sent ═══════════════════════════════════════════════════════════════════════════════ --- -Cordialement, -🤖 API Air Paradis - Système de monitoring automatique -📧 Envoyé depuis : """ + smtp_from +Sincerely, +🤖 API Air Paradis - Automatic monitoring system +📧 Sent from : """ + smtp_from - # Ajouter le corps du message + # Add message body msg.attach(MIMEText(body, 'plain', 'utf-8')) - # Envoyer l'email + # Sending email with smtplib.SMTP(smtp_server, smtp_port) as server: server.starttls() server.login(smtp_email, smtp_password) server.send_message(msg) - print(f"✅ Email de rapport envoyé avec succès depuis {smtp_from}") - print(f"📊 JSON intégré dans le corps du message") + print(f"✅ Report email successfully sent from {smtp_from}") + print("📊 JSON embedded in message body") return True except Exception as e: - print(f"❌ Erreur lors de l'envoi de l'email : {e}") + print(f"❌ Error during email sending : {e}") return False \ No newline at end of file diff --git a/deployment/api/main.py b/deployment/api/main.py index a515a1f..20a22fa 100644 --- a/deployment/api/main.py +++ b/deployment/api/main.py @@ -132,9 +132,9 @@ def predict_sentiment(request: TweetRequest): sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1]) prediction_prob = model.predict(sequence_padded)[0][0] if prediction_prob >=0.5: - sentiment = "positif" + sentiment = "positive" else: - sentiment = "negatif" + sentiment = "negative" # prediction_prob = 1-prediction_prob except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {e}") diff --git a/deployment/api/preprocessing.py b/deployment/api/preprocessing.py index 5c54737..7f822e8 100644 --- a/deployment/api/preprocessing.py +++ b/deployment/api/preprocessing.py @@ -19,7 +19,7 @@ except LookupError: def preprocess_tweet(tweet): """ - Fonction de prétraitement des tweets pour l'API de sentiment analysis + Tweet pre-processing function for the Sentiment Analysis API """ if not isinstance(tweet, str): return "" diff --git a/deployment/api/tests/test_email_service.py b/deployment/api/tests/test_email_service.py index 926832d..226a52c 100644 --- a/deployment/api/tests/test_email_service.py +++ b/deployment/api/tests/test_email_service.py @@ -17,14 +17,14 @@ load_dotenv(dotenv_path) @patch("smtplib.SMTP") def test_email_service_connection(mock_smtp): """ - Teste la connexion SMTP sans envoyer réellement l'email + Test SMTP connection without actually sending the email """ # 🔧 Configuration du mock mock_server = MagicMock() mock_smtp.return_value.__enter__.return_value = mock_server test_reports = { - "Vol annulé sans prévenir": "positif" + "Flight cancelled without notice": "positif" } # Appel de la fonction diff --git a/deployment/api/tests/test_main.py b/deployment/api/tests/test_main.py index 8fc2914..1657e92 100644 --- a/deployment/api/tests/test_main.py +++ b/deployment/api/tests/test_main.py @@ -15,7 +15,7 @@ def test_root_endpoint(): assert response.json() == {"message": "API Sentiment - Air Paradis - en ligne"} def test_predict_sentiment(): - response = client.post("/predict", json={"tweet": "J'adore cette compagnie aérienne !"}) + response = client.post("/predict", json={"tweet": "I love this airline !"}) assert response.status_code == 200 data = response.json() assert "sentiment" in data @@ -29,4 +29,4 @@ def test_missing_tweet(): def test_empty_tweet(): response = client.post("/predict", json={"tweet": ""}) assert response.status_code == 400 - assert response.json()["detail"] == "Tweet vide ou invalide après prétraitement." \ No newline at end of file + assert response.json()["detail"] == "Empty or invalid tweet after pre-processing." \ No newline at end of file diff --git a/docs/blog.md b/docs/blog.md new file mode 100644 index 0000000..f6acc8b --- /dev/null +++ b/docs/blog.md @@ -0,0 +1,162 @@ +# ✨ Analyse de sentiments et déploiement MLOps : retour d’expérience complet + +Dans le cadre du projet *Air Paradis*, nous avons mis en œuvre une chaîne complète d’analyse de sentiments à partir de tweets, depuis la modélisation jusqu’au déploiement MLOps. Ce billet de blog revient sur l’ensemble des étapes réalisées, les choix méthodologiques, la comparaison des modèles et la mise en production d’une API. 🧠🔧 + +--- + +## 🔍 Trois approches de modélisation supervisée + +Nous avons testé trois approches complémentaires pour prédire le sentiment d’un tweet. Chacune s’appuie sur un type de représentation des textes et un algorithme différent : + +### 1. 🧱 Approche simple : Bag-of-Words (BoW) + modèles de machine learning + +Cette approche utilise des vecteurs BoW ou TF-IDF, qui capturent la fréquence des mots dans les tweets. On fait le choix d’éviter des techniques non supervisées comme **LDA**, car elles sont plus adaptées à des tâches de topic modeling et non de classification de sentiments. + +➡️ **Modèles testés :** +- Régression Logistique +- Naive Bayes +- SVM +- Random Forest + +Ces modèles ont été entraînés avec **GridSearchCV**, en optimisant la **précision (precision)**, notre KPI principal. + +📸 *[Insérer ici une capture d’écran MLFlow avec les scores des modèles simples]* + +--- + +### 2. 🔥 Approche avancée : Word Embeddings (Word2Vec & GloVe 300d) + LSTM + +On représente les mots non plus comme des fréquences mais comme des vecteurs denses, porteurs de signification. +Nous avons utilisé : +- **Word2Vec**, entraîné sur un grand corpus (skip-gram), +- **GloVe 300d**, embeddings précalculés par Stanford. + +Ces représentations sont **statistiquement proches** entre mots au sens sémantique. +Elles **ne tiennent cependant pas compte du contexte exact dans la phrase** : +> « banque » aura le même vecteur que ce soit pour une banque d'argent ou une banque de données. + +La classification se fait via un **LSTM** (Long Short-Term Memory), une architecture de **deep learning supervisée** adaptée aux séquences de mots. + +📸 *[Insérer ici une capture d’écran du graphe de précision sur validation pour GloVe+LSTM]* + +--- + +### 3. 🤖 BERT : Bidirectional Encoder Representations from Transformers + +BERT est un modèle de langage avancé, préentraîné par Google, capable de **comprendre le contexte bidirectionnel** d’un mot dans une phrase. Contrairement aux word embeddings classiques, BERT : +- **encode dynamiquement les mots** selon leur contexte d’usage (ex. « banque » n’aura pas le même vecteur selon la phrase), +- est entraîné par **masked language modeling** et **next sentence prediction**, puis fine-tuné pour la classification supervisée. + +Cependant, il a ses **limites** : +> ⚠️ BERT **ne comprend pas l’ironie, le sarcasme ou les références culturelles**. Il modélise des cooccurrences statistiques et non une compréhension intentionnelle ou implicite comme les LLM (ex : GPT). +> Sur Twitter, où le second degré est omniprésent, cela reste un **véritable défi pour les modèles** de ce type. + +📸 *[Insérer ici un graphique montrant la précision de BERT sur le jeu de validation]* + +--- + +## 📐 Choix du KPI : pourquoi la précision (precision) ? + +Nous avons fait le choix de nous focaliser sur la **précision** plutôt que l’**accuracy** : + +> 🎯 **Objectif métier :** éviter de **classer un tweet comme positif alors qu’il est négatif**, car cela pourrait induire une mauvaise communication ou une mauvaise interprétation dans un contexte sensible. + +### Pour chaque type de modèle : +- Modèles classiques → optimisation via **GridSearchCV sur la précision** +- Modèles DL → **EarlyStopping sur val_loss**, mais sélection du **meilleur modèle selon la précision** sur l’ensemble de validation. + +--- + +## ⚙️ Mise en place de l’environnement MLOps maison + +Nous avons choisi une **infrastructure MLOps souveraine et autonome** pour orchestrer les expérimentations. + +### 🏠 Serveur personnel (NAS) + OpenMediaVault +Nous avons déployé MLFlow sur un NAS personnel tournant sous **OpenMediaVault** (basé sur Debian). + +### 📦 Artefacts dans MinIO +Les modèles, métriques, visualisations et paramètres sont stockés dans **MinIO**, un système compatible S3. +➡️ MinIO est open-source, léger, et permet une **gestion souveraine des artefacts**, contrairement à AWS S3. + +> Les notebooks (en particulier ceux entraînés sur Google Colab, pour bénéficier de GPU) envoient automatiquement leurs logs vers MLFlow et leurs artefacts vers MinIO, via des **variables d’environnement** pour sécuriser les identifiants. + +📸 *[Insérer ici une capture du dashboard MLFlow]* + +--- + +## 🧪 Intégration continue, tests, versioning + +Une fois les modèles validés, nous avons implémenté toute la chaîne MLOps pour le déploiement API : + +### ✅ Tests automatisés +- **Tests unitaires** pour valider le prétraitement des tweets, les prédictions et la gestion des erreurs. +- Ces tests sont intégrés dans **GitHub Actions**, et **empêchent tout déploiement Docker si les tests échouent**. + +### 🐳 Conteneurisation & CI/CD +- L’API est packagée via **Docker**, puis poussée automatiquement sur **DockerHub** en cas de succès. +- Cela permet un déploiement cohérent et rapide sur n’importe quelle plateforme. + +--- + +## 🚀 Déploiement de l’API FastAPI + +### 🛠️ Backend : FastAPI +- Fournit un endpoint `/predict`, avec chargement dynamique du modèle via MLFlow et récupération des embeddings (BoW, GloVe ou Word2Vec) selon le tag `embedding_type`. + +### 💻 Frontend local : Streamlit +- Sert d’**interface utilisateur** pour tester les prédictions en local, simuler des requêtes API et visualiser les résultats. +- C’est aussi un outil utile pour les démonstrations ou le debug. + +📸 *[Insérer capture Streamlit en action]* + +--- + +### 🌐 Problème matériel sur le NAS +Le NAS utilise un processeur **Intel N100**, théoriquement compatible AVX2. +> ❗ Malheureusement, **certaines cartes mères désactivent cette instruction** au niveau BIOS, ce qui empêche l’exécution de TensorFlow, nécessaire pour le LSTM et BERT. + +🛠️ Solution : **déploiement sur Google Cloud Run**, à l’adresse suivante : +🔗 [https://sentiment-api-service-7772256003.europe-west1.run.app/](https://sentiment-api-service-7772256003.europe-west1.run.app/) + +📸 *[Insérer capture console Google Cloud / endpoint]* + +--- + +## 📊 Suivi de performance en production : Azure Application Insights + +### 🎯 Objectif : +Mettre en place une **observabilité** pour surveiller l’API une fois déployée. + +### 🧩 Suivi activé : +- Traces des requêtes entrantes/sortantes +- Temps de réponse +- Logs d’erreurs + +📸 *[Insérer capture Application Insight avec traces]* + +### 🔁 Et après ? Stratégie d’amélioration continue + +Voici une **démarche MLOps** possible pour affiner le modèle : + +1. **Suivi des erreurs en production** (ex. sentiment mal classé identifié par des utilisateurs) +2. **Stockage de ces cas edge** dans une base dédiée +3. **Retrain du modèle périodique** avec ces cas en plus +4. Comparaison des performances sur une **valset figée** +5. **Déploiement automatique** d’un nouveau modèle si KPIs dépassés + +--- + +## ✨ Conclusion + +Ce projet a permis d’explorer en profondeur les enjeux de classification de sentiments et les **limites des approches standards face à la richesse du langage naturel**, en particulier sur Twitter. +Il a aussi démontré l’intérêt d’un **pipeline MLOps souverain**, reproductible et automatisé. + +> ✅ En alliant des modèles puissants, des outils robustes comme MLFlow et FastAPI, et un déploiement maîtrisé, on pose les bases d’un produit de NLP industrialisable. + +📸 *[Insérer ici un schéma global du pipeline ou architecture finale]* + +--- + +💬 Merci pour votre lecture ! +Pour tester l’API : [https://sentiment-api-service-7772256003.europe-west1.run.app/](https://sentiment-api-service-7772256003.europe-west1.run.app/) +