---
title: Cómo implementar Stable Diffusion
description: Tras ver cómo funciona Stable Diffusion teóricamente ahora toca implementarlo en Python.
url: https://www.felixsanz.dev/es/articulos/como-implementar-stable-diffusion
language: es
published: 2024-01-15
updated: 2024-01-15
tags:
  - ai
  - stablediffusion
alternate-en: https://www.felixsanz.dev/articles/how-to-implement-stable-diffusion
---

# Cómo implementar Stable Diffusion

## Introducción

En un [artículo anterior](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion), 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:

*[Diagram]*

## 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](https://github.com/huggingface/diffusers) 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](https://huggingface.co/runwayml/stable-diffusion-v1-5/tree/main) encontraremos la siguiente estructura:

```
feature_extractor/
  preprocessor_config.json
safety_checker/
  config.json
  model.fp16.safetensors
  model.safetensors
  pytorch_model.bin
  pytorch_model.fp16.bin
scheduler/
  scheduler_config.json
text_encoder/
  config.json
  model.fp16.safetensors
  model.safetensors
  pytorch_model.bin
  pytorch_model.fp16.bin
tokenizer/
  merges.txt
  special_tokens_map.json
  tokenizer_config.json
  vocab.json
unet/
  config.json
  diffusion_pytorch_model.bin
  diffusion_pytorch_model.fp16.bin
  diffusion_pytorch_model.fp16.safetensors
  diffusion_pytorch_model.non_ema.bin
  diffusion_pytorch_model.non_ema.safetensors
  diffusion_pytorch_model.safetensors
vae/
  config.json
  diffusion_pytorch_model.bin
  diffusion_pytorch_model.fp16.bin
  diffusion_pytorch_model.fp16.safetensors
  diffusion_pytorch_model.safetensors
.gitattributes
README.md
model_index.json
v1-5-pruned-emaonly.ckpt
v1-5-pruned-emaonly.safetensors
v1-5-pruned.ckpt
v1-5-pruned.safetensors
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](https://developer.nvidia.com/cuda-toolkit). Puedes seguir [estos pasos](https://www.felixsanz.dev/es/articulos/compatibilidad-entre-versiones-de-pytorch-cuda-y-xformers#cuda) para su instalación.

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

```bash
python -m venv .venv
```

```bash
# Unix
source .venv/bin/activate

# Windows
.venv\Scripts\activate
```

```bash
# 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.

> [!NOTE]
> **Repositorio del blog**
> Si quieres copiar y pegar el código entero, recuerda que lo tienes disponible en [articles/how-to-implement-stable-diffusion/inference.py](https://github.com/felixsanz/felixsanz_dev/blob/main/articles/how-to-implement-stable-diffusion/inference.py).
> 
> En el [repositorio del blog](https://github.com/felixsanz/felixsanz_dev) 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](https://huggingface.co/runwayml/stable-diffusion-v1-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](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#etiquetas-de-clase-y-el-valor-cfg-scale), 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***).
> 
> *[Diagram]*
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#tokenizer)

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](https://huggingface.co/runwayml/stable-diffusion-v1-5/raw/main/tokenizer/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*:

```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).
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#tokenizer)

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.
> 
> *[Diagram]*
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#embedding)

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.
> 
> *[Diagram]*
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#transformer)

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.
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#el-espacio-latente)

Para generar ruido instanciamos un generador mediante [torch.Generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) 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](https://pytorch.org/docs/stable/generated/torch.randn.html) 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](https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/unet/config.json#L23) 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***).
> 
> *[Diagram]*
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#muestreo)

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](https://github.com/tqdm/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.

> [!WARNING]
> **Reproducibilidad**
> 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](https://www.felixsanz.dev/es/articulos/guia-completa-de-samplers-en-stable-diffusion#variantes-ancestrales) o [estocástico](https://www.felixsanz.dev/es/articulos/guia-completa-de-samplers-en-stable-diffusion#variantes-estoc%C3%A1sticas).
> 
> 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***).
> 
> *[Diagram]*
> 
> [Cómo funciona Stable Diffusion](https://www.felixsanz.dev/es/articulos/como-funciona-stable-diffusion#el-espacio-latente)

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](https://pytorch.org/vision/stable/index.html) se encargue de ello. Utilizamos su transformación [ToPILImage](https://pytorch.org/vision/main/generated/torchvision.transforms.ToPILImage.html) para convertir el tensor de `torch` en una imagen de `pillow` (o varias, dependiendo del `batch_size`). El método `save()` de [Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save) 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](https://www.felixsanz.dev/assets/result.DlINaDdU_Za5BRU.png)

*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](https://huggingface.co/docs/diffusers/api/pipelines/overview) como la de [Stable Diffusion](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/text2img) 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.
