wissalouarda6/SYSTEME-DE-DETECTION-D-EMOTIONS-VOCALES
un système capable de détecter 8 émotions à partir de la voix en temps réel
SYSTÈME DE DÉTECTION D'ÉMOTIONS VOCALES
TABLE DES MATIÈRES
- Vue d'ensemble
- Installation
- Architecture du Projet
- Dataset RAVDESS
- Explication Détaillée du Code
- Utilisation
- Résultats
- Limitations
VUE D'ENSEMBLE
Objectif
Développer un système capable de détecter 8 émotions à partir de la voix en temps réel :
- neutral (neutre)
- calm (calme)
- happy (joyeux)
- sad (triste)
- angry (en colère)
- fearful (peureux)
- disgust (dégoût)
- surprised (surpris)
Technologies Utilisées
- Python 3.8+ : Langage de programmation
- PyTorch : Framework de Deep Learning
- Librosa : Extraction de features audio
- Scikit-learn : Prétraitement et validation
- PyAudio : Capture audio en temps réel
- NumPy/Pandas : Manipulation de données
- Matplotlib : Visualisation
INSTALLATION
Étape 1 : Installer les dépendances
pip install torch torchaudio librosa scikit-learn numpy pandas matplotlib seaborn
pip install pyaudio soundfile joblib tqdmÉtape 2 : Télécharger le dataset RAVDESS
- Aller sur Zenodo RAVDESS
- Télécharger "Audio_Speech_Actors_01-24.zip"
- Extraire dans un dossier (ex:
C:\...\Audio_Speech_Actors_01-24)
Étape 3 : Configuration
Modifier le chemin dans config.py :
DATASET_PATH = "C:\\votre\\chemin\\Audio_Speech_Actors_01-24"ARCHITECTURE DU PROJET
projet/
│
├── app.py # Configuration globale
│
├── models/ # Modèles sauvegardés
│ ├── best_emotion_model.pth
│ ├── emotion_scaler.pkl
│ └── label_encoder.pkl
│
└── logs/ # Historiques d'entraînement
├── training_history.pkl
└── training_history.png
DATASET RAVDESS
Qu'est-ce que RAVDESS ?
RAVDESS = Ryerson Audio-Visual Database of Emotional Speech and Song
- Base de données créée par l'Université Ryerson (Canada)
- Contient des enregistrements d'acteurs professionnels
- Standard dans la recherche en reconnaissance d'émotions
Structure du Dataset
- 24 acteurs (12 hommes, 12 femmes)
- 1440 fichiers audio au total
- Chaque acteur prononce 2 phrases avec différentes émotions
- Format : WAV, 48kHz, 16-bit
Nomenclature des Fichiers
Format : 03-01-06-01-02-01-12.wav
03 = modalité (01=vidéo, 02=vidéo+audio, 03=audio seul)
01 = canal vocal (01=speech, 02=song)
06 = émotion (01=neutral, 02=calm, 03=happy, 04=sad, 05=angry, 06=fearful, 07=disgust, 08=surprised)
01 = intensité émotionnelle (01=normal, 02=strong)
02 = phrase (01="Kids are talking by the door", 02="Dogs are sitting by the door")
01 = répétition (01=1ère prise, 02=2ème prise)
12 = acteur (01-24)
Pourquoi RAVDESS ?
✅ Dataset standard et reconnu
✅ Qualité audio professionnelle
✅ Émotions bien différenciées
✅ Équilibré (même nombre par émotion)
✅ Gratuit et accessible
🔧 EXPLICATION DÉTAILLÉE DU CODE
1. CONFIG.PY - Configuration Globale
class Config:
# Mapping des émotions RAVDESS
EMOTION_MAP = {
1: 'neutral', # Code 01 dans le nom de fichier
2: 'calm', # Code 02
3: 'happy', # Code 03
# ... etc
}Rôle : Centraliser tous les paramètres du projet
Pourquoi ? : Facilite les modifications sans toucher au code principal
Paramètres Importants :
SAMPLE_RATE = 16000 # 16kHz (suffisant pour la voix)
DURATION = 3 # 3 secondes par enregistrement
BATCH_SIZE = 32 # Nombre d'échantillons par batch
EPOCHS = 50 # Nombre de passages sur les données
LEARNING_RATE = 0.001 # Vitesse d'apprentissage
NUM_FEATURES = 200 # Nombre de caractéristiques extraites2. DATASET.PY - Extraction de Features Audio
2.1 Classe FeatureExtractor
Qu'est-ce qu'une Feature ?
Une caractéristique numérique qui représente un aspect du son.
Features Extraites (10 types, ~200 valeurs) :
1. MFCCs (Mel-Frequency Cepstral Coefficients) - 80 valeurs
mfccs = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=40)
features.extend(np.mean(mfccs.T, axis=0)) # 40 moyennes
features.extend(np.std(mfccs.T, axis=0)) # 40 écarts-types- Qu'est-ce que c'est ? Représentation compacte du spectre audio
- Pourquoi ? Capture les caractéristiques vocales importantes
- Comment ? Transforme le son en coefficients (comme une "empreinte")
- 40 coefficients : standard pour la voix
- On calcule moyenne + écart-type pour avoir 80 valeurs
2. Chroma Features - 24 valeurs
chroma = librosa.feature.chroma_stft(y=audio, sr=sr)
features.extend(np.mean(chroma.T, axis=0)) # 12 moyennes
features.extend(np.std(chroma.T, axis=0)) # 12 écarts-types- Qu'est-ce que c'est ? Les 12 notes de musique (Do, Do#, Ré...)
- Pourquoi ? Capture la tonalité de la voix
- Utile pour ? Différencier émotions par le ton
3. Mel-Spectrogram - 40 valeurs
mel_spec = librosa.feature.melspectrogram(y=audio, sr=sr)
features.extend(np.mean(mel_spec.T, axis=0)[:20]) # 20 premières moyennes
features.extend(np.std(mel_spec.T, axis=0)[:20]) # 20 premiers écarts-types- Qu'est-ce que c'est ? Représentation fréquence-temps du son
- Pourquoi ? Montre l'évolution des fréquences dans le temps
- Échelle Mel : proche de la perception humaine
4. Spectral Features - 6 valeurs
spectral_centroid = librosa.feature.spectral_centroid(y=audio, sr=sr)
spectral_bandwidth = librosa.feature.spectral_bandwidth(y=audio, sr=sr)
spectral_rolloff = librosa.feature.spectral_rolloff(y=audio, sr=sr)- Centroid : "Centre de gravité" du spectre (voix aiguë/grave)
- Bandwidth : Largeur du spectre (richesse du son)
- Rolloff : Fréquence en dessous de laquelle est 85% de l'énergie
5. Zero Crossing Rate - 2 valeurs
zcr = librosa.feature.zero_crossing_rate(audio)- Qu'est-ce que c'est ? Nombre de fois que le signal change de signe
- Utile pour ? Différencier voix/bruit, déterminer le type de son
6. RMS Energy - 2 valeurs
rms = librosa.feature.rms(y=audio)- Qu'est-ce que c'est ? Énergie moyenne du signal
- Utile pour ? Mesurer le volume et l'intensité
7. Pitch (Hauteur) - 3 valeurs
pitches = librosa.yin(audio, fmin=50, fmax=500)
features.extend([np.mean(pitches), np.std(pitches), np.median(pitches)])- Qu'est-ce que c'est ? Fréquence fondamentale de la voix
- Pourquoi ? La colère = pitch élevé, tristesse = pitch bas
- 50-500 Hz : gamme de la voix humaine
8. Tempo - 1 valeur
tempo, _ = librosa.beat.beat_track(y=audio, sr=sr)- Qu'est-ce que c'est ? Rythme en BPM (battements par minute)
- Utile pour ? Voix rapide (stress) vs lente (calme)
9. Harmonic/Percussive - 2 valeurs
harmonic, percussive = librosa.effects.hpss(audio)
features.extend([np.mean(harmonic), np.mean(percussive)])- Harmonic : Partie mélodique (voyelles)
- Percussive : Partie percussive (consonnes)
10. Spectral Contrast - 7 valeurs
contrast = librosa.feature.spectral_contrast(y=audio, sr=sr)
features.extend(np.mean(contrast.T, axis=0))- Qu'est-ce que c'est ? Différence entre pics et vallées dans le spectre
- Utile pour ? Texture du son, différencier émotions
TOTAL : ~200 features
2.2 Classe RAVDESSDataset
Rôle : Charger et préparer les données RAVDESS
def _load_dataset(self):
"""Charge tous les fichiers RAVDESS"""
for wav_file in self.audio_dir.rglob("*.wav"):
parts = wav_file.stem.split('-') # Sépare le nom par '-'
if len(parts) >= 7:
emotion_code = int(parts[2]) # 3ème position = émotion
intensity_code = int(parts[3]) # 4ème position = intensité
actor_code = int(parts[6]) # 7ème position = acteur
emotion = Config.EMOTION_MAP.get(emotion_code)
# ... stockageProcessus :
- Parcourt tous les fichiers .wav
- Extrait les codes du nom de fichier
- Mappe le code émotion → nom émotion
- Stocke le chemin et les métadonnées
LabelEncoder :
self.label_encoder = LabelEncoder()
self.emotions_encoded = self.label_encoder.fit_transform(self.emotions)- Convertit les labels texte en nombres : 'happy' → 2, 'sad' → 3...
- Nécessaire pour le modèle ML
StandardScaler :
self.scaler = StandardScaler()
self.scaler.fit(all_features)
all_features = self.scaler.transform(all_features)- Pourquoi ? Normalise les features (moyenne=0, écart-type=1)
- Problème : MFCCs entre -50 et 50, RMS entre 0 et 1 → échelles différentes
- Solution : Le scaler met tout sur la même échelle
- Important : On sauvegarde le scaler pour l'utiliser en inférence
3. MODELS.PY - Architectures Neuronales
3.1 EmotionCNN (Architecture Principale)
Qu'est-ce qu'un CNN ?
Convolutional Neural Network = réseau qui apprend des patterns locaux
class EmotionCNN(nn.Module):
def __init__(self, input_size=200, num_classes=8):
super(EmotionCNN, self).__init__()
# PARTIE 1 : COUCHES CONVOLUTIONNELLES
self.conv_layers = nn.Sequential(
# Bloc 1
nn.Conv1d(1, 64, kernel_size=5, padding=2),
nn.BatchNorm1d(64),
nn.ReLU(),
nn.MaxPool1d(2),
nn.Dropout(0.2),
# ... 2 autres blocs similaires
)
# PARTIE 2 : COUCHES FULLY CONNECTED
self.fc_layers = nn.Sequential(
nn.Linear(256, 512),
# ... classification
)Décomposition Étape par Étape :
1. Input : (batch, 1, 200)
- batch = nombre d'échantillons (ex: 32)
- 1 = un seul canal (comme une image en noir et blanc)
- 200 = nos 200 features
2. Premier Bloc Convolutionnel
nn.Conv1d(1, 64, kernel_size=5, padding=2)- Conv1d : convolution 1D (sur la séquence de features)
- 1 → 64 : transforme 1 canal en 64 "filtres" différents
- kernel_size=5 : chaque filtre regarde 5 features à la fois
- padding=2 : ajoute des zéros aux bords pour garder la taille
- Output : (batch, 64, 200)
nn.BatchNorm1d(64)- BatchNorm : normalise les activations
- Pourquoi ? Stabilise l'entraînement, accélère la convergence
nn.ReLU()- ReLU : fonction d'activation, met les valeurs négatives à 0
- Formule : f(x) = max(0, x)
- Pourquoi ? Introduit de la non-linéarité
nn.MaxPool1d(2)- MaxPooling : garde le maximum tous les 2 éléments
- Effect : réduit la taille de 200 → 100
- Pourquoi ? Réduit les calculs, rend le modèle plus robuste
- Output : (batch, 64, 100)
nn.Dropout(0.2)- Dropout : désactive aléatoirement 20% des neurones
- Pourquoi ? Évite le surapprentissage (overfitting)
3. Deuxième Bloc : 64 → 128 canaux, taille 100 → 50
4. Troisième Bloc : 128 → 256 canaux
nn.AdaptiveAvgPool1d(1)- AdaptiveAvgPool : moyenne tout pour avoir taille 1
- Output : (batch, 256, 1) → après squeeze → (batch, 256)
5. Couches Fully Connected
nn.Linear(256, 512) # 256 entrées → 512 neurones
nn.BatchNorm1d(512)
nn.ReLU()
nn.Dropout(0.4)
nn.Linear(512, 256) # 512 → 256
nn.BatchNorm1d(256)
nn.ReLU()
nn.Dropout(0.3)
nn.Linear(256, 8) # 256 → 8 classes (nos émotions)6. Forward Pass (Passage Avant)
def forward(self, x):
x = x.unsqueeze(1) # (batch, 200) → (batch, 1, 200)
x = self.conv_layers(x) # → (batch, 256, 1)
x = x.squeeze(-1) # → (batch, 256)
x = self.fc_layers(x) # → (batch, 8)
return xPourquoi cette Architecture ?
- Les Conv apprennent des patterns locaux dans les features
- Le MaxPool réduit la dimension progressivement
- Les FC combinent tout pour la classification finale
- Le Dropout évite l'overfitting
3.2 EmotionLSTM (Architecture Alternative)
Qu'est-ce qu'un LSTM ?
Long Short-Term Memory = réseau qui traite des séquences
self.lstm = nn.LSTM(
input_size=input_size, # 200 features
hidden_size=256, # 256 neurones cachés
num_layers=2, # 2 couches LSTM empilées
batch_first=True,
bidirectional=True, # Lit dans les 2 sens
dropout=0.3
)Bidirectionnel :
- Lit la séquence de gauche à droite ET de droite à gauche
- Capture le contexte avant ET après chaque feature
- Output : (batch, seq, 512) car 256*2 directions
Mécanisme d'Attention :
self.attention = nn.Sequential(
nn.Linear(512, 128),
nn.Tanh(),
nn.Linear(128, 1)
)- Apprend à donner plus d'importance à certaines parties
- Calcule un "score" pour chaque position de la séquence
- Fait une moyenne pondérée
Pourquoi LSTM ?
- Bon pour les dépendances temporelles
- Peut oublier/retenir des informations sélectivement
- Bidirectionnel = contexte complet
4. TRAIN.PY - Entraînement du Modèle
4.1 Classe Trainer
Initialisation :
self.criterion = nn.CrossEntropyLoss()- CrossEntropyLoss : fonction de perte pour classification multi-classes
- Mesure l'écart entre prédiction et vérité
self.optimizer = optim.AdamW(
model.parameters(),
lr=0.001, # Learning rate
weight_decay=1e-4 # Régularisation L2
)- AdamW : optimiseur (version améliorée d'Adam)
- Ajuste les poids du modèle pour minimiser l'erreur
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
self.optimizer,
mode='min', # Surveille la loss
factor=0.5, # Réduit lr de moitié
patience=5 # Après 5 epochs sans amélioration
)- Learning Rate Scheduler : réduit le lr si pas d'amélioration
- Permet de "raffiner" l'apprentissage
Epoch d'Entraînement :
def train_epoch(self, train_loader):
self.model.train() # Mode entraînement
for features, labels in train_loader:
# 1. Charger sur GPU si disponible
features = features.to(self.device)
labels = labels.squeeze().to(self.device)
# 2. Réinitialiser gradients
self.optimizer.zero_grad()
# 3. Forward pass
outputs = self.model(features)
# 4. Calculer la loss
loss = self.criterion(outputs, labels)
# 5. Backward pass (calcul gradients)
loss.backward()
# 6. Gradient clipping (éviter explosions)
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
# 7. Mise à jour des poids
self.optimizer.step()Validation :
def validate(self, val_loader):
self.model.eval() # Mode évaluation
with torch.no_grad(): # Pas de calcul de gradients
for features, labels in val_loader:
outputs = self.model(features)
loss = self.criterion(outputs, labels)
# Calcul accuracySauvegarde du Meilleur Modèle :
if val_acc > self.best_val_acc:
self.best_val_acc = val_acc
torch.save({
'epoch': epoch,
'model_state_dict': self.model.state_dict(),
'optimizer_state_dict': self.optimizer.state_dict(),
'val_acc': val_acc,
}, Config.MODEL_PATH)5. INFERENCE.PY - Détection Temps Réel
5.1 Classe RealTimeDetector
Chargement du Modèle :
self.model = EmotionCNN()
checkpoint = torch.load(Config.MODEL_PATH, map_location=self.device)
self.model.load_state_dict(checkpoint['model_state_dict'])
self.model.eval()Chargement Scaler et Encoder :
self.scaler = joblib.load(Config.SCALER_PATH)
self.label_encoder = joblib.load(Config.ENCODER_PATH)- Important : Utiliser le MÊME scaler que l'entraînement
- Sinon les features ne sont pas sur la même échelle
Enregistrement Audio :
def record_audio(self):
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paInt16, # 16-bit
channels=1, # Mono
rate=16000, # 16kHz
input=True,
frames_per_buffer=2048
)Prédiction :
def predict(self, audio):
# 1. Padding/trimming à 3 secondes
if len(audio) > Config.SAMPLES_PER_TRACK:
audio = audio[:Config.SAMPLES_PER_TRACK]
else:
padding = Config.SAMPLES_PER_TRACK - len(audio)
audio = np.pad(audio, (0, padding))
# 2. Extraire features (comme dans training)
features = FeatureExtractor.extract(audio)
# 3. Normaliser (avec le scaler de training)
features_scaled = self.scaler.transform(features.reshape(1, -1))
# 4. Convertir en tensor
features_tensor = torch.FloatTensor(features_scaled).to(self.device)
# 5. Prédiction
with torch.no_grad():
outputs = self.model(features_tensor)
probs = torch.softmax(outputs, dim=1) # Convertir en probabilités
confidence, prediction = torch.max(probs, 1)
# 6. Décoder l'émotion
emotion = self.label_encoder.inverse_transform([prediction.item()])[0]
return {
'emotion': emotion,
'confidence': confidence.item() * 100,
'probabilities': {...}
}Lissage Temporel :
self.history = deque(maxlen=20) # Garde les 20 dernières détections
self.history.append(result['emotion'])- Évite les changements brusques
- Moyenne sur plusieurs prédictions pour stabiliser
🚀 UTILISATION
1. Premier Lancement : Entraîner le Modèle
python main.py
# Choisir option [1]Processus :
- Charge RAVDESS (1440 fichiers)
- Extrait features de chaque fichier (~2-3 min)
- Split train/test 80/20
- Entraîne 50 epochs (~10-15 min sur CPU, 2-3 min sur GPU)
- Sauvegarde le meilleur modèle
Fichiers Créés :
models/best_emotion_model.pth: modèle entraînémodels/emotion_scaler.pkl: scaler pour normalisationmodels/label_encoder.pkl: encodeur labelslogs/training_history.pkl: historique entraînementlogs/training_history.png: courbes loss/accuracy
2. Détection Temps Réel
python main.py
# Choisir option [2]Processus :
- Charge le modèle entraîné
- Ouvre le micro
- Enregistre 3 secondes
- Extrait features
- Prédit l'émotion
- Affiche + répète
Arrêt : Ctrl+C
3. Test sur Fichier Audio
python main.py
# Choisir option [3]- Sélectionne un fichier .wav ou .mp3
- Analyse et affiche l'émotion détectée
📊 RÉSULTATS
Performance Attendue
- Accuracy Train : 90-95%
- Accuracy Validation : 80-87%
- Temps Inférence : ~100-200ms par prédiction
Matrice de Confusion Typique
pred_happy pred_sad pred_angry ...
happy 85% 5% 3%
sad 4% 82% 6%
angry 2% 3% 88%
...
Facteurs de Performance
✅ Bonne Performance :
- Émotions fortes (angry, happy, sad)
- Audio clair et net
- Locuteur proche du dataset
❌ Performance Réduite :
- Émotions subtiles (calm, neutral)
- Bruit de fond important
- Accents très différents
- Voix d'enfants (dataset = adultes)
⚠️ LIMITATIONS
1. Dataset
- Seulement 24 acteurs → généralisation limitée
- Texte limité (2 phrases)
- Émotions "jouées" (pas naturelles)
- Pas de dialectes/accents variés
2. Technique
- Nécessite audio de 3 secondes minimum
- Sensible au bruit de fond
- Une seule langue (anglais)
- Pas de détection multi-émotions
3. Utilisation
- Nécessite microphone de qualité
- Temps de traitement ~200ms (pas instantané)
- Consomme CPU/GPU en continu
🔧 PARAMÈTRES À AJUSTER
Pour Améliorer la Précision
# Dans config.py
EPOCHS = 100 # Plus d'epochs
BATCH_SIZE = 16 # Batch plus petit
LEARNING_RATE = 0.0005 # LR plus faible
# Dans models.py
# Ajouter plus de couches, plus de neuronesPour Accélérer l'Inférence
# Réduire la complexité du modèle
# Utiliser moins de features
# Réduire DURATION à 2 secondesPour Réduire l'Overfitting
# Augmenter Dropout
nn.Dropout(0.5) # Au lieu de 0.3
# Augmenter weight_decay
weight_decay=1e-3📚 RÉFÉRENCES
Dataset
- RAVDESS on Zenodo
- Livingstone SR, Russo FA (2018) The Ryerson Audio-Visual Database of Emotional Speech and Song (RAVDESS)
Bibliothèques
💡 AMÉLIORATIONS POSSIBLES
- Augmentation de données : ajouter du bruit, changer le pitch, etc.
- Ensemblage : combiner CNN + LSTM
- Transfer Learning : utiliser Wav2Vec pré-entraîné
- Multi-langue : entraîner sur datasets multilingues
- Détection continue : analyse par fenêtres glissantes
- Calibration : adapter au locuteur spécifique
Auteur : ouhassine wissal ouarda
Date : Janvier 2026