가우시안 프로세스(Gaussian Processes) 밑바닥부터 구현하기: 함수 위에 사전 확률(prior)을 설정하고 Cholesky 분해를
요약
가우시안 프로세스(Gaussian Process)의 수학적 원리를 밑바닥부터 구현하는 방법을 설명합니다. 커널 선택의 중요성과 Cholesky 분해를 이용한 효율적인 사후 평균 계산 과정을 다룹니다.
핵심 포인트
- GP는 가중치 대신 함수 전체에 대한 분포를 유지함
- 커널(Kernel) 선택이 모델의 특성을 결정하는 핵심 요소임
- Cholesky 분해를 통해 수치적 안정성과 계산 효율성을 확보함
- RBF 커널의 신호 분산과 길이 척도 조절 원리 설명
일반적인 회귀(Ordinary regression)는 가중치(weights)를 맞춥니다. 반면 가우시안 프로세스(Gaussian Process)는 가중치를 버리고 **전체 함수(whole functions)**에 대한 분포를 유지합니다. 즉, 고정된 형태가 전혀 없습니다. 당신은 기저 함수(basis functions)를 선택하는 것이 아니라, 커널(kernel)(어떤 입력이 유사한 출력을 내야 하는지에 대한 개념)을 선택하며, 수학적 계산을 통해 평균 곡선(mean curve), 음영 처리된 불확실성 밴드(uncertainty band), 그리고 실제 샘플링된 함수들을 돌려받게 됩니다. 저는 클릭하여 관측치를 추가하면 밴드가 숨 쉬듯 움직이는 인터랙티브 데모를 제작했습니다. 모든 곡선은 실제 Cholesky 분해(Cholesky solve)를 통해 브라우저에서 실시간으로 조건화(conditioned)됩니다. 작동 원리는 다음과 같습니다.
커널이 모델의 전부입니다
GP는 커널, 즉 두 입력 사이의 출력 간 공분산(covariance)에 의해 완전히 정의됩니다. 제곱 지수(squared-exponential, RBF) 커널은 인접한 입력들이 강하게 상관되어 있으며, 그 상관관계가 거리에 따라 부드럽게 감소한다고 가정합니다. 여기에는 두 개의 조절 노브가 있습니다: 신호 분산(signal variance)인 σ_f²(함수가 얼마나 크게 변동하는지), 그리고 길이 척도(length-scale)인 ℓ(두 입력이 서로에게 영향을 미치지 않게 되는 속도)입니다. ℓ을 조절하면 적합(fit) 결과가 구불구불한 형태에서 매끄러운 형태로 변합니다. 이 단 하나의 선택이 전체 시스템을 움직이게 만듭니다.
import numpy as np
def rbf(a, b, sf2=1.0, ell=1.0):
d = a[:, None] - b[None, :] # 모든 쌍별 입력 차이
...
설계 행렬(design matrix) 없이 세 개의 커널 행렬로
조건화(Conditioning)를 위해서는 하나의 커대한 결합 공분산(joint covariance) 내의 세 가지 블록이 필요합니다: 관측된 입력들 사이의 K(대각 성분에는 관측 노이즈 포함), 테스트 입력과 관측된 입력 사이의 K*, 그리고 테스트 입력들 사이의 K**입니다. 그게 전부입니다. 가중치도, 어디에도 적혀 있지 않은 기저 함수도 없습니다.
K = rbf(X, X) + noise*np.eye(len(X)) # train-train (N x N)
Ks = rbf(Xs, X) # test-train (M x N)
Kss = rbf(Xs, Xs) # test-test (M x M)
Cholesky를 이용한 조건화 — 사후 평균(posterior mean)
사후 평균(posterior mean)은 μ* = K*(K + σ_n²I)⁻¹y입니다. 이 역행렬을 직접 계산하는 일은 결코 없습니다. Cholesky 분해를 통해 K = L Lᵀ를 한 번만 구하면 — 수치적으로 안정적이며 비용도 절반으로 줄어듭니다 — 두 개의 삼각 시스템(triangular systems)을 풀 수 있습니다. 그러면 벡터 alpha를 모든 테스트 포인트에 재사용할 수 있습니다. 이것이 데모에서 클릭하거나 슬라이더를 움직일 때마다 실행되는 정확한 계산 방식입니다.
L = np.linalg.cholesky(K) # K = L Lᵀ
alpha = np.linalg.solve(L.T, np.linalg.solve(L, y)) # solve (K) alpha = y
mean = Ks @ alpha # 사후 평균(posterior mean)
데이터에서 밴드가 조여지고, 데이터 사이에서는 사전 확률(prior)로 돌아감
사후 공분산(posterior covariance)은 Σ* = K** − K*(K + σ_n²I)⁻¹K*ᵀ입니다. 즉, 사전 확률(prior)에서 데이터가 설명하는 부분을 빼는 것입니다. 대각 성분은 포인트별 분산(variance)이며, 여기에 2를 곱한 제곱근 값은 음영 처리된 밴드(band)가 됩니다. 관측 지점에서는 빼지는 항이 사전 확률을 거의 상쇄하므로, 밴드가 노이즈 제한 수준의 허리(waist)까지 조여집니다. 어떤 포인트로부터 멀어지면 K*→0이 되어 아무것도 빼지 않게 되며, 다시 사전 확률의 높이인 σ_f로 완화됩니다. 이러한 동작이 바로 핵심적인 이점입니다. 모델이 자신이 무엇을 모르는지 알고 있는 것입니다.
v = np.linalg.solve(L, Ks.T) # L 재사용
cov = Kss - v.T @ v # K** - K* (K)^-1 K*ᵀ
std = np.sqrt(np.clip(np.diag(cov), 0, None))
...
이는 전날 구현했던 베이지안 선형 회귀(Bayesian linear regression)와 가장 극명하게 대비되는 부분입니다. 선형 모델에서는 외삽(extrapolate)할수록 직선 모델의 불확실성이 무한히 커집니다. 반면 GP의 밴드는 모든 관측 지점에서 노이즈 플로어(noise floor)까지 내려갔다가, 포인트 사이와 그 너머에서는 다시 평평한 _천장(ceiling)_으로 올라갑니다. 즉, 데이터가 없는 곳에서 사전 확률보다 더 많이 알고 있다고 주장하지 않습니다.
함수 전체를 샘플링하면 직관적으로 이해됩니다
밴드는 퍼짐 정도를 요약해 줍니다. 실제 함수를 그려보면 직관적으로 이해가 됩니다. 샘플은 전체 테스트 그리드(test grid)에 대한 결합 가우시안 (joint Gaussian) N(mean, Σ*)에서 추출한 값입니다. (안정성을 위해 약간의 지터(jitter)를 더한) Cholesky 인자 Σ*를 구하고, 여기에 표준 정규 분포 노이즈를 통과시킵니다. 이것이 데모에 나타나는 회색 곡선들이며, 모든 곡선은 관측값(observations)을 정확히 통과합니다. 곡선들이 넓게 퍼지는 곳이 바로 불확실성(uncertainty)이 존재하는 지점입니다.
Ls = np.linalg.cholesky(cov + 1e-6*np.eye(len(Xs))) # 지터(jitter)는 양의 정부호(PD)를 유지해 줍니다
for _ in range(10):
f = mean + Ls @ np.random.randn(len(Xs)) # 하나의 그럴듯한 함수
...
한 줄 요약, 그리고 GP가 제 역할을 다하는 곳
scikit-learn은 이 모든 것을 GaussianProcessRegressor로 제공하며, 그 이상의 기능을 수행합니다. 커널(kernel)을 전달하기만 하면 로그 주변 가능도(log marginal likelihood)를 최대화함으로써 길이 척도(length-scale), 신호 분산(signal variance), 노이즈를 스스로 학습합니다. 즉, 슬라이더를 일일이 수동으로 조정할 필요가 없습니다. return_std=True를 요청하면 보정된 오차 막대(error bars)를 무료로 얻을 수 있습니다.
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, WhiteKernel, ConstantKernel as C
kernel = C(1.0) * RBF(length_scale=1.0) + WhiteKernel(noise_level=0.02)
...
GP는 사실 무한한 기저(basis)를 가진 베이지안 선형 회귀(Bayesian linear regression)입니다. 커널 트릭(kernel trick)을 사용하면 커널을 선택하기만 하면 될 뿐, 기저를 직접 작성할 필요가 전혀 없습니다. 비용은 O(N³)의 Cholesky 분해이며, 따라서 N이 클 경우에는 희소(sparse) 또는 유도 지점(inducing-point) 변형 모델을 사용해야 합니다. 데이터가 중소규모이고, 매끄러움(smoothness)이 타당한 가정이며, 정직한 불확실성 밴드가 실제로 중요한 경우—예를 들어 예측(forecasting), 능동 학습(active learning), 베이지안 최적화(Bayesian optimisation)—에 사용하십시오. 그리고 그 밴드는 단순한 장식이 아닙니다. 자연스러운 다음 단계는 그 밴드를 사용하여 다음에 어디를 샘플링할지 결정하는 것입니다.
차트를 클릭하여 점을 찍고 밴드가 조여지는 것을 확인해 보세요:
https://dev48v.infy.uk/ml/day37-gaussian-processes.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기