디퓨전

지도는 여행이 아니니, 지도를 들고 실제 세계를 걸어봐야 한다.

여기서부터 지도를 들고 실제 세계, 그림을 그리는 생성 모델로 걸어 들어간다.

추론이 “이미 있는 데이터에서 숨겨진 구조를 찾는” 일이었다면, 이 장의 일은 그 반대다. 그림을 그려 주는 생성 모델에 「바닷가의 고양이」라고 적으면, 모델은 아무 의미 없는 무작위 숫자 덩어리(노이즈)에서 출발해 수십 번에서 많게는 천 번까지 조금씩 고쳐 그림 한 장을 내놓는다. 버튼을 다시 누르면 다른 노이즈에서 출발해 다른 고양이가 나오는데, 사람들은 하루에도 수없이 이 버튼을 누른다.

그런데 그 노이즈는 그림과 얼마나 멀리 떨어져 있을까? 가로세로 256픽셀짜리 컬러 사진 한 장은 픽셀마다 빨강·초록·파랑 세 숫자를 가지므로 256 × 256 × 3 = 196,608개, 약 20만 개의 숫자다. 다시 말해 20만 차원 공간의 점 하나다. 그 공간에서 숫자 20만 개를 아무렇게나 뽑으면 거의 확실하게 지지직거리는 노이즈가 나온다. 고양이, 풍경, 얼굴처럼 의미 있는 이미지는 그 거대한 공간의 아주 좁은 곳에만 모여 있다는 뜻이다. 이 관찰을 매니폴드 가설이라고 부른다. 매니폴드는 큰 공간 안에 놓인 차원이 낮은 매끈한 면으로, 3차원 방 안에 펼쳐 놓은 종이 한 장(2차원)을 떠올리면 된다. 매니폴드 가설은 실제 데이터가 고차원 공간 전체에 퍼져 있지 않고 그런 저차원 면 근처에 모여 있다는, 증명이 아니라 경험에서 나온 주장이다.

그러면 물음은 이렇게 된다. 넓은 공간 아무 데서나 출발한 점이, 어떻게 그 좁은 곳을 찾아 걸어 들어가는가? 노이즈에서 이미지가 태어나는 일은 마법인가, 아니면 지금까지 배운 도구들의 조합인가?


안개 속 레이더: 스코어 함수가 방향을 알려 준다
안개 속 레이더: 스코어 함수가 방향을 알려 준다

스코어: 로그 밀도의 그래디언트라는 레이더

20만 차원은 그릴 수 없다. 그래서 공간을 2차원 평야로, 의미 있는 이미지가 모인 좁은 곳을 평야 위의 작은 봉우리 몇 개로 줄여 보자. 차원은 줄었지만 「넓은 곳에 떨어진 점이 좁은 곳을 찾아가야 한다」는 처지는 20만 차원일 때와 같다.

드래곤볼 찾기: 레이더만 보고 걸어라

드래곤볼 찾기 대회. 넓은 평야에 드래곤볼 7개가 숨겨져 있고, 참가자 수천 명이 평야의 아무 지점에 랜덤으로 떨어진다. 주최 측은 각 참가자에게 드래곤 레이더를 하나씩 나눠주면서, 「레이더 바늘만 보고 걸어라」라는 규칙 하나만 정한다.

전파 지형: 봉우리가 일곱 개인 밀도

각 드래곤볼이 내보내는 전파의 신호 강도는 볼에서 가까우면 강하고 멀면 약해서, 거리에 따라 정규분포의 종 모양으로 약해진다. 7개의 신호가 합쳐지면 땅 위에 봉우리가 7개인 전파 강도 지도가 만들어진다.

이 전파 강도 지도는 정규분포 7개에 볼마다 신호의 크기(혼합 비중)를 곱해 더한 혼합분포(mixture)다. 전파 강도 지도 = 확률밀도 p(x).

레이더의 원리: 로그 밀도의 그래디언트

레이더 바늘이 가리키는 방향은 "여기서 신호가 가장 빠르게 커지는 방향"인데, 다만 신호 자체가 아니라 신호의 로그가 가장 빠르게 커지는 방향이다. 아래 그림은 봉우리가 둘인 1차원 지형에서 레이더 바늘, 곧 스코어를 그린 것이다.

위: 봉우리가 −2 와 1.5 에 있는 1차원 밀도 p(x) 와, 그 아래 축 위에 그린 레이더 바늘(스코어의 방향과 크기). 아래: 스코어 s(x) = ∇ₓ log p(x) 의 그래프. 봉우리 꼭대기와 두 봉우리 사이 골짜기에서 0 이 되고, 바깥 벌판으로 갈수록 밀도는 0 에 붙는데 스코어는 직선으로 커진다
위: 봉우리가 −2 와 1.5 에 있는 1차원 밀도 p(x) 와, 그 아래 축 위에 그린 레이더 바늘(스코어의 방향과 크기). 아래: 스코어 s(x) = ∇ₓ log p(x) 의 그래프. 봉우리 꼭대기와 두 봉우리 사이 골짜기에서 0 이 되고, 바깥 벌판으로 갈수록 밀도는 0 에 붙는데 스코어는 직선으로 커진다
s(x)=∇xlog⁡p(x)\textcolor{#b0892a}{s}(\textcolor{#6f8fa6}{x}) = \nabla_{\textcolor{#6f8fa6}{x}} \log \textcolor{#d6479f}{p}(\textcolor{#6f8fa6}{x})
s스코어 함수 (레이더 바늘)x평야 위의 한 점 (데이터 공간의 점)p데이터 분포의 밀도 (전파 강도 지도)∇x데이터 좌표 x 에 대한 그래디언트\begin{array}{ll} \textcolor{#b0892a}{s} & \text{스코어 함수 (레이더 바늘)} \\ \textcolor{#6f8fa6}{x} & \text{평야 위의 한 점 (데이터 공간의 점)} \\ \textcolor{#d6479f}{p} & \text{데이터 분포의 밀도 (전파 강도 지도)} \\ \nabla_{\textcolor{#6f8fa6}{x}} & \text{데이터 좌표 } x \text{ 에 대한 그래디언트} \end{array}

"스코어"라는 이름은 파라미터(분포를 정하는 숫자, 예: 정규분포의 평균과 분산) 쪽에서도 쓰인다. 이름은 같지만 미분하는 변수가 다르다. 파라미터에 대한 스코어 ∇θ log pθ(x)는 파라미터 θ를 흔들어 보는 그래디언트다. 분포족(파라미터만 바꾼 분포들의 모임) 안에서 어느 쪽으로 움직이면 이 데이터가 더 그럴듯해지는가. 이 장의 스코어인 데이터에 대한 스코어 ∇ₓ log p(x)는 분포는 그대로 두고 점 x를 움직여 보는 그래디언트다. 평야 위에서 어느 쪽으로 걸으면 더 그럴듯한 곳에 닿는가. 둘 다 "로그를 미분한다"는 같은 가족이지만, 이 장에서 스코어라고 하면 늘 데이터에 대한 스코어 ∇ₓ log p(x)다.

왜 log인가

가장 중요한 이유부터. 실제 문제에서 우리는 밀도를 상수배까지만 안다. p(x) = p̃(x) / Z 에서 p̃는 계산할 수 있지만, 정규화 상수 Z(전체 합이 1이 되게 나눠 주는 수)는 온 평야에 걸친 적분이라 계산할 수 없다. 그런데 로그를 씌우고 x로 미분하면 Z가 사라진다.

∇xlog⁡p(x)=∇xlog⁡p~(x)−∇xlog⁡Z=∇xlog⁡p~(x)\begin{aligned} \nabla_{\textcolor{#6f8fa6}{x}} \log \textcolor{#d6479f}{p}(\textcolor{#6f8fa6}{x}) &= \nabla_{\textcolor{#6f8fa6}{x}} \log \tilde{\textcolor{#d6479f}{p}}(\textcolor{#6f8fa6}{x}) - \nabla_{\textcolor{#6f8fa6}{x}} \log \textcolor{#7d8a3a}{Z} \\ &= \nabla_{\textcolor{#6f8fa6}{x}} \log \tilde{\textcolor{#d6479f}{p}}(\textcolor{#6f8fa6}{x}) \end{aligned}
p정규화된 밀도p~정규화하지 않은 밀도 (계산 가능)Z정규화 상수=∫p~(x) dxx평야 위의 점\begin{array}{ll} \textcolor{#d6479f}{p} & \text{정규화된 밀도} \\ \tilde{\textcolor{#d6479f}{p}} & \text{정규화하지 않은 밀도 (계산 가능)} \\ \textcolor{#7d8a3a}{Z} & \text{정규화 상수} = \int \tilde{p}(x)\,dx \\ \textcolor{#6f8fa6}{x} & \text{평야 위의 점} \end{array}

Z는 x와 무관한 상수라서 그래디언트가 0이므로, 스코어(레이더)는 전체 지도의 총량을 몰라도 만들 수 있다. log p = θ·t(x) − F(θ) 꼴의 분포 모임(지수족)에서도 합을 1로 맞추는 F(θ)(로그정규화자)를 계산하는 것이 늘 가장 어려운 일이다. 그 어려움을 떠올리면 Z가 사라지는 것이 얼마나 큰 해방인지 알 수 있고, 이 Z의 소거가 디퓨전 모델이 밀도 대신 스코어를 배우는 첫째 이유다.

두 번째 이유는 비율이다. 신호가 0.001인 벌판 한가운데서 0.0001만큼 커지는 것과, 신호가 0.5인 볼 근처에서 0.0001만큼 커지는 것은 의미가 다르다. 벌판에서의 미세한 변화를 놓치면 참가자는 영원히 허허벌판을 헤매므로, 신호의 변화는 비율로 봐야 한다. 그래서 ∇log p = ∇p / p.

연봉 3000만원의 300만원 인상과 3억의 300만원 인상은 다른데, 드래곤 레이더는 이 구분을 할 줄 아는 기기다.

혼합의 스코어

전파 지형이 가우시안 혼합이면 스코어를 손으로 계산할 수 있다.

s(x)=∑kγk(x) μk−xs02γk(x)=πk N(x; μk,s02I)∑jπj N(x; μj,s02I)\begin{aligned} \textcolor{#b0892a}{s}(\textcolor{#6f8fa6}{x}) &= \sum_{k} \textcolor{#7888e8}{\gamma}_k(\textcolor{#6f8fa6}{x})\, \frac{\textcolor{#2e9e6e}{\mu}_k - \textcolor{#6f8fa6}{x}}{\textcolor{#b83840}{s_0}^2} \\ \textcolor{#7888e8}{\gamma}_k(\textcolor{#6f8fa6}{x}) &= \frac{\textcolor{#885868}{\pi}_k\, \mathcal{N}(\textcolor{#6f8fa6}{x};\, \textcolor{#2e9e6e}{\mu}_k, \textcolor{#b83840}{s_0}^2 I)}{\sum_j \textcolor{#885868}{\pi}_j\, \mathcal{N}(\textcolor{#6f8fa6}{x};\, \textcolor{#2e9e6e}{\mu}_j, \textcolor{#b83840}{s_0}^2 I)} \end{aligned}
s혼합분포의 스코어x평야 위의 점μkk 번째 드래곤볼의 위치 (성분 평균)s0봉우리 하나의 폭 (표준편차)πk혼합 비중 (볼 k 의 신호 크기)γk책임도: 신호가 볼 k 에서 왔을 사후확률N정규분포 밀도,  I 는 단위행렬\begin{array}{ll} \textcolor{#b0892a}{s} & \text{혼합분포의 스코어} \\ \textcolor{#6f8fa6}{x} & \text{평야 위의 점} \\ \textcolor{#2e9e6e}{\mu}_k & k \text{ 번째 드래곤볼의 위치 (성분 평균)} \\ \textcolor{#b83840}{s_0} & \text{봉우리 하나의 폭 (표준편차)} \\ \textcolor{#885868}{\pi}_k & \text{혼합 비중 (볼 } k \text{ 의 신호 크기)} \\ \textcolor{#7888e8}{\gamma}_k & \text{책임도: 신호가 볼 } k \text{ 에서 왔을 사후확률} \\ \mathcal{N} & \text{정규분포 밀도}, \; I \text{ 는 단위행렬} \end{array}

봉우리 폭에 아래첨자 0을 붙여 s₀로 적은 것은 스코어 s와 글자가 겹치지 않게 하려는 것뿐이다. π도 원주율이 아니라, 혼합 비중에 흔히 쓰는 글자다. 혼합 스코어 식을 읽어 보면, 볼(봉우리) 하나만 있을 때 스코어는 곧장 그 봉우리의 중심을 가리키는 (μk − x)/s₀²이다. 볼이 여럿이면 스코어는 각 봉우리 중심을 향한 방향의 가중평균이고, 그 무게가 책임도 γk(지금 이 신호가 볼 k에서 왔을 사후확률)다. 가까운 볼일수록, 신호가 큰 볼일수록 무게가 크다.

직접 움직여 보기

불러오는 중…

파이썬

import numpy as np

mu = np.array([[-2.5, -1.5], [1.5, -2.0], [-1.0, 2.0], [2.5, 1.5],
               [0.0, 0.0], [-2.0, 0.5], [1.0, -0.5]])       # 봉우리 7개
w = np.array([.18, .14, .16, .14, .12, .13, .13])            # 혼합 비중 (합 1)
s = 0.4                                                       # 봉우리 폭

def p_tilde(x):          # 정규화 안 한 밀도: 상수 1/(2π s²) 를 일부러 뺐다
    d2 = ((x - mu)**2).sum(1)
    return (w * np.exp(-d2 / (2 * s**2))).sum()

def score(x):            # 책임도 가중 평균: Σ γ_k (μ_k − x)/s²
    d2 = ((x - mu)**2).sum(1)
    lg = np.log(w) - d2 / (2 * s**2)
    g = np.exp(lg - lg.max()); g /= g.sum()
    return (g[:, None] * (mu - x)).sum(0) / s**2

def fd(f, x, h=1e-5):    # 유한차분 그래디언트
    return np.array([(f(x + h * e) - f(x - h * e)) / (2 * h) for e in np.eye(2)])

x = np.array([0.6, 0.9])
print("해석식 스코어       ", score(x).round(4))
print("log p̃ 유한차분      ", fd(lambda u: np.log(p_tilde(u)), x).round(4))
print("log(p̃/Z) 유한차분   ", fd(lambda u: np.log(p_tilde(u) / 123.4), x).round(4))
for x in [np.array([0.6, 0.9]), np.array([3.5, -3.5])]:
    print(x, "|∇p̃| =", f"{np.linalg.norm(fd(p_tilde, x)):.1e}",
          " |∇log p| =", f"{np.linalg.norm(score(x)):.2f}")
# 해석식 스코어        [-3.4203 -5.7833]
# log p̃ 유한차분       [-3.4203 -5.7833]
# log(p̃/Z) 유한차분    [-3.4203 -5.7833]
# [0.6 0.9] |∇p̃| = 2.2e-02  |∇log p| = 6.72
# [ 3.5 -3.5] |∇p̃| = 7.2e-09  |∇log p| = 15.63

Z를 아무 값(여기서는 123.4)으로 바꿔도 스코어는 한 자리도 변하지 않는다. 구석 (3.5, −3.5)에서 밀도의 그래디언트는 10⁻⁹ 수준으로 사라지지만, 스코어는 오히려 더 크다.

이 절의 결론

“스코어 ∇ₓ log p는 정규화 상수를 몰라도 만들 수 있고, 밀도가 0에 가까운 벌판에서도 꺼지지 않는다.”

문제 1. 눈금이 지워진 방문자 그래프

블로그 방문자 수 그래프를 캡처했는데 세로축 눈금이 잘려 나가서, 막대 높이가 어제 4칸, 오늘 4.4칸이라는 것만 알고 한 칸이 몇 명인지는 모른다. (가) 오늘 방문자는 어제보다 몇 명 늘었는가? (나) 몇 퍼센트 늘었는가? (다) 방문자 수의 로그는 얼마나 늘었는가?

세로축 눈금이 잘려 나간 방문자 수 막대그래프. 어제 막대는 4칸, 오늘 막대는 4.4칸이고 한 칸이 몇 명인지는 알 수 없다
세로축 눈금이 잘려 나간 방문자 수 막대그래프. 어제 막대는 4칸, 오늘 막대는 4.4칸이고 한 칸이 몇 명인지는 알 수 없다

함께 풀기

김민준 M04
김민준

한 칸이 몇 명인지 모르면 아무것도 못 구하는 거 아니에요? 한 칸이 10명이면 4명 늘었고, 1,000명이면 400명 늘었어요.

선생님 T01
선생님

(가)는 정말 그래요. 그럼 (나)도 한 칸의 크기에 따라 달라져요?

이서연 S01
이서연

아니요. 한 칸을 c명이라 하면 (4.4c − 4c)/4c = 0.1이라 c가 약분돼요. 10% 늘었어요.

김민준 M01
김민준

(다)는 log 4.4c − log 4c = log 1.1 = 0.0953이네. 로그의 차이에서도 c가 빠져. 로그를 씌우면 모르는 배율이 그냥 더해지는 상수가 되니까, 차이를 내는 순간 사라지는 거구나.

선생님 T01
선생님

스코어가 하는 일이 그거예요. 밀도의 눈금인 정규화 상수 Z를 몰라도 로그의 변화는 읽혀요.

김민준 M01
김민준

조교가 성적을 100점 만점으로 줬는지 10점 만점으로 줬는지 몰라도, 누가 지난번보다 몇 퍼센트 올랐는지는 말할 수 있는 거랑 같네요. 만점이 정규화 상수 Z인 거고요.

문제 2. 봉우리가 둘인 혼합의 스코어

1차원 혼합 p(x) = 0.8·N(x; 2, 1) + 0.2·N(x; −2, 1)을 생각한다. (가) 스코어 ∇log p(x)를 책임도로 쓴 식으로 유도하고, x = 0에서의 값을 구하라. (나) x = 45에서 log p를 유한차분으로 미분하면 무엇이 나오는가? 해석식은 얼마를 주는가?

혼합 p(x) = 0.8·N(x; 2, 1) + 0.2·N(x; −2, 1)의 밀도. 비중 0.8인 봉우리는 x = 2, 비중 0.2인 봉우리는 x = −2에 있고, 스코어를 물을 자리 x = 0과 멀리 떨어진 x = 45를 표시했다
혼합 p(x) = 0.8·N(x; 2, 1) + 0.2·N(x; −2, 1)의 밀도. 비중 0.8인 봉우리는 x = 2, 비중 0.2인 봉우리는 x = −2에 있고, 스코어를 물을 자리 x = 0과 멀리 떨어진 x = 45를 표시했다

함께 풀기

이서연 S01
이서연

(가)는 금방이에요. 두 정규분포를 더한 것의 로그를 미분하면 각 성분 스코어의 가중평균이에요.

김민준 M01
김민준

x = 0에서 성분 스코어는 각각 얼마야?

이서연 S01
이서연

(2 − 0)과 (−2 − 0)이야. x = 0은 두 봉우리의 한가운데니까 반반 섞여서 0이야.

선생님 T01
선생님

가운데에서 스코어가 0이라는 말이네요. 그럼 x = 0에서 걷기 시작한 참가자는 어디로 가요?

이서연 S01
이서연

아무 데도요. 그래디언트가 0인 점(정류점)이니까요.

김민준 M01
김민준

근데 위젯에서 비슷한 걸 해 보면 가운데서도 비중이 큰 봉우리 쪽으로 스코어 화살표가 기울던데.

선생님 T01
선생님

서연 학생, 가중평균의 무게를 다시 써 봐요. 무게가 N₁/(N₁ + N₂)였어요?

이서연 S04
이서연

아… π가 빠졌어요. γ₁ = 0.8·N₁ / (0.8·N₁ + 0.2·N₂)예요. x = 0에서 N₁ = N₂라서 γ₁ = 0.8이에요. 스코어는 0.8 × 2 + 0.2 × (−2) = 1.2예요.

이서연 S01
이서연

가운데는 정류점이 아니에요. 비중 0.8인 봉우리(x = 2) 쪽으로 기울어요. 로그의 합을 미분한 게 아니라 합의 로그를 미분한 거라, 비중이 분자와 분모에 같이 남아요.

선생님 T01
선생님

그래요. 로그가 곱은 풀어 주지만 합은 못 풀어요. 그래서 비중이 책임도 안에 살아남아요. 민준 학생은 (나)를 해 봤어요?

김민준 M05
김민준

네, 그런데 이상해요. x = 5에서는 −3.000, x = 30에서는 −28.000이 깔끔하게 나오는데, x = 45에서는 nan이 나와요. 스코어가 거기서 정의가 안 되는 건가요?

선생님 T01
선생님

x = 45에서 p(x) 자체는 얼마쯤이에요?

김민준 M01
김민준

(45 − 2)²/2가 924.5니까 e−924.5… 아, float64의 가장 작은 수가 10−324쯤이니까 0으로 뭉개졌어요. log 0이 −inf고, −inf 빼기 −inf가 nan이에요.

선생님 T01
선생님

스코어는 멀쩡해요. 해석식으로는요?

김민준 M01
김민준

거기선 γ₁이 사실상 1이니까 (2 − 45)/1 = −43이요. 멀어질수록 직선으로 커지기만 해요. 망가진 건 스코어가 아니라 밀도를 먼저 계산한 제 순서였네요.

김민준 M01
김민준

조별 과제 점수를 곱해서 평균 내다가 0점이 하나 끼면 다 날아가는 거랑 같네요. 밀도가 0으로 뭉개진 게 그 0점이고요. 로그로 먼저 바꿔서 더했어야 했는데.

선생님 T01
선생님

실제 코드도 그래서 log-sum-exp로 계산해요. 이 장의 파이썬도 책임도를 로그에서 최댓값을 빼고 계산했어요.

문제 3. 에너지만 아는 모델의 스코어

ML에는 밀도를 정규화하지 않은 채로 쓰는 모델이 많다. 신경망이 점 x마다 「에너지」 E(x)라는 숫자 하나를 내놓고, 확률은 p(x) = e−E(x) / Z로 정한다(에너지 기반 모델). 1차원 장난감으로는 골짜기가 x = ±1 에 있는 에너지 E(x) = (x² − 1)²를 쓴다. (가) x = 0.5와 x = 2에서 스코어를 구하라. (나) 이 계산에 Z가 필요했는가? x가 1,000차원이면 Z를 구하는 일은 어떻게 되는가?

에너지 E(x) = (x² − 1)²의 그래프. 골짜기는 x = ±1에 있고, 스코어를 물을 두 점 x = 0.5와 x = 2를 표시했다
에너지 E(x) = (x² − 1)²의 그래프. 골짜기는 x = ±1에 있고, 스코어를 물을 두 점 x = 0.5와 x = 2를 표시했다

함께 풀기

김민준 M01
김민준

log p = −E − log Z니까 미분하면 E′이죠. E′(x) = 4x(x² − 1)이고, x = 0.5에서 4 × 0.5 × (−0.75) = −1.5예요.

선생님 T01
선생님

0.5에 선 사람에게 가장 가까운 골짜기는 어디예요? 스코어는 그 골짜기를 가리켜야 하죠.

김민준 M05
김민준

골짜기는 +1이니까 오른쪽인데, −1.5면 왼쪽을 가리켜요. 아, 마이너스가 붙은 걸 미분하면서 떨어뜨렸어요. 스코어는 −E′이라 +1.5예요.

선생님 T01
선생님

x = 2에서는요?

김민준 M01
김민준

−4 × 2 × 3 = −24로, 골짜기 1 쪽으로 세게 끌어요.

이서연 S01
이서연

에너지가 낮은 곳이 확률이 높은 곳이니까, 스코어 −E′는 에너지의 내리막을 가리키는 게 맞아.

선생님 T01
선생님

(나)는요? 지금까지 계산에서 Z를 썼어요?

이서연 S01
이서연

한 번도 안 썼어요. 1차원이면 적분해서 Z ≈ 1.974가 나오지만, 1,000차원이면 축마다 점 100개만 찍어도 1001000개의 점에서 e−E를 계산해야 해요.

선생님 T01
선생님

그래서 정규화하지 않은 모델을 배우거나 그 모델에서 샘플을 뽑을 때 스코어로 일하는 거예요. 앞의 방문자 그래프에서 눈금을 몰라도 로그의 변화가 읽힌 것과 같은 이유예요.