Spaces:
Runtime error
Runtime error
Upload 8 files
Browse files- README.md +246 -6
- app.py +139 -0
- build_dataframe.py +114 -0
- data_processing.py +102 -0
- generate_response.py +135 -0
- main.py +84 -0
- requirements.txt +6 -0
- test_app.py +132 -0
README.md
CHANGED
|
@@ -1,13 +1,253 @@
|
|
| 1 |
---
|
| 2 |
-
title: Amazon Sentiment Analysis
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Amazon Sentiment Analysis
|
| 3 |
+
emoji: 🛍️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# 🛍️ Analyse de Sentiment d'Avis Amazon + Réponses Automatiques
|
| 14 |
+
|
| 15 |
+
## 📋 Description du Projet
|
| 16 |
+
|
| 17 |
+
Pipeline IA complet pour l'analyse d'avis clients Amazon en français avec :
|
| 18 |
+
1. **Nettoyage des textes** (suppression stopwords, ponctuation, normalisation)
|
| 19 |
+
2. **Analyse de sentiment** (positif/négatif)
|
| 20 |
+
3. **Génération automatique de réponses** pour les avis négatifs avec Qwen2.5-3B
|
| 21 |
+
|
| 22 |
+
## 🎯 Objectifs Pédagogiques
|
| 23 |
+
|
| 24 |
+
Projet développé dans le cadre d'un **Master en AI Project Management** au Collège de Paris :
|
| 25 |
+
|
| 26 |
+
- Maîtrise d'un **pipeline NLP complet** (preprocessing → analyse → génération)
|
| 27 |
+
- Utilisation de **modèles de langage open-source** (Qwen2.5-3B-Instruct)
|
| 28 |
+
- **Déploiement avec CI/CD automatique** sur Hugging Face Spaces
|
| 29 |
+
- Application de l'**éthique IA** (transparence, explicabilité)
|
| 30 |
+
|
| 31 |
+
## 🛠️ Technologies Utilisées
|
| 32 |
+
|
| 33 |
+
- **Modèle** : [Qwen/Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct)
|
| 34 |
+
- **Dataset** : [SetFit/amazon_reviews_multi_fr](https://huggingface.co/datasets/SetFit/amazon_reviews_multi_fr)
|
| 35 |
+
- **Framework UI** : Gradio 4.0+
|
| 36 |
+
- **Librairies** : Transformers, PyTorch, Pandas, Datasets
|
| 37 |
+
|
| 38 |
+
## 🏗️ Architecture du Projet
|
| 39 |
+
|
| 40 |
+
### Structure des fichiers
|
| 41 |
+
|
| 42 |
+
```
|
| 43 |
+
📦 amazon-sentiment-analysis-shirin/
|
| 44 |
+
├── app.py # Interface Gradio principale
|
| 45 |
+
├── data_processing.py # Nettoyage et traitement des données
|
| 46 |
+
├── generate_response.py # Génération de réponses (module Qwen)
|
| 47 |
+
├── build_dataframe.py # Construction du DataFrame complet
|
| 48 |
+
├── main.py # Point d'entrée principal
|
| 49 |
+
├── test_app.py # Tests unitaires
|
| 50 |
+
├── requirements.txt # Dépendances Python
|
| 51 |
+
└── README.md # Documentation
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### Modules
|
| 55 |
+
|
| 56 |
+
#### `data_processing.py`
|
| 57 |
+
Fonctions de nettoyage et traitement :
|
| 58 |
+
- `clean_text()` : Nettoyage des avis (stopwords, ponctuation)
|
| 59 |
+
- `label_to_sentiment()` : Conversion label → sentiment
|
| 60 |
+
- `make_fake_email()` : Génération d'emails factices
|
| 61 |
+
|
| 62 |
+
#### `generate_response.py`
|
| 63 |
+
Génération de réponses automatiques :
|
| 64 |
+
- `load_model()` : Chargement du modèle Qwen
|
| 65 |
+
- `generer_reponse()` : Génération de réponse pour avis négatif
|
| 66 |
+
- `build_reply_prompt()` : Construction du prompt
|
| 67 |
+
|
| 68 |
+
#### `build_dataframe.py`
|
| 69 |
+
Construction du DataFrame final :
|
| 70 |
+
- Chargement dataset Amazon
|
| 71 |
+
- Nettoyage et analyse de sentiment
|
| 72 |
+
- Génération de réponses pour avis négatifs
|
| 73 |
+
- Export CSV
|
| 74 |
+
|
| 75 |
+
#### `app.py`
|
| 76 |
+
Interface utilisateur Gradio :
|
| 77 |
+
- Saisie d'avis client
|
| 78 |
+
- Analyse de sentiment
|
| 79 |
+
- Génération de réponse (si négatif)
|
| 80 |
+
- Affichage des résultats
|
| 81 |
+
|
| 82 |
+
#### `main.py`
|
| 83 |
+
Point d'entrée avec options :
|
| 84 |
+
- `python main.py app` → Lance l'interface Gradio
|
| 85 |
+
- `python main.py build` → Construit le DataFrame
|
| 86 |
+
- `python main.py test` → Lance les tests unitaires
|
| 87 |
+
|
| 88 |
+
#### `test_app.py`
|
| 89 |
+
Tests unitaires :
|
| 90 |
+
- Tests de nettoyage de texte
|
| 91 |
+
- Tests de conversion sentiment
|
| 92 |
+
- Tests de génération d'emails
|
| 93 |
+
|
| 94 |
+
## 🚀 Utilisation
|
| 95 |
+
|
| 96 |
+
### Interface Web
|
| 97 |
+
|
| 98 |
+
1. Entrez un avis client dans le champ de texte
|
| 99 |
+
2. (Optionnel) Sélectionnez manuellement le sentiment
|
| 100 |
+
3. Cliquez sur "🚀 Analyser l'avis"
|
| 101 |
+
4. Consultez les résultats :
|
| 102 |
+
- Texte nettoyé
|
| 103 |
+
- Sentiment détecté
|
| 104 |
+
- Réponse générée (si négatif)
|
| 105 |
+
|
| 106 |
+
### Ligne de commande
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# Lancer l'interface Gradio
|
| 110 |
+
python main.py app
|
| 111 |
+
|
| 112 |
+
# Construire le DataFrame complet
|
| 113 |
+
python main.py build
|
| 114 |
+
|
| 115 |
+
# Lancer les tests
|
| 116 |
+
python main.py test
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### Exemples d'avis à tester
|
| 120 |
+
|
| 121 |
+
**Avis négatif :**
|
| 122 |
+
```
|
| 123 |
+
Le produit est arrivé cassé et le service client ne répond pas. Très déçu !
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
**Avis positif :**
|
| 127 |
+
```
|
| 128 |
+
Excellent produit, livraison rapide et conforme à la description. Je recommande !
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 🔄 CI/CD (Intégration Continue / Déploiement Continu)
|
| 132 |
+
|
| 133 |
+
### Pipeline CI/CD Automatique
|
| 134 |
+
|
| 135 |
+
Ce projet utilise **Hugging Face Spaces** qui intègre nativement un pipeline CI/CD complet :
|
| 136 |
+
|
| 137 |
+
```
|
| 138 |
+
Code modifié → Push → Build Auto (CI) → Deploy Auto (CD) → App en ligne
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
### Processus d'Intégration Continue (CI)
|
| 142 |
+
|
| 143 |
+
À chaque modification du code (push Git ou upload), Hugging Face Spaces exécute automatiquement :
|
| 144 |
+
|
| 145 |
+
1. ✅ **Détection des changements** : Trigger automatique
|
| 146 |
+
2. ✅ **Parsing de requirements.txt** : Identification des dépendances
|
| 147 |
+
3. ✅ **Installation des packages** : pip install automatique
|
| 148 |
+
4. ✅ **Téléchargement du modèle** : Qwen2.5-3B depuis Hugging Face Hub
|
| 149 |
+
5. ✅ **Vérification syntaxique** : Tests Python
|
| 150 |
+
6. ✅ **Build de l'environnement** : Container Docker
|
| 151 |
+
|
| 152 |
+
### Processus de Déploiement Continu (CD)
|
| 153 |
+
|
| 154 |
+
Si le build CI réussit :
|
| 155 |
+
|
| 156 |
+
1. ✅ **Déploiement automatique** : Lancement de l'app Gradio
|
| 157 |
+
2. ✅ **Mise à jour URL** : Application accessible immédiatement
|
| 158 |
+
3. ✅ **Rolling update** : Pas de downtime
|
| 159 |
+
4. ✅ **Monitoring** : Logs disponibles en temps réel
|
| 160 |
+
|
| 161 |
+
### Avantages vs Azure App Service + GitHub Actions
|
| 162 |
+
|
| 163 |
+
| Caractéristique | Azure + GitHub Actions | Hugging Face Spaces |
|
| 164 |
+
|----------------|----------------------|---------------------|
|
| 165 |
+
| **CI/CD automatique** | ✅ Oui (via .yml) | ✅ Oui (natif) |
|
| 166 |
+
| **Configuration** | ⚠️ Fichiers workflow | ✅ Aucune config |
|
| 167 |
+
| **Coût** | ❌ CB requise | ✅ 100% gratuit |
|
| 168 |
+
| **Spécialisation IA** | ⚠️ Généraliste | ✅ Optimisé ML |
|
| 169 |
+
| **Complexité** | ⚠️ Moyenne | ✅ Simple |
|
| 170 |
+
|
| 171 |
+
**Pourquoi pas de dossier `.github/workflows/` ?**
|
| 172 |
+
|
| 173 |
+
Hugging Face Spaces intègre la CI/CD **nativement**, sans nécessiter de fichiers de configuration YAML. Le processus est entièrement automatisé et transparent.
|
| 174 |
+
|
| 175 |
+
## ⚙️ Configuration Technique
|
| 176 |
+
|
| 177 |
+
- **Hardware** : CPU Basic (gratuit)
|
| 178 |
+
- **Modèle** : Chargé en FP32 pour compatibilité CPU
|
| 179 |
+
- **Temps de réponse** : ~10-30 secondes selon la charge
|
| 180 |
+
- **Build initial** : ~10-15 minutes (téléchargement modèle)
|
| 181 |
+
- **Builds suivants** : ~3-5 minutes (modèle en cache)
|
| 182 |
+
|
| 183 |
+
## 🧪 Tests
|
| 184 |
+
|
| 185 |
+
Le projet inclut des tests unitaires dans `test_app.py` :
|
| 186 |
+
|
| 187 |
+
```bash
|
| 188 |
+
# Lancer les tests
|
| 189 |
+
python test_app.py
|
| 190 |
+
|
| 191 |
+
# Ou via main.py
|
| 192 |
+
python main.py test
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
Tests couverts :
|
| 196 |
+
- Nettoyage de texte (ponctuation, chiffres, stopwords)
|
| 197 |
+
- Conversion label → sentiment
|
| 198 |
+
- Génération d'emails factices
|
| 199 |
+
- Construction de prompts
|
| 200 |
+
|
| 201 |
+
## 🎓 Contexte Académique
|
| 202 |
+
|
| 203 |
+
**Projet réalisé par** : Coralie
|
| 204 |
+
**Formation** : Master AI Project Management - Collège de Paris (2024-2026)
|
| 205 |
+
**Alternance** : AI Project Manager - IRFA Formation
|
| 206 |
+
**Sujet** : Pipeline NLP complet avec déploiement CI/CD
|
| 207 |
+
|
| 208 |
+
### Choix Techniques Justifiés
|
| 209 |
+
|
| 210 |
+
**Pourquoi Hugging Face Spaces plutôt qu'Azure ?**
|
| 211 |
+
- ✅ Gratuit et sans carte bancaire
|
| 212 |
+
- ✅ Spécialisé pour les modèles IA
|
| 213 |
+
- ✅ CI/CD native simplifiée
|
| 214 |
+
- ✅ Crédibilité académique reconnue
|
| 215 |
+
- ✅ Cohérence (dataset et modèle déjà sur HF)
|
| 216 |
+
|
| 217 |
+
**Pourquoi Qwen2.5-3B-Instruct ?**
|
| 218 |
+
- ✅ Performances excellentes en français
|
| 219 |
+
- ✅ Taille raisonnable (3B paramètres)
|
| 220 |
+
- ✅ Compatible CPU
|
| 221 |
+
- ✅ Licence open-source
|
| 222 |
+
|
| 223 |
+
## 🔮 Évolutions Possibles
|
| 224 |
+
|
| 225 |
+
- [ ] Migration vers ZeroGPU pour accélération
|
| 226 |
+
- [ ] Fine-tuning du modèle sur avis Amazon FR
|
| 227 |
+
- [ ] Classification multi-classes (1-5 étoiles)
|
| 228 |
+
- [ ] API REST pour intégration externe
|
| 229 |
+
- [ ] Batch processing pour volume élevé
|
| 230 |
+
- [ ] Dashboard de monitoring
|
| 231 |
+
|
| 232 |
+
## 📄 Licence
|
| 233 |
+
|
| 234 |
+
Apache 2.0 - Libre d'utilisation et de modification
|
| 235 |
+
|
| 236 |
+
## 🙏 Remerciements
|
| 237 |
+
|
| 238 |
+
- **Hugging Face** pour l'infrastructure Spaces
|
| 239 |
+
- **Qwen Team** pour le modèle open-source
|
| 240 |
+
- **SetFit** pour le dataset Amazon Reviews
|
| 241 |
+
|
| 242 |
+
---
|
| 243 |
+
|
| 244 |
+
## 📚 Références
|
| 245 |
+
|
| 246 |
+
- [Documentation Gradio](https://www.gradio.app/docs)
|
| 247 |
+
- [Qwen2.5 Model Card](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct)
|
| 248 |
+
- [Hugging Face Spaces Guide](https://huggingface.co/docs/hub/spaces)
|
| 249 |
+
- [SetFit Amazon Reviews](https://huggingface.co/datasets/SetFit/amazon_reviews_multi_fr)
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
**🔗 URL du projet** : https://huggingface.co/spaces/Oxyb50410/amazon-sentiment-analysis-shirin
|
app.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Interface Gradio pour l'analyse de sentiment d'avis Amazon
|
| 3 |
+
et la génération automatique de réponses
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import time
|
| 8 |
+
from data_processing import clean_text
|
| 9 |
+
from generate_response import generer_reponse, load_model
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Préchargement du modèle au démarrage
|
| 13 |
+
print("🔄 Préchargement du modèle...")
|
| 14 |
+
load_model()
|
| 15 |
+
print("✅ Application prête !")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def analyze_review(review_text: str, manual_sentiment: str = None):
|
| 19 |
+
"""
|
| 20 |
+
Analyse un avis client et génère une réponse si négatif
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
review_text (str): Texte de l'avis client
|
| 24 |
+
manual_sentiment (str): Sentiment manuel (optionnel)
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
tuple: (résultat, réponse, texte_nettoyé)
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
if not review_text or review_text.strip() == "":
|
| 31 |
+
return "⚠️ Veuillez entrer un avis client.", "", ""
|
| 32 |
+
|
| 33 |
+
start_time = time.time()
|
| 34 |
+
|
| 35 |
+
# 1. Nettoyage du texte
|
| 36 |
+
texte_clean = clean_text(review_text)
|
| 37 |
+
|
| 38 |
+
# 2. Analyse de sentiment
|
| 39 |
+
if manual_sentiment:
|
| 40 |
+
sentiment = manual_sentiment.lower()
|
| 41 |
+
else:
|
| 42 |
+
# Détection automatique basique par mots-clés
|
| 43 |
+
negative_words = ["nul", "mauvais", "horrible", "déçu", "décevant", "pas", "rien", "cassé"]
|
| 44 |
+
sentiment = "negatif" if any(word in texte_clean for word in negative_words) else "positif"
|
| 45 |
+
|
| 46 |
+
# 3. Génération de réponse (uniquement si négatif)
|
| 47 |
+
if sentiment == "negatif":
|
| 48 |
+
try:
|
| 49 |
+
response = generer_reponse(review_text, max_tokens=100, temperature=0.7)
|
| 50 |
+
response_display = f"📧 **Réponse générée :**\n\n{response}"
|
| 51 |
+
except Exception as e:
|
| 52 |
+
response = f"[Erreur : {e}]"
|
| 53 |
+
response_display = f"❌ Erreur lors de la génération : {e}"
|
| 54 |
+
else:
|
| 55 |
+
response = ""
|
| 56 |
+
response_display = "✅ Avis positif - Aucune réponse nécessaire"
|
| 57 |
+
|
| 58 |
+
elapsed = time.time() - start_time
|
| 59 |
+
|
| 60 |
+
# Résultat formaté
|
| 61 |
+
result = f"""
|
| 62 |
+
### 📊 Analyse terminée en {elapsed:.2f}s
|
| 63 |
+
|
| 64 |
+
**Texte nettoyé :** {texte_clean}
|
| 65 |
+
|
| 66 |
+
**Sentiment détecté :** {"🔴 NÉGATIF" if sentiment == "negatif" else "🟢 POSITIF"}
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
return result, response_display, texte_clean
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# Interface Gradio
|
| 73 |
+
with gr.Blocks(title="Analyse de Sentiment Amazon + Réponses Auto", theme=gr.themes.Soft()) as demo:
|
| 74 |
+
|
| 75 |
+
gr.Markdown(
|
| 76 |
+
"""
|
| 77 |
+
# 🛍️ Analyse de Sentiment d'Avis Amazon
|
| 78 |
+
### Pipeline IA complet : Nettoyage + Sentiment + Génération de réponses
|
| 79 |
+
|
| 80 |
+
**Projet Master IA** - Coralie | Modèle : Qwen2.5-3B-Instruct
|
| 81 |
+
|
| 82 |
+
Ce projet utilise un **pipeline CI/CD automatique** via Hugging Face Spaces.
|
| 83 |
+
"""
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
with gr.Row():
|
| 87 |
+
with gr.Column(scale=1):
|
| 88 |
+
review_input = gr.Textbox(
|
| 89 |
+
label="📝 Avis client Amazon",
|
| 90 |
+
placeholder="Entrez un avis client ici...",
|
| 91 |
+
lines=6
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
sentiment_choice = gr.Radio(
|
| 95 |
+
label="🎯 Sentiment (optionnel - sinon détection auto)",
|
| 96 |
+
choices=["Positif", "Negatif"],
|
| 97 |
+
value=None
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
analyze_btn = gr.Button("🚀 Analyser l'avis", variant="primary")
|
| 101 |
+
|
| 102 |
+
with gr.Column(scale=1):
|
| 103 |
+
result_output = gr.Markdown(label="Résultat de l'analyse")
|
| 104 |
+
response_output = gr.Markdown(label="Réponse générée")
|
| 105 |
+
|
| 106 |
+
with gr.Accordion("📋 Texte nettoyé (debug)", open=False):
|
| 107 |
+
clean_output = gr.Textbox(label="Texte après nettoyage", lines=3)
|
| 108 |
+
|
| 109 |
+
gr.Markdown(
|
| 110 |
+
"""
|
| 111 |
+
---
|
| 112 |
+
### 🔍 Exemples d'avis à tester :
|
| 113 |
+
|
| 114 |
+
**Avis négatif :** "Le produit est arrivé cassé et le service client ne répond pas. Très déçu de cet achat !"
|
| 115 |
+
|
| 116 |
+
**Avis positif :** "Excellent produit, livraison rapide et conforme à la description. Je recommande !"
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
### 🔄 CI/CD Automatique
|
| 121 |
+
|
| 122 |
+
Cette application est déployée via **Hugging Face Spaces** avec CI/CD intégrée :
|
| 123 |
+
- ✅ Build automatique à chaque modification
|
| 124 |
+
- ✅ Tests de syntaxe et dépendances
|
| 125 |
+
- ✅ Déploiement continu si build OK
|
| 126 |
+
"""
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Événement
|
| 130 |
+
analyze_btn.click(
|
| 131 |
+
fn=analyze_review,
|
| 132 |
+
inputs=[review_input, sentiment_choice],
|
| 133 |
+
outputs=[result_output, response_output, clean_output]
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# Lancement
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
demo.launch()
|
build_dataframe.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Construction du DataFrame final pour le projet
|
| 3 |
+
Charge les données Amazon, nettoie, analyse sentiment, génère réponses
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from datasets import load_dataset
|
| 8 |
+
from data_processing import clean_text, label_to_sentiment, make_fake_email
|
| 9 |
+
from generate_response import generer_reponse
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def build_dataframe(n_samples: int = 1000, n_negative_responses: int = 100) -> pd.DataFrame:
|
| 13 |
+
"""
|
| 14 |
+
Construit le DataFrame final avec sentiment et réponses
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
n_samples (int): Nombre d'avis à charger du dataset
|
| 18 |
+
n_negative_responses (int): Nombre d'avis négatifs pour lesquels générer une réponse
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
pd.DataFrame: DataFrame avec colonnes texte_clean, sentiment, response, email_client
|
| 22 |
+
"""
|
| 23 |
+
print(f"📊 Construction du DataFrame avec {n_samples} avis...")
|
| 24 |
+
|
| 25 |
+
# 1. Chargement du dataset
|
| 26 |
+
print("1️⃣ Chargement du dataset Amazon Reviews...")
|
| 27 |
+
dataset = load_dataset("SetFit/amazon_reviews_multi_fr", split=f"train[:{n_samples}]")
|
| 28 |
+
df = pd.DataFrame(dataset)[["text", "label"]].copy()
|
| 29 |
+
df = df.rename(columns={"text": "texte_original"})
|
| 30 |
+
print(f" ✅ {len(df)} avis chargés")
|
| 31 |
+
|
| 32 |
+
# 2. Nettoyage des textes
|
| 33 |
+
print("2️⃣ Nettoyage des textes...")
|
| 34 |
+
df["texte_clean"] = df["texte_original"].apply(clean_text)
|
| 35 |
+
print(" ✅ Textes nettoyés")
|
| 36 |
+
|
| 37 |
+
# 3. Analyse de sentiment
|
| 38 |
+
print("3️⃣ Analyse de sentiment...")
|
| 39 |
+
df["sentiment"] = df["label"].apply(label_to_sentiment)
|
| 40 |
+
n_positif = (df["sentiment"] == "positif").sum()
|
| 41 |
+
n_negatif = (df["sentiment"] == "negatif").sum()
|
| 42 |
+
print(f" ✅ Sentiments : {n_positif} positifs, {n_negatif} négatifs")
|
| 43 |
+
|
| 44 |
+
# 4. Génération de réponses pour avis négatifs
|
| 45 |
+
print(f"4️⃣ Génération de réponses pour {n_negative_responses} avis négatifs...")
|
| 46 |
+
df["response"] = ""
|
| 47 |
+
|
| 48 |
+
neg_df = df[df["sentiment"] == "negatif"].head(n_negative_responses)
|
| 49 |
+
|
| 50 |
+
for idx, row in neg_df.iterrows():
|
| 51 |
+
try:
|
| 52 |
+
review_text = row["texte_original"]
|
| 53 |
+
response = generer_reponse(review_text, max_tokens=80, temperature=0.7)
|
| 54 |
+
df.at[idx, "response"] = response
|
| 55 |
+
|
| 56 |
+
if (idx + 1) % 10 == 0:
|
| 57 |
+
print(f" ... {idx + 1}/{len(neg_df)} réponses générées")
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f" ⚠️ Erreur pour l'avis {idx}: {e}")
|
| 60 |
+
df.at[idx, "response"] = "[Erreur de génération]"
|
| 61 |
+
|
| 62 |
+
print(" ✅ Réponses générées")
|
| 63 |
+
|
| 64 |
+
# 5. Ajout des emails factices
|
| 65 |
+
print("5️⃣ Ajout des emails clients...")
|
| 66 |
+
df["email_client"] = [make_fake_email(i) for i in range(1, len(df) + 1)]
|
| 67 |
+
print(" ✅ Emails ajoutés")
|
| 68 |
+
|
| 69 |
+
# 6. DataFrame final
|
| 70 |
+
df_final = df[["texte_clean", "sentiment", "response", "email_client"]].copy()
|
| 71 |
+
|
| 72 |
+
print(f"\n✅ DataFrame final construit : {len(df_final)} lignes")
|
| 73 |
+
print(f" - Colonnes : {list(df_final.columns)}")
|
| 74 |
+
print(f" - Réponses générées : {(df_final['response'] != '').sum()}")
|
| 75 |
+
|
| 76 |
+
return df_final
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def save_dataframe(df: pd.DataFrame, output_path: str = "dataframe_final.csv"):
|
| 80 |
+
"""
|
| 81 |
+
Sauvegarde le DataFrame en CSV
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
df (pd.DataFrame): DataFrame à sauvegarder
|
| 85 |
+
output_path (str): Chemin du fichier de sortie
|
| 86 |
+
"""
|
| 87 |
+
df.to_csv(output_path, index=False)
|
| 88 |
+
print(f"💾 DataFrame sauvegardé : {output_path}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
print("=" * 80)
|
| 93 |
+
print("🚀 CONSTRUCTION DU DATAFRAME AMAZON SENTIMENT ANALYSIS")
|
| 94 |
+
print("=" * 80)
|
| 95 |
+
|
| 96 |
+
# Construction avec paramètres réduits pour test rapide
|
| 97 |
+
df = build_dataframe(n_samples=100, n_negative_responses=10)
|
| 98 |
+
|
| 99 |
+
# Affichage d'exemples
|
| 100 |
+
print("\n📋 Exemples d'avis négatifs avec réponses :")
|
| 101 |
+
print("-" * 80)
|
| 102 |
+
neg_with_response = df[(df["sentiment"] == "negatif") & (df["response"] != "")]
|
| 103 |
+
for i, row in neg_with_response.head(3).iterrows():
|
| 104 |
+
print(f"\nAvis {i+1}:")
|
| 105 |
+
print(f"Texte nettoyé : {row['texte_clean'][:100]}...")
|
| 106 |
+
print(f"Réponse : {row['response'][:150]}...")
|
| 107 |
+
print(f"Email : {row['email_client']}")
|
| 108 |
+
|
| 109 |
+
# Sauvegarde
|
| 110 |
+
save_dataframe(df, "dataframe_amazon_test.csv")
|
| 111 |
+
|
| 112 |
+
print("\n" + "=" * 80)
|
| 113 |
+
print("✅ TERMINÉ !")
|
| 114 |
+
print("=" * 80)
|
data_processing.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module de traitement des données pour l'analyse de sentiment
|
| 3 |
+
Nettoyage des textes et labellisation
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import string
|
| 8 |
+
|
| 9 |
+
# Liste des stopwords français
|
| 10 |
+
FRENCH_STOPWORDS = {
|
| 11 |
+
"a", "à", "ai", "aie", "aient", "aies", "ait", "alors", "as", "au", "aucun", "aura",
|
| 12 |
+
"aurai", "auraient", "aurais", "aurait", "auve", "avec", "avez", "aviez", "avions",
|
| 13 |
+
"avoir", "avons", "bon", "car", "ce", "cela", "ces", "cet", "cette", "ceux", "chaque",
|
| 14 |
+
"comme", "d", "dans", "de", "des", "du", "elle", "en", "encore", "est", "et", "eu",
|
| 15 |
+
"fait", "faites", "fois", "ici", "il", "ils", "je", "la", "le", "les", "leur", "lui",
|
| 16 |
+
"mais", "me", "mes", "moi", "mon", "ne", "nos", "notre", "nous", "on", "ou", "par",
|
| 17 |
+
"pas", "pour", "plus", "qu", "que", "qui", "sa", "se", "ses", "son", "sur",
|
| 18 |
+
"ta", "te", "tes", "toi", "ton", "toujours", "tout", "tous", "très", "tu",
|
| 19 |
+
"un", "une", "vos", "votre", "vous", "y"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# Table de traduction pour remplacer la ponctuation par des espaces
|
| 23 |
+
PUNCT_TABLE = str.maketrans({c: " " for c in string.punctuation})
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def clean_text(text: str) -> str:
|
| 27 |
+
"""
|
| 28 |
+
Nettoie un texte d'avis client :
|
| 29 |
+
- Conversion en minuscules
|
| 30 |
+
- Suppression de la ponctuation
|
| 31 |
+
- Suppression des chiffres
|
| 32 |
+
- Suppression des stopwords français
|
| 33 |
+
- Normalisation des espaces
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
text (str): Texte brut à nettoyer
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
str: Texte nettoyé
|
| 40 |
+
"""
|
| 41 |
+
if not isinstance(text, str):
|
| 42 |
+
return ""
|
| 43 |
+
|
| 44 |
+
# 1. Minuscules
|
| 45 |
+
text = text.lower()
|
| 46 |
+
|
| 47 |
+
# 2. Suppression de la ponctuation
|
| 48 |
+
text = text.translate(PUNCT_TABLE)
|
| 49 |
+
|
| 50 |
+
# 3. Suppression des chiffres
|
| 51 |
+
text = re.sub(r"\d+", " ", text)
|
| 52 |
+
|
| 53 |
+
# 4. Normalisation des espaces
|
| 54 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 55 |
+
|
| 56 |
+
# 5. Suppression des stopwords
|
| 57 |
+
tokens = [tok for tok in text.split() if tok not in FRENCH_STOPWORDS]
|
| 58 |
+
|
| 59 |
+
return " ".join(tokens)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def label_to_sentiment(label_value: int) -> str:
|
| 63 |
+
"""
|
| 64 |
+
Convertit un label numérique (1-5 étoiles) en sentiment positif/négatif
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
label_value (int): Note de 1 à 5 étoiles
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
str: "positif" si >= 3 étoiles, "negatif" sinon
|
| 71 |
+
"""
|
| 72 |
+
try:
|
| 73 |
+
v = int(label_value)
|
| 74 |
+
except Exception:
|
| 75 |
+
v = 0
|
| 76 |
+
|
| 77 |
+
return "positif" if v >= 3 else "negatif"
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def make_fake_email(index: int) -> str:
|
| 81 |
+
"""
|
| 82 |
+
Génère un email factice pour un client
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
index (int): Numéro du client
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
str: Email au format [email protected]
|
| 89 |
+
"""
|
| 90 |
+
return f"client{index:05d}@example.com"
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
if __name__ == "__main__":
|
| 94 |
+
# Tests
|
| 95 |
+
test_text = "Je suis TRÈS déçu de ce produit ! Il est arrivé cassé et le service client ne répond pas..."
|
| 96 |
+
print(f"Original : {test_text}")
|
| 97 |
+
print(f"Nettoyé : {clean_text(test_text)}")
|
| 98 |
+
|
| 99 |
+
print(f"\nLabel 1 → {label_to_sentiment(1)}")
|
| 100 |
+
print(f"Label 5 → {label_to_sentiment(5)}")
|
| 101 |
+
|
| 102 |
+
print(f"\nEmail test : {make_fake_email(42)}")
|
generate_response.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module de génération de réponses automatiques pour avis négatifs
|
| 3 |
+
Utilise le modèle Qwen2.5-3B-Instruct
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
# Configuration du modèle
|
| 10 |
+
MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct"
|
| 11 |
+
|
| 12 |
+
# Variables globales pour le modèle (chargé une seule fois)
|
| 13 |
+
_tokenizer = None
|
| 14 |
+
_model = None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_model():
|
| 18 |
+
"""
|
| 19 |
+
Charge le modèle Qwen2.5-3B-Instruct
|
| 20 |
+
Appelé une seule fois au démarrage
|
| 21 |
+
"""
|
| 22 |
+
global _tokenizer, _model
|
| 23 |
+
|
| 24 |
+
if _tokenizer is None or _model is None:
|
| 25 |
+
print(f"🔄 Chargement du modèle {MODEL_NAME}...")
|
| 26 |
+
|
| 27 |
+
_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 28 |
+
_model = AutoModelForCausalLM.from_pretrained(
|
| 29 |
+
MODEL_NAME,
|
| 30 |
+
torch_dtype=torch.float32, # CPU compatible
|
| 31 |
+
device_map="cpu"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Configuration du token de padding
|
| 35 |
+
if _tokenizer.pad_token is None:
|
| 36 |
+
_tokenizer.pad_token = _tokenizer.eos_token
|
| 37 |
+
_model.config.pad_token_id = _tokenizer.eos_token_id
|
| 38 |
+
|
| 39 |
+
print("✅ Modèle chargé avec succès !")
|
| 40 |
+
|
| 41 |
+
return _tokenizer, _model
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def build_reply_prompt(review_text: str) -> str:
|
| 45 |
+
"""
|
| 46 |
+
Construit le prompt pour générer une réponse au service client
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
review_text (str): Texte de l'avis client négatif
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
str: Prompt formaté pour le modèle
|
| 53 |
+
"""
|
| 54 |
+
prompt = (
|
| 55 |
+
"Tu es un agent du service client Amazon. "
|
| 56 |
+
"Tu dois répondre en français à un avis client NEGATIF.\n\n"
|
| 57 |
+
"Consignes pour ta réponse :\n"
|
| 58 |
+
"- rester poli et professionnel,\n"
|
| 59 |
+
"- reconnaître le problème,\n"
|
| 60 |
+
"- proposer une solution ou un contact,\n"
|
| 61 |
+
"- utiliser un ton empathique.\n\n"
|
| 62 |
+
"Avis du client :\n"
|
| 63 |
+
f"{review_text}\n\n"
|
| 64 |
+
"Réponse du service client :"
|
| 65 |
+
)
|
| 66 |
+
return prompt
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def generer_reponse(review_text: str, max_tokens: int = 100, temperature: float = 0.7) -> str:
|
| 70 |
+
"""
|
| 71 |
+
Génère une réponse automatique pour un avis négatif
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
review_text (str): Texte de l'avis client négatif
|
| 75 |
+
max_tokens (int): Nombre maximum de tokens à générer
|
| 76 |
+
temperature (float): Température pour la génération (0.0 à 1.0)
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
str: Réponse générée par le modèle
|
| 80 |
+
"""
|
| 81 |
+
# Charger le modèle si nécessaire
|
| 82 |
+
tokenizer, model = load_model()
|
| 83 |
+
|
| 84 |
+
# Construire le prompt
|
| 85 |
+
prompt = build_reply_prompt(review_text)
|
| 86 |
+
|
| 87 |
+
# Tokeniser
|
| 88 |
+
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
|
| 89 |
+
|
| 90 |
+
# Générer la réponse
|
| 91 |
+
with torch.no_grad():
|
| 92 |
+
outputs = model.generate(
|
| 93 |
+
inputs.input_ids,
|
| 94 |
+
max_new_tokens=max_tokens,
|
| 95 |
+
temperature=temperature,
|
| 96 |
+
top_p=0.9,
|
| 97 |
+
do_sample=True,
|
| 98 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 99 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Décoder la réponse complète
|
| 103 |
+
full_response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 104 |
+
|
| 105 |
+
# Extraire uniquement la réponse générée (après le prompt)
|
| 106 |
+
split_token = "Réponse du service client :"
|
| 107 |
+
if split_token in full_response:
|
| 108 |
+
answer = full_response.split(split_token, 1)[1].strip()
|
| 109 |
+
else:
|
| 110 |
+
answer = full_response.strip()
|
| 111 |
+
|
| 112 |
+
return answer
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# Fonction alternative avec nom anglais (pour compatibilité)
|
| 116 |
+
def generate_response(review_text: str, max_tokens: int = 100, temperature: float = 0.7) -> str:
|
| 117 |
+
"""Alias anglais de generer_reponse"""
|
| 118 |
+
return generer_reponse(review_text, max_tokens, temperature)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
if __name__ == "__main__":
|
| 122 |
+
# Test du module
|
| 123 |
+
print("=== TEST DU MODULE generate_response.py ===\n")
|
| 124 |
+
|
| 125 |
+
test_reviews = [
|
| 126 |
+
"Le produit est arrivé cassé et le service client ne répond pas. Très déçu !",
|
| 127 |
+
"Horrible expérience, produit défectueux et remboursement refusé."
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
for i, review in enumerate(test_reviews, 1):
|
| 131 |
+
print(f"Test {i}:")
|
| 132 |
+
print(f"Avis: {review}")
|
| 133 |
+
response = generer_reponse(review, max_tokens=80)
|
| 134 |
+
print(f"Réponse: {response}")
|
| 135 |
+
print("-" * 80)
|
main.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Point d'entrée principal du projet Amazon Sentiment Analysis
|
| 3 |
+
Peut lancer soit l'interface Gradio, soit le build du DataFrame
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import argparse
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def launch_gradio_app():
|
| 11 |
+
"""Lance l'application Gradio"""
|
| 12 |
+
print("🚀 Lancement de l'interface Gradio...")
|
| 13 |
+
from app import demo
|
| 14 |
+
demo.launch()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def build_dataset():
|
| 18 |
+
"""Construit le DataFrame complet"""
|
| 19 |
+
print("📊 Construction du DataFrame...")
|
| 20 |
+
from build_dataframe import build_dataframe, save_dataframe
|
| 21 |
+
|
| 22 |
+
# Paramètres personnalisables
|
| 23 |
+
n_samples = 1000
|
| 24 |
+
n_responses = 100
|
| 25 |
+
|
| 26 |
+
print(f"Configuration : {n_samples} avis, {n_responses} réponses générées")
|
| 27 |
+
|
| 28 |
+
df = build_dataframe(n_samples=n_samples, n_negative_responses=n_responses)
|
| 29 |
+
save_dataframe(df, "dataframe_final_amazon.csv")
|
| 30 |
+
|
| 31 |
+
print("✅ DataFrame construit et sauvegardé !")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def run_tests():
|
| 35 |
+
"""Lance les tests unitaires"""
|
| 36 |
+
print("🧪 Lancement des tests...")
|
| 37 |
+
from test_app import run_all_tests
|
| 38 |
+
|
| 39 |
+
success = run_all_tests()
|
| 40 |
+
if success:
|
| 41 |
+
print("✅ Tests réussis !")
|
| 42 |
+
sys.exit(0)
|
| 43 |
+
else:
|
| 44 |
+
print("❌ Tests échoués")
|
| 45 |
+
sys.exit(1)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def main():
|
| 49 |
+
"""Point d'entrée principal avec options en ligne de commande"""
|
| 50 |
+
parser = argparse.ArgumentParser(
|
| 51 |
+
description="Amazon Sentiment Analysis - Projet Master IA"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"mode",
|
| 56 |
+
choices=["app", "build", "test"],
|
| 57 |
+
help="Mode d'exécution : app (Gradio), build (DataFrame), test (tests)"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
args = parser.parse_args()
|
| 61 |
+
|
| 62 |
+
print("=" * 80)
|
| 63 |
+
print("🛍️ AMAZON SENTIMENT ANALYSIS")
|
| 64 |
+
print("Projet Master AI Project Management - Coralie")
|
| 65 |
+
print("=" * 80)
|
| 66 |
+
print()
|
| 67 |
+
|
| 68 |
+
if args.mode == "app":
|
| 69 |
+
launch_gradio_app()
|
| 70 |
+
elif args.mode == "build":
|
| 71 |
+
build_dataset()
|
| 72 |
+
elif args.mode == "test":
|
| 73 |
+
run_tests()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
# Si appelé sans arguments, lance l'app Gradio par défaut
|
| 78 |
+
if len(sys.argv) == 1:
|
| 79 |
+
print("ℹ️ Aucun argument fourni, lancement de l'app Gradio...")
|
| 80 |
+
print(" Usage: python main.py [app|build|test]")
|
| 81 |
+
print()
|
| 82 |
+
launch_gradio_app()
|
| 83 |
+
else:
|
| 84 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
transformers>=4.35.0
|
| 3 |
+
torch>=2.0.0
|
| 4 |
+
accelerate>=0.24.0
|
| 5 |
+
datasets>=2.14.0
|
| 6 |
+
pandas>=2.0.0
|
test_app.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests unitaires pour le projet Amazon Sentiment Analysis
|
| 3 |
+
Teste les fonctions de traitement et de génération
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Import des modules à tester
|
| 10 |
+
from data_processing import clean_text, label_to_sentiment, make_fake_email
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_clean_text():
|
| 14 |
+
"""Test de la fonction de nettoyage de texte"""
|
| 15 |
+
print("\n=== TEST : clean_text ===")
|
| 16 |
+
|
| 17 |
+
# Test 1 : Texte avec ponctuation
|
| 18 |
+
text1 = "Super produit !!! Je recommande."
|
| 19 |
+
result1 = clean_text(text1)
|
| 20 |
+
assert "super" in result1
|
| 21 |
+
assert "produit" in result1
|
| 22 |
+
assert "!" not in result1
|
| 23 |
+
print(f"✅ Test 1 réussi : '{text1}' → '{result1}'")
|
| 24 |
+
|
| 25 |
+
# Test 2 : Texte avec chiffres
|
| 26 |
+
text2 = "Livraison en 24h, excellent !"
|
| 27 |
+
result2 = clean_text(text2)
|
| 28 |
+
assert "24" not in result2
|
| 29 |
+
print(f"✅ Test 2 réussi : '{text2}' → '{result2}'")
|
| 30 |
+
|
| 31 |
+
# Test 3 : Stopwords supprimés
|
| 32 |
+
text3 = "Je suis très content de cet achat"
|
| 33 |
+
result3 = clean_text(text3)
|
| 34 |
+
assert "je" not in result3 # stopword supprimé
|
| 35 |
+
assert "content" in result3
|
| 36 |
+
print(f"✅ Test 3 réussi : '{text3}' → '{result3}'")
|
| 37 |
+
|
| 38 |
+
print("✅ Tous les tests clean_text réussis !")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_label_to_sentiment():
|
| 42 |
+
"""Test de la conversion label → sentiment"""
|
| 43 |
+
print("\n=== TEST : label_to_sentiment ===")
|
| 44 |
+
|
| 45 |
+
# Test labels négatifs (1-2 étoiles)
|
| 46 |
+
assert label_to_sentiment(1) == "negatif"
|
| 47 |
+
assert label_to_sentiment(2) == "negatif"
|
| 48 |
+
print("✅ Labels 1-2 → négatif")
|
| 49 |
+
|
| 50 |
+
# Test labels positifs (3-5 étoiles)
|
| 51 |
+
assert label_to_sentiment(3) == "positif"
|
| 52 |
+
assert label_to_sentiment(4) == "positif"
|
| 53 |
+
assert label_to_sentiment(5) == "positif"
|
| 54 |
+
print("✅ Labels 3-5 → positif")
|
| 55 |
+
|
| 56 |
+
# Test valeur invalide
|
| 57 |
+
assert label_to_sentiment("invalid") == "negatif" # Par défaut
|
| 58 |
+
print("✅ Valeur invalide gérée")
|
| 59 |
+
|
| 60 |
+
print("✅ Tous les tests label_to_sentiment réussis !")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_make_fake_email():
|
| 64 |
+
"""Test de la génération d'emails factices"""
|
| 65 |
+
print("\n=== TEST : make_fake_email ===")
|
| 66 |
+
|
| 67 |
+
# Test format
|
| 68 |
+
email1 = make_fake_email(1)
|
| 69 |
+
assert email1 == "[email protected]"
|
| 70 |
+
print(f"✅ Email 1 : {email1}")
|
| 71 |
+
|
| 72 |
+
email42 = make_fake_email(42)
|
| 73 |
+
assert email42 == "[email protected]"
|
| 74 |
+
assert "@example.com" in email42
|
| 75 |
+
print(f"✅ Email 42 : {email42}")
|
| 76 |
+
|
| 77 |
+
print("✅ Tous les tests make_fake_email réussis !")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_generer_response_mock():
|
| 81 |
+
"""
|
| 82 |
+
Test simulé de la génération de réponse (sans charger le modèle)
|
| 83 |
+
Vérifie que le prompt est bien construit
|
| 84 |
+
"""
|
| 85 |
+
print("\n=== TEST : build_reply_prompt ===")
|
| 86 |
+
|
| 87 |
+
from generate_response import build_reply_prompt
|
| 88 |
+
|
| 89 |
+
review = "Produit cassé"
|
| 90 |
+
prompt = build_reply_prompt(review)
|
| 91 |
+
|
| 92 |
+
# Vérifications
|
| 93 |
+
assert "service client" in prompt.lower()
|
| 94 |
+
assert "Produit cassé" in prompt
|
| 95 |
+
assert "poli et professionnel" in prompt
|
| 96 |
+
|
| 97 |
+
print("✅ Prompt correctement construit")
|
| 98 |
+
print(f"Extrait : {prompt[:100]}...")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def run_all_tests():
|
| 102 |
+
"""Lance tous les tests"""
|
| 103 |
+
print("=" * 80)
|
| 104 |
+
print("🧪 LANCEMENT DES TESTS UNITAIRES")
|
| 105 |
+
print("=" * 80)
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
test_clean_text()
|
| 109 |
+
test_label_to_sentiment()
|
| 110 |
+
test_make_fake_email()
|
| 111 |
+
test_generer_response_mock()
|
| 112 |
+
|
| 113 |
+
print("\n" + "=" * 80)
|
| 114 |
+
print("🎉 TOUS LES TESTS ONT RÉUSSI !")
|
| 115 |
+
print("=" * 80)
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
except AssertionError as e:
|
| 119 |
+
print("\n" + "=" * 80)
|
| 120 |
+
print(f"❌ ÉCHEC DES TESTS : {e}")
|
| 121 |
+
print("=" * 80)
|
| 122 |
+
return False
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print("\n" + "=" * 80)
|
| 125 |
+
print(f"❌ ERREUR LORS DES TESTS : {e}")
|
| 126 |
+
print("=" * 80)
|
| 127 |
+
return False
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
success = run_all_tests()
|
| 132 |
+
sys.exit(0 if success else 1)
|