Initial commit of scripts
This commit is contained in:
parent
96e22496ff
commit
85ae7f4855
26 changed files with 10279 additions and 10279 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,2 +1,2 @@
|
|||
**/mlruns
|
||||
**/downloaded_artifacts
|
||||
**/mlruns
|
||||
**/downloaded_artifacts
|
||||
|
|
|
|||
122
README.fr.md
122
README.fr.md
|
|
@ -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
122
README.md
|
|
@ -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**
|
||||
|
|
@ -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).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
deployment/api/.gitignore
vendored
12
deployment/api/.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# ssh .pem files
|
||||
*.pem
|
||||
|
||||
*aws*
|
||||
|
||||
sentiment-api-*.json
|
||||
# ssh .pem files
|
||||
*.pem
|
||||
|
||||
*aws*
|
||||
|
||||
sentiment-api-*.json
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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., & → &)
|
||||
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., & → &)
|
||||
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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -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 🚀 & I’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 = """
|
||||
"Flight of dreams" w/ @AirParadis! Got food, space & smiles 😄 I’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 & still amazed 😍 Crew = lovely à 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 🚀 & I’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 = """
|
||||
"Flight of dreams" w/ @AirParadis! Got food, space & smiles 😄 I’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 & still amazed 😍 Crew = lovely à every step!
|
||||
#flyhappy #AirParadis
|
||||
"""
|
||||
assert preprocess_tweet(tweet) == '3rd time w < MENTION > still amazed crew lovely every step ! # flyhappy # airparadis'
|
||||
|
|
|
|||
468
docs/blog.fr.md
468
docs/blog.fr.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 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`):
|
||||

|
||||
Modèle `GloVe 300d + LSTM` (`test_precision : 0.792 , val_precision : 0.798`):
|
||||

|
||||
|
||||
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`)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
⚠️ **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 :
|
||||

|
||||
|
||||
Modèles en production (registre de modèles):
|
||||

|
||||
|
||||
Meilleur modèle non BERT retenu :
|
||||

|
||||

|
||||
---
|
||||
|
||||
## 🧪 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)
|
||||
|
||||

|
||||
---
|
||||
|
||||
## 🚀 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🌐 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 :
|
||||

|
||||
|
||||
- ☁️ **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 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.
|
||||

|
||||
|
||||
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**.
|
||||

|
||||
|
||||
Un **système d’alerte mail** notifie automatiquement toute mise à jour effectuée.
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring maison
|
||||
|
||||
Une logique d’alerte légère embarquée dans l’API :
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🧠 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é
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ✨ 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 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`):
|
||||

|
||||
Modèle `GloVe 300d + LSTM` (`test_precision : 0.792 , val_precision : 0.798`):
|
||||

|
||||
|
||||
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`)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
⚠️ **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 :
|
||||

|
||||
|
||||
Modèles en production (registre de modèles):
|
||||

|
||||
|
||||
Meilleur modèle non BERT retenu :
|
||||

|
||||

|
||||
---
|
||||
|
||||
## 🧪 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)
|
||||
|
||||

|
||||
---
|
||||
|
||||
## 🚀 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🌐 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 :
|
||||

|
||||
|
||||
- ☁️ **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 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.
|
||||

|
||||
|
||||
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**.
|
||||

|
||||
|
||||
Un **système d’alerte mail** notifie automatiquement toute mise à jour effectuée.
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring maison
|
||||
|
||||
Une logique d’alerte légère embarquée dans l’API :
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🧠 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é
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ✨ 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 !
|
||||
490
docs/blog.md
490
docs/blog.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 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`):
|
||||

|
||||
Model `GloVe 300d + LSTM` (`test_precision: 0.792, val_precision: 0.798`):
|
||||

|
||||
|
||||
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`)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
⚠️ **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:
|
||||

|
||||
|
||||
Production Models (model registry):
|
||||

|
||||
|
||||
Best non-BERT selected model:
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🧪 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)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🚀 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🌐 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:
|
||||

|
||||
|
||||
- ☁️ **Google Cloud Run**: [sentiment-api-service-7772256003.europe-west1.run.app](https://sentiment-api-service-7772256003.europe-west1.run.app)
|
||||
Google Cloud deployment interface:
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### 🔄 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.
|
||||

|
||||
|
||||
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**.
|
||||

|
||||
|
||||
An **email alert system** automatically notifies any update performed.
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📊 Home-made Monitoring
|
||||
|
||||
A lightweight alert logic embedded in the API:
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
If **3 consecutive prediction errors** are detected within a **5-minute window**, a report is automatically generated.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🧠 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
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ✨ 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 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`):
|
||||

|
||||
Model `GloVe 300d + LSTM` (`test_precision: 0.792, val_precision: 0.798`):
|
||||

|
||||
|
||||
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`)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
⚠️ **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:
|
||||

|
||||
|
||||
Production Models (model registry):
|
||||

|
||||
|
||||
Best non-BERT selected model:
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🧪 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)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🚀 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🌐 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:
|
||||

|
||||
|
||||
- ☁️ **Google Cloud Run**: [sentiment-api-service-7772256003.europe-west1.run.app](https://sentiment-api-service-7772256003.europe-west1.run.app)
|
||||
Google Cloud deployment interface:
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### 🔄 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.
|
||||

|
||||
|
||||
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**.
|
||||

|
||||
|
||||
An **email alert system** automatically notifies any update performed.
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📊 Home-made Monitoring
|
||||
|
||||
A lightweight alert logic embedded in the API:
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
If **3 consecutive prediction errors** are detected within a **5-minute window**, a report is automatically generated.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🧠 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
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ✨ 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
|
||||
|
|
|
|||
250
functions.py
250
functions.py
|
|
@ -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., & 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., & 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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue