Diffusion pour l'audio
Dans ce notebook, nous allons jeter un bref coup dâĆil Ă la gĂ©nĂ©ration dâaudio avec des modĂšles de diffusion. Ce que vous allez apprendre :
- Comment lâaudio est reprĂ©sentĂ© dans un ordinateur
- Les méthodes de conversion entre les données audio brutes et les spectrogrammes
- Comment prĂ©parer un chargeur de donnĂ©es avec une fonction personnalisĂ©e pour convertir des tranches dâaudio en spectrogrammes
- Finetuner un modÚle de diffusion audio existant sur un genre de musique spécifique
- TĂ©lĂ©charger votre pipeline personnalisĂ© sur le Hub dâHugging Face
Mise en garde : il sâagit principalement dâun objectif pĂ©dagogique - rien ne garantit que notre modĂšle sonnera bien đ
Commençons !
Configuration et importations
# !pip install -q datasets diffusers torchaudio accelerate
import torch, random
import numpy as np
import torch.nn.functional as F
from tqdm.auto import tqdm
from IPython.display import Audio
from matplotlib import pyplot as plt
from diffusers import DiffusionPipeline
from torchaudio import transforms as AT
from torchvision import transforms as IT
Echantillonnage Ă partir dâun pipeline audio prĂ©-entraĂźnĂ©
Commençons par suivre la documentation pour charger un modÚle de diffusion audio préexistant :
# Chargement d'un pipeline de diffusion audio pré-entraßné
device = "cuda" if torch.cuda.is_available() else "cpu"
pipe = DiffusionPipeline.from_pretrained("teticio/audio-diffusion-instrumental-hiphop-256").to(device)
Comme pour les pipelines que nous avons utilisés dans les unités précédentes, nous pouvons créer des échantillons en appelant le pipeline comme suit :
# Ăchantillonner Ă partir du pipeline et afficher les rĂ©sultats
output = pipe()
display(output.images[0])
display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate()))
Ici, lâargument rate spĂ©cifie la frĂ©quence dâĂ©chantillonnage de lâaudio ; nous y reviendrons plus tard. Vous remarquerez Ă©galement que le pipeline renvoie plusieurs choses. Que se passe-t-il ici ? Examinons de plus prĂšs les deux sorties.
La premiĂšre est un tableau de donnĂ©es, reprĂ©sentant lâaudio gĂ©nĂ©rĂ© :
# Le tableau audio :
output.audios[0].shape
(1, 130560)
La seconde ressemble Ă une image en niveaux de gris :
# L'image de sortie (spectrogramme)
output.images[0].size
(256, 256)
Cela nous donne un aperçu du fonctionnement de ce pipeline. Lâaudio nâest pas directement gĂ©nĂ©rĂ© par diffusion. Au lieu de cela, le pipeline a le mĂȘme type dâUNet 2D que les pipelines de gĂ©nĂ©ration dâimages inconditionnelles que nous avons vus dans lâunitĂ© 1, qui est utilisĂ© pour gĂ©nĂ©rer le spectrogramme, qui est ensuite post-traitĂ© dans lâaudio final.
Le pipeline possÚde un composant supplémentaire qui gÚre ces conversions, auquel nous pouvons accéder via pipe.mel :
pipe.mel
Mel {
"_class_name": "Mel",
"_diffusers_version": "0.12.0.dev0",
"hop_length": 512,
"n_fft": 2048,
"n_iter": 32,
"sample_rate": 22050,
"top_db": 80,
"x_res": 256,
"y_res": 256
}
De lâaudio Ă lâimage et inversement
Une « forme dâonde » encode les Ă©chantillons audio bruts dans le temps. Il peut sâagir du signal Ă©lectrique reçu dâun microphone, par exemple. Travailler avec cette reprĂ©sentation du « domaine temporel » peut sâavĂ©rer dĂ©licat, câest pourquoi il est courant de la convertir sous une autre forme, communĂ©ment appelĂ©e spectrogramme. Un spectrogramme montre lâintensitĂ© de diffĂ©rentes frĂ©quences (axe y) en fonction du temps (axe x) :
# Calculer et afficher un spectrogramme pour notre échantillon audio généré en utilisant torchaudio
spec_transform = AT.Spectrogram(power=2)
spectrogram = spec_transform(torch.tensor(output.audios[0]))
print(spectrogram.min(), spectrogram.max())
log_spectrogram = spectrogram.log()
plt.imshow(log_spectrogram[0], cmap='gray');
tensor(0.) tensor(6.0842)
Le spectrogramme que nous venons de crĂ©er contient des valeurs comprises entre 0,0000000000001 et 1, la plupart dâentre elles Ă©tant proches de la limite infĂ©rieure de cette plage. Ce nâest pas lâidĂ©al pour la visualisation ou la modĂ©lisation. En fait, nous avons dĂ» prendre le logarithme de ces valeurs pour obtenir un tracĂ© en niveaux de gris qui montre des dĂ©tails. Pour cette raison, nous utilisons gĂ©nĂ©ralement un type spĂ©cial de spectrogramme appelĂ© Mel spectrogramme, qui est conçu pour capturer les types dâinformations qui sont importantes pour lâaudition humaine en appliquant certaines transformations aux diffĂ©rentes composantes de frĂ©quence du signal.
Quelques transformations audio de la documentation torchaudio
Heureusement pour nous, nous nâavons pas besoin de nous prĂ©occuper de ces transformations, la fonctionnalitĂ© mel du pipeline sâoccupe de ces dĂ©tails pour nous. En lâutilisant, nous pouvons convertir une image de spectrogramme en audio comme suit :
a = pipe.mel.image_to_audio(output.images[0])
a.shape
(130560,)
Nous pouvons Ă©galement convertir un tableau de donnĂ©es audio en images de spectrogramme en chargeant dâabord les donnĂ©es audio brutes, puis en appelant la fonction audio_slice_to_image(). Les clips plus longs sont automatiquement dĂ©coupĂ©s en morceaux de la bonne longueur pour produire une image de spectrogramme de 256x256 :
pipe.mel.load_audio(raw_audio=a)
im = pipe.mel.audio_slice_to_image(0)
im
Lâaudio est reprĂ©sentĂ© sous la forme dâun long tableau de nombres. Pour lâĂ©couter nous avons besoin dâune autre information clĂ© : la frĂ©quence dâĂ©chantillonnage. Combien dâĂ©chantillons (valeurs individuelles) utilisons-nous pour reprĂ©senter une seconde dâaudio ?
Nous pouvons voir la frĂ©quence dâĂ©chantillonnage utilisĂ©e lors de lâentraĂźnement de ce pipeline avec :
sample_rate_pipeline = pipe.mel.get_sample_rate()
sample_rate_pipeline
22050
Si nous spĂ©cifions mal la frĂ©quence dâĂ©chantillonnage, nous obtenons un son accĂ©lĂ©rĂ© ou ralenti :
display(Audio(output.audios[0], rate=44100)) # Vitesse x2
Finetuning du pipeline
Maintenant que nous avons une compréhension approximative du fonctionnement du pipeline, nous allons le finetuner sur de nouvelles données audio !
Le jeu de données est une collection de clips audio de différents genres, que nous pouvons charger depuis le Hub de la maniÚre suivante :
from datasets import load_dataset
dataset = load_dataset('lewtun/music_genres', split='train')
dataset
Dataset({
features: ['audio', 'song_id', 'genre_id', 'genre'],
num_rows: 19909
})
Vous pouvez utiliser le code ci-dessous pour voir les diffĂ©rents genres dans le jeu de donnĂ©es et combien dâĂ©chantillons sont contenus dans chacun dâeux :
for g in list(set(dataset['genre'])):
print(g, sum(x==g for x in dataset['genre']))
Pop 945
Blues 58
Punk 2582
Old-Time / Historic 408
Experimental 1800
Folk 1214
Electronic 3071
Spoken 94
Classical 495
Country 142
Instrumental 1044
Chiptune / Glitch 1181
International 814
Ambient Electronic 796
Jazz 306
Soul-RnB 94
Hip-Hop 1757
Easy Listening 13
Rock 3095
Le jeu de données contient les données audio sous forme de tableaux :
audio_array = dataset[0]['audio']['array']
sample_rate_dataset = dataset[0]['audio']['sampling_rate']
print('Audio array shape:', audio_array.shape)
print('Sample rate:', sample_rate_dataset)
display(Audio(audio_array, rate=sample_rate_dataset))
Audio array shape: (1323119,)
Sample rate: 44100
Notez que la frĂ©quence dâĂ©chantillonnage de cet audio est plus Ă©levĂ©e. Si nous voulons utiliser le pipeline existant, nous devrons le « rééchantillonner » pour quâil corresponde Ă la frĂ©quence dâĂ©chantillonnage. Les clips sont Ă©galement plus longs que ceux pour lesquels le pipeline est configurĂ©. Heureusement, lorsque nous chargeons lâaudio Ă lâaide de pipe.mel, il dĂ©coupe automatiquement le clip en sections plus petites :
a = dataset[0]['audio']['array'] # Obtenir le tableau audio
pipe.mel.load_audio(raw_audio=a) # Le charger avec pipe.mel
pipe.mel.audio_slice_to_image(0) # Visualiser la premiĂšre "tranche" sous forme de spectrogramme
Nous devons penser Ă ajuster le taux dâĂ©chantillonnage, car les donnĂ©es de ce jeu de donnĂ©es comportent deux fois plus dâĂ©chantillons par seconde :
sample_rate_dataset = dataset[0]['audio']['sampling_rate']
sample_rate_dataset
44100
Ici, nous utilisons les transformations de torchaudio (importĂ©es sous le nom AT) pour effectuer le rééchantillonnage, le pipeline mel pour transformer lâaudio en image et les transformations de torchvision (importĂ©es sous le nom IT) pour transformer les images en tenseurs. Nous obtenons ainsi une fonction qui transforme un clip audio en un tenseur de spectrogramme que nous pouvons utiliser pour nous entraĂźner :
resampler = AT.Resample(sample_rate_dataset, sample_rate_pipeline, dtype=torch.float32)
to_t = IT.ToTensor()
def to_image(audio_array):
audio_tensor = torch.tensor(audio_array).to(torch.float32)
audio_tensor = resampler(audio_tensor)
pipe.mel.load_audio(raw_audio=np.array(audio_tensor))
num_slices = pipe.mel.get_number_of_slices()
slice_idx = random.randint(0, num_slices-1) # Piocher une tranche aléatoire à chaque fois (à l'exception de la derniÚre tranche courte)
im = pipe.mel.audio_slice_to_image(slice_idx)
return im
Nous utiliserons notre fonction to_image() dans le cadre dâune fonction collate personnalisĂ©e pour transformer notre jeu de donnĂ©es en un chargeur de donnĂ©es utilisable pour lâentraĂźnement. La fonction collate dĂ©finit la maniĂšre de transformer un batch dâexemples du jeu de donnĂ©es en un batch final de donnĂ©es prĂȘtes Ă ĂȘtre entraĂźnĂ©es. Dans ce cas, nous transformons chaque Ă©chantillon audio en une image de spectrogramme et nous empilons les tenseurs rĂ©sultants :
def collate_fn(examples):
# vers l'image -> vers le tenseur -> redimensionnement vers (-1, 1) -> empiler dans le batch
audio_ims = [to_t(to_image(x['audio']['array']))*2-1 for x in examples]
return torch.stack(audio_ims)
# Créer un jeu de données avec uniquement le genre de chansons 'Chiptune / Glitch'
batch_size=4 # 4 sur Colab, 12 sur A100
chosen_genre = 'Electronic' # <<< Essayer d'entraßner sur des genres différents <<<
indexes = [i for i, g in enumerate(dataset['genre']) if g == chosen_genre]
filtered_dataset = dataset.select(indexes)
dl = torch.utils.data.DataLoader(filtered_dataset.shuffle(), batch_size=batch_size, collate_fn=collate_fn, shuffle=True)
batch = next(iter(dl))
print(batch.shape)
torch.Size([4, 1, 256, 256])
NB : Vous devrez utiliser une taille de batch infĂ©rieure (par exemple 4) Ă moins que vous ne disposiez dâune grande quantitĂ© de vRAM GPU.
Boucle dâentraĂźnement
Voici une boucle dâentraĂźnement simple qui sâexĂ©cute Ă travers le chargeur de donnĂ©es pour quelques Ă©poques afin de finetuner le pipeline UNet. Vous pouvez Ă©galement ignorer cette cellule et charger le pipeline avec le code de la cellule suivante.
epochs = 3
lr = 1e-4
pipe.unet.train()
pipe.scheduler.set_timesteps(1000)
optimizer = torch.optim.AdamW(pipe.unet.parameters(), lr=lr)
for epoch in range(epochs):
for step, batch in tqdm(enumerate(dl), total=len(dl)):
# Préparer les images d'entrée
clean_images = batch.to(device)
bs = clean_images.shape[0]
# Ăchantillonner un pas de temps alĂ©atoire pour chaque image
timesteps = torch.randint(
0, pipe.scheduler.num_train_timesteps, (bs,), device=clean_images.device
).long()
# Ajouter du bruit aux images propres en fonction de l'ampleur du bruit à chaque étape
noise = torch.randn(clean_images.shape).to(clean_images.device)
noisy_images = pipe.scheduler.add_noise(clean_images, noise, timesteps)
# Obtenir la prédiction du modÚle
noise_pred = pipe.unet(noisy_images, timesteps, return_dict=False)[0]
# Calculer la perte
loss = F.mse_loss(noise_pred, noise)
loss.backward(loss)
# Mise Ă jour des paramĂštres du modĂšle Ă l'aide de l'optimiseur
optimizer.step()
optimizer.zero_grad()
# OU : Charger la version entraßnée précédemment
pipe = DiffusionPipeline.from_pretrained("johnowhitaker/Electronic_test").to(device)
output = pipe()
display(output.images[0])
display(Audio(output.audios[0], rate=22050))
# Créer un échantillon plus long en passant un tenseur de bruit de départ avec une forme différente
noise = torch.randn(1, 1, pipe.unet.sample_size[0],pipe.unet.sample_size[1]*4).to(device)
output = pipe(noise=noise)
display(output.images[0])
display(Audio(output.audios[0], rate=22050))
Ce ne sont pas les rĂ©sultats les plus impressionnants mais câest un dĂ©but :) Essayez dâajuster le taux dâapprentissage et le nombre dâĂ©poques, et partagez vos meilleurs rĂ©sultats sur Discord pour que nous puissions nous amĂ©liorer ensemble !
Quelques éléments à prendre en compte
- Nous travaillons avec des images de spectrogrammes carrĂ©s de 256 pixels ce qui limite la taille de nos batchs. Pouvez-vous rĂ©cupĂ©rer de lâaudio de qualitĂ© suffisante Ă partir dâun spectrogramme de 128x128 ?
- Au lieu dâune augmentation alĂ©atoire de lâimage, nous choisissons Ă chaque fois des tranches diffĂ©rentes du clip audio, mais cela pourrait-il ĂȘtre amĂ©liorĂ© avec diffĂ©rents types dâaugmentation lorsque lâon sâentraĂźne pendant de nombreuses Ă©poques ?
- Comment pourrions-nous utiliser cette mĂ©thode pour gĂ©nĂ©rer des clips plus longs ? Peut-ĂȘtre pourriez-vous gĂ©nĂ©rer un clip de dĂ©part de 5 secondes, puis utiliser des idĂ©es inspirĂ©es de la complĂ©tion dâimages (inpainting) pour continuer Ă gĂ©nĂ©rer des segments audio supplĂ©mentaires Ă partir du clip initialâŠ
- Quel est lâĂ©quivalent dâune image Ă image dans ce contexte de diffusion de spectrogrammes ?
Pousser sur le Hub
Une fois que vous ĂȘtes satisfait de votre modĂšle, vous pouvez le sauvegarder et le transfĂ©rer sur le Hub pour que dâautres personnes puissent en profiter :
from huggingface_hub import get_full_repo_name, HfApi, create_repo, ModelCard
# Choisir un nom pour le modĂšle
model_name = "audio-diffusion-electronic"
hub_model_id = get_full_repo_name(model_name)
# Sauvegarder le pipeline localement
pipe.save_pretrained(model_name)
# Inspecter le contenu du dossier
!ls {model_name}
mel model_index.json scheduler unet
# Créer un dépÎt
create_repo(hub_model_id)
# Télécharger les fichiers
api = HfApi()
api.upload_folder(
folder_path=f"{model_name}/scheduler", path_in_repo="scheduler", repo_id=hub_model_id
)
api.upload_folder(
folder_path=f"{model_name}/mel", path_in_repo="mel", repo_id=hub_model_id
)
api.upload_folder(folder_path=f"{model_name}/unet", path_in_repo="unet", repo_id=hub_model_id)
api.upload_file(
path_or_fileobj=f"{model_name}/model_index.json",
path_in_repo="model_index.json",
repo_id=hub_model_id,
)
# Pousser une carte de modĂšle
content = f"""
---
license: mit
tags:
- pytorch
- diffusers
- unconditional-audio-generation
- diffusion-models-class
---
# Model Card for Unit 4 of the [Diffusion Models Class đ§š](https://github.com/huggingface/diffusion-models-class)
This model is a diffusion model for unconditional audio generation of music in the genre {chosen_genre}
## Usage
```python
from IPython.display import Audio
from diffusers import DiffusionPipeline
pipe = DiffusionPipeline.from_pretrained("{hub_model_id}")
output = pipe()
display(output.images[0])
display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate()))
```python
"""
card = ModelCard(content)
card.push_to_hub(hub_model_id)
Conclusion
Ce notebook vous a donnĂ©, nous lâespĂ©rons, un petit aperçu du potentiel de la gĂ©nĂ©ration audio. Consultez certaines des rĂ©fĂ©rences liĂ©es Ă la vue dâensemble de cette unitĂ© pour voir des mĂ©thodes plus fantaisistes et des Ă©chantillons stupĂ©fiants quâelles peuvent crĂ©er !