translation + blog draft

This commit is contained in:
Grégoire Mureau 2025-07-11 15:03:06 +02:00
parent f600e1bdb6
commit 365c3cc4d4
7 changed files with 211 additions and 49 deletions

View file

@ -9,10 +9,10 @@ st.set_page_config(
page_title="Air Paradis - Tweet Prediction", page_title="Air Paradis - Tweet Prediction",
page_icon="✈️", # optionnel, tu peux mettre un emoji ou un fichier image 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 # Initialisation des états
if "prediction_result" not in st.session_state: if "prediction_result" not in st.session_state:
@ -34,9 +34,9 @@ def reset_state():
st.session_state.last_report_sent = False st.session_state.last_report_sent = False
# Bouton d'analyse # Bouton d'analyse
if st.button("Analyser le sentiment"): if st.button("Analyze the sentiment"):
if not tweet.strip(): if not tweet.strip():
st.warning("Veuillez entrer un tweet non vide.") st.warning("Please type a valid tweet.")
reset_state() reset_state()
else: else:
try: try:
@ -54,7 +54,7 @@ if st.button("Analyser le sentiment"):
st.error(f"Erreur API : {response.status_code} - {response.text}") st.error(f"Erreur API : {response.status_code} - {response.text}")
reset_state() reset_state()
except Exception as e: 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() reset_state()
# Affichage du résultat # Affichage du résultat
@ -62,11 +62,11 @@ if st.session_state.prediction_result:
sentiment = st.session_state.prediction_result["sentiment"].capitalize() sentiment = st.session_state.prediction_result["sentiment"].capitalize()
prob = st.session_state.prediction_result["probability"] prob = st.session_state.prediction_result["probability"]
st.success(f"Sentiment : {sentiment}") 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 # Gestion du bouton signalement
if st.session_state.show_report_button and not st.session_state.report_sent: 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: try:
response = requests.post( response = requests.post(
f"{API_BASE_URL}/report_error", 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 # Forcer un re-run
st.rerun() st.rerun()
else: 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: 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 # Message si signalement déjà fait
elif st.session_state.report_sent: 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: if st.session_state.last_report_sent:
st.info("📩 Un rapport a été envoyé à l'administrateur du site.") st.info("📩 A complete report has been sent to the website administrator.")

View file

@ -22,7 +22,7 @@ def send_error_report_email(error_reports):
return False return False
try: try:
# Créer le message # Create message
msg = MIMEMultipart() msg = MIMEMultipart()
if smtp_from_alias is not None: if smtp_from_alias is not None:
smtp_from = smtp_from_alias smtp_from = smtp_from_alias
@ -32,55 +32,55 @@ def send_error_report_email(error_reports):
msg['To'] = admin_email msg['To'] = admin_email
msg['Subject'] = f"🚨 Rapport d'erreurs - Prédictions sentiments ({datetime.now().strftime('%d/%m/%Y %H:%M')})" 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 = { json_data = {
"rapport_genere_le": datetime.now().isoformat(), "report_generated_on": datetime.now().isoformat(),
"nombre_signalements": len(error_reports), "reports_amount": len(error_reports),
"signalements": [ "reports": [
{ {
"id": i, "id": i,
"tweet": tweet, "tweet": tweet,
"prediction_incorrecte": prediction, "incorrect_prediction": prediction,
"longueur_tweet": len(tweet) "tweet_length": len(tweet)
} }
for i, (tweet, prediction) in enumerate(error_reports.items(), 1) for i, (tweet, prediction) in enumerate(error_reports.items(), 1)
], ],
"metadata": { "metadata": {
"modele": "SentimentAnalysisLSTM", "model": "SentimentAnalysisLSTM",
"version_api": "1.0", "api_version": "1.0",
"seuil_rapport": 3 "reporting_threshold": 3
} }
} }
# Corps du message avec JSON intégré # Message body with embedded JSON
body = f""" 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)} 📊 Total number of reports : {len(error_reports)}
📅 Date du rapport : {datetime.now().strftime('%d/%m/%Y à %H:%M')} 📅 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): for i, (tweet, prediction) in enumerate(error_reports.items(), 1):
body += f"{i}. Tweet: \"{tweet[:100]}{'...' if len(tweet) > 100 else ''}\"\n" 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 += """ body += """
📈 Actions recommandées : 📈 Recommended actions:
- Analyser les tweets signalés pour identifier des patterns - Analyze reported tweets to identify patterns
- Considérer un réentraînement du modèle si nécessaire - Consider re-training the model if necessary
- Vérifier la qualité des données de training - 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) json_string = json.dumps(json_data, ensure_ascii=False, indent=2)
body += f"```json\n{json_string}\n```" 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, Sincerely,
🤖 API Air Paradis - Système de monitoring automatique 🤖 API Air Paradis - Automatic monitoring system
📧 Envoyé depuis : """ + smtp_from 📧 Sent from : """ + smtp_from
# Ajouter le corps du message # Add message body
msg.attach(MIMEText(body, 'plain', 'utf-8')) msg.attach(MIMEText(body, 'plain', 'utf-8'))
# Envoyer l'email # Sending email
with smtplib.SMTP(smtp_server, smtp_port) as server: with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls() server.starttls()
server.login(smtp_email, smtp_password) server.login(smtp_email, smtp_password)
server.send_message(msg) server.send_message(msg)
print(f"✅ Email de rapport envoyé avec succès depuis {smtp_from}") print(f"✅ Report email successfully sent from {smtp_from}")
print(f"📊 JSON intégré dans le corps du message") print("📊 JSON embedded in message body")
return True return True
except Exception as e: except Exception as e:
print(f"❌ Erreur lors de l'envoi de l'email : {e}") print(f"❌ Error during email sending : {e}")
return False return False

View file

@ -132,9 +132,9 @@ def predict_sentiment(request: TweetRequest):
sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1]) sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1])
prediction_prob = model.predict(sequence_padded)[0][0] prediction_prob = model.predict(sequence_padded)[0][0]
if prediction_prob >=0.5: if prediction_prob >=0.5:
sentiment = "positif" sentiment = "positive"
else: else:
sentiment = "negatif" sentiment = "negative"
# prediction_prob = 1-prediction_prob # prediction_prob = 1-prediction_prob
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {e}") raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {e}")

View file

@ -19,7 +19,7 @@ except LookupError:
def preprocess_tweet(tweet): 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): if not isinstance(tweet, str):
return "" return ""

View file

@ -17,14 +17,14 @@ load_dotenv(dotenv_path)
@patch("smtplib.SMTP") @patch("smtplib.SMTP")
def test_email_service_connection(mock_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 # 🔧 Configuration du mock
mock_server = MagicMock() mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server mock_smtp.return_value.__enter__.return_value = mock_server
test_reports = { test_reports = {
"Vol annulé sans prévenir": "positif" "Flight cancelled without notice": "positif"
} }
# Appel de la fonction # Appel de la fonction

View file

@ -15,7 +15,7 @@ def test_root_endpoint():
assert response.json() == {"message": "API Sentiment - Air Paradis - en ligne"} assert response.json() == {"message": "API Sentiment - Air Paradis - en ligne"}
def test_predict_sentiment(): 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 assert response.status_code == 200
data = response.json() data = response.json()
assert "sentiment" in data assert "sentiment" in data
@ -29,4 +29,4 @@ def test_missing_tweet():
def test_empty_tweet(): def test_empty_tweet():
response = client.post("/predict", json={"tweet": ""}) response = client.post("/predict", json={"tweet": ""})
assert response.status_code == 400 assert response.status_code == 400
assert response.json()["detail"] == "Tweet vide ou invalide après prétraitement." assert response.json()["detail"] == "Empty or invalid tweet after pre-processing."

162
docs/blog.md Normal file
View file

@ -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/)