El texto no es suficiente

Un prompt de texto como "un atardecer sobre montañas" te da un atardecer sobre montañas, pero ¿cuál atardecer? ¿Cuáles montañas? ¿Desde qué ángulo de cámara? ¿Con qué disposición de profundidad? ¿En el estilo de pintura de quién? El texto es una señal de control de alto nivel y semántica — especifica qué generar pero da casi ningún control sobre cómo generarlo. Cada vez que ejecutas el mismo prompt, el modelo inventa una nueva composición, pose, perspectiva y paleta de colores. Para profesionales creativos, esta aleatoriedad es un problema: un ilustrador necesita un personaje en una pose específica, un arquitecto necesita un edificio renderizado desde un mapa de profundidad específico, y un diseñador de marca necesita salidas que coincidan con un estilo visual específico.

¿Cómo especificas pose exacta, estructura de bordes, disposición de profundidad, paleta de colores o el estilo de un artista específico? La comunidad de generación desarrolló varios mecanismos de control complementarios, cada uno apuntando a un eje diferente de control. Este artículo cubre los cuatro más importantes: ControlNet (condiciones estructurales como bordes y profundidad), IP-Adapter (condicionamiento de estilo y referencia basado en imagen), LoRA (personalización ligera del modelo), y Textual Inversion (enseñar nuevos conceptos a través de embeddings). Juntos, forman un stack de control modular que puede mezclarse, combinarse y componerse.

ControlNet: añadiendo condiciones estructurales

La forma más directa de controlar la estructura de la imagen es proporcionar una condición espacial explícita: un mapa de bordes, un mapa de profundidad, un esqueleto de pose, una máscara de segmentación. ControlNet (Zhang et al., 2023) introdujo un patrón arquitectónico limpio para inyectar tales condiciones en un modelo de difusión preentrenado sin destruir sus capacidades aprendidas.

La idea clave: clonar la mitad codificadora de la U-Net , entrenar el clon en pares (condición, imagen), e inyectar sus salidas de vuelta al modelo original congelado vía convoluciones inicializadas a cero . Los pesos de la U-Net original están completamente congelados — nunca cambian durante el entrenamiento de ControlNet. El codificador clonado (la "copia entrenable") procesa la condición espacial (por ejemplo, un mapa de bordes Canny) y produce mapas de características en cada nivel de resolución. Estos mapas de características se añaden a las conexiones residuales de la U-Net congelada, dando al eliminador de ruido guía estructural en cada escala.

¿Por qué clonar el codificador en lugar de entrenar una nueva red desde cero? Porque el codificador clonado comienza con todas las características preentrenadas que el modelo base ya aprendió — detectores de textura, respuestas de bordes, agrupaciones semánticas. Entrenar desde cero tomaría mucho más tiempo y requeriría muchos más datos para redescubrir estas características. El clon las reutiliza inmediatamente y solo necesita aprender cómo mapear desde la nueva modalidad de condición (bordes, profundidad, pose) al espacio de características existente.

El detalle arquitectónico crítico es la zero-conv — una convolución $1 \times 1$ cuyos pesos y sesgos están ambos inicializados a cero:

$$y = \text{Conv}(x;\; W\!=\!0,\; b\!=\!0)$$

Verifiquemos qué sucede en los límites. Al inicio del entrenamiento, $W = 0$ y $b = 0$, así que la salida $y = 0$ para cualquier entrada $x$. Esto significa que ControlNet contribuye exactamente nada a la U-Net congelada al inicio — el comportamiento del modelo base se preserva perfectamente desde el paso cero. A medida que avanza el entrenamiento, los pesos de la zero-conv gradualmente aprenden valores no nulos, y la influencia de ControlNet aumenta suavemente desde cero.

💡 Este principio de "comenzar como identidad" aparece a lo largo del deep learning moderno. Es la misma idea detrás de la inicialización $B=0$ de LoRA (el adaptador comienza con efecto cero en el modelo base) y el AdaLN-Zero de DiT (parámetros de compuerta inicializados a cero para que cada bloque comience como una función identidad). El patrón es: al añadir nuevos parámetros a un modelo preentrenado, inicializarlos para que la salida del modelo no cambie en el paso cero, luego dejar que el entrenamiento gradualmente active la nueva capacidad.

La condición puede ser casi cualquier señal espacial que se alinee con la resolución de la imagen de salida:

  • Mapas de bordes Canny: bordes binarios extraídos de una imagen de referencia. Controla la estructura del contorno.
  • Mapas de profundidad: profundidad por píxel estimada por modelos como MiDaS. Controla la disposición 3D y la perspectiva.
  • Esqueletos OpenPose: poses del cuerpo humano y manos basadas en puntos clave. Controla la postura del personaje.
  • Mapas de segmentación: etiquetas semánticas (cielo, suelo, edificio) que controlan la disposición espacial de las categorías de objetos.
  • Mapas de normales: vectores de orientación de superficie. Controla la geometría de la superficie 3D y la respuesta de iluminación.

Cada tipo de condición requiere un modelo ControlNet entrenado por separado. Sin embargo, múltiples ControlNets pueden apilarse en tiempo de inferencia — por ejemplo, profundidad + pose simultáneamente — simplemente sumando las contribuciones de características de cada ControlNet a las mismas conexiones residuales. Las salidas son aditivas, así que se componen naturalmente.

# ControlNet's zero-conv: output is exactly zero at initialisation
import random

# Simulate a 1x1 conv with zero-initialised weights and bias
# For a real conv: y = W * x + b, with W=0 and b=0

W = 0.0  # weight initialised to zero
b = 0.0  # bias initialised to zero

# Input feature values (arbitrary)
inputs = [random.uniform(-5, 5) for _ in range(6)]

print("Zero-conv at initialisation (W=0, b=0):")
print(f"  Inputs:  {[f'{x:.2f}' for x in inputs]}")
outputs = [W * x + b for x in inputs]
print(f"  Outputs: {[f'{y:.2f}' for y in outputs]}")
print(f"  => ControlNet contributes NOTHING to the frozen U-Net")
print()

# After some training, W and b become non-zero
W_trained = 0.35
b_trained = 0.02
print(f"After training (W={W_trained}, b={b_trained}):")
outputs_trained = [W_trained * x + b_trained for x in inputs]
print(f"  Inputs:  {[f'{x:.2f}' for x in inputs]}")
print(f"  Outputs: {[f'{y:.2f}' for y in outputs_trained]}")
print(f"  => ControlNet now contributes meaningful features")

IP-Adapter: condicionamiento por prompt de imagen

ControlNet da control estructural, pero ¿qué pasa si quieres controlar el estilo ? Intenta describir el estilo visual de una pintura específica, la apariencia exacta de un rostro o la paleta de colores precisa de una fotografía en texto. Es extremadamente difícil. El texto es un canal con pérdida para información visual.

IP-Adapter (Ye et al., 2023) resuelve esto condicionando en una imagen en lugar de (o además de) texto. Dale una pintura de referencia y transfiere el estilo. Dale una foto de rostro y preserva la identidad. La imagen de referencia habla directamente en el dominio visual, evitando completamente el cuello de botella del texto con pérdida.

Arquitectónicamente, un codificador de imagen CLIP preentrenado extrae características de la imagen de referencia. Estos tokens se inyectan en el modelo de difusión vía una capa de atención cruzada separada que se ejecuta en paralelo a la atención cruzada de texto existente. La ruta de texto y la ruta de prompt de imagen están desacopladas , cada una con sus propias keys y values de atención cruzada.

En el modelo de difusión estándar condicionado por texto, cada capa de atención cruzada calcula:

$$Z_{\text{text}} = \text{softmax}\!\left(\frac{Q \cdot K_{\text{text}}^\top}{\sqrt{d_k}}\right) V_{\text{text}}$$

IP-Adapter añade una atención cruzada paralela con sus propios pesos de proyección aprendidos:

$$Z_{\text{IP}} = \text{softmax}\!\left(\frac{Q \cdot K_{\text{ref}}^\top}{\sqrt{d_k}}\right) V_{\text{ref}}$$

Las dos salidas se combinan con un parámetro de ponderación:

$$Z = Z_{\text{text}} + \lambda \, Z_{\text{IP}}$$

Verifiquemos los límites de $\lambda$. Cuando $\lambda = 0$, la contribución de IP-Adapter desaparece completamente y el modelo se comporta exactamente como el modelo condicionado por texto original. Cuando $\lambda = 1$, las características del prompt de imagen tienen el mismo peso que las de texto. A medida que $\lambda$ crece más allá de 1, la imagen de referencia domina cada vez más. En la práctica, $\lambda \in [0.5, 1.0]$ da un buen equilibrio entre controlabilidad del texto y fidelidad a la referencia.

Porque las rutas de texto e IP-Adapter están desacopladas, controlan diferentes aspectos de la salida sin interferir entre sí. El prompt de texto aún controla la escena ( qué generar), mientras la imagen de referencia controla la apariencia ( cómo se ve). Los casos de uso comunes incluyen:

  • Transferencia de estilo: usar una pintura como imagen de referencia. La salida sigue el contenido del prompt de texto pero se renderiza en el estilo de la pintura.
  • Consistencia de rostros: usar una foto de rostro como referencia. La salida preserva la identidad de la persona en diferentes escenas descritas por texto.
  • Preservación de objetos: usar una foto de producto como referencia. La salida coloca ese producto específico en nuevos contextos.
💡 ¿Por qué una atención cruzada separada en lugar de concatenar tokens de imagen a los tokens de texto? La concatenación forzaría a las características de texto e imagen a competir por atención dentro del mismo softmax, haciendo difícil controlar el equilibrio entre ellas. La atención cruzada desacoplada nos permite ajustar $\lambda$ independientemente en tiempo de inferencia — sin necesidad de reentrenamiento para cambiar el equilibrio texto-vs-referencia.

LoRA para difusión: personalización ligera

ControlNet e IP-Adapter controlan estructura y estilo en tiempo de inferencia, pero ¿qué pasa si quieres que el modelo mismo aprenda permanentemente un nuevo estilo, un personaje específico o un concepto novedoso? Aquí es donde entra LoRA (Low-Rank Adaptation) — la misma técnica cubierta en el track de fine-tuning ( ver el artículo de LoRA ), ahora aplicada a U-Nets y DiTs de difusión en lugar de modelos de lenguaje.

La idea es idéntica: congelar la matriz de pesos del modelo base $W_0$ y entrenar dos pequeñas matrices de bajo rango $A$ y $B$ tales que el peso efectivo se convierta en $W_0 + BA$, donde $B \in \mathbb{R}^{d \times r}$ y $A \in \mathbb{R}^{r \times d}$ con $r \ll d$:

$$W = W_0 + BA, \quad B \in \mathbb{R}^{d \times r}, \; A \in \mathbb{R}^{r \times d}$$

Como $B$ se inicializa a cero, el producto $BA = 0$ al inicio del entrenamiento, así que la salida del modelo no cambia en el paso cero — el mismo principio de "comenzar como identidad" que vimos en la zero-conv de ControlNet. El rango $r$ es típicamente entre 4 y 32 para modelos de difusión, apuntando a las capas de atención (proyecciones de query, key, value y salida) en la U-Net o DiT.

Para modelos de difusión, tres tipos de LoRA se han vuelto especialmente comunes:

  • LoRAs de estilo: entrenados en 20-50 imágenes en un estilo artístico específico (anime, acuarela, pixel art, el estilo de un ilustrador específico). Después del entrenamiento, cualquier prompt genera en ese estilo.
  • LoRAs de personaje: entrenados en 10-20 imágenes de un personaje específico (ficticio o real). El modelo aprende las características visuales consistentes del personaje y puede renderizarlos en nuevas poses y escenas.
  • LoRAs de concepto: entrenados en imágenes de un objeto, producto o ubicación específicos. Enseña al modelo un nuevo concepto visual que nunca ha visto: tu producto específico, tu edificio específico, tu mascota específica.

Los archivos LoRA son diminutos — típicamente 5 a 200 MB, comparados con varios gigabytes del modelo base. Este pequeño tamaño tiene una consecuencia importante: los LoRAs pueden apilarse y combinarse . Puedes aplicar un LoRA de estilo y un LoRA de personaje simultáneamente, cada uno ponderado por un escalar. El peso efectivo se convierte en:

$$W = W_0 + w_1 \cdot B_1 A_1 + w_2 \cdot B_2 A_2 + \cdots$$

donde $w_i$ controla la influencia de cada LoRA. Cuando todos $w_i = 0$, recuperamos el modelo base exactamente. A medida que cualquier $w_i$ aumenta, el comportamiento aprendido de ese LoRA se vuelve más fuerte. En la práctica, $w_i \in [0.5, 1.0]$ para cada LoRA funciona bien; valores muy por encima de 1.0 tienden a sobresaturar el estilo y degradar la calidad de imagen.

Esta composabilidad y pequeño tamaño de archivo crearon un ecosistema próspero. Plataformas como CivitAI albergan miles de LoRAs entrenados por la comunidad — para estilos, personajes, conceptos, poses, configuraciones de iluminación — todos composables con cualquier modelo base compatible. Un usuario puede descargar un modelo base como SDXL, luego añadir un LoRA de estilo, un LoRA de personaje y un LoRA de iluminación, cada uno hecho por un creador diferente, y combinarlos en una sola generación.

# LoRA parameter count vs full model — diffusion U-Net example

d = 1024        # typical hidden dimension in SDXL U-Net attention layers
num_layers = 70 # approximate number of attention projections (Q, K, V, Out across blocks)

print("LoRA parameter counts for a diffusion model")
print("=" * 55)
print(f"Hidden dimension d = {d}")
print(f"Number of target layers = {num_layers}")
print(f"Full parameter count per layer = d * d = {d*d:,}")
print(f"Full params (all layers) = {d*d*num_layers:,}")
print()
print(f"{'Rank r':<10} {'Params/layer':<16} {'Total LoRA':<16} {'% of full':<10}")
print("-" * 55)
for r in [4, 8, 16, 32]:
    per_layer = 2 * d * r  # A is r x d, B is d x r
    total = per_layer * num_layers
    pct = total / (d * d * num_layers) * 100
    print(f"{r:<10} {per_layer:<16,} {total:<16,} {pct:<10.2f}%")
print()
print("Even r=32 is <7% of the full model parameters")

Textual Inversion: enseñando nuevas palabras

¿Qué pasa si queremos algo aún más simple que LoRA — una forma de enseñarle al modelo un nuevo concepto sin cambiar ningún peso del modelo? Textual Inversion (Gal et al., 2022) hace exactamente esto. En lugar de modificar el eliminador de ruido, aprende un único nuevo embedding de texto — un vector en el espacio de embedding del codificador de texto — que representa un concepto visual.

La configuración: tienes 3-5 imágenes de un concepto que quieres que el modelo aprenda (digamos, tu perro mascota). Introduces un nuevo token marcador $v^*$ en el vocabulario del codificador de texto e inicializas su embedding aleatoriamente. Luego entrenas solo ese vector de embedding $v^* \in \mathbb{R}^{d_{\text{text}}}$ mientras mantienes todo el modelo de difusión y el resto del codificador de texto congelados.

$$\mathcal{L} = \mathbb{E}_{x_0, \epsilon, t}\left[\|\epsilon - \epsilon_\theta(x_t, t, c_{\text{text}}(v^*))\|^2\right]$$

Aquí $c_{\text{text}}(v^*)$ es el condicionamiento de texto que incluye el token aprendido $v^*$. Solo $v^*$ recibe gradientes; todo lo demás está congelado. Después del entrenamiento, el modelo trata $v^*$ como una palabra ordinaria. Puedes escribir prompts como "una pintura de $v^*$ al estilo de Van Gogh" o "$v^*$ sentado en una playa al atardecer", y el modelo compone $v^*$ con el resto del prompt como compondría dos palabras cualesquiera.

La ventaja es simplicidad extrema: el artefacto aprendido es un único vector (típicamente 768 o 1024 flotantes, solo unos pocos kilobytes). Es trivialmente compartible, composable con cualquier prompt, y no puede romper el modelo porque no modifica nada. Múltiples inversiones textuales pueden coexistir — cada una solo añade un nuevo embedding al vocabulario.

La desventaja es igualmente clara: un único vector de embedding tiene expresividad limitada. Debe comprimir todo sobre un concepto — forma, color, textura, identidad — en un punto en el espacio de embedding. Para conceptos simples (una textura específica, una paleta de colores), esto funciona bien. Para conceptos complejos con muchas características distintivas (un personaje detallado, un estilo artístico matizado), un solo vector no es suficiente, y LoRA (que modifica miles de parámetros a través del eliminador de ruido) capturará mucho más detalle.

# Textual Inversion vs LoRA: what gets trained?

d_text = 768     # CLIP text embedding dimension (SD 1.5)
d_model = 1024   # U-Net hidden dimension
num_lora_layers = 70
lora_rank = 8

# Textual inversion: ONE embedding vector
ti_params = d_text
ti_bytes = ti_params * 4  # float32

# LoRA: low-rank matrices across many layers
lora_params = 2 * d_model * lora_rank * num_lora_layers
lora_bytes = lora_params * 4

# Full model (approximate for SD 1.5 U-Net)
full_params = 860_000_000
full_bytes = full_params * 4

print("Parameter comparison")
print("=" * 50)
print(f"{'Method':<22} {'Parameters':<16} {'File size':<14}")
print("-" * 50)
print(f"{'Textual Inversion':<22} {ti_params:<16,} {ti_bytes / 1024:.1f} KB")
print(f"{'LoRA (r=8)':<22} {lora_params:<16,} {lora_bytes / (1024**2):.1f} MB")
print(f"{'Full model':<22} {full_params:<16,} {full_bytes / (1024**3):.1f} GB")
print()
print("Textual Inversion: extreme simplicity, one vector, a few KB")
print("LoRA: much more expressive, but still <1% of the full model")

Combinando controles

El verdadero poder de estas técnicas es que son composables . Cada una controla un eje diferente del proceso de generación, y pueden apilarse sin interferir entre sí:

  • Prompt de texto: controla el contenido de la escena ("una mujer de pie en un jardín").
  • ControlNet: controla la estructura espacial (el esqueleto de pose especifica su postura exacta, el mapa de profundidad define la disposición del jardín).
  • IP-Adapter: controla el estilo visual (una pintura de referencia establece la paleta de colores y el estilo de pinceladas).
  • LoRA: controla el personaje (un LoRA de personaje asegura que la mujer tenga una apariencia consistente y específica en todas las generaciones).
  • Textual Inversion: controla un concepto específico (un token aprendido $v^*$ que representa una variedad de flor específica que llena el jardín).

¿Por qué funciona esta composabilidad? Porque cada mecanismo opera en un punto diferente de la arquitectura:

  • Textual Inversion modifica la entrada al codificador de texto (un nuevo embedding en el vocabulario).
  • IP-Adapter añade una ruta de atención cruzada paralela (separada de la atención cruzada de texto).
  • ControlNet añade a las conexiones residuales de la U-Net (características estructurales en cada resolución).
  • LoRA modifica los pesos de atención mismos (actualizaciones aditivas de bajo rango).

Como modifican diferentes partes de la red, sus efectos son en gran medida ortogonales. Esta modularidad es la razón por la que el ecosistema de Stable Diffusion se volvió tan rico: el modelo base es una fundación, y la comunidad construye una biblioteca siempre creciente de módulos de control que se conectan a él.

💡 Este stack de control modular es único de los modelos de difusión. Las GANs no tenían un ecosistema comparable porque su arquitectura no separaba limpiamente control estructural, control de estilo y personalización de conceptos en módulos independientes y conectables. La arquitectura aditiva basada en conexiones residuales del marco de difusión hizo esta descomposición natural.

Quiz

Pon a prueba tu comprensión de las técnicas de control y personalización de modelos de difusión.

¿Por qué ControlNet inicializa sus capas de convolución con pesos y sesgos cero (zero-conv)?

En IP-Adapter, ¿qué controla el parámetro de ponderación $\lambda$?

¿Cuál es la limitación principal de Textual Inversion comparado con LoRA para aprender un nuevo concepto visual?

¿Por qué ControlNet, IP-Adapter, LoRA y Textual Inversion pueden componerse en una sola generación sin interferir entre sí?