Speech Emotion Recognition (SER) avec le dataset RAVDESS

S

L’objectif de cet article est de construire un modèle pour réaliser une détection des émotions à partir de la voix (SER – Speech Emotion Recognition) afin d’analyser des segments audio spécifiques, comme par exemple un discours politique de Donald Trump.
La première étape consiste à concevoir et entraîner un modèle pour réaliser cette analyse émotionnelle.

A noter que ce modèle est basé sur un jeu de données constitué d’orateurs anglophones prononçant des phrases en anglais, ce qui le rend spécifiquement adapté à la prédiction des émotions dans des discours en anglais.

 

Cette article constitue (aussi) une exploration au sein d’un projet plus vaste intégrant une approche multimodale (Étude conjointe/synchronisée des indices vocaux, textuels et visuels) en sciences humaines et sociales (SHS).

L’intégralité du script se trouve ICI.

Pour ce faire, j’utiliserai le dataset RAVDESS.
Bien que des alternatives telles que IEMOCAP soient souvent considérées comme mieux structurées, l’accès à ce jeu de données nécessite un mail « universitaire » pour recevoir le DataSet.
Une autre option, le dataset TESS, est également intéressant mais limité à des dialogues prononcés exclusivement par des femmes, ce qui le rend moins généraliste.
Le choix du dataset RAVDESS, bien qu’imparfait, répond au besoin pédagogique de l’article, de plus, de nombreux scripts exploitent déjà ce dataset (Scripts qui m’ont servi de source d’inspiration).

Le script est conçu pour être exécuté à partir de Google Colab.
L’intégration avec Google Drive permet à l’utilisateur de sauvegarder le modèle entraîné pour une utilisation ultérieure (à développer dans un prochain article). Pour gérer efficacement le grand nombre de paramètres (239 912 paramètres), l’utilisation du service payant (10 € / 100 unités) de Google Colab avec un GPU (T4 par exemple) est recommandée.

L’entraînement du modèle sur le jeu de données aboutit à une précision de 64,55 %, ce qui, après avoir consulté divers scripts/articles sur Kaggle, semble être un résultat « standard » pour ce DataSet, d’autant plus que j’utilise une version incomplète des données.
Il est important de noter que certains scripts sur Kaggle affichent des précisions irréalistes allant jusqu’à 98 %, ce qui suscite des doutes sur leur crédibilité (sur-entrainement).

Un tel résultat, bien qu’insuffisant pour des applications concrètes, met en lumière les limites du modèle testé.
Cette  performance (mauvaise/moyenne (?)) a été validée avec un exemple pratique : un test sur un court discours de Donald Trump.
Ce travail, bien que perfectible, s’inscrit dans une démarche pédagogique visant à explorer les hyper-paramètres d’un réseau neuronal convolutionnel (CNN) et à se familiariser avec les étapes incontournables de l’état de l’art dans la création d’un modèle.

 

Comprendre le fonctionnement des CNN

Le modèle des réseaux de neurones convolutifs (CNN) a été introduit par Yann LeCun dans un article révolutionnaire de 1998 (« Gradient-based learning applied to document recognition« . Selon Google Scholar, cet article a été cité plus de 71 000 fois), où il a démontré l’efficacité pour la reconnaissance des chiffres manuscrits avec le dataset MNIST.
Ce travail a posé les bases des avancées modernes en vision par ordinateur, en établissant les principes fondamentaux de la convolution, du pooling et des réseaux multicouches.

Un réseau de neurones convolutif (CNN) est une architecture utilisée principalement pour l’analyse d’images et de données structurées en 1D, 2D ou 3D.
Dans le contexte de l’analyse des émotions à partir de la voix, nous utilisons les CNN pour traiter des spectrogrammes audio. Un spectrogramme est une représentation des signaux sonores, convertissant les caractéristiques temporelles et fréquentielles en données intelligible par un modèle.

Dans notre problème, l’utilisation de CNN, et plus précisément la convolution permet de capturer des motifs présents dans les données, tels que des contours dans une image ou des variations de fréquence dans un spectrogramme.

La convolution en machine learning agit comme un tamis, filtrant les données pour extraire les caractéristiques importantes. Chaque filtre détecte des motifs spécifiques, allant des détails simples aux structures complexes, à travers plusieurs couches. Cette approche hiérarchique réduit la quantité de données traitées.

 

Imaginez une image (ou un spectrogramme audio) comme une grande grille de nombres représentant les intensités des pixels ou des amplitudes sonores.
La convolution consiste à appliquer un filtre (aussi appelé noyau ou kernel), qui est une matrice de petite taille (par exemple, 3×3 ou 5×5), sur cette grille pour détecter des motifs spécifiques, comme des lignes ou des textures.

Le filtre « glisse » sur toute la grille (l’image ou le spectrogramme), en effectuant une multiplication élément par élément entre les valeurs du filtre et les valeurs de la grille à cet emplacement, puis en additionnant les résultats.
Le résultat est une nouvelle grille, appelée carte de caractéristiques (feature map), qui contient des informations sur les motifs détectés.

 

Le DataSet « RAVDESS »

Ryerson Audio-Visual Database of Emotional Speech and Song (RAVDESS) contient un total de 7 356 fichiers.
« In total, the RAVDESS collection includes 7356 files (2880+2024+1440+1012 files », ces fichiers sont séparés en deux grands groupe (l’un avec des elocution, l’autre avec des chants). Nous utiliserons uniquement le DataSet que l’on trouve sur Kaggle et qui contient en réalité 1440 fichiers.

Le DataSet regroupe les enregistrements de 24 acteurs professionnels (12 femmes et 12 hommes) qui prononcent une phrase en ANGLAIS  selon 8 émotions : neutre, calme, joyeux, triste, en colère, apeuré, surpris, dégoûté. Chaque émotion est enregistrée deux fois : une version avec une intensité normale et une version avec une intensité forte (excepté pour l’émotion « neutre » qui n’a pas de version forte).
Les acteurs prononcent chaque phrase deux fois : une fois avec une voix normale et une autre fois avec une voix plus intense.

Les noms de fichiers suivent la structure suivante :

Modalité : 01 = audio-visuel complet, 02 = vidéo uniquement, 03 = audio uniquement. La mention de ces modalités dans la documentation officielle indique que la version complète du dataset inclut ou a inclus des fichiers vidéo (que vous pouvez télécharger ici).
Cependant, le Dataset utilisé à partir de Kaggle comprend que les fichiers « audio-only ». (« Video files are provided as separate zip downloads for each actor (01-24, ~500 MB each), and are split into separate speech and song downloads« )
Canal vocal : 01 = discours, 02 = chant (Il existe un jeu DataSet Ravdess spécialisé dans la reconnaissance des émotions à travers le chant/musique)
Émotion : 01 = neutre, 02 = calm, 03 = happy, 04 = sad, 05 = angry, 06 = fear, 07 = disgust, 08 = surprise.
Intensité émotionnelle : 01 = normale, 02 = forte. (Remarque : il n’y a pas d’intensité forte pour l’émotion « neutre ».)
Déclaration (phrase dite)01 = « Kids are talking by the door », 02 = « Dogs are sitting by the door ».
Répétition01 = 1ère répétition, 02 = 2ème répétition.
Acteur01 à 24. (Les nombres impairs correspondent aux hommes, et les nombres pairs aux femmes.)

Cette organisation permet d’accéder facilement aux fichiers en fonction des critères d’analyse recherchés. Exemple : 03-01-03-01-02-02-01.wav :

  1. Modalité : 03 → Le fichier est audio.
  2. Canal vocal : 01 → Discours (parlé).
  3. Émotion : 03 → Joyeux.
  4. Intensité émotionnelle : 01 → Normale.
  5. Phrase dite : 02« Kids are talking by the door ».
  6. Répétition : 02 → 2ème répétition.
  7. Acteur : 01 → Homme (numéro impair).

Préparation des données et chargement du dataset

Vous devrez au préalable télécharger le dataset Ravdess depuis Kaggle et le placer dans un répertoire de votre Google drive.

### 2. Preparation des données

# Définir le chemin des données
RAV = "/content/drive/MyDrive/DataSet/Ravdess/data/audio_speech_actors_01-24/"  # Modifiez avec votre chemin

# Vérifiez que le répertoire existe
if not os.path.exists(RAV):
    raise FileNotFoundError(f"Le répertoire {RAV} n'existe pas. Vérifiez le chemin.")

Le script commence par définir le chemin du dataset et charge les fichiers audio en extrayant les métadonnées utiles comme le genre de l’orateur et l’émotion exprimée.

# Créer un DataFrame
RAV_df = pd.DataFrame({
    'emotion': emotion,
    'gender': gender,
    'path': path
})


# Mapper les émotions
emotion_map = {
    1: 'neutral', 2: 'calm', 3: 'happy', 4: 'sad',
    5: 'angry', 6: 'fear', 7: 'disgust', 8: 'surprise'
}
RAV_df['emotion'] = RAV_df['emotion'].map(emotion_map)

# Ajouter des colonnes supplémentaires
RAV_df['labels'] = RAV_df['gender'] + '_' + RAV_df['emotion']
RAV_df['source'] = 'RAVDESS'

# Afficher les résultats
print("Données chargées :")
display(RAV_df.head())
display(RAV_df.describe())
print("Répartition des labels :")
print(RAV_df.labels.value_counts())

 

Représentation des données à partir d’un exemple

Pour chaque émotion vous aurez un exemple de représentation des données avec la « waveform » et le « spectrogram ».

 

Prétraitement et augmentation des données

Les données audio brutes sont transformées pour inclure des versions augmentées.

Pourquoi effectuer une augmentation des données ?

L’augmentation des données est essentielle pour enrichir artificiellement un dataset « limité », réduisant ainsi le risque de sur-apprentissage. Cela améliore la robustesse du modèle en simulant des variations réalistes du signal audio.
Par exemple, le Noise Injection ajoute du bruit pour rendre le modèle tolérant aux environnements sonores complexes.
Le « Time Stretching » modifie la vitesse de l’audio, simulant des différences de débit vocal.
Le « Time Shifting » décale le signal temporellement, entraînant le modèle à se concentrer sur des caractéristiques clés indépendantes de leur position.

 

# Fonctions d'augmentation des données
def noise(data):
    noise_amp = 0.035 * np.random.uniform() * np.amax(data)
    return data + noise_amp * np.random.normal(size=data.shape[0])

def stretch(data, rate=0.8):
    return librosa.effects.time_stretch(data, rate=rate)

def shift(data):
    shift_range = int(np.random.uniform(low=-5, high=5) * 1000)
    return np.roll(data, shift_range)

def pitch(data, sampling_rate, pitch_factor=0.7):
    return librosa.effects.pitch_shift(data, sr=sampling_rate, n_steps=pitch_factor)
Enfin, la « Pitch Modification » ajuste la hauteur des sons pour refléter la diversité vocale.
Ces transformations permettent d’enrichir le dataset avec des exemples variés, augmentant ainsi sa taille effective sans nécessiter de nouvelles données.

 

Extraction des caractéristiques

Pour entrainer le modèle CNN avec les données audio, il faut (avant l’entrainement) extraire des caractéristiques représentatives. Ces caractéristiques décrivent les propriétés du signal sonore.

# Fonction pour extraire des caractéristiques spécifiques
def extract_features(data, sample_rate):
    result = np.array([])

    # ZCR
    zcr = np.mean(librosa.feature.zero_crossing_rate(y=data).T, axis=0)
    result = np.hstack((result, zcr))

    # Chroma STFT
    stft = np.abs(librosa.stft(data))
    chroma_stft = np.mean(librosa.feature.chroma_stft(S=stft, sr=sample_rate).T, axis=0)
    result = np.hstack((result, chroma_stft))

    # MFCC
    mfcc = np.mean(librosa.feature.mfcc(y=data, sr=sample_rate).T, axis=0)
    result = np.hstack((result, mfcc))

    # RMS
    rms = np.mean(librosa.feature.rms(y=data).T, axis=0)
    result = np.hstack((result, rms))

    # Mel Spectrogram
    mel = np.mean(librosa.feature.melspectrogram(y=data, sr=sample_rate).T, axis=0)
    result = np.hstack((result, mel))

    return result
  • ZCR (Zero Crossing Rate) : Mesure la fréquence des changements de signe dans le signal, utile pour détecter des sons agités liés à des émotions comme la colère ou la peur.
  • Chroma STFT : Analyse les composantes tonales du signal pour capturer les variations harmoniques liées à l’émotion.
  • MFCC : Représente les caractéristiques spectrales perçues par l’humain, pour différencier les timbres associés aux émotions.
  • RMS (Root Mean Square) : Évalue l’énergie ou l’intensité du signal, reflétant la force d’une émotion comme la colère ou la surprise.
  • Mel Spectrogram : Offre une représentation temps-fréquence alignée avec la perception auditive humaine, mettant en évidence les variations distinctives des émotions dans le spectre sonore.

À ce stade du projet, je choisis de ne pas m’attarder sur une explication approfondie des caractéristiques extraites des données audio. Je tiens toutefois à préciser que je me suis basé sur ces cinq caractéristiques largement reconnues et souvent utilisées dans les modèles CNN pour entraîner des systèmes de reconnaissance des émotions à partir de la voix (SER).
J’admets que cette approche repose sur une confiance implicite envers les travaux des développeurs précédents, ce qui n’est pas idéal sur le plan méthodologique…

Construction et entrainement du modèle

Le modèle est entraîné avec Conv1D car le signal audio est une donnée temporelle unidimensionnelle, composée d’une série de points représentant l’amplitude du son au fil du temps. Conv1D (keras.layers.Conv1D(filters=128, kernel_size=5, strides=1, activation=’relu’)) analyse ce type de données en faisant glisser des filtres (filters=128) le long de l’axe temporel avec une taille définie (kernel_size=5) et un pas spécifique (strides=1).

Le modèle est défini comme une séquence (Sequential) où les couches sont ajoutées linéairement.

### 5. Construction du Modèle

# construction
model = Sequential()

Chaque couche convolutive Conv1D utilise un filtre pour extraire des motifs temporels dans les données d’entrée.
Le kernel_size détermine la taille de ces filtres, tandis que les strides définissent leur déplacement (le pas).

model.add(Conv1D(256, kernel_size=5, strides=1, padding='same', activation='relu', input_shape=(x_train.shape[1], 1)))
model.add(Conv1D(128, kernel_size=5, strides=1, padding='same', activation='relu'))

Les couches MaxPooling1D réduisent la dimensionnalité en conservant les informations importantes, tout en limitant le surapprentissage.

model.add(MaxPooling1D(pool_size=5, strides=2, padding='same'))

La couche Flatten transforme les sorties multidimensionnelles en un vecteur unidimensionnel pour permettre leur traitement par des couches denses.

model.add(Flatten())

La couche dense Dense(64) combine les caractéristiques extraites et la couche finale Dense(units=y_train.shape[1]) génère les probabilités des classes avec softmax.

model.add(Dense(units=64, activation='relu'))
model.add(Dense(units=y_train.shape[1], activation='softmax'))

Enfin, le modèle est compilé avec l’optimiseur adam, la fonction de perte categorical_crossentropy, et l’évaluation basée sur accuracy.

model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

Je ne maîtrise pas encore tous les paramètres nécessaires à la configuration d’un modèle, et pour avancer, je me suis appuyé sur des exemples de code trouvés sur Kaggle ainsi que sur les enseignements de l’ouvrage d’Aurélien Géron, Deep Learning avec Keras et TensorFlow – Mise en oeuvre et cas concrets (3ème édition), Eidtion O’Reilly.
J’avoue que, pour l’instant, je subis un peu les complexités de ce domaine en plein apprentissage 😉

 

Hyper-paramètres et optimisation du modèle

Les hyperparamètres jouent un rôle important dans l’entraînement du modèle.
Par exemple, le nombre d’épochs (ici : epochs=100) correspond au nombre de fois où le modèle parcourt l’ensemble des données d’entraînement.
Un nombre élevé peut améliorer l’apprentissage mais augmenter le risque de sur-apprentissage.

Fonction pour arrêter l’apprentissage automatiquement

Dans ce script, une fonction pourrait être implémentée en ajoutant le callback EarlyStopping, qui arrête l’entraînement lorsque le modèle n’améliore plus sa performance

from tensorflow.keras.callbacks import EarlyStopping

# Callback EarlyStopping
early_stopping = EarlyStopping(
    monitor='val_loss',   # Surveille la perte de validation
    patience=10,          # Nombre d'époques sans amélioration avant arrêt
    restore_best_weights=True  # Restaure les poids du meilleur modèle
)

# Entraînement avec EarlyStopping
history = model.fit(
    x_train,
    y_train,
    batch_size=32,
    epochs=100,
    validation_data=(x_test, y_test),
    callbacks=[rlrp, early_stopping]  # Ajout du callback EarlyStopping
)

 

Résultats et analyse des performances

Avec un taux de précision de 64,55 %, on remarque le modèle ne progresse plus après 58 épochs.

Matrice de confusion

Cette matrice de confusion compare les prédictions du modèle (colonnes) aux véritables étiquettes émotionnelles (lignes).
Chaque cellule indique combien de fois une émotion « vraie » a été classée comme une autre émotion.
Par exemple, pour l’émotion « angry », 85 prédictions sont correctes (diagonale, colonne « angry »), mais 10 cas ont été incorrectement classés comme « happy ».
Une autre confusion notable apparaît pour « neutral », où 13 exemples ont été mal prévus et attribué à « calm ». Cela peut indiquer que le modèle confond ces deux émotions en raison de caractéristiques acoustiques proches.
Il faudrait tester l’entrainement d’un nouveau modèle en mappant/regroupant ces deux émotions : « neutre » et « calm ».

 

Le script sur GoogleColab

Nous arrivons au terme de cet article dense et technique, où chaque étape de la construction et de l’entraînement du modèle a été détaillée. Une fois le modèle entraîné, il sera sauvegardé dans votre Google Drive, prêt à être utilisé pour des analyses concrètes.

C’est à ce moment que l’essentiel commence : mettre ce modèle à l’épreuve des faits en l’appliquant à des segments audio réels, pour en évaluer les performances avec des cas pratiques.

L’intégralité du script se trouve ICI.

L’apprentissage théorique laisse ainsi place à l’expérience terrain !

A propos de l'auteur

Stéphane Meurisse

1 Commentaire

  • […] même, vous devez spécifier le chemin vers votre modèle. Cela fait naturellement référence à l’article sur la construction du modèle. Cependant, si vous préférez éviter une « prise de neurones » 😉 lors de la création […]

Stéphane Meurisse