Stable Diffusion with Python 파이썬으로 배우는 확산 모델
Chapter 06 목차
제2부 · 커스텀 기능 — 8·9장

작은 파일이 모델을 바꾼다

수십 MB의 LoRA는 가중치 변화를 두 저랭크 행렬로 압축하고, 수 KB의 Textual Inversion은 새 임베딩 키 하나를 찾는다 — 원본 checkpoint는 손대지 않은 채로.

LoRA Low-Rank PEFT Textual Inversion Embedding

Section 01LoRA의 발상

특정 요구에 맞춰 모델을 미세 조정하려면, 강력한 GPU 없이는 매우 느리다. 게다가 미세 조정된 모델은 보통 원본 파일과 같은 크기다. LLM 이웃 커뮤니티의 연구자들이 이 문제에 해법을 내놓았다 — Low-Rank Adaptation (LoRA).

Microsoft의 Edward J. Hu 등이 제안한 LoRA는, 원본 checkpoint를 수정 없이 동결하고, 조정된 가중치 변화만을 독립 파일에 저장한다. 이것이 LoRA 파일이다. CIVITAI와 Hugging Face에 수많은 커뮤니티 LoRA가 공유돼 있다.

LoRA — weight merge
W′ = W + α·ΔW
W 원래 UNet 어텐션 가중치 (Q, K, V) · ΔW LoRA에서 미세 조정된 가중치 변화 · α LoRA 가중치의 스케일 (보통 0~1, 1보다 약간 커도 됨) · W′ 병합된 최종 가중치
Intuition — 왜 "o"만 소문자일까 LoRA의 "o"가 소문자인 것은 Low-Rank의 약자임을 드러내기 위함이다. 핵심은 가중치 변화 ΔW가 낮은 랭크를 가진다는 — 즉 두 개의 작은 행렬로 분해될 수 있다는 발상에 있다.

Section 02저랭크 분해

LoRA 파일이 그토록 작은 이유는 ΔW가 두 개의 작은 행렬 AB로 표현될 수 있기 때문이다.

Low-rank decomposition
ΔW = A · Bᵀ
A ∈ ℝ^(n×d) n × d 행렬 · B ∈ ℝ^(m×d) m × d 행렬, Bᵀ는 d × m · 예: 6×8 행렬 ΔW는 48개의 가중치 — 이를 6×2(12개)와 2×8(16개)로 분해하면 총 28개로 줄어든다
저랭크 분해 — 48개 → 28개 STEP 01 / 5
space 재생 · → 다음 · R 리셋
Key — alpha와 rank LoRA를 훈련할 때 하이퍼파라미터 alpha를 4 같은 값으로, rank도 4로 설정한다. 실제 적용 시의 스케일 α는 α = alpha / rank로 계산된다. alpha와 rank를 모두 4로 두면 α는 1이 된다. LoRA 키 이름의 접두사 lora_te_는 텍스트 인코더에, lora_unet_은 UNet에 적용됨을 뜻한다.

Section 03Diffusers로 LoRA 사용

오픈 소스 커뮤니티 덕분에 Python으로 LoRA를 로드하는 일은 그 어느 때보다 쉽다. Diffusers는 LoRA 로딩·오프로딩을 PEFT(Parameter-Efficient Fine-Tuning) 라이브러리로 관리한다. PEFT는 모델의 모든 매개변수가 아니라 작은 부분만 미세 조정해, 소비자 하드웨어에서도 거대 모델을 다룰 수 있게 한다.

기본 설정으로 LoRA 로드
pipeline = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype = torch.float16
).to("cuda:0")

# 파이프라인에 LoRA 로드 — 모델 캐시에 없으면 자동 다운로드
pipeline.load_lora_weights(
    "andrewzhu/MoXinV1",
    weight_name = "MoXinV1.safetensors",
    adapter_name = "MoXinV1"
)
가중치를 지정하고 여러 LoRA 결합
# 두 번째 LoRA 추가
pipeline.load_lora_weights(
    "andrewzhu/civitai-light-shadow-lora",
    weight_name = "light_and_shadow.safetensors",
    adapter_name = "light_and_shadow")

# 어댑터별 가중치 지정
pipeline.set_adapters(
    ["MoXinV1", "light_and_shadow"],
    adapter_weights = [0.5, 1.0])

# 한 줄로 모든 LoRA 비활성화
pipeline.disable_lora()
Pitfall — A1111과 결과가 다르다 Diffusers의 PEFT 기반 LoRA 로딩은 A1111 WebUI 같은 도구와 구현이 다소 다르다. 같은 프롬프트·설정·가중치라도 결과가 달라질 수 있다. 책 본문에서는 LoRA 내부를 직접 파헤쳐 — state_dict에서 A·B 행렬을 찾고 레이어 이름을 매칭해 — A1111과 같은 결과를 내는 커스텀 로더를 구현한다.

Section 04Textual Inversion

Textual Inversion(TI)은 사전 학습 모델에 기능을 더하는 또 다른 방법이다. LoRA가 어텐션 가중치를 미세 조정하는 기법이라면, TI는 가중치를 건드리지 않고 새로운 임베딩 공간을 추가하는 기법이다.

TI 모델을 학습하려면 이미지가 단 3~5개면 충분하고, 결과 파일은 보통 몇 KB짜리 pt 또는 bin이다. 새 요소·개념·스타일을 통합하면서도 뛰어난 이동성을 유지한다.

Textual Inversion 로드 — 토큰 하나로 스타일 주입
pipe = StableDiffusionPipeline.from_pretrained(
    "stablediffusionapi/deliberate-v2",
    torch_dtype = torch.float16).to("cuda")

# TI 를 로드하고 토큰 이름을 지정 — 자동 다운로드
pipe.load_textual_inversion(
    "sd-concepts-library/midjourney-style",
    token = "midjourney-style")

prompt = "a futuristic city in deep space, midjourney-style"
image = pipe(prompt, num_inference_steps=50).images[0]
Intuition — "inversion"이라는 이름 TI 이름의 inversion은, 새 이름을 새 임베딩으로 역변환할 수 있음을 뜻한다. 같은 TI 파일을 token="colorful-magic-style"처럼 다른 이름으로 로드하면, 그 이름이 같은 임베딩으로 역변환되어 동일한 효과를 낸다. 토크나이저에 없는 어떤 문자열이든 플레이스홀더로 쓸 수 있다.

Section 05TI의 작동 원리

TI 학습은 간단히 말해, 대상 이미지(스타일·객체·얼굴)에 가장 잘 맞는 텍스트 임베딩을 찾는 것이다. 핵심은 현재 텍스트 인코더에 존재하지 않는 새 임베딩을 찾는 데 있다. 학습의 유일한 작업은 새 임베딩 v*를 찾고, 토큰 문자열 플레이스홀더 S*로 가리키는 것이다.

매직 박스를 여는 새 키 — TI 임베딩 탐색 STEP 01 / 5
space 재생 · → 다음 · R 리셋
Textual Inversion — embedding optimization
v* = arg min_v E[ ‖ ε − εθ( zₜ , t , cθ(y) ) ‖²₂ ]
v* 찾고자 하는 새 임베딩 · arg min 손실을 최소화하는 값 · z ~ E(x) 입력 이미지를 latent로 인코딩 · cθ(y) 텍스트 y를 임베딩으로 매핑하는 인코더 · εθ(zₜ,t,cθ(y)) UNet이 생성한 노이즈 — Stable Diffusion 학습 과정 그대로 손실을 최소화한다
Key — 매직 박스 비유 사전 학습된 UNet을 행렬 매직 박스의 더미라 하자. 하나의 키(임베딩)가 특정 패턴·스타일·객체가 든 박스를 연다. 박스의 수는 텍스트 인코더가 가진 제한된 키보다 훨씬 많다. TI 학습이란, 아직 열리지 않은 매직 박스를 여는 새 키를 만드는 것이다. 학습 출력은 보통 768개 숫자의 벡터 — 그래서 TI 파일이 그토록 작다. 학습·추론 내내 원본 checkpoint는 변하지 않는다.