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