2023. 3. 14. 01:02ㆍDeep Learning/Stable Diffusion

Introduction
지난 포스트에서는 Stable Diffusion의 원형이 되는 denoising diffusion probabilistic models(DDPM)에 대해 살펴보며 diffusion model의 기본적인 이미지 생성 원리에 대해 알아봤습니다. 하지만 이것만으로는 실제 Stable Diffusion이 하는 것처럼 text-to-image나 image-to-image 작업을 수행할 수는 없었습니다. 이전에 언급했다시피 text-to-image는 이미지의 데이터 공간 상의 임의의 위치에 있는 노이즈를 우리가 원하는 이미지가 있는 위치로 조금씩 이동시키는 과정인데, 이를 위해서는 많은 준비물이 필요하기 때문입니다.
느닷없지만 등산을 한다고 가정해 볼까요? 우리는 거대한 산의 어느 위치에 서 있고, 경사를 오르는 법은 알고 있지만 장비가 하나도 없습니다. 사실 이미지를 생성하기 위해 일정한 알고리즘으로 reverse process를 진행하는 건 높은 봉우리에 올라가기 위해 한 발자국 씩 경사를 오르는 것과 같습니다. 그러다가 어느 봉우리의 정상에 오르게 되면 이미지 하나를 생성한 것과 같겠죠? 문제는 그게 내가 목표로 하던 봉우리냐는 것입니다. 눈앞에 보이는 아무 경사나 오른다고 내가 처음에 목표로 하던 봉우리의 정상에 도달한다는 보장은 전혀 없습니다.
그래서 우리는 다음과 같은 도구들을 준비하고자 합니다. 목적지로 가는 방향을 알려주는 나침반, 가볍고 편한 등산화, 그리고 가파른 경사를 올라 지름길로 갈 수 있도록 도와주는 손도끼입니다. DDPM은 셋 다 없는 상태로 봉우리를 오르는 것과 마찬가지여서 내가 원하는 목적지로 갈 방법이 전혀 없었습니다. 심지어 올라가는 과정도 굉장히 험난했죠.
이번 포스트에서는 우리에게 필요한 세 가지 준비물 중 첫 번째, 목적지로 가는 방향을 알려주는 나침반에 대해 알아보고자 합니다. 생성 모델이 원하는 데이터를 생성하도록 조건을 걸어주는 것을 conditioning이라고 하는데, Stable Diffusion에서는 말 그대로 나침반처럼 reverse process 과정에서 원하는 이미지가 있는 방향과 원하지 않는 이미지가 있는 방향을 모두 알려줌으로써 가우시안 노이즈를 원하는 위치로 보내는 방식으로 conditioning을 합니다. 그럼 이제 그 원리를 바로 살펴보도록 하겠습니다.
Contrastive Language-Image Pre-Training (CLIP)
본격적인 이야기를 꺼내기 전에, 먼저 텍스트가 어떻게 diffusion model이 이해할 수 있는 형태로 변환되고 입력되는지에 대해 알아야 합니다. 이미지의 경우 해상도가 $W \times H$인 RGB 이미지라면 각 픽셀의 값을 $H \times W \times 3$ 크기의 텐서로 저장하여 손쉽게 다룰 수 있지만, 텍스트의 경우는 어떨까요?
신경망이 다룰 수 있는 것은 오직 실수 뿐이므로 텍스트도 어떻게든 수의 형태로 바꿀 수 있어야 합니다. 가장 쉬운 방법은 텍스트에 포함된 모든 문자를 유니코드로 인코딩하는 방식입니다. 하지만 사람이 문장을 읽을 때 문자 하나하나를 읽는다기보다는 단어 단위로 끊어가며 그 의미를 이해하는 것처럼, 모델에 텍스트를 넣어줄 때도 단어 단위로 인코딩을 해주면 그 뜻을 더 효율적으로 이해할 수 있을 것입니다. 이런 식으로 단어 단위로 인코딩하는 걸 토큰화(tokenizing)라고 부르며, 인코딩 된 단어 하나하나는 토큰(token)이라고 부릅니다. 예시로 몇몇 문장을 토큰화 해보자면 다음과 같습니다.
"A white cat" → $[320, 1579, 2368]$
"A white bird" → $[320, 1579, 3329]$
"A black cat on the bed" → $[320, 1449, 2368, 525, 518, 2722]$
문장에 있는 단어들이 특정한 자연수로 일대일 대응되며, 단어의 수만큼 토큰이 생성된다는 걸 확인할 수 있습니다. 이 과정에서 문장의 시작과 끝을 알리기 위해 <|startoftext|>와 <|endoftext|>라는 단어에 해당하는 토큰(49406, 49407)이 양 옆에 붙게 됩니다.
"A white cat" → $[49406, 320, 1579, 2368, 49407]$
"A white bird" → $[49406, 320, 1579, 3329, 49407]$
"A black cat on the bed" → $[49406, 320, 1449, 2368, 525, 518, 2722, 49407]$
또한 보통 신경망에는 batch size라는 수만큼 여러 개의 데이터가 묶여 한번에 입력되는데, 이 때 데이터의 사이즈는 모두 통일되어야 하므로 위의 예시들은 다음과 같이 뒤에 <|endoftext|>를 채우는 방식으로 사이즈를 맞춥니다.1
"A white cat" → $[49406, 320, 1579, 2368, 49407, 49407, 49407, 49407]$
"A white bird" → $[49406, 320, 1579, 3329, 49407, 49407, 49407, 49407]$
"A black cat on the bed" → $[49406, 320, 1449, 2368, 525, 518, 2722, 49407]$
이제 이미지가 $H \times W \times 3$ 크기의 텐서로 표현되듯이 텍스트도 일정 길이의 벡터로 표현이 가능해졌습니다. 하지만 한 가지 단계가 더 남아 있습니다. 딥러닝 모델들은 여러 가지 이유로 RAW한 데이터를 그대로 사용하지 않고 그 안에서 여러 가지 특징적인 정보들을 추출한 뒤 이를 활용하는 방식을 사용합니다. 이 과정을 임베딩(embedding)이라고 하며, Stable Diffusion의 경우 텍스트에서 이런 특징적인 정보를 벡터의 형태로 뽑아줄 인코더로 CLIP2이라는 기법으로 학습된 인코더를 활용합니다.
CLIP은 Contrastive Language-Image Pre-Training의 약자로, 원래는 학습용 이미지와 그를 설명하는 텍스트 간의 유사도가 높아지도록 학습을 진행하는 방법입니다. 사실 이 논문은 이렇게 사전 학습된 이미지 인코더를 어떻게 활용할지를 주로 다뤘고 텍스트 인코더에는 상대적으로 덜 주목했지만, 마찬가지로 openAI에서 개발된 DALL·E 2가 이를 통해 학습된 텍스트 인코더를 text-to-image에 활용해 여러모로 재미를 보자 Stable Diffusion도 이를 가져와서 쓰게 된 것으로 보입니다.

또한, 설정창에서 조절할 수 있는 CLIP skip이라는 패러미터는 텍스트 인코더가 얼마나 많은 레이어를 써서 텍스트 임베딩을 출력할지와 관련 있습니다. CLIP skip이 $n$이라면 인코더의 마지막으로부터 $n$번째 레이어의 출력을 취한다는 것으로, $n=1$은 모든 레이어를 쓰는 가장 일반적인 상황을 의미합니다. 몇몇 레이어를 스킵하고 추출된 텍스트 임베딩은 상대적으로 인코딩이 덜 되기 때문에 해당 키워드에 대해 덜 상세한 정보를 담게 됩니다.
대부분 CLIP skip을 2로 설정했더니 결과물이 더 좋게 나오는 경험을 해보셨을 겁니다. 이는 임베딩이 느슨하게 되어 같은 키워드에 대해 더 넓은 범위의 결과물이 나오고 그만큼 상대적으로 다양한 결과물이 나오기 때문입니다. 그림 2를 보시면 CLIP skip이 $5$일 때는 프롬프트의 50 y.o라는 키워드조차 무시될 만큼 텍스트 임베딩이 느슨하게 되는 것을 확인할 수 있습니다.
Prompt weighting

저는 web UI의 가장 강력한 기능들 중 하나로 prompt weighting을 꼽습니다. 만약 그림 3처럼 A dog (wearing Spiderman suit:1.2)라는 프롬프트를 입력한다면, web UI는 이를 'A dog'와 'wearing Spider-Man's suit'라는 두 문장으로 나누고, 뒷 문장에 1.2배만큼 weight를 줍니다. 그만큼 뒷 문장에 더 집중해서 conditioning을 할 수 있게 하는 것이지요. 이건 단순한 예시지만, 프롬프트를 정말 잘 쓸 줄 아는 장인들의 솜씨를 보면 정말 경이로울 정도로 weighting을 디테일하게 주는 모습을 볼 수 있습니다.
이러한 weighting은 토큰화 된 텍스트를 CLIP 인코더를 통해 임베딩하는 과정에서 일어납니다. 앞서 설명한 전처리 과정을 거쳐 프롬프트는 양쪽 끝의 <|startoftext|>와 <|endoftext|>를 포함해 77개의 토큰으로 변환되는데, 하나의 토큰은 CLIP 인코더를 거쳐 768 개의 값을 가지는 벡터가 됩니다. 따라서 77개의 토큰이 임베딩 된 결과는 $77 \times 768$ 크기의 행렬로 표현될 수 있습니다. 쉽게 말해, 이 행렬은 길이가 768인 임베딩 벡터 77개를 세로로 쌓아서 만들어진 것입니다.
이렇게 만들어진 행렬의 행들 중에서 wearing, Spiderman, suit 토큰에 해당하는 행에만 1.2를 곱해 다른 임베딩보다 상대적으로 큰 값을 갖도록 함으로써 prompt weighting은 끝납니다. 이 경우에는 첫 번째 행이 <|startoftext|>에 대응된다는 걸 생각하면 4, 5, 6행에 1.2가 곱해진다고 생각하면 될 것입니다.
Negative prompt

Web UI의 또다른 막강한 기능은 바로 부정 프롬프트(negative prompt)입니다. 일반적인 프롬프트가 reverse process 과정에서 '결과 이미지가 이렇게 됐으면 좋겠다'라는 지시를 내리는 것이라면, 부정 프롬프트는 그와 반대로 '결과 이미지가 이렇게 되지 않았으면 좋겠다'라는 지시를 내리는 것입니다. 부정 프롬프트도 마찬가지로 토큰화를 거쳐 CLIP 텍스트 인코더로 임베딩이 된 상태로 활용됩니다.
그림 4의 왼쪽 이미지는 A dog wearing Spiderman suit라는 프롬프트로만 생성한 이미지입니다. 눈과 입이 사람과 같이 생겨서 굉장히 불쾌한 형상을 보여주고 있습니다. 오른쪽 이미지는 이를 고치기 위해 부정 프롬프트로 low quality, human eyes, human mouse를 입력한 이미지인데, 훨씬 나아진 모습을 확인할 수 있습니다. 이처럼 '없어야 하는 것'을 명시하는 건 긍정 프롬프트 만으로는 쉽지 않기 때문에 제대로 된 이미지 생성을 위해서는 부정 프롬프트가 항상 필요합니다.
Cross-attention
지난 포스트를 잘 따라오셨다면, reverse process에서 UNet의 첫 번째 레이어에 입력되는 것은 가우시안 노이즈로 열화된 이미지라는 것을 알 수 있습니다. 한편 텍스트로 conditioning을 한다면 UNet 어딘가에 텍스트 임베딩도 입력하긴 해야 한다는 건데, 어떤 방식으로 입력되는 걸까요?

DDPM의 UNet은 그림 5와 같은 레이어들을 가진 블록을 여러 개 쌓은 구조로 되어 있습니다. 이 중에서 우리가 주목할 것은 가장 마지막 레이어인 attention입니다. 트랜스포머3의 기반이 되어 말 그대로 세간의 주목을 한 눈에 받게 된 이 연산은 후에 트랜스포머 기반의 GPT-34나 ChatGPT와 같은 거대 언어 모델(Large Language Model)이 게임 체인저로 부각되면서 더더욱 중요도가 올라가게 된 연산 방식입니다.

Attention 연산은 그림 6과 같이 연산되는데, 이를 입력 데이터 $X$에 대한 수식으로 표현하자면 아래와 같습니다.
$$\text{Attention}(Q, K, V) = \text{softmax}\left( \frac{QK^T}{\sqrt{d_k}} \right) V$$
여기서 $Q$(Query), $K$(Key), $V$(Value)가 뭔지에 따라 attention은 두 종류로 나뉩니다. 다음과 같이 정의된다면 이를 self-attention이라고 부릅니다.
$$Q = XW_{Q}, K = XW_{K}, V = XW_{V}$$
즉, 입력 데이터 $X$를 세 가지 행렬로 각각 곱해준 뒤 attention 연산을 수행해준다면 self-attention이 되는 것입니다. DDPM의 UNet은 이 방식을 사용하며, 중요한 건 아니지만 CLIP의 텍스트 인코더도 self-attention을 핵심 layer로 사용하고 있습니다.
$$Q = XW_{Q}, K = YW_{K}, V = YW_{V}$$
한편, 위와 같이 key와 value를 또다른 입력 데이터 $Y$로부터 계산하여 attention 연산을 진행하는 방식이 있습니다. 바로 이걸 cross-attention이라고 부릅니다. Stable Diffusion의 UNet이 텍스트 임베딩을 입력받는 방식도 이와 같습니다. 즉, $X$는 reverse process 과정에서 복원될 이미지에 해당할 것이고, $Y$는 텍스트 임베딩에 해당하게 될 것입니다. Cross-attention을 통해 이미지 데이터 $X$에는 텍스트라는 완전히 다른 영역의 데이터 $Y$가 잘 섞이게 되고, 이로 인해 reverse process는 그 텍스트에 강한 영향을 받아 동작하게 되는 셈입니다.
안타깝게도 cross-attention을 이용한 conditioning만으로는 좋은 결과를 얻을 수가 없습니다. 그 이유는 너무 복합적이라 여기에 언급할 수 없지만, 그 결과는 바로 확인할 수 있습니다. 예를 들어 CFG scale을 1로 맞춘 뒤 A dog wearing Spiderman suit라는 프롬프트로 이미지를 생성하면 cross-attention만을 이용해 conditioning을 하는 것과 같아지는데, 아래의 그림 7과 같이 굉장히 좋지 않은 결과가 나온다는 걸 확인할 수 있습니다.

보시면 아시겠지만 개도 없고, 딱히 스파이더맨 슈트처럼 보이는 것도 없는 뚱딴지 같은 이미지가 생성되었습니다. 이렇듯 cross-attention만으로는 reverse process에 충분한 conditioning을 줄 수 없기 때문에 또다른 방법이 필요합니다. 이 때 쓰는 방법이 우리가 CFG 스케일을 건드려 가며 conditioning의 정도를 조절하는 classifier-free guidance(CFG)입니다.
Classifier-free Guidance (CFG)

이제 본격적으로 그 '나침반'의 작동 원리와 이를 활용하는 방법을 알아볼 시간입니다. 우선 작동 원리를 한 문장으로 정리하자면 다음과 같습니다.
부정 프롬프트로 생성된 이미지로부터는 멀어지면서,
긍정 프롬프트로 생성된 이미지로는 가까워지는 방향으로 유도한다.
간단하면서도 명쾌하지만, 실제로 어떻게 구현되는지는 또다른 문제입니다. 우선 Stable Diffusion의 UNet을 $\epsilon_{\theta}(\mathrm{x}_{t}, t, c)$라고 표기하기로 합시다. 여기서 $\mathrm{x}_{t}$는 현재 step에서 복원 중인 이미지, $t$는 현재 step, 그리고 $c$는 텍스트 임베딩을 의미합니다. 긍정 프롬프트를 $c_{\text{pos}}$, 부정 프롬프트를 $c_{\text{neg}}$로 나타낸다면$\epsilon_{\theta}(\mathrm{x}_{t}, t, c_{\text{pos}})$와 $\epsilon_{\theta}(\mathrm{x}_{t}, t, c_{\text{neg}})$를 통해 각각 긍정 프롬프트와 부정 프롬프트만으로 생성된 이미지 $\mathrm{x}_{t}^{\text{pos}}$와 $\mathrm{x}_{t}^{\text{neg}}$를 얻을 수 있습니다. 그러면 현위치 $\mathrm{x}_{t}$로부터 각 이미지로 향하는 방향을 벡터 $\vec{\mathrm{x}}_{t}^{\text{pos}}$와 $\vec{\mathrm{x}}_{t}^{\text{neg}}$로 표현할 수 있을 것입니다.
'나침반'은 $\mathrm{x}_{t}^{\text{pos}}$를 향하면서, $\mathrm{x}_{t}^{\text{neg}}$로부터는 벗어나는 방향을 제시해야 한다고 했죠? 다시 말해 $\vec{\mathrm{x}}_{t}^{\text{pos}}$를 향하면서 $\vec{\mathrm{x}}_{t}^{\text{neg}}$의 반대 방향으로 향하도록 하면 된다는 것이고, 이 말은 나침반이 제시할 방향은 $\vec{\mathrm{x}}_{t}^{\text{pos}} - \vec{\mathrm{x}}_{t}^{\text{neg}}$이 된다는 것입니다. 이 방향으로 지금 있는 자리 $\mathrm{x}_{t}$에서 움직여도 좋지만, 더 확실하게 $\mathrm{x}_{t}^{\text{neg}}$로부터 멀어지는 방법이 있습니다. 바로 $\mathrm{x}_{t}^{\text{neg}}$로 우선 이동한 뒤 $\vec{\mathrm{x}}_{t}^{\text{pos}} - \vec{\mathrm{x}}_{t}^{\text{neg}}$ 방향으로 이동하는 것입니다. 가지 말아야 할 곳을 등지고 가야할 곳을 향해 앞으로 나아가면 더 확실하겠죠? 이런 식으로 reverse process의 매 step마다 guidance를 주는 방식을 Classifier-free guidance(CFG)5라고 부르는 것입니다.

여기에 CFG라는 나침반의 말을 얼마나 들을 것인가를 나타내는 scale factor $\eta$가 붙는데, 이것이 바로 우리가 아는 CFG scale입니다. 일반적으로 CFG scale은 작을수록 퀄리티는 떨어지지만 다양한 이미지가 생성되고, (어느 수치까지는) 높을수록 퀄리티는 좋아지지만 천편일률적인 이미지가 생성된다는 게 알려져 있습니다. 이는 나침반이 가리키는 방향을 맹목적으로 믿고 가면 정확히 목표로 했던 봉우리 단 하나만 오를 수 있던지 그 주변 몇개의 봉우리만 오를 수 있지만, 반대로 나침반이 어디를 가리키던 조금씩만 따라간다면 정확히 원하는 곳에 가기 쉽지 않고 심지어는 원하지 않는 봉우리에 갈 수도 있지만 더 다양한 곳을 들를 수 있다고 해석하면 될 것 같습니다.
Conclusion
이번 포스트에서는 원하는 이미지에 대한 정보가 담긴 텍스트가 임베딩 되어 UNet에 입력되는 방식과 CFG를 활용해 reverse process를 부정 네거티브와 긍정 네거티브로 효과적으로 conditioning 하는 방식에 대해 살펴봤습니다. 앞서 언급했던 세 가지 준비물 중 첫 번째인 나침반이 갖춰진 것이지만, 가는 방향을 알더라도 한 걸음 한 걸음 나아가는 게 너무 힘들다면 여전히 쉽지 않은 여정일 것입니다. 다음 포스트에서는 latent diffusion model이라는 가볍고 편한 등산화를 꺼내보도록 하겠습니다.
- 이는 1.x 버전 기준이므로 2.x 버전에서는 차이가 있을 수 있습니다. [본문으로]
- 출처: Radford, Alec, et al. "Learning transferable visual models from natural language supervision." International conference on machine learning. PMLR, 2021. [본문으로]
- 출처: Vaswani, Ashish, et al. "Attention is all you need." Advances in neural information processing systems 30 (2017). [본문으로]
- 출처: Brown, Tom, et al. "Language models are few-shot learners." Advances in neural information processing systems 33 (2020): 1877-1901. [본문으로]
- 출처: Ho, Jonathan, and Tim Salimans. "Classifier-free diffusion guidance." arXiv preprint arXiv:2207.12598 (2022). [본문으로]
'Deep Learning > Stable Diffusion' 카테고리의 다른 글
| [Stable Diffusion] 1. 모든 것의 시작: DDPM과 UNet (0) | 2023.03.10 |
|---|