13장 — 열 방정식과 확산

푸리에 모드: 촘촘한 무늬부터 사라진다

한 점에서 출발한 해는 열핵으로 구했고, 처음 값이 여러 곳에 흩어져 있어도 열핵과의 합성곱으로 답을 적을 수는 있다. 그런데 사진을 흐리게 하면 머리카락 같은 가는 선은 금세 사라지고 얼굴의 윤곽은 오래 남는다. 실제로 궁금한 것은 처음 모양의 어느 부분이 먼저 지워지느냐인데, 합성곱 식은 이 물음에 바로 답해 주지 않는다. 처음 모양을 어떻게 나누어 보면 무엇이 먼저 사라지는지가 보일까?

역사: 삼각급수 논쟁

푸리에가 열 방정식을 풀려고 꺼낸 도구, 곧 아무 함수나 사인과 코사인의 합으로 펼치는 방법은 발표되자마자 당대 최고의 수학자들에게 반대에 부딪혔고, 그 논쟁을 정리하는 데 이후 50년의 해석학이 필요했다. 그는 처음 온도 분포를 사인과 코사인 무늬의 합으로 나누었다. 무늬 하나하나는 모양을 유지한 채 크기만 줄어들고, 촘촘한 무늬일수록 빨리 줄어든다. 문제는 모서리가 있거나 끊어진 함수까지 이런 합으로 쓸 수 있다는 그의 주장이었다. 매끄러운 사인들을 더해서 어떻게 모서리가 생기느냐는 반론에 푸리에는 엄밀한 증명을 내놓지 못했다.

매끄러운 사인을 더해 모서리 있는 계단 모양에 다가가기. 높이 ±1을 오가는 계단(회색)을 sin x, sin 3x, sin 5x, … 의 합으로 펼쳐 첫 1개, 3개, 15개 항까지만 더했다. 항이 늘수록 모서리에 가까워지지만, 모서리 바로 옆의 솟음(약 1.18)은 항을 늘려도 낮아지지 않고 좁아지기만 한다.
매끄러운 사인을 더해 모서리 있는 계단 모양에 다가가기. 높이 ±1을 오가는 계단(회색)을 sin x, sin 3x, sin 5x, … 의 합으로 펼쳐 첫 1개, 3개, 15개 항까지만 더했다. 항이 늘수록 모서리에 가까워지지만, 모서리 바로 옆의 솟음(약 1.18)은 항을 늘려도 낮아지지 않고 좁아지기만 한다.

그는 『열의 해석적 이론』 서론에 「자연을 깊이 연구하는 것이 수학적 발견의 가장 풍부한 원천이다」라고 적었는데, 이 경우에는 그 말이 문자 그대로 이루어졌다.

삼각급수가 언제 원래 함수로 수렴하는지를 처음 증명한 사람은 1829년의 디리클레였다. 1854년 리만은 교수 자격 논문에서 삼각급수로 나타낼 수 있는 함수를 다루려고 적분을 새로 정의했는데, 이것이 오늘날 해석학 수업에서 배우는 리만 적분이다(논문은 그가 죽은 뒤에 출간되었다). 1870년 칸토어는 한 함수를 삼각급수로 나타내는 방법이 하나뿐임을 증명했고, 급수가 수렴하지 않아도 되는 예외 점들을 점점 넓혀 가다가 점들의 집합 자체를 연구하게 되었다. 열이 퍼지는 문제가 함수, 수렴, 적분, 집합이라는 해석학의 기초 개념을 차례로 엄밀하게 다듬도록 몰아붙인 셈이다.

무늬 하나의 해

푸리에의 방법은 무늬 하나에서 출발한다. cos kx처럼 파장이 2π/k인 무늬에 라플라시안을 쓰면 −k² cos kx가 되어 모양은 그대로이고 크기만 −k²배가 된다. 그러니 이 무늬는 열 방정식을 따라 모양을 유지한 채 크기만 e^(−Dk²t)로 줄어든다. 처음 모양을 이런 무늬들의 합으로 나누면 무늬마다 따로 줄어들고, 방정식이 선형이므로 해는 줄어든 무늬들의 합이다. 아래 식에서는 cos kx와 sin kx를 따로 적는 대신 둘을 e^(ikx) = cos kx + i sin kx 하나로 묶어 적는다. 묶어도 라플라시안을 쓰면 −k²배가 되는 것은 같고, 실수 부분만 보면 cos kx 이야기 그대로다.

무늬 하나는 모양을 지킨 채 크기만 줄어든다(D = 1/4). 파장 32칸 무늬(위)는 t = 10에 0.91배, t = 40에 0.68배로 천천히 줄지만, 파장 8칸 무늬(아래)는 t = 10에 0.21배, t = 40에 0.002배로 거의 사라진다. 파장이 4분의 1이면 줄어드는 빠르기는 16배다.
무늬 하나는 모양을 지킨 채 크기만 줄어든다(D = 1/4). 파장 32칸 무늬(위)는 t = 10에 0.91배, t = 40에 0.68배로 천천히 줄지만, 파장 8칸 무늬(아래)는 t = 10에 0.21배, t = 40에 0.002배로 거의 사라진다. 파장이 4분의 1이면 줄어드는 빠르기는 16배다.
u(x,0)=∑kck eikx⟹u(x,t)=∑kck e−Dk2t eikx\textcolor{#1e90ff}{u}(\textcolor{#1b9e77}{x}, 0) = \sum_{\textcolor{#8c8c0e}{k}} \textcolor{#b2003c}{c}_k\, e^{i\textcolor{#8c8c0e}{k}\textcolor{#1b9e77}{x}} \quad\Longrightarrow\quad \textcolor{#1e90ff}{u}(\textcolor{#1b9e77}{x}, \textcolor{#a67a63}{t}) = \sum_{\textcolor{#8c8c0e}{k}} \textcolor{#b2003c}{c}_k\, e^{-\textcolor{#800080}{D}\textcolor{#8c8c0e}{k}^2 \textcolor{#a67a63}{t}}\, e^{i\textcolor{#8c8c0e}{k}\textcolor{#1b9e77}{x}}
u(x,t)시간 t의 모양 (t = 0이면 처음 모양)x위치k파수 = 2π/파장 (볼츠만 상수 k와 글자만 같다)eikx파수 k인 무늬 (푸리에 모드. 실수 부분이 cos kx)ck처음 모양에 든 파수 k 무늬의 크기e−Dk2t시간 t 동안 그 무늬가 줄어드는 배율D, t확산 계수, 시간\begin{array}{ll} \textcolor{#1e90ff}{u}(\textcolor{#1b9e77}{x}, \textcolor{#a67a63}{t}) & \text{시간 t의 모양 (t = 0이면 처음 모양)} \\ \textcolor{#1b9e77}{x} & \text{위치} \\ \textcolor{#8c8c0e}{k} & \text{파수 = 2π/파장 (볼츠만 상수 k와 글자만 같다)} \\ e^{i\textcolor{#8c8c0e}{k}\textcolor{#1b9e77}{x}} & \text{파수 k인 무늬 (푸리에 모드. 실수 부분이 cos kx)} \\ \textcolor{#b2003c}{c}_k & \text{처음 모양에 든 파수 k 무늬의 크기} \\ e^{-\textcolor{#800080}{D}\textcolor{#8c8c0e}{k}^2 \textcolor{#a67a63}{t}} & \text{시간 t 동안 그 무늬가 줄어드는 배율} \\ \textcolor{#800080}{D},\ \textcolor{#a67a63}{t} & \text{확산 계수, 시간} \end{array}

k는 길이 2π 안에 무늬가 몇 번 되풀이되는지, 곧 2π/파장을 나타내며 파수(wavenumber)라 부른다. 파수 k인 무늬를 푸리에 모드 (라플라시안을 써도 모양이 바뀌지 않는 무늬, Fourier mode)라 한다. 줄어드는 빠르기가 k²에 비례하므로 촘촘한 무늬일수록 빨리 사라지고, 크기가 1/e로 줄어드는 시간 1/(Dk²)은 파장의 제곱에 비례한다. D = 1/4인 격자에서 파장 4칸 무늬는 1.6번, 32칸 무늬는 104번 만에 1/e로 줄어든다. 이미지를 흐리게 하면 머리카락 같은 가는 선이 먼저 사라지고 얼굴의 윤곽은 오래 남는 까닭이다. 열핵의 넓이가 σ² = 2Dt로 자라는 것도 이 식에서 나온다. 한 점은 모든 파수의 무늬를 같은 크기로 담고 있고, 시간 t 뒤의 모양은 크기가 e^(−Dk²t)인 무늬들의 합, 곧 분산이 2Dt인 가우시안이다.

직접 움직여 보기푸리에 모드의 배율새 창에서 열기 ↗

격자의 무늬와 바둑판 무늬

격자에서도 무늬는 무늬로 남는다. 가로 파수 k₁, 세로 파수 k₂인 무늬에 이웃 넷의 평균을 한 번 쓰면 크기가 (cos k₁ + cos k₂)/2배가 되고, 파수가 작으면 이 값이 1 − (k₁² + k₂²)/4 ≈ e^(−k²/4), 곧 D = 1/4인 연속 방정식의 e^(−Dk²)과 같아진다. 차이는 가장 촘촘한 무늬에서 난다. 한 칸마다 부호가 바뀌는 바둑판 무늬, 곧 k₁ = k₂ = π인 무늬는 한 번 평균 낼 때마다 −1배가 되어, 크기는 그대로인 채 부호만 번갈아 바뀐다. 격자에서 칸의 색이 번갈아 빈 것은 한 칸에 둔 1에 들어 있던 바둑판 무늬가 영원히 사라지지 않았기 때문이다. 게으른 평균은 배율 λ를 (1 + λ)/2로 바꿔 −1을 0으로 옮기므로, 모든 무늬의 배율이 0과 1 사이에 들어와 부호가 바뀌는 무늬가 없다.

코드: 격자에서 이웃 평균을 되풀이하기

처음의 격자를 그대로 돌려 퍼진 거리와 칸 색이 번갈아 비는 현상, 게으른 평균을 확인한다. 이어서 줄무늬 하나에 평균을 한 번 쓸 때의 배율을 연속 방정식의 e^(−Dk²)과 비교한다.

import numpy as np

L = 241; c = L // 2                          # 241 × 241 격자, 가운데 칸에서 출발
X, Y = np.meshgrid(np.arange(L) - c, np.arange(L) - c, indexing='ij')
R2 = X**2 + Y**2                             # 처음 자리에서 잰 거리의 제곱

def neighbor_avg(u):                         # 모든 칸을 동시에 이웃 넷의 평균으로
    return (np.roll(u, 1, 0) + np.roll(u, -1, 0) + np.roll(u, 1, 1) + np.roll(u, -1, 1)) / 4

u = np.zeros((L, L)); u[c, c] = 1.0          # 한 칸에만 1
lazy = u.copy()                              # 게으른 평균: 절반은 제자리에 남긴다
for n in range(1, 401):
    u = neighbor_avg(u)
    lazy = (lazy + neighbor_avg(lazy)) / 2
    if n in (25, 100, 400):
        odd = (X + Y) % 2 == 1
        print(f"n = {n:3d}: 합 {u.sum():.4f}, 퍼진 거리 {np.sqrt((u * R2).sum()):.4f}, "
              f"홀수 칸의 몫 {u[odd].sum():.1f}, 가운데 {u[c, c]:.6f} (가우시안 {2 / (np.pi * n):.6f}) | "
              f"게으른 평균: 퍼진 거리 {np.sqrt((lazy * R2).sum()):.4f}, 가운데 {lazy[c, c]:.6f}")

# 줄무늬 cos(kx) 하나에 이웃 평균을 한 번 쓰면 몇 배가 되나 (격자) 대 e^(−Dk²) (연속, D = 1/4)
for wavelength in (2, 4, 8, 32):
    k = 2 * np.pi / wavelength
    stripe = np.cos(k * X).astype(float)
    ratio = neighbor_avg(stripe)[c, c] / stripe[c, c]
    print(f"파장 {wavelength:2d}칸: 한 번에 {ratio:+.4f}배, 연속 근사 e^(−k²/4) = {np.exp(-k**2 / 4):.4f}")
checker = np.where((X + Y) % 2 == 0, 1.0, -1.0)
print(f"바둑판 무늬: 한 번에 {neighbor_avg(checker)[c, c] / checker[c, c]:+.1f}배")
# n =  25: 합 1.0000, 퍼진 거리 5.0000, 홀수 칸의 몫 1.0, 가운데 0.000000 (가우시안 0.025465) | 게으른 평균: 퍼진 거리 3.5355, 가운데 0.025488
# n = 100: 합 1.0000, 퍼진 거리 10.0000, 홀수 칸의 몫 0.0, 가운데 0.006334 (가우시안 0.006366) | 게으른 평균: 퍼진 거리 7.0711, 가운데 0.006367
# n = 400: 합 1.0000, 퍼진 거리 20.0000, 홀수 칸의 몫 0.0, 가운데 0.001590 (가우시안 0.001592) | 게으른 평균: 퍼진 거리 14.1421, 가운데 0.001592
# 파장  2칸: 한 번에 +0.0000배, 연속 근사 e^(−k²/4) = 0.0848
# 파장  4칸: 한 번에 +0.5000배, 연속 근사 e^(−k²/4) = 0.5396
# 파장  8칸: 한 번에 +0.8536배, 연속 근사 e^(−k²/4) = 0.8571
# 파장 32칸: 한 번에 +0.9904배, 연속 근사 e^(−k²/4) = 0.9904
# 바둑판 무늬: 한 번에 -1.0배

퍼진 거리는 25·100·400번에 정확히 5·10·20이고, 게으른 평균에서는 √(n/2)이다. 파장이 8칸 이상인 무늬에서는 격자의 배율이 연속 근사와 거의 같지만, 파장 2칸 무늬는 한 번에 사라지고 바둑판 무늬는 −1배가 되어 영원히 남는다.

문제 5. 가는 줄무늬와 굵은 줄무늬

흑백 이미지에 파장 6픽셀과 파장 24픽셀인 세로 줄무늬가 같은 크기로 겹쳐 있다. 이웃 넷의 평균(D = 1/4)을 되풀이해 흐리게 한다. (가) 각 줄무늬의 크기가 1/e로 줄 때까지 몇 번 되풀이해야 하는가? (나) 흐려진 이미지를 거꾸로 되살리는 생성 모델은 무엇부터 그리게 되는가? (풀어 본 뒤 위젯 2의 「문제 5 불러오기」로 확인해 보자.)

김민준 M11
김민준

같은 평균 내기를 쓰는데 줄무늬라고 다르게 흐려질 리 없잖아요. 둘 다 같은 빠르기로 흐려지다가 같이 사라질 것 같아요.

선생님 T02
선생님

세로 줄무늬 cos kx에 이웃 넷의 평균을 한 번 쓴다고 해 봐요. 위아래 이웃과 좌우 이웃은 각각 어떤 값이에요?

이서연 S01
이서연

위아래 이웃은 같은 세로줄이라 자기와 같고, 좌우 이웃은 cos k(x ± 1)이에요. 넷을 평균하면 (cos k + 1)/2배가 돼요. 파장 6이면 k = π/3이라서 0.75배, 파장 24면 k = π/12라서 0.983배예요.

김민준 M04
김민준

파장 6은 한 번마다 4분의 3으로 줄어요? 1/e까지 3.5번, 네 번이면 넘고, 파장 24는… 58번이 넘게 걸려요. 전혀 같은 빠르기가 아니네요.

선생님 T14
선생님

연속 방정식으로 어림한 1/e 시간 1/(Dk²)과도 견줘 봐요.

이서연 S08
이서연

3.6번과 58번이에요. 비가 16으로, 파장 비 4의 제곱이에요. 퍼진 폭이 시간의 제곱근이었으니까 무늬를 지우는 데 걸리는 시간은 파장의 제곱에 비례하는 거고요. 이렇게 무늬로 나눠 푼 게 푸리에죠? 르장드르 진짜 얼굴을 찾았을 때 그 캐리커처에 나란히 그려져 있던 사람이요.

선생님 T13
선생님

맞아요. 그 푸리에가 라그랑주 앞에서 모서리가 있는 함수도 사인의 합으로 쓸 수 있다고 버텼죠. 서연 학생은 해석학 시간에 그 뒷이야기를 배웠을 거예요.

이서연 S07
이서연

리만 적분이 거기서 나왔다는 건 처음 알았어요. 적분의 정의가 괜히 까다로운 게 아니었네요.

선생님 T02
선생님

이제 흐린 사진을 되살리는 쪽으로 가 봐요. 생성 모델은 어디부터 그리게 될까요?

김민준 M01
김민준

가는 무늬가 먼저 사라지니까, 되살릴 때도 가는 무늬부터 되살리겠죠.

선생님 T02
선생님

흐려지는 과정을 영상으로 찍어 거꾸로 튼다고 해 봐요. 처음에 나오는 장면은 앞으로 갈 때 언제의 장면이에요?

김민준 M09
김민준

마지막 장면이요. 아, 그럼 마지막까지 남아 있던 굵은 무늬랑 평균 색이 먼저 나오고, 가장 먼저 사라진 가는 무늬는 맨 나중에 채워지네요.

이서연 S11
이서연

거꾸로 가면 순서도 거꾸로지. 58번을 버틴 굵은 무늬가 되살릴 때 가장 먼저 돌아오고, 3.6번 만에 사라진 가는 무늬는 가장 나중에 돌아오는 거야.

김민준 M07
김민준

그림 그릴 때 큰 윤곽부터 잡고 머리카락은 마지막에 그리는 것처럼요.

이서연 S09
이서연

급수로 함수를 어림할 때도 그렇지. 앞쪽 몇 항이 큰 모양을 정하고, 뒤로 갈수록 잔무늬만 보태니까.