Aller au contenu principal

Module 7 — Conditionnement par le texte

Le module précédent a construit un modèle de diffusion inconditionnel : on part de bruit, on obtient un visage. Pour l'utiliser en production, il faut pouvoir demander « un chat astronaute sur la Lune, style peinture à l'huile » et recevoir précisément cela. Ce module montre les trois pièces qui rendent ce contrôle possible : un encodeur de texte, l'attention croisée qui l'injecte dans le U-Net, et le guidage sans classificateur qui règle la force de l'obéissance à la consigne.

Aligner texte et image avec CLIP

CLIP (Radford, 2021) apprend deux encodeurs, un pour le texte et un pour l'image, entraînés à ce que le plongement d'une image corresponde au plongement de la légende qui la décrit. La perte est contrastive : sur un lot de NN paires (image, légende), chaque plongement d'image doit être plus proche de sa légende que des N1N - 1 autres, et symétriquement pour chaque légende.

LCLIP=12 ⁣[ilogesii/τjesij/τ+ilogesii/τjesji/τ]\mathcal{L}_{\text{CLIP}} = -\frac{1}{2}\!\left[\sum_i \log \frac{e^{s_{ii}/\tau}}{\sum_j e^{s_{ij}/\tau}} + \sum_i \log \frac{e^{s_{ii}/\tau}}{\sum_j e^{s_{ji}/\tau}}\right]

sijs_{ij} est le produit scalaire entre les plongements de l'image ii et du texte jj, et τ\tau une température apprise. Entraîné sur 400 millions de paires collectées sur le web, CLIP fournit deux services précieux : un plongement de texte riche qui capture la sémantique visuelle, et un score de similarité entre une image et un texte qu'on utilise partout ensuite.

Ce plongement est ce que la génération conditionnée par texte va injecter dans le modèle de diffusion. Il n'est pas nécessaire de réentraîner CLIP à chaque nouveau projet : la version publique gelée suffit dans la majorité des cas.

Injecter le texte : l'attention croisée

Une fois qu'on a une séquence de plongements de mots cRL×dc \in \mathbb{R}^{L \times d} (typiquement 77 jetons pour Stable Diffusion), il faut la faire influencer chaque étape de débruitage. La méthode standard est l'attention croisée intercalée dans les blocs du U-Net.

À chaque bloc, on prend les activations spatiales hRN×dh \in \mathbb{R}^{N \times d} (où NN est le nombre de positions spatiales) comme requêtes, et on prend le texte cc comme clés et valeurs. On calcule alors :

Attention(Q,K,V)=softmax ⁣(QKdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{Q K^\top}{\sqrt{d_k}}\right) V

avec Q=hWQQ = h W_Q, K=cWKK = c W_K, V=cWVV = c W_V. Chaque position spatiale « regarde » les jetons du texte et récupère l'information dont elle a besoin pour sa région de l'image. Les cartes d'attention obtenues sont interprétables : on peut visualiser à quel endroit de l'image chaque mot de la consigne a été « posé ».

import torch
from torch import nn

class AttentionCroisee(nn.Module):
def __init__(self, dim, dim_texte, nb_tetes=8):
super().__init__()
self.nb_tetes = nb_tetes
self.dh = dim // nb_tetes
self.q = nn.Linear(dim, dim, bias=False)
self.k = nn.Linear(dim_texte, dim, bias=False)
self.v = nn.Linear(dim_texte, dim, bias=False)
self.sortie = nn.Linear(dim, dim)

def forward(self, h, c):
# h : (B, N, dim), c : (B, L, dim_texte)
B, N, _ = h.shape
Q = self.q(h).view(B, N, self.nb_tetes, self.dh).transpose(1, 2)
K = self.k(c).view(B, -1, self.nb_tetes, self.dh).transpose(1, 2)
V = self.v(c).view(B, -1, self.nb_tetes, self.dh).transpose(1, 2)
poids = torch.softmax(Q @ K.transpose(-2, -1) / self.dh ** 0.5, dim=-1)
out = (poids @ V).transpose(1, 2).contiguous().view(B, N, -1)
return self.sortie(out)

Le U-Net conditionné devient donc epsilon(x_t, t, c) : trois entrées, une seule sortie (le bruit à retirer).

Le guidage sans classificateur : le levier qui compte

Un modèle simplement conditionné a un défaut connu : il suit la consigne, mais avec parcimonie. Sur des consignes précises, la sortie reste générique. Le guidage sans classificateur (CFG, Ho et Salimans, 2022) est l'astuce presque universelle qui règle ce problème.

L'idée : entraîner le même modèle à la fois avec et sans consigne, en remplaçant cc par un jeton vide \emptyset dans 10 à 20 % des lots d'entraînement. On dispose alors, sans coût supplémentaire, de deux modèles pour le prix d'un : εθ(xt,t,c)\varepsilon_\theta(x_t, t, c) (conditionné) et εθ(xt,t,)\varepsilon_\theta(x_t, t, \emptyset) (inconditionnel).

À l'échantillonnage, on combine linéairement les deux prédictions :

ε~(xt,t,c)=εθ(xt,t,)+w[εθ(xt,t,c)εθ(xt,t,)]\tilde\varepsilon(x_t, t, c) = \varepsilon_\theta(x_t, t, \emptyset) + w \cdot \left[\varepsilon_\theta(x_t, t, c) - \varepsilon_\theta(x_t, t, \emptyset)\right]

Le facteur w1w \geq 1 est l'échelle de guidage. À w=1w = 1, on retrouve l'échantillonnage conditionné standard. Au-delà, on exagère la direction que la consigne fait prendre au débruitage. Deux passes avant du réseau sont nécessaires à chaque pas d'échantillonnage, ce qui double le coût — un prix modeste pour le gain de contrôle.

L'échelle de guidage doit rester raisonnable

En pratique, ww se règle entre 3 et 15. En dessous de 2, la consigne se fait à peine sentir. Au-dessus de 15, les images deviennent sursaturées, avec des couleurs trop vives, des contours durs et des artefacts caractéristiques. La valeur par défaut de 7.5 dans Stable Diffusion est un compromis entre fidélité à la consigne et qualité perceptuelle, réglé empiriquement.

Stable Diffusion : l'assemblage final

Stable Diffusion (Rombach, 2022) combine les trois modules précédents :

  1. Un autoencodeur variationnel compresse l'image en une carte latente 64×64×464 \times 64 \times 4 à partir d'une image 512×512×3512 \times 512 \times 3 (module 6, diffusion latente).
  2. Un encodeur CLIP produit une séquence de 77 plongements de dimension 768 (ou 1024 pour SD 2.x).
  3. Un U-Net débruite dans l'espace latent, avec de l'attention croisée sur le plongement de texte à chaque bloc.
  4. Le décodeur du VAE reconstruit l'image pixel finale.

Le tout est entraîné par la MSE sur le bruit prédit du module 6, avec le drop de conditionnement pour le guidage sans classificateur.

L'utilisation en pratique tient en une dizaine de lignes avec la bibliothèque diffusers :

from diffusers import StableDiffusionPipeline
import torch

pipe = StableDiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-2-1",
torch_dtype=torch.float16,
).to("cuda")

image = pipe(
prompt="un chat astronaute sur la Lune, peinture a l'huile",
negative_prompt="flou, artefacts, texte",
num_inference_steps=30,
guidance_scale=7.5,
).images[0]

Le paramètre negative_prompt est un usage direct du CFG : il devient la consigne « inconditionnelle » vers laquelle on s'éloigne au lieu de partir de \emptyset. C'est le mécanisme qui permet aux artistes de dire « pas de flou, pas de mains à six doigts ». Le cours 27 « Génération d'images avancée » revient en détail sur ce pipeline, ses variantes ControlNet et l'entraînement fin par LoRA.

En résumé

  • CLIP aligne les plongements de texte et d'image par apprentissage contrastif ; ses plongements textuels servent de conditionnement pour la génération.
  • L'attention croisée intercalée dans le U-Net utilise les activations spatiales comme requêtes et le plongement de texte comme clés et valeurs, ce qui permet à chaque position spatiale de récupérer l'information textuelle utile.
  • Le guidage sans classificateur entraîne le modèle avec et sans conditionnement (drop 10 %) puis exagère la direction de la consigne à l'échantillonnage, avec une échelle ww typiquement entre 3 et 15.
  • Stable Diffusion est l'assemblage de la diffusion latente, de CLIP et du CFG ; le negative_prompt est un usage direct du CFG qui remplace le conditionnement inconditionnel par une consigne à repousser.

Module suivant : comment mesurer objectivement la qualité d'une génération quand aucun humain ne peut regarder 10 000 images.