텍스트만으로는 제어 신호(control signal)가 둔합니다. ControlNet은 사전 학습된 디퓨전 모델(pretrained diffusion model)을 복제(clone)해서 깊이 맵(depth map), 포즈 스켈레톤(pose skeleton), 스크리블(scribble), 에지 이미지(edge image)로 조향(steer)하게 합니다. LoRA(Low-Rank Adaptation)는 20억(2B) 파라미터 모델을 약 1,000만 파라미터만 학습해 미세조정(fine-tune)할 수 있게 합니다. 둘이 합쳐지면서 Stable Diffusion은 장난감 수준에서 2026년의 모든 에이전시(agency)에서 출시되는 이미지 파이프라인(pipeline)으로 탈바꿈했습니다.
유형: Build
언어: Python
선수 지식: Phase 8 · 07 (잠재 디퓨전(Latent Diffusion)), Phase 10 (밑바닥부터 만드는 LLMs — LoRA 기반 지식)
예상 시간: 약 75분
문제
"a woman in a red dress walking a dog on a busy street"와 같은 프롬프트(prompt)는 모델에게 강아지가 어디에 있는지, 여성이 어떤 포즈(pose)인지, 거리의 원근감(perspective)이 어떤지를 전혀 알려주지 않습니다. 이미지 사양(image specification)에서 텍스트가 고정해 주는 정보는 약 10%에 불과합니다. 나머지는 시각 신호(visual signal)에 해당하며, 이런 정보는 말로 효율적으로 설명하기 어렵습니다.
포즈, 깊이(depth), 캐니(canny), 세그멘테이션(segmentation) 등 각 신호마다 새로운 조건부 모델(conditional model)을 처음부터(scratch) 학습하는 것은 비용이 지나치게 큽니다. 우리는 2.6B 파라미터 규모의 SDXL 백본(backbone)을 고정(freeze)한 채로 두고, 조건(conditioning)을 읽는 작은 사이드 네트워크(side-network)를 붙여 백본의 중간 특성(intermediate feature)을 살짝 보정(nudge)하고 싶습니다. 이것이 바로 ControlNet입니다.
또한 모델에게 새로운 개념(concept) — 예를 들어 내 얼굴, 우리 회사 제품, 특정 스타일 — 을 가르치되 전체 모델을 다시 학습시키는 일은 피하고 싶습니다. 우리가 원하는 것은 100배 더 작은 변화량(delta)입니다. 이것이 바로 LoRA입니다. 기존 어텐션(attention) 가중치(weight)에 꽂아 넣는 저랭크(low-rank) 어댑터(adapter)입니다.
ControlNet + LoRA + 텍스트는 2026년 실무자(practitioner)의 도구 키트(toolkit)입니다. 대부분의 프로덕션 이미지 파이프라인은 SDXL / SD3 / Flux 베이스(base) 위에 LoRA 2–5개, ControlNet 1–3개, 그리고 IP-Adapter를 층(layer)처럼 쌓아 올립니다.
사전 테스트
2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요
1.ControlNet이 스킵 연결을 0으로 초기화된 1x1 컨볼루션(zero convolution)으로 시작하는 이유는 무엇인가요?
2.LoRA는 가중치 행렬 W(d x d)의 전체 미세조정을 저랭크 분해 W' = W + B @ A (A는 r x d, B는 d x r)로 대체합니다. 이것이 파라미터 효율 면에서 극적으로 유리한 이유는 무엇인가요?
0/2 답변 완료
개념
ControlNet (Zhang et al., 2023)
사전 학습된 SD를 가져옵니다. U-Net의 인코더(encoder) 절반을 *복제(clone)*합니다. 원본(original)은 고정합니다. 복제본은 에지, 깊이, 포즈 같은 추가 조건 입력(conditioning input)을 받도록 학습합니다. 복제본은 제로 컨볼루션(zero-convolution) 스킵 연결(skip connection), 즉 0으로 초기화된 1×1 컨볼루션을 통해 원본의 디코더(decoder) 절반에 다시 연결됩니다. 처음에는 아무 동작도 하지 않는(no-op) 상태로 시작해 학습하면서 변화량을 익혀 갑니다.
제로 컨볼루션 초기화 덕분에 ControlNet은 학습 전에도 항등(identity)처럼 동작하므로, 학습이 시작되기 전에 베이스를 해치지 않습니다. 표준 디퓨전 손실(diffusion loss)로 100만 쌍의 (프롬프트, 조건, 이미지) 삼중항(triple)을 학습합니다.
모달리티(modality)별 ControlNet은 작은 사이드 모델(side model) 형태로 배포됩니다. SDXL 기준 약 360M, SD 1.5 기준 약 70M 정도입니다. 추론(inference) 시점에는 여러 ControlNet을 합성(compose)할 수 있습니다.
features += weight_a * control_a(depth) + weight_b * control_b(pose)
LoRA (Hu et al., 2021)
모델 안의 임의 선형 계층(linear layer) W ∈ R^{d×d}에 대해 W를 고정한 뒤, 다음과 같이 저랭크 변화량을 더합니다.
W' = W + ΔW, ΔW = B @ A, A ∈ R^{r×d}, B ∈ R^{d×r}
여기서 r << d입니다. 어텐션에는 보통 랭크(rank) 4–16이 표준이고, 무거운 미세조정에는 랭크 64–128이 쓰입니다. 새로 추가되는 파라미터 수는 d²이 아니라 2 · d · r입니다. SDXL 어텐션에서 d=640, r=16이라면 어댑터당 41만(410k) 파라미터 대신 2만(20k) 파라미터로 줄어 약 20배 감소합니다. 전체 모델 기준으로 보면 LoRA는 보통 20–200MB 수준이고, 베이스 모델은 5GB에 달합니다.
추론 시에는 LoRA를 스케일링(scale)할 수 있습니다. W' = W + α · B @ A이고, α = 0.5–1.5가 일반적인 범위입니다. 여러 LoRA는 가법적으로(additively) 쌓이지만, 비선형(non-linear) 상호작용이 생길 수 있다는 점에 주의해야 합니다.
IP-Adapter (Ye et al., 2023)
텍스트와 함께 이미지를 조건으로 받는 작은 어댑터입니다. CLIP 이미지 인코더(CLIP image encoder)로 이미지 토큰(image token)을 만들고, 텍스트 토큰과 함께 크로스 어텐션(cross-attention)에 주입(inject)합니다. 베이스 모델당 약 20MB입니다. LoRA 없이도 "이 참조 이미지의 스타일로 생성해 줘"와 같은 작업을 가능하게 해 줍니다.
합성 가능성(Composability) 매트릭스
도구
제어 대상
크기
사용 시점
ControlNet
공간 구조(spatial structure): 포즈, 깊이, 에지
70–360MB
정확한 레이아웃과 구도(composition)
LoRA
스타일, 주제(subject), 개념
20–200MB
개인화(personalization), 스타일
IP-Adapter
참조 이미지의 스타일이나 주제
20MB
텍스트로는 외형(look)을 설명하기 어려울 때
Textual Inversion
새 토큰 하나로 표현되는 단일 개념
10KB
레거시(legacy), 대부분 LoRA로 대체됨
DreamBooth
주제에 대한 전체 미세조정
2–5GB
강한 정체성(identity), 높은 연산 비용
T2I-Adapter
더 가벼운 ControlNet 대안
70MB
엣지 디바이스(edge device), 추론 예산이 빠듯할 때
ControlNet은 공간(spatial) 쪽에 가깝고, LoRA는 의미(semantic) 쪽에 가깝습니다. 보통 둘을 함께 사용합니다.
직접 만들기
code/main.py는 두 메커니즘을 1차원(1-D)에서 시뮬레이션합니다.
LoRA. 사전 학습된 선형 계층 W를 고정합니다. 저랭크 B @ A를 학습시켜 W + BA가 목표(target) 선형 계층과 일치하게 만듭니다. 랭크-1 보정(correction)은 r = 1만으로도 완벽히 학습할 수 있음을 보입니다.
ControlNet-lite. "고정된 베이스" 예측기(predictor)와 추가 신호(signal)를 읽는 "사이드 네트워크"를 둡니다. 사이드 네트워크의 출력은 0으로 초기화된 학습 가능한 스칼라 게이트(gate)를 통과합니다(우리 버전의 제로 컨볼루션입니다). 학습이 진행되면서 게이트 값이 올라가는 모습을 관찰합니다.
Step 1: LoRA math
deflora(W, A, B, x, alpha=1.0):
# W는 고정되어 있고, A와 B는 학습 가능한 저랭크 분해 인수입니다.return [W[i][j] * x[j] for i, j in ...] + alpha * (B @ (A @ x))
Step 2: zero-init side network
side_out = control_net(x, condition)
gated = gate * side_out # gate는 0으로 초기화합니다.
h = base(x) + gated
스텝 0에서는 출력이 베이스와 완전히 동일합니다. 학습 초반에는 gate가 천천히 업데이트되기 때문에, 한 번에 큰 변화가 생기는 파국적 표류(catastrophic drift)를 피할 수 있습니다.
함정(Pitfalls)
LoRA를 과도하게 키우기.α = 2나 α = 3은 "효과를 더 세게" 만들고 싶을 때 흔히 쓰는 꼼수이지만, 과도하게 스타일링되었거나(over-stylized) 망가진 결과물(broken output)을 만들기 쉽습니다. α ≤ 1.5를 유지하세요.
ControlNet 가중치 충돌. Pose ControlNet을 가중치 1.0으로, Depth ControlNet도 가중치 1.0으로 함께 사용하면 보통 효과가 과도해집니다(overshoot). 가중치 합이 약 1.0인 것이 안전한 기본값입니다.
잘못된 베이스에 적용된 LoRA. SDXL용 LoRA는 어텐션 차원(attention dimension)이 다르기 때문에 SD 1.5에서는 조용히 아무 효과도 내지 않는(silent no-op) 상태가 됩니다. Diffusers 0.30+ 버전부터 경고(warning)를 표시합니다.
Textual Inversion 표류. 한 체크포인트(checkpoint)에서 학습한 토큰은 다른 체크포인트로 옮기면 크게 표류(drift)합니다. 이런 면에서 LoRA가 더 이식성(portable)이 좋습니다.
LoRA 가중치 병합(merge)과 저장. LoRA를 베이스 모델 가중치에 미리 구워 넣으면(bake) 추론 시 추가 연산이 없어 빨라지지만, 실행 시점에 α를 조정할 수 없게 됩니다. 두 버전을 모두 보관해 두는 것이 좋습니다.
사용해보기
목표
2026년 파이프라인
브랜드의 미술 스타일 재현
큐레이션된 약 30장의 이미지로 랭크 32 LoRA 학습
생성 이미지에 내 얼굴 넣기
DreamBooth 또는 LoRA + IP-Adapter-FaceID
특정 포즈 + 프롬프트
ControlNet-Openpose + SDXL + 텍스트
깊이를 인식하는 구도
ControlNet-Depth + SD3
참조 + 프롬프트
IP-Adapter + 텍스트
정확한 레이아웃
ControlNet-Scribble 또는 ControlNet-Canny
배경 교체
ControlNet-Seg + 인페인팅(Inpainting) (Lesson 09)
1스텝으로 빠르게 스타일링
SDXL-Turbo 위에 LCM-LoRA
산출물 만들기
outputs/skill-sd-toolkit-composer.md를 저장합니다. 이 스킬(skill)은 작업(task)과 입력 자산(input asset) — 프롬프트, 선택적 참조 이미지, 선택적 포즈, 선택적 깊이, 선택적 스크리블 — 을 받아 도구 스택(tool stack), 가중치, 그리고 재현 가능한(reproducible) 시드(seed) 프로토콜을 출력합니다.
연습문제
쉬움.code/main.py에서 LoRA 랭크 r을 1에서 4까지 변화시켜 봅니다. 랭크-2 목표 변화량(target delta)을 정확히 맞추려면 어느 랭크에서부터 가능한지 확인합니다.
중간. 두 가지 서로 다른 목표 변환(target transform)에 대해 별개의 LoRA를 학습합니다. 두 LoRA를 함께 로드(load)해서 그 가법적 상호작용(additive interaction)을 보이고, 언제 선형성(linearity)이 깨지는지 관찰합니다.
어려움. diffusers를 사용해 다음 스택을 구성합니다: SDXL-base + Canny-ControlNet (가중치 0.8) + 스타일 LoRA (α 0.8) + IP-Adapter (가중치 0.6). 스택 가중치를 바꿔 가며 FID와 프롬프트 충실도(prompt adherence) 사이의 트레이드오프(trade-off)를 측정합니다.
핵심 용어
용어
흔한 설명
실제 의미
ControlNet
"공간 제어(spatial control)"
복제된 인코더 + 제로 컨볼루션 스킵 연결; 조건 이미지를 읽습니다.
제로 컨볼루션(zero convolution)
"항등으로 시작한다"
0으로 초기화된 1×1 컨볼루션; ControlNet이 처음에는 no-op로 시작합니다.
LoRA
"저랭크 어댑터(low-rank adapter)"
W + B @ A, r << d; 전체 미세조정 대비 파라미터 수가 100배 적습니다.
랭크 r
"다이얼(the knob)"
LoRA 압축률; 일반적으로 4–16, 무거운 개인화에는 64+를 사용합니다.
α
"LoRA 강도(LoRA strength)"
실행 시점에 LoRA 변화량을 스케일링하는 값.
IP-Adapter
"참조 이미지(reference image)"
CLIP 이미지 토큰을 사용하는 작은 이미지 조건화 어댑터.
DreamBooth
"주제에 대한 전체 미세조정(full subject fine-tune)"
주제 이미지 약 30장으로 모델 전체를 학습합니다.
Textual Inversion
"새 토큰(new token)"
새로운 단어 임베딩(word embedding)만 학습; 레거시로 대부분 LoRA에 의해 대체됨.
프로덕션 노트: LoRA 스왑(swap), ControlNet 레인(lane), 멀티 테넌트 서빙(multi-tenant serving)
실제 텍스트 투 이미지(text-to-image) SaaS는 동일한 베이스 체크포인트 위에서 수백 개의 LoRA와 십여 개의 ControlNet을 함께 서빙(serve)합니다. 이 서빙 문제는 LLM 멀티 테넌시(multi-tenancy)와 매우 닮아 있습니다. 프로덕션 관련 문헌은 이를 LLM 사례에서 연속 배칭(continuous batching)과 LoRAX / S-LoRA 같은 형태로 다룹니다.
LoRA는 병합(merge)하지 말고 핫 스왑(hot-swap)하세요.W' = W + α·B·A를 베이스에 병합해 두면 스텝당 추론이 3–5% 빨라지지만, α와 베이스가 고정됩니다. LoRA는 랭크-r 변화량 형태로 VRAM에 뜨거운(hot) 상태로 유지하세요. diffusers는 요청별 활성화를 위해 pipe.load_lora_weights()와 pipe.set_adapters([...], adapter_weights=[...])를 제공합니다. 스왑 비용은 2 · d · r · num_layers 가중치 정도로 MB 단위이며, 1초 미만으로 끝납니다.
ControlNet은 두 번째 어텐션 레인(attention lane)으로 동작합니다. 복제된 인코더가 베이스와 병렬로 실행됩니다. ControlNet 두 개를 각각 가중치 1.0으로 사용하면, 한 번 병합된 패스가 아니라 스텝당 두 번의 추가 정방향(forward) 패스가 늘어납니다. 배치 사이즈 여유는 제곱(quadratic) 비율로 줄어듭니다. 활성화된 ControlNet 하나당 스텝 비용이 약 1.5배 증가한다고 계산해 두세요.
양자화된 LoRA도 함께. 베이스를 양자화(quantize)했다면(Lesson 07, 8GB에서의 Flux 참조) LoRA 변화량도 8비트(8-bit)나 4비트(4-bit)로 깔끔하게 양자화됩니다. QLoRA 스타일 로딩을 사용하면 4비트 Flux 베이스 위에 5–10개의 LoRA를 메모리 폭주 없이 쌓을 수 있습니다.
Flux 한정: Niels의 Flux-on-8GB 노트북은 베이스를 4비트로 양자화합니다. 그 위에 스타일 LoRA(pipe.load_lora_weights("user/style-lora"))를 weight_name="pytorch_lora_weights.safetensors"로 쌓아도 정상 동작합니다. 이는 2026년 대부분의 SaaS 에이전시가 출시(ship)에 사용하는 레시피(recipe)입니다.