Stable Diffusion — 구조와 파인튜닝(Fine-Tuning)

Stable Diffusion은 사전학습된 변분 오토인코더(VAE)의 잠재 공간(latent space)에서 동작하는 DDPM입니다. 텍스트는 교차 어텐션(cross-attention)을 통해 조건(conditioning)으로 주입되고, 빠른 결정론적(deterministic) ODE 솔버(solver)로 표본을 뽑으며(sampling), 분류기 없는 가이던스(classifier-free guidance)로 출력 방향을 조향합니다.

유형: Learn + Use 언어: Python 선수 학습: Phase 4 Lesson 10 (Diffusion), Phase 7 Lesson 02 (Self-Attention) 소요 시간: 약 75분

학습 목표

  • Stable Diffusion 파이프라인(pipeline)의 다섯 구성 요소인 VAE, 텍스트 인코더(text encoder), U-Net, 스케줄러(scheduler), 안전 검사기(safety checker)를 추적하고 각 요소가 실제로 하는 일을 설명합니다.
  • 잠재 확산(latent diffusion)을 설명하고, 3x512x512 이미지 대신 4x64x64 잠재 공간에서 학습하면 품질 손실 없이 연산량(compute)이 48배 줄어드는 이유를 이해합니다.
  • diffusers로 텍스트-이미지(text-to-image), 이미지-이미지(image-to-image), 인페인팅(inpainting), ControlNet 기반 가이드 생성을 실행합니다.
  • 작은 사용자 데이터셋(custom dataset)에서 LoRA로 Stable Diffusion을 파인튜닝하고 추론(inference) 시점에 LoRA 어댑터(adapter)를 불러옵니다.

문제

512x512 RGB 이미지에서 DDPM을 직접 학습하는 것은 비쌉니다. 매 학습 단계(training step)는 3x512x512 = 786,432개의 입력값을 보는 U-Net을 통해 역전파(backprop)를 수행하고, 표본 생성(sampling)은 같은 U-Net을 50번 이상 순전파(forward)해야 합니다. Stable Diffusion 1.5(2022년 공개) 수준의 품질에서 픽셀 공간 확산(pixel-space diffusion)은 대략 256 GPU-month의 학습과 소비자 GPU에서 이미지당 10-30초의 추론이 필요합니다.

오픈 가중치(open-weight) 텍스트-이미지 생성을 실용적으로 만든 핵심은 잠재 확산(latent diffusion)(Rombach et al., CVPR 2022)입니다. 3x512x512 이미지를 4x64x64 잠재 텐서(latent tensor)로, 다시 이미지로 변환하는 VAE를 학습한 뒤, 확산 과정을 그 잠재 공간에서 수행합니다. 연산량은 (3*512*512)/(4*64*64) = 48x만큼 줄어듭니다. 같은 GPU에서 표본 생성도 수십 초에서 2초 미만으로 내려갑니다.

현대의 이미지 생성 모델(image-generation model) 대부분, 예를 들어 SDXL, SD3, FLUX, HunyuanDiT, Wan-Video는 오토인코더(autoencoder), 잡음 제거기(denoiser; U-Net 또는 DiT), 텍스트 조건 부여(text conditioning)를 변형한 잠재 확산 모델입니다. Stable Diffusion을 배우면 그 공통 틀(template)을 배운 것입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.Stable Diffusion이 3x512x512 픽셀 이미지(pixel image)에서 직접 DDPM을 실행하지 않고 4x64x64 잠재 공간(latent space)에서 실행하는 이유는 무엇인가요?

2.분류기 없는 가이던스(Classifier-Free Guidance; CFG)는 추론(inference)에서 무엇을 하나요?

0/2 답변 완료

개념

파이프라인(Pipeline)

flowchart LR
    TXT["Text prompt"] --> TE["Text encoder<br/>(CLIP-L or T5)"]
    TE --> CT["Text<br/>embedding"]

    NOISE["Noise<br/>4x64x64"] --> UNET["UNet<br/>(denoiser with<br/>cross-attention<br/>to text)"]
    CT --> UNET

    UNET --> SCHED["Scheduler<br/>(DPM-Solver++,<br/>Euler)"]
    SCHED --> LATENT["Clean latent<br/>4x64x64"]
    LATENT --> VAE["VAE decoder"]
    VAE --> IMG["512x512<br/>RGB image"]

    style TE fill:#dbeafe,stroke:#2563eb
    style UNET fill:#fef3c7,stroke:#d97706
    style SCHED fill:#fecaca,stroke:#dc2626
    style IMG fill:#dcfce7,stroke:#16a34a
  • VAE — 가중치를 고정(frozen)한 오토인코더입니다. 인코더(encoder)는 이미지를 잠재 벡터(latent)로 바꿔 img2img와 학습에 사용하고, 디코더(decoder)는 잠재 벡터를 다시 이미지로 복원합니다.
  • 텍스트 인코더(Text encoder) — SD 1.x/2.x에서는 CLIP 텍스트 인코더, SDXL에서는 CLIP-L + CLIP-G, SD3/FLUX에서는 T5-XXL을 사용합니다. 토큰 임베딩(token embedding) 시퀀스를 만듭니다.
  • U-Net — 잡음 제거기(denoiser)입니다. 모든 해상도 단계에서 잠재 벡터가 텍스트 임베딩을 바라보는 교차 어텐션(cross-attention) 층을 갖습니다.
  • 스케줄러(Scheduler) — 표본 생성 알고리즘입니다. DDIM, Euler, DPM-Solver++처럼 시그마(sigma)를 정하고 예측된 잡음을 잠재 벡터에 다시 섞어 넣습니다.
  • 안전 검사기(Safety checker) — 출력 이미지에 대한 선택적 NSFW / 불법 콘텐츠 필터입니다.

분류기 없는 가이던스(Classifier-Free Guidance; CFG)

일반적인 텍스트 조건 부여는 모든 프롬프트 c에 대해 epsilon_theta(x_t, t, c)를 학습합니다. CFG는 학습 중 10% 정도의 확률로 c를 비워둔 임베딩으로 바꿔, 같은 네트워크가 조건부 잡음(conditional noise)과 비조건부 잡음(unconditional noise)을 모두 예측하게 합니다. 추론에서는 다음 식을 씁니다.

eps = eps_uncond + w * (eps_cond - eps_uncond)

w는 가이던스 척도(guidance scale)입니다. w=0은 비조건부, w=1은 일반 조건부, w>1은 다양성을 일부 희생하면서 프롬프트에 더 강하게 맞춘 결과로 밀어붙입니다. SD 기본값은 w=7.5입니다.

CFG는 텍스트-이미지 생성이 실제 서비스 품질로 작동하는 이유입니다. CFG가 없으면 프롬프트는 결과를 약하게만 편향시키지만, CFG가 있으면 프롬프트가 지배적인 조건이 됩니다.

잠재 공간의 기하 구조(Latent Space Geometry)

VAE의 4채널 잠재 벡터는 단순한 압축 이미지가 아닙니다. 프롬프트 엔지니어링(prompt engineering)과 보간(interpolation)이 함께 살아가는, 산술 연산이 어느 정도 의미적 편집(semantic edit)에 대응하는 다양체(manifold)입니다. 확산 U-Net은 이 공간에 모델링 자원 전체를 쓰도록 학습됩니다. 임의의 4x64x64 잠재 벡터를 디코딩한다고 해서 무작위로 보이는 이미지가 나오지는 않습니다. 유효한 이미지는 잠재 부분 다양체(submanifold)의 특정 영역에서만 복원되기 때문입니다.

이로부터 두 가지가 따라옵니다.

  1. Img2img = 이미지를 잠재 벡터로 인코딩하고, 일부 잡음을 더한 뒤 잡음 제거기를 실행해 다시 이미지로 복원합니다. 인코딩이 거의 가역적(invertible)이므로 이미지 구조는 살아 있고, 내용은 프롬프트에 따라 바뀝니다.
  2. 인페인팅(Inpainting) = img2img와 같지만 잡음 제거기가 마스크된 영역(masked region)만 업데이트하고, 마스크되지 않은 영역은 인코딩된 잠재 값 그대로 유지합니다.

U-Net 구조(U-Net Architecture)

SD U-Net은 Lesson 10의 TinyUNet을 크게 키운 뒤 세 가지를 더한 구조입니다.

  • 모든 공간 해상도 단계에 트랜스포머 블록(transformer block)이 있고, 자기 어텐션(self-attention)과 텍스트 임베딩에 대한 교차 어텐션을 포함합니다.
  • 사인파 부호화(sinusoidal encoding)에 MLP를 적용한 시간 임베딩(time embedding)을 씁니다.
  • 인코더와 디코더 사이에 같은 해상도의 스킵 연결(skip connection)이 있습니다.

SD 1.5는 약 860M 매개변수(parameter), SDXL은 약 2.6B, FLUX는 약 12B입니다. 매개변수 증가의 대부분은 어텐션 층에서 발생합니다.

LoRA 파인튜닝(LoRA Fine-Tuning)

Stable Diffusion 전체 파인튜닝은 20GB 이상의 VRAM이 필요하고 860M개의 매개변수를 업데이트합니다. LoRA(Low-Rank Adaptation; 저차원 적응)는 기반 모델을 고정한 채 어텐션 층에 작은 차원 분해 행렬(rank-decomposition matrix)을 주입합니다. SD용 LoRA 어댑터는 보통 10-50MB이고, 단일 소비자 GPU에서 10-60분 안에 학습되며, 추론 시 끼워 넣는(drop-in) 방식으로 불러와 적용합니다.

Original: W_q : (d_in, d_out)   frozen
LoRA:     W_q + alpha * (A @ B)   where A : (d_in, r), B : (r, d_out)

r is typically 4-32.

LoRA는 거의 모든 커뮤니티 파인튜닝이 배포되는 방식입니다. CivitAI와 Hugging Face에는 수많은 LoRA가 올라와 있습니다.

자주 만나게 될 스케줄러(Scheduler)

  • DDIM — 결정론적이며 약 50단계로 동작하고 구조가 단순합니다.
  • Euler ancestral — 확률적(stochastic)이며 30-50단계로 동작하고 조금 더 창의적인 표본을 만듭니다.
  • DPM-Solver++ 2M Karras — 결정론적이며 20-30단계로 동작하고 실제 서비스 기본값으로 쓰입니다.
  • LCM / TCD / Turbo — 일관성 모델(consistency model)과 그 증류 변형(distilled variant)입니다. 1-4단계로 동작하지만 품질 일부를 희생합니다.

diffusers에서 스케줄러 교체는 한 줄이면 끝나며, 재학습 없이 표본 품질 문제를 고치는 경우도 있습니다.

만들어보기

이 레슨은 Stable Diffusion을 처음부터 재구현하지 않고 diffusers를 종단간(end-to-end)으로 사용합니다. VAE, 텍스트 인코더, U-Net, 스케줄러는 각각 별도의 레슨이 될 만큼 큰 주제입니다. 여기서는 실제 서비스용 API에 익숙해지는 것이 목표입니다.

Step 1: 텍스트-이미지 생성(Text-to-Image)

import torch
from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16,
).to("cuda")

image = pipe(
    prompt="a dog riding a skateboard in tokyo, studio ghibli style",
    guidance_scale=7.5,
    num_inference_steps=25,
    generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("dog.png")

float16은 눈에 띄는 품질 손실 없이 VRAM을 절반으로 줄입니다. 기본 DPM-Solver++에서 num_inference_steps=25는 DDIM의 50단계와 비슷한 품질을 냅니다.

Step 2: 스케줄러 교체(Scheduler Swap)

from diffusers import DPMSolverMultistepScheduler, EulerAncestralDiscreteScheduler

pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)

스케줄러 상태(state)는 U-Net 가중치와 분리되어 있습니다. DDPM으로 학습한 모델도 어떤 스케줄러로든 표본을 뽑을 수 있습니다.

Step 3: 이미지-이미지 변환(Image-to-Image)

from diffusers import StableDiffusionImg2ImgPipeline
from PIL import Image

img2img = StableDiffusionImg2ImgPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16,
).to("cuda")

init_image = Image.open("dog.png").convert("RGB").resize((512, 512))
out = img2img(
    prompt="a dog riding a skateboard, oil painting",
    image=init_image,
    strength=0.6,
    guidance_scale=7.5,
).images[0]

strength는 잡음 제거 전에 얼마나 잡음을 더할지를 정하는 값입니다. 0.0은 변경 없음, 1.0은 완전 재생성입니다. 화풍 변환(style transfer)에는 0.5-0.7이 표준 범위입니다.

Step 4: 인페인팅(Inpainting)

from diffusers import StableDiffusionInpaintPipeline

inpaint = StableDiffusionInpaintPipeline.from_pretrained(
    "runwayml/stable-diffusion-inpainting",
    torch_dtype=torch.float16,
).to("cuda")

image = Image.open("dog.png").convert("RGB").resize((512, 512))
mask = Image.open("dog_mask.png").convert("L").resize((512, 512))

out = inpaint(
    prompt="a cat",
    image=image,
    mask_image=mask,
    guidance_scale=7.5,
).images[0]

마스크에서 흰 픽셀은 재생성할 영역이고, 검은 픽셀은 그대로 보존됩니다.

Step 5: LoRA 불러오기(LoRA Loading)

pipe.load_lora_weights("sayakpaul/sd-lora-ghibli")
pipe.fuse_lora(lora_scale=0.8)

image = pipe(prompt="a village square in ghibli style").images[0]

lora_scale은 적용 강도입니다. 0.0은 효과 없음, 1.0은 최대 효과입니다. fuse_lora는 속도를 위해 어댑터를 가중치에 직접 합쳐 넣는데, 그러면 다른 어댑터로 즉시 교체할 수 없습니다. 다른 어댑터를 불러오기 전에는 pipe.unfuse_lora()를 호출합니다.

Step 6: LoRA 학습 흐름(LoRA Training Sketch)

실제 LoRA 학습 구현은 peft 또는 diffusers.training에 있습니다. 큰 흐름은 다음과 같습니다.

# Pseudocode
for step, batch in enumerate(dataloader):
    images, prompts = batch
    latents = vae.encode(images).latent_dist.sample() * 0.18215

    t = torch.randint(0, num_train_timesteps, (batch_size,))
    noise = torch.randn_like(latents)
    noisy_latents = scheduler.add_noise(latents, noise, t)

    text_emb = text_encoder(tokenizer(prompts))

    pred_noise = unet(noisy_latents, t, text_emb)  # LoRA weights injected here

    loss = F.mse_loss(pred_noise, noise)
    loss.backward()
    optimizer.step()

LoRA 행렬만 기울기(gradient)를 받습니다. 기반 U-Net, VAE, 텍스트 인코더는 모두 고정되어 있습니다. 배치 크기(batch size) 1과 기울기 체크포인트(gradient checkpointing)를 함께 쓰면 8GB VRAM에도 들어갑니다.

활용하기

실제 서비스에서 결정하는 항목은 다음과 같습니다.

  • 모델 계열(Model family): SD 1.5는 오픈 소스 커뮤니티 파인튜닝 생태계가 넓고, SDXL은 더 높은 충실도(fidelity)를 제공하며, SD3 / FLUX는 최신 품질과 엄격한 라이선스 요건에 맞습니다.
  • 스케줄러: 20-30단계에서는 DPM-Solver++ 2M Karras, 지연(latency)이 1초 미만이어야 하면 LCM-LoRA를 고려합니다.
  • 정밀도(Precision): 4080/4090에서는 float16, A100 이상에서는 bfloat16, VRAM이 부족하면 bitsandbytes 또는 compel 기반 int8을 고려합니다.
  • 조건 부여(Conditioning): 일반 텍스트만으로도 동작하지만, 더 강한 제어가 필요하면 기본 파이프라인 위에 ControlNet(canny, depth, pose)을 추가합니다.

대량 일괄 생성에는 커뮤니티 도구인 AUTO1111 / ComfyUI가 쓰입니다. 서비스용 API에서는 diffusers + accelerate를 쓰거나, TensorRT 컴파일을 위한 optimum-nvidia를 사용합니다.

산출물 만들기

이 레슨의 최종 산출물은 다음과 같습니다.

  • outputs/prompt-sd-pipeline-planner.md — 지연 예산(latency budget), 충실도 목표(fidelity target), 라이선스 제약(licensing constraint)을 바탕으로 SD 1.5 / SDXL / SD3 / FLUX 중 어느 것을 쓸지, 스케줄러와 정밀도까지 고르는 프롬프트입니다.
  • outputs/skill-lora-training-setup.md — 사용자 데이터셋에 대한 캡션(caption), 차원(rank), 배치 크기, 학습률(learning rate)을 포함한 전체 LoRA 학습 설정을 작성하는 스킬(skill)입니다.

연습문제

  1. (쉬움) 같은 프롬프트를 guidance_scale [1, 3, 5, 7.5, 10, 15]로 생성합니다. 이미지가 어떻게 바뀌는지 설명합니다. 어떤 가이던스 값에서 인공물(artifact)이 나타나나요?
  2. (중간) 실제 사진 하나를 StableDiffusionImg2ImgPipeline에 넣고 strength [0.2, 0.4, 0.6, 0.8, 1.0]으로 실행합니다. 어떤 강도(strength)가 구도를 유지하면서 화풍을 바꾸나요? 왜 1.0에서는 입력을 거의 무시하나요?
  3. (어려움) 하나의 대상(subject), 예를 들어 반려동물, 로고, 캐릭터 이미지 10-20장으로 LoRA를 학습하고 그 대상이 등장하는 새로운 장면을 생성합니다. 입력 이미지에 과적합하지 않으면서 정체성 보존(identity preservation)이 가장 좋았던 LoRA 차원과 학습 단계 수를 보고합니다.

핵심 용어

용어흔한 설명실제 의미
잠재 확산(Latent diffusion)"잠재에서 확산"픽셀 공간(3x512x512) 대신 VAE 잠재 공간(4x64x64)에서 DDPM 전체를 실행합니다. 연산량을 48배 절약합니다.
VAE 척도 계수(VAE scale factor)"0.18215"VAE의 원시 잠재값을 대략 단위 분산으로 재조정하는 상수입니다. SD 파이프라인에 하드코딩되어 있습니다.
분류기 없는 가이던스(Classifier-free guidance; CFG)"CFG"조건부 잡음 예측과 비조건부 잡음 예측을 섞습니다. 추론에서 가장 영향이 큰 조절 손잡이(knob)입니다.
스케줄러(Scheduler)"샘플러(Sampler)"잡음과 모델 예측을 잡음 제거된 잠재 궤적(latent trajectory)으로 바꾸는 알고리즘입니다.
LoRA"저차원 어댑터(Low-rank adapter)"기반 가중치를 건드리지 않고 어텐션 층을 파인튜닝하는 작은 차원 분해 행렬입니다.
교차 어텐션(Cross-attention)"텍스트-이미지 어텐션"잠재 토큰이 텍스트 토큰을 바라보는 어텐션입니다. 프롬프트 정보를 모든 U-Net 단계에 주입합니다.
ControlNet"구조 조건 부여(Structure conditioning)"canny, depth, pose, segmentation 같은 추가 입력으로 SD를 조향하는, 별도로 학습된 어댑터입니다.
DPM-Solver++"기본 스케줄러"2차(second-order) 결정론적 ODE 솔버입니다. 2026년 기준 낮은 단계 수(20-30)에서 품질이 가장 좋습니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 2개

skill-lora-training-setup

Write a full LoRA training config for a custom dataset, including captions, rank, batch size, and learning rate

Skill
prompt-sd-pipeline-planner

Pick SD 1.5 / SDXL / SD3 / FLUX plus scheduler and precision given a latency budget, fidelity target, and licensing constraint

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.SD의 기본 스케줄러를 DPM-Solver++ 2M Karras로 바꾸고 `num_inference_steps`를 50에서 20으로 줄였습니다. 예상 결과는 무엇인가요?

2.Stable Diffusion에서 전체 파인튜닝 대신 LoRA 파인튜닝이 인기 있는 이유는 무엇인가요?

3.같은 프롬프트를 `guidance_scale=15`로 생성했더니 색이 과포화(oversaturate)되고 타들어간 듯한 인공물(burn-in artifact)이 보입니다. 무슨 일이 일어난 것인가요?

0/3 답변 완료