Phase 08 - Lesson 08
ControlNet, LoRA y Condicionamiento
El texto por sí solo es una señal de control imprecisa. ControlNet te permite clonar un modelo de difusión preentrenado y guiarlo con un mapa de profundidad, un esqueleto de pose, un boceto (scribble) o una imagen de bordes. LoRA te permite ajustar un modelo de 2,000 millones de parámetros entrenando solo 10 millones de parámetros. Juntos, transformaron Stable Diffusion de un juguete al pipeline de imágenes de 2026 que se entrega en todas las agencias.
Tipo: Build Lenguajes: Python Prerrequisitos: Fase 8 · 07 (Difusión Latente), Fase 10 (LLMs desde Cero — para la base de LoRA) Tiempo: ~75 minutos
El Problema
Un prompt como "a woman in a red dress walking a dog on a busy street" no le da información al modelo sobre dónde está el perro, qué pose tiene la mujer o la perspectiva de la calle. El texto define aproximadamente el 10% de lo que necesitas para especificar una imagen. El resto es visual y no se puede describir de manera eficiente con palabras.
Entrenar un nuevo modelo condicional desde cero para cada señal (pose, profundidad, canny, segmentación) es prohibitivo. Lo ideal es mantener congelado el backbone de SDXL de 2.6B de parámetros, conectar una pequeña red secundaria (side-network) que lea el condicionamiento y hacer que esta modifique ligeramente las características intermedias del backbone. Eso es ControlNet.
También quieres enseñarle nuevos conceptos al modelo (tu rostro, tu producto, tu estilo) sin volver a entrenar el modelo completo. Quieres un delta 100 veces menor. Eso es LoRA: adaptadores de bajo rango (low-rank adapters) que se acoplan a los pesos de atención existentes.
ControlNet + LoRA + texto = el conjunto de herramientas del profesional en 2026. La mayoría de los pipelines de imágenes en producción superponen de 2 a 5 LoRAs, de 1 a 3 ControlNets y un IP-Adapter sobre una base SDXL / SD3 / Flux.
El Concepto
ControlNet (Zhang et al., 2023)
Toma un SD preentrenado. Clona la mitad del codificador (encoder) de la U-Net. Congela el original. Entrena el clon para aceptar una entrada de condicionamiento adicional (bordas, profundidad, pose). Conecta el clon de vuelta a la mitad del decodificador (decoder) del original con conexiones de atajo de convolución cero (zero-convolution) (convoluções 1×1 inicializadas en cero: comienzan como una operación nula o no-op y aprenden un delta).
SD U-Net decoder: ... ← orig_enc_features + zero_conv(controlnet_enc(condition))
La inicialización de convolución cero (zero-conv) significa que ControlNet comienza como una identidad: no causa daños incluso antes del entrenamiento. Entrénalo con 1 millón de triples (prompt, condición, imagen) con la pérdida de difusión estándar.
Los ControlNets por modalidad se distribuyen como modelos secundarios pequeños (~360M para SDXL, ~70M para SD 1.5). Puedes componerlos en la inferencia:
features += weight_a * control_a(depth) + weight_b * control_b(pose)
LoRA (Hu et al., 2021)
Para cualquier capa lineal W ∈ R^{d×d} en el modelo, congela W y añade un delta de bajo rango (low-rank):
W' = W + ΔW, ΔW = B @ A, A ∈ R^{r×d}, B ∈ R^{d×r}
con r << d. Un rango (rank) de 4 a 16 es el estándar para la atención, y de 64 a 128 para ajustes finos intensos. Cantidad de nuevos parámetros: 2 · d · r en lugar de d². Para la atención de SDXL con d=640, r=16: 20k parámetros por adaptador en lugar de 410k, una reducción de 20 veces. En todo el modelo, un LoRA suele pesar entre 20 y 200 MB, frente a los 5 GB de la base.
En la inferencia puedes escalar el LoRA: W' = W + α · B @ A. Lo normal es usar α = 0.5-1.5. Múltiples LoRAs se apilan de forma aditiva (con la advertencia habitual de que interactúan de formas no lineales).
IP-Adapter (Ye et al., 2023)
Un adaptador diminuto que acepta una imagen como condicionamiento (junto con el texto). Utiliza el codificador de imágenes de CLIP para generar tokens de imagen y los inyecta en la atención cruzada (cross-attention) junto con los tokens de texto. Pesa alrededor de ~20 MB por modelo base. Te permite "generar una imagen con el estilo de esta referencia" sin necesidad de un LoRA.
Matriz de composibilidad
| Herramienta | Qué controla | Tamaño | Cuándo usarlo |
|---|---|---|---|
| ControlNet | Estructura espacial (pose, profundidad, bordes) | 70-360MB | Distribución exacta, composición |
| LoRA | Estilo, sujeto, concepto | 20-200MB | Personalización, estilo |
| IP-Adapter | Estilo o sujeto a partir de una imagen de referencia | 20MB | Ningún texto puede describir la apariencia |
| Textual Inversion | Concepto único como un nuevo token | 10KB | Legado, reemplazado mayoritariamente por LoRA |
| DreamBooth | Ajuste fino completo en un sujeto | 2-5GB | Identidad fuerte, alto costo computacional |
| T2I-Adapter | Alternativa más ligera a ControlNet | 70MB | Dispositivos de borde, presupuesto de inferencia limitado |
ControlNet ≈ espacial. LoRA ≈ semántico. Usa ambos.
Implementación
code/main.py simula los dos mecanismos en 1-D:
LoRA. Una capa lineal preentrenada
W. Congélala. Entrena unB @ Ade bajo rango para queW + BAcoincida con una capa lineal objetivo. Demuestra quer = 1es suficiente para aprender perfectamente una corrección de rango 1 (rank-1).ControlNet-lite. Un predictor de "base congelada" y una "red secundaria" (side network) que lee una señal adicional. La salida de la red secundaria está controlada por una compuerta (gate) escalar entrenable inicializada en cero (nuestra versión de zero-conv). Entrénala y observa cómo se incrementa la compuerta.
Paso 1: Matemática de LoRA
def lora(W, A, B, x, alpha=1.0):
# W is frozen; A, B are the trainable low-rank factors.
return [W[i][j] * x[j] for i, j in ...] + alpha * (B @ (A @ x))
Paso 2: Red secundaria con inicialización en cero
side_out = control_net(x, condition)
gated = gate * side_out # gate initialized to 0
h = base(x) + gated
En el paso 0, la salida es idéntica a la base. El entrenamiento inicial actualiza la compuerta gate lentamente: sin desvíos catastróficos.
Errores Comunes
- Sobreescalado de LoRAs (Over-scaling). Utilizar
α = 2oα = 3es un truque común para "hacerlo más fuerte" que genera salidas demasiado estilizadas o corruptas. Manténα ≤ 1.5. - Conflicto de pesos de ControlNet. Usar un ControlNet de Pose con peso 1.0 y un ControlNet de Profundidade con peso 1.0 generalmente causa distorciones (overshoot). Un valor seguro por defecto es que la suma de los pesos sea ≈ 1.0.
- LoRA en la base incorrecta. Los LoRAs de SDXL no tienen ningún efecto (no-op) de forma silenciosa en SD 1.5 porque las dimensiones de atención no coinciden. Diffusers mostrará una advertencia a partir de la versión 0.30+.
- Desviación de Textual Inversion. Los tokens entrenados en un checkpoint se desvían de forma severa en otro. LoRA es mucho más portable.
- Fusión de pesos y almacenamiento de LoRA. Puedes incorporar (bake) un LoRA en los pesos del modelo base para una inferencia más rápida (sin adición en tiempo de ejecución), pero perderás la capacidad de escalar
αen tiempo de ejecución. Conserva ambas versiones.
Cómo Usarlo
| Objetivo | Pipeline de 2026 |
|---|---|
| Reproducir el estilo artístico de una marca | LoRA entrenado en ~30 imágenes seleccionadas con rango (rank) 32 |
| Poner mi rostro en una imagen generada | DreamBooth o LoRA + IP-Adapter-FaceID |
| Pose específica + prompt | ControlNet-Openpose + SDXL + texto |
| Composición basada en profundidad | ControlNet-Depth + SD3 |
| Referencia + prompt | IP-Adapter + texto |
| Distribución exacta | ControlNet-Scribble o ControlNet-Canny |
| Reemplazar el fondo | ControlNet-Seg + Inpainting (Lección 09) |
| Estilo rápido en 1 paso | LCM-LoRA en SDXL-Turbo |
Prática
Guarda outputs/skill-sd-toolkit-composer.md. La habilidad (skill) recibe una tarea (recursos de entrada: prompt, imagen de referencia opcional, pose opcional, profundidad opcional, boceto opcional) y genera la pila de herramientas, los pesos y un protocolo de semilla (seed) reproducible.
Ejercicios
- Fácil. En
code/main.py, varía el rangorde LoRA de 1 a 4. ¿En qué rango coincide exactamente el LoRA con un delta objetivo de rango 2? - Medio. Entrena dos LoRAs diferentes en dos transformaciones objetivo. Cárgalos juntos y muestra su interacción aditiva. ¿En qué punto la interacción rompe la linealidad?
- Difícil. Utiliza diffusers para apilar: SDXL-base + Canny-ControlNet (peso 0.8) + un LoRA de estilo (α 0.8) + IP-Adapter (peso 0.6). Mide el compromiso (trade-off) entre FID y adherencia al prompt a medida que varían los pesos de la pila.
Términos Clave
| Término | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| ControlNet | "Control espacial" | Codificador clonado + atajos zero-conv; lee una imagen de condicionamiento. |
| Zero convolution | "Comienza como identidad" | Convolución 1×1 inicializada en cero; ControlNet comienza como una operación nula (no-op). |
| LoRA | "Adaptador de bajo rango" | W + B @ A, r << d; 100 veces menos parámetros que un ajuste fino completo. |
| rank r | "La perilla de ajuste" | Compresión de LoRA; típicamente de 4 a 16, 64+ para personalizaciones intensas. |
| α | "Fuerza del LoRA" | Escala en tiempo de ejecución del delta de LoRA. |
| IP-Adapter | "Imagen de referencia" | Adaptador pequeño de condicionamiento de imagen mediante tokens de imagen CLIP. |
| DreamBooth | "Ajuste fino completo del sujeto" | Entrena el modelo completo con ~30 imágenes de un sujeto. |
| Textual Inversion | "Nuevo token" | Aprende solo un nuevo embedding de palabra; legado, mayoritariamente reemplazado. |
Nota de producción: Intercambio de LoRAs, carriles de ControlNet y servicio multi-tenant
Un SaaS real de texto a imagen sirve cientos de LoRAs y una docena de ControlNets sobre el mismo checkpoint base. El problema de servicio se parece mucho a la multi-tenencia (multi-tenancy) en LLMs (la literatura de producción aborda el caso de los LLMs bajo procesamiento por lotes continuo — continuous batching — y LoRAX / S-LoRA):
- Intercambia LoRAs en tiempo real (hot-swap), no los fusiones. Fusionar
W' = W + α·B·Aen la base ofrece una inferencia por paso ~3-5% más rápida, pero congelaαy la base. Mantén los LoRAs activos en VRAM como deltas de rango r; diffusers exponepipe.load_lora_weights()+pipe.set_adapters([...], adapter_weights=[...])para la activación por solicitud. El costo de intercambio son los pesos de tamaño2 · d · r · num_layers, en escala de MB y en menos de un segundo. - ControlNet como un segundo carril de atención. El codificador clonado se ejecuta en paralelo con la base. Dos ControlNets con peso 1.0 cada uno = dos pasadas hacia adelante (forward passes) adicionales por paso, no una sola pasada unificada. El margen del tamaño del lote disminuye de forma cuadrática. Presupuesta aproximadamente ~1.5 veces el costo del paso por cada ControlNet activo.
- LoRAs cuantizados también. Si cuantizaste la base (ver Lección 07, Flux en 8GB), el delta de LoRA también se cuantiza limpiamente a 8 o 4 bits. La carga estilo QLoRA te permite apilar de 5 a 10 LoRAs sobre una base Flux de 4 bits sin agotar la memoria.
Específico de Flux: el notebook Flux-on-8GB de Niels cuantiza la base a 4 bits; apilar un LoRA de estilo (pipe.load_lora_weights("user/style-lora")) sobre esa base cuantizada en weight_name="pytorch_lora_weights.safetensors" sigue funcionando. Esta es la receta que la mayoría de las agencias SaaS entregan en 2026.
Lectura Adicional
- Zhang, Rao, Agrawala (2023). Adding Conditional Control to Text-to-Image Diffusion Models — ControlNet.
- Hu et al. (2021). LoRA: Low-Rank Adaptation of Large Language Models — LoRA (originalmente para LLMs; adaptado para difusión).
- Ye et al. (2023). IP-Adapter: Text Compatible Image Prompt Adapter — IP-Adapter.
- Mou et al. (2023). T2I-Adapter: Learning Adapters to Dig Out More Controllable Ability — alternativa más ligera a ControlNet.
- Ruiz et al. (2023). DreamBooth: Fine Tuning Text-to-Image Diffusion Models for Subject-Driven Generation — DreamBooth.
- HuggingFace Diffusers — ControlNet / LoRA / IP-Adapter docs — pipelines de referencia.