Cómo implementar Stable Diffusion

Tras ver cómo funciona Stable Diffusion teóricamente ahora toca implementarlo en Python.

Introducción

En un artículo anterior, vimos como funciona Stable Diffusion entrando en detalle pero sin utilizar ni una sola línea de código. Vimos como se entrena un modelo (difusión hacia delante) para después utilizarlo en el proceso de inferencia y generar espectaculares imágenes con Inteligencia Artificial (difusión inversa). Si aún no has leído ese artículo, te recomiendo que lo hagas antes de continuar con este.

En este artículo vamos a implementar cada parte del proceso de inferencia mediante código en Python, para así comprender de manera más técnica su funcionamiento. Es probable que no entiendas a la perfección todas las líneas de código de este artículo. No te preocupes, no tiene importancia. La idea es ver de forma general cómo funciona, cuáles son sus componentes y cómo interactúan entre ellos.

Vamos a utilizar la misma versión que en el artículo anterior (Stable Diffusion 1.5) y un condicionamiento de tipo texto-a-imagen (txt2img).

Empecemos recordando los pasos que sigue el proceso de inferencia utilizando la imagen final del anterior artículo:

Espacio latentePredictor de ruido (U-Net)CondicionamientosVAE decoderperro disfrazado de batmanCLIP tokenizer25 56 97 12CLIP transformerMapa de profundidadSeedCLIP embeddingsTexto-34 1 045 66 295 -8 33 86 4-43 6 9-2 -4 64 19 -845 62 177 -2 448 99 18 -81 455 15 7Etiquetas de claseOtrosImagen[68 5 99...][44 6 -8...][63 95 6...]tensor con ruidotensor sin ruidoVAE encoder[15, 23, 1...][-94 6 7...][85 58 1...][48, 91, 0...][-8 -5 12...][59 6 -8...][15, 23, 1...][-94 6 7...][85 58 1...][48, 91, 0...][-8 -5 12...][59 6 -8...]

Estructura del modelo

Antes de comenzar viene bien familiarizarse con su estructura. Recordemos que Stable Diffusion contiene varios componentes en su interior:

  • Tokenizer: convierte texto en tokens.
  • Transformer: transforma los embeddings mediante mecanismos de atención.
  • Variational autoencoder (VAE): convierte imágenes en tensores dentro del espacio latente y viceversa.
  • U-Net: predice el ruido de un tensor.
  • Scheduler: guía al predictor de ruido y muestrea (sampling) imágenes con menos ruido en cada paso.
  • Otros modelos, como el filtro NSFW.

Estos modelos se distribuyen en varios formatos, siendo los más comunes ckpt y safetensors (ya que agregan todos los componentes en único archivo). Gracias a Hugging Face los modelos también están disponibles en formato diffusers, para utilizarlos fácilmente con su librería que lleva el mismo nombre. Este formato consiste en varias carpetas con todas los componentes por separado, perfecto para entender su composición.

Si navegamos por el repositorio de Stable Diffusion 1.5 encontraremos la siguiente estructura:

  1. feature_extractor
    1. preprocessor_config.json
  2. safety_checker
    1. config.json
    2. model.fp16.safetensors
    3. model.safetensors
    4. pytorch_model.bin
    5. pytorch_model.fp16.bin
  3. scheduler
    1. scheduler_config.json
  4. text_encoder
    1. config.json
    2. model.fp16.safetensors
    3. model.safetensors
    4. pytorch_model.bin
    5. pytorch_model.fp16.bin
  5. tokenizer
    1. merges.txt
    2. special_tokens_map.json
    3. tokenizer_config.json
    4. vocab.json
  6. unet
    1. config.json
    2. diffusion_pytorch_model.bin
    3. diffusion_pytorch_model.fp16.bin
    4. diffusion_pytorch_model.fp16.safetensors
    5. diffusion_pytorch_model.non_ema.bin
    6. diffusion_pytorch_model.non_ema.safetensors
    7. diffusion_pytorch_model.safetensors
  7. vae
    1. config.json
    2. diffusion_pytorch_model.bin
    3. diffusion_pytorch_model.fp16.bin
    4. diffusion_pytorch_model.fp16.safetensors
    5. diffusion_pytorch_model.safetensors
  8. .gitattributes
  9. README.md
  10. model_index.json
  11. v1-5-pruned-emaonly.ckpt
  12. v1-5-pruned-emaonly.safetensors
  13. v1-5-pruned.ckpt
  14. v1-5-pruned.safetensors
  15. v1-inference.yaml

Puedes ver qué scheduler, tokenizer, transformer, U-Net o VAE utiliza Stable Diffusion 1.5 simplemente explorando los archivos .json que encontrarás dentro de estas carpetas.

Aquí encontrarás los modelos individuales en formato .bin o .safetensors. Ambos con variante fp16 que, a diferencia de fp32, utiliza la mitad de espacio en disco y memoria gracias a una bajada en la precisión de los números decimales, sin que apenas afecte al resultado final.

Instalación de librerías

Antes de nada, asegúrate de tener Python 3.10.

Además, si tienes una gráfica NVIDIA y vas a utilizar CUDA para acelerar el proceso (en este artículo lo utilizaré), necesitarás instalar CUDA Toolkit. Puedes seguir estos pasos para su instalación.

Ahora sí, creamos un entorno virtual e instalamos las librerías necesarias:

Crear el entorno virtual
python -m venv .venv
Activar el entorno virtual
# Unix
source .venv/bin/activate

# Windows
.venv\Scripts\activate
Instalar librerías necesarias
# Si no vas a utilizar CUDA, elimina el parámetro --index-url
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
pip install transformers accelerate diffusers tqdm pillow

Proceso de inferencia

Ya podemos crear un archivo (por ejemplo inference.py), donde escribiremos el código de nuestra aplicación.

Si quieres copiar y pegar el código entero, recuerda que lo tienes disponible en articles/how-to-implement-stable-diffusion/inference.py.

En el repositorio del blog en GitHub encontrarás todo el contenido asociado con este y otros artículos.

Importar lo necesario

Lo primero es importar las librerías y métodos que vamos a utilizar:

  • Python
import torch
from torchvision.transforms import ToPILImage
from transformers import CLIPTextModel, CLIPTokenizer
from diffusers import AutoencoderKL, UNet2DConditionModel, EulerDiscreteScheduler
from tqdm.auto import tqdm
from PIL import Image

Más adelante entenderás para qué es cada cosa.

Instanciar modelos

Vamos a instanciar los modelos necesarios para así tenerlos disponibles en toda la aplicación.

Necesitamos el tokenizador, el transformer (text encoder), el predictor de ruido (U-Net), el scheduler y el variational autoencoder (VAE). Todos los modelos los obtenemos del repositorio en Hugging Face de Stable Diffusion 1.5.

Cada modelo se extrae de una carpeta específica (subfolder) y hacemos uso del formato .safetensors cuando esté disponible. Además, los modelos parametrizados los movemos a la tarjeta gráfica mediante to('cuda'), para acelerar los cálculos.

  • Python
tokenizer = CLIPTokenizer.from_pretrained(
  'runwayml/stable-diffusion-v1-5',
  subfolder='tokenizer',
)

text_encoder = CLIPTextModel.from_pretrained(
  'runwayml/stable-diffusion-v1-5',
  subfolder='text_encoder',
  use_safetensors=True,
).to('cuda')

scheduler = EulerDiscreteScheduler.from_pretrained(
  'runwayml/stable-diffusion-v1-5',
  subfolder='scheduler',
)

unet = UNet2DConditionModel.from_pretrained(
  'runwayml/stable-diffusion-v1-5',
  subfolder='unet',
  use_safetensors=True,
).to('cuda')

vae = AutoencoderKL.from_pretrained(
  'runwayml/stable-diffusion-v1-5',
  subfolder='vae',
  use_safetensors=True,
).to('cuda')

Stable Diffusion 1.5 utiliza el scheduler PLMS (también llamado PNDM), pero nosotros vamos a utilizar Euler para mostrar lo fácil que es sustituirlo (ya lo hemos hecho).

Inicializar parámetros

A continuación, definimos los parámetros que necesitamos para la generación de imágenes.

Vamos a definir nuestro prompt. Utilizamos una lista por si quisiéramos generar varias imágenes al mismo tiempo utilizando diferentes prompts (['prompt1', 'prompt2', '...']) o varias imágenes del mismo prompt utilizando una semilla aleatoria (['prompt'] * 4). De momento no nos vamos a complicar, utilizamos un único prompt:

  • Python
prompts = ['silly dog wearing a batman costume, funny, realistic, canon, award winning photography']

Para hacer el código más legible, almacenamos cuántas imágenes vamos a generar al mismo tiempo:

  • Python
batch_size = 1

Para el proceso de muestreo, especificamos que vamos a utilizar 30 pasos (steps o sampling steps). Es decir, se eliminará ruido de la imagen 30 veces.

  • Python
inference_steps = 30

Para obtener un resultado reproducible especificamos una semilla en vez de ser aleatoria. Este número se utilizará más adelante para generar un tensor con ruido desde el que iremos limpiando la imagen hasta obtener el resultado. Si partimos del mismo ruido, siempre obtendremos el mismo resultado.

  • Python
seed = 1055747

Y por último, guardamos también en unas variables el valor de CFG, así como el tamaño de la imagen que queremos generar:

  • Python
cfg_scale = 7
height = 512
width = 512

Condicionamiento

Empecemos generando el tensor que contiene la información para guiar al predictor de ruido hacia la imagen que esperamos obtener.

Tokenizer

Como los ordenadores no entienden de letras, la primera tarea es utilizar un tokenizador (tokenizer) para convertir cada palabra en un número llamado símbolo (token).

perro disfrazado de batmanCLIP tokenizer25 56 97 12Texto

Vamos a convertir un prompt de prueba en tokens:

  • Python
print(tokenizer('dog in batman costume'))
{'input_ids': [49406, 1929, 530, 7223, 7235, 49407], 'attention_mask': [1, 1, 1, 1, 1, 1]}

Nos ha devuelto un diccionario donde input_ids es una lista con los siguientes tokens: 49406, 1929, 530, 7223, 7235, 49407.

Si abres el archivo vocab.json que encontrarás en la carpeta tokenizer, hallarás un diccionario que asigna tokens a todos los términos posibles (recuerda, no tienen por qué ser siempre palabras).

Así pues, podemos observar como se ha tokenizado este prompt:

vocab.json
"<|startoftext|>": 49406,
"dog</w>": 1929,
"in</w>": 530,
"batman</w>": 7223,
"costume</w>": 7235,
"<|endoftext|>": 49407,

Fácil, ¿verdad?

[...] los tokens se guardan en un vector que tiene un tamaño de 77 tokens (1x77).

Como vimos en el anterior artículo, el vector tiene que tener un tamaño de 77 tokens. Si se supera este límite se pueden eliminar o solventar con técnicas de concatenación y retroalimentación para utilizar todos los tokens. En este ejemplo solo tenemos 6 y en nuestro prompt real tampoco tenemos 77. ¿De dónde sacamos el resto? Demos la bienvenida al padding y truncation.

Padding es la técnica que inserta un token especial para rellenar los elementos que faltan. Truncation, por otro lado, es una técnica que elimina tokens cuando se sobrepasa la cantidad deseada.

Vamos pues a tokenizar nuestro prompt de la siguiente manera:

  • Python
cond_input = tokenizer(
  prompts,
  max_length=tokenizer.model_max_length,   # Tamaño que necesitamos (77)
  padding='max_length',                    # Aplicar padding si fuera necesario
  truncation=True,                         # Aplicar truncation si fuera necesario
  return_tensors='pt',
)

print(cond_input)
{'input_ids': tensor([[49406,  1929,   530,  7223,  7235, 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, 49407]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0]])}

Ahora sí, podemos ver como input_ids contiene 77 elementos. El token 49407 se ha repetido todas las veces que ha sido necesario. Además, ahora input_ids es un tensor gracias al argumento return_tensors='pt'.

Embedding

Cabe destacar que cada token contendrá 768 dimensiones. Es decir, si utilizamos la palabra coche en nuestro prompt, ese token se convertirá en un vector de 768 dimensiones. Una vez se realiza esto con todos los tokens tendremos un embedding de tamaño 1x77x768.

25 56 97 12Mapa de profundidadCLIP embeddings-34 1 045 66 295 -8 33 86 4-43 6 9-2 -4 64 19 -845 62 177 -2 448 99 18 -81 455 15 7Etiquetas de claseOtros

Tarea sencilla para nosotros. Llamamos a la función text_encoder(), pasándole como argumento la propiedad input_ids del tensor y quedándonos con el primer elemento que devuelve.

  • Python
with torch.no_grad():
  cond_embeddings = text_encoder(cond_input.input_ids.to('cuda'))[0]

print(cond_embeddings)
tensor([[[-0.3884,  0.0229, -0.0522,  ..., -0.4899, -0.3066,  0.0675],
         [-1.8022,  0.5477,  1.0725,  ..., -1.5483, -0.5022, -0.2065],
         [-0.3402,  1.4715, -0.7753,  ..., -1.0974, -0.6557,  0.0747],
         ...,
         [-0.9392,  0.1777,  0.2575,  ...,  0.9130, -0.3660, -1.0388],
         [-0.9334,  0.1780,  0.2590,  ...,  0.9113, -0.3683, -1.0343],
         [-0.8973,  0.1848,  0.2609,  ...,  0.9189, -0.3297, -1.0798]]],
       device='cuda:0')

Como estamos utilizando CUDA tenemos que mandar el tensor guardado en cond_input.input_ids a la tarjeta gráfica mediante to('cuda').

La línea with torch.no_grad() desactiva el cálculo automático del gradiente. Sin entrar en detalle, es algo que no necesitamos para el proceso de inferencia y evitamos utilizar memoria innecesariamente. Entraremos en este contexto cada vez que hagamos uso de un modelo parametrizado.

Ya tenemos nuestro embedding listo.

Transformer

Este es el último paso del condicionamiento. En esta pieza se procesan los embeddings mediante un modelo transformer de CLIP.

CLIP transformer-34 1 045 66 295 -8 33 86 4-43 6 9-2 -4 64 19 -845 62 177 -2 448 99 18 -81 455 15 7[68 5 99...][44 6 -8...][63 95 6...]

No te sientas engañado, pero el text_encoder() de CLIP ya se encarga de aplicar los mecanismos de atención a la hora de crear el embedding, así que no hay que hacer nada más.

Con esto terminamos el condicionamiento.

Incondicionamiento

A Stable Diffusion tambien hay que proveerle de un embedding no condicionado. Se hace de la misma manera pero el prompt es una cadena vacía tantas veces como imágenes estemos generando a la vez.

  • Python
uncond_input = tokenizer(
  [''] * batch_size,
  max_length=tokenizer.model_max_length,
  padding='max_length',
  truncation=True,
  return_tensors='pt',
)

with torch.no_grad():
  uncond_embeddings = text_encoder(uncond_input.input_ids.to('cuda'))[0]

Unimos estos embeddings, tanto condicionados como no condicionados, en un único tensor:

  • Python
text_embeddings = torch.cat([uncond_embeddings, cond_embeddings])

Prompt negativo

¿Quieres añadir un prompt negativo? ¡Se trata del embedding no condicionado!

Cuando utilizamos un prompt positivo estamos guiando al predictor de ruido en esa dirección. Si le decimos que queremos un ramo de rosas (bouquet of roses), el predictor de ruido irá en esa dirección.

El prompt no condicionado aleja al predictor de ruido de esos tokens. Si no lo utilizasemos, la calidad se vería gravemente afectada ya que no sabría de donde alejarse.

Al utilizar un prompt no condicionado vacío le estamos dando ruido extra. Es como decirle que se aleje del ruido. ¿Y qué es lo contrario? Una imagen de calidad.

Si en vez de ruido utilizamos el embedding no condicionado para añadir palabras (prompt negativo), seremos aún más específicos a la hora de alejar al predictor de ruido. Si utilizamos el prompt negativo red, pink, lo que le estamos diciendo es que se aleje del color rojo y rosa, así que lo más probable es que nos genere una imagen con un ramo de rosas blancas, azules o amarillas.

En este artículo no vamos a utilizar prompt negativo pero te recomiendo que siempre añadas uno para obtener mucha mayor calidad en el resultado. Si utilizas un prompt como bad quality, deformed, oversaturated, le estarás alejando de todo eso y el modelo buscará lo contrario.

Generar un tensor con ruido

Al principio del proceso, en vez de generar una imagen llena de ruido se genera ruido latente y se guarda en un tensor.

Para generar ruido instanciamos un generador mediante torch.Generator y le asignamos la semilla desde la que comenzaremos:

  • Python
generator = torch.Generator(device='cuda')
generator.manual_seed(seed)

Después, utilizamos la función torch.randn para obtener un tensor con ruido. El parámetro que recibe es una secuencia de enteros que define la forma del tensor:

  • Python
latents = torch.randn(
  (batch_size, unet.config.in_channels, height // 8, width // 8),
  generator=generator,
  device='cuda',
)

Le estamos pasando como valor (1, 4, 64, 64). Estos números provienen de:

  • 1: El valor de batch_size. Es decir, cuántas imágenes generamos a la vez.
  • 4: La cantidad de canales de entrada que tiene la red neuronal del predictor de ruido (U-Net).
  • 64/64: El tamaño de la imagen en el espacio latente (height / width). Nuestra imagen es de tamaño 512x512 pero en el espacio latente ocupa 8 veces menos. Este divisor viene especificado en la arquitectura de Stable Diffusion 1.5.

Recuerda que al utilizar CUDA, lo hemos especificado en ambas funciones mediante el argumento device='cuda'.

Ahora latents es un tensor con ruido sobre el que ya podemos trabajar como si de un lienzo se tratase.

Limpiar el ruido del tensor

El predictor de ruido estima cuánto ruido hay en la imagen. Tras esto, el algoritmo llamado sampler genera una imagen con esa cantidad de rudio y se resta de la imagen original. Este proceso se repite la cantidad de veces especificada por los pasos (steps o sampling steps).

Predictor de ruido (U-Net)Seedtensor con ruidotensor sin ruido[15, 23, 1...][-94 6 7...][85 58 1...][48, 91, 0...][-8 -5 12...][59 6 -8...][15, 23, 1...][-94 6 7...][85 58 1...][48, 91, 0...][-8 -5 12...][59 6 -8...]

Vamos a configurar el scheduler para indicarle en cuántos pasos queremos limpiar el tensor:

  • Python
scheduler.set_timesteps(inference_steps)

Podemos comprobar cómo funciona internamente imprimiendo la siguiente propiedad:

  • Python
print(scheduler.timesteps)
tensor([999.0000, 964.5517, 930.1035, 895.6552, 861.2069, 826.7586, 792.3104,
        757.8621, 723.4138, 688.9655, 654.5172, 620.0690, 585.6207, 551.1724,
        516.7241, 482.2758, 447.8276, 413.3793, 378.9310, 344.4828, 310.0345,
        275.5862, 241.1379, 206.6897, 172.2414, 137.7931, 103.3448,  68.8966,
         34.4483,   0.0000])

Como son 30 pasos, se han generado 30 elementos separados por la misma distancia (34.4483 unidades).

Algunos schedulers como DPM2 Karras o Euler, necesitan que desde el primer paso los valores del tensor ya estén multiplicados por la desviación estándar de la distribución de ruido inicial. Los schedulers que no lo necesitan simplemente multiplicarán por 1. El caso... que hay que añadir la siguiente operación:

  • Python
latents = latents * scheduler.init_noise_sigma

Ya tenemos el tensor con ruido, los condicionamientos y el scheduler. Con esto ya podemos generar el bucle que limpiará el tensor a lo largo de 30 vueltas. Utilizamos la librería tqdm para mostrar una barra de progreso. Explicaré cada línea directamente en el código.

  • Python
for t in tqdm(scheduler.timesteps):
  # Como estamos utilizando classifier-free guidance, duplicamos el tensor para evitar hacer dos pasadas
  # Una pasada será por los valores condicionados y otra por los incondicionados
  # Con esto se gana eficiencia
  latent_model_input = torch.cat([latents] * 2)

  # Esta línea asegura compatibilidad entre varios schedulers
  # Básicamente, los que necesitan escalar la entrada según el timestep
  latent_model_input = scheduler.scale_model_input(latent_model_input, timestep=t)

  with torch.no_grad():
    # Se le pide a la U-Net que haga una predicción de la cantidad de ruido que hay en el tensor
    noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample

  # Asignamos la mitad del ruido estimado al condicionamiento y la otra mitad al incondicionamiento
  noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)

  # Aquí se ajusta la predicción para guiar hacia un resultado condicionado
  # Es decir, se le da más o menos importancia al condicionamiento (al prompt en este caso)
  noise_pred = noise_pred_uncond + cfg_scale * (noise_pred_cond - noise_pred_uncond)

  # Se genera un nuevo tensor restando la cantidad de ruido que hemos calculado previamente
  # Este es el proceso que limpia el ruido hasta que todos los pasos del scheduler han finalizado
  latents = scheduler.step(noise_pred, t, latents).prev_sample

Tras finalizar este bucle, ya tenemos nuestro tensor libre de ruido y listo para ser convertido en una espectacular imagen. O quizás en un churro, ahora saldremos de dudas.

Si a pesar de estar utilizando la misma semilla no consigues obtener la misma imagen como resultado, este problema de reproducibilidad seguramente se deba a que estás utilizando un sampler ancestral o estocástico.

Esto ocurre porque los samplers ancestrales añaden ruido extra en cada paso y los samplers estocásticos utilizan información del paso anterior. Por lo tanto, necesitan tener acceso al generador para ofrecer esta variabilidad en cada paso. Solo hay que añadirlo a esta línea:

  • Python
latents = scheduler.step(noise_pred, t, latents, generator=generator).prev_sample

Convertir el tensor en una imagen

Un variational autoencoder (VAE) es un tipo de red neuronal que convierte una imagen en un tensor en el espacio latente (encoder) o un tensor del espacio latente en una imagen (decoder).

EspaciolatenteVAE encoderVAE decoder15 23 1-94 6 785 58 1tensor

Ya queda poco. Hay que tener en cuenta el factor de escala (vae.config.scale_factor) del propio VAE, un valor fijado en 0.18215. Una vez normalizado el tensor ya podemos decodificarlo mediante vae.decode() para sacarlo del espacio latente e introducirlo en el espacio de imagen.

  • Python
latents = latents / vae.config.scale_factor
with torch.no_grad():
  images = vae.decode(latents).sample

Seguimos teniendo un tensor, pero ya no está dentro de la Matrix. Este tensor tiene valores que van desde -1 a 1, así que primero lo normalizamos a un rango desde 0 hasta 1.

Podríamos hacer cálculos para convertir estos valores en valores RGB pero dejemos que torchvision se encargue de ello. Utilizamos su transformación ToPILImage para convertir el tensor de torch en una imagen de pillow (o varias, dependiendo del batch_size). El método save() de Pillow se encargará de guardar las imágenes en el disco.

  • Python
images = (images / 2 + 0.5).clamp(0, 1)

to_pil = ToPILImage()

for i in range(1, batch_size + 1):
  image = to_pil(images[i])
  image.save(f'image_{i}.png')

Ejecuta python inference.py... ¡y ya podemos visualizar nuesta magnífica y perfectísima imagen!

Perro disfrazado de batman
Bueno... vale, es un churro. Habría que mejorar el prompt, cambiar de modelo...

Conclusión

Hemos visto de qué components se compone un modelo de Stable Diffusion y también qué resultado producen para así poder conectarlas entre ellas. Gracias a la librería diffusers hemos podido abstraer el código lo suficiente como para no tener que reinventar la rueda desde cero, pero tampoco quedarnos en la superficie sin entender nada.

En diffusers tenemos pipelines como la de Stable Diffusion para ejecutar el proceso de inferencia en un par de líneas:

  • Python
import torch
from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained('runwayml/stable-diffusion-v1-5').to('cuda')
image = pipe('dog in batman costume').images[0]

Podría decirse que hemos implementado nuestra propia pipeline, en la que podemos intercambiar componentes como por ejemplo el scheduler o el VAE.

Espero que no se te haya hecho muy complicado y que este artículo te haya sido de ayuda para entender cómo funciona el proceso de inferencia de Stable Diffusion.

Puedes apoyarme para que pueda dedicar aún más tiempo a escribir artículos y tener recursos para crear nuevos proyectos. ¡Gracias!