Travaux pratiques - Reconnaissance du style musical

Pour cette séance de travaux pratiques, nous allons nous appuyer sur la bibliothèque torchaudio. Cette bibliothèque logicielle, écrite en Python, est une extension du framework d’apprentissage profond PyTorch. Vous pourrez trouver la documentation de torchaudio à l’adresse : https://pytorch.org/audio/stable/index.html.

L’objectif de cette séance est de reconnaître automatiquement le style d’une musique. Pour ce faire, nous allons nous appuyer sur le jeu de données GTZAN Genre Collection. Il s’agit d’une collection de pistes musicales de 30 secondes chacune. Dans le cas du TP, nous commencerons par nous intéresser à un sous-ensemble MiniGenres, qui ne contient que 50 exemples répartis dans 5 styles différents : jazz, classique, rock, pop et métal.

Manipulation d’un fichier audio avec torchaudio

Dans un premier temps, voyons comment manipuler un fichier son et le charger en mémoire à l’aide de torchaudio. La bibliothèque utilise une couche d’abstraction permettant de manipuler différents formats audio sans avoir à se préoccuper de la façon dont les données sont encodées.

# Décommentez la ligne suivante pour installer tous les paquets (utile
# pour Google Colab ou votre installation locale, inutile sur le
# JupyterHub du Cnam) :

#!pip install torch torchaudio scikit-learn tqdm numpy matplotlib requests
# si vous êtes sous Windows:
#!pip install soundfile
# Import des modules utiles
import matplotlib.pyplot as plt
import numpy as np
import torch
import torchaudio
import requests
torchaudio.set_audio_backend("sox_io")
# si vous êtes sous Windows: torchaudio.set_audio_backend("soundfile")

from tqdm.notebook import trange, tqdm
# Fixe l'aléatoire pour la reproductibilité
np.random.seed(0)
torch.manual_seed(0)

Le code suivant télécharge un exemple de fichier son (le sifflet d’une locomative à vapeur). Ce fichier est au format .wav.

Charger ce fichier en mémoire avec torchaudio est simple : il suffit d’utiliser la fonction torchaudio.load() et de lui passer le chemin vers le fichier sur le disque.

url = "https://pytorch.org/tutorials/_static/img/steam-train-whistle-daniel_simon-converted-from-mp3.wav"
r = requests.get(url)

with open('steam-train-whistle-daniel_simon-converted-from-mp3.wav', 'wb') as f:
    f.write(r.content)

filename = "steam-train-whistle-daniel_simon-converted-from-mp3.wav"
waveform, sample_rate = torchaudio.load(filename)

La fonction load() renvoie un tuple à deux éléments :

  • waveform contient le tableau NumPy qui représente la forme d’onde du signal audio.

  • sample_rate contient la fréquence d’échantillonnage du signal.

# uniquement sous IPython ou Jupyter !
import IPython
IPython.display.Audio("steam-train-whistle-daniel_simon-converted-from-mp3.wav")

Question

Quelles sont les dimensions du tableau waveform ? Pourquoi ?

Correction

waveform est de dimensions \((2 \times 276858)\). La première valeur correspond au fait qu’il s’agit d’un fichier stéréo (à 2 canaux). La seconde valeur correspond à la durée de la séquence multipliée par la fréquence d’échantillonnage.

La forme d’onde étant représentée par un tableau NumPy, cela simplifie grandement les calculs et sa manipulation. Par exemple, en utilisant matplotlib, nous pouvons tracer les formes d’onde correspondant aux deux canaux (gauche et droite) :

plt.figure()
plt.title(f"Forme d'onde du sifflet (fréquence d'échantillonnage : {sample_rate}Hz)")
plt.plot(waveform.t().numpy())
plt.show()

Transformations sur la donnée audio

torchaudio implémente différentes fonctions classiques sur les sons. Ces opérations sont définies dans le sous-module transforms.

from torchaudio import transforms

Question

En vous basant sur la documentation du sous-module transforms et la transformation Resample(), rééchantillonner le signal ci-dessus à 1000 Hz. Vérifiez le résultat en affichant la longueur de la forme d’onde avant et après rééchantillonnage.

Correction

print(f"Avant rééchantillonnage, la forme d'onde est une séquence de {len(waveform[0])} éléments.")
r_waveform = transforms.Resample(sample_rate, 1000)(waveform)
plt.plot(r_waveform.t().numpy())
plt.title("Sifflet rééchantillonné à 1000Hz")
plt.show()
print(f"Après rééchantillonnage, la forme d'onde est une séquence de {len(r_waveform[0])} éléments.")

Spectrogramme

Par défaut, torchaudio manipule les données sonores comme des formes d’onde. Mais il est bien sûr possible et facile de convertir ces signaux en spectogramme (en réalité, en sonagramme) en utilisant torchaudio.transforms.Spectrogram().

spectrogram = transforms.Spectrogram()
specgram = spectrogram(waveform[0])
plt.figure(figsize=(16, 8))
plt.imshow(specgram.log2().numpy(), cmap='gray') and plt.show()

Ce spectrogramme a pour dimensions \(N \times F = 201 \times 1385\). Le nombre de fréquences \(F\) est déterminé par le nombre d’intervalles de fréquences considérés (n_fft // 2 + 1, avec le paramètre n_fft = 400 par défaut). La largeur temporelle \(N\) dépend de la taille de la fenêtre de la transformée de Fourier (ici, win_length = 400) et du pas de la fenêtre glissante (hop_length, qui par défaut vaut la moitié de win_length (soit 200). On a donc un spectre calculé tous les 200 pas de temps, soit \(N = 276858 // 200 = 1385\).

Question

Quelle est l’utilité de .log2() dans le code ci-dessus ?

Correction

Le passage au logarithme permet d’afficher l’intensité sonore dans une échelle en décibels, généralement plus lisible.

Coefficients MFCC

Les coefficients MFCC sont calculables très facilement à l’aide de torchaudio. Les paramètres importants de la transformation MFCC sont les suivants:

  • sample_rate, la fréquence d’échantillonnage,

  • n_mfcc, le nombre de coefficients à conserver (par défaut, 40).

Vous pouvez consulter les autres paramètres dans la documentation.

mfcc = transforms.MFCC(sample_rate, n_mfcc=20)

Une fois la transformée créée, elle s’utilise comme une fonction sur la forme d’onde :

coeff = mfcc(waveform[0])

Le résultat est un tenseur PyTorch. Sa première dimension correspond à l’indice du coefficient et la seconde à sa position dans le temps.

coeff.size()

Classification des genres musicaux

Pour cette séance, nous allons nous intéresser à la classification des genres musicaux. Nous allons travailler avec un sous-ensemble du jeu de données GTZAN Genre Collection.

Notre jeu de données jouet, intitulé mini-genres comporte 50 pistes de 30 secondes chacune réparties dans 5 catégories musicales (jazz, métal, rock, pop et classique, 10 exemples par classe).

Les deux commandes suivantes téléchargent et décompressent le jeu de données dans le répertoire mini-genres/ :

# à exécuter dans un terminal
wget https://cedric.cnam.fr/vertigo/Cours/RCP217/data/mini-genres.tar.gz
tar xvzf mini-genres.tar.gz

En voici un exemple :

IPython.display.Audio("mini-genres/jazz/jazz.00000.au.ogg")

Le code qui suit définit un objet Dataset de PyTorch permettant de charger ces données en mémoire à l’aide de torchaudio. Vous n’avez pas besoin de comprendre en détails le fonctionnement de cette classe pour la suite du TP, elle est donnée pour votre information. Notez seulement que l’on conserve pour chaque séquence audio les 660 000 premiers échantillons (ce qui correspond aux 30 premières secondes).

from torch.utils import data
from sklearn.preprocessing import LabelEncoder
import os


class MiniGenresDataset(data.Dataset):
    """
        Cette classe implémente un jeu de données audio au format PyTorch.

        On suppose que le dossier contenant le jeu de données possède la
        structure suivante:
        dataset
        ├── classe1
        │   ├── classe1_fichier1.wav
        │   ├── classe1_fichier2.wav
        │   └── ...
        ├── classe2
        │   ├── classe2_fichier1.wav
        │   └── ...
        ├── ...
        │   ├── ...
        │   └── ...
        └── classeN
            ├── classeN_fichier1.wav
            ├── ...
            └── classeN_fichierK.wav

        La détection des fichiers répartis dans les N classes est automatique.
    """
    def __init__(self, path="./mini-genres/", sample_rate=22050):
        """
            Créé un objet MiniGenresDataset

            Arguments
            ---------
            - path (str) : chemin vers le dossier contenant le jeu de données
                           (par défaut: ./mini-genres)
            - sample_rate (int) : fréquence d'échantillonnage (par défaut: 22050 Hz)
        """
        super(MiniGenresDataset).__init__()
        filenames, labels = [], []
        if not os.path.isdir(path):
            raise ValueError(f"Le répertoire {path} est incorrect.")
        for dirname, _, files in os.walk(path):
            if dirname == path:
                continue
            filenames += sorted([os.path.join(dirname, f) for f in files])
            labels += [os.path.basename(dirname)] * len(files)
        self.label_encoder_ = LabelEncoder()
        self.filenames = filenames
        self.labels = self.label_encoder_.fit_transform(labels)
        self.classes = self.label_encoder_.classes_
        self.sample_rate = sample_rate

    def __len__(self):
        """ Renvoie la longueur du jeu de données (nombre d'échantillons sonores) """
        return len(self.filenames)

    def __getitem__(self, idx):
        """ Accède à un élément du jeu de données (son + étiquette) """
        filename, label = self.filenames[idx], self.labels[idx]
        audio, sample_rate = torchaudio.load(filename)
        return audio[:, :660000], label

La classe MiniGenresDataset hérite de la classe torch.utils.data.Dataset. En pratique, un objet de cette classe se parcourt comme n’importe quelle liste. C’est la méthode __getitem__(i) définit comment chaque paire \((X_i, y_i)\) est extraite du jeu de données.

ds = MiniGenresDataset()
classes = ds.label_encoder_.classes_
idx = 48
audio, label = ds[idx]
print(f"Échantillon n°{idx} : X de dimensions {tuple(audio.size())}, y = {label} ({classes[label]})")

Question

Visualiser le sonagramme du premier extrait sonore du jeu de données ds.

Correction

audio, label = ds[0]
plt.plot(spectrogram(waveform[0]).numpy()) and plt.show()

Catégorisation à l’aide des MFCC

Dans un premier temps, nous allons réaliser la classification en utilisant les coefficients cepstraux de Mel (ou MFCC). Plus précisément, nous allons caractériser un extrait sonore par la moyenne et l’écart type de ses MFCC.

L’avantage de cette représentation est qu’elle ne dépend pas de la durée de la séquence sonore. Les caractéristiques audio ainsi obtenues peuvent ensuite être utilisées dans n’importe quel modèle décisionnel classique (SVM, forêts aléatoires, etc.).

La fonction torch.utils.data.random_split permet de diviser un jeu de données PyTorch (un Dataset) en deux ensembles : un jeu d’apprentissage et un jeu de test.

Question

En vous basant sur la documentation de PyTorch, utilisez la fonction random_split pour gérer deux jeux de données train_ds et test_ds contenant chacun 50% du jeu de données ds.

Correction

ds = MiniGenresDataset()
train_ds, test_ds = data.random_split(ds, [len(ds) // 2, len(ds) // 2])

La fonction extract_mfcc définie ci-dessous permet d’extraire les coefficients MFCC à partir d’une forme d’onde en utilisant torchaudio :

def extract_mfcc(waveform, sample_rate, n_mfcc=2):
    """
        Extraie les coefficients MFCC d'une forme d'onde et renvoie
        un vecteur contenant leur moyenne et leur écart-type pour
        chaque bande de fréquence.

        Arguments
        ---------
        - waveform (torch.Tensor): formes d'ondes représentées par un tenseur (k, N)
                                   où k est le nombre de pistes et N le nombre d'échantillons
        - sample_rate (int): fréquence d'échantillonnage
        - n_mfcc (int): nombre de coefficients de Mel à conserver (défaut: 40)

        Renvoie
        -------
        - features (np.array): vecteur de longueur n_mfcc contenant
                               les coefficients moyennées sur le segment audio.

    """
    mfcc = transforms.MFCC(sample_rate, n_mfcc=n_mfcc, melkwargs={"n_mels": 64})
    coeffs = mfcc(waveform[0]).numpy()
    features = np.mean(coeffs, axis=1)
    return features

Pour effectuer la classification à partir des MFCC, nous allons convertir ces jeux de données en matrices d’observation au format NumPy.

Question

Écrire une fonction to_mfcc_dataset(dataset) qui convertit le jeu de données contenant des paires (forme d’onde, étiquette) en paires (coefficients MFCC, étiquette) en utilisant la fonction extract_mfcc. Le résultat doit être un tableau NumPy.

Correction

def to_mfcc_dataset(dataset, n_mfcc=40):
    X, y = [], []
    for audio, label in dataset:
        X.append(extract_mfcc(audio, sample_rate=ds.sample_rate, n_mfcc=n_mfcc))
        y.append(label)
    X, y = np.array(X), np.array(y)
    return X, y

Nous pouvons maintenant convertir nos jeux de données et produire nos ensembles \((X, y)\) d’apprentissage et d’évaluation.

X_train, y_train = to_mfcc_dataset(train_ds)
X_test, y_test = to_mfcc_dataset(test_ds)

Il est possible d’examiner les caractéristiques que nous venons d’extraire.

X_train[0]

Question

À l’aide de scikit-learn, entraîner et évaluer une SVM linéaire sur mini-genres. On prendra soin d’utiliser une validation croisée pour la recherche du meilleur hyperparamètre de régularisation \(C\), par exemple en utilisant sklearn.model_selection.GridSearchCV. Voir à cette les pages de la documentation de sklearn concernant les SVM et la recherche par grille.

Correction

from sklearn import svm
from sklearn import model_selection

clf = model_selection.GridSearchCV(svm.SVC(),
                                   {'C': [1e-4, 1e-3, 1e-2, 0.1, 1, 10, 100, 100]},
                                   cv=3)
clf.fit(X_train, y_train)
clf.score(X_test, y_test)

Réseaux convolutifs unidimensionnels

Comme nous l’avons vu en cours, les réseaux convolutifs unidimensionnels sont particulièrement adaptés comme modèles décisionnels sur des données audio.

Voyons comment définir un tel modèle à l’aide de PyTorch. Les couches neuronales sont définies dans le sous-module nn.

Pour cette partie, nous allons construire un modèle simple (4 couches seulement) qui prendra en entrée la séquence de coefficients MFCC. Nous conserverons 40 coefficients, c’est-à-dire que l’entrée du modèle sera de dimension \(40\times660000\).

import torch.nn as nn
import torch.optim as optim

Question

Implémenter un CNN 1D en utilisant l’interface Sequential de PyTorch. Vous pouvez vous inspirer de l’exemple proposé dans la documentation du module torch.nn. Vous aurez besoin des couches Conv1d, ReLU, AdaptiveAvgPool1d, Flatten et Linear.

Le réseau doit posséder 3 couches convolutives ayant respectivement :

  • 40 canaux d’entrée, 80 filtres, un noyau \(15\times15\) et une stride de 4,

  • 160 filtres, un noyau \(7\times7\) et une stride de 2,

  • 320 filtres, un noyau \(7\times7\) et une stride de 2.

Chaque couche est suivie d’une activation ReLU. La dernière couche est suivie d’un échantillonnage adaptatif et d’une couche entièrement connectée qui transforme les 320 en un vecteur de probabilité de longueur 5 (pour les 5 classes).

Correction

num_classes = len(ds.label_encoder_.classes_)

def get_cnn1d_model(n_classes):
    cnn1d_model = nn.Sequential(
        nn.Conv1d(in_channels=40, out_channels=80, kernel_size=15, stride=4),
        nn.ReLU(),
        nn.Conv1d(in_channels=80, out_channels=160, kernel_size=7, stride=2),
        nn.ReLU(),
        nn.Conv1d(in_channels=160, out_channels=320, kernel_size=7, stride=2),
        nn.ReLU(),
        nn.AdaptiveAvgPool1d(output_size=1),
        nn.Flatten(),
        nn.Linear(in_features=320, out_features=n_classes)
    )
    return cnn1d_model

Comme nous avons déjà découpé les données, nous pouvons maintenant créer l’objet DataLoader. Cet objet PyTorch définit comment les données seront données au modèle, c’est-à-dire:

  • batch_size définit la taille du batch,

  • shuffle=True indique qu’il faut mélanger les données à chaque époque.

train_data = data.DataLoader(train_ds, batch_size=16, shuffle=True)

Nous pouvons désormais écrire la boucle d’apprentissage :

# 30 époques suffisent pour notre problème
epochs = 30
# On construit le modèle
cnn1d_model = get_cnn1d_model(len(ds.classes))
# Erreur de classification = entropie croisée
criterion = nn.CrossEntropyLoss()
# Optimisation : descente de gradient avec moment
optimizer = optim.SGD(cnn1d_model.parameters(), lr=0.0005, momentum=0.9)
# On définit la transformation MFCC adaptée
mfcc = transforms.MFCC(sample_rate=ds.sample_rate, melkwargs={"n_mels": 64})

for e in trange(1, epochs+1):
    # On met le modèle en mode `train`
    cnn1d_model = cnn1d_model.train()
    average = 0.

    for data_, labels in train_data: # charge les données
        optimizer.zero_grad() # réinitialise les gradients à 0
        data_ = mfcc(data_).squeeze(dim=1) # calcul des MFCC
        output = cnn1d_model(data_) # calcul de sprédictions
        loss = criterion(output, labels) # calcul de la loss
        average += loss.item() # calcul de l'erreur moyenne
        loss.backward() # rétroprogation
        optimizer.step() # descente de gradient
    tqdm.write(f"Erreur à l'epoch {e} = {average/len(train_data)}")

Question

En s’inspirant de la boucle d’apprentissage ci-dessus, écrire une fonction score qui prend en entrée un Dataset et calcule l’accuracy. On pourra notamment utiliser la fonction accuracy de scikit-learn. Utiliser cette fonction score pour calculer :

  • le taux de bonnes prédictions sur le jeu d’apprentissage,

  • le taux de bonnes prédictions sur le jeu de test.

Que constatez-vous ?

Correction

from sklearn.metrics import accuracy_score, confusion_matrix

def score(model, dataset, with_matrix=False):
    model = model.eval()

    results = []
    labels = []
    for batch, l in dataset:
        with torch.no_grad():
            data_ = mfcc(batch).squeeze(dim=1)
            output = model(data_)
            prediction = torch.argmax(output).item()
        results.append(prediction)
        labels.append(l)
    if with_matrix:
        plt.matshow(confusion_matrix(results, labels)) and plt.show()
    accuracy = accuracy_score(results, labels)
    return accuracy

 print(score(cnn1d_model, train_ds))
 print(score(cnn1d_model, test_ds))

On constate que le modèle surapprend (100% de bonnes prédictions en apprentissage mais pas en test).

Question

À l’aide des fonctions sklearn.metrics.confusion_matrix et de la fonction plt.matshow() de matplotlib, modifier la fonction score précédemment créée pour qu’elle affiche la matrice de confusion. Afficher la matrice de confusion du CNN 1D entraîné sur le Mel-spectrogram sur le jeu de test.

Correction

print(score(cnn1d_model, train_ds))
print(score(cnn1d_model, test_ds))

Pour aller plus loin : le jeu de données complet

Cette partie est optionnelle mais permet d’aller plus en détails. Il est préférable de réaliser cette section sur une machine dotée d’une carte graphique (par exemple, via Google Colab ou sur votre PC personnel s’il est doté d’un GPU NVIDIA).

L’objectif de cette section est de comparer les performances de notre CNN simple sur le jeu de données GTZAN complet avec celui d’un CNN temporel appliqué sur la forme d’onde.

Pour commencer, il nous faut télécharger le jeu de données complet. La version sans compression pèse presque 2Go, par conséquent nous utilisons une version alternative dans laquelle les fichiers audio ont été compressés au format Ogg Vorbis (avec perte, mais nettement plus légers). L’archive pèse environ 200Mo.

# à exécuter dans un terminal
wget https://cedric.cnam.fr/vertigo/Cours/RCP217/data/genres.tar.gz
tar xvzf genres.tar.gz

La même classe MiniGenresDataset que précédemment gère aussi le jeu de données complet. À nouveau, nous pouvons diviser le jeu de données en deux ensemble d’apprentissage et de test :

ds = MiniGenresDataset(path='./genres/')
train_ds, test_ds = data.random_split(ds, [len(ds) // 2, len(ds) // 2])

Nous réécrivons ci-dessous des versions améliorées des fonctions train et score introduites plus tôt.

def score(model, dataset, bs=16, with_matrix=False, use_mfcc=False, device="cpu"):
    model = model.eval().to(device)

    results = []
    labels = []
    loader = data.DataLoader(dataset, batch_size=bs)
    for batch, l in tqdm(loader):
        with torch.no_grad():
            if use_mfcc:
                batch = mfcc(batch).squeeze(dim=1)
            batch = batch.to(device)
            output = model(batch)
            prediction = torch.argmax(output, dim=1)
        results.append(prediction.to("cpu").numpy())
        labels.append(l.numpy())
    results = np.concatenate(results)
    labels = np.concatenate(labels)
    if with_matrix:
        plt.matshow(confusion_matrix(results, labels)) and plt.show()
    accuracy = accuracy_score(results, labels)
    return accuracy
def train(model, train_data, epochs=50, use_mfcc=False, bs=32, device="cpu"):
    criterion = nn.CrossEntropyLoss()
    model = model.to(device)
    optimizer = optim.Adam(model.parameters())

    mfcc = transforms.MFCC(sample_rate=ds.sample_rate, melkwargs={"n_mels": 64})
    train_data = data.DataLoader(train_ds, batch_size=bs, shuffle=True, num_workers=4)

    for e in trange(1, epochs+1):
        cnn1d_model = model.train()
        average = 0.
        for data_, labels in train_data:
            optimizer.zero_grad() # reset gradients
            if use_mfcc:
                data_ = mfcc(data_).squeeze(dim=1)
            data_, labels = data_.to(device), labels.to(device)
            output = model(data_)
            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()
            average += loss.to("cpu").item()
        tqdm.write(f"Erreur à l'époque {e} = {average/len(train_data)}")

Question

À quoi sert l’argument optionnel use_mfcc ? À quoi sert l’argument optionel device ?

Correction

Le paramètre use_mfcc permet de réaliser l’apprentissage sur les coefficients MFCC extraits du spectrogramme plutôt que sur la forme d’onde. Le paramètre device permet de spécifier si l’apprentissage se fait sur le processeur (« cpu ») ou sur la carte graphique (« cuda »).

Question

En utilisant les fonctions ci-dessus, entraîner et évaluer le modèle CNN 1D sur les MFCC sur le jeu de données Genres complet.

Correction

Entraînement :

cnn1d_model = get_cnn1d_model(len(ds.classes))
# utilise le GPU si possible
device = "cuda" if torch.cuda.is_available else "cpu"
train(cnn1d_model, train_data, epochs=50, use_mfcc=True, device=device)

Évaluation :

score(cnn1d_model, test_ds, with_matrix=True, use_mfcc=True, device=device)

Ce score nous donne les performances du modèle spectral, c’est-à-dire qui travaille sur les coefficients MFCC issus du spectogramme. Nous allons ensuite les comparer à un modèle qui travaille sur la forme d’onde dans son intégralité.

CNN temporel : l’exemple de SampleCNN

Pour cette partie, nous allons travailler avec SampleCNN. Cette architecture travaille sur la forme d’onde et est décrite dans cet article de Lee et al..

Question

À partir de la Table 1 de l’article SampleCNN, implémenter cette architecture en PyTorch. Il s’agit d’un CNN temporel sur la forme d’onde, l’entrée est donc de dimensions \((1\times660000)\) car nos données audio sont mono (un seul canal).

Notez que chaque couche convolutive est suivie d’une activation non-linéaire ReLU et d’une couche de BatchNormalization1d.

Comme nos fichiers audio sont plus longs que ceux de l’article, il sera nécessaire d’ajouter une couche d”AdaptivePooling1d à la fin du modèle.

Entraîner SampleCNN sur le jeu d’apprentissage à l’aide de la fonction train définie ci-dessus et l’évaluer sur le jeu de test.

Correction

Définition du modèle :

def get_temporal_cnn(num_classes):
   net = nn.Sequential(
       nn.Conv1d(1, 128, kernel_size=3, stride=3),
       nn.BatchNorm1d(128),
       nn.ReLU(),
       nn.Conv1d(128, 128, kernel_size=3, stride=3),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(128),
       nn.ReLU(),
       nn.Conv1d(128, 256, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(256),
       nn.ReLU(),
       nn.Conv1d(256, 256, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(256),
       nn.ReLU(),
       nn.Conv1d(256, 256, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(256),
       nn.ReLU(),
       nn.Conv1d(256, 256, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(256),
       nn.ReLU(),
       nn.Conv1d(256, 256, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(256),
       nn.ReLU(),
       nn.Conv1d(256, 512, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(512),
       nn.ReLU(),
       nn.Conv1d(512, 512, kernel_size=3, stride=1),
       nn.MaxPool1d(kernel_size=3, stride=3),
       nn.BatchNorm1d(512),
       nn.ReLU(),
       nn.Conv1d(512, 512, kernel_size=1),
       nn.BatchNorm1d(512),
       nn.AdaptiveAvgPool1d(output_size=1),
       nn.Flatten(),
       nn.Linear(in_features=512, out_features=num_classes)
   )
   return net

Entraînement (cela prend une dizaine de minutes sur GPU) :

device = "cuda" if torch.cuda.is_available else "cpu"
train(net, train_ds, epochs=50, bs=8, use_mfcc=False, device=device)

Évaluation :

score(net, test_ds, use_mfcc=False, with_matrix=True, device=device)

Les deux modèles obtiennent des performances comparables. Cependant, SampleCNN est nettement plus lent (le modèle est plus complexe et doit travailler sur des données d’entrée beaucoup plus grandes).