Initial commit of scripts

This commit is contained in:
Grégoire Mureau 2025-11-10 16:40:12 +01:00
parent 96e22496ff
commit 85ae7f4855
26 changed files with 10279 additions and 10279 deletions

4
.gitignore vendored
View file

@ -1,2 +1,2 @@
**/mlruns
**/downloaded_artifacts
**/mlruns
**/downloaded_artifacts

View file

@ -1,62 +1,62 @@
# Projet Air Paradis – Analyse de sentiments
📘 Ce projet est également disponible en [anglais 🇬🇧](./README.md)
Ce dépôt contient les notebooks et le code source du projet d'analyse de sentiments sur des tweets, réalisé dans le cadre du programme OpenClassrooms **"Air Paradis"**.
---
## Contexte métier
L'objectif est de prédire si un tweet (ou une courte expression écrite) est perçu comme **positif ou négatif**, dans le contexte d'une compagnie aérienne.
Le projet repose sur le jeu de données public **Sentiment140**.
---
## Description du projet
Le projet vise à développer un système complet d'analyse de sentiments, comprenant :
- Une analyse exploratoire des données textuelles,
- Une comparaison rigoureuse de 10+ modèles de classification (classiques ML, LSTM + word embeddings, DistilBERT),
- Le déploiement d'une API REST FastAPI exposant le meilleur modèle avancé (LSTM + Word2Vec),
- La mise en place d'un pipeline MLOps complet : MLFlow, CI/CD (GitHub Actions), monitoring,
- Un hébergement local sur serveur NAS pour garantir la souveraineté des données.
📖 **Pour des informations détaillées sur les expérimentations menées et les résultats obtenus, consultez [docs/blog.fr.md](./docs/blog.fr.md).**
---
## Structure du dépôt
- `notebooks/` : notebooks d'analyse exploratoire et de modélisation,
- `deployment/` : code de l'API FastAPI, tests unitaires, Dockerfile et configuration déploiement,
- `docs/` : documentation détaillée (blog.fr.md), diagrammes, captures d'écran,
- `presentation/` : support de présentation du projet (PDF),
- `../.github/workflows/` : pipelines CI/CD (tests automatisés, build Docker, push DockerHub).
---
## Entraînement des modèles
Les phases d'entraînement ont principalement été réalisées sur **Google Colab**, afin de bénéficier de ressources GPU nécessaires aux modèles de deep learning.
---
## Utilisation
Les notebooks détaillent les étapes d'analyse et d'entraînement.
L'API est disponible dans le dossier `deployment/api` avec interface Streamlit de test.
Pour tester l'API en production : https://sentiment-api.greg-madman-nas.duckdns.org
---
## Remarques
- Les fichiers `requirements.txt` dans `notebooks/` proviennent des environnements Google Colab et peuvent contenir des dépendances peu optimisées pour un usage local.
- Le fichier `.env`, exclu du dépôt, contient les variables d'environnement nécessaires (configuration MLFlow, MinIO, alerting).
---
# Projet Air Paradis – Analyse de sentiments
📘 Ce projet est également disponible en [anglais 🇬🇧](./README.md)
Ce dépôt contient les notebooks et le code source du projet d'analyse de sentiments sur des tweets, réalisé dans le cadre du programme OpenClassrooms **"Air Paradis"**.
---
## Contexte métier
L'objectif est de prédire si un tweet (ou une courte expression écrite) est perçu comme **positif ou négatif**, dans le contexte d'une compagnie aérienne.
Le projet repose sur le jeu de données public **Sentiment140**.
---
## Description du projet
Le projet vise à développer un système complet d'analyse de sentiments, comprenant :
- Une analyse exploratoire des données textuelles,
- Une comparaison rigoureuse de 10+ modèles de classification (classiques ML, LSTM + word embeddings, DistilBERT),
- Le déploiement d'une API REST FastAPI exposant le meilleur modèle avancé (LSTM + Word2Vec),
- La mise en place d'un pipeline MLOps complet : MLFlow, CI/CD (GitHub Actions), monitoring,
- Un hébergement local sur serveur NAS pour garantir la souveraineté des données.
📖 **Pour des informations détaillées sur les expérimentations menées et les résultats obtenus, consultez [docs/blog.fr.md](./docs/blog.fr.md).**
---
## Structure du dépôt
- `notebooks/` : notebooks d'analyse exploratoire et de modélisation,
- `deployment/` : code de l'API FastAPI, tests unitaires, Dockerfile et configuration déploiement,
- `docs/` : documentation détaillée (blog.fr.md), diagrammes, captures d'écran,
- `presentation/` : support de présentation du projet (PDF),
- `../.github/workflows/` : pipelines CI/CD (tests automatisés, build Docker, push DockerHub).
---
## Entraînement des modèles
Les phases d'entraînement ont principalement été réalisées sur **Google Colab**, afin de bénéficier de ressources GPU nécessaires aux modèles de deep learning.
---
## Utilisation
Les notebooks détaillent les étapes d'analyse et d'entraînement.
L'API est disponible dans le dossier `deployment/api` avec interface Streamlit de test.
Pour tester l'API en production : https://sentiment-api.greg-madman-nas.duckdns.org
---
## Remarques
- Les fichiers `requirements.txt` dans `notebooks/` proviennent des environnements Google Colab et peuvent contenir des dépendances peu optimisées pour un usage local.
- Le fichier `.env`, exclu du dépôt, contient les variables d'environnement nécessaires (configuration MLFlow, MinIO, alerting).
---
**Projet réalisé dans le cadre du parcours "Data Scientist – Projet 7" d'OpenClassrooms**

122
README.md
View file

@ -1,62 +1,62 @@
# Air Paradis Project – Sentiment Analysis
📘 This project is also available in [French 🇫🇷](./README.fr.md)
This repository contains the notebooks and source code for the sentiment analysis project on tweets, conducted as part of the OpenClassrooms **"Air Paradis"** program.
---
## Business Context
The objective is to predict whether a tweet (or short written expression) is perceived as **positive or negative**, in the context of an airline company.
The project is based on the public **Sentiment140** dataset.
---
## Project Description
The project aims to develop a complete sentiment analysis system, including:
- Exploratory analysis of textual data,
- Rigorous comparison of 10+ classification models (classical ML, LSTM + word embeddings, DistilBERT),
- Deployment of a FastAPI REST API exposing the best advanced model (LSTM + Word2Vec),
- Implementation of a complete MLOps pipeline: MLFlow, CI/CD (GitHub Actions), monitoring,
- Local hosting on NAS server to ensure data sovereignty.
📖 **For detailed information on experiments conducted and results obtained, see [docs/blog.md](./docs/blog.md).**
---
## Repository Structure
- `notebooks/`: exploratory analysis and modeling notebooks,
- `deployment/`: FastAPI API code, unit tests, Dockerfile and deployment configuration,
- `docs/`: detailed documentation (blog.md), diagrams, screenshots,
- `presentation/`: project presentation slides (PDF),
- `../.github/workflows/`: CI/CD pipelines (automated tests, Docker build, DockerHub push).
---
## Model Training
Training phases were primarily conducted on **Google Colab** to leverage GPU resources required for deep learning models.
---
## Usage
Notebooks detail the analysis and training steps.
The API is available in the `deployment/api` folder with Streamlit testing interface.
To test the production API: https://sentiment-api.greg-madman-nas.duckdns.org
---
## Notes
- The `requirements.txt` files in `notebooks/` come from Google Colab environments and may contain dependencies not optimized for local use.
- The `.env` file, excluded from the repository, contains necessary environment variables (MLFlow, MinIO, alerting configuration).
---
# Air Paradis Project – Sentiment Analysis
📘 This project is also available in [French 🇫🇷](./README.fr.md)
This repository contains the notebooks and source code for the sentiment analysis project on tweets, conducted as part of the OpenClassrooms **"Air Paradis"** program.
---
## Business Context
The objective is to predict whether a tweet (or short written expression) is perceived as **positive or negative**, in the context of an airline company.
The project is based on the public **Sentiment140** dataset.
---
## Project Description
The project aims to develop a complete sentiment analysis system, including:
- Exploratory analysis of textual data,
- Rigorous comparison of 10+ classification models (classical ML, LSTM + word embeddings, DistilBERT),
- Deployment of a FastAPI REST API exposing the best advanced model (LSTM + Word2Vec),
- Implementation of a complete MLOps pipeline: MLFlow, CI/CD (GitHub Actions), monitoring,
- Local hosting on NAS server to ensure data sovereignty.
📖 **For detailed information on experiments conducted and results obtained, see [docs/blog.md](./docs/blog.md).**
---
## Repository Structure
- `notebooks/`: exploratory analysis and modeling notebooks,
- `deployment/`: FastAPI API code, unit tests, Dockerfile and deployment configuration,
- `docs/`: detailed documentation (blog.md), diagrams, screenshots,
- `presentation/`: project presentation slides (PDF),
- `../.github/workflows/`: CI/CD pipelines (automated tests, Docker build, DockerHub push).
---
## Model Training
Training phases were primarily conducted on **Google Colab** to leverage GPU resources required for deep learning models.
---
## Usage
Notebooks detail the analysis and training steps.
The API is available in the `deployment/api` folder with Streamlit testing interface.
To test the production API: https://sentiment-api.greg-madman-nas.duckdns.org
---
## Notes
- The `requirements.txt` files in `notebooks/` come from Google Colab environments and may contain dependencies not optimized for local use.
- The `.env` file, excluded from the repository, contains necessary environment variables (MLFlow, MinIO, alerting configuration).
---
**Project completed as part of OpenClassrooms "Data Scientist – Project 7" program**

View file

@ -1,128 +1,128 @@
# Déploiement de l’API – Projet Air Paradis
📘 Cette documentation est aussi disponible en [anglais 🇬🇧](./README.md)
Ce dossier contient le code et la configuration nécessaires au déploiement de l’API de prédiction de sentiment.
---
## 🎯 Objectif
Déployer une API REST permettant de :
- Prédire le **sentiment d’un tweet** (positif ou négatif) à partir d’un modèle LSTM entraîné sur des embeddings (Word2Vec ou GloVe),
- Signaler les erreurs de prédiction.
L’API utilise le **meilleur modèle enregistré** dans MLflow et le sert via **FastAPI**.
---
## 📦 Architecture globale
Schéma :
```
┌──────────────────────────────┐
│ Utilisateur final (UI) │
│ (interface Streamlit) │
└────────────┬─────────────────┘
│
[Appel HTTP vers /predict]
│
┌────────────▼─────────────┐
│ API FastAPI │
│ (serveur lancé via │
│ Uvicorn) │
└────────────┬─────────────┘
│
Chargement du modèle (MLflow)
│
Prétraitement du tweet
│
Prédiction (inférence)
│
Réponse JSON avec le sentiment
```
---
## 🧪 Fonctionnement de l’API
1. **Chargement du modèle**
- Le meilleur modèle est enregistré dans MLflow sous le nom `SentimentAnalysisLSTM`, avec le stage `Production`.
- L’API le charge au démarrage via l’URI :
```
models:/SentimentAnalysisLSTM/Production
```
2. **Prétraitement du texte**
- Nettoyage, tokenisation, lemmatisation, puis conversion en vecteurs d’embedding.
3. **Prédiction**
- Le modèle LSTM prédit le sentiment.
- Le résultat (`positive` ou `negative`) est retourné en JSON.
4. **Interface utilisateur (optionnelle)**
- Une interface Streamlit permet une interaction visuelle avec l’API.
---
## 🐳 Déploiement via Docker
Un `Dockerfile` est fourni pour simplifier le déploiement sur tout hôte compatible Docker (NAS, cloud, etc.).
L’image Docker est poussée sur DockerHub à chaque version validée (voir CI/CD plus bas).
Par souci de **souveraineté des données**, le déploiement est préféré sur un **NAS équipé d’un processeur Intel N100** (peu énergivore et suffisant pour l’inférence LSTM).
Le redéploiement peut être automatisé via :
- une tâche `cron` simple, ou
- un conteneur **Watchtower** (dans docker-compose ou kubernetes).
**Commande pour test local :**
```bash
docker build -t sentiment-api .
docker run --rm --env-file ../../.env -p 8000:8000 -p 8080:8080 sentiment-api
```
En production, les variables d’environnement doivent être définies dans le `docker-compose.yml` ou via un gestionnaire de secrets.
---
## 🛠 Problèmes AVX2 / Déploiement cloud
**Mise à jour :** au départ, le NAS ne supportait pas les instructions AVX2 nécessaires à TensorFlow.
En réalité, les CPU N100 les prennent en charge, mais certains fabricants désactivent cette option dans le BIOS.
En attendant un accès physique au NAS, nous avons opté pour un déploiement sur **Google Cloud**.
Problème : un seul port peut être exposé publiquement sur ces services.
Deux solutions possibles :
- Séparer le backend (FastAPI) et le frontend (Streamlit) en deux conteneurs,
- Utiliser un **reverse proxy** (ex. : nginx) pour rediriger les ports.
➡️ Nous avons choisi la **deuxième solution**, pour garder une **seule image Docker**.
**Mise à jour 2 :** AVX2 a été activé via le BIOS. Le déploiement est donc opérationnel **à la fois en local (NAS)** et **sur le cloud**, avec déploiement automatique pour le NAS (voir section suivante).
---
## 🔁 CI/CD – GitHub Actions
Un pipeline est défini dans `.github/workflows/` pour :
- Lancer les tests unitaires à chaque `push`,
- Vérifier la stabilité de l’API avant déploiement (NAS ou cloud),
- Construire et pousser l’image Docker sur DockerHub.
Les tests sont situés dans :
```
deployment/api/tests/
```
Le redéploiement automatique sur le NAS est géré via **Watchtower**, qui surveille les mises à jour de l’image Docker (fonctionne comme un `cron`, avec logs).
---
# Déploiement de l’API – Projet Air Paradis
📘 Cette documentation est aussi disponible en [anglais 🇬🇧](./README.md)
Ce dossier contient le code et la configuration nécessaires au déploiement de l’API de prédiction de sentiment.
---
## 🎯 Objectif
Déployer une API REST permettant de :
- Prédire le **sentiment d’un tweet** (positif ou négatif) à partir d’un modèle LSTM entraîné sur des embeddings (Word2Vec ou GloVe),
- Signaler les erreurs de prédiction.
L’API utilise le **meilleur modèle enregistré** dans MLflow et le sert via **FastAPI**.
---
## 📦 Architecture globale
Schéma :
```
┌──────────────────────────────┐
│ Utilisateur final (UI) │
│ (interface Streamlit) │
└────────────┬─────────────────┘
│
[Appel HTTP vers /predict]
│
┌────────────▼─────────────┐
│ API FastAPI │
│ (serveur lancé via │
│ Uvicorn) │
└────────────┬─────────────┘
│
Chargement du modèle (MLflow)
│
Prétraitement du tweet
│
Prédiction (inférence)
│
Réponse JSON avec le sentiment
```
---
## 🧪 Fonctionnement de l’API
1. **Chargement du modèle**
- Le meilleur modèle est enregistré dans MLflow sous le nom `SentimentAnalysisLSTM`, avec le stage `Production`.
- L’API le charge au démarrage via l’URI :
```
models:/SentimentAnalysisLSTM/Production
```
2. **Prétraitement du texte**
- Nettoyage, tokenisation, lemmatisation, puis conversion en vecteurs d’embedding.
3. **Prédiction**
- Le modèle LSTM prédit le sentiment.
- Le résultat (`positive` ou `negative`) est retourné en JSON.
4. **Interface utilisateur (optionnelle)**
- Une interface Streamlit permet une interaction visuelle avec l’API.
---
## 🐳 Déploiement via Docker
Un `Dockerfile` est fourni pour simplifier le déploiement sur tout hôte compatible Docker (NAS, cloud, etc.).
L’image Docker est poussée sur DockerHub à chaque version validée (voir CI/CD plus bas).
Par souci de **souveraineté des données**, le déploiement est préféré sur un **NAS équipé d’un processeur Intel N100** (peu énergivore et suffisant pour l’inférence LSTM).
Le redéploiement peut être automatisé via :
- une tâche `cron` simple, ou
- un conteneur **Watchtower** (dans docker-compose ou kubernetes).
**Commande pour test local :**
```bash
docker build -t sentiment-api .
docker run --rm --env-file ../../.env -p 8000:8000 -p 8080:8080 sentiment-api
```
En production, les variables d’environnement doivent être définies dans le `docker-compose.yml` ou via un gestionnaire de secrets.
---
## 🛠 Problèmes AVX2 / Déploiement cloud
**Mise à jour :** au départ, le NAS ne supportait pas les instructions AVX2 nécessaires à TensorFlow.
En réalité, les CPU N100 les prennent en charge, mais certains fabricants désactivent cette option dans le BIOS.
En attendant un accès physique au NAS, nous avons opté pour un déploiement sur **Google Cloud**.
Problème : un seul port peut être exposé publiquement sur ces services.
Deux solutions possibles :
- Séparer le backend (FastAPI) et le frontend (Streamlit) en deux conteneurs,
- Utiliser un **reverse proxy** (ex. : nginx) pour rediriger les ports.
➡️ Nous avons choisi la **deuxième solution**, pour garder une **seule image Docker**.
**Mise à jour 2 :** AVX2 a été activé via le BIOS. Le déploiement est donc opérationnel **à la fois en local (NAS)** et **sur le cloud**, avec déploiement automatique pour le NAS (voir section suivante).
---
## 🔁 CI/CD – GitHub Actions
Un pipeline est défini dans `.github/workflows/` pour :
- Lancer les tests unitaires à chaque `push`,
- Vérifier la stabilité de l’API avant déploiement (NAS ou cloud),
- Construire et pousser l’image Docker sur DockerHub.
Les tests sont situés dans :
```
deployment/api/tests/
```
Le redéploiement automatique sur le NAS est géré via **Watchtower**, qui surveille les mises à jour de l’image Docker (fonctionne comme un `cron`, avec logs).
---

View file

@ -1,126 +1,126 @@
# API Deployment – Air Paradis Project
📘 This documentation is also available in [French 🇫🇷](./README.fr.md)
This folder contains the code and configuration necessary to deploy the sentiment prediction API.
---
## 🎯 Objective
Deploy a REST API that allows to:
- Predict the sentiment of a tweet (positive or negative) using an LSTM model trained on embeddings (Word2Vec or GloVe),
- Report prediction errors.
The API uses the best-performing model registered on MLflow and serves it via FastAPI.
---
## 📦 Overall Architecture
Diagram:
```
┌──────────────────────────────┐
│ End user (UI) │
│ (Streamlit interface) │
└────────────┬─────────────────┘
│
[HTTP call to /predict]
│
┌────────────▼─────────────┐
│ FastAPI API │
│ (server launched via │
│ Uvicorn) │
└────────────┬─────────────┘
│
Model loading from MLflow
│
Tweet preprocessing
│
Prediction (inference)
│
Sentiment response (JSON)
```
---
## 🧪 API Workflow
1. **Model loading**
- The best performing model is registered in MLflow under a fixed name (`SentimentAnalysisLSTM`) and a `Production` stage.
- The API loads this model at startup using the URI:
```
models:/SentimentAnalysisLSTM/Production
```
2. **Text preprocessing**
- Text is cleaned, tokenized, lemmatized, and converted into vectors (embeddings).
3. **Prediction**
- The LSTM model (loaded from MLflow) makes a prediction.
- The result (`positive` or `negative`) is returned as JSON.
4. **User interface (optional)**
- A Streamlit interface allows visual interaction with the API.
---
## 🐳 Deployment with Docker
A `Dockerfile` is provided for easy deployment on any Docker-compatible host (NAS, cloud, etc.).
This Docker image should be pushed to DockerHub with every validated version (see CI/CD below).
For data sovereignty, deployment is preferably done on a **NAS with Intel N100** CPU (energy-efficient, sufficient for inference).
Docker can be automatically redeployed using:
- `cron` (simple), or
- `Watchtower` in docker-compose/kubernetes setups.
**Development command (local test):**
```bash
docker build -t sentiment-api .
docker run --rm --env-file ../../.env -p 8000:8000 -p 8080:8080 sentiment-api
```
In production (e.g. via docker-compose or cloud services), environment variables must be defined in the compose file or secret manager.
---
## 🛠 Notes on AVX2 / Cloud Deployment
**Update:** the NAS initially failed due to lack of AVX2 support. Some N100-based NAS motherboards disable AVX2 in BIOS.
When not accessible, fallback deployment is done on **Google Cloud**.
Issue: public cloud services do **not allow multiple ports** to be exposed easily.
Two solutions:
- Split into two containers: FastAPI + Streamlit,
- Use a **reverse proxy** (e.g., Nginx) to map ports.
➡️ We chose the second option to keep a **single Docker image**.
**Update 2:** AVX2 was enabled via BIOS. Deployment is now active **both locally (NAS)** and **on cloud**, with continuous deployment on NAS (see section below).
---
## 🔁 CI/CD – GitHub Actions
A pipeline is defined in `.github/workflows/` to:
- Run unit tests on every push,
- Check API health before deployment (local/cloud),
- Build and push Docker image to DockerHub.
Tests are defined in:
```
deployment/api/tests/
```
Docker image auto-redeployment on NAS is handled by **Watchtower**, which checks DockerHub updates (like a cron job, with logs).
---
# API Deployment – Air Paradis Project
📘 This documentation is also available in [French 🇫🇷](./README.fr.md)
This folder contains the code and configuration necessary to deploy the sentiment prediction API.
---
## 🎯 Objective
Deploy a REST API that allows to:
- Predict the sentiment of a tweet (positive or negative) using an LSTM model trained on embeddings (Word2Vec or GloVe),
- Report prediction errors.
The API uses the best-performing model registered on MLflow and serves it via FastAPI.
---
## 📦 Overall Architecture
Diagram:
```
┌──────────────────────────────┐
│ End user (UI) │
│ (Streamlit interface) │
└────────────┬─────────────────┘
│
[HTTP call to /predict]
│
┌────────────▼─────────────┐
│ FastAPI API │
│ (server launched via │
│ Uvicorn) │
└────────────┬─────────────┘
│
Model loading from MLflow
│
Tweet preprocessing
│
Prediction (inference)
│
Sentiment response (JSON)
```
---
## 🧪 API Workflow
1. **Model loading**
- The best performing model is registered in MLflow under a fixed name (`SentimentAnalysisLSTM`) and a `Production` stage.
- The API loads this model at startup using the URI:
```
models:/SentimentAnalysisLSTM/Production
```
2. **Text preprocessing**
- Text is cleaned, tokenized, lemmatized, and converted into vectors (embeddings).
3. **Prediction**
- The LSTM model (loaded from MLflow) makes a prediction.
- The result (`positive` or `negative`) is returned as JSON.
4. **User interface (optional)**
- A Streamlit interface allows visual interaction with the API.
---
## 🐳 Deployment with Docker
A `Dockerfile` is provided for easy deployment on any Docker-compatible host (NAS, cloud, etc.).
This Docker image should be pushed to DockerHub with every validated version (see CI/CD below).
For data sovereignty, deployment is preferably done on a **NAS with Intel N100** CPU (energy-efficient, sufficient for inference).
Docker can be automatically redeployed using:
- `cron` (simple), or
- `Watchtower` in docker-compose/kubernetes setups.
**Development command (local test):**
```bash
docker build -t sentiment-api .
docker run --rm --env-file ../../.env -p 8000:8000 -p 8080:8080 sentiment-api
```
In production (e.g. via docker-compose or cloud services), environment variables must be defined in the compose file or secret manager.
---
## 🛠 Notes on AVX2 / Cloud Deployment
**Update:** the NAS initially failed due to lack of AVX2 support. Some N100-based NAS motherboards disable AVX2 in BIOS.
When not accessible, fallback deployment is done on **Google Cloud**.
Issue: public cloud services do **not allow multiple ports** to be exposed easily.
Two solutions:
- Split into two containers: FastAPI + Streamlit,
- Use a **reverse proxy** (e.g., Nginx) to map ports.
➡️ We chose the second option to keep a **single Docker image**.
**Update 2:** AVX2 was enabled via BIOS. Deployment is now active **both locally (NAS)** and **on cloud**, with continuous deployment on NAS (see section below).
---
## 🔁 CI/CD – GitHub Actions
A pipeline is defined in `.github/workflows/` to:
- Run unit tests on every push,
- Check API health before deployment (local/cloud),
- Build and push Docker image to DockerHub.
Tests are defined in:
```
deployment/api/tests/
```
Docker image auto-redeployment on NAS is handled by **Watchtower**, which checks DockerHub updates (like a cron job, with logs).
---

View file

@ -1,22 +1,22 @@
# Ignorer les caches Python
__pycache__/
*.pyc
# Ignorer les artefacts temporaires
downloaded_artifacts/
mlruns/
logs/
# Ignorer les tests unitaires
tests/
# Ignorer les variables d'environnement. Elles seront plutôt chargées dans la commande docker run `docker run --env-file ./deployment/api/.env -p 8000:8000 sentiment-api` en développement local, et via des variables définies dans le yaml
*.env
# Ignorer fichier conf amazon web services
*aws*
.elasticbeanstalk
sentiment-api-*.json
docker-compose.yml.example
# Ignorer les caches Python
__pycache__/
*.pyc
# Ignorer les artefacts temporaires
downloaded_artifacts/
mlruns/
logs/
# Ignorer les tests unitaires
tests/
# Ignorer les variables d'environnement. Elles seront plutôt chargées dans la commande docker run `docker run --env-file ./deployment/api/.env -p 8000:8000 sentiment-api` en développement local, et via des variables définies dans le yaml
*.env
# Ignorer fichier conf amazon web services
*aws*
.elasticbeanstalk
sentiment-api-*.json
docker-compose.yml.example

View file

@ -1,6 +1,6 @@
# ssh .pem files
*.pem
*aws*
sentiment-api-*.json
# ssh .pem files
*.pem
*aws*
sentiment-api-*.json

View file

@ -1,42 +1,42 @@
FROM python:3.12-slim
WORKDIR /app
# # Install nginx and system packages
RUN apt-get update && \
apt-get install -y nginx curl && \
# Créer l'utilisateur et le groupe www-data si ce n'est pas déjà fait par l'image de base
# Assurer que l'utilisateur nginx (souvent www-data) a un ID consistent
adduser --system --no-create-home --group www-data || true && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install nltk resources at build time
RUN python3 -m nltk.downloader punkt \
&& python3 -m nltk.downloader punkt_tab \
&& python3 -m nltk.downloader stopwords \
&& python3 -m nltk.downloader wordnet \
&& python3 -m nltk.downloader omw-1.4
# Copy app code and configs
COPY . .
# Copy nginx config - l'emplacement par défaut de Nginx est /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/nginx.conf
# Pas besoin de créer les dossiers ici, le start.sh s'en chargera avec les bonnes permissions
RUN mkdir -p /var/log/nginx /var/lib/nginx /run
# Ensure script is executable
RUN chmod +x /app/start.sh
# Expose only NGINX port (Cloud Run utilise 8080 par défaut)
EXPOSE 8080
# Start everything via script
CMD ["/app/start.sh"]
FROM python:3.12-slim
WORKDIR /app
# # Install nginx and system packages
RUN apt-get update && \
apt-get install -y nginx curl && \
# Créer l'utilisateur et le groupe www-data si ce n'est pas déjà fait par l'image de base
# Assurer que l'utilisateur nginx (souvent www-data) a un ID consistent
adduser --system --no-create-home --group www-data || true && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install nltk resources at build time
RUN python3 -m nltk.downloader punkt \
&& python3 -m nltk.downloader punkt_tab \
&& python3 -m nltk.downloader stopwords \
&& python3 -m nltk.downloader wordnet \
&& python3 -m nltk.downloader omw-1.4
# Copy app code and configs
COPY . .
# Copy nginx config - l'emplacement par défaut de Nginx est /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/nginx.conf
# Pas besoin de créer les dossiers ici, le start.sh s'en chargera avec les bonnes permissions
RUN mkdir -p /var/log/nginx /var/lib/nginx /run
# Ensure script is executable
RUN chmod +x /app/start.sh
# Expose only NGINX port (Cloud Run utilise 8080 par défaut)
EXPOSE 8080
# Start everything via script
CMD ["/app/start.sh"]

View file

@ -1,124 +1,124 @@
import os
import streamlit as st
import requests
API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:8080") + "/api"
st.set_page_config(
page_title="Air Paradis - Tweet Prediction",
page_icon="✈️", # optionnel, tu peux mettre un emoji ou un fichier image
)
st.title("Sentiment Analysis - Air Paradis")
tweet = st.text_area("Prompt your tweet here :", 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("Analyze the sentiment"):
if not tweet.strip():
st.warning("Please type a valid tweet.")
reset_state()
else:
try:
response = requests.post(
f"{API_BASE_URL}/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"Error during API request : {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"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("Report a wrong prediction"):
try:
response = requests.post(
f"{API_BASE_URL}/report_error",
json={
"tweet": tweet,
"prediction": sentiment,
"probability": st.session_state.prediction_result["probability"] # Transmettre la probabilité
}
)
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"API Error : {response.status_code} - {response.text}")
except Exception as e:
st.error(f"Error during API request : {e}")
# Message si signalement déjà fait
elif st.session_state.report_sent:
st.success("✅ Thanks for your report.")
if st.session_state.last_report_sent:
st.info("📩 A complete report has been sent to the website administrator.")
st.markdown(
"""
<style>
.floating-message {
position: fixed;
bottom: 10px;
right: 10px;
background-color: #f0f0f0;
color: #333333;
padding: 10px 15px;
border-radius: 8px;
font-size: 0.85rem;
box-shadow: 0 0 8px rgba(0,0,0,0.15);
max-width: 300px;
z-index: 1000;
opacity: 0.85;
}
</style>
<div class="floating-message">
Reported wrong predictions help improve the model. Please avoid including personal information.
</div>
""",
unsafe_allow_html=True,
)
import os
import streamlit as st
import requests
API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:8080") + "/api"
st.set_page_config(
page_title="Air Paradis - Tweet Prediction",
page_icon="✈️", # optionnel, tu peux mettre un emoji ou un fichier image
)
st.title("Sentiment Analysis - Air Paradis")
tweet = st.text_area("Prompt your tweet here :", 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("Analyze the sentiment"):
if not tweet.strip():
st.warning("Please type a valid tweet.")
reset_state()
else:
try:
response = requests.post(
f"{API_BASE_URL}/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"Error during API request : {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"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("Report a wrong prediction"):
try:
response = requests.post(
f"{API_BASE_URL}/report_error",
json={
"tweet": tweet,
"prediction": sentiment,
"probability": st.session_state.prediction_result["probability"] # Transmettre la probabilité
}
)
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"API Error : {response.status_code} - {response.text}")
except Exception as e:
st.error(f"Error during API request : {e}")
# Message si signalement déjà fait
elif st.session_state.report_sent:
st.success("✅ Thanks for your report.")
if st.session_state.last_report_sent:
st.info("📩 A complete report has been sent to the website administrator.")
st.markdown(
"""
<style>
.floating-message {
position: fixed;
bottom: 10px;
right: 10px;
background-color: #f0f0f0;
color: #333333;
padding: 10px 15px;
border-radius: 8px;
font-size: 0.85rem;
box-shadow: 0 0 8px rgba(0,0,0,0.15);
max-width: 300px;
z-index: 1000;
opacity: 0.85;
}
</style>
<div class="floating-message">
Reported wrong predictions help improve the model. Please avoid including personal information.
</div>
""",
unsafe_allow_html=True,
)

View file

@ -1,32 +1,32 @@
services:
api:
build:
context: ./deployment/api
dockerfile: Dockerfile
container_name: sentiment_api
ports:
- "8000:8000" # API FastAPI
- "8501:8501" # Streamlit
environment:
MLFLOW_TRACKING_URI: "${MLFLOW_TRACKING_URI}" # ex: http://mlflow:5000
MLFLOW_S3_ENDPOINT_URL: "${MLFLOW_S3_ENDPOINT_URL}" # ex: url MinIO ou S3
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" # clé ID S3
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" # clé secrète S3
SMTP_SERVER: "${SMTP_SERVER}" # serveur SMTP
SMTP_PORT: "${SMTP_PORT}" # port SMTP (ex: 587)
SMTP_EMAIL: "${SMTP_EMAIL}" # email expéditeur
SMTP_PASSWORD: "${SMTP_PASSWORD}" # mot de passe SMTP
SMTP_FROM_ALIAS: "${SMTP_FROM_ALIAS}" # alias (optionnel)
ADMIN_EMAIL: "${ADMIN_EMAIL}" # email admin destinataire
SENTIMENT_API_BASE_URL: "${SENTIMENT_API_BASE_URL}" # public api url (used to redirect Streamlit requests to FastAPI)
# only if you have a reverse proxy server
# networks:
# - swag
restart: always
# only if you have a reverse proxy server
# networks:
# swag:
# external: true
services:
api:
build:
context: ./deployment/api
dockerfile: Dockerfile
container_name: sentiment_api
ports:
- "8000:8000" # API FastAPI
- "8501:8501" # Streamlit
environment:
MLFLOW_TRACKING_URI: "${MLFLOW_TRACKING_URI}" # ex: http://mlflow:5000
MLFLOW_S3_ENDPOINT_URL: "${MLFLOW_S3_ENDPOINT_URL}" # ex: url MinIO ou S3
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" # clé ID S3
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" # clé secrète S3
SMTP_SERVER: "${SMTP_SERVER}" # serveur SMTP
SMTP_PORT: "${SMTP_PORT}" # port SMTP (ex: 587)
SMTP_EMAIL: "${SMTP_EMAIL}" # email expéditeur
SMTP_PASSWORD: "${SMTP_PASSWORD}" # mot de passe SMTP
SMTP_FROM_ALIAS: "${SMTP_FROM_ALIAS}" # alias (optionnel)
ADMIN_EMAIL: "${ADMIN_EMAIL}" # email admin destinataire
SENTIMENT_API_BASE_URL: "${SENTIMENT_API_BASE_URL}" # public api url (used to redirect Streamlit requests to FastAPI)
# only if you have a reverse proxy server
# networks:
# - swag
restart: always
# only if you have a reverse proxy server
# networks:
# swag:
# external: true

View file

@ -1,111 +1,111 @@
import smtplib
import os
import json
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
def send_error_report_email(error_reports):
"""
Envoie un email avec le rapport d'erreurs et le JSON intégré
"""
smtp_server = os.getenv("SMTP_SERVER")
smtp_port = int(os.getenv("SMTP_PORT", 587))
smtp_email = os.getenv("SMTP_EMAIL") # main email address. If smt_from_alias is not, it will also be the mail sender
smtp_password = os.getenv("SMTP_PASSWORD")
smtp_from_alias = os.getenv("SMTP_FROM_ALIAS", smtp_email) # mail sender if not none (alias)
admin_email = os.getenv("ADMIN_EMAIL") # this one will receive the mail
if not all([smtp_server, smtp_email, smtp_password, admin_email]):
print("❌ Configuration email manquante dans .env")
return False
try:
# Create message
msg = MIMEMultipart()
if smtp_from_alias is not None:
smtp_from = smtp_from_alias
else:
smtp_from = smtp_email
msg['From'] = f"Air Paradis Monitor <{smtp_from}>"
msg['To'] = admin_email
msg['Subject'] = f"🚨 Rapport d'erreurs - Prédictions sentiments ({datetime.now().strftime('%d/%m/%Y %H:%M')})"
# Prepare JSON data
json_data = {
"report_generated_on": datetime.now().isoformat(),
"reports_amount": len(error_reports),
"reports": [
{
"id": i,
"tweet": tweet,
"incorrect_prediction": prediction,
"tweet_length": len(tweet)
}
for i, (tweet, prediction) in enumerate(error_reports.items(), 1)
],
"metadata": {
"model": "SentimentAnalysisLSTM",
"api_version": "1.0",
"reporting_threshold": 3
}
}
# Message body with embedded JSON
body = f"""
Hello,
A new error report has been generated for the Air Paradis sentiment prediction API.
📊 Total number of reports : {len(error_reports)}
📅 Report date : {datetime.now().strftime('%d/%m/%Y à %H:%M')}
📋 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" Prediction reported as incorrect: {prediction.upper()}\n\n"
body += """
📈 Recommended actions:
- Analyze reported tweets to identify patterns
- Consider re-training the model if necessary
- Check quality of training data
📊 COMPLETE DATA (JSON) :
═══════════════════════════════════════════════════════════════════════════════
"""
# ✅ Directly add JSON inside the body
json_string = json.dumps(json_data, ensure_ascii=False, indent=2)
body += f"```json\n{json_string}\n```"
body += """
═══════════════════════════════════════════════════════════════════════════════
---
Sincerely,
🤖 API Air Paradis - Automatic monitoring system
📧 Sent from : """ + smtp_from
# Add message body
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# 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"✅ Report email successfully sent from {smtp_from}")
print("📊 JSON embedded in message body")
return True
except Exception as e:
print(f"❌ Error during email sending : {e}")
import smtplib
import os
import json
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
def send_error_report_email(error_reports):
"""
Envoie un email avec le rapport d'erreurs et le JSON intégré
"""
smtp_server = os.getenv("SMTP_SERVER")
smtp_port = int(os.getenv("SMTP_PORT", 587))
smtp_email = os.getenv("SMTP_EMAIL") # main email address. If smt_from_alias is not, it will also be the mail sender
smtp_password = os.getenv("SMTP_PASSWORD")
smtp_from_alias = os.getenv("SMTP_FROM_ALIAS", smtp_email) # mail sender if not none (alias)
admin_email = os.getenv("ADMIN_EMAIL") # this one will receive the mail
if not all([smtp_server, smtp_email, smtp_password, admin_email]):
print("❌ Configuration email manquante dans .env")
return False
try:
# Create message
msg = MIMEMultipart()
if smtp_from_alias is not None:
smtp_from = smtp_from_alias
else:
smtp_from = smtp_email
msg['From'] = f"Air Paradis Monitor <{smtp_from}>"
msg['To'] = admin_email
msg['Subject'] = f"🚨 Rapport d'erreurs - Prédictions sentiments ({datetime.now().strftime('%d/%m/%Y %H:%M')})"
# Prepare JSON data
json_data = {
"report_generated_on": datetime.now().isoformat(),
"reports_amount": len(error_reports),
"reports": [
{
"id": i,
"tweet": tweet,
"incorrect_prediction": prediction,
"tweet_length": len(tweet)
}
for i, (tweet, prediction) in enumerate(error_reports.items(), 1)
],
"metadata": {
"model": "SentimentAnalysisLSTM",
"api_version": "1.0",
"reporting_threshold": 3
}
}
# Message body with embedded JSON
body = f"""
Hello,
A new error report has been generated for the Air Paradis sentiment prediction API.
📊 Total number of reports : {len(error_reports)}
📅 Report date : {datetime.now().strftime('%d/%m/%Y à %H:%M')}
📋 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" Prediction reported as incorrect: {prediction.upper()}\n\n"
body += """
📈 Recommended actions:
- Analyze reported tweets to identify patterns
- Consider re-training the model if necessary
- Check quality of training data
📊 COMPLETE DATA (JSON) :
═══════════════════════════════════════════════════════════════════════════════
"""
# ✅ Directly add JSON inside the body
json_string = json.dumps(json_data, ensure_ascii=False, indent=2)
body += f"```json\n{json_string}\n```"
body += """
═══════════════════════════════════════════════════════════════════════════════
---
Sincerely,
🤖 API Air Paradis - Automatic monitoring system
📧 Sent from : """ + smtp_from
# Add message body
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# 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"✅ Report email successfully sent from {smtp_from}")
print("📊 JSON embedded in message body")
return True
except Exception as e:
print(f"❌ Error during email sending : {e}")
return False

View file

@ -1,210 +1,210 @@
import os
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
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.sequence import pad_sequences
import pandas as pd
from datetime import datetime, timedelta
# # TO UNCOMMENT FOR DEVELOPMENT. OTHERWISE, LEAVE AS IS
# # Access the .env file
# PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
# sys.path.append(PARENT_DIR)
# # 🔐 Load environment variables (.env two levels above)
# load_dotenv()
# 🔁 Import the preprocessing function (LOCAL)
from preprocessing import preprocess_tweet
# 📧 Import the email service
from email_service import send_error_report_email
# 🔗 Configure MLflow
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI")
if mlflow_tracking_uri:
mlflow.set_tracking_uri(mlflow_tracking_uri)
else:
print("⚠️ MLFLOW_TRACKING_URI not defined in .env")
# 📌 Model name and stage
MODEL_NAME = "SentimentAnalysisLSTM"
STAGE = "Production"
# 🧾 Structure of POST requests
class TweetRequest(BaseModel):
tweet: str
class ReportRequest(BaseModel):
tweet: str
prediction: str
probability: float
# 🧠 Storage for error reports
error_reports = {}
# 🔍 Function to load model + 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"No version found for model '{model_name}' in stage '{stage}'")
version_info = versions[0]
run_id = version_info.run_id
if not run_id:
raise RuntimeError("Run ID not found.")
# Extract embedding tag
model_version_info = client.get_model_version(model_name, version_info.version)
# glove is used as default if embedding_type is not found
embedding_type = model_version_info.tags.get("embedding_type", "glove").lower()
# Determine paths to artifacts
local_dir = "./downloaded_artifacts"
os.makedirs(local_dir, exist_ok=True)
base_path = "local_artifacts"
tokenizer_file = "tokenizer.pickle"
if "glove" in embedding_type:
keras_file = "final_model_LSTM_GloVe-300d-Fige.keras"
elif "word2vec" in embedding_type:
keras_file = "final_model_LSTM_Word2Vec-Fige.keras"
else:
raise ValueError(f"Unknown embedding type: {embedding_type}")
print(f"{embedding_type} model found. Downloading model associated with run_id: {run_id}")
def dl(path):
return client.download_artifacts(run_id=run_id, path=f"{base_path}/{path}", dst_path=local_dir)
# 🔽 Download artifacts
model_path = dl(keras_file)
tokenizer_path = dl(tokenizer_file)
print("Contents of downloaded_artifacts:", os.listdir(local_dir))
# 📦 Load
model = load_model(model_path)
with open(tokenizer_path, "rb") as f:
tokenizer = pickle.load(f)
return model, tokenizer, embedding_type
# 🚀 Launch the API
app = FastAPI(title="Sentiment Prediction API - Air Paradis")
# app = FastAPI(title="Sentiment Prediction API - Air Paradis",
# root_path="/api")
try:
model, tokenizer, embedding_type = load_model_and_artifacts()
except Exception as e:
print(f"\n❌ Error loading artifacts: {e}")
model, tokenizer = None, None
@app.get("/")
def root():
return {"message": "Sentiment API - Air Paradis - online"}
@app.post("/predict")
def predict_sentiment(request: TweetRequest):
if model is None or tokenizer is None:
raise HTTPException(status_code=503, detail="Model or artifacts unavailable.")
try:
processed_tweet = preprocess_tweet(request.tweet)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Preprocessing error: {e}")
if not processed_tweet.strip():
raise HTTPException(status_code=400, detail="Tweet is empty or invalid after preprocessing.")
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]
if prediction_prob >= 0.5:
sentiment = "positive"
else:
sentiment = "negative"
# prediction_prob = 1 - prediction_prob
except Exception as e:
raise HTTPException(status_code=500, detail=f"Inference error: {e}")
return {
"sentiment": sentiment,
"probability": float(prediction_prob)
}
# Replace the dict with a global empty DataFrame initially
error_reports_df = pd.DataFrame(columns=["tweet", "prediction", "probability", "timestamp"])
@app.post("/report_error")
def report_error(request: ReportRequest):
global error_reports_df
tweet = request.tweet.strip()
prediction = request.prediction.strip().lower()
probability = request.probability
if not tweet or not prediction:
raise HTTPException(status_code=400, detail="Missing tweet or prediction.")
if probability < 0 or probability > 1:
raise HTTPException(status_code=400, detail="Invalid probability. Must be between 0 and 1.")
now = datetime.utcnow()
# Check if tweet already exists to avoid duplicates
existing_idx = error_reports_df.index[error_reports_df["tweet"] == tweet].tolist()
if existing_idx:
# Update existing row
idx = existing_idx[0]
error_reports_df.at[idx, "prediction"] = prediction + f' (p = {probability:.2f} ) '
error_reports_df.at[idx, "probability"] = probability
error_reports_df.at[idx, "timestamp"] = now
else:
# Add new row
new_row = {
"tweet": tweet,
"prediction": prediction + f' (p = {probability:.2f} ) ',
"probability": probability,
"timestamp": now
}
error_reports_df = pd.concat([error_reports_df, pd.DataFrame([new_row])], ignore_index=True)
report_sent = False
# Condition: every 3 reports and max 5 min interval between the last 3
try:
if len(error_reports_df) % 3 == 0:
last_three = error_reports_df.sort_values("timestamp", ascending=False).head(3)
times = last_three["timestamp"].sort_values()
delta = times.iloc[-1] - times.iloc[0]
if delta <= timedelta(minutes=5):
reports_dict = dict(zip(
last_three["tweet"],
last_three["prediction"]
))
print(f"📧 Attempting to send email for {len(error_reports_df)} reports...")
email_success = send_error_report_email(reports_dict)
if email_success:
print(f"✅ Email successfully sent for {len(error_reports_df)} reports")
report_sent = True
# DELETE the last 3 reports sent
# Identify indexes to delete
to_remove_idx = last_three.index
error_reports_df = error_reports_df.drop(to_remove_idx).reset_index(drop=True)
else:
print(f"❌ Failed to send email for {len(error_reports_df)} reports")
except Exception as e:
print(f"❌ Error while sending email: {e}")
# Continue even if it fails
return {"report_sent": report_sent}
import os
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
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.sequence import pad_sequences
import pandas as pd
from datetime import datetime, timedelta
# # TO UNCOMMENT FOR DEVELOPMENT. OTHERWISE, LEAVE AS IS
# # Access the .env file
# PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
# sys.path.append(PARENT_DIR)
# # 🔐 Load environment variables (.env two levels above)
# load_dotenv()
# 🔁 Import the preprocessing function (LOCAL)
from preprocessing import preprocess_tweet
# 📧 Import the email service
from email_service import send_error_report_email
# 🔗 Configure MLflow
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI")
if mlflow_tracking_uri:
mlflow.set_tracking_uri(mlflow_tracking_uri)
else:
print("⚠️ MLFLOW_TRACKING_URI not defined in .env")
# 📌 Model name and stage
MODEL_NAME = "SentimentAnalysisLSTM"
STAGE = "Production"
# 🧾 Structure of POST requests
class TweetRequest(BaseModel):
tweet: str
class ReportRequest(BaseModel):
tweet: str
prediction: str
probability: float
# 🧠 Storage for error reports
error_reports = {}
# 🔍 Function to load model + 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"No version found for model '{model_name}' in stage '{stage}'")
version_info = versions[0]
run_id = version_info.run_id
if not run_id:
raise RuntimeError("Run ID not found.")
# Extract embedding tag
model_version_info = client.get_model_version(model_name, version_info.version)
# glove is used as default if embedding_type is not found
embedding_type = model_version_info.tags.get("embedding_type", "glove").lower()
# Determine paths to artifacts
local_dir = "./downloaded_artifacts"
os.makedirs(local_dir, exist_ok=True)
base_path = "local_artifacts"
tokenizer_file = "tokenizer.pickle"
if "glove" in embedding_type:
keras_file = "final_model_LSTM_GloVe-300d-Fige.keras"
elif "word2vec" in embedding_type:
keras_file = "final_model_LSTM_Word2Vec-Fige.keras"
else:
raise ValueError(f"Unknown embedding type: {embedding_type}")
print(f"{embedding_type} model found. Downloading model associated with run_id: {run_id}")
def dl(path):
return client.download_artifacts(run_id=run_id, path=f"{base_path}/{path}", dst_path=local_dir)
# 🔽 Download artifacts
model_path = dl(keras_file)
tokenizer_path = dl(tokenizer_file)
print("Contents of downloaded_artifacts:", os.listdir(local_dir))
# 📦 Load
model = load_model(model_path)
with open(tokenizer_path, "rb") as f:
tokenizer = pickle.load(f)
return model, tokenizer, embedding_type
# 🚀 Launch the API
app = FastAPI(title="Sentiment Prediction API - Air Paradis")
# app = FastAPI(title="Sentiment Prediction API - Air Paradis",
# root_path="/api")
try:
model, tokenizer, embedding_type = load_model_and_artifacts()
except Exception as e:
print(f"\n❌ Error loading artifacts: {e}")
model, tokenizer = None, None
@app.get("/")
def root():
return {"message": "Sentiment API - Air Paradis - online"}
@app.post("/predict")
def predict_sentiment(request: TweetRequest):
if model is None or tokenizer is None:
raise HTTPException(status_code=503, detail="Model or artifacts unavailable.")
try:
processed_tweet = preprocess_tweet(request.tweet)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Preprocessing error: {e}")
if not processed_tweet.strip():
raise HTTPException(status_code=400, detail="Tweet is empty or invalid after preprocessing.")
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]
if prediction_prob >= 0.5:
sentiment = "positive"
else:
sentiment = "negative"
# prediction_prob = 1 - prediction_prob
except Exception as e:
raise HTTPException(status_code=500, detail=f"Inference error: {e}")
return {
"sentiment": sentiment,
"probability": float(prediction_prob)
}
# Replace the dict with a global empty DataFrame initially
error_reports_df = pd.DataFrame(columns=["tweet", "prediction", "probability", "timestamp"])
@app.post("/report_error")
def report_error(request: ReportRequest):
global error_reports_df
tweet = request.tweet.strip()
prediction = request.prediction.strip().lower()
probability = request.probability
if not tweet or not prediction:
raise HTTPException(status_code=400, detail="Missing tweet or prediction.")
if probability < 0 or probability > 1:
raise HTTPException(status_code=400, detail="Invalid probability. Must be between 0 and 1.")
now = datetime.utcnow()
# Check if tweet already exists to avoid duplicates
existing_idx = error_reports_df.index[error_reports_df["tweet"] == tweet].tolist()
if existing_idx:
# Update existing row
idx = existing_idx[0]
error_reports_df.at[idx, "prediction"] = prediction + f' (p = {probability:.2f} ) '
error_reports_df.at[idx, "probability"] = probability
error_reports_df.at[idx, "timestamp"] = now
else:
# Add new row
new_row = {
"tweet": tweet,
"prediction": prediction + f' (p = {probability:.2f} ) ',
"probability": probability,
"timestamp": now
}
error_reports_df = pd.concat([error_reports_df, pd.DataFrame([new_row])], ignore_index=True)
report_sent = False
# Condition: every 3 reports and max 5 min interval between the last 3
try:
if len(error_reports_df) % 3 == 0:
last_three = error_reports_df.sort_values("timestamp", ascending=False).head(3)
times = last_three["timestamp"].sort_values()
delta = times.iloc[-1] - times.iloc[0]
if delta <= timedelta(minutes=5):
reports_dict = dict(zip(
last_three["tweet"],
last_three["prediction"]
))
print(f"📧 Attempting to send email for {len(error_reports_df)} reports...")
email_success = send_error_report_email(reports_dict)
if email_success:
print(f"✅ Email successfully sent for {len(error_reports_df)} reports")
report_sent = True
# DELETE the last 3 reports sent
# Identify indexes to delete
to_remove_idx = last_three.index
error_reports_df = error_reports_df.drop(to_remove_idx).reset_index(drop=True)
else:
print(f"❌ Failed to send email for {len(error_reports_df)} reports")
except Exception as e:
print(f"❌ Error while sending email: {e}")
# Continue even if it fails
return {"report_sent": report_sent}

View file

@ -1,38 +1,38 @@
# This reverse proxy configuration aims to make calls to different services running under different ports (Streamlit :8501 and FastAPI :8000) by calling only one port : the nginx one (:8080)
# The 8080 has to be opened (Dockerfile) and used by app.py (streamlit) in order to make right calls.
worker_processes 1;
events { worker_connections 1024; }
http {
server {
listen 8080;
location /api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:8501/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
}
# This reverse proxy configuration aims to make calls to different services running under different ports (Streamlit :8501 and FastAPI :8000) by calling only one port : the nginx one (:8080)
# The 8080 has to be opened (Dockerfile) and used by app.py (streamlit) in order to make right calls.
worker_processes 1;
events { worker_connections 1024; }
http {
server {
listen 8080;
location /api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:8501/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
}

View file

@ -1,80 +1,80 @@
import re
import html
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
# Ensure that the necessary resources are available
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')
# PREPROCESSING FUNCTION FOR CLASSIC AND ADVANCED MODELS
def preprocess_tweet(tweet):
"""
Preprocesses a tweet for classic ML models or word embedding models (e.g., Word2Vec, GloVe).
The text is cleaned, normalized, lemmatized, and stripped of unnecessary noise.
"""
# Ensure input is a string
if not isinstance(tweet, str):
return ""
# Decode HTML entities (e.g., &amp; → &)
tweet = html.unescape(tweet)
# print(tweet + '\n')
# Convert text to lowercase
tweet = tweet.lower()
# print(tweet + '\n')
# Replace URLs with a generic token
tweet = re.sub(r'https?://\S+|www\.\S+', '<URL>', tweet)
# print(tweet + '\n')
# Replace mentions (@username) with a generic token
tweet = re.sub(r'@\w+', '<MENTION>', tweet)
# print(tweet + '\n')
# Separate hashtags from the word to preserve semantics during tokenization
tweet = re.sub(r'#(\w+)', r'# \1', tweet)
# print(tweet + '\n')
# Remove unwanted punctuation and special characters (except useful ones like < > @ # ! ?)
tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet)
# print(tweet + '\n')
# Remove non-printable/control characters (e.g., \n, \t, emojis, corrupted unicode)
tweet = re.sub(r'[^\x20-\x7E]', '', tweet)
# print(tweet + '\n')
# Tokenize the tweet into individual words
tokens = word_tokenize(tweet)
# print(tokens)
# print('\n')
# Lemmatize each token to its base form (e.g., "running" → "run")
lemmatizer = WordNetLemmatizer()
tokens = [lemmatizer.lemmatize(token) for token in tokens]
# print(tokens)
# print('\n')
# Remove stopwords, but keep negation-related words (important for sentiment)
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]
# print(tokens)
# print('\n')
# Reconstruct the cleaned tweet
import re
import html
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
# Ensure that the necessary resources are available
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')
# PREPROCESSING FUNCTION FOR CLASSIC AND ADVANCED MODELS
def preprocess_tweet(tweet):
"""
Preprocesses a tweet for classic ML models or word embedding models (e.g., Word2Vec, GloVe).
The text is cleaned, normalized, lemmatized, and stripped of unnecessary noise.
"""
# Ensure input is a string
if not isinstance(tweet, str):
return ""
# Decode HTML entities (e.g., &amp; → &)
tweet = html.unescape(tweet)
# print(tweet + '\n')
# Convert text to lowercase
tweet = tweet.lower()
# print(tweet + '\n')
# Replace URLs with a generic token
tweet = re.sub(r'https?://\S+|www\.\S+', '<URL>', tweet)
# print(tweet + '\n')
# Replace mentions (@username) with a generic token
tweet = re.sub(r'@\w+', '<MENTION>', tweet)
# print(tweet + '\n')
# Separate hashtags from the word to preserve semantics during tokenization
tweet = re.sub(r'#(\w+)', r'# \1', tweet)
# print(tweet + '\n')
# Remove unwanted punctuation and special characters (except useful ones like < > @ # ! ?)
tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet)
# print(tweet + '\n')
# Remove non-printable/control characters (e.g., \n, \t, emojis, corrupted unicode)
tweet = re.sub(r'[^\x20-\x7E]', '', tweet)
# print(tweet + '\n')
# Tokenize the tweet into individual words
tokens = word_tokenize(tweet)
# print(tokens)
# print('\n')
# Lemmatize each token to its base form (e.g., "running" → "run")
lemmatizer = WordNetLemmatizer()
tokens = [lemmatizer.lemmatize(token) for token in tokens]
# print(tokens)
# print('\n')
# Remove stopwords, but keep negation-related words (important for sentiment)
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]
# print(tokens)
# print('\n')
# Reconstruct the cleaned tweet
return ' '.join(tokens)

View file

@ -1,21 +1,21 @@
# === API FastAPI ===
fastapi==0.115.14
uvicorn==0.35.0
pydantic==2.11.7
starlette==0.46.2
# === Interface Streamlit ===
streamlit==1.46.1
requests==2.32.4
# === Machine Learning ===
tensorflow==2.19.0
mlflow==3.1.1
boto3==1.39.2
numpy==2.1.3
pandas==2.3.0
# === Traitement de texte ===
nltk==3.9.1
# === Utilitaires ===
python-dotenv==1.1.1
httpx==0.28.1
# === Dépendances système ===
protobuf==3.20.3
# === API FastAPI ===
fastapi==0.115.14
uvicorn==0.35.0
pydantic==2.11.7
starlette==0.46.2
# === Interface Streamlit ===
streamlit==1.46.1
requests==2.32.4
# === Machine Learning ===
tensorflow==2.19.0
mlflow==3.1.1
boto3==1.39.2
numpy==2.1.3
pandas==2.3.0
# === Traitement de texte ===
nltk==3.9.1
# === Utilitaires ===
python-dotenv==1.1.1
httpx==0.28.1
# === Dépendances système ===
protobuf==3.20.3

View file

@ -1,35 +1,35 @@
#!/bin/bash
set -e
# S'assurer que les répertoires de cache et de log Nginx sont accessibles en écriture
mkdir -p /var/cache/nginx /var/run/nginx /var/log/nginx
chown -R www-data:www-data /var/cache/nginx /var/run/nginx /var/log/nginx
echo "Starting Streamlit..."
streamlit run app.py \
--server.port 8501 \
--server.address 0.0.0.0 \
--server.enableXsrfProtection false \
--server.enableCORS false \
& # Lance en arrière-plan
echo "Starting FastAPI..."
uvicorn main:app --host 0.0.0.0 --port 8000 --loop asyncio & # Lance en arrière-plan
# MODIFICATION IMPORTANTE ICI: Vérifier Streamlit sur le bon chemin
echo "Waiting for Streamlit to be ready..."
until curl -s http://127.0.0.1:8501 > /dev/null; do
sleep 1
done
echo "Streamlit is ready!"
# MODIFICATION: Vérifier FastAPI sur un endpoint existant, par exemple /docs ou /health
echo "Waiting for FastAPI to be ready..."
until curl -s http://127.0.0.1:8000/docs > /dev/null; do # Assurez-vous que /docs est un endpoint léger
sleep 1
done
echo "FastAPI is ready!"
echo "Launching NGINX..."
nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
#!/bin/bash
set -e
# S'assurer que les répertoires de cache et de log Nginx sont accessibles en écriture
mkdir -p /var/cache/nginx /var/run/nginx /var/log/nginx
chown -R www-data:www-data /var/cache/nginx /var/run/nginx /var/log/nginx
echo "Starting Streamlit..."
streamlit run app.py \
--server.port 8501 \
--server.address 0.0.0.0 \
--server.enableXsrfProtection false \
--server.enableCORS false \
& # Lance en arrière-plan
echo "Starting FastAPI..."
uvicorn main:app --host 0.0.0.0 --port 8000 --loop asyncio & # Lance en arrière-plan
# MODIFICATION IMPORTANTE ICI: Vérifier Streamlit sur le bon chemin
echo "Waiting for Streamlit to be ready..."
until curl -s http://127.0.0.1:8501 > /dev/null; do
sleep 1
done
echo "Streamlit is ready!"
# MODIFICATION: Vérifier FastAPI sur un endpoint existant, par exemple /docs ou /health
echo "Waiting for FastAPI to be ready..."
until curl -s http://127.0.0.1:8000/docs > /dev/null; do # Assurez-vous que /docs est un endpoint léger
sleep 1
done
echo "FastAPI is ready!"
echo "Launching NGINX..."
nginx -c /etc/nginx/nginx.conf -g 'daemon off;'

View file

@ -1,38 +1,38 @@
# Utilisation de mock pour simuler l'envoi de mails mais sans spammer une boîte.
from unittest.mock import patch, MagicMock
import os
import sys
from dotenv import load_dotenv
# Ajouter le répertoire parent au path pour importer email_service
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from email_service import send_error_report_email
# Charger les variables d'environnement
dotenv_path = '../../../.env'
load_dotenv(dotenv_path)
@patch("smtplib.SMTP")
def test_email_service_connection(mock_smtp):
"""
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 = {
"Flight cancelled without notice": "positif"
}
# Appel de la fonction
result = send_error_report_email(test_reports)
# ✅ Assertions
assert result is True
mock_smtp.assert_called_with(os.getenv("SMTP_SERVER"), int(os.getenv("SMTP_PORT", 587)))
mock_server.starttls.assert_called_once()
mock_server.login.assert_called_once_with(os.getenv("SMTP_EMAIL"), os.getenv("SMTP_PASSWORD"))
mock_server.send_message.assert_called_once()
# Utilisation de mock pour simuler l'envoi de mails mais sans spammer une boîte.
from unittest.mock import patch, MagicMock
import os
import sys
from dotenv import load_dotenv
# Ajouter le répertoire parent au path pour importer email_service
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from email_service import send_error_report_email
# Charger les variables d'environnement
dotenv_path = '../../../.env'
load_dotenv(dotenv_path)
@patch("smtplib.SMTP")
def test_email_service_connection(mock_smtp):
"""
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 = {
"Flight cancelled without notice": "positif"
}
# Appel de la fonction
result = send_error_report_email(test_reports)
# ✅ Assertions
assert result is True
mock_smtp.assert_called_with(os.getenv("SMTP_SERVER"), int(os.getenv("SMTP_PORT", 587)))
mock_server.starttls.assert_called_once()
mock_server.login.assert_called_once_with(os.getenv("SMTP_EMAIL"), os.getenv("SMTP_PASSWORD"))
mock_server.send_message.assert_called_once()

View file

@ -1,32 +1,32 @@
import os
import sys
from fastapi.testclient import TestClient
# Add parent directory to path to import email_service
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from main import app
client = TestClient(app)
def test_root_endpoint():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Sentiment API - Air Paradis - online"}
def test_predict_sentiment():
response = client.post("/predict", json={"tweet": "I love this airline !"})
assert response.status_code == 200
data = response.json()
assert "sentiment" in data
assert "probability" in data
assert data["sentiment"] in ["positive", "negative"]
def test_missing_tweet():
response = client.post("/predict", json={})
assert response.status_code == 422 # Unprocessable Entity (FastAPI)
def test_empty_tweet():
response = client.post("/predict", json={"tweet": ""})
assert response.status_code == 400
import os
import sys
from fastapi.testclient import TestClient
# Add parent directory to path to import email_service
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from main import app
client = TestClient(app)
def test_root_endpoint():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Sentiment API - Air Paradis - online"}
def test_predict_sentiment():
response = client.post("/predict", json={"tweet": "I love this airline !"})
assert response.status_code == 200
data = response.json()
assert "sentiment" in data
assert "probability" in data
assert data["sentiment"] in ["positive", "negative"]
def test_missing_tweet():
response = client.post("/predict", json={})
assert response.status_code == 422 # Unprocessable Entity (FastAPI)
def test_empty_tweet():
response = client.post("/predict", json={"tweet": ""})
assert response.status_code == 400
assert response.json()["detail"] == "Tweet is empty or invalid after preprocessing."

View file

@ -1,28 +1,28 @@
import os
import sys
# Ajouter le répertoire parent au path pour importer email_service
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from preprocessing import preprocess_tweet
def test_preprocess_tweet_removes_hashtags():
tweet = """
Just flew w/ @AirParadis &#128640; &amp; I&#8217;m in LOVE! Epic service, comfy seats, good prices at https://airparadis.com
#bestflight #AirParadis
"""
assert preprocess_tweet(tweet) == 'flew w < MENTION > im love ! epic service comfy seat good price < URL > # bestflight # airparadis'
def test_preprocess_tweet_removes_mentions():
tweet = """
&quot;Flight of dreams&quot; w/ @AirParadis! Got food, space &amp; smiles &#128516; I&#8217;ll book again!
#AirParadis
"""
assert preprocess_tweet(tweet) == 'flight dream w < MENTION > ! got food space smile ill book ! # airparadis'
def test_preprocess_tweet_lowercases_text():
tweet = """
My 3rd time w/ @AirParadis &amp; still amazed &#128525; Crew = lovely &agrave; every step!
#flyhappy #AirParadis
"""
assert preprocess_tweet(tweet) == '3rd time w < MENTION > still amazed crew lovely every step ! # flyhappy # airparadis'
import os
import sys
# Ajouter le répertoire parent au path pour importer email_service
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
from preprocessing import preprocess_tweet
def test_preprocess_tweet_removes_hashtags():
tweet = """
Just flew w/ @AirParadis &#128640; &amp; I&#8217;m in LOVE! Epic service, comfy seats, good prices at https://airparadis.com
#bestflight #AirParadis
"""
assert preprocess_tweet(tweet) == 'flew w < MENTION > im love ! epic service comfy seat good price < URL > # bestflight # airparadis'
def test_preprocess_tweet_removes_mentions():
tweet = """
&quot;Flight of dreams&quot; w/ @AirParadis! Got food, space &amp; smiles &#128516; I&#8217;ll book again!
#AirParadis
"""
assert preprocess_tweet(tweet) == 'flight dream w < MENTION > ! got food space smile ill book ! # airparadis'
def test_preprocess_tweet_lowercases_text():
tweet = """
My 3rd time w/ @AirParadis &amp; still amazed &#128525; Crew = lovely &agrave; every step!
#flyhappy #AirParadis
"""
assert preprocess_tweet(tweet) == '3rd time w < MENTION > still amazed crew lovely every step ! # flyhappy # airparadis'

View file

@ -1,235 +1,235 @@
# ✨ 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 les étapes clés du projet : choix méthodologiques, comparaison des modèles, mise en production d’une API robuste, et déploiement sur deux environnements (local + cloud). 🧠🔧
---
## 🔍 Trois approches de modélisation supervisée
Nous avons testé trois approches complémentaires pour prédire le sentiment d’un tweet. Chacune repose sur une représentation textuelle différente et un algorithme d’apprentissage adapté :
### 1. 🧱 Approche simple : Bag-of-Words (BoW) + modèles de machine learning
On vectorise les tweets via BoW ou BoW + TF-IDF, qui comptent la fréquence des mots.
Contrairement à d'autres projets NLP, nous avons évité LDA (non supervisé, orienté topic modeling).
➡️ **Modèles testés** :
- Régression Logistique
- Naive Bayes
- SVM
- Random Forest
Ces modèles sont entraînés avec **GridSearchCV**, en optimisant la **précision (`precision`)**, notre KPI métier. Différents KPI ont été calculés sur l'ensemble de test.
![Scores modèles simples](./Basic-Models.png)
---
### 2. 🔥 Approche avancée : Word Embeddings + LSTM
Nous utilisons des vecteurs denses Word2Vec (entraînés) et GloVe 300d (préentraînés par Stanford), qui capturent les similarités sémantiques entre mots. Ces représentations ne prennent toutefois **pas en compte le contexte** exact (ex : "annulé" est probablement un mot très négatif pour un vol mais positif pour un procès contenant des charges contre vous).
La classification est réalisée par un **LSTM**, capable d'exploiter la séquentialité des mots.
Le **modèle déployé est automatiquement sélectionné** entre Word2Vec + LSTM et GloVe + LSTM, selon la **précision maximale enregistrée dans MLFlow**.
Modèle `Word2Vec + LSTM` (`test_precision : 0.797 , val_precision : 0.795`):
![Scores entrainement modèle avancé LSTM + Word2Vec](./Word2Vec_LSTM_epoch9.png)
Modèle `GloVe 300d + LSTM` (`test_precision : 0.792 , val_precision : 0.798`):
![Scores entrainement modèles avancés LSTM + GloVe 300d](./GloVe-300d_LSTM_epoch12.png)
Bien que GloVe 300d + LSTM présente un score de précision légèrement supérieur à Word2Vec + LSTM sur l'ensemble de validation, il reste inférieur pour l'ensemble de test (relativement négligeable dans l'absolu).
---
### 3. 🤖 Approche transformer : DistilBERT, embeddings contextuels allégés
Nous avons utilisé **DistilBERT**, une version plus légère et plus rapide de BERT (créée par Hugging Face).
Il conserve **95 % de la performance de BERT** tout en étant **40 % plus petit** et **60 % plus rapide**, ce qui le rend particulièrement adapté à un **entraînement sur GPU limité** (Google Colab) et à une **inférence plus rapide**.
Comme BERT, DistilBERT produit des **embeddings contextuels**, c’est-à-dire que le sens des mots varie selon leur contexte. Par exemple, *"banque"* dans *"je vais à la banque"* et *"banque de données"* n’aura pas le même vecteur.
Il a été **fine-tuné** ici pour une classification binaire (positif/négatif), avec un tokenizer et un encodage spécifiques à son architecture.
> **Modèle `DistilBERT`** (`test_precision : 0.861 , val_precision : 0.858`)
![Courbes d'entraînement DistilBERT](./bert_training_curves_epoch1.png)
![Matrice de Confusion](./bert_confusion_matrix.png)
![ROC Curve](./bert_roc_curve.png)
⚠️ **Limites** : comme tous les modèles préentraînés, DistilBERT **n’est pas sensible à l’ironie, aux jeux de mots, ni aux références culturelles implicites**.
Or, Twitter est un terrain de jeu idéal pour ce genre de contenus détournés. Ces limites s’appliquent aussi à Word2Vec, GloVe et aux modèles classiques.
---
## 🎯 Pourquoi optimiser la précision (et non l'accuracy) ?
**Accuracy** = proportion de bonnes prédictions globales.
**Precision** = proportion de tweets *prévus positifs* qui sont *vraiment positifs*.
> Exemple : sur 100 tweets, si le modèle prédit 30 positifs, mais 15 sont des erreurs, la précision est de 50 %.
Nous avons préféré **minimiser les faux positifs**, car dans un contexte sensible, **annoncer un tweet positif alors qu’il est négatif peut être problématique**.
- Modèles classiques : **GridSearchCV optimisé sur la précision**
- Modèles DL : **EarlyStopping sur val_loss**, mais **sélection finale sur la précision**
---
## ⚙️ Environnement MLOps souverain
### 🖥️ Serveur personnel NAS + OpenMediaVault
MLFlow est installé sur un **NAS personnel (Intel N100)**, sous **OpenMediaVault (Debian)**.
Nous utilisons **MinIO** pour stocker artefacts et modèles, garantissant une gestion locale, souveraine et compatible S3.
> Les notebooks, exécutés sur Google Colab pour bénéficier de GPU, loggent dans MLFlow et envoient les modèles vers MinIO via des **variables d’environnement**.
Dashboard Expériences MLFlow :
![Dashboard MLFLow](./mlflow_experiments_dashboard.png)
Modèles en production (registre de modèles):
![Models Registry](mlflow_models_registry_2.png)
Meilleur modèle non BERT retenu :
![Modèle retenu](mlflow_models_registry_best_LSTM_model.png)
![Meilleur modèle non BERT](mlflolw_experiments_dashboard_best_LSTM_model.png)
---
## 🧪 CI, tests et versioning
### ✅ Tests unitaires
- Vérifient le bon fonctionnement du prétraitement, des prédictions et de la gestion d’erreurs.
- Intégrés à **GitHub Actions**, **ils bloquent le déploiement si les tests échouent.**
### 🐳 Conteneurisation & CI/CD
L’API FastAPI est packagée via Docker, puis poussée sur DockerHub.
Un [pipeline CI/CD](https://github.com/GrgMdmn/OpenClassrooms_AI_Projects/blob/main/.github/workflows/p7_ci-cd.yml) complet garantit **un déploiement reproductible**. Il assure:
- La validation de tests unitaires
- La construction et le push de l'api conteneurisée sur [dockerhub](https://hub.docker.com/r/grgmdmn/sentiment_api)
![github workflow](github_ci-cd.png)
---
## 🚀 Déploiement de l’API FastAPI (double)
### 🔧 Backend : FastAPI
- Expose un endpoint `/predict`
- Charge dynamiquement le **meilleur modèle Word Embedding + LSTM** via le *Model Registry* MLFlow
- Applique les bons embeddings selon le tag `embedding_type`
### 💻 Interface Streamlit
Interface locale pour tester les requêtes, debug, démonstrations.
![Streamlit Local](./streamlit_interface_local.png)
---
### 🌐 Double déploiement (NAS + Google Cloud)
#### 🔧 Problème AVX2
Initialement, l’API ne fonctionnait pas sur le NAS (processeur Intel N100), car celui-ci ne prenait pas en charge les instructions **AVX2**, requises par TensorFlow.
> 🛠️ **Solution** : activation manuelle du support AVX2 dans le BIOS. Cette option est souvent désactivée par défaut sur les cartes mères orientées NAS pour des raisons de consommation énergétique ou de stabilité.
Depuis cette modification, le NAS peut exécuter l’API sans erreurs.
---
#### 🚀 API déployées
Le projet propose un **double déploiement** de l’API, en local et dans le cloud :
- 🏠 **NAS local** : [sentiment-api.greg-madman-nas.duckdns.org](https://sentiment-api.greg-madman-nas.duckdns.org)\
Interface Docker Compose sur le NAS :
![docker compose openmediavault](openmediavault_docker-compose_settings.png)
- ☁️ **Google Cloud Run** : [sentiment-api-service-7772256003.europe-west1.run.app](https://sentiment-api-service-7772256003.europe-west1.run.app)\
Interface de déploiement Google Cloud :
![déploiement google cloud](google_cloud_interface.png)
---
#### 🔄 Déploiement continu automatisé (NAS uniquement)
Pour assurer un **déploiement continu** sur le NAS, il a été choisi d'installer [**Watchtower**](https://containrrr.dev/watchtower/arguments/) pour rester dans un logique d'environnement 100% `docker`. Cet outil surveille toutes les **10 minutes** l’image Docker de l’API (`sentiment_api`) et, en cas de nouvelle version disponible sur DockerHub, redémarre automatiquement le conteneur avec la dernière image.
![Docker Watchtower dans OMV Docker-Compose](watchtower_docker.png)
Ce mécanisme garantit que la version déployée sur le NAS reste toujours synchronisée avec la dernière version validée par les tests sur GitHub Actions, **sans intervention manuelle**.
![Logs watchtower dans docker (console SSH)](watchtower_logs.png)
Un **système d’alerte mail** notifie automatiquement toute mise à jour effectuée.
![Rapport mail watchtower update sentiment_api](watchtower_logs_by_email.png)
---
## 📊 Monitoring maison
Une logique d’alerte légère embarquée dans l’API :
![1ère erreur](./api_first_error.png)
![2ème erreur](./api_second_error.png)
![3ème erreur](./api_third_error.png)
Si **3 erreurs de prédiction** consécutives sont détectées sur une **fenêtre de 5 min**, un rapport est généré automatiquement.
![Rapport d'erreur envoyé par mail](./api_error_report.png)
---
## 🧠 Schéma global du pipeline MLOps
### Schéma synthétique
```
┌────────────┐
│ Données │
│ (tweets) │
└─────┬──────┘
│
Prétraitement (regex, tokenisation, lemmatisation, stopwords)
│
┌─────────▼──────────┐
│ Entraînement ML │
└─────────┬──────────┘
│
Log via MLFlow + artefacts MinIO
│
Sélection du meilleur modèle "avancé" (hors BERT) selon précision
│
┌─────────▼──────────┐
│ API FastAPI │
│ + Streamlit │
└─────────┬──────────┘
│
Déploiement :
├─ Google Cloud Run (manuel)
└─ NAS local (automatisé via Watchtower)
│
┌──────────▼─────────────┐
│ Monitoring léger maison│
│ (alertes sur mauvaises │
│ prédictions) │
└────────────────────────┘
```
### Schéma détaillé
![Diagramme détaillé](diagram.drawio.png)
---
## ✨ Conclusion
Ce projet démontre comment construire une chaîne MLOps complète, autonome et reproductible pour du NLP appliqué à Twitter.
Il souligne les **forces et limites des modèles** (y compris BERT), la nécessité d’une métrique adaptée au contexte (précision vs accuracy), et l’importance d’un **déploiement maîtrisé**, même sur une infrastructure personnelle.
---
# ✨ 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 les étapes clés du projet : choix méthodologiques, comparaison des modèles, mise en production d’une API robuste, et déploiement sur deux environnements (local + cloud). 🧠🔧
---
## 🔍 Trois approches de modélisation supervisée
Nous avons testé trois approches complémentaires pour prédire le sentiment d’un tweet. Chacune repose sur une représentation textuelle différente et un algorithme d’apprentissage adapté :
### 1. 🧱 Approche simple : Bag-of-Words (BoW) + modèles de machine learning
On vectorise les tweets via BoW ou BoW + TF-IDF, qui comptent la fréquence des mots.
Contrairement à d'autres projets NLP, nous avons évité LDA (non supervisé, orienté topic modeling).
➡️ **Modèles testés** :
- Régression Logistique
- Naive Bayes
- SVM
- Random Forest
Ces modèles sont entraînés avec **GridSearchCV**, en optimisant la **précision (`precision`)**, notre KPI métier. Différents KPI ont été calculés sur l'ensemble de test.
![Scores modèles simples](./Basic-Models.png)
---
### 2. 🔥 Approche avancée : Word Embeddings + LSTM
Nous utilisons des vecteurs denses Word2Vec (entraînés) et GloVe 300d (préentraînés par Stanford), qui capturent les similarités sémantiques entre mots. Ces représentations ne prennent toutefois **pas en compte le contexte** exact (ex : "annulé" est probablement un mot très négatif pour un vol mais positif pour un procès contenant des charges contre vous).
La classification est réalisée par un **LSTM**, capable d'exploiter la séquentialité des mots.
Le **modèle déployé est automatiquement sélectionné** entre Word2Vec + LSTM et GloVe + LSTM, selon la **précision maximale enregistrée dans MLFlow**.
Modèle `Word2Vec + LSTM` (`test_precision : 0.797 , val_precision : 0.795`):
![Scores entrainement modèle avancé LSTM + Word2Vec](./Word2Vec_LSTM_epoch9.png)
Modèle `GloVe 300d + LSTM` (`test_precision : 0.792 , val_precision : 0.798`):
![Scores entrainement modèles avancés LSTM + GloVe 300d](./GloVe-300d_LSTM_epoch12.png)
Bien que GloVe 300d + LSTM présente un score de précision légèrement supérieur à Word2Vec + LSTM sur l'ensemble de validation, il reste inférieur pour l'ensemble de test (relativement négligeable dans l'absolu).
---
### 3. 🤖 Approche transformer : DistilBERT, embeddings contextuels allégés
Nous avons utilisé **DistilBERT**, une version plus légère et plus rapide de BERT (créée par Hugging Face).
Il conserve **95 % de la performance de BERT** tout en étant **40 % plus petit** et **60 % plus rapide**, ce qui le rend particulièrement adapté à un **entraînement sur GPU limité** (Google Colab) et à une **inférence plus rapide**.
Comme BERT, DistilBERT produit des **embeddings contextuels**, c’est-à-dire que le sens des mots varie selon leur contexte. Par exemple, *"banque"* dans *"je vais à la banque"* et *"banque de données"* n’aura pas le même vecteur.
Il a été **fine-tuné** ici pour une classification binaire (positif/négatif), avec un tokenizer et un encodage spécifiques à son architecture.
> **Modèle `DistilBERT`** (`test_precision : 0.861 , val_precision : 0.858`)
![Courbes d'entraînement DistilBERT](./bert_training_curves_epoch1.png)
![Matrice de Confusion](./bert_confusion_matrix.png)
![ROC Curve](./bert_roc_curve.png)
⚠️ **Limites** : comme tous les modèles préentraînés, DistilBERT **n’est pas sensible à l’ironie, aux jeux de mots, ni aux références culturelles implicites**.
Or, Twitter est un terrain de jeu idéal pour ce genre de contenus détournés. Ces limites s’appliquent aussi à Word2Vec, GloVe et aux modèles classiques.
---
## 🎯 Pourquoi optimiser la précision (et non l'accuracy) ?
**Accuracy** = proportion de bonnes prédictions globales.
**Precision** = proportion de tweets *prévus positifs* qui sont *vraiment positifs*.
> Exemple : sur 100 tweets, si le modèle prédit 30 positifs, mais 15 sont des erreurs, la précision est de 50 %.
Nous avons préféré **minimiser les faux positifs**, car dans un contexte sensible, **annoncer un tweet positif alors qu’il est négatif peut être problématique**.
- Modèles classiques : **GridSearchCV optimisé sur la précision**
- Modèles DL : **EarlyStopping sur val_loss**, mais **sélection finale sur la précision**
---
## ⚙️ Environnement MLOps souverain
### 🖥️ Serveur personnel NAS + OpenMediaVault
MLFlow est installé sur un **NAS personnel (Intel N100)**, sous **OpenMediaVault (Debian)**.
Nous utilisons **MinIO** pour stocker artefacts et modèles, garantissant une gestion locale, souveraine et compatible S3.
> Les notebooks, exécutés sur Google Colab pour bénéficier de GPU, loggent dans MLFlow et envoient les modèles vers MinIO via des **variables d’environnement**.
Dashboard Expériences MLFlow :
![Dashboard MLFLow](./mlflow_experiments_dashboard.png)
Modèles en production (registre de modèles):
![Models Registry](mlflow_models_registry_2.png)
Meilleur modèle non BERT retenu :
![Modèle retenu](mlflow_models_registry_best_LSTM_model.png)
![Meilleur modèle non BERT](mlflolw_experiments_dashboard_best_LSTM_model.png)
---
## 🧪 CI, tests et versioning
### ✅ Tests unitaires
- Vérifient le bon fonctionnement du prétraitement, des prédictions et de la gestion d’erreurs.
- Intégrés à **GitHub Actions**, **ils bloquent le déploiement si les tests échouent.**
### 🐳 Conteneurisation & CI/CD
L’API FastAPI est packagée via Docker, puis poussée sur DockerHub.
Un [pipeline CI/CD](https://github.com/GrgMdmn/OpenClassrooms_AI_Projects/blob/main/.github/workflows/p7_ci-cd.yml) complet garantit **un déploiement reproductible**. Il assure:
- La validation de tests unitaires
- La construction et le push de l'api conteneurisée sur [dockerhub](https://hub.docker.com/r/grgmdmn/sentiment_api)
![github workflow](github_ci-cd.png)
---
## 🚀 Déploiement de l’API FastAPI (double)
### 🔧 Backend : FastAPI
- Expose un endpoint `/predict`
- Charge dynamiquement le **meilleur modèle Word Embedding + LSTM** via le *Model Registry* MLFlow
- Applique les bons embeddings selon le tag `embedding_type`
### 💻 Interface Streamlit
Interface locale pour tester les requêtes, debug, démonstrations.
![Streamlit Local](./streamlit_interface_local.png)
---
### 🌐 Double déploiement (NAS + Google Cloud)
#### 🔧 Problème AVX2
Initialement, l’API ne fonctionnait pas sur le NAS (processeur Intel N100), car celui-ci ne prenait pas en charge les instructions **AVX2**, requises par TensorFlow.
> 🛠️ **Solution** : activation manuelle du support AVX2 dans le BIOS. Cette option est souvent désactivée par défaut sur les cartes mères orientées NAS pour des raisons de consommation énergétique ou de stabilité.
Depuis cette modification, le NAS peut exécuter l’API sans erreurs.
---
#### 🚀 API déployées
Le projet propose un **double déploiement** de l’API, en local et dans le cloud :
- 🏠 **NAS local** : [sentiment-api.greg-madman-nas.duckdns.org](https://sentiment-api.greg-madman-nas.duckdns.org)\
Interface Docker Compose sur le NAS :
![docker compose openmediavault](openmediavault_docker-compose_settings.png)
- ☁️ **Google Cloud Run** : [sentiment-api-service-7772256003.europe-west1.run.app](https://sentiment-api-service-7772256003.europe-west1.run.app)\
Interface de déploiement Google Cloud :
![déploiement google cloud](google_cloud_interface.png)
---
#### 🔄 Déploiement continu automatisé (NAS uniquement)
Pour assurer un **déploiement continu** sur le NAS, il a été choisi d'installer [**Watchtower**](https://containrrr.dev/watchtower/arguments/) pour rester dans un logique d'environnement 100% `docker`. Cet outil surveille toutes les **10 minutes** l’image Docker de l’API (`sentiment_api`) et, en cas de nouvelle version disponible sur DockerHub, redémarre automatiquement le conteneur avec la dernière image.
![Docker Watchtower dans OMV Docker-Compose](watchtower_docker.png)
Ce mécanisme garantit que la version déployée sur le NAS reste toujours synchronisée avec la dernière version validée par les tests sur GitHub Actions, **sans intervention manuelle**.
![Logs watchtower dans docker (console SSH)](watchtower_logs.png)
Un **système d’alerte mail** notifie automatiquement toute mise à jour effectuée.
![Rapport mail watchtower update sentiment_api](watchtower_logs_by_email.png)
---
## 📊 Monitoring maison
Une logique d’alerte légère embarquée dans l’API :
![1ère erreur](./api_first_error.png)
![2ème erreur](./api_second_error.png)
![3ème erreur](./api_third_error.png)
Si **3 erreurs de prédiction** consécutives sont détectées sur une **fenêtre de 5 min**, un rapport est généré automatiquement.
![Rapport d'erreur envoyé par mail](./api_error_report.png)
---
## 🧠 Schéma global du pipeline MLOps
### Schéma synthétique
```
┌────────────┐
│ Données │
│ (tweets) │
└─────┬──────┘
│
Prétraitement (regex, tokenisation, lemmatisation, stopwords)
│
┌─────────▼──────────┐
│ Entraînement ML │
└─────────┬──────────┘
│
Log via MLFlow + artefacts MinIO
│
Sélection du meilleur modèle "avancé" (hors BERT) selon précision
│
┌─────────▼──────────┐
│ API FastAPI │
│ + Streamlit │
└─────────┬──────────┘
│
Déploiement :
├─ Google Cloud Run (manuel)
└─ NAS local (automatisé via Watchtower)
│
┌──────────▼─────────────┐
│ Monitoring léger maison│
│ (alertes sur mauvaises │
│ prédictions) │
└────────────────────────┘
```
### Schéma détaillé
![Diagramme détaillé](diagram.drawio.png)
---
## ✨ Conclusion
Ce projet démontre comment construire une chaîne MLOps complète, autonome et reproductible pour du NLP appliqué à Twitter.
Il souligne les **forces et limites des modèles** (y compris BERT), la nécessité d’une métrique adaptée au contexte (précision vs accuracy), et l’importance d’un **déploiement maîtrisé**, même sur une infrastructure personnelle.
---
💬 Merci pour votre lecture !

View file

@ -1,245 +1,245 @@
# ✨ Sentiment Analysis and MLOps Deployment: A Complete Experience Report
As part of the *Air Paradis* project, we implemented a full sentiment analysis pipeline from tweets, covering everything from modeling to MLOps deployment. This blog post reviews the key project stages: methodological choices, model comparison, deployment of a robust API, and deployment on two environments (local + cloud). 🧠🔧
---
## 🔍 Three Supervised Modeling Approaches
We tested three complementary approaches to predict the sentiment of a tweet. Each relies on a different text representation and a suitable learning algorithm:
### 1. 🧱 Simple Approach: Bag-of-Words (BoW) + Machine Learning Models
Tweets are vectorized using BoW or BoW + TF-IDF, which count word frequencies.
Unlike other NLP projects, we avoided LDA (unsupervised, topic modeling oriented).
➡️ **Tested models**:
- Logistic Regression
- Naive Bayes
- SVM
- Random Forest
These models are trained with **GridSearchCV**, optimizing **precision (`precision`)**, our key business KPI. Various KPIs were computed on the test set.
![Simple Models Scores](./Basic-Models.png)
---
### 2. 🔥 Advanced Approach: Word Embeddings + LSTM
We use dense Word2Vec vectors (trained) and GloVe 300d (pretrained by Stanford), which capture semantic similarities between words. However, these representations **do not take into account the exact context** (e.g., "cancelled" is probably very negative for a flight but positive in a legal case with charges against you).
Classification is done by an **LSTM**, capable of exploiting word sequentiality.
The **deployed model is automatically selected** between Word2Vec + LSTM and GloVe + LSTM, according to the **highest precision recorded in MLFlow**.
Model `Word2Vec + LSTM` (`test_precision: 0.797, val_precision: 0.795`):
![Advanced LSTM + Word2Vec Training Scores](./Word2Vec_LSTM_epoch9.png)
Model `GloVe 300d + LSTM` (`test_precision: 0.792, val_precision: 0.798`):
![Advanced LSTM + GloVe 300d Training Scores](./GloVe-300d_LSTM_epoch12.png)
Although GloVe 300d + LSTM shows a slightly better precision score on the validation set than Word2Vec + LSTM, it remains lower on the test set (relatively negligible overall).
---
### 3. 🤖 Transformer Approach: DistilBERT, Lightweight Contextual Embeddings
We used **DistilBERT**, a lighter and faster version of BERT (created by Hugging Face).
It retains **95% of BERT's performance** while being **40% smaller** and **60% faster**, making it especially suited for **training on limited GPUs** (Google Colab) and **faster inference**.
Like BERT, DistilBERT produces **contextual embeddings**, meaning word meanings vary depending on context. For example, *"bank"* in *"I go to the bank"* and *"database bank"* will not have the same vector.
It was **fine-tuned** here for binary classification (positive/negative), with a tokenizer and encoding specific to its architecture.
> **Model `DistilBERT`** (`test_precision: 0.861, val_precision: 0.858`)
![DistilBERT Training Curves](./bert_training_curves_epoch1.png)
![Confusion Matrix](./bert_confusion_matrix.png)
![ROC Curve](./bert_roc_curve.png)
⚠️ **Limits**: Like all pretrained models, DistilBERT **is not sensitive to irony, puns, or implicit cultural references**.
Twitter is an ideal playground for this kind of nuanced content. These limits also apply to Word2Vec, GloVe, and classical models.
---
## 🎯 Why Optimize Precision (Not Accuracy)?
**Accuracy** = overall proportion of correct predictions.
**Precision** = proportion of *predicted positive* tweets that are *actually positive*.
> Example: out of 100 tweets, if the model predicts 30 positives but 15 are wrong, precision is 50%.
We preferred to **minimize false positives**, because in a sensitive context, **announcing a tweet as positive when it is negative can be problematic**.
- Classical models: **GridSearchCV optimized for precision**
- Deep Learning models: **EarlyStopping on val_loss**, but **final selection based on precision**
---
## ⚙️ Sovereign MLOps Environment
### 🖥️ Personal NAS Server + OpenMediaVault
MLFlow is installed on a **personal NAS (Intel N100)** running **OpenMediaVault (Debian)**.
We use **MinIO** to store artifacts and models, ensuring local, sovereign, and S3-compatible management.
> Notebooks, run on Google Colab to leverage GPUs, log in MLFlow and send models to MinIO via **environment variables**.
MLFlow Experiments Dashboard:
![MLFlow Dashboard](./mlflow_experiments_dashboard.png)
Production Models (model registry):
![Models Registry](mlflow_models_registry_2.png)
Best non-BERT selected model:
![Selected Model](mlflow_models_registry_best_LSTM_model.png)
![Best Non-BERT Model](mlflolw_experiments_dashboard_best_LSTM_model.png)
---
## 🧪 CI, Testing and Versioning
### ✅ Unit Tests
- Check correct preprocessing, predictions, and error handling.
- Integrated into **GitHub Actions**, **blocking deployment if tests fail.**
### 🐳 Containerization & CI/CD
The FastAPI API is packaged with Docker, then pushed to DockerHub.
A full [CI/CD pipeline](https://github.com/GrgMdmn/OpenClassrooms_AI_Projects/blob/main/.github/workflows/p7_ci-cd.yml) guarantees **reproducible deployment**, ensuring:
- Validation of unit tests
- Build and push of the containerized API to [DockerHub](https://hub.docker.com/r/grgmdmn/sentiment_api)
![GitHub Workflow](github_ci-cd.png)
---
## 🚀 FastAPI API Deployment (Dual)
### 🔧 Backend: FastAPI
- Exposes a `/predict` endpoint
- Dynamically loads the **best Word Embedding + LSTM model** via MLFlow Model Registry
- Applies appropriate embeddings according to the `embedding_type` tag
### 💻 Streamlit Interface
Local interface for request testing, debugging, demonstrations.
![Streamlit Local](./streamlit_interface_local.png)
---
### 🌐 Dual Deployment (NAS + Google Cloud)
#### 🔧 AVX2 Issue
Initially, the API did not run on the NAS (Intel N100 CPU) because it did not support **AVX2 instructions**, required by TensorFlow.
> 🛠️ **Solution**: manual activation of AVX2 support in BIOS. This option is often disabled by default on NAS motherboards for power consumption or stability reasons.
Since this change, the NAS can run the API without errors.
---
#### 🚀 Deployed APIs
The project offers a **dual API deployment**, locally and in the cloud:
- 🏠 **Local NAS**: [sentiment-api.greg-madman-nas.duckdns.org](https://sentiment-api.greg-madman-nas.duckdns.org)
Docker Compose interface on the NAS:
![Docker Compose OpenMediaVault](openmediavault_docker-compose_settings.png)
- ☁️ **Google Cloud Run**: [sentiment-api-service-7772256003.europe-west1.run.app](https://sentiment-api-service-7772256003.europe-west1.run.app)
Google Cloud deployment interface:
![Google Cloud Deployment](google_cloud_interface.png)
---
#### 🔄 Automated Continuous Deployment (NAS only)
To ensure **continuous deployment** on the NAS, we chose to install [**Watchtower**](https://containrrr.dev/watchtower/arguments/) to maintain a 100% `docker` environment. This tool checks every **10 minutes** the Docker image of the API (`sentiment_api`) and, if a new version is available on DockerHub, automatically restarts the container with the latest image.
![Docker Watchtower in OMV Docker-Compose](watchtower_docker.png)
This mechanism ensures that the version deployed on the NAS always stays synchronized with the latest version validated by GitHub Actions tests, **without manual intervention**.
![Logs watchtower in docker (SSH console)](watchtower_logs.png)
An **email alert system** automatically notifies any update performed.
![Watchtower Logs by Email](watchtower_logs_by_email.png)
---
## 📊 Home-made Monitoring
A lightweight alert logic embedded in the API:
![1st Error](./api_first_error.png)
![2nd Error](./api_second_error.png)
![3rd Error](./api_third_error.png)
If **3 consecutive prediction errors** are detected within a **5-minute window**, a report is automatically generated.
![Error Report Sent by Email](./api_error_report.png)
---
## 🧠 Overall MLOps Pipeline Diagram
### Summary Diagram
```
┌────────────┐
│ Data │
│ (tweets) │
└─────┬──────┘
│
Preprocessing (regex, tokenization, lemmatization, stopwords)
│
┌─────────▼──────────┐
│ ML Training │
└─────────┬──────────┘
│
Logging via MLFlow + artifacts MinIO
│
Select best "advanced" model (excluding BERT) by precision
│
┌─────────▼──────────┐
│ FastAPI │
│ + Streamlit │
└─────────┬──────────┘
│
Deployment:
├─ Google Cloud Run (manual)
└─ Local NAS (automated via Watchtower)
│
┌──────────▼─────────────┐
│ Lightweight in-house │
│ Monitoring │
│ (alerts on bad │
│ predictions) │
└────────────────────────┘
```
### Detailed Diagram
![Detailed Diagram](diagram.drawio.png)
---
## ✨ Conclusion
This project demonstrates how to build a complete, autonomous, and reproducible MLOps pipeline for NLP applied to Twitter.
It highlights the **strengths and limitations of the models** (including BERT), the need for a **metric adapted to the context** (precision vs accuracy), and the importance of **controlled deployment**, even on personal infrastructure.
---
💬 Thank you for reading!
Demander à ChatGPT
# ✨ Sentiment Analysis and MLOps Deployment: A Complete Experience Report
As part of the *Air Paradis* project, we implemented a full sentiment analysis pipeline from tweets, covering everything from modeling to MLOps deployment. This blog post reviews the key project stages: methodological choices, model comparison, deployment of a robust API, and deployment on two environments (local + cloud). 🧠🔧
---
## 🔍 Three Supervised Modeling Approaches
We tested three complementary approaches to predict the sentiment of a tweet. Each relies on a different text representation and a suitable learning algorithm:
### 1. 🧱 Simple Approach: Bag-of-Words (BoW) + Machine Learning Models
Tweets are vectorized using BoW or BoW + TF-IDF, which count word frequencies.
Unlike other NLP projects, we avoided LDA (unsupervised, topic modeling oriented).
➡️ **Tested models**:
- Logistic Regression
- Naive Bayes
- SVM
- Random Forest
These models are trained with **GridSearchCV**, optimizing **precision (`precision`)**, our key business KPI. Various KPIs were computed on the test set.
![Simple Models Scores](./Basic-Models.png)
---
### 2. 🔥 Advanced Approach: Word Embeddings + LSTM
We use dense Word2Vec vectors (trained) and GloVe 300d (pretrained by Stanford), which capture semantic similarities between words. However, these representations **do not take into account the exact context** (e.g., "cancelled" is probably very negative for a flight but positive in a legal case with charges against you).
Classification is done by an **LSTM**, capable of exploiting word sequentiality.
The **deployed model is automatically selected** between Word2Vec + LSTM and GloVe + LSTM, according to the **highest precision recorded in MLFlow**.
Model `Word2Vec + LSTM` (`test_precision: 0.797, val_precision: 0.795`):
![Advanced LSTM + Word2Vec Training Scores](./Word2Vec_LSTM_epoch9.png)
Model `GloVe 300d + LSTM` (`test_precision: 0.792, val_precision: 0.798`):
![Advanced LSTM + GloVe 300d Training Scores](./GloVe-300d_LSTM_epoch12.png)
Although GloVe 300d + LSTM shows a slightly better precision score on the validation set than Word2Vec + LSTM, it remains lower on the test set (relatively negligible overall).
---
### 3. 🤖 Transformer Approach: DistilBERT, Lightweight Contextual Embeddings
We used **DistilBERT**, a lighter and faster version of BERT (created by Hugging Face).
It retains **95% of BERT's performance** while being **40% smaller** and **60% faster**, making it especially suited for **training on limited GPUs** (Google Colab) and **faster inference**.
Like BERT, DistilBERT produces **contextual embeddings**, meaning word meanings vary depending on context. For example, *"bank"* in *"I go to the bank"* and *"database bank"* will not have the same vector.
It was **fine-tuned** here for binary classification (positive/negative), with a tokenizer and encoding specific to its architecture.
> **Model `DistilBERT`** (`test_precision: 0.861, val_precision: 0.858`)
![DistilBERT Training Curves](./bert_training_curves_epoch1.png)
![Confusion Matrix](./bert_confusion_matrix.png)
![ROC Curve](./bert_roc_curve.png)
⚠️ **Limits**: Like all pretrained models, DistilBERT **is not sensitive to irony, puns, or implicit cultural references**.
Twitter is an ideal playground for this kind of nuanced content. These limits also apply to Word2Vec, GloVe, and classical models.
---
## 🎯 Why Optimize Precision (Not Accuracy)?
**Accuracy** = overall proportion of correct predictions.
**Precision** = proportion of *predicted positive* tweets that are *actually positive*.
> Example: out of 100 tweets, if the model predicts 30 positives but 15 are wrong, precision is 50%.
We preferred to **minimize false positives**, because in a sensitive context, **announcing a tweet as positive when it is negative can be problematic**.
- Classical models: **GridSearchCV optimized for precision**
- Deep Learning models: **EarlyStopping on val_loss**, but **final selection based on precision**
---
## ⚙️ Sovereign MLOps Environment
### 🖥️ Personal NAS Server + OpenMediaVault
MLFlow is installed on a **personal NAS (Intel N100)** running **OpenMediaVault (Debian)**.
We use **MinIO** to store artifacts and models, ensuring local, sovereign, and S3-compatible management.
> Notebooks, run on Google Colab to leverage GPUs, log in MLFlow and send models to MinIO via **environment variables**.
MLFlow Experiments Dashboard:
![MLFlow Dashboard](./mlflow_experiments_dashboard.png)
Production Models (model registry):
![Models Registry](mlflow_models_registry_2.png)
Best non-BERT selected model:
![Selected Model](mlflow_models_registry_best_LSTM_model.png)
![Best Non-BERT Model](mlflolw_experiments_dashboard_best_LSTM_model.png)
---
## 🧪 CI, Testing and Versioning
### ✅ Unit Tests
- Check correct preprocessing, predictions, and error handling.
- Integrated into **GitHub Actions**, **blocking deployment if tests fail.**
### 🐳 Containerization & CI/CD
The FastAPI API is packaged with Docker, then pushed to DockerHub.
A full [CI/CD pipeline](https://github.com/GrgMdmn/OpenClassrooms_AI_Projects/blob/main/.github/workflows/p7_ci-cd.yml) guarantees **reproducible deployment**, ensuring:
- Validation of unit tests
- Build and push of the containerized API to [DockerHub](https://hub.docker.com/r/grgmdmn/sentiment_api)
![GitHub Workflow](github_ci-cd.png)
---
## 🚀 FastAPI API Deployment (Dual)
### 🔧 Backend: FastAPI
- Exposes a `/predict` endpoint
- Dynamically loads the **best Word Embedding + LSTM model** via MLFlow Model Registry
- Applies appropriate embeddings according to the `embedding_type` tag
### 💻 Streamlit Interface
Local interface for request testing, debugging, demonstrations.
![Streamlit Local](./streamlit_interface_local.png)
---
### 🌐 Dual Deployment (NAS + Google Cloud)
#### 🔧 AVX2 Issue
Initially, the API did not run on the NAS (Intel N100 CPU) because it did not support **AVX2 instructions**, required by TensorFlow.
> 🛠️ **Solution**: manual activation of AVX2 support in BIOS. This option is often disabled by default on NAS motherboards for power consumption or stability reasons.
Since this change, the NAS can run the API without errors.
---
#### 🚀 Deployed APIs
The project offers a **dual API deployment**, locally and in the cloud:
- 🏠 **Local NAS**: [sentiment-api.greg-madman-nas.duckdns.org](https://sentiment-api.greg-madman-nas.duckdns.org)
Docker Compose interface on the NAS:
![Docker Compose OpenMediaVault](openmediavault_docker-compose_settings.png)
- ☁️ **Google Cloud Run**: [sentiment-api-service-7772256003.europe-west1.run.app](https://sentiment-api-service-7772256003.europe-west1.run.app)
Google Cloud deployment interface:
![Google Cloud Deployment](google_cloud_interface.png)
---
#### 🔄 Automated Continuous Deployment (NAS only)
To ensure **continuous deployment** on the NAS, we chose to install [**Watchtower**](https://containrrr.dev/watchtower/arguments/) to maintain a 100% `docker` environment. This tool checks every **10 minutes** the Docker image of the API (`sentiment_api`) and, if a new version is available on DockerHub, automatically restarts the container with the latest image.
![Docker Watchtower in OMV Docker-Compose](watchtower_docker.png)
This mechanism ensures that the version deployed on the NAS always stays synchronized with the latest version validated by GitHub Actions tests, **without manual intervention**.
![Logs watchtower in docker (SSH console)](watchtower_logs.png)
An **email alert system** automatically notifies any update performed.
![Watchtower Logs by Email](watchtower_logs_by_email.png)
---
## 📊 Home-made Monitoring
A lightweight alert logic embedded in the API:
![1st Error](./api_first_error.png)
![2nd Error](./api_second_error.png)
![3rd Error](./api_third_error.png)
If **3 consecutive prediction errors** are detected within a **5-minute window**, a report is automatically generated.
![Error Report Sent by Email](./api_error_report.png)
---
## 🧠 Overall MLOps Pipeline Diagram
### Summary Diagram
```
┌────────────┐
│ Data │
│ (tweets) │
└─────┬──────┘
│
Preprocessing (regex, tokenization, lemmatization, stopwords)
│
┌─────────▼──────────┐
│ ML Training │
└─────────┬──────────┘
│
Logging via MLFlow + artifacts MinIO
│
Select best "advanced" model (excluding BERT) by precision
│
┌─────────▼──────────┐
│ FastAPI │
│ + Streamlit │
└─────────┬──────────┘
│
Deployment:
├─ Google Cloud Run (manual)
└─ Local NAS (automated via Watchtower)
│
┌──────────▼─────────────┐
│ Lightweight in-house │
│ Monitoring │
│ (alerts on bad │
│ predictions) │
└────────────────────────┘
```
### Detailed Diagram
![Detailed Diagram](diagram.drawio.png)
---
## ✨ Conclusion
This project demonstrates how to build a complete, autonomous, and reproducible MLOps pipeline for NLP applied to Twitter.
It highlights the **strengths and limitations of the models** (including BERT), the need for a **metric adapted to the context** (precision vs accuracy), and the importance of **controlled deployment**, even on personal infrastructure.
---
💬 Thank you for reading!
Demander à ChatGPT

View file

@ -1,125 +1,125 @@
import re
import html
import numpy as np
import pandas as pd
from multiprocessing import Pool
# NLTK imports for NLP preprocessing
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
# Ensure necessary NLTK resources are downloaded and available
try:
stopwords.words('english')
word_tokenize("test")
WordNetLemmatizer().lemmatize("testing")
except LookupError:
nltk.download('punkt') # Tokenizer models
nltk.download('punkt_tab') # Additional tokenizer support
nltk.download('stopwords') # Stopwords lists
nltk.download('wordnet') # WordNet lexicon for lemmatization
nltk.download('omw-1.4') # WordNet lemmas data
# Convert sentiment labels to binary: 0 for negative, 1 for all other classes
def convert_sentiment_label(df):
"""
Convert the 'target' column in the DataFrame to binary sentiment labels:
- 0 for negative sentiment (label 0)
- 1 for any other sentiment label
Returns a new DataFrame with updated labels.
"""
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
# Downsample the dataset to balance classes by limiting samples per class
def downsample_data(df, n_samples=50000, random_state=42):
"""
Perform downsampling to balance dataset classes.
Selects `n_samples` examples from both negative (0) and positive (1) classes.
Returns a balanced DataFrame with equal number of samples per class.
"""
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
# PREPROCESSING FUNCTION FOR CLASSIC AND ADVANCED MODELS
def preprocess_tweet(tweet):
"""
Preprocess a tweet for classic ML or word embedding models (e.g., Word2Vec, GloVe).
Steps:
- Normalize text (lowercase)
- Replace URLs and mentions with tokens
- Decode HTML entities
- Remove unwanted special characters and non-printable chars
- Tokenize text
- Lemmatize tokens
- Remove stopwords except negation words
Returns cleaned, normalized text as a single string.
"""
# Ensure input is a string
if not isinstance(tweet, str):
return ""
# Decode HTML entities (e.g., &amp; to &)
tweet = html.unescape(tweet)
# Convert to lowercase
tweet = tweet.lower()
# Replace URLs with placeholder token
tweet = re.sub(r'https?://\S+|www\.\S+', '<URL>', tweet)
# Replace user mentions with placeholder token
tweet = re.sub(r'@\w+', '<MENTION>', tweet)
# Separate hashtags from words to facilitate tokenization
tweet = re.sub(r'#(\w+)', r'# \1', tweet)
# Remove unwanted punctuation and special characters except some useful ones
tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet)
# Remove non-printable and control characters (e.g., tabs, newlines, emojis)
tweet = re.sub(r'[^\x20-\x7E]', '', tweet)
# Tokenize tweet into words
tokens = word_tokenize(tweet)
# Lemmatize tokens to their base form
lemmatizer = WordNetLemmatizer()
tokens = [lemmatizer.lemmatize(token) for token in tokens]
# Remove stopwords but keep negation-related words to preserve sentiment meaning
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]
# Rebuild cleaned tweet text
return ' '.join(tokens)
# Parallel processing of a DataFrame using multiple CPU cores
def process_in_parallel(df, func, n_jobs=4):
"""
Split a DataFrame into `n_jobs` parts and apply `func` to each part in parallel,
using multiprocessing Pool for speed-up.
Returns the concatenated processed DataFrame.
"""
df_split = np.array_split(df, n_jobs)
with Pool(n_jobs) as pool:
df = pd.concat(pool.map(func, df_split))
return df
# Apply preprocessing function on a DataFrame partition
def apply_preprocessing(df_part):
"""
Apply tweet preprocessing to the 'text' column of a DataFrame partition.
Stores results in a new column 'processed_text'.
Returns the updated DataFrame partition.
"""
df_part['processed_text'] = df_part['text'].apply(preprocess_tweet)
return df_part
import re
import html
import numpy as np
import pandas as pd
from multiprocessing import Pool
# NLTK imports for NLP preprocessing
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
# Ensure necessary NLTK resources are downloaded and available
try:
stopwords.words('english')
word_tokenize("test")
WordNetLemmatizer().lemmatize("testing")
except LookupError:
nltk.download('punkt') # Tokenizer models
nltk.download('punkt_tab') # Additional tokenizer support
nltk.download('stopwords') # Stopwords lists
nltk.download('wordnet') # WordNet lexicon for lemmatization
nltk.download('omw-1.4') # WordNet lemmas data
# Convert sentiment labels to binary: 0 for negative, 1 for all other classes
def convert_sentiment_label(df):
"""
Convert the 'target' column in the DataFrame to binary sentiment labels:
- 0 for negative sentiment (label 0)
- 1 for any other sentiment label
Returns a new DataFrame with updated labels.
"""
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
# Downsample the dataset to balance classes by limiting samples per class
def downsample_data(df, n_samples=50000, random_state=42):
"""
Perform downsampling to balance dataset classes.
Selects `n_samples` examples from both negative (0) and positive (1) classes.
Returns a balanced DataFrame with equal number of samples per class.
"""
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
# PREPROCESSING FUNCTION FOR CLASSIC AND ADVANCED MODELS
def preprocess_tweet(tweet):
"""
Preprocess a tweet for classic ML or word embedding models (e.g., Word2Vec, GloVe).
Steps:
- Normalize text (lowercase)
- Replace URLs and mentions with tokens
- Decode HTML entities
- Remove unwanted special characters and non-printable chars
- Tokenize text
- Lemmatize tokens
- Remove stopwords except negation words
Returns cleaned, normalized text as a single string.
"""
# Ensure input is a string
if not isinstance(tweet, str):
return ""
# Decode HTML entities (e.g., &amp; to &)
tweet = html.unescape(tweet)
# Convert to lowercase
tweet = tweet.lower()
# Replace URLs with placeholder token
tweet = re.sub(r'https?://\S+|www\.\S+', '<URL>', tweet)
# Replace user mentions with placeholder token
tweet = re.sub(r'@\w+', '<MENTION>', tweet)
# Separate hashtags from words to facilitate tokenization
tweet = re.sub(r'#(\w+)', r'# \1', tweet)
# Remove unwanted punctuation and special characters except some useful ones
tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet)
# Remove non-printable and control characters (e.g., tabs, newlines, emojis)
tweet = re.sub(r'[^\x20-\x7E]', '', tweet)
# Tokenize tweet into words
tokens = word_tokenize(tweet)
# Lemmatize tokens to their base form
lemmatizer = WordNetLemmatizer()
tokens = [lemmatizer.lemmatize(token) for token in tokens]
# Remove stopwords but keep negation-related words to preserve sentiment meaning
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]
# Rebuild cleaned tweet text
return ' '.join(tokens)
# Parallel processing of a DataFrame using multiple CPU cores
def process_in_parallel(df, func, n_jobs=4):
"""
Split a DataFrame into `n_jobs` parts and apply `func` to each part in parallel,
using multiprocessing Pool for speed-up.
Returns the concatenated processed DataFrame.
"""
df_split = np.array_split(df, n_jobs)
with Pool(n_jobs) as pool:
df = pd.concat(pool.map(func, df_split))
return df
# Apply preprocessing function on a DataFrame partition
def apply_preprocessing(df_part):
"""
Apply tweet preprocessing to the 'text' column of a DataFrame partition.
Stores results in a new column 'processed_text'.
Returns the updated DataFrame partition.
"""
df_part['processed_text'] = df_part['text'].apply(preprocess_tweet)
return df_part

View file

@ -1,46 +1,46 @@
# Notebooks
Ce dossier contient les notebooks Jupyter utilisés pour le développement des modèles et les analyses du projet.
## Contenu
- Analyse exploratoire des données
- Modèles classiques (régression logistique, SVM, etc.)
- Modèles avancés (LSTM avec embeddings GloVe)
- Modèle BERT
## Note concernant les fichiers requirements
Les fichiers `*requirements.txt` ont été générés depuis un environnement Google Colab. Cet environnement inclut de nombreuses librairies préinstallées, dont toutes ne sont pas nécessaires au projet.
Un nettoyage manuel ou une génération spécifique des fichiers requirements sera nécessaire pour alléger ces fichiers avant un usage en production ou un déploiement.
---
## Résumé des performances des modèles
### Modèles classiques (entraînés sur 200 000 tweets)
| Modèle | Précision | Temps d'entraînement|
|--------------------------------|-----------|---------------------|
| Random Forest + BoW | 0,7798 | ~5 heures |
| Régression logistique + TF-IDF | 0,7796 | <2 minutes |
### Modèles avancés basés sur des embeddings (entraînés sur 200 000 tweets, accélérés GPU sur Google Colab)
| Modèle | Précision | Temps d'entraînement|
|------------------------|-----------|---------------------|
| Word2Vec + LSTM | 0,797 | 57 secondes |
| GloVe (300d) + LSTM | 0,792 | 47 secondes |
### Modèle de pointe (entraîné sur 100 000 tweets en raison des limites RAM)
| Modèle | Précision | Temps d'entraînement|
|---------|-----------|---------------------|
| BERT | 0,882 | 1,3 minutes |
---
Ce résumé met en lumière les compromis entre précision des modèles et temps d’entraînement. Si les modèles classiques sont plus rapides à entraîner, les modèles avancés utilisant des embeddings et LSTM offrent une meilleure précision, avec BERT qui atteint la meilleure précision malgré un jeu de données plus restreint.
L’écart de précision entre BERT et les modèles à embeddings peut s’expliquer principalement par la capacité bidirectionnelle de BERT à comprendre le contexte.
# Notebooks
Ce dossier contient les notebooks Jupyter utilisés pour le développement des modèles et les analyses du projet.
## Contenu
- Analyse exploratoire des données
- Modèles classiques (régression logistique, SVM, etc.)
- Modèles avancés (LSTM avec embeddings GloVe)
- Modèle BERT
## Note concernant les fichiers requirements
Les fichiers `*requirements.txt` ont été générés depuis un environnement Google Colab. Cet environnement inclut de nombreuses librairies préinstallées, dont toutes ne sont pas nécessaires au projet.
Un nettoyage manuel ou une génération spécifique des fichiers requirements sera nécessaire pour alléger ces fichiers avant un usage en production ou un déploiement.
---
## Résumé des performances des modèles
### Modèles classiques (entraînés sur 200 000 tweets)
| Modèle | Précision | Temps d'entraînement|
|--------------------------------|-----------|---------------------|
| Random Forest + BoW | 0,7798 | ~5 heures |
| Régression logistique + TF-IDF | 0,7796 | <2 minutes |
### Modèles avancés basés sur des embeddings (entraînés sur 200 000 tweets, accélérés GPU sur Google Colab)
| Modèle | Précision | Temps d'entraînement|
|------------------------|-----------|---------------------|
| Word2Vec + LSTM | 0,797 | 57 secondes |
| GloVe (300d) + LSTM | 0,792 | 47 secondes |
### Modèle de pointe (entraîné sur 100 000 tweets en raison des limites RAM)
| Modèle | Précision | Temps d'entraînement|
|---------|-----------|---------------------|
| BERT | 0,882 | 1,3 minutes |
---
Ce résumé met en lumière les compromis entre précision des modèles et temps d’entraînement. Si les modèles classiques sont plus rapides à entraîner, les modèles avancés utilisant des embeddings et LSTM offrent une meilleure précision, avec BERT qui atteint la meilleure précision malgré un jeu de données plus restreint.
L’écart de précision entre BERT et les modèles à embeddings peut s’expliquer principalement par la capacité bidirectionnelle de BERT à comprendre le contexte.

View file

@ -1,46 +1,46 @@
# Notebooks
This folder contains the Jupyter notebooks used for model development and analyses in the project.
## Contents
- Exploratory Data Analysis
- Classical Models (logistic regression, SVM, etc.)
- Advanced Models (LSTM with GloVe embeddings)
- BERT Model
## Note on requirements files
The `*requirements.txt` files were generated from a Google Colab environment. This environment includes many pre-installed libraries, not all necessary for the project.
Manual cleanup or specific generation of requirements files will be needed to slim down these files before production use or deployment.
---
## Model Performance Summary
### Classical Models (trained on 200,000 tweets)
| Model | Precision | Training Time |
|------------------------------|-----------|-----------------|
| Random Forest + BoW | 0.7798 | ~5 hours |
| Logistic Regression + TF-IDF | 0.7796 | <2 minutes |
### Advanced Models based on word-embeddings (trained on 200,000 tweets, GPU accelerated on Google Colab)
| Model | Precision | Training Time |
|------------------------|-----------|---------------|
| Word2Vec + LSTM | 0.797 | 57 seconds |
| GloVe (300d) + LSTM | 0.792 | 47 seconds |
### State-of-the-art Model (trained on 100,000 tweets due to RAM limits)
| Model | Precision | Training Time |
|--------|-----------|---------------|
| BERT | 0.882 | 1.3 minutes |
---
This summary highlights the trade-offs between model precision and training time. While the classical models are faster to train, advanced models leveraging embeddings and LSTM yield better precision, with BERT achieving the highest precision despite being trained on a smaller dataset.
The precision offset between BERT and word embeddings models may mainly be due to the BERT bidirectionnal context abilities.
# Notebooks
This folder contains the Jupyter notebooks used for model development and analyses in the project.
## Contents
- Exploratory Data Analysis
- Classical Models (logistic regression, SVM, etc.)
- Advanced Models (LSTM with GloVe embeddings)
- BERT Model
## Note on requirements files
The `*requirements.txt` files were generated from a Google Colab environment. This environment includes many pre-installed libraries, not all necessary for the project.
Manual cleanup or specific generation of requirements files will be needed to slim down these files before production use or deployment.
---
## Model Performance Summary
### Classical Models (trained on 200,000 tweets)
| Model | Precision | Training Time |
|------------------------------|-----------|-----------------|
| Random Forest + BoW | 0.7798 | ~5 hours |
| Logistic Regression + TF-IDF | 0.7796 | <2 minutes |
### Advanced Models based on word-embeddings (trained on 200,000 tweets, GPU accelerated on Google Colab)
| Model | Precision | Training Time |
|------------------------|-----------|---------------|
| Word2Vec + LSTM | 0.797 | 57 seconds |
| GloVe (300d) + LSTM | 0.792 | 47 seconds |
### State-of-the-art Model (trained on 100,000 tweets due to RAM limits)
| Model | Precision | Training Time |
|--------|-----------|---------------|
| BERT | 0.882 | 1.3 minutes |
---
This summary highlights the trade-offs between model precision and training time. While the classical models are faster to train, advanced models leveraging embeddings and LSTM yield better precision, with BERT achieving the highest precision despite being trained on a smaller dataset.
The precision offset between BERT and word embeddings models may mainly be due to the BERT bidirectionnal context abilities.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long