Cours dâapprentissage profond de la New York University
Edition 2021
Le cours de Yann LE CUN et Alfredo CANZIANI
traduit en français par Loïck BOURDOIS
Accueil
Bienvenue au cours sur les modĂšles de diffusion đ€ !
Ă quoi sâattendre ?
Dans ce cours gratuit, vous allez :
- đ©âđ Ătudier la thĂ©orie des modĂšles de diffusion
- đ§š Apprendre Ă gĂ©nĂ©rer des images et de lâaudio avec la bibliothĂšque populaire đ€ Diffusers
- đïžââïž EntraĂźner vos propres modĂšles de diffusion Ă partir de zĂ©ro
- đ» Affiner des modĂšles de diffusion existants sur de nouveaux jeux de donnĂ©es
- đș Explorer la gĂ©nĂ©ration conditionnelle et le guidage
- đ§âđŹ CrĂ©er vos propres pipelines de modĂšles de diffusion personnalisĂ©s
Prérequis
Ce cours requiert un bon niveau en Python et des bases en apprentissage profond et Pytorch. Si ce nâest pas encore le cas, vous pouvez consulter ces ressources gratuites (en anglais) :
- Python : https://www.udacity.com/course/introduction-to-pythonâud1110
- Introduction Ă lâapprentissage profond avec PyTorch : https://www.udacity.com/course/deep-learning-pytorchâud188
- PyTorch en 60 min : https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
Pour pousser vos modĂšles sur le Hub dâHugging Face, vous aurez besoin dâun compte. Vous pouvez en crĂ©er un gratuitement Ă lâadresse suivante : https://huggingface.co/join.
Quel est le programme ?
Le cours est constituĂ© de quatre unitĂ©s. Chacune dâelle est composĂ©e dâune partie thĂ©orie listant Ă©galement des ressources / papiers, ainsi que de deux notebooks. Plus prĂ©cisĂ©ment, nous avons :
- Unité 1 : Introduction aux modÚles de diffusion
Introduction Ă đ€ Diffusers et implĂ©mentation Ă partir de 0 - UnitĂ© 2 : Finetuning et guidage
Finetuner un modÚle de diffusion sur de nouvelles données et ajout du guidage - Unité 3 : Stable Diffusion
Exploration dâun puissant modĂšle de diffusion latent conditionnĂ© par le texte - UnitĂ© 4 : Faire plus avec la diffusion
Techniques avancées pour aller plus loin dans la diffusion
Qui sommes-nous ?
Ă propos des auteurs de ce cours :
Jonathan Whitaker est TODO.
Lewis Tunstall est ingĂ©nieur en apprentissage machine chez Hugging Face et dĂ©vouĂ© au dĂ©veloppement dâoutils open source avec la volontĂ© de les rendre accessibles Ă une communautĂ© plus large. Il est Ă©galement co-auteur du livre Natural Language Processing with Transformers.
FAQ
Voici quelques réponses aux questions fréquemment posées :
-
Suivre ce cours mĂšne-t-il Ă une certification ?
Actuellement, nous nâavons pas de certification pour ce cours. -
Combien de temps dois-je consacrer Ă ce cours ?
Chaque chapitre de ce cours est conçu pour ĂȘtre complĂ©tĂ© en une semaine, avec environ 6 Ă 8 heures de travail par unitĂ©. Cependant, vous pouvez prendre tout le temps nĂ©cessaire pour le suivre. -
OĂč puis-je poser une question si jâen ai une ?
Si vous avez une question sur lâune des sections du cours, il vous suffit de cliquer sur la banniĂšre « Ask a question » en haut de la page pour ĂȘtre automatiquement redirigĂ© vers le Discord de Hugging Face pour poser votre question dans le channel#diffusion-models-class.

- OĂč puis-je obtenir le code du cours ?
Pour chaque section, vous pouvez cliquer sur la banniÚre en haut de la page pour exécuter son code :

-
Comment puis-je contribuer au cours ?
Il existe de nombreuses façons de contribuer au cours ! Si vous trouvez une coquille ou un bug, veuillez ouvrir une « Issue » sur le dépÎtdiffusion-models-class. Si vous souhaitez aider à traduire le cours dans votre langue maternelle, consultez les instructions ici. -
Peut-on réutiliser ce cours?
Bien sĂ»r ! Le cours est publiĂ© sous la licence Apache 2 license. Cela signifie que vous devez crĂ©diter de maniĂšre appropriĂ©e, fournir un lien vers la licence et indiquer si des modifications ont Ă©tĂ© apportĂ©es. Vous pouvez le faire de toute maniĂšre raisonnable, mais pas dâune façon qui suggĂšre que le distributeur de la licence vous approuve ou approuve votre utilisation. Si vous souhaitez citer le cours, veuillez utiliser le BibTeX suivant :
@misc{huggingfacecourse,
author = {Hugging Face},
title = {The Hugging Face Diffusion Models Course, 2022},
howpublished = "\url{https://huggingface.co/course}",
year = {2022},
note = "[Online; accessed <today>]"
}
Câest parti !
Ătes-vous prĂȘt Ă commencer ? Alors rendez vous Ă la premiĂšre unitĂ© pour dĂ©buter le cours.
1. Vue d'ensemble
Dans cette unitĂ©, vous apprendrez les bases de fonctionnement des modĂšles de diffusion et comment crĂ©er les vĂŽtres Ă lâaide de la bibliothĂšque đ€ Diffusers.
Vue dâensemble de cette unitĂ© :rocket:
Les différentes étapes à suivre pour cette unité :
- Lisez le matĂ©riel dâintroduction ci-dessous ainsi que toutes les ressources supplĂ©mentaires listĂ©es en bas de page qui vous sembleront intĂ©ressantes.
- Consultez le notebook Introduction Ă Diffusers pour mettre en pratique la thĂ©orie avec la bibliothĂšque đ€ Diffusers.
- EntraĂźnez et partagez votre propre modĂšle de diffusion en utilisant le notebook ou le script dâentraĂźnement associĂ©.
- (Facultatif) Approfondissez avec le notebook Implémentation à partir de 0 des modÚles de diffusion à partir de zéro si vous souhaitez voir une implémentation minimale à partir de zéro et explorer les différentes décisions de conception en jeu.
- (Facultatif) Regardez cette vidéo (en anglais) pour une présentation informelle du matériel de cette unité.
Que sont les modĂšles de diffusion ?
Les modĂšles de diffusion sont un ajout relativement rĂ©cent Ă un groupe dâalgorithmes connus sous le nom de modĂšles gĂ©nĂ©ratifs. Lâobjectif de la modĂ©lisation gĂ©nĂ©rative est dâapprendre Ă gĂ©nĂ©rer des donnĂ©es, telles que des images ou des sons, Ă partir dâun certain nombre dâexemples dâentraĂźnement. Un bon modĂšle gĂ©nĂ©ratif crĂ©era un ensemble diversifiĂ© de sorties qui ressemblent aux donnĂ©es dâentraĂźnement sans ĂȘtre des copies exactes. Comment les modĂšles de diffusion y parviennent-ils ? Concentrons-nous sur le cas de la gĂ©nĂ©ration dâimages Ă des fins dâillustration.
Figure tirée du papier DDPM de Ho et al. (2020) (https://arxiv.org/abs/2006.11239).
Le secret de la rĂ©ussite des modĂšles de diffusion rĂ©side dans la nature itĂ©rative du processus de diffusion. La gĂ©nĂ©ration commence par un bruit alĂ©atoire, mais celui-ci est progressivement affinĂ© au cours dâun certain nombre dâĂ©tapes jusquâĂ ce quâune image de sortie Ă©merge. Ă chaque Ă©tape, le modĂšle estime comment nous pourrions passer de lâentrĂ©e actuelle x_t Ă une version complĂštement dĂ©bruitĂ©e x_0. Cependant, comme nous nâeffectuons quâun petit changement Ă chaque Ă©tape t, toute erreur dans cette estimation aux premiers stades (oĂč il est extrĂȘmement difficile de prĂ©dire le rĂ©sultat final) peut ĂȘtre corrigĂ©e dans les mises Ă jour ultĂ©rieures.
EntraĂźner le modĂšle est relativement simple par rapport Ă dâautres types de modĂšles gĂ©nĂ©ratifs. Nous procĂ©dons de maniĂšre rĂ©pĂ©tĂ©e 1) Nous chargeons quelques images Ă partir des donnĂ©es dâentraĂźnement. 2) Nous ajoutons du bruit, en diffĂ©rentes quantitĂ©s. Nâoubliez pas que nous voulons que le modĂšle soit capable dâestimer comment « corriger » (dĂ©bruiter) Ă la fois des images extrĂȘmement bruitĂ©es et des images qui sont proches de la perfection. 3) Nous introduisons les versions bruitĂ©es des donnĂ©es dâentrĂ©e dans le modĂšle. 4) Nous Ă©valuons lâefficacitĂ© du modĂšle Ă dĂ©bruiter ces donnĂ©es dâentrĂ©e. 5) Nous utilisons ces informations pour mettre Ă jour les poids du modĂšle.
Pour gĂ©nĂ©rer de nouvelles images Ă lâaide dâun modĂšle entraĂźnĂ©, nous commençons par une entrĂ©e totalement alĂ©atoire que nous soumettons au modĂšle de maniĂšre rĂ©pĂ©tĂ©e, en lâactualisant Ă chaque fois dâune petite quantitĂ© basĂ©e sur la prĂ©diction du modĂšle. Comme nous le verrons, il existe un certain nombre de mĂ©thodes dâĂ©chantillonnage qui tentent de rationaliser ce processus afin de gĂ©nĂ©rer de bonnes images en un minimum dâĂ©tapes.
Nous montrerons chacune de ces Ă©tapes en dĂ©tail dans les notebooks de lâunitĂ© 1. Dans lâunitĂ© 2, nous verrons comment ce processus peut ĂȘtre modifiĂ© pour ajouter un contrĂŽle supplĂ©mentaire sur les rĂ©sultats du modĂšle par le biais dâun conditionnement supplĂ©mentaire (tel quâune Ă©tiquette de classe) ou de techniques telles que le guidage. Les unitĂ©s 3 et 4 exploreront un modĂšle de diffusion extrĂȘmement puissant appelĂ© Stable Diffusion, qui peut gĂ©nĂ©rer des images Ă partir de descriptions textuelles.
Notebooks
A ce stade, vous en savez assez pour vous lancer dans les notebooks de cette unitĂ© ! Les deux notebooks abordent la mĂȘme idĂ©e de maniĂšre diffĂ©rente.
| Chapitre | Colab | Kaggle | Gradient | Studio Lab |
|---|---|---|---|---|
| Introduction Ă Diffusers | ||||
| Implémentation à partir de 0 |
Dans Introduction Ă Diffusers, nous montrons les diffĂ©rentes Ă©tapes dĂ©crites ci-dessus en utilisant les blocs de la bibliothĂšque đ€ Diffusers. Vous verrez rapidement comment crĂ©er, entraĂźner et Ă©chantillonner vos propres modĂšles de diffusion sur les donnĂ©es de votre choix. Ă la fin du notebook, vous serez en mesure de lire et de modifier le script dâentraĂźnement illustratif pour entraĂźner des modĂšles de diffusion et les partager avec le monde entier ! Ce notebook introduit Ă©galement lâexercice principal associĂ© Ă cette unitĂ©, oĂč nous tenterons collectivement de trouver de bonnes « recettes dâentraĂźnement » pour les modĂšles de diffusion Ă diffĂ©rentes Ă©chelles (voir la section suivante pour plus dâinformations).
Dans ModĂšles de diffusion Ă partir de 0, nous montrons ces mĂȘmes Ă©tapes (ajout de bruit aux donnĂ©es, crĂ©ation dâun modĂšle, entraĂźnement et Ă©chantillonnage) mais implĂ©mentĂ©es Ă partir de zĂ©ro dans PyTorch aussi simplement que possible. Nous comparons ensuite cet « exemple-jouet » avec la version de đ€ Diffusers, en notant les diffĂ©rences entre les deux et les amĂ©liorations qui ont Ă©tĂ© apportĂ©es. Lâobjectif est de se familiariser avec les diffĂ©rents composants et les dĂ©cisions de conception qui les sous-tendent, afin de pouvoir identifier rapidement les idĂ©es clĂ©s pour une nouvelle implĂ©mentation.
Projet
Une fois les bases assimilĂ©es grĂące aux notebooks, essayez dâentraĂźner un ou plusieurs modĂšles de diffusion ! Quelques suggestions sont incluses Ă la fin du notebook Introduction Ă Diffusers. Nâoubliez pas de partager vos rĂ©sultats, vos recettes dâentraĂźnement et vos dĂ©couvertes avec la communautĂ© afin que nous puissions trouver ensemble les meilleures façons dâentraĂźner ces modĂšles.
Ressources complémentaires
Une liste non exhaustive de ressources (en anglais) Ă consulter :
- Le modĂšle de diffusion annotĂ© est une prĂ©sentation trĂšs approfondie du code et de la thĂ©orie qui sous-tend les DDPM, avec des mathĂ©matiques et du code montrant tous les diffĂ©rents composants. Il liste Ă©galement un certain nombre dâarticles pour une lecture plus approfondie.
- La documentation dâHugging Face sur la GĂ©nĂ©ration dâimages inconditionnelle contient des exemples dâentraĂźnement de modĂšles de diffusion Ă lâaide du script dâentraĂźnement officiel, y compris le code montrant comment crĂ©er votre propre jeu de donnĂ©es.
- La vidĂ©o dâAI Coffee Break sur les modĂšles de diffusion
- La vidéo de Yannic Kilcher sur les DDPM
Vous avez identifiĂ© dâautres ressources intĂ©ressantes ? Faites-le nous savoir et nous les ajouterons Ă cette liste.
1.1. Introduction Ă đ€ Diffusers
Dans ce notebook, vous allez entraĂźner votre premier modĂšle de diffusion pour gĂ©nĂ©rer des images de mignons papillons đŠ. En cours de route, vous apprendrez les composants de base de la bibliothĂšque đ€ Diffusers, qui fournira une bonne assise pour les applications plus avancĂ©es que nous couvrirons plus tard dans le cours.
DĂ©butons par une vue dâensemble de ce quâon va faire dans ce notebook. Nous allons :
- Voir un puissant pipeline de modÚles de diffusion personnalisé en action (avec des informations sur la façon de créer votre propre version).
- Créer votre propre mini-pipeline en :
- Récapitulant les idées principales derriÚre les modÚles de diffusion
- Chargement de donnĂ©es Ă partir du Hub pour lâentraĂźnement
- Explorer comment ajouter du bruit Ă ces donnĂ©es Ă lâaide dâun planificateur
- Créer et entraßner le modÚle UNet
- Rassembler les piĂšces du puzzle pour en faire un pipeline fonctionnel
- Ăditer et exĂ©cuter un script pour initialiser des sĂ©ries dâentraĂźnement plus longues, qui gĂšrera
- EntraĂźnement multi-GPU via đ€ Accelerate
- Journalisation de lâexpĂ©rience pour suivre les statistiques critiques
- TĂ©lĂ©chargement du modĂšle final sur le Hub dâHugging Face
Installation des bibliothĂšques
ExĂ©cutez la cellule suivante pour installer la bibliothĂšque đ€ Diffusers ainsi que quelques autres prĂ©requis :
%pip install -qq -U diffusers datasets transformers accelerate ftfy pyarrow==9.0.0
Ensuite, rendez-vous sur https://huggingface.co/settings/tokens et crĂ©ez un tokens dâaccĂšs avec autorisation dâĂ©criture si vous nâen avez pas dĂ©jĂ un :
Vous pouvez vous connecter avec ce token en utilisant la ligne de commande (huggingface-cli login) ou en exécutant la cellule suivante :
from huggingface_hub import notebook_login
notebook_login()
Vous devez ensuite installer Git-LFS pour télécharger les checkpoints de votre modÚle :
%%capture
!sudo apt -qq install git-lfs
!git config --global credential.helper store
Enfin, importons les bibliothÚques que nous utiliserons et définissons quelques fonctions de confort que nous utiliserons plus tard dans le notebook :
import numpy as np
import torch
import torch.nn.functional as F
from matplotlib import pyplot as plt
from PIL import Image
def show_images(x):
"""Ătant donnĂ© un lot d'images x, faire une grille et convertir en PIL"""
x = x * 0.5 + 0.5 # On va de (-1, 1) et revenons (0, 1)
grid = torchvision.utils.make_grid(x)
grid_im = grid.detach().cpu().permute(1, 2, 0).clip(0, 1) * 255
grid_im = Image.fromarray(np.array(grid_im).astype(np.uint8))
return grid_im
def make_grid(images, size=64):
"""Ătant donnĂ© une liste d'images PIL, les empiler en une ligne pour faciliter la visualisation."""
output_im = Image.new("RGB", (size * len(images), size))
for i, im in enumerate(images):
output_im.paste(im.resize((size, size)), (i * size, 0))
return output_im
# Les utilisateurs de Mac peuvent avoir besoin de device = 'mps' (non testé)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
OK, nous sommes prĂȘts !
Exemple gĂ©nĂ©rique dâinfĂ©rence avec Dreambooth, un avant-goĂ»t de ce qui nous attend
Si vous avez un tant soit peu consultĂ© les mĂ©dias sociaux au cours des derniers mois, vous avez certainement entendu parler de Stable Diffusion. Il sâagit dâun puissant modĂšle de diffusion latent conditionnĂ© par le texte (ne vous inquiĂ©tez pas, nous allons apprendre ce que cela signifie). Mais il a un dĂ©faut : il ne sait pas Ă quoi vous ou moi ressemblons, Ă moins que nous soyons suffisamment cĂ©lĂšbres pour que nos images soient rĂ©pandues sur internet.
Dreambooth nous permet de crĂ©er notre propre variante de modĂšle avec une connaissance supplĂ©mentaire dâun visage, dâun objet ou dâun style spĂ©cifique. Le Corridor Crew a rĂ©alisĂ© une excellente vidĂ©o (en anglais) en utilisant cette technique pour raconter des histoires avec des personnages cohĂ©rents, ce qui est un excellent exemple de ce que cette technique peut faire :
from IPython.display import YouTubeVideo
YouTubeVideo("W4Mcuh38wyM")
Voici un exemple dâune sortie dâun modĂšle entraĂźnĂ© sur 5 photos du jouet Monsieur Patate.
Tout dâabord, nous chargeons le pipeline. Ceci tĂ©lĂ©charge les poids du modĂšle depuis le Hub. Ătant donnĂ© que plusieurs gigaoctets de donnĂ©es sont tĂ©lĂ©chargĂ©s pour une dĂ©monstration dâune ligne, vous pouvez sauter cette cellule et simplement admirer la sortie de lâexemple !
from diffusers import StableDiffusionPipeline
# Consultez https://huggingface.co/sd-dreambooth-library pour découvrir de nombreux modÚles provenant de la communauté
model_id = "sd-dreambooth-library/mr-potato-head"
# Chargement du pipeline
pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to(
device
)
Une fois le chargement du pipeline terminé, nous pouvons générer des images avec :
prompt = "an abstract oil painting of sks mr potato head by picasso"
image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0]
image
âïž Ă votre tour ! Essayez vous-mĂȘme avec des prompts diffĂ©rents. Le token
sksreprĂ©sente un identifiant unique pour le nouveau concept : que se passe-t-il si vous lâomettez ? Vous pouvez aussi expĂ©rimenter en changeant le nombre de pas dâĂ©chantillonnage (jusquâoĂč pouvez-vous descendre ?) et le paramĂštreguidance_scale, qui dĂ©termine jusquâĂ quel point le modĂšle va essayer de correspondre au prompt.Il se passe beaucoup de choses dans ce pipeline ! Ă la fin du cours, vous saurez comment tout cela fonctionne. Pour lâinstant, voyons comment nous pouvons entraĂźner un modĂšle de diffusion Ă partir de zĂ©ro.
MVP (Minimum Viable Pipeline)
Exemple dâinfĂ©rence sur les papillons
LâAPI de base de đ€ Diffusers est divisĂ©e en trois composants principaux :
- Pipelines : classes de haut niveau conçues pour générer rapidement des échantillons à partir de modÚles de diffusion populaires entraßnés de maniÚre conviviale.
- Models : architectures populaires pour entraĂźner de nouveaux modĂšles de diffusion, par exemple UNet.
- Schedulers : diverses techniques pour gĂ©nĂ©rer des images Ă partir du bruit pendant lâinfĂ©rence ainsi que pour gĂ©nĂ©rer des images bruitĂ©es pour lâentraĂźnement.
Les pipelines sont parfaits pour les utilisateurs finaux, mais si vous ĂȘtes ici pour ce cours, nous supposons que vous voulez savoir ce qui se passe sous le capot ! Dans le reste de ce notebook, nous allons donc construire notre propre pipeline capable de gĂ©nĂ©rer de petites images de papillons. Voici le rĂ©sultat final en action :
from diffusers import DDPMPipeline
# Chargement du pipeline de papillons
butterfly_pipeline = DDPMPipeline.from_pretrained(
"johnowhitaker/ddpm-butterflies-32px"
).to(device)
# Création de 8 images
images = butterfly_pipeline(batch_size=8).images
# Visualisation du résultat
make_grid(images)
Ce nâest peut-ĂȘtre pas aussi impressionnant que lâexemple de DreamBooth, mais nous entraĂźnons notre modĂšle Ă partir de zĂ©ro avec ~0,0001% des donnĂ©es utilisĂ©es pour entraĂźner Stable Diffusion. En parlant dâentraĂźnement, rappelez-vous que lâentraĂźnement dâun modĂšle de diffusion ressemble Ă ceci :
- Chargement de quelques images à partir des données entraßnées.
- Ajout de bruit, en différentes quantités.
- Introduction des versions bruitĂ©es des donnĂ©es dâentrĂ©e dans le modĂšle.
- Ăvaluation de la capacitĂ© du modĂšle Ă dĂ©bruiter ces donnĂ©es dâentrĂ©e
- Utilisation de ces informations pour mettre à jour les poids du modÚle, et répétition.
Nous allons explorer ces Ă©tapes une par une dans les prochaines parties jusquâĂ ce que nous ayons une boucle dâentraĂźnement complĂšte, puis nous verrons comment Ă©chantillonner Ă partir du modĂšle entraĂźnĂ© et comment regrouper le tout dans un pipeline pour faciliter le partage. Commençons par les donnĂ©es.
TĂ©lĂ©charger le jeu de donnĂ©es dâentraĂźnement
Pour cet exemple, nous utilisons un jeu de donnĂ©es dâimages provenant du Hub dâHugging Face. Plus prĂ©cisĂ©ment, cette collection de 1000 images de papillons. Il sâagit dâun trĂšs petit jeu de donnĂ©es, câest pourquoi nous avons aussi inclus des lignes en commentaires pour quelques options plus importantes. Si vous prĂ©fĂ©rez utiliser votre propre collection dâimages, vous pouvez Ă©galement utiliser lâexemple de code commentĂ© pour charger des images Ă partir dâun dossier.
import torchvision
from datasets import load_dataset
from torchvision import transforms
dataset = load_dataset("huggan/smithsonian_butterflies_subset", split="train")
# Ou charger des images Ă partir d'un dossier local
# dataset = load_dataset("imagefolder", data_dir="path/to/folder")
# Nous entraßnerons sur des images carrées de 32 pixels, mais vous pouvez aussi essayer des tailles plus grandes
image_size = 32
# Vous pouvez réduire la taille de votre batch si vous manquez de mémoire GPU
batch_size = 64
# Définition les augmentations de données
preprocess = transforms.Compose(
[
transforms.Resize((image_size, image_size)), # Redimensionner
transforms.RandomHorizontalFlip(), # Retournement aléatoire
transforms.ToTensor(), # Convertir en tenseur (0, 1)
transforms.Normalize([0.5], [0.5]), # Passage en (-1, 1)
]
)
def transform(examples):
images = [preprocess(image.convert("RGB")) for image in examples["image"]]
return {"images": images}
dataset.set_transform(transform)
# Créer un chargeur de données à partir du jeu de données pour servir les images transformées en batchs
train_dataloader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True
)
Nous pouvons saisir un batch dâimages et en visualiser quelques-unes comme suit :
xb = next(iter(train_dataloader))["images"].to(device)[:8]
print("X shape:", xb.shape)
show_images(xb).resize((8 * 64, 64), resample=Image.NEAREST)
Nous nous en tenons Ă un petit jeu de donnĂ©es avec des images de 32 pixels pour que les temps dâentraĂźnement restent raisonnables dans ce notebook.
Définir le planificateur
Notre plan dâentraĂźnement consiste Ă prendre ces images dâentrĂ©e et Ă leur ajouter du bruit, puis Ă transmettre les images bruitĂ©es au modĂšle. Lors de lâinfĂ©rence, nous utiliserons les prĂ©dictions du modĂšle pour supprimer le bruit de maniĂšre itĂ©rative. Dans đ€ Diffusers, ces deux processus sont gĂ©rĂ©s par le scheduler (planificateur).
Le planificateur de bruit dĂ©termine la quantitĂ© de bruit ajoutĂ©e Ă diffĂ©rents moments. Voici comment nous pourrions crĂ©er un planificateur en utilisant les paramĂštres par dĂ©faut pour lâentraĂźnement et lâĂ©chantillonnage âDDPMâ (dâaprĂšs lâarticle dâaprĂšs lâarticle Denoising Diffusion Probabalistic Models) :
from diffusers import DDPMScheduler
noise_scheduler = DDPMScheduler(num_train_timesteps=1000)
Le papier DDPM dĂ©crit un processus de corruption qui ajoute une petite quantitĂ© de bruit Ă chaque pas de temps. Ătant donnĂ© $x_{t-1}$ pour un certain pas de temps, nous pouvons obtenir la version suivante (lĂ©gĂšrement plus bruyante) $x_t$ avec :
\[\begin{aligned} q(\mathbf{x}_t \vert \mathbf{x}_{t-1}) &= \mathcal{N}(\mathbf{x}_t; \sqrt{1 - \beta_t} \mathbf{x}_{t-1}, \beta_t\mathbf{I}) \\ q(\mathbf{x}_{1:T} \vert \mathbf{x}_0) &= \prod^T_{t=1} q(\mathbf{x}_t \vert \mathbf{x}_{t-1}) \end{aligned}\]Nous prenons $x_{t-1}$, lâĂ©chelonnons de $\sqrt{1 - \beta_t}$ et ajoutons du bruit Ă©chelonnĂ© par $\beta_t$. Ce $\beta$ est dĂ©fini pour chaque $t$ selon un certain planificateur et dĂ©termine la quantitĂ© de bruit ajoutĂ©e par pas de temps. Maintenant, nous ne voulons pas nĂ©cessairement faire cette opĂ©ration 500 fois pour obtenir $x_{500}$, nous avons donc une autre formule pour obtenir $x_t$ pour nâimporte quel t Ă©tant donnĂ© $x_0$ :
\[\begin{aligned} q(\mathbf{x}_t \vert \mathbf{x}_0) &= \mathcal{N}(\mathbf{x}_t; \sqrt{\bar{\alpha}_t} \mathbf{x}_0, {(1 - \bar{\alpha}_t)} \mathbf{I}) \end{aligned}\]oĂč :
\[\bar{\alpha}_t = \prod_{i=1}^{T} \alpha_i,\quad \alpha_i = 1 - \beta_i\]La notation mathĂ©matique fait toujours peur ! Heureusement, le planificateur sâen charge pour nous. Nous pouvons tracer $\sqrt{\bar{\alpha}_t}$ (appelĂ© sqrt_alpha_prod) et $\sqrt{(1 - \bar{\alpha}_t)}$ (appelĂ© sqrt_one_minus_alpha_prod) pour voir comment lâentrĂ©e ($x$) et le bruit sont mis Ă lâĂ©chelle et mĂ©langĂ©s Ă travers diffĂ©rents pas de temps :
plt.plot(noise_scheduler.alphas_cumprod.cpu() ** 0.5, label=r"${\sqrt{\bar{\alpha}_t}}$")
plt.plot((1 - noise_scheduler.alphas_cumprod.cpu()) ** 0.5, label=r"$\sqrt{(1 - \bar{\alpha}_t)}$")
plt.legend(fontsize="x-large");
âïž Ă votre tour ! Vous pouvez explorer comment ce graphique change avec diffĂ©rents paramĂštres pour
beta_start,beta_endetbeta_scheduleen remplaçant lâune des options commentĂ©es ci-dessous :
## Exemple avec beaucoup de bruit ajouté :
# noise_scheduler = DDPMScheduler(num_train_timesteps=1000, beta_start=0.001, beta_end=0.004)
## Le planificateur cosinus pouvant s'avérer meilleur pour les images de petite taille :
# noise_scheduler = DDPMScheduler(num_train_timesteps=1000, beta_schedule='squaredcos_cap_v2')
Quel que soit le planificateur que vous avez choisi, nous pouvons maintenant lâutiliser pour ajouter du bruit en diffĂ©rentes quantitĂ©s en utilisant la fonction noise_scheduler.add_noise comme suit :
timesteps = torch.linspace(0, 999, 8).long().to(device)
noise = torch.randn_like(xb)
noisy_xb = noise_scheduler.add_noise(xb, noise, timesteps)
print("Noisy X shape", noisy_xb.shape)
show_images(noisy_xb).resize((8 * 64, 64), resample=Image.NEAREST)
LĂ encore, Ă©tudiez lâeffet de lâutilisation de diffĂ©rents planificateurs et paramĂštres de bruit. Cette vidĂ©o (en anglais) explique en dĂ©tail certains des calculs ci-dessus et constitue une excellente introduction Ă certains de ces concepts.
Définir le modÚle
Nous en arrivons maintenant Ă lâĂ©lĂ©ment central : le modĂšle lui-mĂȘme.
La plupart des modĂšles de diffusion utilisent des architectures qui sont des variantes dâun U-net et câest ce que nous utiliserons ici.
En bref :
- lâimage en entrĂ©e du modĂšle passe par plusieurs blocs de couches ResNet, chacun divisant la taille de lâimage par 2
- puis elle passe Ă travers le mĂȘme nombre de blocs qui la surĂ©chantillonnent.
- il y a des skip connections qui relient les caractéristiques sur le chemin du sous-échantillonnage aux couches correspondantes dans le chemin du suréchantillonnage.
Lâune des principales caractĂ©ristiques de ce modĂšle est quâil prĂ©dit des images de la mĂȘme taille que lâentrĂ©e, ce qui est exactement ce dont nous avons besoin ici.
đ€ Diffusers nous fournit une classe UNet2DModel pratique qui crĂ©e lâarchitecture dĂ©sirĂ©e dans PyTorch.
CrĂ©ons un U-net pour la taille dâimage dĂ©sirĂ©e. Notez que les down_block_types correspondent aux blocs de sous-Ă©chantillonnage (en vert sur le diagramme ci-dessus), et que les up_block_types sont les blocs de surĂ©chantillonnage (en rouge sur le diagramme) :
from diffusers import UNet2DModel
# Création d'un modÚle
model = UNet2DModel(
sample_size=image_size, # la résolution de l'image cible
in_channels=3, # le nombre de canaux d'entrée, 3 pour les images RVB
out_channels=3, # le nombre de canaux de sortie
layers_per_block=2, # le nombre de couches ResNet Ă utiliser par bloc UNet
block_out_channels=(64, 128, 128, 256), # Plus de canaux -> plus de paramĂštres
down_block_types=(
"DownBlock2D", # un bloc de sous-échantillonnage ResNet standard
"DownBlock2D",
"AttnDownBlock2D", # un bloc de sous-échantillonnage ResNet avec auto-attention spatiale
"AttnDownBlock2D",
),
up_block_types=(
"AttnUpBlock2D",
"AttnUpBlock2D", # un bloc de suréchantillonnage ResNet avec auto-attention spatiale
"UpBlock2D",
"UpBlock2D", # un bloc de suréchantillonnage ResNet standard
),
)
model.to(device)
Lorsque vous traitez des donnĂ©es dâentrĂ©e en haute rĂ©solution, vous pouvez utiliser davantage de blocs descendants et ascendants, et ne conserver les couches dâattention que pour les couches de rĂ©solution les plus basses (infĂ©rieures) afin de rĂ©duire lâutilisation de la mĂ©moire. Nous verrons plus tard comment vous pouvez expĂ©rimenter pour trouver les meilleurs paramĂštres pour votre cas dâutilisation.
Nous pouvons vĂ©rifier que le passage dâun batch de donnĂ©es et de pas de temps alĂ©atoires produit une sortie de mĂȘme forme que les donnĂ©es dâentrĂ©e :
with torch.no_grad():
model_prediction = model(noisy_xb, timesteps).sample
model_prediction.shape
Dans la section suivante, nous verrons comment entraĂźner ce modĂšle.
CrĂ©er une boucle dâentraĂźnement
Il est temps dâentraĂźner ! Voici une boucle dâoptimisation typique dans PyTorch, oĂč nous parcourons les donnĂ©es batch par batch et mettons Ă jour les paramĂštres de notre modĂšle Ă chaque Ă©tape Ă lâaide dâun optimiseur, ici, lâoptimiseur AdamW avec un taux dâapprentissage de 0,0004.
Pour chaque batch de données, nous
- échantillonnons des pas de temps aléatoires
- bruitons les données en conséquence
- transmettons les données bruitées au modÚle
- comparons les prĂ©dictions du modĂšle avec la cible (câest-Ă -dire le bruit dans ce cas) en utilisant lâerreur quadratique moyenne comme fonction de perte
- mettons Ă jour les paramĂštres du modĂšle via
loss.backward()etoptimizer.step().
Au cours de ce processus, nous enregistrons aussi les pertes au fil du temps pour un tracé ultérieur.
NB : ce code prend prĂšs de 10 minutes Ă exĂ©cuter. NâhĂ©sitez pas Ă sauter ces deux cellules et Ă utiliser le modĂšle prĂ©-entraĂźnĂ© si vous ĂȘtes pressĂ©. Vous pouvez Ă©galement Ă©tudier comment la rĂ©duction du nombre de canaux dans chaque couche via la dĂ©finition du modĂšle ci-dessus peut accĂ©lĂ©rer les choses.
Lâexemple officiel dâentraĂźnement de đ€ Diffusers entraĂźne un modĂšle plus grand sur ce jeu de donnĂ©es Ă une rĂ©solution plus Ă©levĂ©e, et constitue une bonne rĂ©fĂ©rence pour ce Ă quoi ressemble une boucle dâentraĂźnement moins minimale :
# Définir le planificateur de bruit
noise_scheduler = DDPMScheduler(
num_train_timesteps=1000, beta_schedule="squaredcos_cap_v2"
)
# Boucle d'entraĂźnement
optimizer = torch.optim.AdamW(model.parameters(), lr=4e-4)
losses = []
for epoch in range(30):
for step, batch in enumerate(train_dataloader):
clean_images = batch["images"].to(device)
# Exemple de bruit Ă ajouter aux images
noise = torch.randn(clean_images.shape).to(clean_images.device)
bs = clean_images.shape[0]
# Ăchantillonner un pas de temps alĂ©atoire pour chaque image
timesteps = torch.randint(
0, noise_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
noisy_images = noise_scheduler.add_noise(clean_images, noise, timesteps)
# Obtenir la prédiction du modÚle
noise_pred = model(noisy_images, timesteps, return_dict=False)[0]
# Calculer la perte
loss = F.mse_loss(noise_pred, noise)
loss.backward(loss)
losses.append(loss.item())
# Mise Ă jour des paramĂštres du modĂšle Ă l'aide de l'optimiseur
optimizer.step()
optimizer.zero_grad()
if (epoch + 1) % 5 == 0:
loss_last_epoch = sum(losses[-len(train_dataloader) :]) / len(train_dataloader)
print(f"Epoch:{epoch+1}, loss: {loss_last_epoch}")
En traçant la perte, nous constatons que le modĂšle sâamĂ©liore rapidement dans un premier temps, puis continue Ă sâamĂ©liorer Ă un rythme plus lent (ce qui est plus Ă©vident si nous utilisons une Ă©chelle logarithmique, comme indiquĂ© Ă droite) :
fig, axs = plt.subplots(1, 2, figsize=(12, 4))
axs[0].plot(losses)
axs[1].plot(np.log(losses))
plt.show()
Au lieu dâexĂ©cuter le code dâentraĂźnement ci-dessus, vous pouvez utiliser le modĂšle du pipeline comme suit :
## Décommenter pour charger le modÚle que j'ai entraßné plus tÎt à la place :
# model = butterfly_pipeline.unet
Générer des images
Comment obtenir des images avec ce modĂšle ?
âą Option 1 : CrĂ©ation dâun pipeline :
from diffusers import DDPMPipeline
image_pipe = DDPMPipeline(unet=model, scheduler=noise_scheduler)
pipeline_output = image_pipe()
pipeline_output.images[0]
Nous pouvons enregistrer un pipeline dans un dossier local comme suit :
image_pipe.save_pretrained("my_pipeline")
Inspection du contenu du dossier :
!ls my_pipeline/
model_index.json scheduler unet
Les sous-dossiers scheduler et unet contiennent tout ce qui est nĂ©cessaire pour recrĂ©er ces composants. Par exemple, dans le dossier unet vous trouverez les poids du modĂšle (diffusion_pytorch_model.bin) ainsi quâun fichier de configuration qui spĂ©cifie lâarchitecture UNet.
!ls my_pipeline/unet/
config.json diffusion_pytorch_model.bin
Ensemble, ces fichiers contiennent tout ce qui est nĂ©cessaire pour recrĂ©er le pipeline. Vous pouvez les tĂ©lĂ©charger manuellement sur le Hub pour partager le pipeline avec dâautres personnes, ou consulter le code pour le faire via lâAPI dans la section suivante.
âą Option 2 : Ă©crire une boucle dâĂ©chantillonnage
Si vous inspectez la méthode forward du pipeline, vous pourrez voir ce qui se passe lorsque nous lançons image_pipe() :
# ??image_pipe.forward
Nous commençons par un bruit alĂ©atoire et parcourons les pas de temps de lâordonnanceur du plus bruyant au moins bruyant, en supprimant une petite quantitĂ© de bruit Ă chaque Ă©tape sur la base de la prĂ©diction du modĂšle :
# Point de départ aléatoire (8 images aléatoires) :
sample = torch.randn(8, 3, 32, 32).to(device)
for i, t in enumerate(noise_scheduler.timesteps):
# Obtenir le modÚle de prédiction
with torch.no_grad():
residual = model(sample, t).sample
# Mise à jour de l'échantillon avec le pas
sample = noise_scheduler.step(residual, t, sample).prev_sample
show_images(sample)
La fonction noise_scheduler.step() effectue les calculs nĂ©cessaires pour mettre Ă jour sample de maniĂšre appropriĂ©e. Il existe un certain nombre de mĂ©thodes dâĂ©chantillonnage. Dans lâunitĂ© suivante, nous verrons comment nous pouvons Ă©changer un Ă©chantillonneur diffĂ©rent pour accĂ©lĂ©rer la gĂ©nĂ©ration dâimages avec des modĂšles existants, et nous parlerons plus en dĂ©tail de la thĂ©orie derriĂšre lâĂ©chantillonnage des modĂšles de diffusion.
Pousser votre modĂšle vers le Hub
Dans lâexemple ci-dessus, nous avons enregistrĂ© notre pipeline dans un dossier local. Pour pousser notre modĂšle vers le Hub, nous aurons besoin dâun dĂ©pĂŽt de modĂšles dans lequel nous pourrons pousser nos fichiers. Nous dĂ©terminerons le nom du dĂ©pĂŽt Ă partir de lâID du modĂšle que nous voulons donner Ă notre modĂšle (nâhĂ©sitez pas Ă remplacer le nom du modĂšle par votre propre choix ; il doit juste contenir votre nom dâutilisateur, ce que fait la fonction get_full_repo_name()) :
from huggingface_hub import get_full_repo_name
model_name = "sd-class-butterflies-32"
hub_model_id = get_full_repo_name(model_name)
hub_model_id
Ensuite, crĂ©er un dĂ©pĂŽt de modĂšle sur le đ€ Hub et pousser notre modĂšle :
from huggingface_hub import HfApi, create_repo
create_repo(hub_model_id)
api = HfApi()
api.upload_folder(
folder_path="my_pipeline/scheduler", path_in_repo="", repo_id=hub_model_id
)
api.upload_folder(folder_path="my_pipeline/unet", path_in_repo="", repo_id=hub_model_id)
api.upload_file(
path_or_fileobj="my_pipeline/model_index.json",
path_in_repo="model_index.json",
repo_id=hub_model_id,
)
La derniĂšre chose Ă faire est de crĂ©er une belle carte modĂšle afin que notre gĂ©nĂ©rateur de papillons puisse ĂȘtre facilement trouvĂ© sur le đ€ Hub (nâhĂ©sitez pas Ă dĂ©velopper et Ă modifier la description !) :
from huggingface_hub import ModelCard
content = f"""
---
license: mit
tags:
- pytorch
- diffusers
- unconditional-image-generation
- diffusion-models-class
---
# Model Card for Unit 1 of the [Diffusion Models Class đ§š](https://github.com/huggingface/diffusion-models-class)
This model is a diffusion model for unconditional image generation of cute đŠ.
## Usage
```python
from diffusers import DDPMPipeline
pipeline = DDPMPipeline.from_pretrained('{hub_model_id}')
image = pipeline().images[0]
image
```python
"""
card = ModelCard(content)
card.push_to_hub(hub_model_id)
Maintenant que le modĂšle est sur le Hub, vous pouvez le tĂ©lĂ©charger de nâimporte oĂč en utilisant la mĂ©thode from_pretrained() de DDPMPipeline comme suit :
from diffusers import DDPMPipeline
image_pipe = DDPMPipeline.from_pretrained(hub_model_id)
pipeline_output = image_pipe()
pipeline_output.images[0]
Bien, ça marche !
Passer Ă lâĂ©chelle supĂ©rieure avec đ€ Accelerate
Ce notebook a Ă©tĂ© conçu Ă des fins dâapprentissage, et en tant que tel, nous avons essayĂ© de garder le code aussi minimal et propre que possible. Pour cette raison, nous avons omis certaines choses que vous pourriez souhaiter si vous deviez entraĂźner un modĂšle plus grand sur beaucoup plus de donnĂ©es, comme le support multi-GPU, la trace de la progression et des images dâexemple, la sauvegarde du gradient pour supporter des tailles de batch plus importantes, le tĂ©lĂ©chargement automatique des modĂšles et ainsi de suite. Heureusement, la plupart de ces fonctionnalitĂ©s sont disponibles dans lâexemple de script dâentraĂźnement ici..
Vous pouvez télécharger le fichier comme suit :
!wget https://github.com/huggingface/diffusers/raw/main/examples/unconditional_image_generation/train_unconditional.py
Ouvrez le fichier et vous verrez oĂč le modĂšle est dĂ©fini et quels sont les paramĂštres disponibles. Nous exĂ©cutons le script Ă lâaide de la commande suivante :
# Donnons un nom Ă notre nouveau modĂšle pour le Hub
model_name = "sd-class-butterflies-64"
hub_model_id = get_full_repo_name(model_name)
!accelerate launch train_unconditional.py \
--dataset_name="huggan/smithsonian_butterflies_subset" \
--resolution=64 \
--output_dir={model_name} \
--train_batch_size=32 \
--num_epochs=50 \
--gradient_accumulation_steps=1 \
--learning_rate=1e-4 \
--lr_warmup_steps=500 \
--mixed_precision="no"
Comme prĂ©cĂ©demment, poussons le modĂšle vers le Hub et crĂ©ons une belle carte de modĂšle (et nâhĂ©sitez pas Ă lâĂ©diter comme vous le souhaitez !):
create_repo(hub_model_id)
api = HfApi()
api.upload_folder(
folder_path=f"{model_name}/scheduler", path_in_repo="", repo_id=hub_model_id
)
api.upload_folder(
folder_path=f"{model_name}/unet", path_in_repo="", 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,
)
content = f"""
---
license: mit
tags:
- pytorch
- diffusers
- unconditional-image-generation
- diffusion-models-class
---
# Model Card for Unit 1 of the [Diffusion Models Class đ§š](https://github.com/huggingface/diffusion-models-class)
This model is a diffusion model for unconditional image generation of cute đŠ.
## Usage
```python
from diffusers import DDPMPipeline
pipeline = DDPMPipeline.from_pretrained('{hub_model_id}')
image = pipeline().images[0]
image
```python
"""
card = ModelCard(content)
card.push_to_hub(hub_model_id)
Environ 45 minutes plus tard, voici le résultat :
pipeline = DDPMPipeline.from_pretrained(hub_model_id).to(device)
images = pipeline(batch_size=8).images
make_grid(images)
âïž Ă votre tour ! Essayez de trouver des paramĂštres dâentraĂźnement/de modĂšle qui donnent de bons rĂ©sultats en un minimum de temps, et partagez vos rĂ©sultats avec la communautĂ©. Fouillez dans le script pour voir si vous pouvez comprendre le code, et demandez des Ă©claircissements sur tout ce qui vous semble confus.
Pistes pour approndonfir
Nous espĂ©rons vous avoir donnĂ© un avant-goĂ»t de ce que vous pouvez faire avec la bibliothĂšque đ€ Diffusers ! Voici quelques pistes possibles pour la suite :
- Essayez dâentraĂźner un modĂšle de diffusion inconditionnel sur un nouveau jeu de donnĂ©es. Points bonus si vous en crĂ©ez un vous-mĂȘme. Vous pouvez trouver dâexcellents jeux de donnĂ©es dâimages pour cette tĂąche dans lâorganisation HugGan sur le Hub. Assurez-vous simplement de les sous-Ă©chantillonner si vous ne voulez pas attendre trĂšs longtemps pour que le modĂšle sâentraĂźne !
- Essayez DreamBooth pour créer votre propre pipeline de Stable Diffusion personnalisé en utilisant ce Space ou ce notebook.
- Modifiez le script dâentraĂźnement pour explorer diffĂ©rents hyperparamĂštres UNet (nombre de couches, canaux, etc.), diffĂ©rents schĂ©mas de bruit, etc.
- Consultez le notebook Implémentation à partir de 0 pour une approche différente des idées fondamentales que nous avons abordées dans cette unité.
1.2. Implémentation à partir de 0
Il est parfois utile de considĂ©rer la version la plus simple possible dâune chose pour mieux en comprendre le fonctionnement. Câest ce que nous allons essayer de faire dans ce notebook, en commençant par un modĂšle de diffusion jouet pour voir comment les diffĂ©rents Ă©lĂ©ments fonctionnent, puis en examinant en quoi ils diffĂšrent dâune mise en Ćuvre plus complexe.
Nous examinerons :
- Le processus de corruption (ajouter du bruit aux données)
- Ce quâest un UNet, et comment en implĂ©menter un extrĂȘmement minimal Ă partir de zĂ©ro
- LâentraĂźnement au modĂšle de diffusion
- La thĂ©orie de lâĂ©chantillonnage
Ensuite, nous comparerons nos versions avec lâimplĂ©mentation DDPM des diffuseurs, en explorant :
- Les améliorations par rapport à notre mini UNet
- Le schéma de bruit du DDPM
- Les diffĂ©rences dans lâobjectif dâentraĂźnement
- Le conditionnement du pas de temps
- Les approches dâĂ©chantillonnage
Ce notebook est assez approfondi, et peut ĂȘtre sautĂ© en toute sĂ©curitĂ© si vous nâĂȘtes pas enthousiaste Ă lâidĂ©e dâune plongĂ©e en profondeur Ă partir de zĂ©ro !
Il convient Ă©galement de noter que la plupart du code ici est utilisĂ© Ă des fins dâillustration, et nous ne recommandons pas de lâadopter directement pour votre propre travail (Ă moins que vous nâessayiez dâamĂ©liorer les exemples montrĂ©s ici Ă des fins dâapprentissage).
Configuration et importations
!pip install -q diffusers
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
from diffusers import DDPMScheduler, UNet2DModel
from matplotlib import pyplot as plt
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f'Using device: {device}')
Les données
Nous allons tester les choses avec un trĂšs petit jeu de donnĂ©es : MNIST. Si vous souhaitez donner au modĂšle un dĂ©fi un peu plus difficile Ă relever sans rien changer dâautre, torchvision.datasets.FashionMNIST devrait faire lâaffaire.
dataset = torchvision.datasets.MNIST(root="mnist/", train=True, download=True, transform=torchvision.transforms.ToTensor())
train_dataloader = DataLoader(dataset, batch_size=8, shuffle=True)
x, y = next(iter(train_dataloader))
print('Input shape:', x.shape)
print('Labels:', y)
plt.imshow(torchvision.utils.make_grid(x)[0], cmap='Greys')
Input shape: torch.Size([8, 1, 28, 28])
Labels: tensor([1, 9, 7, 3, 5, 2, 1, 4])
Chaque image est un dessin en niveaux de gris de 28 par 28 pixels dâun chiffre, avec des valeurs allant de 0 Ă 1.
Le processus de corruption
Supposons que vous nâayez lu aucun papier sur les modĂšles de diffusion, mais que vous sachiez que le processus implique lâajout de bruit. Comment feriez-vous ?
Nous souhaitons probablement disposer dâun moyen simple de contrĂŽler le degrĂ© de corruption. Et si nous prenions un paramĂštre pour la quantitĂ© de bruit Ă ajouter, et que nous le faisions :
noise = torch.rand_like(x)
noisy_x = (1-amount)*x + amount*noise
Si amount = 0, nous rĂ©cupĂ©rons lâentrĂ©e sans aucun changement. Si le montant atteint $1$, nous rĂ©cupĂ©rons du bruit sans aucune trace de lâentrĂ©e $x$. En mĂ©langeant lâentrĂ©e avec du bruit de cette façon, nous gardons la sortie dans la mĂȘme plage ($0$ Ă $1$).
Nous pouvons mettre cela en Ćuvre assez facilement (il suffit de surveiller les formes pour ne pas se faire piĂ©ger par les rĂšgles de diffusion) :
def corrupt(x, amount):
"""Corrompre l'entrée `x` en la mélangeant avec du bruit selon `amount`"""
noise = torch.rand_like(x)
amount = amount.view(-1, 1, 1, 1) # Trier les formes pour que la transmission fonctionne
return x*(1-amount) + noise*amount
Et regarder les résultats visuellement pour voir que cela fonctionne comme prévu :
# Tracer les données d'entrée
fig, axs = plt.subplots(2, 1, figsize=(12, 5))
axs[0].set_title('Input data')
axs[0].imshow(torchvision.utils.make_grid(x)[0], cmap='Greys')
# Ajouter du bruit
amount = torch.linspace(0, 1, x.shape[0]) # De gauche Ă droite -> plus de corruption
noised_x = corrupt(x, amount)
# Tracé de la version bruitée
axs[1].set_title('Corrupted data (-- amount increases -->)')
axs[1].imshow(torchvision.utils.make_grid(noised_x)[0], cmap='Greys')
Lorsque la quantitĂ© de bruit sâapproche de 1, nos donnĂ©es commencent Ă ressembler Ă du bruit alĂ©atoire pur. Mais pour la plupart des noise_amounts, vous pouvez deviner le chiffre assez bien. Pensez-vous que cela soit optimal ?
Le modĂšle
Nous aimerions un modĂšle qui prenne en compte des images bruitĂ©es de 28px et qui produise une prĂ©diction de la mĂȘme forme. Un choix populaire ici est une architecture appelĂ©e UNet. InventĂ© Ă lâorigine pour les tĂąches de segmentation en imagerie mĂ©dicale, un UNet se compose dâun âchemin de compressionâ par lequel les donnĂ©es sont comprimĂ©es et dâun âchemin dâexpansionâ par lequel elles sâĂ©tendent Ă nouveau jusquâĂ la dimension dâorigine (similaire Ă un autoencodeur), mais il comporte Ă©galement des connexions de saut qui permettent aux informations et aux gradients de circuler Ă diffĂ©rents niveaux.
Certains UNets comportent des blocs complexes Ă chaque Ă©tape, mais pour cette petite dĂ©monstration, nous construirons un exemple minimal qui prend une image Ă un canal et la fait passer par trois couches convolutives sur le chemin descendant (les down_layers dans le diagramme et le code) et trois sur le chemin ascendant, avec des sauts de connexion entre les couches descendantes et ascendantes. Nous utiliserons max pooling pour le downsampling et nn.Upsample pour le upsampling plutĂŽt que de nous appuyer sur des couches apprenantes comme les UNets plus complexes. Voici lâarchitecture approximative montrant le nombre de canaux dans la sortie de chaque couche :
Voici Ă quoi cela ressemble dans le code :
class BasicUNet(nn.Module):
"""Une mise en Ćuvre minimale du UNet"""
def __init__(self, in_channels=1, out_channels=1):
super().__init__()
self.down_layers = torch.nn.ModuleList([
nn.Conv2d(in_channels, 32, kernel_size=5, padding=2),
nn.Conv2d(32, 64, kernel_size=5, padding=2),
nn.Conv2d(64, 64, kernel_size=5, padding=2),
])
self.up_layers = torch.nn.ModuleList([
nn.Conv2d(64, 64, kernel_size=5, padding=2),
nn.Conv2d(64, 32, kernel_size=5, padding=2),
nn.Conv2d(32, out_channels, kernel_size=5, padding=2),
])
self.act = nn.SiLU() # La fonction d'activation
self.downscale = nn.MaxPool2d(2)
self.upscale = nn.Upsample(scale_factor=2)
def forward(self, x):
h = []
for i, l in enumerate(self.down_layers):
x = self.act(l(x)) # Ă travers la couche et la fonction d'activation
if i < 2: # Pour toutes les couches sauf la troisiĂšme (derniĂšre) :
h.append(x) # Stockage de la sortie pour la skip connexion
x = self.downscale(x) # Réduction d'échelle pour la couche suivante
for i, l in enumerate(self.up_layers):
if i > 0:
x = self.upscale(x) # Upscale
x += h.pop() # Récupération d'un résultat stocké (skip connection)
x = self.act(l(x)) # Par le biais de la couche et de la fonction d'activation
return x
Nous pouvons vĂ©rifier que la forme de la sortie est la mĂȘme que celle de lâentrĂ©e, comme nous nous y attendions :
net = BasicUNet()
x = torch.rand(8, 1, 28, 28)
net(x).shape
torch.Size([8, 1, 28, 28])
Ce réseau compte un peu plus de 300 000 paramÚtres :
sum([p.numel() for p in net.parameters()])
309057
Vous pouvez envisager de modifier le nombre de canaux dans chaque couche ou dâintervertir les architectures si vous le souhaitez.
Entraßner le réseau
Que doit faire exactement le modĂšle ? LĂ encore, il y a plusieurs façons de procĂ©der, mais pour cette dĂ©monstration, choisissons un cadre simple : Ă©tant donnĂ© une entrĂ©e corrompue noisy_x, le modĂšle doit produire sa meilleure estimation de ce Ă quoi ressemble lâoriginal $x$. Nous comparerons cette valeur Ă la valeur rĂ©elle par le biais de lâerreur quadratique moyenne. Nous comparerons cette estimation Ă la valeur rĂ©elle par le biais de lâerreur quadratique moyenne.
Nous pouvons maintenant entraßner le réseau.
- Obtenir un batch de données
- Corrompre les données de maniÚre aléatoire
- Nourrir le modÚle avec ces données
- Comparer les prédictions du modÚle avec les images propres pour calculer notre perte
- Mettre à jour les paramÚtres du modÚle en conséquence.
NâhĂ©sitez pas Ă modifier ce modĂšle et Ă voir si vous pouvez lâamĂ©liorer !
# Chargeur de données (vous pouvez modifier la taille des batchs)
batch_size = 128
train_dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# Combien de fois devrions-nous passer les données en revue ?
n_epochs = 3
# Créer le réseau
net = BasicUNet()
net.to(device)
# Notre fonction de perte
loss_fn = nn.MSELoss()
# L'optimiseur
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
# Conserver une trace des pertes pour les consulter ultérieurement
losses = []
# La boucle d'entraĂźnement
for epoch in range(n_epochs):
for x, y in train_dataloader:
# Obtenir des données et préparer la version corrompue
x = x.to(device) # Data on the GPU
noise_amount = torch.rand(x.shape[0]).to(device) # Pick random noise amounts
noisy_x = corrupt(x, noise_amount) # Create our noisy x
# Obtenir la prédiction du modÚle
pred = net(noisy_x)
# Calculer la perte
loss = loss_fn(pred, x) # Dans quelle mesure la sortie est-elle proche du véritable x "propre" ?
# Rétropropager et mettre à jour les paramÚtres
opt.zero_grad()
loss.backward()
opt.step()
# Stocker la perte pour plus tard
losses.append(loss.item())
# Afficher la moyenne des valeurs de perte pour cette époque :
avg_loss = sum(losses[-len(train_dataloader):])/len(train_dataloader)
print(f'Finished epoch {epoch}. Average loss for this epoch: {avg_loss:05f}')
# Visualiser la courbe des pertes
plt.plot(losses)
plt.ylim(0, 0.1)
Finished epoch 0. Average loss for this epoch: 0.026736
Finished epoch 1. Average loss for this epoch: 0.020692
Finished epoch 2. Average loss for this epoch: 0.018887
Nous pouvons essayer de voir à quoi ressemblent les prédictions du modÚle en saisissant un batch de données, en les corrompant à différents degrés et en visualisant ensuite les prédictions du modÚle :
# Récupérer des données
x, y = next(iter(train_dataloader))
x = x[:8] # Seuls les 8 premiers sont utilisés pour faciliter le graphique
# Corruption avec une échelle de montants
amount = torch.linspace(0, 1, x.shape[0]) # De gauche Ă droite -> plus de corruption
noised_x = corrupt(x, amount)
# Obtenir les prédictions du modÚle
with torch.no_grad():
preds = net(noised_x.to(device)).detach().cpu()
# Graphique
fig, axs = plt.subplots(3, 1, figsize=(12, 7))
axs[0].set_title('Input data')
axs[0].imshow(torchvision.utils.make_grid(x)[0].clip(0, 1), cmap='Greys')
axs[1].set_title('Corrupted data')
axs[1].imshow(torchvision.utils.make_grid(noised_x)[0].clip(0, 1), cmap='Greys')
axs[2].set_title('Network Predictions')
axs[2].imshow(torchvision.utils.make_grid(preds)[0].clip(0, 1), cmap='Greys
Vous pouvez constater que pour les montants les plus faibles, les prĂ©dictions sont plutĂŽt bonnes ! Mais lorsque le niveau devient trĂšs Ă©levĂ©, le modĂšle a moins dâĂ©lĂ©ments pour travailler, et lorsque nous arrivons Ă amount=1, il produit un dĂ©sordre flou proche de la moyenne du jeu de donnĂ©es pour essayer de couvrir ses paris sur ce Ă quoi la sortie pourrait ressemblerâŠ
Ăchantillonnage
Si nos prédictions à des niveaux de bruit élevés ne sont pas trÚs bonnes, comment générer des images ?
Et si nous partions dâun bruit alĂ©atoire, que nous regardions les prĂ©dictions du modĂšle, mais que nous ne nous rapprochions que trĂšs peu de cette prĂ©diction (disons, 20 % du chemin). Nous disposons alors dâune image trĂšs bruyante dans laquelle il y a peut-ĂȘtre un soupçon de structure, que nous pouvons introduire dans le modĂšle pour obtenir une nouvelle prĂ©diction. Nous espĂ©rons que cette nouvelle prĂ©diction est lĂ©gĂšrement meilleure que la premiĂšre (puisque notre point de dĂ©part est lĂ©gĂšrement moins bruitĂ©) et que nous pouvons donc faire un autre petit pas avec cette nouvelle et meilleure prĂ©diction.
Nous rĂ©pĂ©tons lâopĂ©ration plusieurs fois et (si tout se passe bien) nous obtenons une image ! Voici ce processus illustrĂ© en seulement 5 Ă©tapes, en visualisant lâentrĂ©e du modĂšle (Ă gauche) et les images dĂ©bruitĂ©es prĂ©dites (Ă droite) Ă chaque Ă©tape. Notez que mĂȘme si le modĂšle prĂ©dit lâimage dĂ©bruitĂ©e dĂšs lâĂ©tape 1, nous ne faisons quâune partie du chemin. Au fil des Ă©tapes, les structures apparaissent et sont affinĂ©es, jusquâĂ ce que nous obtenions nos rĂ©sultats finaux.
n_steps = 5
x = torch.rand(8, 1, 28, 28).to(device) # Commencer au hasard
step_history = [x.detach().cpu()]
pred_output_history = []
for i in range(n_steps):
with torch.no_grad(): # Pas besoin de suivre les gradients pendant l'inférence
pred = net(x) # Prédire le x0 débruité
pred_output_history.append(pred.detach().cpu()) # Stocker les résultats du modÚle pour les tracer
mix_factor = 1/(n_steps - i) # Dans quelle mesure nous nous rapprochons de la prédiction
x = x*(1-mix_factor) + pred*mix_factor # Déplacer une partie du chemin
step_history.append(x.detach().cpu()) # Stocker l'étape pour le graphique
fig, axs = plt.subplots(n_steps, 2, figsize=(9, 4), sharex=True)
axs[0,0].set_title('x (model input)')
axs[0,1].set_title('model prediction')
for i in range(n_steps):
axs[i, 0].imshow(torchvision.utils.make_grid(step_history[i])[0].clip(0, 1), cmap='Greys')
axs[i, 1].imshow(torchvision.utils.make_grid(pred_output_history[i])[0].clip(0, 1), cmap='Greys')
Nous pouvons diviser le processus en plusieurs étapes et espérer ainsi obtenir de meilleures images :
n_steps = 40
x = torch.rand(64, 1, 28, 28).to(device)
for i in range(n_steps):
noise_amount = torch.ones((x.shape[0], )).to(device) * (1-(i/n_steps)) # Starting high going low
with torch.no_grad():
pred = net(x)
mix_factor = 1/(n_steps - i)
x = x*(1-mix_factor) + pred*mix_factor
fig, ax = plt.subplots(1, 1, figsize=(12, 12))
ax.imshow(torchvision.utils.make_grid(x.detach().cpu(), nrow=8)[0].clip(0, 1), cmap='Greys')
Ce nâest pas gĂ©nial, mais il y a des chiffres reconnaissables ! Vous pouvez expĂ©rimenter en entraĂźnant plus longtemps (disons, 10 ou 20 Ă©poques) et en modifiant la configuration du modĂšle, le taux dâapprentissage, lâoptimiseur, etc. Nâoubliez pas non plus que fashionMNIST peut ĂȘtre remplacĂ© en une ligne si vous voulez essayer un jeu de donnĂ©es un peu plus difficile.
Comparaison avec DDPM
Dans cette section, nous allons voir comment notre implĂ©mentation diffĂšre de lâapproche utilisĂ©e dans lâautre notebook (Introduction Ă Diffusers), qui est basĂ© sur lâarticle de DDPM.
Nous verrons que
- Le diffuseur
UNet2DModelest un peu plus avancé que notre BasicUNet - Le processus de corruption est traité différemment
- Lâobjectif dâentraĂźnement est diffĂ©rent, puisquâil sâagit de prĂ©dire le bruit plutĂŽt que lâimage dĂ©bruitĂ©e.
- Le modĂšle est conditionnĂ© sur la quantitĂ© de bruit prĂ©sent via un conditionnement par pas de temps, oĂč t est transmis comme un argument supplĂ©mentaire Ă la mĂ©thode forward.
- Il existe un certain nombre de stratĂ©gies dâĂ©chantillonnage diffĂ©rentes, qui devraient fonctionner mieux que notre version simpliste ci-dessus.
Un certain nombre dâamĂ©liorations ont Ă©tĂ© suggĂ©rĂ©es depuis la publication de lâarticle sur le DDPM, mais nous espĂ©rons que cet exemple est instructif en ce qui concerne les diffĂ©rentes dĂ©cisions de conception possibles. Une fois que vous aurez lu cet article, vous pourrez vous plonger dans le document intitulĂ© Elucidating the Design Space of Diffusion-Based Generative Models qui examine tous ces composants en dĂ©tail et formule de nouvelles recommandations sur la maniĂšre dâobtenir les meilleures performances.
Si tout cela est trop technique ou intimidant, ne vous inquiĂ©tez pas ! NâhĂ©sitez pas Ă sauter le reste de ce notebook ou Ă le garder pour un jour de pluie.
LâUNet
Le modĂšle UNet2DModel de đ€ Diffusers comporte un certain nombre dâamĂ©liorations par rapport Ă notre UNet de base ci-dessus :
- GroupNorm applique une normalisation par groupe aux entrées de chaque bloc
- Couches de dropout pour un entraĂźnement plus doux
- Plusieurs couches de ResNet par bloc (si layers_per_block nâest pas fixĂ© Ă 1)
- Attention (généralement utilisé uniquement pour les blocs à faible résolution)
- Conditionnement sur le pas de temps
- Blocs de sous-Ă©chantillonnage et de surĂ©chantillonnage avec des paramĂštres pouvant ĂȘtre appris
Créons et inspectons un modÚle UNet2DModel :
model = UNet2DModel(
sample_size=28, # la résolution de l'image cible
in_channels=1, # le nombre de canaux d'entrée, 3 pour les images RVB
out_channels=1, # le nombre de canaux de sortie
layers_per_block=2, # le nombre de couches ResNet Ă utiliser par bloc UNet
block_out_channels=(32, 64, 64), # Correspondant Ă peu prĂšs Ă notre exemple UNet de base
down_block_types=(
"DownBlock2D", # un bloc de sous-échantillonnage ResNet normal
"AttnDownBlock2D", # un bloc de sous-échantillonnage ResNet avec auto-attention spatiale
"AttnDownBlock2D",
),
up_block_types=(
"AttnUpBlock2D",
"AttnUpBlock2D", # un bloc de suréchantillonnage ResNet avec auto-attention spatiale
"UpBlock2D", # un bloc de suréchantillonnage ResNet standard
),
)
print(model)
Afficher / masquer la sortie de print(model)
UNet2DModel(
(conv_in): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_proj): Timesteps()
(time_embedding): TimestepEmbedding(
(linear_1): Linear(in_features=32, out_features=128, bias=True)
(act): SiLU()
(linear_2): Linear(in_features=128, out_features=128, bias=True)
)
(down_blocks): ModuleList(
(0): DownBlock2D(
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 32, eps=1e-05, affine=True)
(conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
(norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 32, eps=1e-05, affine=True)
(conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
(norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
)
(downsamplers): ModuleList(
(0): Downsample2D(
(conv): Conv2d(32, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
)
)
)
(1): AttnDownBlock2D(
(attentions): ModuleList(
(0): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
(1): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
)
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 32, eps=1e-05, affine=True)
(conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(32, 64, kernel_size=(1, 1), stride=(1, 1))
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
)
(downsamplers): ModuleList(
(0): Downsample2D(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
)
)
)
(2): AttnDownBlock2D(
(attentions): ModuleList(
(0): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
(1): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
)
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
)
)
)
(up_blocks): ModuleList(
(0): AttnUpBlock2D(
(attentions): ModuleList(
(0): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
(1): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
(2): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
)
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
(conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
(conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
)
(2): ResnetBlock2D(
(norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
(conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
)
)
(upsamplers): ModuleList(
(0): Upsample2D(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
)
)
(1): AttnUpBlock2D(
(attentions): ModuleList(
(0): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
(1): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
(2): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
)
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
(conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 128, eps=1e-05, affine=True)
(conv1): Conv2d(128, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1))
)
(2): ResnetBlock2D(
(norm1): GroupNorm(32, 96, eps=1e-05, affine=True)
(conv1): Conv2d(96, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(96, 64, kernel_size=(1, 1), stride=(1, 1))
)
)
(upsamplers): ModuleList(
(0): Upsample2D(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
)
)
(2): UpBlock2D(
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 96, eps=1e-05, affine=True)
(conv1): Conv2d(96, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
(norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(96, 32, kernel_size=(1, 1), stride=(1, 1))
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
(norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(64, 32, kernel_size=(1, 1), stride=(1, 1))
)
(2): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=32, bias=True)
(norm2): GroupNorm(32, 32, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
(conv_shortcut): Conv2d(64, 32, kernel_size=(1, 1), stride=(1, 1))
)
)
)
)
(mid_block): UNetMidBlock2D(
(attentions): ModuleList(
(0): AttentionBlock(
(group_norm): GroupNorm(32, 64, eps=1e-05, affine=True)
(query): Linear(in_features=64, out_features=64, bias=True)
(key): Linear(in_features=64, out_features=64, bias=True)
(value): Linear(in_features=64, out_features=64, bias=True)
(proj_attn): Linear(in_features=64, out_features=64, bias=True)
)
)
(resnets): ModuleList(
(0): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
(1): ResnetBlock2D(
(norm1): GroupNorm(32, 64, eps=1e-05, affine=True)
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(time_emb_proj): Linear(in_features=128, out_features=64, bias=True)
(norm2): GroupNorm(32, 64, eps=1e-05, affine=True)
(dropout): Dropout(p=0.0, inplace=False)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(nonlinearity): SiLU()
)
)
)
(conv_norm_out): GroupNorm(32, 32, eps=1e-05, affine=True)
(conv_act): SiLU()
(conv_out): Conv2d(32, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
Comme vous pouvez le constater, il y a un peu plus de choses qui se passent ! Il a également beaucoup plus de paramÚtres que notre BasicUNet :
sum([p.numel() for p in model.parameters()]) # 1,7M contre les ~309k paramĂštres du BasicUNet
1707009
Nous pouvons reproduire lâentraĂźnement prĂ©sentĂ© ci-dessus en utilisant ce modĂšle Ă la place de notre modĂšle original. Nous devons passer x et le pas de temps au modĂšle (ici, nous passons toujours t=0 pour montrer quâil fonctionne sans ce conditionnement de pas de temps et pour faciliter le code dâĂ©chantillonnage, mais vous pouvez Ă©galement essayer dâintroduire (amount*1000) pour obtenir un Ă©quivalent de pas de temps Ă partir du montant de la corruption). Les lignes modifiĂ©es sont indiquĂ©es par #<<< si vous souhaitez inspecter le code.
# Dataloader (vous pouvez modifier la taille du batch)
batch_size = 128
train_dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# Combien de fois devrions-nous passer les données en revue ?
n_epochs = 3
# Créer le réseau
net = UNet2DModel(
sample_size=28, # la résolution de l'image cible
in_channels=1, # le nombre de canaux d'entrée, 3 pour les images RVB
out_channels=1, # le nombre de canaux de sortie
layers_per_block=2, # le nombre de couches ResNet Ă utiliser par bloc UNet
block_out_channels=(32, 64, 64), # Correspondant Ă peu prĂšs Ă notre exemple UNet de base
down_block_types=(
"DownBlock2D", # un bloc de sous-échantillonnage ResNet normal
"AttnDownBlock2D", # un bloc de sous-échantillonnage ResNet avec auto-attention spatiale
"AttnDownBlock2D",
),
up_block_types=(
"AttnUpBlock2D",
"AttnUpBlock2D", # un bloc de suréchantillonnage ResNet avec auto-attention spatiale
"UpBlock2D", # un bloc de suréchantillonnage ResNet standard
),
)
net.to(device)
# Notre protection contre la perte
loss_fn = nn.MSELoss()
# L'optimiseur
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
# Conserver une trace des pertes pour les visualiser plus tard
losses = []
# La boucle d'entraĂźnement
for epoch in range(n_epochs):
for x, y in train_dataloader:
# Obtenir des données et préparer la version corrompue
x = x.to(device) # Data on the GPU
noise_amount = torch.rand(x.shape[0]).to(device) # Choisir des quantités de bruit aléatoires
noisy_x = corrupt(x, noise_amount) # Créer notre bruit x
# Obtenir la prédiction du modÚle
pred = net(noisy_x, 0).sample #<<< En utilisant toujours le pas de temps 0, en ajoutant .sample
# Calculer la perte
loss = loss_fn(pred, x) # Dans quelle mesure la sortie est-elle proche du véritable x "propre" ?
# Rétropropager et mettre à jour les paramÚtres
opt.zero_grad()
loss.backward()
opt.step()
# Stocker la perte pour plus tard
losses.append(loss.item())
# Afficher la moyenne des valeurs de perte pour cette époque :
avg_loss = sum(losses[-len(train_dataloader):])/len(train_dataloader)
print(f'Finished epoch {epoch}. Average loss for this epoch: {avg_loss:05f}')
# Graphique
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
# Perte
axs[0].plot(losses)
axs[0].set_ylim(0, 0.1)
axs[0].set_title('Loss over time')
# Ăchantillons
n_steps = 40
x = torch.rand(64, 1, 28, 28).to(device)
for i in range(n_steps):
noise_amount = torch.ones((x.shape[0], )).to(device) * (1-(i/n_steps)) # De haut en bas
with torch.no_grad():
pred = net(x, 0).sample
mix_factor = 1/(n_steps - i)
x = x*(1-mix_factor) + pred*mix_factor
axs[1].imshow(torchvision.utils.make_grid(x.detach().cpu(), nrow=8)[0].clip(0, 1), cmap='Greys')
axs[1].set_title('Generated Samples')
Finished epoch 0. Average loss for this epoch: 0.018925
Finished epoch 1. Average loss for this epoch: 0.012785
Finished epoch 2. Average loss for this epoch: 0.011694
Ces rĂ©sultats sont bien meilleurs que notre premiĂšre sĂ©rie de rĂ©sultats ! Vous pouvez envisager de modifier la configuration du Unet ou de prolonger lâentraĂźnement afin dâobtenir des performances encore meilleures.
Le processus de corruption
Le papier DDPM dĂ©crit un processus de corruption qui ajoute une petite quantitĂ© de bruit Ă chaque « pas de temps ». Ătant donnĂ© $x_{t-1}$ pour un certain pas de temps, nous pouvons obtenir la version suivante (lĂ©gĂšrement plus bruitĂ©e) $x_t$ avec :
\[\begin{aligned} q(\mathbf{x}_t \vert \mathbf{x}_{t-1}) &= \mathcal{N}(\mathbf{x}_t; \sqrt{1 - \beta_t} \mathbf{x}_{t-1}, \beta_t\mathbf{I}) \\ q(\mathbf{x}_{1:T} \vert \mathbf{x}_0) &= \prod^T_{t=1} q(\mathbf{x}_t \vert \mathbf{x}_{t-1}) \end{aligned}\]Nous prenons $x_{t-1}$, lâĂ©chelonnons de $\sqrt{1 - \beta_t}$ et ajoutons du bruit Ă©chelonnĂ© de $\beta_t$.
Ce $\beta$ est dĂ©fini pour chaque t en fonction dâun certain planificateur, et dĂ©termine la quantitĂ© de bruit ajoutĂ©e par pas de temps.
Nous ne voulons pas nĂ©cessairement faire cette opĂ©ration 500 fois pour obtenir $x_{500}$, nous avons donc une autre formule pour obtenir $x_t$ pour nâimporte quel t Ă©tant donnĂ© $x_0$ :
oĂč :
\[\bar{\alpha}_t = \prod_{i=1}^{T} \alpha_i,\quad \alpha_i = 1 - \beta_i\]noise_scheduler = DDPMScheduler(num_train_timesteps=1000)
plt.plot(noise_scheduler.alphas_cumprod.cpu() ** 0.5, label=r"${\sqrt{\bar{\alpha}_t}}$")
plt.plot((1 - noise_scheduler.alphas_cumprod.cpu()) ** 0.5, label=r"$\sqrt{(1 - \bar{\alpha}_t)}$")
plt.legend(fontsize="x-large")
Au départ, le $x$ bruité est principalement $x$ (sqrt_alpha_prod ~= 1), mais au fil du temps, la contribution de $x$ diminue et la composante bruit augmente. Contrairement à notre mélange linéaire de $x$ et de bruit en fonction de la quantité, celui-ci devient bruyant relativement rapidement. Nous pouvons visualiser cela sur quelques données :
# Bruit d'un batch d'images pour visualiser l'effet
fig, axs = plt.subplots(3, 1, figsize=(16, 10))
xb, yb = next(iter(train_dataloader))
xb = xb.to(device)[:8]
xb = xb * 2. - 1. # Pour aller dans (-1, 1)
print('X shape', xb.shape)
# Afficher les entrées propres
axs[0].imshow(torchvision.utils.make_grid(xb[:8])[0].detach().cpu(), cmap='Greys')
axs[0].set_title('Clean X')
# Ajouter du bruit avec le planificateur
timesteps = torch.linspace(0, 999, 8).long().to(device)
noise = torch.randn_like(xb) # << NB: randn et non rand
noisy_xb = noise_scheduler.add_noise(xb, noise, timesteps)
print('Noisy X shape', noisy_xb.shape)
# Afficher la version bruyante (avec et sans coupure)
axs[1].imshow(torchvision.utils.make_grid(noisy_xb[:8])[0].detach().cpu().clip(-1, 1), cmap='Greys')
axs[1].set_title('Noisy X (clipped to (-1, 1)')
axs[2].imshow(torchvision.utils.make_grid(noisy_xb[:8])[0].detach().cpu(), cmap='Greys')
axs[2].set_title('Noisy X')
X shape torch.Size([8, 1, 28, 28])
Noisy X shape torch.Size([8, 1, 28, 28])
Une autre dynamique est en jeu : la version DDPM ajoute un bruit tirĂ© dâune distribution gaussienne (moyenne 0, Ă©cart-type 1 de torch.randn) plutĂŽt que le bruit uniforme entre 0 et 1 (de torch.rand) que nous avons utilisĂ© dans notre fonction corrompue dâorigine. En gĂ©nĂ©ral, il est judicieux de normaliser Ă©galement les donnĂ©es dâentraĂźnement. Dans lâautre notebook, vous verrez Normalize(0.5, 0.5) dans la liste des transformations, qui fait correspondre les donnĂ©es de lâimage de (0, 1) Ă (-1, 1) et qui est âsuffisanteâ pour nos besoins. Nous ne lâavons pas fait pour ce notebook, mais la cellule de visualisation ci-dessus lâajoute pour une mise Ă lâĂ©chelle et une visualisation plus prĂ©cises.
Objectif dâentraĂźnement
Dans notre exemple, le modĂšle tente de prĂ©dire lâimage dĂ©bruitĂ©e. Dans le DDPM et dans de nombreuses autres implĂ©mentations de modĂšles de diffusion, le modĂšle prĂ©dit le bruit utilisĂ© dans le processus de corruption (avant la mise Ă lâĂ©chelle, donc un bruit Ă variance unitaire). Dans le code, cela ressemble Ă quelque chose comme :
noise = torch.randn_like(xb) # << NB: randn et non rand
noisy_x = noise_scheduler.add_noise(x, noise, timesteps)
model_prediction = model(noisy_x, timesteps).sample
loss = mse_loss(model_prediction, noise) # le bruit comme cible
Vous pouvez penser que prĂ©dire le bruit (Ă partir duquel nous pouvons dĂ©duire Ă quoi ressemble lâimage dĂ©bruitĂ©e) est Ă©quivalent Ă prĂ©dire directement lâimage dĂ©bruitĂ©e. Alors pourquoi privilĂ©gier lâune plutĂŽt que lâautre : est-ce simplement pour des raisons de commoditĂ© mathĂ©matique ?
Il sâavĂšre quâil existe une autre subtilitĂ©. Nous calculons la perte Ă diffĂ©rents moments (choisis au hasard) au cours de lâentraĂźnement. Ces diffĂ©rents objectifs conduiront Ă une âpondĂ©ration impliciteâ diffĂ©rente de ces pertes, oĂč la prĂ©diction du bruit donne plus de poids aux niveaux de bruit plus faibles. Vous pouvez choisir des objectifs plus complexes pour modifier cette âpondĂ©ration implicite des pertesâ. Vous pouvez aussi choisir un calendrier de bruit qui donnera plus dâexemples Ă un niveau de bruit plus Ă©levĂ©. Vous pouvez demander au modĂšle de prĂ©dire une âvitesseâ v, que nous dĂ©finissons comme une combinaison de lâimage et du bruit dĂ©pendant du niveau de bruit (voir Progressive Distillation for Fast Sampling of Diffusion Models). Il se peut que le modĂšle prĂ©dise le bruit, mais quâil rĂ©duise ensuite la perte en fonction dâun facteur dĂ©pendant de la quantitĂ© de bruit, sur la base dâun peu de thĂ©orie (voir Perception Prioritized Training of Diffusion Models) ou dâexpĂ©riences visant Ă dĂ©terminer quels niveaux de bruit sont les plus informatifs pour le modĂšle (voir Elucidating the Design Space of Diffusion-Based Generative Models). En rĂ©sumĂ© : le choix de lâobjectif a un effet sur les performances du modĂšle, et des recherches sont en cours pour dĂ©terminer la âmeilleureâ option.
Pour lâinstant, la prĂ©diction du bruit (epsilon ou eps) est lâapproche privilĂ©giĂ©e, mais avec le temps, nous verrons probablement dâautres objectifs pris en charge dans la bibliothĂšque et utilisĂ©s dans diffĂ©rentes situations.
Conditionnement du pas de temps
Le modÚle UNet2DModel prend en compte à la fois x et le pas de temps. Ce dernier est transformé en intégration et introduit dans le modÚle à plusieurs endroits.
La thĂ©orie sous-jacente est quâen donnant au modĂšle des informations sur le niveau de bruit, il peut mieux accomplir sa tĂąche. Bien quâil soit possible dâentraĂźner un modĂšle sans ce conditionnement du pas de temps, cela semble amĂ©liorer les performances dans certains cas et la plupart des implĂ©mentations lâincluent, du moins dans la littĂ©rature actuelle.
Ăchantillonnage
Ătant donnĂ© un modĂšle qui estime le bruit prĂ©sent dans une entrĂ©e bruyante (ou qui prĂ©dit la version dĂ©bruitĂ©e), comment produire de nouvelles images ?
Nous pourrions introduire du bruit pur et espĂ©rer que le modĂšle prĂ©dise une bonne image en tant que version dĂ©bruitĂ©e en une seule Ă©tape. Cependant, comme nous lâavons vu dans les expĂ©riences ci-dessus, cela ne fonctionne gĂ©nĂ©ralement pas bien. Câest pourquoi nous procĂ©dons Ă un certain nombre de petites Ă©tapes basĂ©es sur la prĂ©diction du modĂšle, en Ă©liminant de maniĂšre itĂ©rative une petite partie du bruit Ă la fois.
La maniĂšre exacte de procĂ©der dĂ©pend de la mĂ©thode dâĂ©chantillonnage utilisĂ©e. Nous nâentrerons pas dans la thĂ©orie trop profondĂ©ment, mais les questions clĂ©s de la conception sont les suivantes :
- Quelle est lâampleur du pas Ă franchir ? En dâautres termes, quel « calendrier de bruit » devez-vous suivre ?
- Utilisez-vous uniquement la prĂ©diction actuelle du modĂšle pour informer lâĂ©tape de mise Ă jour (comme DDPM, DDIM et beaucoup dâautres) ? Ăvaluez-vous le modĂšle plusieurs fois pour estimer les gradients dâordre supĂ©rieur en vue dâune Ă©tape plus importante et plus prĂ©cise (mĂ©thodes dâordre supĂ©rieur et certains solveurs dâEDO discrĂštes) ? Ou bien conservez-vous un historique des prĂ©dictions passĂ©es pour essayer de mieux informer lâĂ©tape de mise Ă jour actuelle (Ă©chantillonneurs linĂ©aires multi-Ă©tapes et ancestraux) ?
- Ajoutez-vous du bruit supplĂ©mentaire (parfois appelĂ© « churn ») pour ajouter plus de stochasticitĂ© (caractĂšre alĂ©atoire) au processus dâĂ©chantillonnage, ou le gardez-vous complĂštement dĂ©terministe ? De nombreux Ă©chantillonneurs contrĂŽlent ce paramĂštre (tel que « eta » pour les Ă©chantillonneurs DDIM) afin que lâutilisateur puisse choisir.
La recherche sur les mĂ©thodes dâĂ©chantillonnage pour les modĂšles de diffusion Ă©volue rapidement et de plus en plus de mĂ©thodes permettant de trouver de bonnes solutions en moins dâĂ©tapes sont proposĂ©es. Les courageux et les curieux trouveront peut-ĂȘtre intĂ©ressant de parcourir le code des diffĂ©rentes implĂ©mentations disponibles dans la bibliothĂšque đ€ Diffusers ici ou de consulter la documentation qui renvoient souvent aux articles pertinents.
Conclusions
Nous espĂ©rons que ce notebook vous a permis dâaborder les modĂšles de diffusion sous un angle lĂ©gĂšrement diffĂ©rent.
Ce notebook a Ă©tĂ© Ă©crit pour le cours de Hugging Face par Jonathan Whitaker, et recoupe une version incluse dans son propre cours, The Generative Landscape. Consultez-le (en anglais) si vous souhaitez voir cet exemple de base Ă©tendu avec du bruit et du conditionnement de classe. Les questions ou les bugs peuvent ĂȘtre communiquĂ©s via GitHub issues ou via Discord. Vous pouvez Ă©galement envoyer un message via Twitter Ă @johnowhitaker.
2. Vue d'ensemble
Dans cette unité, vous apprendrez à utiliser et à adapter les modÚles de diffusion pré-entraßnés de nouvelles façons. Vous verrez également comment nous pouvons créer des modÚles de diffusion qui prennent des entrées supplémentaires comme conditionnement pour contrÎler le processus de génération.
Vue dâensemble de cette unitĂ© :rocket:
Les différentes étapes à suivre pour cette unité :
- Lisez le matĂ©riel ci-dessous pour avoir une vue dâensemble des idĂ©es clĂ©s de cette unitĂ©
- Consultez le notebook _Finetuning et guidage_ pour finetuner un modĂšle de diffusion existant sur un nouveau jeu de donnĂ©es en utilisant la bibliothĂšque đ€ *Diffusers et pour modifier la procĂ©dure dâĂ©chantillonnage en utilisant le guidage.
- Suivez lâexemple dans le notebook pour partager une dĂ©mo Gradio pour votre modĂšle personnalisĂ©
- (Facultatif) Consultez le *notebook ModÚle de diffusion conditionné par la classe pour voir comment nous pouvons ajouter un contrÎle supplémentaire au processus de génération
- (Facultatif) Regardez cette vidéo (en anglais) pour une présentation informelle du matériel de cette unité
Finetuning
Comme vous avez pu le constater dans lâunitĂ© 1, entraĂźner des modĂšles de diffusion Ă partir de zĂ©ro peut prendre beaucoup de temps ! Le temps et les donnĂ©es nĂ©cessaires pour entraĂźner un modĂšle Ă partir de zĂ©ro peuvent devenir irrĂ©alisables, en particulier lorsque lâon passe Ă des rĂ©solutions plus Ă©levĂ©es. Heureusement, il existe une solution : commencer par un modĂšle qui a dĂ©jĂ Ă©tĂ© entraĂźnĂ© ! Ainsi, nous partons dâun modĂšle qui a dĂ©jĂ appris Ă dĂ©bruiter des images, et nous espĂ©rons que cela constituera un meilleur point de dĂ©part quâun modĂšle initialisĂ© de maniĂšre alĂ©atoire.

Le finetuning fonctionne gĂ©nĂ©ralement mieux si les nouvelles donnĂ©es ressemblent quelque peu aux donnĂ©es dâentraĂźnement originales du modĂšle de base (par exemple, commencer avec un modĂšle entraĂźnĂ© sur les visages est probablement une bonne idĂ©e si vous essayez de gĂ©nĂ©rer des visages de dessins animĂ©s), mais il est surprenant de constater que les avantages persistent mĂȘme si le domaine est modifiĂ© de maniĂšre assez radicale. Lâimage ci-dessus est gĂ©nĂ©rĂ©e Ă partir dâun modĂšle entraĂźnĂ© sur le jeu de donnĂ©es LSUN Bedrooms et finetunĂ© sur 500 Ă©tapes sur le jeu de donnĂ©es WikiArt. Le script dâentraĂźnement est inclus Ă titre de rĂ©fĂ©rence dans les notebooks de cette unitĂ©.
Guidage
Les modĂšles inconditionnels ne donnent pas beaucoup de contrĂŽle sur ce qui est gĂ©nĂ©rĂ©. Nous pouvons entraĂźner un modĂšle conditionnel (plus dâinformations Ă ce sujet dans la section suivante) qui prend des entrĂ©es supplĂ©mentaires pour aider Ă diriger le processus de gĂ©nĂ©ration, mais que faire si nous avons dĂ©jĂ entraĂźnĂ© un modĂšle inconditionnel que nous aimerions utiliser ? Câest lĂ quâintervient le guidage, un processus par lequel les prĂ©dictions du modĂšle Ă chaque Ă©tape du processus de gĂ©nĂ©ration sont Ă©valuĂ©es par rapport Ă une fonction de guidage et modifiĂ©es de maniĂšre Ă ce que lâimage finale gĂ©nĂ©rĂ©e corresponde mieux Ă nos attentes.

Cette fonction de guidage peut ĂȘtre presque nâimporte quoi, ce qui en fait une technique puissante ! Dans le notebook, nous partons dâun exemple simple (contrĂŽler la couleur, comme illustrĂ© dans lâexemple de sortie ci-dessus) pour arriver Ă un exemple utilisant un puissant modĂšle prĂ©-entraĂźnĂ© appelĂ© CLIP qui nous permet de guider la gĂ©nĂ©ration sur la base dâune description textuelle.
Conditionnement
Le guidage est un excellent moyen dâexploiter davantage un modĂšle de diffusion inconditionnel, mais si nous disposons dâinformations supplĂ©mentaires (telles quâune Ă©tiquette de classe ou une lĂ©gende dâimage) pendant lâentraĂźnement, nous pouvons Ă©galement les transmettre au modĂšle afin quâil les utilise pour Ă©tablir ses prĂ©dictions. Ce faisant, nous crĂ©ons un modĂšle conditionnel, que nous pouvons contrĂŽler au moment de lâinfĂ©rence en contrĂŽlant ce qui est fourni comme conditionnement. Le notebook montre un exemple de modĂšle conditionnĂ© par une classe qui apprend Ă gĂ©nĂ©rer des images en fonction dâune Ă©tiquette de classe.

Il existe un certain nombre de façons de transmettre ces informations de conditionnement, par exemple
- En les introduisant sous forme de canaux supplĂ©mentaires dans lâentrĂ©e du UNet. Cette mĂ©thode est souvent utilisĂ©e lorsque lâinformation de conditionnement a la mĂȘme forme que lâimage, comme un masque de segmentation, une carte de profondeur ou une version floue de lâimage (dans le cas dâun modĂšle de restauration/superrĂ©solution). Cela fonctionne aussi pour dâautres types de conditionnement. Par exemple, dans le notebook, lâĂ©tiquette de la classe est associĂ©e en avec un enchĂąssement puis Ă©tendue pour avoir la mĂȘme largeur et la mĂȘme hauteur que lâimage dâentrĂ©e, de sorte quâelle puisse ĂȘtre introduite sous forme de canaux supplĂ©mentaires.
- La crĂ©ation dâun enchĂąssement et sa projection Ă une taille correspondant au nombre de canaux Ă la sortie dâune ou de plusieurs couches internes du UNet, puis son ajout Ă ces sorties. Câest ainsi que le conditionnement du pas de temps est gĂ©rĂ©, par exemple. La sortie de chaque bloc Resnet est complĂ©tĂ©e par une projection de lâenchĂąssement du pas de temps. Ceci est utile lorsque vous avez un vecteur tel quâune lâenchĂąssement CLIP comme information de conditionnement. Un exemple notable est âImage Variationsâ version of Stable Diffusion qui fait exactement cela.
- Lâajout de couches dâattention croisĂ©e qui peuvent sâoccuper dâune sĂ©quence transmise en tant que conditionnement. Ceci est particuliĂšrement utile lorsque le conditionnement se prĂ©sente sous la forme dâun texte. Le texte est mis en correspondance avec une sĂ©quence dâenchĂąssements Ă lâaide dâun transformer puis les couches dâattention croisĂ©e du UNet sont utilisĂ©es pour incorporer cette information dans le chemin de dĂ©bruitage. Nous verrons cela en action dans lâunitĂ© 3 lorsque nous examinerons comment Stable Diffusion gĂšre le conditionnement du texte.
Notebooks
| Chapitre | Colab | Kaggle | Gradient | Studio Lab |
|---|---|---|---|---|
| Finetuning et guidage | ||||
| ModÚle de diffusion conditionné par la classe |
La plus grande partie du matĂ©riel se trouve dans Finetuning et guidage, oĂč nous explorons ces deux sujets Ă travers des exemples travaillĂ©s. Le notebook montre comment vous pouvez finetuner un modĂšle existant sur de nouvelles donnĂ©es, ajouter des conseils, et partager le rĂ©sultat sous forme de dĂ©mo Gradio. Il y a un script dâaccompagnement (finetune_model.py) qui facilite lâexpĂ©rimentation de diffĂ©rents paramĂštres de finetuning, et un Space que vous pouvez utiliser comme patron pour partager votre propre dĂ©mo sur đ€ Spaces.
Dans le notebook ModĂšle de diffusion conditionnĂ© par la classe, nous montrons un bref exemple de crĂ©ation dâun modĂšle de diffusion conditionnĂ© par les Ă©tiquettes de classe Ă lâaide du jeu de donnĂ©es MNIST. Lâobjectif est de dĂ©montrer lâidĂ©e principale aussi simplement que possible : en donnant au modĂšle des informations supplĂ©mentaires sur ce quâil est censĂ© dĂ©bruiter, nous pouvons contrĂŽler ultĂ©rieurement les types dâimages gĂ©nĂ©rĂ©es au moment de lâinfĂ©rence.
Projet
En suivant les exemples du notebook Fine-tuning and Guidance, finetunez votre propre modĂšle ou choisissez un modĂšle existant, puis crĂ©ez une dĂ©mo Gradio pour mettre en valeur vos nouvelles compĂ©tences en guidage. Nâoubliez pas de partager votre dĂ©mo sur Discord, Twitter, etc., afin que nous puissions admirer votre travail !
Ressources complémentaires
Une liste non exhaustive de ressources (en anglais) Ă consulter :
- Denoising Diffusion Implicit Models est une introduction de la mĂ©thode dâĂ©chantillonnage DDIM (utilisĂ©e par DDIMScheduler)
- GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models est une introduction de méthodes pour conditionner les modÚles de diffusion sur le texte
- eDiffi: Text-to-Image Diffusion Models with an Ensemble of Expert Denoisers montre comment diffĂ©rents types de conditionnement peuvent ĂȘtre utilisĂ©s ensemble pour contrĂŽler encore davantage les types dâĂ©chantillons gĂ©nĂ©rĂ©s
Vous avez identifiĂ© dâautres ressources intĂ©ressantes ? Faites-le nous savoir et nous les ajouterons Ă cette liste.
2.1. Finetuning et guidage
Dans ce notebook, nous allons couvrir deux approches principales pour adapter les modĂšles de diffusion existants :
- Avec le finetuning, nous rĂ©entraĂźnerons les modĂšles existants sur de nouvelles donnĂ©es afin de modifier le type de rĂ©sultats quâils produisent.
- Avec le guidage, nous prenons un modĂšle existant et dirigeons le processus de gĂ©nĂ©ration au moment de lâinfĂ©rence pour un contrĂŽle supplĂ©mentaire.
Ce que vous apprendrez :
A la fin de ce notebook, vous saurez comment :
- CrĂ©er une boucle dâĂ©chantillonnage et gĂ©nĂ©rer des Ă©chantillons plus rapidement Ă lâaide dâun nouveau planificateur
- Finetuner un modÚle de diffusion existant sur de nouvelles données, y compris :
- Utiliser lâaccumulation du gradient pour contourner certains des problĂšmes liĂ©s aux petits batchs.
- Enregistrer les Ă©chantillons dans Weights and Biases pendant lâentraĂźnement pour suivre la progression (via le script dâexemple joint).
- Sauvegarder le pipeline résultant et le télécharger sur le Hub
- Guider le processus dâĂ©chantillonnage avec des fonctions de perte supplĂ©mentaires pour ajouter un contrĂŽle sur les modĂšles existants, y compris :
- Explorer différentes approches de guidage avec une simple perte basée sur la couleur
- Utiliser CLIP pour guider la gĂ©nĂ©ration Ă lâaide dâun prompt de texte
- Partager une boucle dâĂ©chantillonnage personnalisĂ©e en utilisant Gradio et đ€ Spaces.
Configuration et importations
Pour enregistrer vos modĂšles finetunĂ©s sur le Hub dâHugging Face, vous devrez vous connecter avec un token qui a un accĂšs en Ă©criture. Le code ci-dessous vous invite Ă le faire et vous renvoie Ă la page des tokens de votre compte. Vous aurez Ă©galement besoin dâun compte Weights and Biases si vous souhaitez utiliser le script dâentraĂźnement pour enregistrer des Ă©chantillons au fur et Ă mesure que le modĂšle sâentraĂźne. LĂ encore, le code devrait vous inviter Ă vous connecter lĂ oĂč câest nĂ©cessaire.
A part cela, la seule chose Ă faire est dâinstaller quelques dĂ©pendances, dâimporter tout ce dont nous aurons besoin et de spĂ©cifier lâappareil que nous utiliserons :
!pip install -qq diffusers datasets accelerate wandb open-clip-torch
# Code pour se connecter au Hub d'Hugging Face, nécessaire pour partager les modÚles
# Assurez-vous d'utiliser un *token* avec un accÚs WRITE (écriture)
from huggingface_hub import notebook_login
notebook_login()
Token is valid.
Your token has been saved in your configured git credential helpers (store).
Your token has been saved to /root/.huggingface/token
Login successful
import numpy as np
import torch
import torch.nn.functional as F
import torchvision
from datasets import load_dataset
from diffusers import DDIMScheduler, DDPMPipeline
from matplotlib import pyplot as plt
from PIL import Image
from torchvision import transforms
from tqdm.auto import tqdm
device = (
"mps"
if torch.backends.mps.is_available()
else "cuda"
if torch.cuda.is_available()
else "cpu"
)
Chargement dâun pipeline prĂ©-entraĂźnĂ©
Pour commencer ce notebook, chargeons un pipeline existant et voyons ce que nous pouvons en faire :
image_pipe = DDPMPipeline.from_pretrained("google/ddpm-celebahq-256")
image_pipe.to(device);
La gĂ©nĂ©ration dâimages est aussi simple que lâexĂ©cution de la mĂ©thode __call__ du pipeline en lâappelant comme une fonction :
images = image_pipe().images
images[0]
Sympathique, mais LENT ! Avant dâaborder les sujets principaux du jour, jetons un coup dâĆil Ă la boucle dâĂ©chantillonnage proprement dite et voyons comment nous pouvons utiliser un Ă©chantillonneur plus sophistiquĂ© pour lâaccĂ©lĂ©rer.
Ăchantillonnage plus rapide avec DDIM
Ă chaque Ă©tape, le modĂšle est nourri dâune entrĂ©e bruyante et il lui est demandĂ© de prĂ©dire le bruit (et donc une estimation de ce Ă quoi lâimage entiĂšrement dĂ©bruitĂ©e pourrait ressembler). Au dĂ©part, ces prĂ©dictions ne sont pas trĂšs bonnes, câest pourquoi nous dĂ©composons le processus en plusieurs Ă©tapes. Cependant, lâutilisation de plus de 1000 Ă©tapes sâest avĂ©rĂ©e inutile, et une multitude de recherches rĂ©centes ont explorĂ© la maniĂšre dâobtenir de bons Ă©chantillons avec le moins dâĂ©tapes possible.
Dans la bibliothĂšque đ€ Diffusers, ces mĂ©thodes dâĂ©chantillonnage sont gĂ©rĂ©es par un planificateur, qui doit effectuer chaque mise Ă jour via la fonction step(). Pour gĂ©nĂ©rer une image, on commence par un bruit alĂ©atoire $x$. Ensuite, pour chaque pas de temps dans le planificateur de bruit, nous introduisons lâentrĂ©e bruitĂ©e $x$ dans le modĂšle et transmettons la prĂ©diction rĂ©sultante Ă la fonction step(). Celle-ci renvoie une sortie avec un attribut prev_sample. âpreviousâ parce que nous revenons en arriĂšre dans le temps, dâun niveau de bruit Ă©levĂ© Ă un niveau de bruit faible (Ă lâinverse du processus de diffusion vers lâavant).
Voyons cela en action ! Tout dâabord, nous chargeons un planificateur, ici un DDIMScheduler basĂ© sur le papier Denoising Diffusion Implicit Models qui peut donner des Ă©chantillons dĂ©cents en beaucoup moins dâĂ©tapes que lâimplĂ©mentation originale du DDPM :
# Créer un nouveau planificateur et définir le nombre d'étapes d'inférence
scheduler = DDIMScheduler.from_pretrained("google/ddpm-celebahq-256")
scheduler.set_timesteps(num_inference_steps=40)
Vous pouvez constater que ce modÚle effectue 40 étapes au total, chaque saut équivalant à 25 étapes du programme original de 1000 étapes :
scheduler.timesteps
tensor([975, 950, 925, 900, 875, 850, 825, 800, 775, 750, 725, 700, 675, 650,
625, 600, 575, 550, 525, 500, 475, 450, 425, 400, 375, 350, 325, 300,
275, 250, 225, 200, 175, 150, 125, 100, 75, 50, 25, 0])
CrĂ©ons 4 images alĂ©atoires et exĂ©cutons la boucle dâĂ©chantillonnage, en visualisant Ă la fois le $x$ actuel et la version dĂ©bruitĂ©e prĂ©dite au fur et Ă mesure de lâavancement du processus :
# Le point de départ aléatoire
x = torch.randn(4, 3, 256, 256).to(device) # Batch de 4 images Ă 3 canaux de 256 x 256 px
# Boucle sur les pas de temps d'échantillonnage
for i, t in tqdm(enumerate(scheduler.timesteps)):
# Préparer l'entrée du modÚle
model_input = scheduler.scale_model_input(x, t)
# Obtenir la prédiction
with torch.no_grad():
noise_pred = image_pipe.unet(model_input, t)["sample"]
# Calculer la forme que devrait prendre l'échantillon mis à jour à l'aide du planificateur
scheduler_output = scheduler.step(noise_pred, t, x)
# Mise Ă jour de x
x = scheduler_output.prev_sample
# Occasionnellement, afficher à la fois x et les images débruitées prédites
if i % 10 == 0 or i == len(scheduler.timesteps) - 1:
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
grid = torchvision.utils.make_grid(x, nrow=4).permute(1, 2, 0)
axs[0].imshow(grid.cpu().clip(-1, 1) * 0.5 + 0.5)
axs[0].set_title(f"Current x (step {i})")
pred_x0 = (
scheduler_output.pred_original_sample
) # Non disponible pour tous les planificateurs
grid = torchvision.utils.make_grid(pred_x0, nrow=4).permute(1, 2, 0)
axs[1].imshow(grid.cpu().clip(-1, 1) * 0.5 + 0.5)
axs[1].set_title(f"Predicted denoised images (step {i})")
plt.show()
Comme vous pouvez le voir, les prĂ©dictions initiales ne sont pas trĂšs bonnes, mais au fur et Ă mesure que le processus se poursuit, les rĂ©sultats prĂ©dits deviennent de plus en plus prĂ©cis. Si vous ĂȘtes curieux de savoir ce qui se passe Ă lâintĂ©rieur de la fonction step(), inspectez le code (bien commentĂ©) avec :
# ??scheduler.step
Vous pouvez également insérer ce nouveau planificateur à la place du planificateur original fourni avec le pipeline, et échantillonner de la maniÚre suivante :
image_pipe.scheduler = scheduler
images = image_pipe(num_inference_steps=40).images
images[0]
TrÚs bien, nous pouvons maintenant obtenir des échantillons dans un délai raisonnable ! Cela devrait accélérer les choses au fur et à mesure que nous avançons dans le reste de ce notebook :)
Finetuning
Et maintenant, le plus amusant ! Ătant donnĂ© ce pipeline prĂ©-entraĂźnĂ©, comment pouvons-nous rĂ©entraĂźner le modĂšle pour gĂ©nĂ©rer des images sur la base de nouvelles donnĂ©es dâentraĂźnement ?
Il sâavĂšre que cela est presque identique Ă entraĂźner un modĂšle Ă partir de zĂ©ro (comme nous lâavons vu dans lâunitĂ© 1), sauf que nous commençons avec le modĂšle existant. Voyons cela en action et abordons quelques considĂ©rations supplĂ©mentaires au fur et Ă mesure.
Tout dâabord, le jeu de donnĂ©es : vous pouvez essayer ce jeu de donnĂ©es de visages vintage ou ces visages animĂ©s pour quelque chose de plus proche des donnĂ©es dâentraĂźnement originales de ce modĂšle de visages. Mais pour le plaisir, utilisons plutĂŽt le mĂȘme petit jeu de donnĂ©es de papillons que nous avons utilisĂ© pour nous entraĂźner Ă partir de zĂ©ro dans lâunitĂ© 1. ExĂ©cutez le code ci-dessous pour tĂ©lĂ©charger le jeu de donnĂ©es papillons et crĂ©er un chargeur de donnĂ©es Ă partir duquel nous pouvons Ă©chantillonner un batch dâimages :
# Pas sur Colab ? Les commentaires avec #@ permettent de modifier l'interface utilisateur comme les titres ou les entrées
# mais peuvent ĂȘtre ignorĂ©s si vous travaillez sur une plateforme diffĂ©rente.
dataset_name = "huggan/smithsonian_butterflies_subset" # @param
dataset = load_dataset(dataset_name, split="train")
image_size = 256 # @param
batch_size = 4 # @param
preprocess = transforms.Compose(
[
transforms.Resize((image_size, image_size)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]),
]
)
def transform(examples):
images = [preprocess(image.convert("RGB")) for image in examples["image"]]
return {"images": images}
dataset.set_transform(transform)
train_dataloader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True
)
print("Previewing batch:")
batch = next(iter(train_dataloader))
grid = torchvision.utils.make_grid(batch["images"], nrow=4)
plt.imshow(grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5);
ConsidĂ©ration 1 : notre taille de batch ici (4) est assez petite, puisque nous entraĂźnons sur une grande taille dâimage (256 pixels) en utilisant un modĂšle assez grand et que nous manquerons de RAM du GPU si nous augmentons trop la taille du batch. Vous pouvez rĂ©duire la taille de lâimage pour accĂ©lĂ©rer les choses et permettre des batchs plus importants, mais ces modĂšles ont Ă©tĂ© conçus et entraĂźnĂ©s Ă lâorigine pour une gĂ©nĂ©ration de 256 pixels.
Passons maintenant Ă la boucle dâentraĂźnement. Nous allons mettre Ă jour les poids du modĂšle prĂ©-entraĂźnĂ© en fixant la cible dâoptimisation Ă image_pipe.unet.parameters(). Le reste est presque identique Ă lâexemple de boucle dâentraĂźnement de lâunitĂ© 1. Cela prend environ 10 minutes Ă exĂ©cuter sur Colab, câest donc le bon moment pour prendre un cafĂ© ou un thĂ© pendant que vous attendez :
num_epochs = 2 # @param
lr = 1e-5 # 2param
grad_accumulation_steps = 2 # @param
optimizer = torch.optim.AdamW(image_pipe.unet.parameters(), lr=lr)
losses = []
for epoch in range(num_epochs):
for step, batch in tqdm(enumerate(train_dataloader), total=len(train_dataloader)):
clean_images = batch["images"].to(device)
# bruit Ă ajouter aux images
noise = torch.randn(clean_images.shape).to(clean_images.device)
bs = clean_images.shape[0]
# un pas de temps aléatoire pour chaque image
timesteps = torch.randint(
0,
image_pipe.scheduler.num_train_timesteps,
(bs,),
device=clean_images.device,
).long()
# Ajouter du bruit aux images propres en fonction de la magnitude du bruit Ă chaque pas de temps
# (il s'agit du processus de diffusion vers l'avant)
noisy_images = image_pipe.scheduler.add_noise(clean_images, noise, timesteps)
# Obtenir la prédiction du modÚle pour le bruit
noise_pred = image_pipe.unet(noisy_images, timesteps, return_dict=False)[0]
# Comparez la prédiction avec le bruit réel :
loss = F.mse_loss(
noise_pred, noise
) # NB : essayer de prédire le bruit (eps) pas (noisy_ims-clean_ims) ou juste (clean_ims)
# Stocker pour un plot ultérieur
losses.append(loss.item())
# Mettre Ă jour les paramĂštres du modĂšle avec l'optimiseur sur la base de cette perte
loss.backward(loss)
# Accumulation des gradients
if (step + 1) % grad_accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
print(
f"Epoch {epoch} average loss: {sum(losses[-len(train_dataloader):])/len(train_dataloader)}"
)
# Tracer la courbe de perte :
plt.plot(losses)
ConsidĂ©ration 2 : notre signal de perte est extrĂȘmement bruyant, puisque nous ne travaillons quâavec quatre exemples Ă des niveaux de bruit alĂ©atoires pour chaque Ă©tape. Ce nâest pas idĂ©al pour lâentraĂźnement. Une solution consiste Ă utiliser un taux dâapprentissage extrĂȘmement faible pour limiter la taille de la mise Ă jour Ă chaque Ă©tape. Ce serait encore mieux si nous pouvions trouver un moyen dâobtenir les mĂȘmes avantages quâen utilisant une taille de batch plus importante sans que les besoins en mĂ©moire ne montent en flĂšcheâŠ
Entrez dans lâaccumulation des gradients. Si nous appelons loss.backward() plusieurs fois avant dâexĂ©cuter optimizer.step() et optimizer.zero_grad(), PyTorch accumule (somme) les gradients, fusionnant effectivement le signal de plusieurs batchs pour donner une seule (meilleure) estimation qui est ensuite utilisĂ©e pour mettre Ă jour les paramĂštres. Il en rĂ©sulte moins de mises Ă jour totales, tout comme nous le verrions si nous utilisions une taille de batch plus importante. Câest quelque chose que de nombreux frameworks gĂšrent pour vous (par exemple, đ€ Accelerate rend cela facile), mais il est agrĂ©able de le voir mis en Ćuvre Ă partir de zĂ©ro car il sâagit dâune technique utile pour traiter lâentraĂźnement sous les contraintes de mĂ©moire du GPU ! Comme vous pouvez le voir dans le code ci-dessus (aprĂšs le commentaire # Gradient accumulation), il nây a pas vraiment besoin de beaucoup de code.
âïž Ă votre tour ! Voyez si vous pouvez ajouter lâaccumulation des gradients Ă la boucle dâentraĂźnement de lâunitĂ© 1. Comment se comporte-t-elle ? RĂ©flĂ©chissez Ă la maniĂšre dont vous pourriez ajuster le taux dâapprentissage en fonction du nombre dâĂ©tapes dâaccumulation des gradients ; devrait-il rester identique Ă auparavant ?
ConsidĂ©ration 3 : Cela prend encore beaucoup de temps, et afficher une mise Ă jour dâune ligne Ă chaque Ă©poque nâest pas suffisant pour nous donner une bonne idĂ©e de ce qui se passe. Nous devrions probablement :
- GĂ©nĂ©rer quelques Ă©chantillons de temps en temps pour examiner visuellement la performance qualitativement au fur et Ă mesure que le modĂšle sâentraĂźne.
- Enregistrer des Ă©lĂ©ments tels que la perte et les gĂ©nĂ©rations dâĂ©chantillons pendant lâentraĂźnement, peut-ĂȘtre en utilisant quelque chose comme Weights and Biases ou Tensorboard.
Nous avons créé un script rapide (finetune_model.py) qui reprend le code dâentraĂźnement ci-dessus et y ajoute une fonctionnalitĂ© minimale de logging. Vous pouvez voir les logs dâun entraĂźnement ci-dessous :
%wandb johnowhitaker/dm_finetune/2upaa341 # Vous aurez besoin d'un compte W&B pour que cela fonctionne - sautez si vous ne voulez pas vous connecter.
Il est amusant de voir comment les Ă©chantillons gĂ©nĂ©rĂ©s changent au fur et Ă mesure que lâentraĂźnement progresse. MĂȘme si la perte ne semble pas sâamĂ©liorer beaucoup, on peut voir une progression du domaine original (images de chambres Ă coucher) vers les nouvelles donnĂ©es dâentraĂźnement (wikiart). A la fin de ce notebook se trouve un code commentĂ© pour finetunĂ© un modĂšle en utilisant ce script comme alternative Ă lâexĂ©cution de la cellule ci-dessus.
âïž Ă votre tour ! Voyez si vous pouvez modifier lâexemple officiel de script dâentraĂźnement que nous avons vu dans lâunitĂ© 1 pour commencer avec un modĂšle prĂ©-entraĂźnĂ© plutĂŽt que dâentraĂźner Ă partir de zĂ©ro. Comparez-le au script minimal dont le lien figure ci-dessus ; quelles sont les fonctionnalitĂ©s supplĂ©mentaires qui manquent au script minimal ? En gĂ©nĂ©rant quelques images avec ce modĂšle, nous pouvons voir que ces visages ont dĂ©jĂ lâair trĂšs Ă©tranges !
x = torch.randn(8, 3, 256, 256).to(device) # Batch de 8
for i, t in tqdm(enumerate(scheduler.timesteps)):
model_input = scheduler.scale_model_input(x, t)
with torch.no_grad():
noise_pred = image_pipe.unet(model_input, t)["sample"]
x = scheduler.step(noise_pred, t, x).prev_sample
grid = torchvision.utils.make_grid(x, nrow=4)
plt.imshow(grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5);
ConsidĂ©ration 4 : Le finetuning peut ĂȘtre tout Ă fait imprĂ©visible ! Si nous entraĂźnions plus longtemps, nous pourrions voir des papillons parfaits. Mais les Ă©tapes intermĂ©diaires peuvent ĂȘtre extrĂȘmement intĂ©ressantes en elles-mĂȘmes, surtout si vos intĂ©rĂȘts sont plutĂŽt artistiques ! EntraĂźnez sur des pĂ©riodes trĂšs courtes ou trĂšs longues et faites varier le taux dâapprentissage pour voir comment cela affecte les types de rĂ©sultats produits par le modĂšle final.
Code pour finetuner un modĂšle en utilisant le script dâexemple minimal que nous avons utilisĂ© sur le modĂšle de dĂ©monstration WikiArt
Si vous souhaitez entraßner un modÚle similaire à celui que nous avons créé sur WikiArt, vous pouvez décommenter et exécuter les cellules ci-dessous. Comme cela prend un certain temps et peut épuiser la mémoire de votre GPU, nous vous conseillons de le faire aprÚs avoir parcouru le reste de ce notebook.
## Pour télécharger le script de finetuning :
# !wget https://github.com/huggingface/diffusion-models-class/raw/main/unit2/finetune_model.py
## Pour exécuter le script, entraßnant le modÚle de visage sur des visages vintage
## (l'idéal est d'exécuter ce script dans un terminal) :
# !python finetune_model.py --image_size 128 --batch_size 8 --num_epochs 16\
# --grad_accumulation_steps 2 --start_model "google/ddpm-celebahq-256"\
# --dataset_name "Norod78/Vintage-Faces-FFHQAligned" --wandb_project 'dm-finetune'\
# --log_samples_every 100 --save_model_every 1000 --model_save_name 'vintageface'
Sauvegarde et chargement des pipelines finetunés
Maintenant que nous avons finetuné le UNet dans notre modÚle de diffusion, sauvegardons-le dans un dossier local en exécutant :
image_pipe.save_pretrained("my-finetuned-model")
Comme nous lâavons vu dans lâunitĂ© 1, cela permet de sauvegarder la configuration, le modĂšle et le planificateur :
!ls {"my-finetuned-model"}
Ensuite, vous pouvez suivre les mĂȘmes Ă©tapes que celles dĂ©crites dans le notebook dâintroduction Ă đ€ Diffusers de lâunitĂ© 1 pour pousser le modĂšle vers le Hub en vue dâune utilisation ultĂ©rieure :
# Code pour télécharger un pipeline sauvegardé localement vers le Hub
from huggingface_hub import HfApi, ModelCard, create_repo, get_full_repo_name
# Mise en place du repo et téléchargement des fichiers
model_name = "ddpm-celebahq-finetuned-butterflies-2epochs" # @param Le nom que vous souhaitez lui donner sur le Hub
local_folder_name = "my-finetuned-model" # @param Créé par le script ou par vous via image_pipe.save_pretrained('save_name')
description = "Describe your model here" # @param
hub_model_id = get_full_repo_name(model_name)
create_repo(hub_model_id)
api = HfApi()
api.upload_folder(
folder_path=f"{local_folder_name}/scheduler", path_in_repo="", repo_id=hub_model_id
)
api.upload_folder(
folder_path=f"{local_folder_name}/unet", path_in_repo="", repo_id=hub_model_id
)
api.upload_file(
path_or_fileobj=f"{local_folder_name}/model_index.json",
path_in_repo="model_index.json",
repo_id=hub_model_id,
)
# Ajouter une carte modĂšle (facultatif mais sympa !)
content = f"""
---
license: mit
tags:
- pytorch
- diffusers
- unconditional-image-generation
- diffusion-models-class
---
# Example Fine-Tuned Model for Unit 2 of the [Diffusion Models Class đ§š](https://github.com/huggingface/diffusion-models-class)
{description}
## Usage
```python
from diffusers import DDPMPipeline
pipeline = DDPMPipeline.from_pretrained('{hub_model_id}')
image = pipeline().images[0]
image
```python
"""
card = ModelCard(content)
card.push_to_hub(hub_model_id)
'https://huggingface.co/lewtun/ddpm-celebahq-finetuned-butterflies-2epochs/blob/main/README.md'
Félicitations, vous avez maintenant finetuné votre premier modÚle de diffusion !
Pour le reste de ce notebook, nous utiliserons un modĂšle que nous avons finetunĂ© Ă partir dâun modĂšle entraĂźnĂ© sur LSUN bedrooms environ une fois sur le WikiArt dataset. Si vous prĂ©fĂ©rez, vous pouvez sauter cette cellule et utiliser le pipeline faces/butterflies que nous avons finetunĂ© dans la section prĂ©cĂ©dente ou en charger un depuis le Hub Ă la place :
# Chargement du pipeline pré-entraßné
pipeline_name = "johnowhitaker/sd-class-wikiart-from-bedrooms"
image_pipe = DDPMPipeline.from_pretrained(pipeline_name).to(device)
# Ăchantillon d'images avec un planificateur DDIM sur 40 Ă©tapes
scheduler = DDIMScheduler.from_pretrained(pipeline_name)
scheduler.set_timesteps(num_inference_steps=40)
# Point de départ aléatoire (batch de 8 images)
x = torch.randn(8, 3, 256, 256).to(device)
# Boucle d'échantillonnage minimale
for i, t in tqdm(enumerate(scheduler.timesteps)):
model_input = scheduler.scale_model_input(x, t)
with torch.no_grad():
noise_pred = image_pipe.unet(model_input, t)["sample"]
x = scheduler.step(noise_pred, t, x).prev_sample
# Voir les résultats
grid = torchvision.utils.make_grid(x, nrow=4)
plt.imshow(grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5);
ConsidĂ©ration 5 : Il est souvent difficile de savoir si le finetunĂ© fonctionne bien, et ce que lâon entend par âbonnes performancesâ peut varier selon le cas dâutilisation. Par exemple, si vous finetunĂ© un modĂšle conditionnĂ© par du texte comme Stable Diffusion sur un petit jeu de donnĂ©es, vous voudrez probablement quâil conserve la plus grande partie de son apprentissage original afin de pouvoir comprendre des prompts arbitraires non couverts par votre nouveau jeu de donnĂ©es, tout en sâadaptant pour mieux correspondre au style de vos nouvelles donnĂ©es dâentraĂźnement. Cela pourrait signifier lâutilisation dâun faible taux dâapprentissage avec quelque chose comme la moyenne exponentielle du modĂšle, comme dĂ©montrĂ© dans cet excellent article de blog sur la crĂ©ation dâune version Pokemon de Stable Diffusion. Dans une autre situation, vous pouvez vouloir rĂ©-entraĂźner complĂštement un modĂšle sur de nouvelles donnĂ©es (comme notre exemple chambre â wikiart), auquel cas un taux dâapprentissage plus Ă©levĂ© et un entraĂźnement plus poussĂ© sâavĂšrent judicieux. MĂȘme si le graphique de la perte ne montre pas beaucoup dâamĂ©lioration, les Ă©chantillons sâĂ©loignent clairement des donnĂ©es dâorigine et sâorientent vers des rĂ©sultats plus âartistiquesâ, bien quâils restent pour la plupart incohĂ©rents.
Ce qui nous amĂšne Ă la section suivante, oĂč nous examinons comment nous pourrions ajouter des conseils supplĂ©mentaires Ă un tel modĂšle pour mieux contrĂŽler les rĂ©sultats.
Guidage
Que faire si lâon souhaite exercer un certain contrĂŽle sur les Ă©chantillons gĂ©nĂ©rĂ©s ? Par exemple, supposons que nous voulions biaiser les images gĂ©nĂ©rĂ©es pour quâelles soient dâune couleur spĂ©cifique. Comment procĂ©der ? Câest lĂ quâintervient le guidage, une technique qui permet dâajouter un contrĂŽle supplĂ©mentaire au processus dâĂ©chantillonnage.
La premiĂšre Ă©tape consiste Ă crĂ©er notre fonction de conditionnement : une mesure (perte) que nous souhaitons minimiser. En voici une pour lâexemple de la couleur, qui compare les pixels dâune image Ă une couleur cible (par dĂ©faut, une sorte de sarcelle claire) et renvoie lâerreur moyenne :
def color_loss(images, target_color=(0.1, 0.9, 0.5)):
"""Ătant donnĂ© une couleur cible (R, G, B), retourner une perte correspondant Ă la distance moyenne entre
les pixels de l'image et cette couleur. Par défaut, il s'agit d'une couleur sarcelle claire : (0.1, 0.9, 0.5)"""
target = (
torch.tensor(target_color).to(images.device) * 2 - 1
) # Map target color to (-1, 1)
target = target[
None, :, None, None
] # Obtenir la forme nécessaire pour fonctionner avec les images (b, c, h, w)
error = torch.abs(
images - target
).mean() # Différence absolue moyenne entre les pixels de l'image et la couleur cible
return error
Ensuite, nous allons crĂ©er une version modifiĂ©e de la boucle dâĂ©chantillonnage oĂč, Ă chaque Ă©tape, nous ferons ce qui suit :
- Créer une nouvelle version de
xavecrequires_grad = True - Calculer la version débruitée (
x0) - Introduire la version prédite
x0dans notre fonction de perte - Trouver le gradient de cette fonction de perte par rapport Ă
x - Utiliser ce gradient de conditionnement pour modifier
xavant dâutiliser le planificateur, en espĂ©rant pousser x dans une direction qui conduira Ă une perte plus faible selon notre fonction dâorientation.
Il existe deux variantes que vous pouvez explorer. Dans la premiĂšre, nous fixons requires_grad sur x aprĂšs avoir obtenu notre prĂ©diction de bruit du UNet, ce qui est plus efficace en termes de mĂ©moire (puisque nous nâavons pas Ă retracer les gradients Ă travers le modĂšle de diffusion), mais donne un gradient moins prĂ©cis. Dans le second cas, nous dĂ©finissons dâabord requires_grad sur x, puis nous le faisons passer par lâunet et nous calculons le x0 prĂ©dit.
# Variante 1 : méthode rapide
# L'échelle de guidance détermine l'intensité de l'effet
guidance_loss_scale = 40 # Envisagez de modifier cette valeur Ă 5, ou Ă 100
x = torch.randn(8, 3, 256, 256).to(device)
for i, t in tqdm(enumerate(scheduler.timesteps)):
# Préparer l'entrée du modÚle
model_input = scheduler.scale_model_input(x, t)
# Prédire le bruit résiduel
with torch.no_grad():
noise_pred = image_pipe.unet(model_input, t)["sample"]
# Fixer x.requires_grad Ă True
x = x.detach().requires_grad_()
# Obtenir la valeur prédite x0
x0 = scheduler.step(noise_pred, t, x).pred_original_sample
# Calculer la perte
loss = color_loss(x0) * guidance_loss_scale
if i % 10 == 0:
print(i, "loss:", loss.item())
# Obtenir le gradient
cond_grad = -torch.autograd.grad(loss, x)[0]
# Modifier x en fonction de ce gradient
x = x.detach() + cond_grad
# Le planificateur
x = scheduler.step(noise_pred, t, x).prev_sample
# Voir le résultat
grid = torchvision.utils.make_grid(x, nrow=4)
im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
Image.fromarray(np.array(im * 255).astype(np.uint8))
0 loss: 27.279136657714844
10 loss: 11.286816596984863
20 loss: 10.683112144470215
30 loss: 10.942476272583008
Cette deuxiĂšme option nĂ©cessite presque le double de RAM GPU pour fonctionner, mĂȘme si nous ne gĂ©nĂ©rons quâun batch de quatre images au lieu de huit. Voyez si vous pouvez repĂ©rer la diffĂ©rence et rĂ©flĂ©chissez Ă la raison pour laquelle cette mĂ©thode est plus « prĂ©cise » :
# Variante 2 : définir x.requires_grad avant de calculer les prédictions du modÚle
guidance_loss_scale = 40
x = torch.randn(4, 3, 256, 256).to(device)
for i, t in tqdm(enumerate(scheduler.timesteps)):
# Définir requires_grad avant la passe avant du modÚle
x = x.detach().requires_grad_()
model_input = scheduler.scale_model_input(x, t)
# prédire (avec grad cette fois)
noise_pred = image_pipe.unet(model_input, t)["sample"]
# Obtenir la valeur prédite x0 :
x0 = scheduler.step(noise_pred, t, x).pred_original_sample
# Calculer la perte
loss = color_loss(x0) * guidance_loss_scale
if i % 10 == 0:
print(i, "loss:", loss.item())
# Obtenir le gradient
cond_grad = -torch.autograd.grad(loss, x)[0]
# Modifier x en fonction de ce gradient
x = x.detach() + cond_grad
# Le planificateur
x = scheduler.step(noise_pred, t, x).prev_sample
grid = torchvision.utils.make_grid(x, nrow=4)
im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
Image.fromarray(np.array(im * 255).astype(np.uint8))
0 loss: 30.750328063964844
10 loss: 18.550724029541016
20 loss: 17.515094757080078
30 loss: 17.55681037902832
Dans la seconde variante, les besoins en mĂ©moire sont plus importants et lâeffet est moins prononcĂ©, de sorte que vous pouvez penser quâelle est infĂ©rieure. Cependant, les rĂ©sultats sont sans doute plus proches des types dâimages sur lesquels le modĂšle a Ă©tĂ© entraĂźnĂ©, et vous pouvez toujours augmenter lâĂ©chelle de guidage pour obtenir un effet plus important. Lâapproche que vous utiliserez dĂ©pendra en fin de compte de ce qui fonctionne le mieux sur le plan expĂ©rimental.
âïž Ă votre tour ! Choisissez votre couleur prĂ©fĂ©rĂ©e et recherchez ses valeurs dans lâespace RGB. Modifiez la ligne
color_loss()dans la cellule ci-dessus pour recevoir ces nouvelles valeurs RGB et examinez les résultats ; correspondent-ils à ce que vous attendez ?
Guidage avec CLIP
Guider vers une couleur nous donne un peu de contrÎle, mais que se passerait-il si nous pouvions simplement taper un texte décrivant ce que nous voulons ?
CLIP est un modĂšle créé par OpenAI qui nous permet de comparer des images Ă des lĂ©gendes textuelles. Câest extrĂȘmement puissant, car cela nous permet de quantifier Ă quel point une image correspond Ă un prompt. Et comme le processus est diffĂ©rentiable, nous pouvons lâutiliser comme fonction de perte pour guider notre modĂšle de diffusion !
Nous nâentrerons pas dans les dĂ©tails ici. Lâapproche de base est la suivante :
- EnchĂąsser le prompt pour obtenir un enchĂąssement CLIP Ă 512 dimensions
- Pour chaque étape du processus du modÚle de diffusion :
- CrĂ©er plusieurs variantes de lâimage dĂ©bruitĂ©e prĂ©dite (le fait dâavoir plusieurs variantes permet dâobtenir un signal de perte plus propre).
- Pour chacune dâentre elles, enchĂąsser lâimage avec CLIP et comparez cet enchĂąssement avec celui du prompt (Ă lâaide dâune mesure appelĂ©e « distance du grand cercle »).
- Calculer le gradient de cette perte par rapport Ă lâimage bruyante actuelle x et utiliser ce gradient pour modifier x avant de le mettre Ă jour avec le planificateur.
Pour une explication plus approfondie de CLIP, consultez cette leçon sur le sujet ou ce rapport sur le projet OpenCLIP que nous utilisons pour charger le modÚle CLIP. Exécutez la cellule suivante pour charger un modÚle CLIP :
import open_clip
clip_model, _, preprocess = open_clip.create_model_and_transforms(
"ViT-B-32", pretrained="openai"
)
clip_model.to(device)
# Transformations pour redimensionner et augmenter une image + normalisation pour correspondre aux données entraßnées par CLIP
tfms = torchvision.transforms.Compose(
[
torchvision.transforms.RandomResizedCrop(224), # CROP aléatoire à chaque fois
torchvision.transforms.RandomAffine(
5
), # Une augmentation aléatoire possible : biaiser l'image
torchvision.transforms.RandomHorizontalFlip(), # Vous pouvez ajouter des augmentations supplémentaires si vous le souhaitez
torchvision.transforms.Normalize(
mean=(0.48145466, 0.4578275, 0.40821073),
std=(0.26862954, 0.26130258, 0.27577711),
),
]
)
# Et définir une fonction de perte qui prend une image, l'enchùsse et la compare avec les caractéristiques textuelles du prompt
def clip_loss(image, text_features):
image_features = clip_model.encode_image(
tfms(image)
) # Note : applique les transformations ci-dessus
input_normed = torch.nn.functional.normalize(image_features.unsqueeze(1), dim=2)
embed_normed = torch.nn.functional.normalize(text_features.unsqueeze(0), dim=2)
dists = (
input_normed.sub(embed_normed).norm(dim=2).div(2).arcsin().pow(2).mul(2)
) # Distance du grand cercle
return dists.mean()
Une fois la fonction de perte dĂ©finie, notre boucle dâĂ©chantillonnage guidĂ© ressemble aux exemples prĂ©cĂ©dents, en remplaçant color_loss() par notre nouvelle fonction de perte basĂ©e sur CLIP :
prompt = "Red Rose (still life), red flower painting" # @param
# Explorer en changeant ça
guidance_scale = 8 # @param
n_cuts = 4 # @param
# Plus d'étapes -> plus de temps pour que le guidage ait un effet
scheduler.set_timesteps(50)
# Nous enchĂąssons un prompt avec CLIP comme cible
text = open_clip.tokenize([prompt]).to(device)
with torch.no_grad(), torch.cuda.amp.autocast():
text_features = clip_model.encode_text(text)
x = torch.randn(4, 3, 256, 256).to(
device
) # L'utilisation de la RAM est Ă©levĂ©e, vous ne voulez peut-ĂȘtre qu'une seule image Ă la fois.
for i, t in tqdm(enumerate(scheduler.timesteps)):
model_input = scheduler.scale_model_input(x, t)
# prédire le bruit résiduel
with torch.no_grad():
noise_pred = image_pipe.unet(model_input, t)["sample"]
cond_grad = 0
for cut in range(n_cuts):
# nécessite un grad sur x
x = x.detach().requires_grad_()
# Obtenir le x0 prédit
x0 = scheduler.step(noise_pred, t, x).pred_original_sample
# Calculer la perte
loss = clip_loss(x0, text_features) * guidance_scale
# Obtenir le gradient (échelle par n_cuts puisque nous voulons la moyenne)
cond_grad -= torch.autograd.grad(loss, x)[0] / n_cuts
if i % 25 == 0:
print("Step:", i, ", Guidance loss:", loss.item())
# Modifier x en fonction de ce gradient
alpha_bar = scheduler.alphas_cumprod[i]
x = (
x.detach() + cond_grad * alpha_bar.sqrt()
) # Note the additional scaling factor here!
# Le planificateur
x = scheduler.step(noise_pred, t, x).prev_sample
grid = torchvision.utils.make_grid(x.detach(), nrow=4)
im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
Image.fromarray(np.array(im * 255).astype(np.uint8))
Step: 0 , Guidance loss: 7.437869548797607
Step: 25 , Guidance loss: 7.174620628356934
Cela ressemble un peu Ă des roses ! Ce nâest pas parfait, mais si vous jouez avec les paramĂštres, vous pouvez obtenir des images agrĂ©ables.
Si vous examinez le code ci-dessus, vous verrez que nous mettons Ă lâĂ©chelle le gradient de conditionnement par un facteur de alpha_bar.sqrt(). Il existe des thĂ©ories sur la âbonneâ maniĂšre dâĂ©chelonner ces gradients, mais en pratique, vous pouvez expĂ©rimenter. Pour certains types de guidage, vous voudrez peut-ĂȘtre que la plupart des effets soient concentrĂ©s dans les premiĂšres Ă©tapes, pour dâautres (par exemple, une perte de style axĂ©e sur les textures), vous prĂ©fĂ©rerez peut-ĂȘtre quâils nâinterviennent que vers la fin du processus de gĂ©nĂ©ration. Quelques programmes possibles sont prĂ©sentĂ©s ci-dessous :
plt.plot([1 for a in scheduler.alphas_cumprod], label="no scaling")
plt.plot([a for a in scheduler.alphas_cumprod], label="alpha_bar")
plt.plot([a.sqrt() for a in scheduler.alphas_cumprod], label="alpha_bar.sqrt()")
plt.plot(
[(1 - a).sqrt() for a in scheduler.alphas_cumprod], label="(1-alpha_bar).sqrt()"
)
plt.legend()
plt.title("Possible guidance scaling schedules")
ExpĂ©rimentez avec diffĂ©rents planificateurs, Ă©chelles de guidage et toute autre astuce Ă laquelle vous pouvez penser (lâĂ©crĂȘtage des gradients dans une certaine plage est une modification populaire) pour voir jusquâĂ quel point vous pouvez obtenir ce rĂ©sultat ! Nâoubliez pas non plus dâessayer dâintervertir dâautres modĂšles. Peut-ĂȘtre le modĂšle de visages que nous avons chargĂ© au dĂ©but ; pouvez-vous le guider de maniĂšre fiable pour produire un visage masculin ? Que se passe-t-il si vous combinez le guidage CLIP avec la perte de couleur que nous avons utilisĂ©e plus tĂŽt ? Etc.
Si vous consultez quelques codes pour la diffusion guidĂ©e par CLIP en pratique, vous verrez une approche plus complexe avec une meilleure classe pour choisir des dĂ©coupes alĂ©atoires dans les images et de nombreux ajustements supplĂ©mentaires de la fonction de perte pour de meilleures performances. Avant lâapparition des modĂšles de diffusion conditionnĂ©s par le texte, il sâagissait du meilleur systĂšme de conversion texte-image qui soit ! La petite version de notre jouet peut encore ĂȘtre amĂ©liorĂ©e, mais elle capture lâidĂ©e principale : grĂące au guidage et aux capacitĂ©s Ă©tonnantes de CLIP, nous pouvons ajouter le contrĂŽle du texte Ă un modĂšle de diffusion inconditionnel đš.
Partager une boucle dâĂ©chantillonnage personnalisĂ©e en tant que dĂ©mo Gradio
Vous avez peut-ĂȘtre trouvĂ© une perte amusante pour guider la gĂ©nĂ©ration et vous souhaitez maintenant partager avec le monde entier votre modĂšle finetunĂ© et cette stratĂ©gie dâĂ©chantillonnage personnalisĂ©eâŠ
Entrez dans Gradio. Gradio est un outil gratuit et open-source qui permet aux utilisateurs de crĂ©er et de partager facilement des modĂšles interactifs dâapprentissage automatique via une simple interface web. Avec Gradio, les utilisateurs peuvent construire des interfaces personnalisĂ©es pour leurs modĂšles dâapprentissage automatique, qui peuvent ensuite ĂȘtre partagĂ©s avec dâautres par le biais dâune URL unique. Il est Ă©galement intĂ©grĂ© Ă đ€ Spaces, ce qui permet dâhĂ©berger facilement des dĂ©mos et de les partager avec dâautres.
Nous placerons notre logique de base dans une fonction qui prend certaines entrĂ©es et produit une image en sortie. Cette fonction peut ensuite ĂȘtre enveloppĂ©e dans une interface simple qui permet Ă lâutilisateur de spĂ©cifier certains paramĂštres (qui sont transmis en tant quâentrĂ©es Ă la fonction principale de gĂ©nĂ©ration). De nombreux composants sont disponibles ; pour cet exemple, nous utiliserons un curseur pour lâĂ©chelle dâorientation et un sĂ©lecteur de couleurs pour dĂ©finir la couleur cible.
!pip install -q gradio
import gradio as gr
from PIL import Image, ImageColor
# La fonction qui fait le gros du travail
def generate(color, guidance_loss_scale):
target_color = ImageColor.getcolor(color, "RGB") # Couleur cible en RGB
target_color = [a / 255 for a in target_color] # Rééchelonner de (0, 255) à (0, 1)
x = torch.randn(1, 3, 256, 256).to(device)
for i, t in tqdm(enumerate(scheduler.timesteps)):
model_input = scheduler.scale_model_input(x, t)
with torch.no_grad():
noise_pred = image_pipe.unet(model_input, t)["sample"]
x = x.detach().requires_grad_()
x0 = scheduler.step(noise_pred, t, x).pred_original_sample
loss = color_loss(x0, target_color) * guidance_loss_scale
cond_grad = -torch.autograd.grad(loss, x)[0]
x = x.detach() + cond_grad
x = scheduler.step(noise_pred, t, x).prev_sample
grid = torchvision.utils.make_grid(x, nrow=4)
im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
im = Image.fromarray(np.array(im * 255).astype(np.uint8))
im.save("test.jpeg")
return im
# Voir la documentation de gradio pour les types d'entrées et de sorties disponibles.
inputs = [
gr.ColorPicker(label="color", value="55FFAA"), # Ajoutez ici toutes les entrées dont vous avez besoin
gr.Slider(label="guidance_scale", minimum=0, maximum=30, value=3),
]
outputs = gr.Image(label="result")
# Et l'interface minimale
demo = gr.Interface(
fn=generate,
inputs=inputs,
outputs=outputs,
examples=[
["#BB2266", 3],
["#44CCAA", 5], # Vous pouvez fournir des exemples d'entrées pour aider les gens à démarrer
],
)
demo.launch(debug=True) # debug=True vous permet de voir les erreurs et les sorties dans Colab
Il est possible de construire des interfaces beaucoup plus compliquĂ©es, avec un style fantaisiste et un large Ă©ventail dâentrĂ©es possibles, mais pour cette dĂ©mo, nous la gardons aussi simple que possible.
Les dĂ©mos sur đ€ Spaces sâexĂ©cutent par dĂ©faut sur CPU, il est donc prĂ©fĂ©rable de prototyper votre interface dans Colab (comme ci-dessus) avant de la migrer. Lorsque vous ĂȘtes prĂȘt Ă partager votre dĂ©mo, vous devez crĂ©er un Space, mettre en place un fichier requirements.txt listant les bibliothĂšques que votre code utilisera, puis placer tout le code dans un fichier app.py qui dĂ©finit les fonctions pertinentes et lâinterface.
Heureusement pour vous, il est Ă©galement possible de âdupliquerâ un Space. Vous pouvez visiter le Space ici et cliquer sur âDupliquer cet espaceâ pour obtenir un modĂšle que vous pouvez ensuite modifier pour utiliser votre propre modĂšle et votre propre fonction dâorientation.
Dans les paramĂštres, vous pouvez configurer votre Space pour quâil fonctionne avec du matĂ©riel plus sophistiquĂ© (qui est facturĂ© Ă lâheure). Vous avez créé quelque chose dâextraordinaire et vous voulez le partager sur un meilleur matĂ©riel, mais vous nâavez pas lâargent nĂ©cessaire ? Faites-le nous savoir via Discord et nous verrons si nous pouvons vous aider !
Résumé et prochaines étapes
Nous avons couvert beaucoup de choses dans ce notebook ! Récapitulons les idées principales :
- Il est relativement facile de charger des modÚles existants et de les échantillonner avec différents planificateurs
- Le finetuning ressemble Ă lâentraĂźnement Ă partir de zĂ©ro, sauf quâen partant dâun modĂšle existant, nous espĂ©rons obtenir de meilleurs rĂ©sultats plus rapidement.
- Pour finetuner de grands modĂšles sur de grandes images, nous pouvons utiliser des astuces comme lâaccumulation de gradient pour contourner les limitations de la taille des batchs.
- Lâenregistrement dâĂ©chantillons dâimages est important pour le finetuning, oĂč une courbe de perte peut ne pas fournir beaucoup dâinformations utiles.
- Le guidage nous permet de prendre un modĂšle inconditionnel et dâorienter le processus de gĂ©nĂ©ration sur la base dâune fonction de guidage/perte, oĂč Ă chaque Ă©tape nous trouvons le gradient de la perte par rapport Ă lâimage bruitĂ©e $x$ et lâactualisons en fonction de ce gradient avant de passer Ă lâĂ©tape temporelle suivante.
- Le guidage avec CLIP nous permet de contrĂŽler des modĂšles inconditionnels avec du texte !
Pour mettre cela en pratique, voici quelques étapes spécifiques que vous pouvez suivre :
- FinetunĂ© votre propre modĂšle et le pousser vers le Hub. Cela implique de choisir un point de dĂ©part (par exemple, un modĂšle entraĂźnĂ© sur faces, bedrooms, cats ou wikiart et un jeu de donnĂ©es (peut-ĂȘtre ces faces dâanimaux ou vos propres images), puis dâentraĂźner soit le code de ce notebook, soit le script dâexemple (utilisation de dĂ©monstration ci-dessous).
- Explorer le guidage en utilisant votre modĂšle finetunĂ©, soit en utilisant lâune des fonctions de guidage de lâexemple (color_loss ou CLIP), soit en inventant la vĂŽtre.
- Partagez une dĂ©mo basĂ©e sur ceci en utilisant Gradio, soit en modifiant le Space dâexemple pour utiliser votre propre modĂšle, soit en crĂ©ant votre propre version personnalisĂ©e avec plus de fonctionnalitĂ©s.
Nous sommes impatients de voir vos rĂ©sultats sur Discord, Twitter et ailleurs đ€ !
2.2. ModÚle de diffusion conditionné par la classe
Dans ce notebook, nous allons illustrer une façon dâajouter des informations de conditionnement Ă un modĂšle de diffusion. Plus prĂ©cisĂ©ment, nous allons entraĂźner un modĂšle de diffusion conditionnĂ© par la classe sur MNIST Ă la suite de lâexemple dâentraĂźnement Ă partir de 0 de lâunitĂ© 1, oĂč nous pouvons spĂ©cifier quel chiffre nous voulons que le modĂšle gĂ©nĂšre au moment de lâinfĂ©rence.
Comme indiquĂ© dans lâintroduction de cette unitĂ©, il sâagit dâune des nombreuses façons dâajouter des informations de conditionnement supplĂ©mentaires Ă un modĂšle de diffusion, et elle a Ă©tĂ© choisie pour sa relative simplicitĂ©. Tout comme le notebook de lâunitĂ© 1, ce notebook nâa quâun but illustratif et vous pouvez lâignorer si vous le souhaitez.
Configuration et préparation des données
!pip install -q diffusers
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
from diffusers import DDPMScheduler, UNet2DModel
from matplotlib import pyplot as plt
from tqdm.auto import tqdm
device = 'mps' if torch.backends.mps.is_available() else 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using device: {device}')
# Charger le jeu de données
dataset = torchvision.datasets.MNIST(root="mnist/", train=True, download=True, transform=torchvision.transforms.ToTensor())
# Introduire les données dans un chargeur de données (batch de taille 8 ici pour la démonstration)
train_dataloader = DataLoader(dataset, batch_size=8, shuffle=True)
# Visualiser quelques exemples
x, y = next(iter(train_dataloader))
print('Input shape:', x.shape)
print('Labels:', y)
plt.imshow(torchvision.utils.make_grid(x)[0], cmap='Greys')
Input shape: torch.Size([8, 1, 28, 28])
Labels: tensor([8, 1, 5, 9, 7, 6, 2, 2])
CrĂ©ation dâune UNet conditionnĂ©e par la classe
La façon dont nous introduirons le conditionnement de la classe est la suivante :
- Créer un
UNet2DModelstandard avec quelques canaux dâentrĂ©e supplĂ©mentaires. - Associer lâĂ©tiquette de la classe Ă un vecteur appris de forme (
class_emb_size) via une couche dâenchĂąssement. - ConcatĂ©ner ces informations en tant que canaux supplĂ©mentaires pour lâentrĂ©e interne du UNet avec
net_input = torch.cat((x, class_cond), 1) - Introduire ce
net_input(qui a (class_emb_size+1) canaux au total) dans lâUNet pour obtenir la prĂ©diction finale.
Dans cet exemple, nous avons fixĂ© la taille de class_emb_size Ă 4, mais câest complĂštement arbitraire et vous pourriez envisager de la fixer Ă 1 (pour voir si cela fonctionne toujours), Ă 10 (pour correspondre au nombre de classes), ou de remplacer le nn.Embedding appris par un simple encodage Ă un coup de lâĂ©tiquette de la classe directement.
Voici Ă quoi ressemble lâimplĂ©mentation :
class ClassConditionedUNet(nn.Module):
def __init__(self, num_classes=10, class_emb_size=4):
super().__init__()
# La couche d'intégration associe l'étiquette de la classe à un vecteur de taille `class_emb_size`
self.class_emb = nn.Embedding(num_classes, class_emb_size)
# Self.model est un UNet inconditionnel avec des canaux d'entrée supplémentaires pour accepter les informations de conditionnement (l'enchùssement de la classe).
self.model = UNet2DModel(
sample_size=28, # la résolution de l'image cible
in_channels=1 + class_emb_size, # Canaux d'entrée supplémentaires pour la classe conditionnée
out_channels=1, # le nombre de canaux de sortie
layers_per_block=2, # le nombre de couches ResNet Ă utiliser par bloc UNet
block_out_channels=(32, 64, 64),
down_block_types=(
"DownBlock2D", # un bloc de sous-échantillonnage ResNet standard
"AttnDownBlock2D", # un bloc de sous-échantillonnage ResNet avec auto-attention spatiale
"AttnDownBlock2D",
),
up_block_types=(
"AttnUpBlock2D",
"AttnUpBlock2D", # un bloc de suréchantillonnage ResNet avec auto-attention spatiale
"UpBlock2D", # un bloc de suréchantillonnage ResNet standard
),
)
# Notre méthode de transfert prend maintenant les étiquettes de la classe comme argument supplémentaire
def forward(self, x, t, class_labels):
# Forme de x :
bs, ch, w, h = x.shape
# conditionnement de la classe en bon état pour l'ajouter comme canaux d'entrée supplémentaires
class_cond = self.class_emb(class_labels) # Map to embedding dinemsion
class_cond = class_cond.view(bs, class_cond.shape[1], 1, 1).expand(bs, class_cond.shape[1], w, h)
# x est de forme (bs, 1, 28, 28) et class_cond est maintenant (bs, 4, 28, 28)
# L'entrée nette est maintenant x et la classe cond concaténée ensemble le long de la dimension 1
net_input = torch.cat((x, class_cond), 1) # (bs, 5, 28, 28)
# Cette information est transmise Ă l'UNet en mĂȘme temps que le pas de temps et renvoie la prĂ©diction
return self.model(net_input, t).sample # (bs, 1, 28, 28)
Si lâune des formes ou des transformations vous semble confuse, ajoutez des print() pour afficher les formes pertinentes et vĂ©rifiez quâelles correspondent Ă vos attentes. Nous avons Ă©galement annotĂ© les formes de certaines variables intermĂ©diaires dans lâespoir de rendre les choses plus claires.
Entraßnement et échantillonnage
Alors quâauparavant nous faisions quelque chose comme prediction = UNet(x, t), nous allons maintenant ajouter les bonnes Ă©tiquettes comme troisiĂšme argument (prediction = UNet(x, t, y)) pendant lâentraĂźnement, et lors de lâinfĂ©rence nous pouvons passer les Ă©tiquettes que nous voulons et si tout va bien le modĂšle devrait gĂ©nĂ©rer des images qui correspondent. $y$ dans ce cas est lâĂ©tiquette des chiffres MNIST, avec des valeurs de 0 Ă 9.
La boucle dâentraĂźnement est trĂšs similaire Ă lâexemple de lâunitĂ© 1. Nous prĂ©disons maintenant le bruit (plutĂŽt que lâimage dĂ©bruitĂ©e comme dans lâunitĂ© 1) pour correspondre Ă lâobjectif attendu par le DDPMScheduler par dĂ©faut que nous utilisons pour ajouter du bruit pendant lâentraĂźnement et pour gĂ©nĂ©rer des Ă©chantillons au moment de lâinfĂ©rence. LâentraĂźnement prend du temps. LâaccĂ©lĂ©rer pourrait ĂȘtre un mini-projet amusant, mais la plupart dâentre vous peuvent probablement parcourir le code (et en fait tout ce notebook) sans lâexĂ©cuter puisque nous ne faisons quâillustrer une idĂ©e.
# Créer un planificateur
noise_scheduler = DDPMScheduler(num_train_timesteps=1000, beta_schedule='squaredcos_cap_v2')
# Redéfinition du chargeur de données pour fixer la taille du batch à un niveau supérieur à la démonstration de 8
train_dataloader = DataLoader(dataset, batch_size=128, shuffle=True)
# Combien de fois devrions-nous passer les données en revue ?
n_epochs = 10
# Notre réseau
net = ClassConditionedUNet().to(device)
# Notre fonction de perte
loss_fn = nn.MSELoss()
# L'optimiseur
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
# Conserver une trace des pertes pour les consulter ultérieurement
losses = []
# La boucle d'entraĂźnement
for epoch in range(n_epochs):
for x, y in tqdm(train_dataloader):
# Obtenir des données et préparer la version corrompue
x = x.to(device) * 2 - 1 # Données sur le GPU (sur (-1, 1))
y = y.to(device)
noise = torch.randn_like(x)
timesteps = torch.randint(0, 999, (x.shape[0],)).long().to(device)
noisy_x = noise_scheduler.add_noise(x, noise, timesteps)
# Obtenir la prédiction du modÚle
pred = net(noisy_x, timesteps, y) # Notez que nous passons les étiquettes y
# Calculer la perte
loss = loss_fn(pred, noise) # Quelle est la distance entre la sortie et le bruit ?
# Rétropopagation et mise à jour des paramÚtres :
opt.zero_grad()
loss.backward()
opt.step()
# Stocker la perte pour plus tard
losses.append(loss.item())
# Afficher la moyenne des 100 derniÚres valeurs de perte pour vous faire une idée de la progression :
avg_loss = sum(losses[-100:])/100
print(f'Finished epoch {epoch}. Average of the last 100 loss values: {avg_loss:05f}')
# Visualiser la courbe des pertes
plt.plot(losses)
Une fois lâentraĂźnement terminĂ©, nous pouvons Ă©chantillonner quelques images en introduisant diffĂ©rentes Ă©tiquettes comme conditionnement :
# Préparer un x aléatoire comme point de départ, ainsi que les étiquettes souhaitées y
x = torch.randn(80, 1, 28, 28).to(device)
y = torch.tensor([[i]*8 for i in range(10)]).flatten().to(device)
# Boucle d'échantillonnage
for i, t in tqdm(enumerate(noise_scheduler.timesteps)):
# Obtenir la prédiction du modÚle
with torch.no_grad():
residual = net(x, t, y) # Notez à nouveau que nous transmettons nos étiquettes y
# Mise à jour de l'échantillon avec l'étape
x = noise_scheduler.step(residual, t, x).prev_sample
# Montrer les résultats
fig, ax = plt.subplots(1, 1, figsize=(12, 12))
ax.imshow(torchvision.utils.make_grid(x.detach().cpu().clip(-1, 1), nrow=8)[0], cmap='Greys')
Nous y voilĂ ! Nous pouvons maintenant contrĂŽler les images produites.
Nous espĂ©rons que cet exemple vous a plu. Comme toujours, nâhĂ©sitez pas Ă poser des questions sur Discord.
âïž Ă votre tour !
Essayez de refaire la mĂȘme chose avec FashionMNIST. Modifiez le taux dâapprentissage, la taille du batch et le nombre dâĂ©poques.Pouvez-vous obtenir des images de mode dĂ©centes avec moins de temps dâentraĂźnement que dans lâexemple ci-dessus ?
3. Vue d'ensemble
Dans cette unitĂ©, vous allez dĂ©couvrir un puissant modĂšle de diffusion appelĂ© Stable Diffusion (SD) et explorer ce quâil peut faire.
Vue dâensemble de cette unitĂ©
Les différentes étapes à suivre pour cette unité :
- Lisez le matĂ©riel ci-dessous pour avoir une vue dâensemble des idĂ©es clĂ©s de cette unitĂ©
- Consultez le notebook Introduction Ă Stable Diffusion pour voir lâapplication pratique de SD dans des cas dâutilisation courants.
- (Facultatif) Consultez la vidĂ©o Stable Diffusion Deep Dive (en anglais) et le notebook qui lâaccompagne pour une exploration plus approfondie des diffĂ©rents composants et de la façon dont ils peuvent ĂȘtre adaptĂ©s Ă diffĂ©rents effets. Ce matĂ©riel a Ă©tĂ© créé pour le cours de FastAI, Stable Diffusion from the Foundations (en anglais), ce qui en fait un excellent complĂ©ment Ă ce cours pour tous ceux qui sont curieux de construire ce type de modĂšles Ă partir de zĂ©ro.
Introduction

Exemples dâimages gĂ©nĂ©rĂ©es Ă lâaide de Stable Diffusion
Stable Diffusion est un puissant modÚle de diffusion latent conditionné par le texte. Ne vous inquiétez pas, nous expliquerons ces mots dans quelques instants ! Sa capacité à créer des images étonnantes à partir de descriptions textuelles en a fait une sensation sur Internet. Dans cette unité, nous allons explorer le fonctionnement du modÚle de diffusion latent et voir quels sont ses autres atouts.
Diffusion latente
Plus la taille des images augmente, plus la puissance de calcul nĂ©cessaire pour travailler avec ces images sâaccroĂźt. Ceci est particuliĂšrement prononcĂ© dans une opĂ©ration appelĂ©e auto-attention, oĂč le nombre dâopĂ©rations croĂźt de façon quadratique avec le nombre dâentrĂ©es. Une image carrĂ©e de 128 pixels a 4 fois plus de pixels quâune image carrĂ©e de 64 pixels, et nĂ©cessite donc 16 fois (câest-Ă -dire 4ÂČ) la mĂ©moire et le calcul dans une couche dâauto-attention. Ce problĂšme se pose pour tous ceux qui souhaitent gĂ©nĂ©rer des images en haute rĂ©solution !

Diagramme provenant du papier High-Resolution Image Synthesis with Latent Diffusion Models
La diffusion latente permet dâattĂ©nuer ce problĂšme en utilisant un modĂšle distinct appelĂ© auto-encodeur variationnel (VAE pour Variational Auto-Encoder) pour compresser les images dans une dimension spatiale plus petite. Le raisonnement sous-jacent est que les images ont tendance Ă contenir une grande quantitĂ© dâinformations redondantes. Avec suffisamment de donnĂ©es dâentraĂźnement, un VAE peut espĂ©rer apprendre Ă produire une reprĂ©sentation beaucoup plus petite dâune image dâentrĂ©e et ensuite reconstruire lâimage sur la base de cette petite reprĂ©sentation latente avec un haut degrĂ© de fidĂ©litĂ©. Le VAE utilisĂ© dans SD prend des images Ă 3 canaux et produit une reprĂ©sentation latente Ă 4 canaux avec un facteur de rĂ©duction de 8 pour chaque dimension spatiale. En dâautres termes, une image dâentrĂ©e carrĂ©e de 512 pixels sera compressĂ©e en une reprĂ©sentation latente de 4x64x64.
En appliquant le processus de diffusion Ă ces reprĂ©sentations latentes plutĂŽt quâĂ des images en pleine rĂ©solution, nous pouvons bĂ©nĂ©ficier de nombreux avantages liĂ©s Ă lâutilisation dâimages plus petites (moins dâutilisation de mĂ©moire, moins de couches nĂ©cessaires dans le UNet, des temps de gĂ©nĂ©ration plus rapidesâŠ) tout en dĂ©codant le rĂ©sultat en une image en haute rĂ©solution une fois que nous sommes prĂȘts Ă voir le rĂ©sultat final. Cette solution permet de rĂ©duire considĂ©rablement le coĂ»t de lâentraĂźnement et dâexĂ©cution de ces modĂšles.
Conditionnement
Dans lâunitĂ© 2, nous avons montrĂ© comment lâapport dâinformations supplĂ©mentaires au UNet nous permet dâexercer un contrĂŽle supplĂ©mentaire sur les types dâimages gĂ©nĂ©rĂ©es. Câest ce que nous appelons le conditionnement. Ătant donnĂ© une version bruitĂ©e dâune image, le modĂšle est chargĂ© de prĂ©dire la version dĂ©bruitĂ©e en fonction dâindices supplĂ©mentaires tels quâune Ă©tiquette de classe ou, dans le cas de Stable Diffusion, une description textuelle de lâimage. Au moment de lâinfĂ©rence, nous pouvons introduire la description dâune image que nous aimerions voir et un peu de bruit pur comme point de dĂ©part, et le modĂšle fait de son mieux pour « dĂ©bruiter » lâentrĂ©e alĂ©atoire en quelque chose qui corresponde Ă la lĂ©gende.

Diagramme montrant le processus dâencodage de texte qui transforme le prompt dâentrĂ©e en un ensemble dâenchĂąssements de texte (les encoder_hidden_states) qui peuvent ensuite ĂȘtre introduits dans lâUNet en tant que condition.
Pour que cela fonctionne, nous devons crĂ©er une reprĂ©sentation numĂ©rique du texte qui capture des informations pertinentes sur ce quâil dĂ©crit. Pour ce faire, SD sâappuie sur un transformer prĂ©-entraĂźnĂ© basĂ© sur ce que lâon appelle CLIP. Lâencodeur textuel de CLIP a Ă©tĂ© conçu pour traiter les lĂ©gendes dâimages sous une forme pouvant ĂȘtre utilisĂ©e pour comparer les images et le texte, il est donc bien adaptĂ© Ă la tĂąche de crĂ©ation de reprĂ©sentations utiles Ă partir de descriptions dâimages. Un prompt est dâabord tokenizer (sur la base dâun large vocabulaire oĂč chaque mot ou sous-mot se voit attribuer un token spĂ©cifique), puis transmis Ă lâencodeur textuel de CLIP, qui produit un vecteur Ă 768 dimensions (dans le cas de SD 1.X) ou Ă 1024 dimensions (SD 2.X) pour chaque tokens. Pour que les choses restent cohĂ©rentes, les prompts sont toujours rembourrĂ©s/tronquĂ©s pour avoir une longueur de 77 tokens, de sorte que la reprĂ©sentation finale que nous utilisons comme conditionnement est un tenseur de forme 77x1024 par prompt.

Alors, comment introduire ces informations de conditionnement dans lâUNet pour quâil les utilise dans ses prĂ©dictions ? La rĂ©ponse est ce que lâon appelle lâattention croisĂ©e. Des couches dâattention croisĂ©e sont dissĂ©minĂ©es dans lâUNet. Chaque emplacement spatial de lâUNet peut « sâintĂ©resser » Ă diffĂ©rents tokens dans le conditionnement du texte, en apportant des informations pertinentes provenant du prompt. Le diagramme ci-dessus montre comment ce conditionnement textuel (ainsi que le conditionnement basĂ© sur le temps) est fourni Ă diffĂ©rents endroits. Comme vous pouvez le constater, Ă chaque niveau, lâUNet a de nombreuses possibilitĂ©s dâutiliser ce conditionnement !
Guidage sans classification
Il sâavĂšre que mĂȘme avec tous les efforts dĂ©ployĂ©s pour rendre le texte de conditionnement aussi utile que possible, le modĂšle a toujours tendance Ă sâappuyer principalement sur lâimage dâentrĂ©e bruyante plutĂŽt que sur le prompt lorsquâil fait ses prĂ©dictions. Dâune certaine maniĂšre, de nombreuses lĂ©gendes ne sont que vaguement liĂ©es aux images qui leur sont associĂ©es et le modĂšle apprend donc Ă ne pas trop sâappuyer sur les descriptions ! Toutefois, cela nâest pas souhaitable lorsquâil sâagit de gĂ©nĂ©rer de nouvelles images : si le modĂšle ne suit pas le prompt, nous risquons dâobtenir des images qui ne sont pas du tout liĂ©es Ă notre description.

Images gĂ©nĂ©rĂ©es Ă partir du prompt « Une peinture Ă lâhuile dâun colley avec un chapeau haut de forme » avec lâĂ©chelle CFG 0, 1, 2 et 10 (de gauche Ă droite)
Pour rĂ©soudre ce problĂšme, nous utilisons une astuce appelĂ©e « Classifier-Free Guidance » (CGF). Pendant lâentraĂźnement, le conditionnement du texte est parfois laissĂ© en blanc, ce qui oblige le modĂšle Ă apprendre Ă dĂ©bruiter les images sans aucune information textuelle (gĂ©nĂ©ration inconditionnelle). Ensuite, au moment de lâinfĂ©rence, nous faisons deux prĂ©dictions distinctes : lâune avec le texte prompt comme conditionnement et lâautre sans. Nous pouvons ensuite utiliser la diffĂ©rence entre ces deux prĂ©dictions pour crĂ©er une prĂ©diction combinĂ©e finale qui pousse encore plus loin dans la direction indiquĂ©e par la prĂ©diction conditionnĂ©e par le texte selon un certain facteur dâĂ©chelle (lâĂ©chelle de guidage), avec lâespoir dâobtenir une image qui corresponde mieux au prompt. Lâimage ci-dessus montre les rĂ©sultats dâun prompt Ă diffĂ©rentes Ă©chelles de guidage. Comme vous pouvez le voir, des valeurs plus Ă©levĂ©es donnent des images qui correspondent mieux Ă la description.
Autres types de conditionnement : super-rĂ©solution, peinture et profondeur dâimage
Il est possible de crĂ©er des versions de Stable Diffusion qui prennent en compte dâautres types de conditionnement. Par exemple, le modĂšle Depth-to-Image model possĂšde des canaux dâentrĂ©e supplĂ©mentaires qui recueillent des informations approfondies sur lâimage en cours de dĂ©bruitage et, au moment de lâinfĂ©rence, nous pouvons introduire la carte de profondeur dâune image cible (estimĂ©e Ă lâaide dâun modĂšle distinct) pour espĂ©rer gĂ©nĂ©rer une image dont la structure globale est similaire.

SD conditionnĂ© par la profondeur est capable de gĂ©nĂ©rer des images diffĂ©rentes avec la mĂȘme structure globale (exemple provenant de StabilityAI)
De la mĂȘme maniĂšre, nous pouvons introduire une image basse rĂ©solution comme conditionnement et demander au modĂšle de gĂ©nĂ©rer la version haute rĂ©solution (comme utilisĂ© par le Stable Diffusion Upscaler). Enfin, nous pouvons introduire un masque montrant une rĂ©gion de lâimage Ă rĂ©gĂ©nĂ©rer dans le cadre de la tĂąche de complĂ©tion dâimage (inpainting), oĂč les rĂ©gions non masquĂ©es doivent rester intactes tandis que le nouveau contenu est gĂ©nĂ©rĂ© pour la zone masquĂ©e.
Finetuning avec DreamBooth
Image provenant de la page du projet dreambooth basée sur le modÚle Imagen
DreamBooth est une technique permettant de finetuner un modĂšle texte-image afin de lui « apprendre » un nouveau concept, tel quâun objet ou un style spĂ©cifique. La technique a Ă©tĂ© dĂ©veloppĂ©e Ă lâorigine pour le modĂšle Imagen de Google, mais a Ă©tĂ© rapidement adaptĂ©e pour fonctionner pour Stable Diffusion. Les rĂ©sultats peuvent ĂȘtre extrĂȘmement impressionnants (si vous avez vu quelquâun avec une photo de profil IA sur les mĂ©dias sociaux rĂ©cemment, il y a de fortes chances quâelle provienne dâun service basĂ© sur Dreambooth), mais la technique est aussi sensible aux paramĂštres utilisĂ©s, alors consultez notre notebook et cet article de blog sur les diffĂ©rents paramĂštres dâentraĂźnement pour obtenir des conseils sur la façon de la faire fonctionner le mieux possible.
Notebooks
| Chapitre | Colab | Kaggle | Gradient | Studio Lab |
|---|---|---|---|---|
| Introduction Ă Stable Diffusion | ||||
| Plongée dans Stable Diffusion |
Le notebook Stable Diffusion Introduction est une courte introduction Ă Stable Diffusion avec la bibliothĂšque đ€ Diffusers, prĂ©sentant quelques exemples dâutilisation de base en utilisant des pipelines pour gĂ©nĂ©rer et modifier des images.
Enfin, le notebook et la vidĂ©o Stable Diffusion Deep Dive dĂ©composent chaque Ă©tape dâun pipeline de gĂ©nĂ©ration typique, en suggĂ©rant de nouvelles façons de modifier chaque Ă©tape pour un contrĂŽle crĂ©atif supplĂ©mentaire.
Project
LIEN VERS LâEVENT
Ressources complémentaires
Une liste non exhaustive de ressources (en anglais) Ă consulter :
- High-Resolution Image Synthesis with Latent Diffusion Models, le papier qui a introduit lâapproche derriĂšre Stable Diffusion
- CLIP apprend Ă relier le texte aux images et lâencodeur textuel est utilisĂ© pour transformer un prompt textuel en la riche reprĂ©sentation numĂ©rique utilisĂ©e par SD. Voir Ă©galement cet article sur OpenCLIP pour en savoir plus sur les rĂ©centes variantes de CLIP open-source (dont lâune est utilisĂ©e pour la version 2 de SD).
- GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models un papier précoce démontrant le conditionnement de texte et le CFG
Vous avez identifiĂ© dâautres ressources intĂ©ressantes ? Faites-le nous savoir et nous les ajouterons Ă cette liste.
3.1. Introduction Ă Stable Diffusion
Ce notebook va couvrir les bases de lâutilisation de Stable Diffusion pour crĂ©er et modifier des images en utilisant les pipelines existants. Nous allons Ă©galement jeter un bref coup dâĆil aux composants clĂ©s au sein du pipeline, tout en laissant une exploration plus approfondie de ces derniers au notebook de plongĂ©e profonde. Plus prĂ©cisĂ©ment, nous aborderons les points suivants :
- GĂ©nĂ©rer des images Ă partir dâun texte en utilisant le
StableDiffusionPipelineet en expérimentant avec les arguments disponibles - Voir certains des composants clés du pipeline en action
- Le VAE qui en fait un « modÚle de diffusion latent ».
- Le tokenizer et lâencodeur qui traitent le prompt textuel
- LâUNet lui-mĂȘme
- Le planificateur et lâexploration de diffĂ©rents planificateurs
- Reproduction de la boucle dâĂ©chantillonnage avec les composants du pipeline
- Ădition dâimages existantes avec le pipeline Img2Img
- Utilisation des pipelines de complĂ©tion dâimage et Depth2Img
Configuration
!pip install -Uq diffusers ftfy accelerate
# Installer transformers Ă partir de la source car nous avons besoin de la derniĂšre version pour Depth2Img
!pip install -Uq git+https://github.com/huggingface/transformers
import torch
import requests
from PIL import Image
from io import BytesIO
from matplotlib import pyplot as plt
# Nous allons explorer un certain nombre de pipelines aujourd'hui !
from diffusers import (
StableDiffusionPipeline,
StableDiffusionImg2ImgPipeline,
StableDiffusionInpaintPipeline,
StableDiffusionDepth2ImgPipeline
)
# Nous utiliserons quelques images de démonstration plus loin dans le notebook
def download_image(url):
response = requests.get(url)
return Image.open(BytesIO(response.content)).convert("RGB")
# Télécharger des images pour l'exemple de complétion d'image
img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png"
mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png"
init_image = download_image(img_url).resize((512, 512))
mask_image = download_image(mask_url).resize((512, 512))
# Définir l'appareil
device = (
"mps"
if torch.backends.mps.is_available()
else "cuda"
if torch.cuda.is_available()
else "cpu"
)
GĂ©nĂ©rer des images Ă partir dâun texte
Chargeons un pipeline Stable Diffusion et voyons ce quâil peut faire. Il existe plusieurs versions de Stable Diffusion, la plus rĂ©cente Ă©tant la version 2.1. Si vous souhaitez explorer une version plus ancienne, remplacez simplement lâID du modĂšle par le modĂšle appropriĂ© (par exemple, vous pouvez essayer âCompVis/stable-diffusion-v1-4â ou choisir un modĂšle Ă partir de la bibliothĂšque de concepts dreambooth).
# Charger le pipeline
model_id = "stabilityai/stable-diffusion-2-1-base"
pipe = StableDiffusionPipeline.from_pretrained(model_id).to(device)
Si vous manquez de mĂ©moire GPU, vous pouvez faire certaines choses pour rĂ©duire lâutilisation de la RAM :
- Chargez la version FP16 (non supportĂ©e par tous les systĂšmes). Avec cette version, vous devrez peut-ĂȘtre convertir les tenseurs en torch.float16 lorsque vous expĂ©rimenterez avec les composants individuels du pipeline :
pipe = StableDiffusionPipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16).to(device) - Activer le dĂ©coupage de lâattention. Cela permet de rĂ©duire lâutilisation de la mĂ©moire du GPU au prix dâune lĂ©gĂšre rĂ©duction de la vitesse :
pipe.enable_attention_slicing() - Réduire la taille des images générées
Une fois le pipeline chargĂ©, nous pouvons gĂ©nĂ©rer une image sur la base dâun prompt avec le code suivant :
# Mise en place d'un générateur pour la reproductibilité
generator = torch.Generator(device=device).manual_seed(42)
# Exécuter le pipeline, en montrant certains des arguments disponibles
pipe_output = pipe(
prompt="Palette knife painting of an autumn cityscape", # Ce qu'il faut générer
negative_prompt="Oversaturated, blurry, low quality", # Ce qu'il ne faut PAS générer
height=480, width=640, # Spécifier la taille de l'image
guidance_scale=8, # Comment suivre avec fermeté le prompt
num_inference_steps=35, # Nombre d'étapes
generator=generator # Graine aléatoire fixe
)
# Visualiser l'image obtenue :
pipe_output.images[0]
âïž Ă votre tour !
Passez un peu de temps Ă jouer avec la cellule ci-dessus en utilisant vos propres prompts et en modifiant les paramĂštres pour voir comment ils affectent la sortie. Utilisez une graine alĂ©atoire diffĂ©rente ou supprimez lâargument du gĂ©nĂ©rateur pour obtenir des rĂ©sultats diffĂ©rents Ă chaque fois. Arguments clĂ©s Ă modifier :
- La largeur et la hauteur spĂ©cifient la taille de lâimage gĂ©nĂ©rĂ©e. Elles doivent ĂȘtre divisibles par 8 pour que le VAE fonctionne (ce que nous verrons dans une prochaine section).
- Le nombre de pas influence la qualitĂ© de la gĂ©nĂ©ration. La valeur par dĂ©faut (50) fonctionne bien, mais dans certains cas, on peut se contenter de 20 pas, ce qui est pratique pour lâexpĂ©rimentation.
- Le prompt nĂ©gatif est utilisĂ© pendant le processus dâorientation sans classifieur et peut ĂȘtre un moyen utile dâajouter un contrĂŽle supplĂ©mentaire. Vous pouvez ne pas lâutiliser, mais de nombreux utilisateurs trouvent utile de lister certaines descriptions indĂ©sirables dans le prompt nĂ©gatif, comme illustrĂ© ci-dessus.
- Lâargument guidance_scale dĂ©termine lâintensitĂ© du guidage sans classifieur (CFG pour classifier-free guidance). Des Ă©chelles plus Ă©levĂ©es poussent les images gĂ©nĂ©rĂ©es Ă mieux correspondre au prompt, mais si lâĂ©chelle est trop Ă©levĂ©e, les rĂ©sultats peuvent devenir sursaturĂ©s et dĂ©sagrĂ©ables.
Si vous souhaitez vous inspirer dâun prompt, le Stable Diffusion Prompt Book est un bon point de dĂ©part.
Vous pouvez voir lâeffet de lâaugmentation de lâĂ©chelle dâorientation dans la cellule suivante :
cfg_scales = [1.1, 8, 12] #@param
prompt = "A collie with a pink hat" #@param
fig, axs = plt.subplots(1, len(cfg_scales), figsize=(16, 5))
for i, ax in enumerate(axs):
im = pipe(prompt, height=480, width=480,
guidance_scale=cfg_scales[i], num_inference_steps=35,
generator=torch.Generator(device=device).manual_seed(42)).images[0]
ax.imshow(im); ax.set_title(f'CFG Scale {cfg_scales[i]}');
Modifiez les valeurs ci-dessus pour essayer diffĂ©rentes Ă©chelles et diffĂ©rents prompts. LâinterprĂ©tation est bien sĂ»r subjective, mais par expĂ©rience, toute valeur comprise entre 8 et 12 donne de meilleurs rĂ©sultats que les valeurs infĂ©rieures ou supĂ©rieures Ă cette fourchette.
Composants du pipeline
Le StableDiffusionPipeline que nous utilisons est un peu plus complexe que le DDPMPipeline que nous avons explorĂ© dans les unitĂ©s prĂ©cĂ©dentes. En plus du UNet et du planificateur, il y a un certain nombre dâautres composants inclus dans le pipeline :
print(list(pipe.components.keys())) # Liste des composants
['vae', 'text_encoder', 'tokenizer', 'unet', 'scheduler', 'safety_checker', 'feature_extractor']
Pour mieux comprendre le fonctionnement du pipeline, voyons briÚvement chaque composant en action individuellement, puis assemblons-les pour reproduire la fonctionnalité du pipeline.
Le VAE
Le VAE (auto-encodeur variationnel) est un type de modĂšle capable dâencoder son entrĂ©e dans une reprĂ©sentation comprimĂ©e, puis de dĂ©coder cette reprĂ©sentation latente pour la rendre proche de lâentrĂ©e dâorigine. Lors de la gĂ©nĂ©ration dâimages avec diffusion stable, nous gĂ©nĂ©rons dâabord les latents en appliquant le processus de diffusion dans lâespace latent du VAE, puis nous les dĂ©codons Ă la fin pour visualiser lâimage rĂ©sultante.
Voici un code qui prend une image dâentrĂ©e, lâencode dans une reprĂ©sentation latente et la dĂ©code Ă nouveau Ă lâaide de la VAE :
# Créez de fausses données (une image aléatoire, une plage (-1, 1))
images = torch.rand(1, 3, 512, 512).to(device) * 2 - 1
print("Input images shape:", images.shape)
# Encoder dans l'espace latent
with torch.no_grad():
latents = 0.18215 * pipe.vae.encode(images).latent_dist.mean
print("Encoded latents shape:", latents.shape)
# Décoder à nouveau
with torch.no_grad():
decoded_images = pipe.vae.decode(latents / 0.18215).sample
print("Decoded images shape:", decoded_images.shape)
Input images shape: torch.Size([1, 3, 512, 512])
Encoded latents shape: torch.Size([1, 4, 64, 64])
Decoded images shape: torch.Size([1, 3, 512, 512])
Comme vous pouvez le constater, lâimage 512x512 est compressĂ©e en une reprĂ©sentation latente 64x64 (avec quatre canaux). Cette rĂ©duction par 8 de chaque dimension spatiale est la raison pour laquelle la largeur et la hauteur spĂ©cifiĂ©es doivent ĂȘtre des multiples de 8.
Travailler avec ces latents 4x64x64 riches en informations est plus efficace que de travailler avec des images massives de 512 px, ce qui permet dâobtenir des modĂšles de diffusion plus rapides dont lâentraĂźnement et lâutilisation nĂ©cessitent moins de ressources. Le processus de dĂ©codage du VAE nâest pas parfait, mais il est suffisamment bon pour que le petit compromis de qualitĂ© en vaille gĂ©nĂ©ralement la peine.
NB : Lâexemple de code ci-dessus inclut un facteur dâĂ©chelle de 0,18215 nĂ©cessaire pour correspondre au traitement utilisĂ© lors de lâentraĂźnement de SD.
Le tokenizer et lâencodeur
Lâobjectif de lâencodeur est de transformer une chaĂźne dâentrĂ©e (le prompt) en une reprĂ©sentation numĂ©rique qui peut ĂȘtre transmise Ă lâUNet en tant que conditionnement. Le texte est dâabord transformĂ© en une sĂ©rie de tokens Ă lâaide du tokenizer du pipeline. Lâencodeur dispose dâun vocabulaire dâenviron 50K tokens et tout mot ne figurant pas dans ce vocabulaire est divisĂ© en sous-mots plus petits. Les tokens sont ensuite transmis Ă lâencodeur lui-mĂȘme : un transformer qui a Ă©tĂ© entraĂźnĂ© Ă lâorigine comme encodeur pour CLIP. Nous espĂ©rons que ce transformer prĂ©-entraĂźnĂ© a appris des reprĂ©sentations riches du texte qui seront Ă©galement utiles pour la tĂąche de diffusion.
Testons ce processus en encodant un prompt dâexemple, dâabord en le tokenizant manuellement et en le faisant passer par lâencodeur puis en utilisant la mĂ©thode _encode_prompt pour montrer le processus complet, y compris le rembourrage/la troncature de la longueur jusquâĂ la longueur maximale de 77 tokens :
# Tokenizer et encoder un prompt d'exemple manuellement :
# Tokenizer
input_ids = pipe.tokenizer(["A painting of a flooble"])['input_ids']
print("Input ID -> decoded token")
for input_id in input_ids[0]:
print(f"{input_id} -> {pipe.tokenizer.decode(input_id)}")
# Passage par l'encodeur de texte CLIP
input_ids = torch.tensor(input_ids).to(device)
with torch.no_grad():
text_embeddings = pipe.text_encoder(input_ids)['last_hidden_state']
print("Text embeddings shape:", text_embeddings.shape)
Input ID -> decoded token
49406 -> <|startoftext|>
320 -> a
3086 -> painting
539 -> of
320 -> a
4062 -> floo
1059 -> ble
49407 -> <|endoftext|>
Text embeddings shape: torch.Size([1, 8, 1024])
# Obtenir les enchĂąssements finaux Ă l'aide de la fonction _encode_prompt du pipeline :
text_embeddings = pipe._encode_prompt("A painting of a flooble", device, 1, False, '')
text_embeddings.shape
torch.Size([1, 77, 1024])
Ces enchĂąssements (les âĂ©tats cachĂ©sâ du dernier bloc de transformation dans le modĂšle de lâencodeur) seront transmis Ă lâUNet en tant quâargument supplĂ©mentaire de la mĂ©thode forward, que nous verrons dans la section suivante.
LâUNet
LâUNet prend une entrĂ©e bruitĂ©e et prĂ©dit le bruit, tout comme les UNets que nous avons vus dans les unitĂ©s prĂ©cĂ©dentes. Contrairement aux exemples prĂ©cĂ©dents, lâentrĂ©e nâest pas une image mais une reprĂ©sentation latente dâune image. En plus du conditionnement du pas de temps, ce UNet prend Ă©galement en compte lâenchĂąssement du prompt en tant quâentrĂ©e supplĂ©mentaire. Ici, il fait des prĂ©dictions sur des donnĂ©es fictives :
# Entrées fictives
timestep = pipe.scheduler.timesteps[0]
latents = torch.randn(1, 4, 64, 64).to(device)
text_embeddings = torch.randn(1, 77, 1024).to(device)
# Prédiction du modÚle
with torch.no_grad():
unet_output = pipe.unet(latents, timestep, text_embeddings).sample
print('UNet output shape:', unet_output.shape) # MĂȘme forme que les latents d'entrĂ©e
UNet output shape: torch.Size([1, 4, 64, 64])
Le planificateur
Le planificateur stocke le plan de bruit et gĂšre la mise Ă jour de lâĂ©chantillon bruitĂ© sur la base des prĂ©dictions du modĂšle. Le planificateur par dĂ©faut est un PNDMScheduler, mais vous pouvez en utiliser dâautres (comme LMSDiscreteScheduler) tant quâils sont initialisĂ©s avec la mĂȘme configuration.
Nous pouvons tracer le plan de bruit pour voir le niveau de bruit (basé sur $bar{\alpha}$) au fil du temps :
plt.plot(pipe.scheduler.alphas_cumprod, label=r'$\bar{\alpha}$')
plt.xlabel('Timestep (high noise to low noise ->)')
plt.title('Noise schedule');plt.legend()
Si vous souhaitez essayer un autre planificateur, vous pouvez le remplacer comme suit :
from diffusers import LMSDiscreteScheduler
# Remplacer le planificateur
pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config)
# Afficher la configuration
print('Scheduler config:', pipe.scheduler)
# Générer une image avec ce nouveau planificateur
pipe(prompt="Palette knife painting of an winter cityscape", height=480, width=480,
generator=torch.Generator(device=device).manual_seed(42)).images[0]
Scheduler config: LMSDiscreteScheduler {
"_class_name": "LMSDiscreteScheduler",
"_diffusers_version": "0.11.1",
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"clip_sample": false,
"num_train_timesteps": 1000,
"prediction_type": "epsilon",
"set_alpha_to_one": false,
"skip_prk_steps": true,
"steps_offset": 1,
"trained_betas": null
}
Vous pouvez lire plus de dĂ©tails sur lâutilisation de diffĂ©rents planificateurs ici.
Une boucle dâĂ©chantillonnage par vous-mĂȘme
Maintenant que nous avons vu tous ces composants en action, nous pouvons les assembler pour reproduire la fonctionnalité du pipeline :
guidance_scale = 8 #@param
num_inference_steps=30 #@param
prompt = "Beautiful picture of a wave breaking" #@param
negative_prompt = "zoomed in, blurry, oversaturated, warped" #@param
# Encoder le prompt
text_embeddings = pipe._encode_prompt(prompt, device, 1, True, negative_prompt)
# Créer notre point de départ aléatoire
latents = torch.randn((1, 4, 64, 64), device=device, generator=generator)
latents *= pipe.scheduler.init_noise_sigma
# Preparer le planificateur
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Boucle sur les pas de temps d'échantillonnage
for i, t in enumerate(pipe.scheduler.timesteps):
# développer les latents si l'on procÚde à un guidage sans classifieur
latent_model_input = torch.cat([latents] * 2)
# Appliquer tout échelonnement requis par le planificateur
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel avec l'UNet
with torch.no_grad():
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# réaliser un guidage
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# calculer l'échantillon bruité précédent x_t -> x_t-1
latents = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# Décoder les latents résultants en une image
with torch.no_grad():
image = pipe.decode_latents(latents.detach())
# Visualiser
pipe.numpy_to_pil(image)[0]
Dans la plupart des cas, il sera plus facile dâutiliser les pipelines existants, mais le fait de disposer de cette boucle dâĂ©chantillonnage bidouillable peut sâavĂ©rer utile pour comprendre et modifier le fonctionnement de chaque composant. Si vous souhaitez voir ce code et tous les diffĂ©rents composants explorĂ©s et modifiĂ©s en profondeur, consultez le notebook et la video âStable Diffusion Deep Diveâ pour une exploration plus approfondie.
Pipelines supplémentaires
Que pouvons-nous faire dâautre que de gĂ©nĂ©rer des images Ă partir dâun prompt ? Beaucoup de choses ! Dans cette section, nous allons dĂ©montrer quelques pipelines sympas pour vous donner un avant-goĂ»t des autres tĂąches pour lesquelles Stable Diffusion peut ĂȘtre utilisĂ©. Plusieurs dâentre eux nĂ©cessitent le tĂ©lĂ©chargement de nouveaux modĂšles, donc si vous ĂȘtes pressĂ©, vous pouvez parcourir cette section en vous contentant de regarder les rĂ©sultats existants plutĂŽt que de tĂ©lĂ©charger et dâexĂ©cuter tous les modĂšles vous-mĂȘme.
Img2Img
Dans les exemples prĂ©sentĂ©s jusquâĂ prĂ©sent, nous avons gĂ©nĂ©rĂ© des images en partant de latents alĂ©atoires et en appliquant la boucle complĂšte dâĂ©chantillonnage par diffusion. Mais il nâest pas nĂ©cessaire de partir de zĂ©ro. Le pipeline Img2Img encode dâabord une image existante dans un ensemble de latents, puis ajoute du bruit aux latents et utilise cette image comme point de dĂ©part. La quantitĂ© de bruit ajoutĂ©e et le nombre dâĂ©tapes de dĂ©bruitage appliquĂ©es dĂ©terminent la « force » du processus Img2Img. Lâajout dâune petite quantitĂ© de bruit (force faible) nâentraĂźnera que trĂšs peu de changements, tandis que lâajout dâune quantitĂ© maximale de bruit et lâexĂ©cution du processus de dĂ©bruitage complet donneront une image qui ne ressemblera guĂšre Ă lâimage dâentrĂ©e, hormis quelques similitudes au niveau de la structure gĂ©nĂ©rale.
Ce pipeline ne nĂ©cessite pas de modĂšles particuliers, et tant que lâID du modĂšle est le mĂȘme que celui de notre exemple texte-image ci-dessus, aucun nouveau fichier ne devra ĂȘtre tĂ©lĂ©chargĂ©.
# Chargement d'un pipeline Img2Img
model_id = "stabilityai/stable-diffusion-2-1-base"
img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id).to(device)
Dans la section Configuration nous avons chargé un exemple init_image à utiliser pour cette démo, mais vous pouvez le remplacer par votre propre image si vous préférez. Voici le pipeline en action :
# Appliquer Img2Img
result_image = img2img_pipe(
prompt="An oil painting of a man on a bench",
image = init_image, # L'image de départ
strength = 0.6, # 0 pour aucun changement, 1.0 pour une force maximale
).images[0]
# Voir le résultat
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(result_image);axs[1].set_title('Result')
âïž Ă votre tour !
ExpĂ©rimentez avec ce pipeline. Essayez vos propres images, ou jouez avec diffĂ©rentes forces et diffĂ©rents prompts. Vous pouvez utiliser les mĂȘmes arguments que pour le pipeline texte-image, alors nâhĂ©sitez pas Ă essayer diffĂ©rentes tailles, diffĂ©rents nombres dâĂ©tapes, etc.
ComplĂ©tion dâimage (inpainting)
Que se passerait-il si nous voulions conserver une partie de lâimage dâentrĂ©e inchangĂ©e mais gĂ©nĂ©rer quelque chose de nouveau dans dâautres parties ? Câest ce quâon appelle la complĂ©tion dâimage (inpainting). Bien quâil soit possible de le faire avec le mĂȘme modĂšle que les dĂ©monstrations prĂ©cĂ©dentes (via StableDiffusionInpaintPipelineLegacy), nous pouvons obtenir de meilleurs rĂ©sultats en utilisant une version finetunĂ©e personnalisĂ©e de Stable Diffusion qui prend un masque comme condition supplĂ©mentaire. Lâimage du masque doit avoir la mĂȘme forme que lâimage dâentrĂ©e, avec du blanc dans les zones Ă remplacer et du noir dans les zones Ă garder inchangĂ©es. Voici comment charger un tel pipeline et lâappliquer Ă lâimage dâexemple et au masque chargĂ©s dans la section Configuration :
# Charger le pipeline de complétion d'image (nécessite un modÚle de complétion d'image approprié)
pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting")
pipe = pipe.to(device)
# Complétion d'image avec un prompt pour avoir le résultat souhaité
prompt = "A small robot, high resolution, sitting on a park bench"
image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0]
# Voir le résultat
fig, axs = plt.subplots(1, 3, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(mask_image);axs[1].set_title('Mask')
axs[2].imshow(image);axs[2].set_title('Result')
Ce modĂšle peut ĂȘtre particuliĂšrement puissant lorsquâil est combinĂ© Ă un autre modĂšle pour gĂ©nĂ©rer automatiquement des masques. Par exemple, ce Space utilise un modĂšle appelĂ© CLIPSeg pour masquer un objet Ă remplacer sur la base dâune description textuelle.
En marge : gestion du cache de votre modĂšle
Lâexploration de diffĂ©rents pipelines et variantes de modĂšles peut remplir votre espace disque. Vous pouvez voir quels modĂšles sont actuellement tĂ©lĂ©chargĂ©s avec :
!ls ~/.cache/huggingface/diffusers/ # Liste du contenu du répertoire cache
Consultez la documentation sur la mise en cache pour savoir comment visualiser et gérer efficacement votre cache.
Depth2Image
Image dâentrĂ©e, image de profondeur et exemples gĂ©nĂ©rĂ©s (source de lâimage : StabilityAI)
Img2Img est trĂšs bien, mais parfois nous voulons crĂ©er une nouvelle image avec la composition de lâoriginal mais avec des couleurs ou des textures complĂštement diffĂ©rentes. Il peut ĂȘtre difficile de trouver une force dâImg2Img qui prĂ©serve ce que nous voulons de la mise en page sans conserver les couleurs dâentrĂ©e.
Il est temps dâadopter un autre modĂšle finetunĂ© ! Celui-ci prend en compte les informations de profondeur comme condition supplĂ©mentaire lors de la gĂ©nĂ©ration. Le pipeline utilise un modĂšle dâestimation de la profondeur pour crĂ©er une carte de profondeur, qui est ensuite transmise au UNet finetunĂ© lors de la gĂ©nĂ©ration dâimages afin de prĂ©server (si possible) la profondeur et la structure de lâimage initiale tout en remplissant un contenu complĂštement nouveau.
# Charger le pipeline Depth2Img (nécessite un modÚle approprié)
pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth")
pipe = pipe.to(device)
# Complétion d'image avec un prompt pour avoir le résultat souhaité
prompt = "An oil painting of a man on a bench"
image = pipe(prompt=prompt, image=init_image).images[0]
# Voir le résultat
fig, axs = plt.subplots(1, 2, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(image);axs[1].set_title('Result');
Notez la comparaison avec lâexemple img2img. Ici, la variation de couleur est beaucoup plus importante mais la structure globale reste fidĂšle Ă lâoriginal. Ce nâest pas idĂ©al dans ce cas, car lâhomme a Ă©tĂ© dotĂ© dâune anatomie extrĂȘmement bizarre pour correspondre Ă la forme du chien, mais dans certains cas, câest extraordinairement utile. Pour un exemple de cette approche, regardez ce tweet montrant le modĂšle de profondeur utilisĂ© pour texturer une scĂšne en 3D !
Et maintenant ?
Nous espĂ©rons vous avoir donnĂ© un avant-goĂ»t des nombreuses possibilitĂ©s offertes par Stable Diffusion ! Une fois que vous en aurez assez de jouer avec les exemples de ce notebook, allez voir le notebook du hackathon DreamBooth pour voir comment finetuner votre propre version de Stable Diffusion qui peut ĂȘtre utilisĂ©e avec les pipelines texte-image ou img2img que nous avons vus ici.
Si vous ĂȘtes curieux dâapprofondir le fonctionnement des diffĂ©rents composants, consultez le notebook Stable Diffusion Deep Dive qui va beaucoup plus loin dans les dĂ©tails et montre quelques astuces supplĂ©mentaires que nous pouvons faire.
Nâoubliez pas de partager vos crĂ©ations avec nous et la communautĂ© !
3.2. Stable Diffusion : plongée en profondeur
Stable Diffusion est un puissant modĂšle de texte Ă image. Il existe plusieurs sites web et outils pour rendre son utilisation aussi simple que possible. Il est Ă©galement intĂ©grĂ© Ă la bibliothĂšque de Diffusers dâHuggingface, ce qui permet de gĂ©nĂ©rer des images en toute simplicitĂ© :
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", revision="fp16", torch_dtype=torch.float16, use_auth_token=True).to("cuda")
image = pipe("An astronaught scuba diving").images[0]
Dans ce notebook, nous allons nous plonger dans le code qui se cache derriĂšre ces interfaces faciles Ă utiliser, pour voir ce qui se passe sous le capot. Nous commencerons par recrĂ©er la fonctionnalitĂ© ci-dessus sous la forme dâun morceau de code effrayant, puis, un par un, nous inspecterons les diffĂ©rents composants et comprendrons ce quâils font. Ă la fin de ce notebook, cette mĂȘme boucle dâĂ©chantillonnage devrait ressembler Ă quelque chose que vous pouvez peaufiner et modifier Ă votre guise.
Configuration et importations
Vous devrez vous connecter Ă Hugging Face et accepter les termes de la licence pour ce modĂšle (voir la carte de modĂšle pour plus de dĂ©tails). Lorsque vous exĂ©cuterez ce notebook pour la premiĂšre fois, vous devrez dĂ©commenter les deux cellules suivantes pour installer les prĂ©requis et vous connecter au Hub avec un token dâaccĂšs.
# !pip install -q --upgrade transformers diffusers ftfy
from base64 import b64encode
import numpy
import torch
from diffusers import AutoencoderKL, LMSDiscreteScheduler, UNet2DConditionModel
from huggingface_hub import notebook_login
# Pour l'affichage vidéo
from IPython.display import HTML
from matplotlib import pyplot as plt
from pathlib import Path
from PIL import Image
from torch import autocast
from torchvision import transforms as tfms
from tqdm.auto import tqdm
from transformers import CLIPTextModel, CLIPTokenizer, logging
torch.manual_seed(1)
if not (Path.home()/'.huggingface'/'token').exists(): notebook_login()
# Suppression de certains avertissements inutiles lors du chargement de CLIPTextModel
logging.set_verbosity_error()
# Définir l'appareil
torch_device = "cuda" if torch.cuda.is_available() else "cpu"
Chargement des modĂšles
Ce code (et celui de la section suivante) provient du notebook illustratif dâHuggingface.
Il tĂ©lĂ©charge et configure les modĂšles et les composants que nous utiliserons. ExĂ©cutons-le pour lâinstant et passons Ă la section suivante pour vĂ©rifier que tout fonctionne avant dâaller plus loin.
Si vous avez chargé un pipeline, vous pouvez aussi accéder à ces composants en utilisant pipe.unet, pipe.vae et ainsi de suite.
Dans ce notebook, nous ne faisons pas dâĂ©conomies de mĂ©moire. Si vous vous retrouvez Ă court de RAM GPU, regardez le code du pipeline pour vous inspirer avec des choses comme le dĂ©coupage de lâattention, le passage Ă la demi-prĂ©cision (fp16), le maintien du VAE sur le CPU et dâautres modifications.
# Charger le modÚle auto-encodeur qui sera utilisé pour décoder les latents dans l'espace de l'image
vae = AutoencoderKL.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="vae")
# Charger le tokenizer et l'encodeur
tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14")
# Le modÚle UNet pour générer les latents
unet = UNet2DConditionModel.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="unet")
# Le planificateur de bruit
scheduler = LMSDiscreteScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000)
# Nous allons au GPU !
vae = vae.to(torch_device)
text_encoder = text_encoder.to(torch_device)
unet = unet.to(torch_device)
Une boucle de diffusion
Si tout ce que vous voulez, câest crĂ©er une image avec du texte, vous pouvez ignorer ce notebook et utiliser lâun des outils existants (comme DreamStudio) ou utiliser le pipeline simplifiĂ© dâHugging Face comme documentĂ© ici.
Ce que nous voulons faire ici, câest approfondir un peu plus la façon dont cela fonctionne. Nous allons donc commencer par vĂ©rifier que le code de lâexemple sâexĂ©cute. Il ressemble beaucoup Ă ce que vous trouverez si vous inspectez la mĂ©thode call() du pipeline de Stable Diffusion.
# Quelques paramĂštres
prompt = ["A watercolor painting of an otter"]
height = 512 # hauteur par défaut de Stable Diffusion
width = 512 # largeur par défaut de Stable Diffusion
num_inference_steps = 30 # Nombre d'étapes de débruitage
guidance_scale = 7.5 # Ăchelle pour un guidage sans classifieur
generator = torch.manual_seed(32) # Générateur de la graine pour créer le bruit latent initial
batch_size = 1
# Preparation du texte
text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
with torch.no_grad():
text_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]
max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
[""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
with torch.no_grad():
uncond_embeddings = text_encoder(uncond_input.input_ids.to(torch_device))[0]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# Preparation du planificateur
scheduler.set_timesteps(num_inference_steps)
# Preparation des latents
latents = torch.randn(
(batch_size, unet.in_channels, height // 8, width // 8),
generator=generator,
)
latents = latents.to(torch_device)
latents = latents * scheduler.init_noise_sigma # Mise à l'échelle (versions précédentes) latents = latents * self.scheduler.sigmas[0]
# Boucle
with autocast("cuda"):
for i, t in tqdm(enumerate(scheduler.timesteps)):
# étendre les latents si nous procédons à un guidage sans classifieur afin d'éviter de faire deux passages en avant
latent_model_input = torch.cat([latents] * 2)
sigma = scheduler.sigmas[i]
# mettre à l'échelle les latents (préconditionnement)
# latent_model_input = latent_model_input / ((sigma**2 + 1) ** 0.5) # Diffusers 0.3 et moins
latent_model_input = scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
with torch.no_grad():
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# effectuer le guidage
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# calculer l'échantillon bruité précédent x_t -> x_t-1
# latents = scheduler.step(noise_pred, i, latents)["prev_sample"] # Diffusers 0.3 et moins
latents = scheduler.step(noise_pred, t, latents).prev_sample
# mettre à l'échelle et décoder les latents de l'image à l'aide du vae
latents = 1 / 0.18215 * latents
with torch.no_grad():
image = vae.decode(latents).sample
# Affichage
image = (image / 2 + 0.5).clamp(0, 1)
image = image.detach().cpu().permute(0, 2, 3, 1).numpy()
images = (image * 255).round().astype("uint8")
pil_images = [Image.fromarray(image) for image in images]
pil_images[0]
Cela fonctionne, mais cela fait beaucoup de code ! Examinons les composants un par un.
Lâauto-encodeur (AE)
LâAE peut encoder une image dans une sorte de reprĂ©sentation latente, et la dĂ©coder Ă nouveau en une image. Nous avons regroupĂ© le code dans quelques fonctions pour que nous puissions voir Ă quoi cela ressemble en action :
def pil_to_latent(input_im):
# Une seule image -> un seul latent dans un batch (donc taille 1, 4, 64, 64)
with torch.no_grad():
latent = vae.encode(tfms.ToTensor()(input_im).unsqueeze(0).to(torch_device)*2-1) # Note scaling
return 0.18215 * latent.latent_dist.sample()
def latents_to_pil(latents):
# bain de latents -> liste d'images
latents = (1 / 0.18215) * latents
with torch.no_grad():
image = vae.decode(latents).sample
image = (image / 2 + 0.5).clamp(0, 1)
image = image.detach().cpu().permute(0, 2, 3, 1).numpy()
images = (image * 255).round().astype("uint8")
pil_images = [Image.fromarray(image) for image in images]
return pil_images
Nous utiliserons ici une image provenant du web, mais vous pouvez charger la vÎtre en la téléchargeant et en modifiant le nom du fichier dans la cellule suivante.
# Télécharger une image de démonstration
!curl --output macaw.jpg 'https://lafeber.com/pet-birds/wp-content/uploads/2018/06/Scarlet-Macaw-2.jpg'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 62145 100 62145 0 0 10874 0 0:00:05 0:00:05 --:--:-- 15633
# Charger l'image avec PIL
input_image = Image.open('macaw.jpg').resize((512, 512))
input_image
Lâencodage dans lâespace latent de lâAE Ă lâaide de la fonction dĂ©finie ci-dessus se prĂ©sente comme suit :
# Encoder dans l'espace latent
encoded = pil_to_latent(input_image)
encoded.shape
torch.Size([1, 4, 64, 64])
# Visualisons les quatre canaux de cette représentation latente :
fig, axs = plt.subplots(1, 4, figsize=(16, 4))
for c in range(4):
axs[c].imshow(encoded[0][c].cpu(), cmap='Greys')
Ce tenseur 4x64x64 capture de nombreuses informations sur lâimage, suffisamment, espĂ©rons-le, pour que lorsque nous lâintroduisons dans le dĂ©codeur, nous obtenions en retour quelque chose de trĂšs proche de notre image dâentrĂ©e :
# Décoder cette représentation latente en une image
decoded = latents_to_pil(encoded)[0]
decoded
Vous verrez de petites diffĂ©rences si vous plissez les yeux ! Concentrez-vous sur lâĆil si vous ne voyez rien dâĂ©vident. Câest assez impressionnant : cette image latente de 4x64x64 semble contenir beaucoup plus dâinformations quâune image de 64px.
Cet auto-encodeur a Ă©tĂ© entraĂźnĂ© Ă rĂ©duire une image Ă une reprĂ©sentation plus petite, puis Ă recrĂ©er lâimage Ă partir de cette version compressĂ©e.
Dans ce cas particulier, le facteur de compression est de 48, nous partons dâune image 3x512x512(cannaux x hauteur x largeur) et elle est compressĂ©e en un vecteur latent 4x64x64. Chaque volume de 3x8x8 pixels dans lâimage dâentrĂ©e est compressĂ© en seulement 4 nombres (4x1x1). Il est possible de trouver des AEs avec un taux de compression plus Ă©levĂ© (par exemple f16 comme certains modĂšles populaires de VQGAN) mais Ă un moment donnĂ©, ils commencent Ă introduire des artefacts que nous ne voulons pas.
Pourquoi utiliser un auto-encodeur ? Nous pouvons faire de la diffusion dans lâespace des pixels oĂč le modĂšle reçoit toutes les donnĂ©es de lâimage comme entrĂ©es et produit une prĂ©diction de sortie de la mĂȘme forme. Mais cela implique le traitement dâun grand nombre de donnĂ©es et rend la gĂ©nĂ©ration dâimages Ă haute rĂ©solution trĂšs coĂ»teuse sur le plan informatique. Certaines solutions consistent Ă effectuer la diffusion Ă basse rĂ©solution (64 px par exemple), puis Ă entraĂźner un modĂšle distinct pour augmenter lâĂ©chelle de maniĂšre rĂ©pĂ©tĂ©e (comme avec D2/Imagen). La diffusion latente, quant Ă elle, effectue le processus de diffusion dans cet espace latent, en utilisant les reprĂ©sentations compressĂ©es de notre AE plutĂŽt que des images brutes. Ces reprĂ©sentations sont riches en informations et peuvent ĂȘtre suffisamment petites pour ĂȘtre gĂ©rĂ©es par du matĂ©riel grand public. Une fois que nous avons gĂ©nĂ©rĂ© une nouvelle image en tant que reprĂ©sentation latente, lâauto-encodeur peut prendre ces sorties latentes finales et les transformer en pixels rĂ©els.
Le planificateur
Nous devons maintenant parler de lâajout de bruit.
Pendant lâentraĂźnement, nous ajoutons du bruit Ă une image, puis nous demandons au modĂšle dâessayer de prĂ©dire le bruit. Si nous ajoutons toujours beaucoup de bruit, le modĂšle risque de ne pas avoir grand-chose Ă faire. Si nous nâen ajoutons quâune infime quantitĂ©, le modĂšle ne pourra pas faire grand-chose avec les points de dĂ©part alĂ©atoires que nous utilisons pour lâĂ©chantillonnage. Au cours de lâentraĂźnement, la quantitĂ© de bruit varie donc en fonction dâune certaine distribution.
Pendant lâĂ©chantillonnage, nous voulons « dĂ©bruiter » sur un certain nombre dâĂ©tapes. Le nombre dâĂ©tapes et la quantitĂ© de bruit que nous devons viser Ă chaque Ă©tape affecteront le rĂ©sultat final.
Le planificateur est chargé de gérer tous ces détails. Par exemple : scheduler = LMSDiscreteScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000) met en place un scheduler qui correspond à celui utilisé pour entraßner ce modÚle. Lorsque nous voulons échantillonner sur un plus petit nombre de pas, nous le faisons avec scheduler.set_timesteps :
# Réglage du nombre de pas d'échantillonnage :
scheduler.set_timesteps(15)
Vous pouvez voir comment notre nouvel ensemble dâĂ©tapes correspond Ă celles utilisĂ©es dans lâentraĂźnement :
# Voyez ça en termes de 1000 étapes originales utilisées pour l'entraßnement :
print(scheduler.timesteps)
tensor([999.0000, 927.6429, 856.2857, 784.9286, 713.5714, 642.2143, 570.8571,
499.5000, 428.1429, 356.7857, 285.4286, 214.0714, 142.7143, 71.3571,
0.0000], dtype=torch.float64)
Et quelle est la quantité de bruit présente à chaque endroit :
# Examinez les niveaux de bruit équivalents :
print(scheduler.sigmas)
tensor([14.6146, 9.6826, 6.6780, 4.7746, 3.5221, 2.6666, 2.0606, 1.6156,
1.2768, 1.0097, 0.7913, 0.6056, 0.4397, 0.2780, 0.0292, 0.0000])
Pendant lâĂ©chantillonnage, nous partons dâun niveau de bruit Ă©levĂ© (en fait, notre entrĂ©e sera du bruit pur) et nous « dĂ©bruitons » progressivement jusquâĂ obtenir une image, selon ce calendrier.
# Affichage du planificateur de bruit :
plt.plot(scheduler.sigmas)
plt.title('Noise Schedule')
plt.xlabel('Sampling step')
plt.ylabel('sigma')
plt.show()
Ce « sigma » est la quantité de bruit ajoutée à la représentation latente. Voyons ce que cela donne en ajoutant un peu de bruit à notre image codée, puis en décodant cette version bruitée :
noise = torch.randn_like(encoded) # Bruit aléatoire
sampling_step = 10 # Equivalent à une étape 10 sur 15 dans la grille ci-dessus
# encoded_and_noised = scheduler.add_noise(encoded, noise, timestep) # Diffusers 0.3 et en dessous
encoded_and_noised = scheduler.add_noise(encoded, noise, timesteps=torch.tensor([scheduler.timesteps[sampling_step]]))
latents_to_pil(encoded_and_noised.float())[0] # Affichage
Ă quoi cela ressemble-t-il Ă diffĂ©rents pas de temps ? Faites lâexpĂ©rience et voyez par vous-mĂȘme !
Si vous dĂ©commentez la cellule ci-dessous, vous verrez que dans ce cas, la fonction scheduler.add_noise ne fait quâajouter du bruit Ă lâĂ©chelle sigma : noisy_samples = original_samples + noise * sigmas
# ??scheduler.add_noise
Dâautres modĂšles de diffusion peuvent ĂȘtre entraĂźnĂ©s avec diffĂ©rentes approches de bruits et dâordonnancement, dont certaines maintiennent la variance relativement constante entre les niveaux de bruit (« prĂ©servation de la variance ») avec diffĂ©rentes astuces de mise Ă lâĂ©chelle et de mĂ©lange au lieu dâavoir des latents bruitĂ©s avec une variance de plus en plus Ă©levĂ©e au fur et Ă mesure que lâon ajoute du bruit (« explosion de la variance »).
Si nous voulons partir dâun bruit alĂ©atoire au lieu dâune image bruitĂ©e, nous devons la mettre Ă lâĂ©chelle de la plus grande valeur sigma utilisĂ©e pendant lâentraĂźnement, soit ~14 dans ce cas. Et avant que ces latents bruitĂ©s ne soient introduits dans le modĂšle, ils sont Ă nouveau mis Ă lâĂ©chelle dans lâĂ©tape dite de prĂ©-conditionnement : latent_model_input = latent_model_input / ((sigma**2 + 1) ** 0.5) (maintenant gĂ©rĂ© par latent_model_input = scheduler.scale_model_input(latent_model_input, t)).
Encore une fois, cette mise Ă lâĂ©chelle/prĂ©-conditionnement diffĂšre entre les articles et les implĂ©mentations, alors gardez un Ćil sur ce point si vous travaillez avec un type diffĂ©rent de modĂšle de diffusion.
Boucle Ă partir de la version bruitĂ©e de lâentrĂ©e (AKA image2image)
Voyons ce qui se passe lorsque nous utilisons notre image comme point de départ, en ajoutant un peu de bruit et en effectuant les derniÚres étapes de débruitage dans la boucle avec un nouveau prompt.
Nous allons utiliser une boucle similaire à celle de la premiÚre démonstration, mais nous allons sauter les premiÚres étapes start_step.
Pour bruiter notre image, nous utiliserons un code comme celui montrĂ© ci-dessus, en utilisant le planificateur pour la bruiter Ă un niveau Ă©quivalent Ă lâĂ©tape 10 (start_step).
# ParamĂštres (les mĂȘmes que prĂ©cĂ©demment, Ă l'exception du nouveau prompt)
prompt = ["A colorful dancer, nat geo photo"]
height = 512 # hauteur par défaut de Stable Diffusion
width = 512 # largeur par défaut de Stable Diffusion
num_inference_steps = 30 # Nombre d'étapes de débruitage
guidance_scale = 7.5 # Ăchelle pour un guidage sans classifieur
generator = torch.manual_seed(32) # Générateur de la graine pour créer le bruit latent initial
batch_size = 1
# Preparation du texte (comme précédemment)
text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
with torch.no_grad():
text_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]
max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
[""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
with torch.no_grad():
uncond_embeddings = text_encoder(uncond_input.input_ids.to(torch_device))[0]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# Preparation du planificateur (définition du nombre d'étapes de l'inférence)
scheduler.set_timesteps(num_inference_steps)
# Preparation des latents (bruitage approprié pour start_step)
start_step = 10
start_sigma = scheduler.sigmas[start_step]
noise = torch.randn_like(encoded)
latents = scheduler.add_noise(encoded, noise, timesteps=torch.tensor([scheduler.timesteps[start_step]]))
latents = latents.to(torch_device).float()
# Boucle
for i, t in tqdm(enumerate(scheduler.timesteps)):
if i >= start_step: # << C'est la seule modification que nous apportons Ă la boucle.
# étendre les latents si nous procédons à un guidage sans classifieur afin d'éviter de faire deux passages en avant
latent_model_input = torch.cat([latents] * 2)
sigma = scheduler.sigmas[i]
latent_model_input = scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
with torch.no_grad():
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings)["sample"]
# effectuer le guidage
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# calculer l'échantillon bruité précédent x_t -> x_t-1
latents = scheduler.step(noise_pred, t, latents).prev_sample
latents_to_pil(latents)[0]
Vous pouvez voir que certaines couleurs et structures de lâimage sont conservĂ©es, mais nous avons maintenant une nouvelle image ! Plus vous ajoutez de bruit et plus vous effectuez dâĂ©tapes, plus lâimage sâĂ©loigne de lâimage dâentrĂ©e.
Câest ainsi que fonctionne le cĂ©lĂšbre pipeline img2img. Encore une fois, si câest votre objectif final, il existe des outils qui facilitent la tĂąche !
Mais vous pouvez voir que sous le capot, câest la mĂȘme chose que la boucle de gĂ©nĂ©ration, en sautant les premiĂšres Ă©tapes et en partant dâune image bruitĂ©e plutĂŽt que dâune image purement bruitĂ©e.
Essayez de changer le nombre dâĂ©tapes sautĂ©es et de voir comment cela affecte la quantitĂ© de changement de lâimage par rapport Ă lâentrĂ©e.
Exploration du pipeline texte -> enchĂąssement
Nous utilisons un modĂšle dâencodage de texte pour transformer notre texte en un ensemble dâenchĂąssements qui sont transmis au modĂšle de diffusion en tant que conditionnement. Suivons un morceau de texte tout au long de ce processus et voyons comment il fonctionne.
# Notre prompt textuel
prompt = 'A picture of a puppy'
Nous commençons par la tokenisation :
# Transformer le texte en une séquence de tokens :
text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
text_input['input_ids'][0] # Voir les tokens
tensor([49406, 320, 1674, 539, 320, 6829, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407])
# Voir les tokens individuels
for t in text_input['input_ids'][0][:8]: # Nous nous contenterons d'examiner les 7 premiers pour vous éviter un mur d'<|endoftext|>'
print(t, tokenizer.decoder.get(int(t)))
tensor(49406) <|startoftext|>
tensor(320) a</w>
tensor(1674) picture</w>
tensor(539) of</w>
tensor(320) a</w>
tensor(6829) puppy</w>
tensor(49407) <|endoftext|>
tensor(49407) <|endoftext|>
Nous pouvons passer directement aux enchĂąssements finaux (de sortie) de la maniĂšre suivante :
# Récupérer les enchùssements de sortie
output_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]
print('Shape:', output_embeddings.shape)
output_embeddings
Shape: torch.Size([1, 77, 768])
tensor([[[-0.3884, 0.0229, -0.0522, ..., -0.4899, -0.3066, 0.0675],
[ 0.0290, -1.3258, 0.3085, ..., -0.5257, 0.9768, 0.6652],
[ 0.6942, 0.3538, 1.0991, ..., -1.5716, -1.2643, -0.0121],
...,
[-0.0221, -0.0053, -0.0089, ..., -0.7303, -1.3830, -0.3011],
[-0.0062, -0.0246, 0.0065, ..., -0.7326, -1.3745, -0.2953],
[-0.0536, 0.0269, 0.0444, ..., -0.7159, -1.3634, -0.3075]]],
device='cuda:0', grad_fn=<NativeLayerNormBackward0>)
Nous passons nos tokens Ă travers text_encoder et nous obtenons comme par magie des nombres que nous pouvons introduire dans le modĂšle.
Comment ces chiffres sont-ils gĂ©nĂ©rĂ©s ? Les tokens sont transformĂ©s en un ensemble dâenchĂąssements dâentrĂ©e, qui sont ensuite introduits dans le transformer pour obtenir les enchĂąssements de sortie finaux.
Pour obtenir ces enchĂąssements dâentrĂ©e, il y a en fait deux Ă©tapes comme le rĂ©vĂšle lâinspection de text_encoder.text_model.embeddings :
text_encoder.text_model.embeddings
CLIPTextEmbeddings(
(token_embedding): Embedding(49408, 768)
(position_embedding): Embedding(77, 768)
)
EnchĂąssement de tokens
Le token est envoyĂ© Ă la fonction token_embedding pour le transformer en vecteur. Le nom de la fonction get_input_embeddings est trompeur puisque ces enchĂąssements de tokens doivent ĂȘtre combinĂ©s avec les enchĂąssements de positions avant dâĂȘtre utilisĂ©s comme entrĂ©es dans le modĂšle ! Quoi quâil en soit, examinons dâabord la partie relative Ă lâenchĂąssements des tokens.
Nous pouvons regarder la couche dâenchĂąssement :
# Accéder à la couche enchùssement
token_emb_layer = text_encoder.text_model.embeddings.token_embedding
token_emb_layer # Taille du vocabulaire 49408, emb_dim 768
Embedding(49408, 768)
Et enchĂąsser un token comme suit :
# EnchĂąsser un *token*, dans ce cas, celui du "chiot"
embedding = token_emb_layer(torch.tensor(6829, device=torch_device))
embedding.shape # représentation en 768-dim
torch.Size([768])
Cet unique tokens a été associé avec un vecteur à 768 dimensions.
Nous pouvons faire la mĂȘme chose avec tous les tokens du prompt pour obtenir tous les enchĂąssements de tokens :
token_embeddings = token_emb_layer(text_input.input_ids.to(torch_device))
print(token_embeddings.shape) # taille du batch 1, 77 *tokens*, 768 valeurs pour chaque
token_embeddings
torch.Size([1, 77, 768])
tensor([[[ 0.0011, 0.0032, 0.0003, ..., -0.0018, 0.0003, 0.0019],
[ 0.0013, -0.0011, -0.0126, ..., -0.0124, 0.0120, 0.0080],
[ 0.0235, -0.0118, 0.0110, ..., 0.0049, 0.0078, 0.0160],
...,
[ 0.0012, 0.0077, -0.0011, ..., -0.0015, 0.0009, 0.0052],
[ 0.0012, 0.0077, -0.0011, ..., -0.0015, 0.0009, 0.0052],
[ 0.0012, 0.0077, -0.0011, ..., -0.0015, 0.0009, 0.0052]]],
device='cuda:0', grad_fn=<EmbeddingBackward0>)
EnchĂąssements positionnels
Les enchĂąssements positionnels indiquent au modĂšle Ă quel endroit dâune sĂ©quence se trouve un token. Tout comme lâenchĂąssement de * tokens, il sâagit dâun ensemble de paramĂštres (qui peuvent Ă©ventuellement ĂȘtre appris). Mais maintenant, au lieu de traiter ~50k *tokens nous avons juste besoin dâun pour chaque position (77 au total) :
pos_emb_layer = text_encoder.text_model.embeddings.position_embedding
pos_emb_layer
Embedding(77, 768)
Nous pouvons obtenir lâenchĂąssement positionnel pour chaque position :
position_ids = text_encoder.text_model.embeddings.position_ids[:, :77]
position_embeddings = pos_emb_layer(position_ids)
print(position_embeddings.shape)
position_embeddings
torch.Size([1, 77, 768])
tensor([[[ 0.0016, 0.0020, 0.0002, ..., -0.0013, 0.0008, 0.0015],
[ 0.0042, 0.0029, 0.0002, ..., 0.0010, 0.0015, -0.0012],
[ 0.0018, 0.0007, -0.0012, ..., -0.0029, -0.0009, 0.0026],
...,
[ 0.0216, 0.0055, -0.0101, ..., -0.0065, -0.0029, 0.0037],
[ 0.0188, 0.0073, -0.0077, ..., -0.0025, -0.0009, 0.0057],
[ 0.0330, 0.0281, 0.0289, ..., 0.0160, 0.0102, -0.0310]]],
device='cuda:0', grad_fn=<EmbeddingBackward0>)
Combiner les enchĂąssements de tokens et de positions
Il est temps de combiner les deux. Comment faire ? Il suffit de les additionner ! Dâautres approches sont possibles, mais pour ce modĂšle, câest ainsi que nous procĂ©dons.
En les combinant de cette maniĂšre, nous obtenons les enchĂąssements dâentrĂ©e finaux, prĂȘts Ă ĂȘtre introduits dans le transformer :
# En les combinant, nous obtenons les enchùssements d'entrée finaux
input_embeddings = token_embeddings + position_embeddings
print(input_embeddings.shape)
input_embeddings
torch.Size([1, 77, 768])
tensor([[[ 2.6770e-03, 5.2133e-03, 4.9323e-04, ..., -3.1321e-03,
1.0659e-03, 3.4316e-03],
[ 5.5371e-03, 1.7510e-03, -1.2381e-02, ..., -1.1410e-02,
1.3508e-02, 6.8378e-03],
[ 2.5356e-02, -1.1019e-02, 9.7663e-03, ..., 1.9460e-03,
6.8375e-03, 1.8573e-02],
...,
[ 2.2781e-02, 1.3262e-02, -1.1241e-02, ..., -8.0054e-03,
-2.0560e-03, 8.9366e-03],
[ 2.0026e-02, 1.5015e-02, -8.7638e-03, ..., -4.0313e-03,
1.8487e-05, 1.0885e-02],
[ 3.4206e-02, 3.5826e-02, 2.7768e-02, ..., 1.4465e-02,
1.1110e-02, -2.5745e-02]]], device='cuda:0', grad_fn=<AddBackward0>)
Nous pouvons vĂ©rifier que ces rĂ©sultats sont les mĂȘmes que ceux obtenus avec text_encoder.text_model.embeddings :
# La procédure suivante combine toutes les étapes ci-dessus (mais ne nous permet pas de les modifier !)
text_encoder.text_model.embeddings(text_input.input_ids.to(torch_device))
tensor([[[ 2.6770e-03, 5.2133e-03, 4.9323e-04, ..., -3.1321e-03,
1.0659e-03, 3.4316e-03],
[ 5.5371e-03, 1.7510e-03, -1.2381e-02, ..., -1.1410e-02,
1.3508e-02, 6.8378e-03],
[ 2.5356e-02, -1.1019e-02, 9.7663e-03, ..., 1.9460e-03,
6.8375e-03, 1.8573e-02],
...,
[ 2.2781e-02, 1.3262e-02, -1.1241e-02, ..., -8.0054e-03,
-2.0560e-03, 8.9366e-03],
[ 2.0026e-02, 1.5015e-02, -8.7638e-03, ..., -4.0313e-03,
1.8487e-05, 1.0885e-02],
[ 3.4206e-02, 3.5826e-02, 2.7768e-02, ..., 1.4465e-02,
1.1110e-02, -2.5745e-02]]], device='cuda:0', grad_fn=<AddBackward0>)
Passage dans le transformer
Nous voulons modifier les enchĂąssements dâentrĂ©e (en particulier les enchĂąssements de tokens) avant de les envoyer dans le reste du modĂšle, mais nous devons dâabord nous assurer que nous savons comment le faire. Nous avons lu le code de la mĂ©thode forward du text_encoder, et nous nous sommes basĂ©s sur ce code pour la mĂ©thode forward du text_model que le text_encoder englobe. Pour lâinspecter vous-mĂȘme, tapez ??text_encoder.text_model.forward et vous obtiendrez les informations sur la fonction et le code source, une astuce de dĂ©bogage utile !
Quoi quâil en soit, nous pouvons copier les bits dont nous avons besoin pour obtenir ce que lâon appelle le « dernier Ă©tat cachĂ© » et ainsi gĂ©nĂ©rer nos enchĂąssements finaux :
def get_output_embeds(input_embeddings):
# Le modÚle de texte de CLIP utilise le masquage causal, c'est pourquoi nous le préparons ici :
bsz, seq_len = input_embeddings.shape[:2]
causal_attention_mask = text_encoder.text_model._build_causal_attention_mask(bsz, seq_len, dtype=input_embeddings.dtype)
# Obtenir les enchĂąssements de sortie implique d'appeler le modĂšle en passant output_hidden_states=True
# afin qu'il ne renvoie pas uniquement les prédictions finales regroupées :
encoder_outputs = text_encoder.text_model.encoder(
inputs_embeds=input_embeddings,
attention_mask=None, # Nous n'utilisons pas de masque d'attention, cela peut donc ĂȘtre None.
causal_attention_mask=causal_attention_mask.to(torch_device),
output_attentions=None,
output_hidden_states=True, # Nous voulons le résultat des enchùssements et non le résultat final.
return_dict=None,
)
# Seul l'état caché de sortie nous intéresse
output = encoder_outputs[0]
# Il existe une normalisation de couche finale par laquelle nous devons passer
output = text_encoder.text_model.final_layer_norm(output)
# Et maintenant, elles sont prĂȘtes !
return output
out_embs_test = get_output_embeds(input_embeddings) # Alimenter le modĂšle Ă l'aide de notre nouvelle fonction
print(out_embs_test.shape) # Vérifier la forme de la sortie
out_embs_test # Inspecter la sortie
torch.Size([1, 77, 768])
tensor([[[-0.3884, 0.0229, -0.0522, ..., -0.4899, -0.3066, 0.0675],
[ 0.0290, -1.3258, 0.3085, ..., -0.5257, 0.9768, 0.6652],
[ 0.6942, 0.3538, 1.0991, ..., -1.5716, -1.2643, -0.0121],
...,
[-0.0221, -0.0053, -0.0089, ..., -0.7303, -1.3830, -0.3011],
[-0.0062, -0.0246, 0.0065, ..., -0.7326, -1.3745, -0.2953],
[-0.0536, 0.0269, 0.0444, ..., -0.7159, -1.3634, -0.3075]]],
device='cuda:0', grad_fn=<NativeLayerNormBackward0>)
Notez que cela correspond aux output_embeddings que nous avons vu au dĂ©but. Nous avons trouvĂ© comment diviser cette Ă©tape (âobtenir les enchĂąssementsâ) en plusieurs sous-Ă©tapes prĂȘtes Ă ĂȘtre modifiĂ©es.
Maintenant que nous avons mis en place ce processus, nous pouvons remplacer lâencodage dâentrĂ©e dâun token par un nouvel encodage de notre choix, ce qui dans notre cas dâutilisation final, sera quelque chose que nous apprendrons. Pour dĂ©montrer le concept, remplaçons lâencodage dâentrĂ©e de « puppy » dans le prompt avec lequel nous avons jouĂ© avec lâenchĂąssement du token 2368, obtenons un nouvel ensemble dâenchĂąssement de sortie basĂ©s sur celui-ci et utilisons-les pour gĂ©nĂ©rer une image afin de voir ce que nous obtenons :
prompt = 'A picture of a puppy'
# Tokeniser
text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
input_ids = text_input.input_ids.to(torch_device)
# Obtenir les enchĂąssements des tokens
token_embeddings = token_emb_layer(input_ids)
# Le nouvel enchùssement. Dans ce cas, il s'agit simplement de l'enchùssement d'entrée du token 2368
replacement_token_embedding = text_encoder.get_input_embeddings()(torch.tensor(2368, device=torch_device))
# Insérer ceci dans les enchùssements de token
token_embeddings[0, torch.where(input_ids[0]==6829)] = replacement_token_embedding.to(torch_device)
# Combiner avec le'enchĂąssement positionnel
input_embeddings = token_embeddings + position_embeddings
# Passage dans le transformer pour obtenir les enchĂąssements finaux
modified_output_embeddings = get_output_embeds(input_embeddings)
print(modified_output_embeddings.shape)
modified_output_embeddings
torch.Size([1, 77, 768])
tensor([[[-0.3884, 0.0229, -0.0522, ..., -0.4899, -0.3066, 0.0675],
[ 0.0290, -1.3258, 0.3085, ..., -0.5257, 0.9768, 0.6652],
[ 0.6942, 0.3538, 1.0991, ..., -1.5716, -1.2643, -0.0121],
...,
[-0.6034, -0.5322, 0.0629, ..., -0.3964, 0.0877, -0.9558],
[-0.5936, -0.5407, 0.0731, ..., -0.3876, 0.0906, -0.9436],
[-0.6393, -0.4703, 0.1103, ..., -0.3904, 0.1351, -0.9726]]],
device='cuda:0', grad_fn=<NativeLayerNormBackward0>)
Les premiers sont identiques, les derniers ne le sont pas. Tout ce qui se trouve à la position du token que nous remplaçons et aprÚs sera affecté.
Si tout sâest bien passĂ©, nous devrions voir autre chose quâun chiot lorsque nous les utiliserons pour gĂ©nĂ©rer une image. Et bien sĂ»r, câest le cas !
# Génération d'une image avec ces enchùssements modifiés
def generate_with_embs(text_embeddings):
height = 512 # hauteur par défaut de Stable Diffusion
width = 512 # largeur par défaut de Stable Diffusion
num_inference_steps = 30 # Nombre d'étapes de débruitage
guidance_scale = 7.5 # Ăchelle pour un guidage sans classifieur
generator = torch.manual_seed(32) # Générateur de la graine pour créer le bruit latent initial
batch_size = 1
max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
[""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
with torch.no_grad():
uncond_embeddings = text_encoder(uncond_input.input_ids.to(torch_device))[0]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# Preparation du planificateur
scheduler.set_timesteps(num_inference_steps)
# Preparation des latents
latents = torch.randn(
(batch_size, unet.in_channels, height // 8, width // 8),
generator=generator,
)
latents = latents.to(torch_device)
latents = latents * scheduler.init_noise_sigma
# Boucle
for i, t in tqdm(enumerate(scheduler.timesteps)):
# étendre les latents si nous procédons à un guidage sans classifieur afin d'éviter de faire deux passages en avant
latent_model_input = torch.cat([latents] * 2)
sigma = scheduler.sigmas[i]
latent_model_input = scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
with torch.no_grad():
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings)["sample"]
# réaliser un guidage
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# calculer l'échantillon bruité précédent x_t -> x_t-1
latents = scheduler.step(noise_pred, t, latents).prev_sample
return latents_to_pil(latents)[0]
generate_with_embs(modified_output_embeddings)
Surprise ! Vous savez maintenant ce que signifie le token 2368.
Que pouvons-nous en faire ? Pourquoi nous sommes-nous donnĂ© tout ce mal ? Eh bien, nous verrons bientĂŽt un cas dâutilisation plus convaincant, mais en rĂ©sumĂ©, une fois que nous pouvons accĂ©der aux enchĂąssements de tokens et les modifier, nous pouvons faire des choses comme les remplacer par autre chose. Dans lâexemple que nous venons de faire, il sâagissait simplement dâun autre enchĂąssement de tokens du vocabulaire du modĂšle, ce qui Ă©quivaut Ă une simple modification du prompt. Mais nous pouvons Ă©galement mĂ©langer les tokens. Par exemple, voici un mi-chiot / mi-mouflette :
# Au cas oĂč vous vous demanderiez comment obtenir le *token* d'un mot, ou l'enchĂąssement d'un *token* :
prompt = 'skunk'
print('tokenizer(prompt):', tokenizer(prompt))
print('token_emb_layer([token_id]) shape:', token_emb_layer(torch.tensor([8797], device=torch_device)).shape)
tokenizer(prompt): {'input_ids': [49406, 42194, 49407], 'attention_mask': [1, 1, 1]}
token_emb_layer([token_id]) shape: torch.Size([1, 768])
prompt = 'A picture of a puppy'
# Tokeniser
text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
input_ids = text_input.input_ids.to(torch_device)
# Obtenir les enchĂąssements des tokens
token_embeddings = token_emb_layer(input_ids)
# Le nouvel enchùssement. Il s'agit maintenant d'un mélange d'enchùssement des tokens "puppy" et "skunk"
puppy_token_embedding = token_emb_layer(torch.tensor(6829, device=torch_device))
skunk_token_embedding = token_emb_layer(torch.tensor(42194, device=torch_device))
replacement_token_embedding = 0.5*puppy_token_embedding + 0.5*skunk_token_embedding
# Insérer ceci dans les enchùssements de token
token_embeddings[0, torch.where(input_ids[0]==6829)] = replacement_token_embedding.to(torch_device)
# Combiner avec le'enchĂąssement positionnel
input_embeddings = token_embeddings + position_embeddings
# Passage dans le transformer pour obtenir les enchĂąssements finaux
modified_output_embeddings = get_output_embeds(input_embeddings)
# Générer une image
generate_with_embs(modified_output_embeddings)
Inversion textuelle
Nous pouvons donc insĂ©rer un enchĂąssement de token modifiĂ© et lâutiliser pour gĂ©nĂ©rer une image. Nous avons utilisĂ© lâenchĂąssement de token pour « chat » dans lâexemple ci-dessus, mais que se passerait-il si nous pouvions « apprendre » un nouvel enchĂąssement de token pour un concept spĂ©cifique ? Câest lâidĂ©e qui sous-tend lâ« Inversion textuelle », dans laquelle quelques exemples dâimages sont utilisĂ©s pour crĂ©er un nouvel enchĂąssement de token :
Diagramme tirĂ© de lâarticle de blog sur lâinversion textuelle. Notez quâil ne montre pas lâĂ©tape des enchĂąssements positionnels pour des raisons de simplicitĂ©.
Nous ne verrons pas comment cet entraĂźnement fonctionne, mais nous pouvons essayer de charger lâun de ces nouveaux âconceptsâ Ă partir de la bibliothĂšque de concepts SD créée par la communautĂ© et voir comment il sâintĂšgre dans notre exemple ci-dessus. Nous utiliserons https://huggingface.co/sd-concepts-library/birb-style puisque câest le premier que nous avons créé. TĂ©lĂ©chargez le fichier learned_embeds.bin Ă partir de lĂ et tĂ©lĂ©chargez-le Ă lâendroit oĂč se trouve ce notebook avant dâexĂ©cuter la cellule suivante :
birb_embed = torch.load('learned_embeds.bin')
birb_embed.keys(), birb_embed['<birb-style>'].shape
(dict_keys(['<birb-style>']), torch.Size([768]))
Nous obtenons un dictionnaire avec une clĂ© et lâenchĂąssement de token correspondant. Comme dans lâexemple prĂ©cĂ©dent, remplaçons lâenchĂąssement de « puppy » par celui-ci et voyons ce qui se passe :
prompt = 'A mouse in the style of puppy'
# Tokeniser
text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
input_ids = text_input.input_ids.to(torch_device)
# Obtenir les enchĂąssements des tokens
token_embeddings = token_emb_layer(input_ids)
# Le nouvel enchùssement, notre mot d'ordre spécial
replacement_token_embedding = birb_embed['<birb-style>'].to(torch_device)
# Insérer ceci dans les enchùssements de token
token_embeddings[0, torch.where(input_ids[0]==6829)] = replacement_token_embedding.to(torch_device)
# Combiner avec le'enchĂąssement positionnel
input_embeddings = token_embeddings + position_embeddings
# Passage dans le transformer pour obtenir les enchĂąssements finaux
modified_output_embeddings = get_output_embeds(input_embeddings)
# Générer une image
generate_with_embs(modified_output_embeddings)
Le token a Ă©tĂ© remplacĂ© par une expression qui reprĂ©sente un style particulier de peinture, mais il pourrait tout aussi bien reprĂ©senter un objet ou une classe dâobjets spĂ©cifique.
Encore une fois, il existe un beau notebook dâinfĂ©rence dâHugging Face pour faciliter lâutilisation des diffĂ©rents concepts, qui gĂšre correctement lâutilisation des noms dans les prompts (â*A
Mélanger les enchùssements
Outre le simple remplacement de lâenchĂąssement des tokens dâun seul mot, il existe dâautres astuces que nous pouvons essayer. Par exemple, que se passe-t-il si nous crĂ©ons une « chimĂšre » en calculant la moyenne des enchĂąssements de deux prompts diffĂ©rents ?
# EnchĂąsser deux prompts
text_input1 = tokenizer(["A mouse"], padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
text_input2 = tokenizer(["A leopard"], padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
with torch.no_grad():
text_embeddings1 = text_encoder(text_input1.input_ids.to(torch_device))[0]
text_embeddings2 = text_encoder(text_input2.input_ids.to(torch_device))[0]
# Les mixer ensemble
mix_factor = 0.35
mixed_embeddings = (text_embeddings1*mix_factor + \
text_embeddings2*(1-mix_factor))
# Generer
generate_with_embs(mixed_embeddings)
LâUNet et le CFG (Classifier Free Guidance)
Il est maintenant temps dâexaminer le modĂšle de diffusion proprement dit. Il sâagit gĂ©nĂ©ralement dâun UNet qui prend en compte les latents bruyants (x) et prĂ©dit le bruit. Nous utilisons un modĂšle conditionnel qui prend Ă©galement en compte le pas de temps (t) et notre enchĂąssement de texte (aka encoder_hidden_states) comme conditionnement. Lâintroduction de tous ces Ă©lĂ©ments dans le modĂšle se prĂ©sente comme suit : noise_pred = unet(latents, t, encoder_hidden_states=text_embeddings)["sample"]
Nous pouvons lâessayer et voir Ă quoi ressemble le rĂ©sultat :
# Preparation du planificateur
scheduler.set_timesteps(num_inference_steps)
# Quel est notre pas de temps ?
t = scheduler.timesteps[0]
sigma = scheduler.sigmas[0]
# Un latent bruyant
latents = torch.randn(
(batch_size, unet.in_channels, height // 8, width // 8),
generator=generator,
)
latents = latents.to(torch_device)
latents = latents * scheduler.init_noise_sigma
# L'enchĂąssement du texte
text_input = tokenizer(['A macaw'], padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
with torch.no_grad():
text_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]
# Passage dans l'UNet pour prédire le bruit résiduel
with torch.no_grad():
noise_pred = unet(latents, t, encoder_hidden_states=text_embeddings)["sample"]
latents.shape, noise_pred.shape # Nous obtenons des prĂ©dictions de la mĂȘme forme que l'entrĂ©e
(torch.Size([1, 4, 64, 64]), torch.Size([1, 4, 64, 64]))
Ătant donnĂ© un ensemble de latents bruyants, le modĂšle prĂ©dit la composante de bruit. Nous pouvons retirer ce bruit des latents bruyants pour voir Ă quoi ressemble lâimage de sortie (latents_x0 = latents - sigma * noise_pred). Et nous pouvons ajouter la plus grande partie du bruit Ă cette sortie prĂ©dite pour obtenir lâentrĂ©e (lĂ©gĂšrement moins bruitĂ©e, espĂ©rons-le) pour lâĂ©tape de diffusion suivante. Pour visualiser cela, gĂ©nĂ©rons une autre image, en sauvegardant Ă la fois la sortie prĂ©dite (x0) et lâĂ©tape suivante (xt-1) aprĂšs chaque Ă©tape :
prompt = 'Oil painting of an otter in a top hat'
height = 512
width = 512
num_inference_steps = 50
guidance_scale = 8
generator = torch.manual_seed(32)
batch_size = 1
# Créer un dossier pour stocker les résultats
!rm -rf steps/
!mkdir -p steps/
# Preparation du texte
text_input = tokenizer([prompt], padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
with torch.no_grad():
text_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]
max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
[""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
with torch.no_grad():
uncond_embeddings = text_encoder(uncond_input.input_ids.to(torch_device))[0]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# Preparation du planificateur
scheduler.set_timesteps(num_inference_steps)
# Preparation des latents
latents = torch.randn(
(batch_size, unet.in_channels, height // 8, width // 8),
generator=generator,
)
latents = latents.to(torch_device)
latents = latents * scheduler.init_noise_sigma
# Boucle
for i, t in tqdm(enumerate(scheduler.timesteps)):
# étendre les latents si nous procédons à un guidage sans classifieur afin d'éviter de faire deux passages en avant
latent_model_input = torch.cat([latents] * 2)
sigma = scheduler.sigmas[i]
latent_model_input = scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
with torch.no_grad():
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings)["sample"]
# effectuer le guidage
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# Obtenir la valeur prédite x0 :
# latents_x0 = latents - sigma * noise_pred # Calculer nous-mĂȘmes
latents_x0 = scheduler.step(noise_pred, t, latents).pred_original_sample # Utilisation du planificateur (Diffuseurs 0.4 et plus)
# calculer l'échantillon bruité précédent x_t -> x_t-1
latents = scheduler.step(noise_pred, t, latents).prev_sample
# Vers des images PIL
im_t0 = latents_to_pil(latents_x0)[0]
im_next = latents_to_pil(latents)[0]
# Combinez les deux images et enregistrez-les pour une visualisation ultérieure
im = Image.new('RGB', (1024, 512))
im.paste(im_next, (0, 0))
im.paste(im_t0, (512, 0))
im.save(f'steps/{i:04}.jpeg')
# Réaliser et diffuser la vidéo sur l'état d'avancement (modifier la largeur à 1024 pour une pleine résolution)
!ffmpeg -v 1 -y -f image2 -framerate 12 -i steps/%04d.jpeg -c:v libx264 -preset slow -qp 18 -pix_fmt yuv420p out.mp4
mp4 = open('out.mp4','rb').read()
data_url = "data:video/mp4;base64," + b64encode(mp4).decode()
HTML("""
<video width=600 controls>
<source src="%s" type="video/mp4">
</video>
""" % data_url)
La version de droite montre la « sortie finale » prĂ©dite (x0) Ă chaque Ă©tape, et câest ce qui est gĂ©nĂ©ralement utilisĂ© pour les vidĂ©os de progression, etc. La version de gauche reprĂ©sente lâĂ©tape suivante. Nous trouvons intĂ©ressant de comparer les deux, en regardant les vidĂ©os de progression, on pourrait penser que des changements radicaux se produisent, en particulier aux premiers stades, mais comme les changements apportĂ©s Ă chaque Ă©tape sont relativement faibles, le processus rĂ©el est beaucoup plus progressif.
CFG (Classifier Free Guidance)
Par dĂ©faut, le modĂšle ne fait pas souvent ce que nous lui demandons. Si nous voulons quâil suive mieux le prompt, nous utilisons un hack appelĂ© CFG. Il y a une bonne explication dans cette vidĂ©o video dâAI Coffee Break with Letitia.
Dans le code, cela revient Ă faire :
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
Cela fonctionne Ă©tonnamment bien :) Essayez de changer le guidance_scale dans le code ci-dessus et voyez comment cela affecte les rĂ©sultats. JusquâoĂč pouvez-vous aller avant que les rĂ©sultats nâempirent ?
Ăchantillonnage
Il y a encore de la complexitĂ© cachĂ©e dans latents = scheduler.step(noise_pred, i, latents)["prev_sample"]. Comment lâĂ©chantillonneur passe-t-il exactement des latents bruyants actuels Ă une version lĂ©gĂšrement moins bruyante ? Pourquoi ne pas utiliser le modĂšle en une seule Ă©tape ? Existe-t-il dâautres façons de voir les choses ?
Le modĂšle tente de prĂ©dire le bruit dans une image. Pour des valeurs de bruit faibles, nous supposons quâil fait un assez bon travail. Pour des niveaux de bruit plus Ă©levĂ©s, la tĂąche est ardue ! Ainsi, au lieu de produire une image parfaite, les rĂ©sultats ont tendance Ă ressembler Ă un dĂ©sordre flou. Voir le dĂ©but de la vidĂ©o citĂ©e Ă lâinstant pour une illustration ! Les Ă©chantillonneurs utilisent donc les prĂ©dictions du modĂšle pour sâen rapprocher lĂ©gĂšrement (en Ă©liminant une partie du bruit), puis obtiennent une autre prĂ©diction basĂ©e sur cette entrĂ©e marginalement moins mauvaise, en espĂ©rant que cela amĂ©liorera le rĂ©sultat de maniĂšre itĂ©rative.
Les diffĂ©rents Ă©chantillonneurs procĂšdent de diffĂ©rentes maniĂšres. Vous pouvez essayer dâinspecter le code de lâĂ©chantillonneur LMS par dĂ©faut avec :
# ??scheduler.step
Guidage
Ok, derniÚre astuce ! Comment pouvons-nous ajouter un contrÎle supplémentaire à ce processus de génération ?
Ă chaque Ă©tape, nous allons utiliser notre modĂšle comme prĂ©cĂ©demment pour prĂ©dire la composante bruit de $x$. Ensuite, nous allons lâutiliser pour produire une image de sortie prĂ©dite, et appliquer une fonction de perte Ă cette image.
Cette fonction peut ĂȘtre nâimporte quoi, mais nous allons faire une dĂ©monstration avec un exemple trĂšs simple. Si nous voulons des images avec beaucoup de bleu, nous pouvons crĂ©er une fonction de perte qui donne une perte Ă©levĂ©e si les pixels ont une faible composante bleue :
def blue_loss(images):
# Quelle est la distance entre les valeurs du canal bleu et 0,9 ?
error = torch.abs(images[:,2] - 0.9).mean() # [:,2] -> toutes les images dans le batch, seulement le canal bleu
return error
Lors de chaque Ă©tape de mise Ă jour, nous trouvons le gradient de la perte par rapport aux latents bruyants actuels et nous les modifions dans la direction qui rĂ©duit cette perte tout en effectuant lâĂ©tape de mise Ă jour normale :
prompt = 'A campfire (oil on canvas)' #@param
height = 512 # hauteur par défaut de Stable Diffusion
width = 512 # largeur par défaut de Stable Diffusion
num_inference_steps = 50 #@param # Nombre d'étapes de débruitage
guidance_scale = 8 #@param # Ăchelle pour un guidage sans classifieur
generator = torch.manual_seed(32) # Générateur de graines pour créer le bruit latent initial
batch_size = 1
blue_loss_scale = 200 #@param
# Preparation du texte
text_input = tokenizer([prompt], padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")
with torch.no_grad():
text_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]
# Et l'entrée non conditionnelle comme précédemment :
max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
[""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
with torch.no_grad():
uncond_embeddings = text_encoder(uncond_input.input_ids.to(torch_device))[0]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# Preparation du planificateur
scheduler.set_timesteps(num_inference_steps)
# Preparation des latents
latents = torch.randn(
(batch_size, unet.in_channels, height // 8, width // 8),
generator=generator,
)
latents = latents.to(torch_device)
latents = latents * scheduler.init_noise_sigma
# Boucle
for i, t in tqdm(enumerate(scheduler.timesteps)):
# étendre les latents si nous procédons à un guidage sans classifieur afin d'éviter de faire deux passages en avant
latent_model_input = torch.cat([latents] * 2)
sigma = scheduler.sigmas[i]
latent_model_input = scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
with torch.no_grad():
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings)["sample"]
# réaliser le CFG
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
#### GUIDAGES SUPPLĂMENTAIRES ###
if i%5 == 0:
# Requires_grad sur les latents
latents = latents.detach().requires_grad_()
# Obtenir la valeur prédite x0 :
# latents_x0 = latents - sigma * noise_pred
latents_x0 = scheduler.step(noise_pred, t, latents).pred_original_sample
# Décodage vers l'espace d'image
denoised_images = vae.decode((1 / 0.18215) * latents_x0).sample / 2 + 0.5 # range (0, 1)
# Calculer la perte
loss = blue_loss(denoised_images) * blue_loss_scale
# Imprimer occasionnellement
if i%10==0:
print(i, 'loss:', loss.item())
# Obtenir le gradient
cond_grad = torch.autograd.grad(loss, latents)[0]
# Modifier les latents en fonction de ce gradient
latents = latents.detach() - cond_grad * sigma**2
# Etape avec le planificateur
latents = scheduler.step(noise_pred, t, latents).prev_sample
latents_to_pil(latents)[0]
0 loss: 182.02133178710938
10 loss: 43.55351257324219
20 loss: 15.30621337890625
30 loss: 9.746519088745117
40 loss: 8.846868515014648
Ajustez lâĂ©chelle (blue_loss_scale). A faible valeur, lâimage est principalement rouge et orange grĂące au prompt. Avec des valeurs plus Ă©levĂ©es, lâimage est surtout bleutĂ©e ! Si lâĂ©chelle est trop Ă©levĂ©e, lâimage devient bleue.
Comme câest lent, vous remarquerez que nous appliquons cette perte seulement une fois toutes les 5 itĂ©rations. Pour vos propres tests, vous pouvez envisager dâutiliser une Ă©chelle plus basse pour la perte et de lâappliquer Ă chaque itĂ©ration Ă la place :)
NB : Nous devrions mettre latents requires_grad=True avant de faire la passe avant de lâUNet (en enlevant avec torch.no_grad()) si nous voulons des gradients prĂ©cis. MAIS cela nĂ©cessite beaucoup de mĂ©moire supplĂ©mentaire. Vous verrez les deux approches utilisĂ©es en fonction de lâimplĂ©mentation que vous regardez.
Le guidage avec des modĂšles de classification peut vous donner des images dâune classe spĂ©cifique. Lâutilisation dâun modĂšle comme CLIP permet de mieux faire correspondre un prompt Ă un texte. Lâutilisation dâune perte de style permet dâajouter un style particulier. Le guidage avec une sorte de perte de perception peut lâorienter vers lâaspect gĂ©nĂ©ral dâune image cible. Et ainsi de suite.
Conclusion
Nous espĂ©rons que vous avez maintenant une idĂ©e un peu plus prĂ©cise de ce qui se passe lorsque vous crĂ©ez une image avec lâun de ces modĂšles, et de la façon dont vous pouvez modifier le processus de maniĂšre crĂ©ative. En espĂ©rant que cela vous donne envie de faire quelque chose dâamusant :)
Ce notebook a Ă©tĂ© Ă©crit par Jonathan Whitaker, adaptĂ© de Grokking Stable Diffusion qui Ă©tait sa premiĂšre tentative pour comprendre ces composants par lui-mĂȘme. Si vous repĂ©rez des bugs ou si vous avez des questions, nâhĂ©sitez pas Ă le contacter via @johnowhitaker :) Bonne lecture !
4. Vue d'ensemble
Dans cette unité, nous examinerons certaines des nombreuses améliorations et extensions des modÚles de diffusion apparaissant dans les recherches les plus récentes. Elle sera moins axée sur le code que les unités précédentes et est conçue pour vous donner un point de départ pour des recherches plus approfondies.
Vue dâensemble de cette unitĂ© đ
Les différentes étapes à suivre pour cette unité :
- Lisez le matĂ©riel ci-dessous pour avoir une vue dâensemble des idĂ©es clĂ©s de cette unitĂ©
- Approfondissez les sujets spécifiques grùce aux vidéos et aux ressources associées.
- Explorez les notebooks de démonstration, puis lisez la section « Et ensuite ? » pour obtenir des suggestions de projets.
Ăchantillonnage plus rapide par distillation
La distillation progressive est une technique permettant de prendre un modĂšle de diffusion existant et de lâutiliser pour entraĂźner une nouvelle version du modĂšle qui nĂ©cessite moins dâĂ©tapes pour lâinfĂ©rence. Le modĂšle âĂ©lĂšveâ est initialisĂ© Ă partir des poids du modĂšle âenseignantâ. Pendant lâentraĂźnement, le modĂšle enseignant effectue deux Ă©tapes dâĂ©chantillonnage et le modĂšle de Ă©tudiant tente de faire correspondre la prĂ©diction rĂ©sultante en une seule Ă©tape. Ce processus peut ĂȘtre rĂ©pĂ©tĂ© plusieurs fois, le modĂšle Ă©tudiant de lâitĂ©ration prĂ©cĂ©dente devenant le modĂšle enseignant pour lâĂ©tape suivante. Le rĂ©sultat est un modĂšle qui peut produire des Ă©chantillons dĂ©cents en beaucoup moins dâĂ©tapes (gĂ©nĂ©ralement 4 ou 8) que le modĂšle enseignant dâorigine. Le mĂ©canisme de base est illustrĂ© dans ce diagramme tirĂ© de lâarticle qui a introduit lâidĂ©e :

Illustration de la distillation progressive issue de ce papier
LâidĂ©e dâutiliser un modĂšle existant pour âenseignerâ un nouveau modĂšle peut ĂȘtre Ă©tendue pour crĂ©er des modĂšles guidĂ©s dans lesquels la technique de guidage sans classifieur est utilisĂ©e par le modĂšle enseignant et le modĂšle Ă©tudiant doit apprendre Ă produire un rĂ©sultat Ă©quivalent en une seule Ă©tape sur la base dâune entrĂ©e supplĂ©mentaire spĂ©cifiant lâĂ©chelle de guidage ciblĂ©e. Cela permet de rĂ©duire encore le nombre dâĂ©valuations de modĂšles nĂ©cessaires pour produire des Ă©chantillons de haute qualitĂ©. Cette vidĂ©o (en anglais) donne un aperçu de lâapproche.
Références principales :
- Progressive Distillation For Fast Sampling Of Diffusion Models
- On Distillation Of Guided Diffusion Models
AmĂ©lioration de lâentraĂźnement
Plusieurs astuces supplĂ©mentaires ont Ă©tĂ© mises au point pour amĂ©liorer lâentraĂźnement des modĂšles de diffusion. Dans cette section, nous avons essayĂ© de prĂ©senter les idĂ©es principales des articles rĂ©cents. Il y a un flux constant de recherches qui sortent avec des amĂ©liorations supplĂ©mentaires, donc si vous voyez un article qui devrait ĂȘtre ajoutĂ© ici, veuillez nous le faire savoir !
Figure 2 du papier ERNIE-ViLG 2.0
AmĂ©liorations principales de lâentraĂźnement :
- RĂ©glage du planificateur du bruit, de la pondĂ©ration de la perte et des trajectoires dâĂ©chantillonnage pour un entraĂźnement plus efficace. Un excellent papier explorant certains de ces choix de conception est Elucidating the Design Space of Diffusion-Based Generative Models par Karras et al.
- EntraĂźnement sur divers rapports dâaspect, comme dĂ©crit dans cette vidĂ©o du lancement du cours (en anglais).
- ModĂšles de diffusion en cascade, entraĂźnant un modĂšle Ă basse rĂ©solution, puis un ou plusieurs modĂšles en super-rĂ©solution. UtilisĂ©s dans DALLE-2, Imagen et dâautres pour la gĂ©nĂ©ration dâimages Ă haute rĂ©solution.
- Meilleur conditionnement, incorporation dâenchĂąssement textuels riches (Imagen utilise un grand modĂšle de langage appelĂ© T5) ou plusieurs types de conditionnement (eDiffi).
- âAmĂ©lioration des connaissancesâ : incorporation de modĂšles de sous-titrage dâimages et de dĂ©tection dâobjets prĂ©-entraĂźnĂ©s dans le processus dâentraĂźnement afin de crĂ©er des sous-titres plus informatifs et dâobtenir de meilleures performances (ERNIE-ViLG 2.0).
- âMĂ©lange dâexperts de dĂ©bruitageâ (MoDE) : entraĂźner diffĂ©rentes variantes du modĂšle (âexpertsâ) pour diffĂ©rents niveaux de bruit, comme illustrĂ© dans lâimage ci-dessus tirĂ©e du papier ERNIE-ViLG 2.0.
Références principales :
- Elucidating the Design Space of Diffusion-Based Generative Models
- eDiffi: Text-to-Image Diffusion Models with an Ensemble of Expert Denoisers
- ERNIE-ViLG 2.0: Improving Text-to-Image Diffusion Model with Knowledge-Enhanced Mixture-of-Denoising-Experts
- Imagen - Photorealistic Text-to-Image Diffusion Models with Deep Language Understanding (site démo)
Plus de contrĂŽle pour la gĂ©nĂ©ration et lâĂ©dition
Outre les amĂ©liorations apportĂ©es Ă lâentraĂźnement, plusieurs innovations ont Ă©tĂ© apportĂ©es Ă la phase dâĂ©chantillonnage et dâinfĂ©rence, y compris de nombreuses approches qui peuvent ajouter de nouvelles capacitĂ©s aux modĂšles de diffusion existants.
Ăchantillons gĂ©nĂ©rĂ©s par eDiffi
La vidĂ©o âEditing Images with Diffusion Modelsâ (en anglais) donne un aperçu des diffĂ©rentes mĂ©thodes utilisĂ©es pour Ă©diter des images existantes avec des modĂšles de diffusion. Les techniques disponibles peuvent ĂȘtre divisĂ©es en quatre catĂ©gories principales :
1) Ajouter du bruit, puis dĂ©bruiter avec un nouveau prompt. Câest lâidĂ©e qui sous-tend le pipeline img2img, qui a Ă©tĂ© modifiĂ© et Ă©tendu dans plusieurs articles :
- SDEdit et MagicMix sâinspirent de cette idĂ©e
- DDIM inversion utilise le modĂšle pour âinverserâ la trajectoire dâĂ©chantillonnage plutĂŽt que dâajouter un bruit alĂ©atoire, ce qui permet un meilleur contrĂŽle
- Null-text Inversion amĂ©liore considĂ©rablement les performances de ce type dâapproche en optimisant Ă chaque Ă©tape les enchĂąssements de texte inconditionnels utilisĂ©s pour le guidage sans classifieur, ce qui permet dâobtenir une Ă©dition dâimages textuelles de trĂšs haute qualitĂ©. 2) Extension des idĂ©es du point (1), mais avec un masque permettant de contrĂŽler lâendroit oĂč lâeffet est appliquĂ©
- Blended Diffusion introduit lâidĂ©e de base
- Cette dĂ©mo utilise un modĂšle de segmentation existant (CLIPSeg) pour crĂ©er le masque sur la base dâune description textuelle
- DiffEdit est un excellent papier montrant comment le modĂšle de diffusion lui-mĂȘme peut ĂȘtre utilisĂ© pour gĂ©nĂ©rer un masque appropriĂ© pour lâĂ©dition de lâimage en fonction du texte
- SmartBrush: Text and Shape Guided Object Inpainting with Diffusion Model finetune un modĂšle de diffusion pour une peinture guidĂ©e par un masque 3) ContrĂŽle de lâattention croisĂ©e : utilisation du mĂ©canisme dâattention croisĂ©e dans les modĂšles de diffusion pour contrĂŽler lâemplacement spatial des modifications afin dâexercer un contrĂŽle plus fin
- Prompt-to-Prompt Image Editing with Cross Attention Control est lâarticle clĂ© qui a introduit cette idĂ©e, et la technique a depuis Ă©tĂ© appliquĂ©e Ă Stable Diffusion
- TCette idĂ©e est Ă©galement utilisĂ©e pour âpaint-with-wordsâ (eDiffi, voir ci-dessus) 4) Finetuner (surapprendre) sur une seule image, puis gĂ©nĂ©rer avec le modĂšle finetunĂ©. Les articles suivants ont tous deux publiĂ© des variantes de cette idĂ©e Ă peu prĂšs au mĂȘme moment :
- Imagic: Text-Based Real Image Editing with Diffusion Models
- UniTune: Text-Driven Image Editing by Fine Tuning an Image Generation Model on a Single Image
Le papier InstructPix2Pix : Learning to Follow Image Editing Instructions est remarquable en ce sens quâil utilise certaines des techniques dâĂ©dition dâimages dĂ©crites ci-dessus pour construire un jeu de donnĂ©es synthĂ©tique de paires dâimages accompagnĂ©es dâinstructions dâĂ©dition dâimages (gĂ©nĂ©rĂ©es avec GPT3.5) afin dâentraĂźner un nouveau modĂšle capable dâĂ©diter des images sur la base dâinstructions en langage naturel.
Video
Images fixes dâexemples de vidĂ©os gĂ©nĂ©rĂ©es avec Imagen Video
Une vidĂ©o peut ĂȘtre reprĂ©sentĂ©e comme une sĂ©quence dâimages, et les idĂ©es fondamentales des modĂšles de diffusion peuvent ĂȘtre appliquĂ©es Ă ces sĂ©quences. Les travaux rĂ©cents se sont concentrĂ©s sur la recherche dâarchitectures appropriĂ©es (telles que les « 3D UNets » qui opĂšrent sur des sĂ©quences entiĂšres) et sur lâutilisation efficace des donnĂ©es vidĂ©o. Ătant donnĂ© que les vidĂ©os Ă haute frĂ©quence dâimages comportent beaucoup plus de donnĂ©es que les images fixes, les approches actuelles tendent Ă gĂ©nĂ©rer dâabord des vidĂ©os Ă faible rĂ©solution et Ă faible frĂ©quence dâimages, puis Ă appliquer la super-rĂ©solution spatiale et temporelle pour produire les sorties vidĂ©o finales de haute qualitĂ©.
Références principales :
Audio

Un spectrogramme généré avec Riffusion
Bien que des travaux aient Ă©tĂ© rĂ©alisĂ©s pour gĂ©nĂ©rer du son directement Ă lâaide de modĂšles de diffusion (par exemple DiffWave), lâapproche la plus fructueuse jusquâĂ prĂ©sent a consistĂ© Ă convertir le signal audio en ce que lâon appelle un spectrogramme, qui âencodeâ effectivement le son sous la forme dâune âimageâ en 2D qui peut ensuite ĂȘtre utilisĂ©e pour entraĂźner les modĂšles de diffusion que nous avons lâhabitude dâutiliser pour la gĂ©nĂ©ration dâimages. Les spectrogrammes ainsi gĂ©nĂ©rĂ©s peuvent ensuite ĂȘtre convertis en donnĂ©es audio Ă lâaide des mĂ©thodes existantes. Cette approche est Ă lâorigine de Riffusion, qui a rĂ©cemment Ă©tĂ© publiĂ© et a permis de finetuner Stable Diffusion pour gĂ©nĂ©rer des spectrogrammes conditionnĂ©s par le texte. Essayez-le ici.
Le domaine de la gĂ©nĂ©ration dâaudio Ă©volue trĂšs rapidement. Au cours de la semaine derniĂšre (Ă lâheure oĂč nous Ă©crivons ces lignes), au moins cinq nouvelles avancĂ©es ont Ă©tĂ© annoncĂ©es, qui sont marquĂ©es dâune Ă©toile dans la liste ci-dessous :
Références principales :
- DiffWave: A Versatile Diffusion Model for Audio Synthesis
- Riffusion (et son code)
- â MusicLM de Google gĂ©nĂšre un son cohĂ©rent Ă partir dâun texte et peut ĂȘtre conditionnĂ© avec des mĂ©lodies fredonnĂ©es ou sifflĂ©es.
- â RAVE2, une nouvelle version dâun auto-encodeur variationnel qui sera utile pour la diffusion latente dans les tĂąches audio. Il est utilisĂ© dans le modĂšle AudioLDM.
- â Noise2Music, un modĂšle de diffusion entraĂźnĂ© Ă produire des clips audio de 30 secondes en haute qualitĂ© sur la base de descriptions textuelles.
- â Make-An-Audio: Text-To-Audio Generation with Prompt-Enhanced Diffusion Models, un modĂšle de diffusion entraĂźnĂ© Ă gĂ©nĂ©rer divers sons Ă partir dâun texte.
- â MoĂ»sai: Text-to-Music Generation with Long-Context Latent Diffusion
Nouvelles architectures et approches : vers un « raffinement itératif »

Figure 1 du papier Cold Diffusion
Nous dĂ©passons peu Ă peu la dĂ©finition Ă©troite initiale des modĂšles de « diffusion » pour nous orienter vers une classe plus gĂ©nĂ©rale de modĂšles qui effectuent un raffinement itĂ©ratif, oĂč une certaine forme de corruption (comme lâajout dâun bruit gaussien dans le processus de diffusion vers lâavant) est progressivement inversĂ©e pour gĂ©nĂ©rer des Ă©chantillons. Lâarticle « Cold Diffusion » a dĂ©montrĂ© que de nombreux autres types de corruption peuvent ĂȘtre « dĂ©faits » de maniĂšre itĂ©rative pour gĂ©nĂ©rer des images (exemples ci-dessus), et des approches rĂ©centes basĂ©es sur des transfomers ont dĂ©montrĂ© lâefficacitĂ© du remplacement ou du masquage de token en tant que stratĂ©gie de bruitage.

Pipeline de MaskGIT
Lâarchitecture UNet au cĆur de nombreux modĂšles de diffusion actuels est Ă©galement remplacĂ©e par dâautres solutions, notamment diverses architectures basĂ©es sur des transformers. Dans Scalable Diffusion Models with Transformers (DiT), un transformer est utilisĂ© Ă la place du UNet pour une approche de modĂšle de diffusion assez standard, avec dâexcellents rĂ©sultats. Recurrent Interface Networks applique une nouvelle architecture basĂ©e sur un transformer et une stratĂ©gie dâentraĂźnement Ă la recherche dâune efficacitĂ© accrue. MaskGIT et MUSE utilisent des transformers pour travailler avec des reprĂ©sentations dâimages par tokens, bien que le modĂšle Paella dĂ©montre quâun UNet peut Ă©galement ĂȘtre appliquĂ© avec succĂšs Ă ces rĂ©gimes basĂ©s sur des tokens.
Avec chaque nouveau papier, des approches plus efficaces sont dĂ©veloppĂ©es, et il faudra peut-ĂȘtre attendre un certain temps avant de voir Ă quoi ressemblent les performances maximales pour ce type de tĂąches dâaffinage itĂ©ratif. Il reste encore beaucoup de choses Ă explorer !
Références principales :
- Cold Diffusion: Inverting Arbitrary Image Transforms Without Noise
- Scalable Diffusion Models with Transformers (DiT)
- MaskGIT: Masked Generative Image Transformer
- Muse: Text-To-Image Generation via Masked Generative Transformers
- Fast Text-Conditional Discrete Denoising on Vector-Quantized Latent Spaces (Paella)
- Recurrent Interface Networks : une nouvelle architecture prometteuse qui permet de gĂ©nĂ©rer des images Ă haute rĂ©solution sans recourir Ă la diffusion latente ou Ă la super-rĂ©solution. Voir Ă©galement simple diffusion : End-to-end diffusion for high-resolution images qui souligne lâimportance du planificateur du bruit pour lâentraĂźnement Ă des rĂ©solutions plus Ă©levĂ©es.
Notebooks
| Chapitre | Colab | Kaggle | Gradient | Studio Lab |
|---|---|---|---|---|
| Débruitage inverse des modÚles de diffusion implicites | ||||
| Diffusion pour lâaudio |
Nous avons abordĂ© un grand nombre dâidĂ©es diffĂ©rentes dans cette unitĂ©, dont beaucoup mĂ©riteraient de faire lâobjet de leçons plus dĂ©taillĂ©es Ă lâavenir. Pour lâinstant, vous pouvez aborder deux de ces nombreux sujets via les notebook que nous avons prĂ©parĂ©s.
- Le dĂ©bruitage inverse des modĂšles de diffusion implicites montre comment une technique appelĂ©e inversion peut ĂȘtre utilisĂ©e pour Ă©diter des images Ă lâaide de modĂšles de diffusion existants.
- Diffusion pour lâaudio introduit lâidĂ©e de spectrogrammes et montre un exemple minimal de finetuning dâun modĂšle de diffusion pour lâaudio sur un genre de musique spĂ©cifique.
Et ensuite ?
Il sâagit de la derniĂšre unitĂ© de ce cours actuellement, ce qui signifie que la suite ne dĂ©pend que de vous ! Nâoubliez pas que vous pouvez toujours poser des questions et discuter de vos projets sur le Discord dâHugging Face. Nous avons hĂąte de voir ce que vous allez crĂ©er đ€
4.1. Débruitage inverse des modÚles de diffusion implicites (DDIM)
Dans ce notebook, nous allons explorer lâinversion, voir comment elle est liĂ©e Ă lâĂ©chantillonnage, et lâappliquer Ă la tĂąche dâĂ©dition dâimages avec Stable Diffusion. Ce que vous allez apprendre :
- Comment fonctionne lâĂ©chantillonnage DDIM
- Ăchantillonneurs dĂ©terministes et stochastiques
- La thĂ©orie derriĂšre lâinversion DDIM
- LâĂ©dition dâimages avec lâinversion
Commençons !
Configuration
# !pip install -q transformers diffusers accelerate
import torch
import requests
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
from io import BytesIO
from tqdm.auto import tqdm
from matplotlib import pyplot as plt
from torchvision import transforms as tfms
from diffusers import StableDiffusionPipeline, DDIMScheduler
# Une fonction utile pour plus tard
def load_image(url, size=None):
response = requests.get(url,timeout=0.2)
img = Image.open(BytesIO(response.content)).convert('RGB')
if size is not None:
img = img.resize(size)
return img
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
Chargement dâun pipeline existant
# Charger un pipeline
pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5").to(device)
# Mettre en place un planificateur DDIM
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
# Ăchantillon d'une image pour s'assurer que tout fonctionne bien
prompt = 'Beautiful DSLR Photograph of a penguin on the beach, golden hour'
negative_prompt = 'blurry, ugly, stock photo'
im = pipe(prompt, negative_prompt=negative_prompt).images[0]
im.resize((256, 256)) # redimensionner pour une meilleure visualisation
Echantillonage DDIM
Ă un moment donnĂ© $t$, lâimage bruitĂ©e $x_t$ est un mĂ©lange de lâimage originale ($x_0$) et de bruit ($\epsilon$). Voici la formule pour $x_t$ tirĂ©e de lâarticle DDIM, Ă laquelle nous nous rĂ©fĂ©rerons dans cette section :
\[x_t = \sqrt{\alpha_t}x_0 + \sqrt{1-\alpha_t}\epsilon\]$\epsilon$ est un bruit gaussien de variance unitaire
$\alpha_t$ (âalphaâ) est la valeur qui est appelĂ©e de maniĂšre confuse $\bar{\alpha}$ (âalpha_barâ) dans le papier DDPM ( !!) et qui dĂ©finit le planificateur de bruit. Dans đ€ Diffusers, le planificateur alpha est calculĂ© et les valeurs sont stockĂ©es dans le scheduler.alphas_cumprod. Je sais que câest dĂ©routant ! Traçons ces valeurs, et nâoubliez pas que pour le reste de ce notebook, nous utiliserons la notation de DDIM.
# Tracer 'alpha' (alpha_bar dans DDPM, alphas_cumprod dans Diffusers)
timesteps = pipe.scheduler.timesteps.cpu()
alphas = pipe.scheduler.alphas_cumprod[timesteps]
plt.plot(timesteps, alphas, label='alpha_t');
plt.legend()
Au départ (étape 0, cÎté gauche du graphique), nous commençons avec une image propre et sans bruit. $\alpha_t = 1$. Au fur et à mesure que nous passons à des pas de temps plus élevés, nous nous retrouvons avec presque tout le bruit et $\alpha_t$ chute vers 0.
Lors de lâĂ©chantillonnage, nous commençons avec du bruit pur au pas de temps $1000$ et nous nous rapprochons lentement du pas de temps $0$. Pour calculer le prochain $t$ de la trajectoire dâĂ©chantillonnage ($x_{t-1}$ puisque nous passons dâun $t$ Ă©levĂ© Ă un $t$ faible), nous prĂ©disons le bruit ($\epsilon_\theta(x_t)$, qui est la sortie de notre modĂšle) et nous lâutilisons pour calculer lâimage dĂ©bruitĂ©e prĂ©dite $x_0$. Nous utilisons ensuite cette prĂ©diction pour nous dĂ©placer sur une petite distance dans la « direction pointant vers $x_t$ ». Enfin, nous pouvons ajouter du bruit supplĂ©mentaire Ă lâĂ©chelle de $\sigma_t$. Voici la section de lâarticle qui montre cette mĂ©thode en action :
Nous disposons donc dâune Ă©quation permettant de passer de $x_t$ Ă $x_{t-1}$, avec une quantitĂ© de bruit contrĂŽlable. Dans notre cas prĂ©sent, nous nous intĂ©ressons plus particuliĂšrement au cas oĂč nous nâajoutons aucun bruit supplĂ©mentaire, ce qui nous donne un Ă©chantillonnage DDIM entiĂšrement dĂ©terministe. Voyons ce que cela donne en code :
# Fonction d'échantillonnage (DDIM standard)
@torch.no_grad()
def sample(prompt, start_step=0, start_latents=None,
guidance_scale=3.5, num_inference_steps=30,
num_images_per_prompt=1, do_classifier_free_guidance=True,
negative_prompt='', device=device):
# Encoder le prompt
text_embeddings = pipe._encode_prompt(
prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt
)
# Nombre d'étapes d'inférence
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Créer un point de départ aléatoire si nous n'en avons pas déjà un
if start_latents is None:
start_latents = torch.randn(1, 4, 64, 64, device=device)
start_latents *= pipe.scheduler.init_noise_sigma
latents = start_latents.clone()
for i in tqdm(range(start_step, num_inference_steps)):
t = pipe.scheduler.timesteps[i]
# développer les latents si l'on procÚde à un guidage sans classifieur
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
#réaliser un guidage
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# Normalement, nous devrions nous fier au planificateur pour gérer l'étape de mise à jour :
# latents = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# Au lieu de cela, faisons-le nous-mĂȘmes :
prev_t = max(1, t.item() - (1000//num_inference_steps)) # t-1
alpha_t = pipe.scheduler.alphas_cumprod[t.item()]
alpha_t_prev = pipe.scheduler.alphas_cumprod[prev_t]
predicted_x0 = (latents - (1-alpha_t).sqrt()*noise_pred) / alpha_t.sqrt()
direction_pointing_to_xt = (1-alpha_t_prev).sqrt()*noise_pred
latents = alpha_t_prev.sqrt()*predicted_x0 + direction_pointing_to_xt
# Post-traitement
images = pipe.decode_latents(latents)
images = pipe.numpy_to_pil(images)
return images
# Tester notre fonction d'échantillonnage en générant une image
sample('Watercolor painting of a beach sunset', negative_prompt=negative_prompt, num_inference_steps=50)[0].resize((256, 256))
Voyez si vous pouvez faire correspondre le code avec lâĂ©quation de lâarticle. Notez que $\sigma$=0 puisque nous ne nous intĂ©ressons quâau cas oĂč il nây a pas de bruit supplĂ©mentaire, nous pouvons donc laisser de cĂŽtĂ© ces Ă©lĂ©ments de lâĂ©quation.
Inversion
Lâobjectif est dâinverser le processus dâĂ©chantillonnage. Nous voulons obtenir un latent bruitĂ© qui, sâil est utilisĂ© comme point de dĂ©part de notre procĂ©dure dâĂ©chantillonnage habituelle, permet de gĂ©nĂ©rer lâimage originale.
Ici, nous chargeons une image comme image initiale, mais vous pouvez Ă©galement en gĂ©nĂ©rer une vous-mĂȘme pour lâutiliser Ă la place.
# https://www.pexels.com/photo/a-beagle-on-green-grass-field-8306128/
input_image = load_image('https://images.pexels.com/photos/8306128/pexels-photo-8306128.jpeg', size=(512, 512))
input_image
Nous allons Ă©galement utiliser un prompt pour effectuer lâinversion avec lâaide dâun classifieur libre, alors entrez une description de lâimage :
input_image_prompt = "Photograph of a puppy on the grass"
Ensuite, nous devons transformer cette image PIL en un ensemble de latents que nous utiliserons comme point de départ de notre inversion :
# encoder avec le VAE
with torch.no_grad(): latent = pipe.vae.encode(tfms.functional.to_tensor(input_image).unsqueeze(0).to(device)*2-1)
l = 0.18215 * latent.latent_dist.sample()
TrĂšs bien, il est temps de passer Ă la partie amusante. Cette fonction ressemble Ă la fonction dâĂ©chantillonnage ci-dessus, mais nous nous dĂ©plaçons Ă travers les pas de temps dans la direction opposĂ©e, en commençant Ă $t=0$ et en nous dĂ©plaçant vers un bruit de plus en plus Ă©levĂ©. Et au lieu de mettre Ă jour nos latents pour quâils soient moins bruyants, nous estimons le bruit prĂ©dit et lâutilisons pour ANNULER une Ă©tape de mise Ă jour, en les dĂ©plaçant de $t$ Ă $t+1$.
## Inversion
@torch.no_grad()
def invert(start_latents, prompt, guidance_scale=3.5, num_inference_steps=80,
num_images_per_prompt=1, do_classifier_free_guidance=True,
negative_prompt='', device=device):
# Encoder le prompt
text_embeddings = pipe._encode_prompt(
prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt
)
# les latents sont maintenant les latents de départ spécifiés
latents = start_latents.clone()
# Nous garderons une liste des latents inversés au fur et à mesure du processus
intermediate_latents = []
# Définir le nombre d'étapes de l'inférence
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Pas de temps inversés <<<<<<<<<<<<<<<<<<<<
timesteps = reversed(pipe.scheduler.timesteps)
for i in tqdm(range(1, num_inference_steps), total=num_inference_steps-1):
# Nous allons sauter l'itération finale
if i >= num_inference_steps - 1: continue
t = timesteps[i]
# développer les latents si l'on fait de l'orientation sans classifieur
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# prédire le bruit résiduel
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# effectuer un guidage
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
current_t = max(0, t.item() - (1000//num_inference_steps))#t
next_t = t # min(999, t.item() + (1000//num_inference_steps)) # t+1
alpha_t = pipe.scheduler.alphas_cumprod[current_t]
alpha_t_next = pipe.scheduler.alphas_cumprod[next_t]
# Ătape de mise Ă jour inversĂ©e (rĂ©organisation de l'Ă©tape de mise Ă jour pour obtenir x(t) (nouveaux latents) en fonction de x(t-1) (latents actuels)
latents = (latents - (1-alpha_t).sqrt()*noise_pred)*(alpha_t_next.sqrt()/alpha_t.sqrt()) + (1-alpha_t_next).sqrt()*noise_pred
# Stockage
intermediate_latents.append(latents)
return torch.cat(intermediate_latents)
En lâexĂ©cutant sur la reprĂ©sentation latente de notre photo de chiot, nous obtenons un ensemble de tous les latents intermĂ©diaires créés au cours du processus dâinversion :
inverted_latents = invert(l, input_image_prompt,num_inference_steps=50)
inverted_latents.shape
torch.Size([48, 4, 64, 64])
Nous pouvons visualiser lâensemble final de latents - ceux-ci constitueront, nous lâespĂ©rons, le point de dĂ©part bruyant de nos nouvelles tentatives dâĂ©chantillonnage :
# Décoder les latents inversés finaux
with torch.no_grad():
im = pipe.decode_latents(inverted_latents[-1].unsqueeze(0))
pipe.numpy_to_pil(im)[0]
Vous pouvez transmettre ces latents inversés au pipeline en utilisant la méthode__call__ normale :
pipe(input_image_prompt, latents=inverted_latents[-1][None], num_inference_steps=50, guidance_scale=3.5).images[0]
Mais câest lĂ que nous voyons notre premier problĂšme : ce nâest pas tout Ă fait lâimage avec laquelle nous avons commencĂ© ! En effet, lâinversion DDIM repose sur une hypothĂšse critique selon laquelle la prĂ©diction du bruit Ă lâinstant $t$ et Ă lâinstant $t+1$ sera la mĂȘme, ce qui nâest pas vrai lorsque lâinversion ne porte que sur $50$ ou $100$ pas de temps. Nous pourrions utiliser davantage de pas de temps pour espĂ©rer obtenir une inversion plus prĂ©cise, mais nous pouvons Ă©galement tricher et commencer Ă partir de, disons, $20/50$ pas dâĂ©chantillonnage avec les latents intermĂ©diaires correspondants que nous avons sauvegardĂ©s lors de lâinversion :
# La raison pour laquelle nous voulons pouvoir spécifier l'étape de démarrage
start_step=20
sample(input_image_prompt, start_latents=inverted_latents[-(start_step+1)][None],
start_step=start_step, num_inference_steps=50)[0]
TrĂšs proche de notre image dâentrĂ©e ! Pourquoi faisons-nous cela ? Eh bien, lâespoir est que si nous Ă©chantillonnons maintenant avec un nouveau prompt, nous obtiendrons une image qui correspond Ă lâoriginal SAUF aux endroits pertinents pour le nouveau prompt. Par exemple, en remplaçant « puppy » par « cat », nous devrions voir un chat avec un dos et un arriĂšre-plan presque identiques :
# Ăchantillonnage avec un nouveau prompt
start_step=10
new_prompt = input_image_prompt.replace('puppy', 'cat')
sample(new_prompt, start_latents=inverted_latents[-(start_step+1)][None],
start_step=start_step, num_inference_steps=50)[0]
Pourquoi ne pas utiliser img2img ?
Pourquoi sâembĂȘter Ă inverser ? Ne peut-on pas simplement ajouter du bruit Ă lâimage dâentrĂ©e et la dĂ©bruiter avec le nouveau prompt ? Nous le pouvons, mais cela entraĂźnera des changements beaucoup plus radicaux partout (si nous ajoutons beaucoup de bruit) ou des changements insuffisants partout (si nous ajoutons moins de bruit). Essayez vous-mĂȘme :
start_step = 10
num_inference_steps=50
pipe.scheduler.set_timesteps(num_inference_steps)
noisy_l = pipe.scheduler.add_noise(l, torch.randn_like(l), pipe.scheduler.timesteps[start_step])
sample(new_prompt, start_latents=noisy_l, start_step=start_step, num_inference_steps=num_inference_steps)[0]
Notez la modification beaucoup plus importante de la pelouse et de lâarriĂšre-plan.
Rassembler le tout
Rassemblons le code que nous avons Ă©crit jusquâĂ prĂ©sent dans une fonction simple qui prend une image et deux prompts et effectue une modification en utilisant lâinversion :
def edit(input_image, input_image_prompt, edit_prompt, num_steps=100, start_step=30, guidance_scale=3.5):
with torch.no_grad(): latent = pipe.vae.encode(tfms.functional.to_tensor(input_image).unsqueeze(0).to(device)*2-1)
l = 0.18215 * latent.latent_dist.sample()
inverted_latents = invert(l, input_image_prompt,num_inference_steps=num_steps)
final_im = sample(edit_prompt, start_latents=inverted_latents[-(start_step+1)][None],
start_step=start_step, num_inference_steps=num_steps, guidance_scale=guidance_scale)[0]
return final_im
Et en action :
edit(input_image, 'A puppy on the grass', 'an old grey dog on the grass', num_steps=50, start_step=10)
edit(input_image, 'A puppy on the grass', 'A blue dog on the lawn', num_steps=50, start_step=12, guidance_scale=6)
âïž Ă votre tour ! Essayez ceci sur dâautres images ! Explorez les diffĂ©rents paramĂštres.
Plus de pas = meilleure performance
Si vous avez des problĂšmes avec des inversions moins prĂ©cises, vous pouvez essayer dâutiliser plus de pas (au prix dâun temps dâexĂ©cution plus long). Pour tester lâinversion, vous pouvez utiliser notre fonction dâĂ©dition avec le mĂȘme prompt :
# Test d'inversion avec beaucoup plus d'étapes :
edit(input_image, 'A puppy on the grass', 'A puppy on the grass', num_steps=350, start_step=1)
Câest beaucoup mieux ! Et en essayant de lâĂ©diter :
edit(input_image, 'A photograph of a puppy', 'A photograph of a grey cat', num_steps=150, start_step=30, guidance_scale=5.5)
# source: https://www.pexels.com/photo/girl-taking-photo-1493111/
face = load_image('https://images.pexels.com/photos/1493111/pexels-photo-1493111.jpeg', size=(512, 512))
face
edit(face, 'A photograph of a face', 'A photograph of a face with sunglasses', num_steps=250, start_step=30, guidance_scale=3.5)
edit(face, 'A photograph of a face', 'Acrylic palette knife painting of a face, colorful', num_steps=250, start_step=65, guidance_scale=5.5)
Et ensuite ?
ArmĂ© des connaissances de ce notebook, nous vous recommandons dâĂ©tudier Null-text Inversion qui sâappuie sur DDIM en optimisant le texte nul (prompt inconditionnel) lors de lâinversion pour des inversions plus prĂ©cises et de meilleures Ă©ditions.
4.2. 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 !
5. ĂvĂ©nement pour la sortie du cours
Pour accompagner la sortie du cours, nous organisons un Ă©vĂ©nement communautaire en direct le 30 novembre 2022 auquel vous ĂȘtes conviĂ©s ! Au programme, des interventions passionnantes des crĂ©ateurs de Stable Diffusion, des chercheurs de Stability AI et de Meta, et bien dâautres encore !
Les interventions se concentreront sur une présentation de haut niveau des modÚles de diffusion et des outils permettant de créer des applications.
Intelligence collective et IA créative par David Ha
David Ha est responsable de la stratĂ©gie chez Stability AI. Auparavant, il a travaillĂ© comme chercheur chez Google, au sein de lâĂ©quipe Brain au Japon. Ses recherches portent sur les systĂšmes complexes, lâauto-organisation et les applications crĂ©atives de lâapprentissage automatique. Avant de rejoindre Google, il a travaillĂ© chez Goldman Sachs en tant que Managing Director, oĂč il a codirigĂ© les activitĂ©s de nĂ©gociation de titres Ă revenu fixe au Japon. Il a obtenu une licence et une maĂźtrise Ă lâuniversitĂ© de Toronto, ainsi quâun doctorat Ă lâuniversitĂ© de Tokyo.
Vous pouvez le trouver sur Twitter ou sur son site personnel.
IA pour augmenter la créativité humaine par Devi Parikh
Devi Parikh est directrice de recherche au laboratoire Fundamental AI Research (FAIR) de Meta et professeur associĂ© Ă lâĂ©cole dâinformatique interactive de Georgia Tech. Elle a occupĂ© des postes dâintervenante Ă lâuniversitĂ© Cornell, Ă lâuniversitĂ© du Texas Ă Austin, Ă Microsoft Research, au MIT, Ă lâuniversitĂ© Carnegie Mellon et Ă Facebook AI Research. Elle a obtenu sa maĂźtrise et son doctorat du dĂ©partement dâingĂ©nierie Ă©lectrique et informatique de lâuniversitĂ© Carnegie Mellon en 2007 et 2009 respectivement. Ses recherches portent sur la vision artificielle, le traitement du langage naturel, lâintelligence artificielle incarnĂ©e, la collaboration entre lâhomme et lâintelligence artificielle et lâintelligence artificielle au service de la crĂ©ativitĂ©.
Vous pouvez la trouver sur Twitter ou sur son site personnel.
Nourriture pour Diffusion par Patrick Esser
Patrick Esser est chercheur principal chez Runway, oĂč il dirige les efforts de recherche appliquĂ©e, notamment le modĂšle de base de Stable Diffusion, Ă©galement connu sous le nom de High-Resolution Image Synthesis with Latent Diffusion Models.
Vous pouvez le trouver sur Twitter.
Au-delĂ du texte : Donner de nouvelles capacitĂ©s Ă Stable Diffusion par Justin Pinkney Justin est chercheur senior en apprentissage automatique chez Lambda Labs. Il travaille sur la gĂ©nĂ©ration et lâĂ©dition dâimages, en particulier pour les applications artistiques et crĂ©atives. Il adore jouer et bidouiller des modĂšles prĂ©-entraĂźnĂ©s pour leur ajouter de nouvelles capacitĂ©s, et est probablement mieux connu pour des modĂšles comme : Toonify, Stable Diffusion Image Variations, et Text-to-Pokemon. Vous pouvez le trouver sur Twitter ou sur son site personnel.
Les modĂšles de diffusion sont cool mais quâarrive tâil aprĂšs aprĂšs lâengouement ? par ApolinĂĄrio Passos
ApolinĂĄrio Passos est un ingĂ©nieur en apprentissage automatique chez Hugging Face et un artiste qui se concentre sur lâart gĂ©nĂ©ratif et les mĂ©dias gĂ©nĂ©ratifs. Il a fondĂ© la plateforme multimodal.art et le compte Twitter correspondant, et travaille sur lâorganisation, lâagrĂ©gation et la plateformisation des modĂšles dâapprentissage automatique des mĂ©dias gĂ©nĂ©ratifs open-source.
Vous pouvez le trouver sur Twitter.
Stable Diffusion et amis : SynthĂšse dâimages en haute rĂ©solution via des modĂšles gĂ©nĂ©ratifs en deux Ă©tapes par Robin Rombach
Robin est chercheur Ă Stability AI. AprĂšs avoir Ă©tudiĂ© la physique Ă lâUniversitĂ© de Heidelberg de 2013 Ă 2020, il a commencĂ© un doctorat en informatique dans le groupe Computer Vision Ă Heidelberg en 2020 sous la supervision de Björn Ommer et a dĂ©mĂ©nagĂ© Ă LMU Munich avec le groupe de recherche en 2021. Ses recherches portent sur les modĂšles gĂ©nĂ©ratifs dâapprentissage profond, en particulier les systĂšmes texte-image. Au cours de son doctorat, Robin a jouĂ© un rĂŽle dĂ©terminant dans le dĂ©veloppement et la publication de plusieurs projets dĂ©sormais largement utilisĂ©s, tels que VQGAN et Taming Transformers, et Latent Diffusion Models. En collaboration avec Stability AI, Robin a mis Ă lâĂ©chelle lâapproche de diffusion latente et a publiĂ© une sĂ©rie de modĂšles maintenant connus sous le nom de Stable Diffusion, qui ont Ă©tĂ© largement adaptĂ©s par la communautĂ©.
Vous pouvez le trouver sur Twitter.
5.1. Hackathon DreamBooth đ
đŁ Le hackathon est maintenant terminĂ© et les gagnants ont Ă©tĂ© annoncĂ©s sur Discord. Vous ĂȘtes toujours invitĂ©s Ă entraĂźner des modĂšles et Ă les soumettre au classement, mais nous nâoffrirons plus de prix ou de certificats pour le moment.
Bienvenue au Hackathon DreamBooth !
Câest un Ă©vĂ©nement communautaire oĂč vous personnaliserez un modĂšle de Stable Diffusion en le finetunant sur une poignĂ©e de vos images. Pour ce faire, vous utiliserez une technique puissante appelĂ©e DreamBooth, qui permet dâimplanter un sujet (par exemple votre animal de compagnie ou votre plat prĂ©fĂ©rĂ©) dans le domaine de sortie du modĂšle de sorte quâil puisse ĂȘtre synthĂ©tisĂ© avec un unique identifiant dans le prompt.
Cette compĂ©tition est composĂ©e de 5 thĂšmes, oĂč chacun rassemblera des modĂšles appartenant aux catĂ©gories suivantes :
- Animal đš : Utilisez ce thĂšme pour gĂ©nĂ©rer des images de votre animal de compagnie ou de votre animal favori en train de se promener dans lâAcropole, de nager ou de voler dans lâespace.
- Science đŹ : Utilisez ce thĂšme pour gĂ©nĂ©rer des images synthĂ©tiques de galaxies, de protĂ©ines ou de tout autre domaine des sciences naturelles et mĂ©dicales.
- Nourriture đ : Utilisez ce thĂšme pour finetuner un modĂšle sur votre plat ou cuisine prĂ©fĂ©rĂ©.
- Paysage đ : Utilisez ce thĂšme pour crĂ©er de magnifiques paysages de votre montagne, lac ou jardin prĂ©fĂ©rĂ©.
- Carte blanche đ„ : Utilisez ce thĂšme pour crĂ©er des modĂšles dans la catĂ©gorie de votre choix !
Nous attribuerons des prix aux 3 modĂšles les plus apprĂ©ciĂ©s par thĂšme, et vous ĂȘtes encouragĂ©s Ă soumettre autant de modĂšles que vous le souhaitez !
Pour commencer
Suivez les étapes ci-dessous pour participer à cet événement :
- Rejoignez le serveur Discord dâHugging Face et consultez le canal
#dreambooth-hackathonpour vous tenir au courant du dĂ©roulement de lâĂ©vĂ©nement. - Lancez et exĂ©cutez le notebook pour entraĂźner vos modĂšles en cliquant sur lâun des liens ci-dessous. Veillez Ă sĂ©lectionner lâexĂ©cution sur GPU pour vous assurer que vos modĂšles sâentraĂźnent rapidement !
| Notebook | Colab | Kaggle | Gradient | Studio Lab |
|---|---|---|---|---|
| EntraĂźnement de DreamBooth |
Note đ :
Le notebook DreamBooth utilise le checkpoint CompVis/stable-diffusion-v1-4 comme modĂšle de Diffusion Stable Ă finetuner. Cependant, vous ĂȘtes totalement libre dâutiliser nâimporte quel checkpoint, il vous suffira dâajuster le code pour charger les composants appropriĂ©s et le vĂ©rificateur de sĂ©curitĂ© (sâil existe). Voici quelques modĂšles intĂ©ressants Ă finetuner :
runwayml/stable-diffusion-v1-5prompthero/openjourneystabilityai/stable-diffusion-2hakurei/waifu-diffusionstabilityai/stable-diffusion-2-1nitrosocke/elden-ring-diffusion
Evaluation & Classement
Pour ĂȘtre dans la course aux prix, poussez un ou plusieurs modĂšles DreamBooth sur le Hub avec le tag dreambooth-hackathon dans la carte du modĂšle (exemple). Ce tag est créé automatiquement par le notebook de DreamBooth, mais vous devrez lâajouter si vous exĂ©cutez vos propres scripts.
Les modĂšles sont Ă©valuĂ©s en fonction du nombre de jâaimes et vous pouvez suivre le classement de votre modĂšle sur le tableau de classement du hackathon :
Chronologie
- 21 décembre 2022 : Date de démarrage
- 31 dĂ©cembre 2022 : Date limite dâinscription Ă Colab Pro
- 22 janvier 2023 : Date limite de soumission (clĂŽture du classement)
- 23-27 janvier 2023 : Annonce des gagnants de chaque thĂšme
Tous les délais sont fixés à 23h59 UTC le jour correspondant, sauf indication contraire.
Prix
Nous attribuerons 3 prix par thĂšme, les gagnants Ă©tant dĂ©terminĂ©s par les modĂšles ayant le plus de jâaimes sur le tableau de classement :
1Ăšre place
- Abonnement Hugging Face Pro pour 1 an ou un bon dâachat de 100 $ pour le magasin de produits dĂ©rivĂ©s dâHugging Face
2Ăšme place
- Une copie du livre NLP with Transformers ou un bon dâachat de 50 $ pour le magasin de produits dĂ©rivĂ©s dâHugging Face
3Ăšme place
- Abonnement Hugging Face Pro pour 1 mois ou un bon dâachat de 15 $ pour le magasin de produits dĂ©rivĂ©s dâHugging Face
Nous fournirons Ă©galement un certificat dâachĂšvement Ă tous les participants qui soumettront au moins 1 modĂšle DreamBooth au hackathon đ„.
Calculs
Google Colab sponsorisera cet Ă©vĂ©nement en offrant des crĂ©dits Colab Pro Ă 100 participants (sĂ©lectionnĂ©s au hasard). Les crĂ©dits seront distribuĂ©s en janvier 2023 et vous avez jusquâau 31 dĂ©cembre pour vous inscrire. Pour vous inscrire, veuillez remplir ce formulaire.
FAQ
Quelles sont les données autorisées pour le finetuning ?
Vous pouvez utiliser toutes les images qui vous appartiennent ou pour lesquelles une licence permissive vous autorise Ă le faire. Si vous souhaitez soumettre un modĂšle entraĂźnĂ© sur des visages (par exemple dans le cadre dâune carte blanche), nous vous recommandons dâutiliser vos propres traits. IdĂ©alement, utilisez vos propres donnĂ©es lorsque vous le pouvez - nous aimerions voir vos animaux de compagnie ou vos paysages locaux prĂ©fĂ©rĂ©s, et nous soupçonnons que les jâaimes et les prix iront Ă ceux qui font quelque chose de gentil et de personnel đ.
Dâautres techniques de finetuning comme lâinversion textuelle sont-elles autorisĂ©es ?
Absolument ! Bien que ce hackathon soit centrĂ© sur DreamBooth, vous ĂȘtes les bienvenus (et encouragĂ©s) Ă expĂ©rimenter dâautres techniques de finetuning. Cela signifie aussi que vous pouvez utiliser nâimporte quel framework, code ou service qui vous aide Ă crĂ©er des modĂšles magnifiques pour que la communautĂ© puisse en profiter đ„°.
5.2. Sprint Dreambooth en Keras
Cette paget rĂ©sume toutes les informations pertinentes requises pour lâĂ©vĂ©nement. đ.
Introduction
Dreambooth est une technique de finetuning permettant dâenseigner de nouveaux concepts visuels Ă des modĂšles de diffusion conditionnĂ©s par le texte en utilisant seulement 3 Ă 5 images. Avec Dreambooth, vous pouvez gĂ©nĂ©rer des images drĂŽles et rĂ©alistes de votre chien, de vous-mĂȘme et de nâimporte quel concept avec quelques images en utilisant Stable Diffusion. DreamBooth a Ă©tĂ© proposĂ© dans DreamBooth : Fine Tuning Text-to-Image Diffusion Models for Subject-Driven Generation par Ruiz et al. (2022)
Nous allons entraĂźner les modĂšles Dreambooth Ă lâaide de KerasCV et construire des dĂ©monstrations sur ces modĂšles.
Dates importantes
- Lancement de lâĂ©vĂ©nement : 6 mars 2023
Avec Nataniel Ruiz sur DreamBooth, François Chollet sur KerasCV et Apolinario Passos sur đ€ Diffusers
- Début du sprint : 7 mars 2023
- Fin du sprint : 1er avril 2023
- Résultats : 7 avril 2023
DĂ©marrer đ
Pour commencer, rejoignez-nous sur hf.co/join/discord et prenez le rĂŽle #open-source, et rencontrez-nous sur le canal #keras-working-group.
Nous hĂ©bergerons nos dĂ©monstrations dans cette organisation sur Hugging Face Hub : keras-dreambooth, envoyez une demande ici si vous souhaitez soumettre une proposition đ
Nous allons :
- Finetuner Stable Diffusion sur nâimporte quel concept que nous voulons en utilisant Dreambooth,
- Pousser le modĂšle vers le Hub dâHugging Face,
- Remplir la carte du modĂšle,
- Construire une démo à partir du modÚle.
Avertissement: Les modĂšles entraĂźnĂ©s doivent ĂȘtre dans lâune des 4 catĂ©gories mentionnĂ©es dans la section Soumission. Veuillez y jeter un coup dâĆil avant dâentraĂźner votre modĂšle.
EntraĂźnement du modĂšle
Vous pouvez trouver le notebook ici (en anglais) et lâadapter Ă votre propre jeu de donnĂ©es.
Quelques inspirations pour le finetuning :
- Lowpoly World : Ce modĂšle gĂ©nĂšre des mondes low poly đ€Żđ
- Future Diffusion : Ce modĂšle gĂ©nĂšre des images dans des concepts de science-fiction futuristes đ€
- Fantasy sword : Ce modĂšle gĂ©nĂšre des Ă©pĂ©es pour des jeux Ă thĂšme fantastique đ§ââïž
Si vous avez besoin de plus dâindications sur lâimplĂ©mentation de Dreambooth avec Keras, vous pouvez consulter ce dĂ©pĂŽt.
Dreambooth avec KerasCV
Pour lâinstant, les options dâinfĂ©rence et de dĂ©ploiement de KerasCV sont limitĂ©es, et câest lĂ que la bibliothĂšque diffusers vient Ă la rescousse. Avec seulement quelques lignes de code, nous pouvons convertir un modĂšle KerasCV en un modĂšle diffusers et utiliser les pipelines diffusers pour effectuer lâinfĂ©rence. Vous pouvez obtenir plus dâinformations ici. Consultez aussi ce Space pour convertir votre modĂšle KerasCV en un modĂšle diffusers.
Les dĂ©pĂŽts diffusers sur le Hub bĂ©nĂ©ficient dâune API dâinfĂ©rence gratuite et de petits widgets dans la page du modĂšle oĂč les utilisateurs peuvent jouer avec le modĂšle.
from diffusers import StableDiffusionPipeline
# point de contrĂŽle de Stable Diffusion converti de KerasCV
model_ckpt = "sayakpaul/text-unet-dogs-kerascv_sd_diffusers_pipeline"
pipeline = StableDiffusionPipeline.from_pretrained(model_ckpt)
pipeline.to("cuda")
unique_id = "sks"
class_label = "dog"
prompt = f"A photo of {unique_id} {class_label} in a bucket"
image = pipeline(prompt, num_inference_steps=50).images[0]
Hébergement du modÚle
Ă la fin du notebook vous verrez une section dĂ©diĂ©e Ă lâhĂ©bergement, et une section sĂ©parĂ©e pour lâinfĂ©rence. Nous utiliserons les fonctions de chargement et de poussĂ©e de modĂšles spĂ©cifiques Ă Keras de la bibliothĂšque huggingface_hub : push_to_hub_keras et from_pretrained_keras. Nous allons dâabord pousser le modĂšle en utilisant push_to_hub_keras. Une fois le modĂšle poussĂ©, vous verrez que le modĂšle est hĂ©bergĂ© avec une carte de modĂšle comme ci-dessous :

Pour mieux versionner les modĂšles, permettre la dĂ©couvrabilitĂ© et la reproductibilitĂ©, nous allons remplir la carte de modĂšle. Cliquez sur *Edit model card*. Nous allons dâabord remplir la section Metadata de la fiche de modĂšle. Si votre modĂšle est entraĂźnĂ© avec un jeu de donnĂ©es du Hub, vous pouvez remplir la section des jeux de donnĂ©es avec le jeu de donnĂ©es. Nous allons remplir pipeline_tag avec text-to-image et choisir une licence pour notre modĂšle.

Ensuite, nous remplirons la partie markdown. Les hyperparamĂštres et le graphe sont automatiquement gĂ©nĂ©rĂ©s, nous pouvons donc Ă©crire une courte explication pour la description, lâutilisation prĂ©vue et le jeu de donnĂ©es.
Vous pouvez trouver lâexemple de dĂ©pĂŽt ci-dessous ici.
Démo
Nous allons utiliser Gradio pour construire nos dĂ©monstrations pour les modĂšles que nous avons entraĂźnĂ©s. Avec la classe Interface, câest simple :
from huggingface_hub import from_pretrained_keras
from keras_cv import models
import gradio as gr
sd_dreambooth_model = models.StableDiffusion(
img_width=512, img_height=512
)
db_diffusion_model = from_pretrained_keras("merve/dreambooth_diffusion_model")
sd_dreambooth_model._diffusion_model = db_diffusion_model
# générer des images
def infer(prompt):
generated_images = sd_dreambooth_model.text_to_image(
prompt
)
return generated_images
output = gr.Gallery(label="Outputs").style(grid=(2,2))
# la fonction de passage, le type d'entrée pour le prompt, la sortie pour les images multiples
gr.Interface(infer, inputs=["text"], outputs=[output]).launch()
Vous pouvez consulter le fichier app.py de lâapplication ci-dessous et le rĂ©utiliser pour votre modĂšle !
Dreambooth Submission - a Hugging Face Space par keras-dreambooth
Cette application gĂ©nĂšre des images dâun corgi đ¶

Hébergement de la démonstration sur Spaces
Une fois notre application terminĂ©e, nous pouvons crĂ©er un Space sur Hugging Face pour hĂ©berger notre application. Vous pouvez aller sur huggingface.co, cliquer sur votre profil en haut Ă droite et sĂ©lectionner âNew Spaceâ.

Nous pouvons nommer notre Space, choisir une licence et sélectionner « Gradio » comme Space SDK.

AprĂšs avoir créé le Space, vous pouvez utiliser soit les instructions ci-dessous pour cloner le dĂ©pĂŽt localement, ajouter vos fichiers et pousser, OU, lâinterface graphique pour crĂ©er les fichiers et Ă©crire le code dans le navigateur.

Pour télécharger votre fichier, cliquez sur « Add File » et faites glisser/déposer votre fichier.

Enfin, nous devons créer un fichier appelé requirements.txt et ajouter les conditions du projet Dreambooth comme ci-dessous :
keras-cv
tensorflow
huggingface-hub
Et votre application devrait ĂȘtre opĂ©rationnelle !
Nous hĂ©bergerons nos modĂšles et nos Spaces sous cette organisation. Vous pouvez transporter vos modĂšles et Spaces dans lâonglet paramĂštres sous Rename or transfer this model et sĂ©lectionner keras-dreambooth dans le menu dĂ©roulant.
Si vous ne voyez pas keras-dreambooth dans la liste dĂ©roulante, il est probable que vous ne soyez pas membre de lâorganisation. Utilisez ce lien pour demander Ă rejoindre lâorganisation.
Soumission
Vous pouvez soumettre votre projet dans trois thĂšmes :
- Nature et animaux (
nature) - Univers de science-fiction/fantastique (
sci-fi) - Conscient (
consentful) : Associez-vous Ă un artiste pour finetuner son style avec son consentement ! Assurez-vous dâinclure une rĂ©fĂ©rence au consentement explicite de lâartiste (par exemple un tweet) dans votre carte de modĂšle. - Carte blanche (
wild-card) : Si votre soumission appartient Ă une catĂ©gorie qui nâest pas mentionnĂ©e ci-dessus, nâhĂ©sitez pas Ă lâĂ©tiqueter avecwild-cardafin que nous puissions lâĂ©valuer en dehors de cette catĂ©gorie.
Ajoutez les catégories et leurs identifiants à votre carte modÚle et ajoutez keras-dreambooth aux métadonnées dans la section des tags. Voici un exemple de carte de modÚle. Toutes les soumissions seront compilées dans ce classement et classées en fonction du nombre de likes sur un espace donné afin de déterminer les gagnants.
Prix
Nous choisirons trois gagnants parmi les applications soumises, en fonction du nombre de likes accordés à un espace dans une catégorie donnée.
đïž Le premier remportera un bon dâachat de 100$ sur hf.co/shop ou un an dâabonnement Ă Hugging Face Pro
đïž La deuxiĂšme remportera un bon dâachat de 50$ sur hf.co/shop ou le livre Natural Language Processing with Transformers.
đïž Le troisiĂšme remportera un bon dâachat de 30$ sur hf.co/shop ou trois mois dâabonnement Ă Hugging Face Pro
5.3. Sprint ControlNet en JAX/Diffusers
Bienvenue au sprint communautaire en JAX/Diffusers ! Lâobjectif de ce sprint est de travailler sur des modĂšles de diffusion amusants et crĂ©atifs en utilisant JAX et Diffusers.
Lors de cet événement, nous créerons diverses applications avec des modÚles de diffusion en JAX/Flax et Diffusers en utilisant des heures TPU gratuites généreusement fournies par Google Cloud.
Ce document présente toutes les informations importantes pour faire une soumission au sprint.
Organisation
Les participants peuvent proposer des idées pour un projet intéressant impliquant des modÚles de diffusion. Des équipes de 3 à 5 personnes seront ensuite formées autour des projets les plus prometteurs et les plus intéressants. Assurez-vous de lire la section Communication pour savoir comment proposer des projets, commenter les idées de projet des autres participants et créer une équipe.
Pour aider chaque Ă©quipe Ă mener Ă bien son projet, nous organiserons des confĂ©rences donnĂ©es par des scientifiques et des ingĂ©nieurs de Google, de Hugging Face et de la communautĂ© open source. Les confĂ©rences auront lieu le 17 avril. Assurez-vous dâassister aux confĂ©rences pour tirer le meilleur parti de votre participation ! Consultez la section ConfĂ©rences pour avoir une vue dâensemble des confĂ©rences, y compris lâorateur et lâheure de la confĂ©rence.
Chaque Ă©quipe bĂ©nĂ©ficiera ensuite dâun accĂšs gratuit Ă une VM TPU v4-8 du 14 avril au 1er mai. De plus, nous fournirons un exemple dâentraĂźnement en JAX/Flax et Diffusers pour entraĂźner un ControlNet afin de lancer votre projet. Nous fournirons Ă©galement des exemples sur la façon de prĂ©parer les jeux de donnĂ©es. Pendant le sprint, nous nous assurerons de rĂ©pondre Ă toutes les questions que vous pourriez avoir sur JAX/Flax et Diffusers et nous aiderons chaque Ă©quipe autant que possible !
Nous ne distribuerons pas de TPU pour les Ă©quipes composĂ©es dâun seul membre. Nous vous encourageons donc Ă rejoindre une Ă©quipe ou Ă trouver des coĂ©quipiers pour votre idĂ©e.
Ă la fin du sprint, chaque soumission sera Ă©valuĂ©e par un jury et les trois meilleures dĂ©monstrations recevront un prix. Consultez la section Comment soumettre une dĂ©mo pour plus dâinformations et de suggestions sur la maniĂšre de soumettre votre projet.
Note : MĂȘme si nous fournissons un exemple pour entraĂźner ControlNet, les participants peuvent proposer des idĂ©es qui nâimpliquent pas du tout un ControlNet du moment quâelles sont centrĂ©es sur les modĂšles de diffusion.
Dates importantes
- 29/03 : Annonce officielle de la semaine de la communauté.
- 31/03 : Commencez Ă former des groupes dans le canal #jax-diffusers-ideas sur Discord.
- 10/04 : Collecte des données.
- 13/04 & 14/04 & 17/04 : Conférences de lancement sur YouTube.
- 14/04 Ă 17/04 : DĂ©but de lâaccĂšs aux TPU.
- 01/05 : Fermeture de lâaccĂšs aux TPU.
- 08/05 : Annonce des 10 meilleurs projets et des prix.
Note : Nous accepterons les candidatures tout au long du sprint.
Communication
Toutes les communications importantes auront lieu sur notre serveur Discord. Rejoignez le serveur en utilisant ce lien. AprĂšs avoir rejoint le serveur, prenez le rĂŽle Diffusers dans le canal #role-assignment et dirigez-vous vers le canal #jax-diffusers-ideas pour partager votre idĂ©e sous la forme dâun message de forum. Pour vous inscrire, remplissez le formulaire dâinscription et nous vous donnerons accĂšs Ă deux canaux Discord supplĂ©mentaires pour les discussions et le support technique, ainsi quâun accĂšs aux TPU.
Les annonces importantes de lâĂ©quipe Hugging Face, Flax/JAX et Google Cloud seront publiĂ©es sur le serveur.
Le serveur Discord sera le lieu central oĂč les participants pourront publier leurs rĂ©sultats, partager leurs expĂ©riences dâapprentissage, poser des questions et obtenir une assistance technique pour les divers obstacles quâils rencontrent.
Pour les problÚmes liés à Flax/JAX, Diffusers, Datasets ou pour des questions spécifiques à votre projet, nous interagirons à travers les dépÎts publics et les forums :
- Flax : Issues, Questions
- JAX : Issues, Questions
- đ€ Diffusers : Issues, Questions
- đ€ Dataset s: Issues, Questions
- Questions spĂ©cifiques aux projets : Elles peuvent ĂȘtre posĂ©es sur le canal #jax-diffusers-ideas sur Discord.
- Questions relatives au TPU : Canal
#jax-diffusers-tpu-supportsur Discord. - Discussion générale :
#jax-diffusers-sprint channelsur Discord. Vous aurez accÚs aux canaux#jax-diffusers-tpu-supportet#jax-diffusers-sprintune fois que vous aurez été accepté pour participer au sprint.
Lorsque vous demandez de lâaide, nous vous encourageons Ă poster le lien vers le forum sur le serveur Discord, plutĂŽt que de poster directement des issues ou des questions. De cette façon, nous nous assurons que tout le monde peut bĂ©nĂ©ficier de vos questions, mĂȘme aprĂšs la fin du sprint.
Note : AprĂšs le 10 avril, si vous vous ĂȘtes inscrit sur le formulaire Google, mais que vous nâĂȘtes pas dans le canal Discord, veuillez laisser un message sur lâannonce officielle du forum et envoyer un ping Ă
@mervenoyan,@sayakpaul, et@patrickvonplaten. Il se peut que nous prenions un jour pour traiter ces demandes.
Conférences
Nous avons invitĂ© dâĂ©minents chercheurs et ingĂ©nieurs de Google, Hugging Face, et de la communautĂ© open-source qui travaillent dans le domaine de lâIA gĂ©nĂ©rative. Nous mettrons Ă jour cette section avec des liens vers les confĂ©rences, alors gardez un Ćil ici ou sur Discord dans le canal diffusion models core-announcements et programmez vos rappels !
13 avril 2023
| Intervenant | Sujet | Horaire | Video |
|---|---|---|---|
| Emiel Hoogeboom, Google Brain | Pixel-Space Diffusion models for High Resolution Images | 4.00pm-4.40pm CEST / 7.00am-7.40am PST | |
| ApolinĂĄrio Passos, Hugging Face | Introduction to Diffusers library | 4.40pm-5.20pm CEST / 7.40am-08.20am PST | |
| Ting Chen, Google Brain | Diffusion++: discrete data and high-dimensional generation | 5.45pm-6.25pm CEST / 08.45am-09.25am PST |
14 avril 2023
| Intervenant | Sujet | Horaire | Video |
|---|---|---|---|
| Tim Salimans, Google Brain | Efficient image and video generation with distilled diffusion models | 4.00pm-4.40pm CEST / 7.00am-7.40am PST | |
| Suraj Patil, Hugging Face | Masked Generative Models: MaskGIT/Muse | 4.40pm-5.20pm CEST / 7.40am-08.20am PST | |
| Sabrina Mielke, John Hopkins University | From stateful code to purified JAX: how to build your neural net framework | 5.20pm-6.00pm CEST / 08.20am-09.00am PST |
17 avril 2023
| Intervenant | Sujet | Horaire | Video |
|---|---|---|---|
| Andreas Steiner, Google Brain | JAX & ControlNet | 4.00pm-4.40pm CEST / 7.00am-7.40am PST | |
| Boris Dayma, craiyon | DALL-E Mini | 4.40pm-5.20pm CEST / 7.40am-08.20am PST | |
| Margaret Mitchell, Hugging Face | Ethics of Text-to-Image | 5.20pm-6.00pm CEST / 08.20am-09.00am PST |
Données et prétraitement
Dans cette section, nous verrons comment construire votre propre jeu de données pour entraßner ControlNet.
Préparer un grand jeu de données local
Monter un disque
Si vous avez besoin dâespace supplĂ©mentaire, vous pouvez suivre ce guide pour crĂ©er un disque persistant, lâattacher Ă votre VM TPU et crĂ©er un rĂ©pertoire pour monter le disque. Vous pouvez ensuite utiliser ce rĂ©pertoire pour stocker votre jeu de donnĂ©es.
Par ailleurs, la VM TPU attribuĂ©e Ă votre Ă©quipe dispose dâun disque de stockage persistant de 3 To. Pour apprendre Ă lâutiliser, consultez ce guide.
Prétraitement des données
Nous montrons ici comment préparer un grand jeu de données pour entraßner un modÚle ControlNet avec filtre de Canny. Plus précisément, nous fournissons un exemple de script qui :
- SĂ©lectionne 1 million de paires image-texte Ă partir dâun jeu de donnĂ©es existant COYO-700M.
- TĂ©lĂ©charge chaque image et utilise le filtre de Canny pour gĂ©nĂ©rer lâimage de conditionnement.
- Crée un métafichier qui relie toutes les images et les images traitées à leurs légendes.
Utilisez la commande suivante pour exĂ©cuter le script de prĂ©traitement des donnĂ©es de lâexemple. Si vous avez montĂ© un disque sur votre TPU, vous devez placer vos fichiers train_data_dir et cache_dir sur le disque montĂ©.
python3 coyo_1m_dataset_preprocess.py \
--train_data_dir="/mnt/disks/persist/data" \
--cache_dir="/mnt/disks/persist" \
--max_train_samples=1000000 \
--num_proc=16
Une fois le script exécuté, vous trouverez un dossier de données dans le répertoire train_data_dir spécifié avec la structure de dossier ci-dessous :
data
âââ images
â âââ image_1.png
â âââ .......
â âââ image_1000000.jpeg
âââ processed_images
â âââ image_1.png
â âââ .......
â âââ image_1000000.jpeg
âââ meta.jsonl
Charger un jeu de données
Pour charger un jeu de donnĂ©es Ă partir du dossier de donnĂ©es que vous venez de crĂ©er, vous devez ajouter un script de chargement de jeu de donnĂ©es Ă votre dossier de donnĂ©es. Le script de chargement de donnĂ©es doit porter le mĂȘme nom que le dossier. Par exemple, si votre dossier de donnĂ©es est data, vous devez ajouter un script de chargement de donnĂ©es nommĂ© data.py. Nous fournissons un exemple de script de chargement de donnĂ©es que vous pouvez utiliser. Tout ce que vous avez Ă faire est de mettre Ă jour le DATA_DIR avec le chemin correct de votre dossier de donnĂ©es. Pour plus de dĂ©tails sur lâĂ©criture dâun script de chargement de donnĂ©es, reportez-vous Ă la documentation.
Une fois que le script de chargement de données est ajouté à votre dossier de données, vous pouvez le charger avec :
dataset = load_dataset("/mnt/disks/persist/data", cache_dir="/mnt/disks/persist" )
Notez que vous pouvez utiliser --train_data_dir pour passer le rĂ©pertoire de votre dossier de donnĂ©es au script dâentraĂźnement et gĂ©nĂ©rer votre jeu de donnĂ©es automatiquement pendant lâentraĂźnement.
Pour les grands jeux de donnĂ©es, nous recommandons de gĂ©nĂ©rer le jeu de donnĂ©es une seule fois et de le sauvegarder sur le disque Ă lâaide de la commande
dataset.save_to_disk("/mnt/disks/persist/dataset")
Vous pouvez ensuite réutiliser le jeu de données sauvegardé pour votre entraßnement en passant --load_from_disk.
Voici un exemple dâexĂ©cution dâun script dâentraĂźnement qui chargera le jeu de donnĂ©es depuis le disque.
export MODEL_DIR="runwayml/stable-diffusion-v1-5"
export OUTPUT_DIR="/mnt/disks/persist/canny_model"
export DATASET_DIR="/mnt/disks/persist/dataset"
export DISK_DIR="/mnt/disks/persist"
python3 train_controlnet_flax.py \
--pretrained_model_name_or_path=$MODEL_DIR \
--output_dir=$OUTPUT_DIR \
--train_data_dir=$DATASET_DIR \
--load_from_disk \
--cache_dir=$DISK_DIR \
--resolution=512 \
--learning_rate=1e-5 \
--train_batch_size=2 \
--revision="non-ema" \
--from_pt \
--max_train_steps=500000 \
--checkpointing_steps=10000 \
--dataloader_num_workers=16
Préparer un jeu de données avec MediaPipe et Hugging Face
Nous fournissons un notebook ( ) qui vous montre comment préparer un jeu de données pour entraßner ControlNet en utilisant MediaPipe et Hugging Face. Plus précisément, dans le notebook, nous montrons :
- Comment tirer parti des solutions MediaPipe pour extraire les articulations du corps de la pose Ă partir des images dâentrĂ©e.
- PrĂ©dire les lĂ©gendes en utilisant BLIP-2 Ă partir des images dâentrĂ©e en utilisant đ€ Transformers.
- Construire et pousser le jeu de donnĂ©es final vers le Hugging Face Hub en utilisant đ€ Datasets.
Vous pouvez vous rĂ©fĂ©rer au notebook pour crĂ©er vos propres jeux de donnĂ©es en utilisant dâautres solutions MediaPipe. Ci-dessous, nous listons toutes les solutions pertinentes :
EntraĂźner ControlNet
Câest peut-ĂȘtre la partie la plus amusante et la plus intĂ©ressante de ce document, car nous vous montrons ici comment entraĂźner un modĂšle ControlNet personnalisĂ©.
Note : Pour ce sprint, vous nâĂȘtes PAS limitĂ© Ă entraĂźner des ControlNets. Nous fournissons ce script dâentraĂźnement comme rĂ©fĂ©rence pour vous permettre de dĂ©marrer.
Pour un entraĂźnement plus rapide sur les TPU et les GPU, vous pouvez tirer parti de lâexemple dâentraĂźnement Flax. Suivez les instructions ci-dessus pour obtenir le modĂšle et le jeu de donnĂ©es avant dâexĂ©cuter le script.
Mise en place de la VM TPU
Avant de continuer avec le reste de cette section, vous devez vous assurer que lâadresse email que vous utilisez a Ă©tĂ© ajoutĂ©e au projet hf-flax sur Google Cloud Platform. Si ce nâest pas le cas, merci de nous le faire savoir sur le serveur Discord (vous pouvez taguer @sayakpaul, @merve, et @patrickvonplaten).
Dans ce qui suit, nous allons dĂ©crire comment le faire en utilisant une console standard, mais vous devriez Ă©galement ĂȘtre en mesure de vous connecter Ă la VM TPU via des IDE, comme Visual Studio Code, etc.
-
Vous devez installer le Google Cloud SDK. Veuillez suivre les instructions sur https://cloud.google.com/sdk.
-
Une fois le Google Cloud SDK installé, vous devez configurer votre compte en exécutant la commande suivante. Assurez-vous que
correspond à l'adresse gmail que vous avez utilisée pour vous inscrire à cet événement. gcloud config set account <your-email-adress> -
Assurons-nous Ă©galement que le bon projet est dĂ©fini au cas oĂč votre email serait utilisĂ© pour plusieurs projets gcloud :
gcloud config set project hf-flax -
Ensuite, vous devez vous authentifier. Vous pouvez le faire en exécutant la commande
gcloud auth loginVous devriez obtenir un lien vers un site web oĂč vous pouvez authentifier votre compte gmail.
-
Enfin, vous pouvez Ă©tablir un tunnel SSH dans la VM TPU ! Veuillez exĂ©cuter la commande suivante en rĂ©glant la ââzoneâ sur
us-central2-bet sur le nom de la TPU qui vous a Ă©tĂ© envoyĂ© par email par lâĂ©quipe de Hugging Face.gcloud alpha compute tpus tpu-vm ssh <tpu-name> --zone <zone> --project hf-flax
Cela devrait établir un tunnel SSH dans la VM TPU !
Note : Vous nâĂȘtes PAS supposĂ© avoir accĂšs Ă la console Google Cloud. Aussi, il se peut que vous ne receviez pas de lien dâinvitation pour rejoindre le projet
hf-flax. Mais vous devriez tout de mĂȘme pouvoir accĂ©der Ă la VM TPU en suivant les Ă©tapes ci-dessus .
Note : Les VM TPU sont dĂ©jĂ attachĂ©es Ă des disques de stockage persistants (de 3 TB). Cela sera utile au cas oĂč votre Ă©quipe souhaiterait entraĂźner localement un jeu de donnĂ©es volumineux. Le nom du disque de stockage devrait Ă©galement figurer dans lâe-mail que vous avez reçu. Suivez cette section pour plus de dĂ©tails.
Installation de JAX
Commençons par créer un environnement virtuel Python :
python3 -m venv <your-venv-name>
Nous pouvons activer lâenvironnement en lançant :
source ~/<your-venv-name>/bin/activate
Installez ensuite Diffusers et les dĂ©pendances dâentraĂźnement de la bibliothĂšque :
pip install git+https://github.com/huggingface/diffusers.git
Ensuite, clonez ce dépÎt et installez JAX, Flax et les autres dépendances :
git clone https://github.com/huggingface/community-events
cd community-events/jax-controlnet-sprint/training_scripts
pip install -U -r requirements_flax.txt
Pour vérifier que JAX a été correctement installé, vous pouvez exécuter la commande suivante :
import jax
jax.device_count()
Cela devrait afficher le nombre de cĆurs de la TPU, qui devrait ĂȘtre de 4 sur une VM TPUv4-8. Si Python nâest pas capable de dĂ©tecter le pĂ©riphĂ©rique TPU, veuillez consulter la section des erreurs possibles plus bas pour des solutions.
Si vous souhaitez utiliser le logging Weights and Biases, vous devez également installer wandb maintenant :
pip install wandb
Note : Weights & Biases est gratuit pour les Ă©tudiants, les Ă©ducateurs et les chercheurs universitaires. Tous les participants Ă notre Ă©vĂ©nement sont qualifiĂ©s pour obtenir un compte dâĂ©quipe acadĂ©mique Weights & Biases. Pour crĂ©er votre Ă©quipe, vous pouvez visiter le site https://wandb.ai/create-team et choisir le type dâĂ©quipe âAcademicâ. Pour plus dâinformations sur la crĂ©ation et la gestion dâune Ă©quipe Weights & Biases, vous pouvez consulter le site https://docs.wandb.ai/guides/app/features/teams.
ExĂ©cution du script dâentraĂźnement
Maintenant, tĂ©lĂ©chargeons deux images de conditionnement que nous utiliserons pour lancer la validation pendant lâentraĂźnement afin de suivre nos progrĂšs
wget https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/controlnet_training/conditioning_image_1.png
wget https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/controlnet_training/conditioning_image_2.png
Nous vous encourageons Ă stocker ou Ă partager votre modĂšle avec la communautĂ©. Pour utiliser le Hub, veuillez vous connecter Ă votre compte Hugging Face, ou (en crĂ©er un si vous nâen avez pas dĂ©jĂ un) :
huggingface-cli login
Assurez-vous que les variables dâenvironnement MODEL_DIR, OUTPUT_DIR et HUB_MODEL_ID sont dĂ©finies. Les variables OUTPUT_DIR et HUB_MODEL_ID spĂ©cifient oĂč sauvegarder le modĂšle sur le Hub :
export MODEL_DIR="runwayml/stable-diffusion-v1-5"
export OUTPUT_DIR="runs/fill-circle-{timestamp}"
export HUB_MODEL_ID="controlnet-fill-circle"
Et enfin, dĂ©marrez lâentraĂźnement (assurez-vous dâĂȘtre dans le rĂ©pertoire jax-controlnet-sprint/training_scripts) !
python3 train_controlnet_flax.py \
--pretrained_model_name_or_path=$MODEL_DIR \
--output_dir=$OUTPUT_DIR \
--dataset_name=fusing/fill50k \
--resolution=512 \
--learning_rate=1e-5 \
--validation_image "./conditioning_image_1.png" "./conditioning_image_2.png" \
--validation_prompt "red circle with blue background" "cyan circle with brown floral background" \
--validation_steps=1000 \
--train_batch_size=2 \
--revision="non-ema" \
--from_pt \
--report_to="wandb" \
--tracker_project_name=$HUB_MODEL_ID \
--num_train_epochs=11 \
--push_to_hub \
--hub_model_id=$HUB_MODEL_ID
Notez que lâargument --from_pt convertira votre point de contrĂŽle pytorch en flax. Cependant, il ne fonctionnera quâavec les points de contrĂŽle au format diffusers. Si votre MODEL_DIR ne contient pas de points de contrĂŽle au format diffusers, vous ne pouvez pas utiliser lâargument --from_pt. Vous pouvez convertir vos points de contrĂŽle ckpt ou safetensors au format diffusers en utilisant ce script.
Puisque nous avons passĂ© lâargument --push_to_hub, il va automatiquement crĂ©er un repo de modĂšle sous votre compte Hugging Face basĂ© sur $HUB_MODEL_ID. Ă la fin de lâentraĂźnement, le point de contrĂŽle final sera automatiquement stockĂ© sur le Hub. Vous pouvez trouver un exemple de modĂšle ici.
Notre script dâentraĂźnement fournit Ă©galement un support limitĂ© pour le streaming de grands jeux de donnĂ©es Ă partir du Hub. Afin dâactiver le streaming, il faut Ă©galement dĂ©finir --max_train_samples. Voici un exemple de commande (tirĂ© de cet article de blog) :
export MODEL_DIR="runwayml/stable-diffusion-v1-5"
export OUTPUT_DIR="runs/uncanny-faces-{timestamp}"
export HUB_MODEL_ID="controlnet-uncanny-faces"
python3 train_controlnet_flax.py \
--pretrained_model_name_or_path=$MODEL_DIR \
--output_dir=$OUTPUT_DIR \
--dataset_name=multimodalart/facesyntheticsspigacaptioned \
--streaming \
--conditioning_image_column=spiga_seg \
--image_column=image \
--caption_column=image_caption \
--resolution=512 \
--max_train_samples 100000 \
--learning_rate=1e-5 \
--train_batch_size=1 \
--revision="flax" \
--report_to="wandb" \
--tracker_project_name=$HUB_MODEL_ID
Notez cependant que les performances des TPUs peuvent ĂȘtre limitĂ©es car le streaming avec datasets nâest pas optimisĂ© pour les images. Pour assurer un dĂ©bit maximal, nous vous encourageons Ă explorer les options suivantes :
Lorsque vous travaillez avec un jeu de donnĂ©es plus important, vous pouvez avoir besoin dâexĂ©cuter le processus dâentraĂźnement pendant une longue pĂ©riode et il est utile dâenregistrer des points de contrĂŽle rĂ©guliers au cours du processus. Vous pouvez utiliser lâargument suivant pour activer les points de contrĂŽle intermĂ©diaires :
--checkpointing_steps=500
Cela permet dâenregistrer le modĂšle entraĂźnĂ© dans des sous-dossiers du dossier output_dir. Le nom des sous-dossiers correspond au nombre dâĂ©tapes effectuĂ©es jusquâĂ prĂ©sent ; par exemple : un point de contrĂŽle sauvegardĂ© aprĂšs 500 Ă©tapes dâentraĂźnement serait sauvegardĂ© dans un sous-dossier nommĂ© 500
Vous pouvez alors commencer votre entraßnement à partir de ce point de contrÎle sauvegardé avec
--controlnet_model_name_or_path="./control_out/500"
Nous soutenons lâentraĂźnement avec la stratĂ©gie de pondĂ©ration Min-SNR proposĂ©e dans Efficient Diffusion Training via Min-SNR Weighting Strategy qui permet dâobtenir une convergence plus rapide en rééquilibrant la perte. Pour lâutiliser, il faut dĂ©finir lâargument --snr_gamma. La valeur recommandĂ©e est 5.0.
Nous supportons Ă©galement lâaccumulation de gradient, technique qui vous permet dâutiliser une taille de batch plus grande que celle que votre machine serait normalement capable de mettre en mĂ©moire. Vous pouvez utiliser lâargument gradient_accumulation_steps pour dĂ©finir les Ă©tapes dâaccumulation du gradient. Lâauteur de ControlNet recommande dâutiliser lâaccumulation de gradient pour obtenir une meilleure convergence. Pour en savoir plus voir ici.
Vous pouvez profiler votre code avec :
--profile_steps==5
Reportez-vous à la documentation JAX sur le profilage. Pour inspecter la trace de profil, vous devez installer et démarrer Tensorboard avec le plugin de profil :
pip install tensorflow tensorboard-plugin-profile
tensorboard --logdir runs/fill-circle-100steps-20230411_165612/
Le profil peut alors ĂȘtre inspectĂ© Ă lâadresse http://localhost:6006/#profile.
Parfois vous obtiendrez des conflits de version (messages dâerreur comme Duplicate plugins for name projector), ce qui signifie que vous devez dĂ©sinstaller et rĂ©installer toutes les versions de Tensorflow/Tensorboard (par exemple avec pip uninstall tensorflow tf-nightly tensorboard tb-nightly tensorboard-plugin-profile && pip install tf-nightly tbp-nightly tensorboard-plugin-profile).
Notez que la fonctionnalitĂ© de dĂ©bogage du plugin Tensorboard profile est toujours en cours de dĂ©veloppement. Toutes les vues ne sont pas entiĂšrement fonctionnelles, et par exemple le trace_viewer coupe les Ă©vĂ©nements aprĂšs 1M (ce qui peut rĂ©sulter en la perte de toutes vos traces de pĂ©riphĂ©riques si par exemple vous profilez lâĂ©tape de compilation par accident).
Dépannage de votre VM TPU
TRES IMPORTANT : Un seul processus peut accĂ©der aux cĆurs de la TPU Ă la fois. Cela signifie que si plusieurs membres de lâĂ©quipe essaient de se connecter aux cĆurs de la TPU, vous obtiendrez des erreurs telles que :
libtpu.so already in used by another process. Not attempting to load libtpu.so in this process.
Nous recommandons Ă chaque membre de lâĂ©quipe de crĂ©er son propre environnement virtuel, mais une seule personne devrait exĂ©cuter les processus dâentraĂźnement lourds. De plus, veuillez vous relayer lors de lâinstallation de la TPUv4-8 afin que tout le monde puisse vĂ©rifier que JAX est correctement installĂ©.
Si les membres de votre Ă©quipe nâutilisent pas actuellement la TPU mais que vous obtenez toujours ce message dâerreur. Vous devez tuer le processus qui utilise la TPU avec :
kill -9 PID
vous devrez remplacer le terme « PID » par le PID du processus qui utilise TPU. Dans la plupart des cas, cette information est incluse dans le message dâerreur. Par exemple, si vous obtenez
The TPU is already in use by a process with pid 1378725. Not attempting to load libtpu.so in this process.
vous pouvez faire
kill -9 1378725
Vous pouvez Ă©galement utiliser la commande suivante pour trouver les processus utilisant chacune des puces TPU (par exemple, /dev/accel0 est lâune des puces TPU)
sudo lsof -w /dev/accel0
Pour tuer tous les processus Ă lâaide de /dev/accel0, il faut
sudo lsof -t /dev/accel0 | xargs kill -9
Si Python nâest pas capable de dĂ©tecter votre pĂ©riphĂ©rique TPU (i.e. quand vous faites jax.device_count() et quâil sort 0), cela peut ĂȘtre dĂ» au fait que vous nâavez pas les droits dâaccĂšs aux logs tpu, ou que vous avez un fichier tpu lock qui traĂźne. ExĂ©cutez les commandes suivantes pour rĂ©soudre le problĂšme
sudo rm -f /tmp/libtpu_lockfile
sudo chmod o+w /tmp/tpu_logs/
Comment faire une soumission
Pour faire une soumission complĂšte, vous devez avoir les Ă©lĂ©ments suivants sur le Hub dâHugging Face :
- Un dépÎt de modÚle avec les poids du modÚle et la carte du modÚle,
- (Facultatif) Un dépÎt de jeu de données avec une carte de jeu de données,
- Un Space qui permet aux autres dâinteragir avec votre modĂšle.
Pousser les poids du modĂšle et la carte du modĂšle vers le Hub
Si vous utilisez le script dâentraĂźnement (train_controlnet_flax.py) fourni dans ce rĂ©pertoire
Lâactivation de lâargument push_to_hub dans les arguments dâentraĂźnement va :
- Créer un dépÎt de modÚles localement et à distance sur le Hub,
- CrĂ©er une carte de modĂšle et lâĂ©crire dans le dĂ©pĂŽt de modĂšles local,
- Sauvegarder votre modÚle dans le référentiel de modÚles local,
- Pousser le dépÎt local vers le Hub.
Votre carte de modÚle générée automatiquement ressemblera à ceci :
.
Vous pouvez modifier la carte de modĂšle pour quâelle soit plus informative. Les cartes de modĂšle qui sont plus informatives que les autres auront plus de poids lors de lâĂ©valuation.
Si vous avez entraĂźnĂ© un modĂšle personnalisĂ© et que vous nâavez pas utilisĂ© le script
Vous devez vous authentifier avec huggingface-cli login comme indiqué ci-dessus. Si vous utilisez une des classes de modÚles disponibles dans diffusers, sauvegardez votre modÚle avec la méthode save_pretrained de votre modÚle.
model.save_pretrained("path_to_your_model_repository")
AprÚs avoir sauvegardé votre modÚle dans un dossier, vous pouvez simplement utiliser le script ci-dessous pour pousser votre modÚle vers le Hub :
from huggingface_hub import create_repo, upload_folder
create_repo("username/my-awesome-model")
upload_folder(
folder_path="path_to_your_model_repository",
repo_id="username/my-awesome-model"
)
Ceci poussera votre modĂšle vers Hub. AprĂšs avoir poussĂ© cela, vous devez crĂ©er la carte de modĂšle vous-mĂȘme.
Vous pouvez utiliser lâinterface graphique pour lâĂ©diter.

Chaque carte de modĂšle se compose de deux sections, les mĂ©tadonnĂ©es et le texte libre. Vous pouvez Ă©diter les mĂ©tadonnĂ©es Ă partir des sections dans lâinterface graphique. Si vous avez sauvegardĂ© votre modĂšle en utilisant save_pretrained, vous nâavez pas besoin de fournir pipeline_tag et library_name. Sinon, fournissez pipeline_tag, library_name et le jeu de donnĂ©es sâil existe sur Hugging Face Hub. En plus de cela, vous devez ajouter jax-diffusers-event Ă la section tags.
---
license: apache-2.0
library_name: diffusers
tags:
- jax-diffusers-event
datasets:
- red_caps
pipeline_tag: text-to-image
---

Créer notre Space
Rédiger notre application
Nous utiliserons Gradio pour crĂ©er nos applications. Gradio possĂšde deux API principales : Interface et Blocks. Interface est une API de haut niveau qui vous permet de crĂ©er une interface avec quelques lignes de code, et Blocks est une API de plus bas niveau qui vous donne plus de flexibilitĂ© sur les interfaces que vous pouvez construire. Le code doit ĂȘtre inclus dans un fichier appelĂ© app.py.
Essayons de crĂ©er une application ControlNet comme exemple. LâAPI Interface fonctionne simplement comme suit :
import gradio as gr
# La fonction d'inférence prend en compte le prompt, le prompt négatif et l'image
def infer(prompt, negative_prompt, image):
# implémentez votre fonction d'inférence ici
return output_image
# vous devez passer les entrées et les sorties en fonction de la fonction d'inférence
gr.Interface(fn = infer, inputs = ["text", "text", "image"], outputs = "image").launch()
Vous pouvez personnaliser votre interface en passant title, description et examples Ă la fonction Interface.
title = "ControlNet on Canny Filter"
description = "This is a demo on ControlNet based on canny filter."
# vous devez passer vos exemples en fonction de vos entrées
# chaque liste intérieure est un exemple, chaque élément de la liste correspondant à un composant des `inputs`.
examples = [["a cat with cake texture", "low quality", "cat_image.png"]]
gr.Interface(fn = infer, inputs = ["text", "text", "image"], outputs = "image",
title = title, description = description, examples = examples, theme='gradio/soft').launch()
Votre interface ressemblera Ă ceci :

Avec les blocs, vous pouvez ajouter des marques, des onglets, des composants sous les colonnes et les lignes, etc. Supposons que nous ayons deux ControlNets et que nous voulions les inclure dans un Space. Nous les placerons sous différents onglets dans une démo comme ci-dessous :
import gradio as gr
def infer_segmentation(prompt, negative_prompt, image):
# votre fonction d'inférence pour le contrÎle de la segmentation
return im
def infer_canny(prompt, negative_prompt, image):
# votre fonction d'inférence pour un contrÎle efficace
return im
with gr.Blocks(theme='gradio/soft') as demo:
gr.Markdown("## Stable Diffusion with Different Controls")
gr.Markdown("In this app, you can find different ControlNets with different filters. ")
with gr.Tab("ControlNet on Canny Filter "):
prompt_input_canny = gr.Textbox(label="Prompt")
negative_prompt_canny = gr.Textbox(label="Negative Prompt")
canny_input = gr.Image(label="Input Image")
canny_output = gr.Image(label="Output Image")
submit_btn = gr.Button(value = "Submit")
canny_inputs = [prompt_input_canny, negative_prompt_canny, canny_input]
submit_btn.click(fn=infer_canny, inputs=canny_inputs, outputs=[canny_output])
with gr.Tab("ControlNet with Semantic Segmentation"):
prompt_input_seg = gr.Textbox(label="Prompt")
negative_prompt_seg = gr.Textbox(label="Negative Prompt")
seg_input = gr.Image(label="Image")
seg_output = gr.Image(label="Output Image")
submit_btn = gr.Button(value = "Submit")
seg_inputs = [prompt_input_seg, negative_prompt_seg, seg_input]
submit_btn.click(fn=infer_segmentation, inputs=seg_inputs, outputs=[seg_output])
demo.launch()
La démo ci-dessus ressemblera à ce qui suit :

Créer notre Space
Une fois notre application Ă©crite, nous pouvons crĂ©er un espace Hugging Face pour hĂ©berger notre application. Vous pouvez aller sur huggingface.co, cliquer sur votre profil en haut Ă droite et sĂ©lectionner âNew Spaceâ.

Nous pouvons nommer notre Space, choisir une licence et sélectionner « Gradio » comme Space SDK.

AprĂšs avoir créé le Space, vous pouvez soit utiliser les instructions ci-dessous pour cloner le dĂ©pĂŽt localement, ajouter vos fichiers et pousser, soit utiliser lâinterface graphique pour crĂ©er les fichiers et Ă©crire le code dans le navigateur.

Pour télécharger votre fichier de candidature, cliquez sur « Add File » et faites glisser votre fichier.

Enfin, nous devons crĂ©er un fichier appelĂ© requirements.txt et ajouter les conditions requises pour notre projet. Assurez-vous dâinstaller les versions de jax, diffusers et autres dĂ©pendances comme ci-dessous.
-f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html
jax[cuda11_cudnn805]
jaxlib
git+https://github.com/huggingface/diffusers@main
opencv-python
transformers
flax
Nous vous accorderons une dotation GPU afin que votre application puisse fonctionner sur GPU.
Nous avons un classement hébergé ici et nous distribuerons des prix à partir de ce classement. Pour que votre Space apparaisse sur le leaderboard, éditez simplement README.md de votre Space pour avoir le tag jax-diffusers-event sous les tags comme ci-dessous :
---
title: Canny Coyo1m
emoji: đ
...py
tags:
- jax-diffusers-event
---
Prix
Pour ce sprint, nous aurons de nombreux prix. Nous choisirons les dix premiers projets de ce classement, vous devez donc tagger votre Space pour le classement afin que votre soumission soit complĂšte, comme indiquĂ© dans la section ci-dessus. Les projets sont classĂ©s en fonction du nombre de jâaimes, nous augmenterons donc partagerons vos Spaces pour en augmenter la visibilitĂ© pour que les gens puissent voter en laissant un jâaime sur votre Space. Nous sĂ©lectionnerons les dix premiers projets du classement et le jury votera pour dĂ©terminer les trois premiĂšres places. Ces projets seront mis en valeur par Google et Hugging Face. Les interfaces Ă©laborĂ©es ainsi que les projets dont les bases de code et les modĂšles sont en libre accĂšs augmenteront probablement les chances de gagner des prix.
Les prix sont les suivants et sont remis Ă chaque membre de lâĂ©quipe :
PremiĂšre place : Un bon dâachat de 150 $ Ă dĂ©penser sur le Hugging Face Store, un abonnement dâun an Ă Hugging Face Hub PRO, le livre Natural Language Processing with Transformers.
DeuxiĂšme place : Un bon dâachat de 125$ Ă dĂ©penser sur le Hugging Face Store, un abonnement dâun an Ă Hugging Face Hub PRO.
TroisiĂšme place : Un bon dâachat de 100 $ Ă dĂ©penser sur le Hugging Face Store, un abonnement dâun an Ă Hugging Face Hub PRO.
Les dix premiers projets du classement (indĂ©pendamment de la dĂ©cision du jury) gagneront un kit de merch exclusivement conçu pour ce sprint par Hugging Face, ainsi quâun kit de merch sĂ©parĂ© JAX de Google.
Jury
Le jury de ce sprint était composé des personnes suivantes :
- Robin Rombach, Stability AI
- Huiwen Chang, Google Research
- Jun-Yan Zhu, Carnegie Mellon University
- Merve Noyan, Hugging Face
FAQ
Dans cette section, nous rassemblons les réponses aux questions fréquemment posées sur notre canal discord.
Comment utiliser VSCode avec TPU VM ?
Vous pouvez suivre ce guide gĂ©nĂ©ral sur la façon dâutiliser VSCode remote pour se connecter Ă Google Cloud VMs. Une fois que câest configurĂ©, vous pouvez dĂ©velopper sur la VM TPU en utilisant VSCode.
Pour obtenir votre IP externe, utilisez cette commande :
gcloud compute tpus tpu-vm describe <node_name> --zone=<zone>
Elle devrait ĂȘtre listĂ©e sous âaccessConfigâ -> âexternalIpâ
Comment tester votre code localement ?
Puisque les membres de lâĂ©quipe partagent la VM TPU, il peut ĂȘtre pratique dâĂ©crire et de tester votre code localement sur une unitĂ© centrale pendant que vos coĂ©quipiers exĂ©cutent le processus dâentraĂźnement sur la VM. Pour effectuer des tests locaux, il est important de mettre le drapeau xla_force_host_platform_device_count Ă 4. Pour en savoir plus, consultez la documentation.
Gagnants du sprint
Les 10 meilleurs projets (basĂ©s sur le nombre de likes sur leurs dĂ©mos) sont disponibles sur ce classement. Nous avons soumis ce classement Ă notre jury pour quâil juge les 10 meilleurs projets sur la base de plusieurs facteurs tels que les points de contrĂŽle du modĂšle, les jeux de donnĂ©es et les bases de code open-source, lâexhaustivitĂ© du modĂšle et des cartes de jeux de donnĂ©es, etc. En consĂ©quence, les trois projets suivants sont sortis vainqueurs :