에필로그
처음부터 돌아보기
마지막 시간. 화면에는 ComfyUI 의 KSampler 마디가 떠 있다. 시드, 걸음 수, cfg, 샘플러 이름, 걸음 일정 이름, positive, negative, latent_image.

마디를 짚기 전에 처음으로 돌아가 볼게요. 첫 시간에 칠판에 세로 막대 하나를 그었죠. 기억나요?

p(라벨 ∣ 사진)과 p(사진 ∣ 라벨)이요. 앞의 것은 막대 몇 개로 끝나는데, 뒤의 것은 그림 공간 전체에 퍼진 구름이라 표로 적을 수가 없었어요. 생성기는 그 구름에서 하나씩 뽑는 기계여야 했고요.

그 구름에서 뽑으라고, 손글씨 숫자를 97% 넘게 맞히는 MLP 에게 「3을 하나 그려 봐」라고 했죠.

기억나요. 제곱 오차로 배우니까 3들의 평균이 나왔잖아요. 흐린 회색 3이요. 정답들의 평균은 아무도 그린 적 없는 3이었고요. 라벨이 정하는 건 흩어짐의 21%뿐이고, 나머지는 데이터에 원래 있는 흩어짐이었어요.

그래서 그림 자신을 정답으로 삼는 오토인코더를 만들었는데, 가운데 칸이 어디에 놓일지 아무도 정하지 않아서 새 그림을 뽑을 자리가 없었어. VAE 는 그 칸을 구름으로 바꾸고 N(0, I) 쪽으로 당겨서 뽑을 자리를 만들었고.

그 VAE 가 그린 그림은 어땠어요?

여전히 흐릿했어요. 잠재 변수가 다 정하지 못한 것을 디코더가 칸마다 따로 동전을 던져서 메우는 수밖에 없었으니까요.

그다음 생각이 재밌었어요. 정답이 분명한 일, 망가뜨린 그림 고치기만 가르쳤거든요. 얼마나 망가졌는지 알려 주고 순수한 잡음에서부터 조금씩 고쳐 가게 했더니 새 숫자가 나왔어요. 고치기를 거듭하던 수리공이 만드는 법을 터득한 거죠.

그 수리공은 그림의 무엇부터 정했어요?

큰 꼴부터요. 실눈으로 보이는 굵은 무늬가 먼저 정해지고 가는 결이 나중에 붙었어요. 잡음은 가는 무늬부터 덮으니까, 잡음을 줄여 가며 고치면 굵은 대역부터 차례로 드러나는 거였어요.

그래서 디퓨전은 대역별로 나눠 여러 번 디노이징하는 일이었어요. 그 걸음을 식으로 적으려니, 퍼진 분포를 열 방정식 그대로 거꾸로 풀면 촘촘한 무늬가 e^(+Dk²t)배로 커져서 터졌죠. 대신 레이더 바늘, 곧 로그 밀도의 기울기인 스코어를 알면 점 하나하나를 바늘 쪽으로 옮겨서 돌아올 수 있었어요. 정규화 상수를 몰라도 읽히고요.

문제는 그 바늘을 모른다는 거였는데, 그림에 잡음을 섞어 본 기록으로 배웠죠. 기록마다 떠나온 점 쪽 바늘을 정답으로 주면 제곱 오차의 답이 그 자리에 선 사람들의 평균이 되고, 그 평균이 진짜 바늘이었어요. 결국 섞은 잡음을 맞히는 것과 같은 일이고요.

처음에 배운 ELBO 는 그 뒤로 어디서 다시 나왔죠?

DDPM 의 손실이요. 잡음을 천 걸음에 나눠 섞는 사슬을 VAE 로 보면, 그림이 데이터고 나머지 사슬이 잠재 변수고, 인코더는 고정된 잡음 섞기예요. 그 ELBO 가 걸음마다의 잡음 맞히기 오차에 가중치를 붙인 합이 됐어요. 흐릿하다고 넘어간 모델의 바닥이 거기서 그대로 손실이 될 줄은 몰랐어요.

신경망 쪽은요?

그림 크기의 MLP 대신 대역을 고르는 합성곱으로 갔고, 굵은 대역과 가는 대역을 나눠 읽으려고 U-Net 으로 해상도를 낮췄고, 그래도 몫이 가운데에 몰려서 멀리 읽는 어텐션을 끼웠어요. Stable Diffusion 1.x 에서는 어텐션 블록이 매개변수의 31.1%였는데 SDXL 에서는 86.0%였죠. 계산이 너무 커서 잠재 공간으로 내려갔고, 끝에는 합성곱 단계를 다 걷어 내고 트랜스포머 블록만 쌓았어요.

그동안 길은 어떻게 걸었어요?

바늘을 따라 걸으며 바람을 맞는 길, 곧 걸음마다 잡음을 다시 넣는 역방향 SDE 와, 바람 없이 흐르는 확률 흐름 ODE 가 같은 분포를 지났어요. 그런데 그 길이 휘어 있어서, 처음부터 곧은 짝의 길로 속도를 통째로 배우는 플로우 매칭으로 넘어갔고요. 잡음과 속도와 원래 그림은 같은 정보인데도 무엇을 맞히게 하느냐에 따라 잡음 수준마다 배점이 달랐고, 그림이 좁은 면 위에 모여 있으면 원래 그림을 맞히는 쪽이 넓은 신경망을 덜 요구했어요. 몇 걸음에 어떻게 걸을지는 그다음에 샘플러로 따로 정했고요.

그럼 마디를 볼까요. 민준 학생이 칸을 하나씩 짚어 봐요.

steps, sampler_name, scheduler 는 같은 길을 몇 걸음에, 어떤 걸음법으로, 어느 잡음 크기에 디디며 걸을지예요. cfg 가 1이 아니면 걸음마다 신경망을 두 번 불러요. 조건을 넣은 예측과 빈 조건의 예측을 읽어서 그 차이를 cfg 배로 키우는 거죠. 그리고 seed 는 출발점의 잡음이니까, seed 만 고정해 두면 걸음 수를 바꿔도 같은 그림이 나와요.

sampler_name 이 euler_ancestral 이라면요? 시드는 그대로 두고 steps 만 20에서 30으로 바꾸면?

앤세스트럴은 걸음마다 읽은 잡음을 덜어 내고 새 잡음으로 채우니까… 걸음 수가 바뀌면 새로 넣는 잡음도 달라지네요. 같은 시드에서도 다른 그림이 나와요. 출발점이 도착점을 정하는 건 잡음을 다시 넣지 않는 euler 같은 걸음일 때고요.

그러니까 시드는 뽑을 잡음을 정할 뿐이고, 그 잡음을 몇 번 어디에 쓸지는 샘플러와 걸음 수가 정하는 거야.

조교가 문제 은행 순서를 고정해 줘도, 몇 문제짜리 시험이냐에 따라 받는 문제가 달라지는 거랑 같네.

서연 학생은 negative 칸을 볼까요.

빈 조건 자리에 피할 글을 넣는 칸이에요. 그래서 cfg 가 1이면 아무 일도 하지 않아요. 그때는 빈 조건 쪽 예측을 아예 계산하지 않으니까요. positive 칸의 글은 글 인코더가 토큰마다의 벡터 줄로 바꿔서 크로스 어텐션이나 합동 어텐션으로 넣고요. 예전에는 CLIP 의 마지막 층이나 끝에서 둘째 층을 받았고, 요즘 모델은 언어모델의 여러 층을 쌓아 넘겨요.

마지막으로 부록에서 한 일을 떠올려 봐요. 사람이 고른 그림 쌍으로 모델을 다듬으려면 그림의 로그확률이 있어야 했죠.

그림의 로그확률은 잴 수 없으니까 ELBO 바닥으로 바꿨어요. 바닥은 언제나 천장 아래에 있으니까, 바닥끼리 견주면 어느 두 모델이든 괜찮은 거 아닌가요?

바닥과 천장 사이의 틈은 그림마다 같았나요?

아니요, 그림마다 크게 달랐어요. 그 틈이 지워진 건 마진의 「차이의 차이」에서였고, 같은 그림에서 두 모델의 틈이 비슷할 때만이에요. 같은 계열의, 너무 멀지 않은 두 모델이라는 단서가 있었어요. 적분 상수랑 같네요. 같은 함수의 원시함수끼리 빼야 상수가 지워지지, 다른 함수의 원시함수끼리 빼면 남잖아요.

흐린 회색 3 한 장에서 시작해서 여기까지 왔네요. 그런데 이 마디의 칸을 다 짚을 수 있게 됐다고 해서 물음이 다 끝난 건 아니에요.