차원 축소 — PCA, t-SNE, UMAP
고차원 데이터에는 구조가 있습니다. 올바른 각도에서 보면 그 구조를 찾을 수 있습니다.
유형: Build
언어: Python
선수 지식: Phase 1, Lessons 01 (Linear Algebra Intuition), 02 (Vectors, Matrices & Operations), 03 (Eigenvalues & Eigenvectors), 06 (Probability & Distributions)
예상 시간: 약 90분
학습 목표
- PCA를 처음부터 구현합니다. 데이터 중심화(data centering), 공분산 행렬(covariance matrix) 계산, 고유분해(eigendecomposition), 사영(projection)을 수행합니다.
- 설명분산비(explained variance ratio)와 엘보 방법(elbow method)을 사용해 주성분(principal component) 수를 선택합니다.
- MNIST 숫자(MNIST digit)를 2D로 시각화하며 PCA, t-SNE, UMAP의 장단점(tradeoff)을 비교합니다.
- 표준 PCA(standard PCA)가 처리하지 못하는 비선형 데이터 구조(nonlinear data structure)를 분리하기 위해 RBF kernel을 사용하는 커널 PCA(kernel PCA)를 적용합니다.
문제
샘플(sample) 하나마다 특징(feature)이 784개인 데이터셋(dataset)이 있습니다. 손글씨 숫자(handwritten digit)의 픽셀값(pixel value)일 수도 있고, 유전자 발현 수준(gene expression level)일 수도 있고, 사용자 행동 신호(user behavior signal)일 수도 있습니다. 784차원(dimension)은 시각화할 수 없습니다. 그릴(plot) 수도 없고, 머릿속으로 생각하기도 어렵습니다.
하지만 그 784개 특징(feature) 대부분은 중복(redundant)됩니다. 실제 정보(information)는 훨씬 작은 표면(surface) 위에 있습니다. 손글씨 숫자 "7"을 설명하는 데 784개의 독립 숫자(independent number)가 필요하지 않습니다. 획의 각도(stroke angle), 가로획 길이(crossbar length), 얼마나 기울었는지 같은 몇 가지가 중요합니다. 나머지는 잡음(noise)입니다.
차원 축소(Dimensionality Reduction)는 그 작은 표면(surface)을 찾습니다. 784차원 데이터(784-dimensional data)를 2, 10, 50차원으로 압축(compress)하면서 중요한 구조(structure)를 유지합니다.
개념
차원의 저주
고차원 공간은 직관적이지 않습니다. 차원(dimension)이 커지면 세 가지가 깨집니다.
거리(distance)가 의미를 잃습니다. 고차원에서는 임의의 두 점(random point) 사이 거리가 거의 같은 값으로 수렴합니다. 모든 점이 서로 비슷한 거리에 있으면 최근접 이웃 탐색(nearest-neighbor search)이 제대로 동작하기 어렵습니다.
차원 임의의 두 점 사이 평균 거리 비율 (max/min)
2 ~5.0
10 ~1.8
100 ~1.2
1000 ~1.02
부피(volume)가 모서리(corner)에 집중됩니다. d차원 단위 초입방체(unit hypercube)에는 2^d개의 모서리가 있습니다. 100차원에서는 부피 대부분이 중심에서 멀리 떨어진 모서리에 모여 있습니다. 데이터 점(data point)은 가장자리로 퍼지고, 모델(model)은 내부 데이터가 부족해집니다.
지수적으로 더 많은 데이터(exponentially more data)가 필요합니다. 같은 표본 밀도(sample density)를 유지하려면 2차원에서 20차원으로 가는 것만으로도 10^18배 더 많은 데이터가 필요합니다. 충분한 데이터는 결코 확보되지 않습니다. 차원을 줄이면 데이터 밀도(data density)가 다시 다룰 수 있는 수준으로 돌아옵니다.
PCA: 중요한 방향 찾기
주성분분석(PCA, Principal Component Analysis)은 데이터 분산(data variance)이 가장 큰 축(axis)을 찾습니다. 좌표계(coordinate system)를 회전해 첫 번째 축이 가장 큰 분산(variance)을, 두 번째 축이 그다음 분산을 잡도록 만듭니다.
알고리즘:
1. 데이터 중심화(center) (각 특징(feature)에서 평균(mean)을 뺌)
2. 공분산(covariance) 계산 (특징들이 함께 움직이는 방식)
3. 고유분해(eigendecomposition) (주성분 방향(principal direction) 찾기)
4. 고유값(eigenvalue) 기준 정렬 (분산(variance)이 큰 순서대로)
5. 사영(project) (상위 k개 고유벡터(eigenvector)만 유지하고 나머지는 버림)
왜 고유분해(eigendecomposition)를 사용할까요? 공분산 행렬(covariance matrix)은 대칭(symmetric)이며 양의 준정부호(positive semi-definite)입니다. 고유벡터(eigenvector)는 특징 공간(feature space)의 직교 방향(orthogonal direction)입니다. 고유값(eigenvalue)은 각 방향이 포착(capture)하는 분산이 얼마인지 알려 줍니다. 가장 큰 고유값을 가진 고유벡터는 분산이 최대인 방향(maximum variance direction)을 가리킵니다.
graph LR
A["Original data (2D)\nData spread in both\nx and y directions"] -->|"PCA rotation"| B["After PCA\nPC1 captures the elongated spread\nPC2 captures the narrow spread\nDrop PC2 and you lose little info"]
- PCA 이전: 데이터 구름(data cloud)이 x축과 y축 양쪽에 걸쳐 대각선 방향으로 퍼져 있습니다.
- PCA 이후: 좌표계가 회전해 PC1은 분산이 최대인 방향, PC2는 분산이 최소인 방향과 맞춰집니다.
- 차원 축소: PC2를 버리고 PC1에 사영(project)해도 정보 손실(information loss)이 작습니다.
설명분산비(Explained variance ratio)
각 주성분(principal component)은 총분산(total variance)의 일부를 포착(capture)합니다. 설명분산비(explained variance ratio)는 그 비율입니다.
주성분 고유값 설명분산비 누적
PC1 4.73 0.473 0.473
PC2 2.51 0.251 0.724
PC3 1.12 0.112 0.836
PC4 0.89 0.089 0.925
...
누적 설명분산(cumulative explained variance)이 0.95에 도달하면 해당 주성분 수가 정보의 95%를 포착한다는 뜻입니다. 그 뒤는 대체로 잡음(noise)입니다.
성분(Component) 수 고르기
세 가지 전략이 있습니다.
- 임계값(threshold). 분산의 90-95%를 설명할 만큼 주성분을 유지합니다.
- 엘보 방법(elbow method). 주성분별 설명분산을 그래프로 그리고 급격히 꺾이는 지점을 찾습니다.
- 다운스트림 성능(downstream performance). PCA를 전처리(preprocessing)로 사용하면서
k 값을 바꿔가며(sweep) 모델 정확도(accuracy)를 측정합니다. 정확도가 더 이상 오르지 않고 평탄해지는(plateau) 지점이 적절한 k입니다.
t-SNE: 이웃 관계(neighborhood) 보존하기
t-SNE(t-Distributed Stochastic Neighbor Embedding)는 시각화(visualization)를 위해 설계되었습니다. 고차원 데이터(high-dimensional data)를 2D 또는 3D로 매핑(mapping)하되, 어떤 점(point)들이 서로 가까웠는지 보존합니다.
직관(intuition)은 이렇습니다. 원래 공간(original space)에서 점 쌍(point pair)의 거리(distance)를 바탕으로 확률분포(probability distribution)를 계산합니다. 가까운 점은 높은 확률(high probability), 먼 점은 낮은 확률(low probability)을 가집니다. 그런 다음 2D 배치(arrangement)에서도 같은 확률분포가 유지되도록 찾습니다. 784차원에서 이웃(neighbor)이었던 점이 2D에서도 이웃으로 남습니다.
t-SNE의 주요 특성:
- 비선형(non-linear)입니다. PCA가 펼치지 못하는 복잡한 다양체(complex manifold)를 펼칠 수 있습니다.
- 확률적(stochastic)입니다. 실행(run)마다 레이아웃이 달라질 수 있습니다.
- 퍼플렉서티(perplexity) 매개변수는 고려할 이웃(neighbor) 수를 제어합니다. 보통 5-50 사이 값을 씁니다.
- 출력에서 군집(cluster) 사이 거리는 의미가 없습니다. 군집 자체만 보아야 합니다.
- 대규모 데이터셋에서는 느립니다. 기본적으로 시간 복잡도가
O(n^2)입니다.
UMAP: 더 빠르고 전역 구조(global structure)를 더 잘 보존
UMAP(Uniform Manifold Approximation and Projection)은 t-SNE와 비슷하지만 두 가지 장점이 있습니다.
- 더 빠릅니다. 모든 쌍별 거리(pairwise distance)를 계산하지 않고 근사 최근접 이웃 그래프(approximate nearest-neighbor graph)를 사용합니다.
- 전역 구조(global structure)가 더 잘 보존됩니다. 출력(output)에서 군집(cluster)의 상대 위치(relative position)가 t-SNE보다 의미 있는 경우가 많습니다.
UMAP은 고차원 공간에서 가중 그래프(weighted graph), 즉 퍼지 위상 표현(fuzzy topological representation)을 만들고, 이를 최대한 보존하는 저차원 레이아웃을 찾습니다.
주요 매개변수:
n_neighbors: 국소 구조(local structure)를 정의하는 이웃 수입니다. 퍼플렉서티와 비슷한 역할을 하며, 값이 클수록 전역 구조를 더 보존합니다.
min_dist: 출력에서 점이 얼마나 촘촘히 모이는지 결정합니다. 작을수록 조밀한 군집(dense cluster)을 만듭니다.
무엇을 언제 쓸까
| 방법 | 사용 사례 | 보존 대상 | 속도 |
|---|
| PCA | 학습(training) 전 전처리(preprocessing) | 전역 분산(global variance) | 빠름. 정확(exact)하며 표본 수가 매우 많아도 가능 |
| PCA | 빠른 탐색적 시각화(exploratory visualization) | 선형 구조(linear structure) | 빠름 |
| t-SNE | 출판 품질(publication-quality)의 2D 그림 | 국소 이웃(local neighborhood) | 느림. 표본 수 1만 개 이하가 이상적 |
| UMAP | 대규모 2D 시각화 | 국소 구조와 일부 전역 구조 | 중간. 수백만 표본도 처리 가능 |
| PCA | 모델용 특징 축소(feature reduction) | 분산 순위로 정렬된 특징(variance-ranked features) | 빠름 |
| t-SNE / UMAP | 군집 구조(cluster structure) 이해 | 군집 분리(cluster separation) | 중간에서 느림 |
경험칙(rule of thumb): 전처리와 데이터 압축에는 PCA를 씁니다. 2차원에서 구조를 살펴보고 싶을 때는 t-SNE 또는 UMAP을 씁니다.
커널 PCA(Kernel PCA)
표준 PCA(standard PCA)는 선형 부분공간(linear subspace)을 찾습니다. 좌표계를 회전한 뒤 축을 버리는 방식입니다. 하지만 데이터가 비선형 다양체(nonlinear manifold) 위에 있다면 어떨까요? 2차원의 원(circle)은 어떤 직선으로도 분리되지 않습니다. 표준 PCA로는 해결할 수 없습니다.
커널 PCA(kernel PCA)는 커널 함수(kernel function)가 유도하는 고차원 특징 공간(high-dimensional feature space)에서 PCA를 적용합니다. 그 특징 공간의 좌표를 명시적으로 계산하지는 않습니다. 이것이 바로 커널 트릭(kernel trick)이며, 서포트 벡터 머신(SVM) 뒤에 있는 아이디어와 같습니다.
알고리즘:
- 커널 행렬(kernel matrix)
K를 계산합니다. K_ij = k(x_i, x_j)입니다.
- 특징 공간에서 커널 행렬을 중심화(center)합니다.
- 중심화한 커널 행렬을 고유분해(eigendecompose)합니다.
- 상위 고유벡터를
1/sqrt(eigenvalue)로 정규화(scale)해 사영(projection)으로 사용합니다.
자주 쓰는 커널 함수:
| 커널 | 공식 | 적합한 경우 |
|---|
| RBF (Gaussian) | `exp(-gamma * | |
| 다항식(polynomial) | (x . y + c)^d | 다항 관계 |
| 시그모이드(sigmoid) | tanh(alpha * x . y + c) | 인공신경망 형태의 매핑 |
커널 PCA와 표준 PCA 비교:
| 기준 | 표준 PCA | 커널 PCA |
|---|
| 데이터 구조 | 선형 부분공간 | 비선형 다양체 |
| 속도 | O(min(n^2 d, d^2 n)) | O(n^2 d + n^3) |
| 해석 가능성 | 주성분이 특징의 선형 결합(linear combination) | 주성분을 원본 특징으로 직접 해석하기 어려움 |
| 확장성 | 수백만 표본까지 처리 가능 | 커널 행렬이 n x n이라 메모리에 제약 |
| 복원(reconstruction) | 역변환(inverse transform) 직접 가능 | 사전 이미지 근사(pre-image approximation) 필요 |
대표 예시는 동심원(concentric circles)입니다. 2차원 평면에 두 개의 원이 있고 하나는 안쪽, 하나는 바깥쪽입니다. 표준 PCA는 두 원을 같은 직선에 사영하기 때문에 분류에는 쓸모가 없습니다. RBF 커널 PCA는 안쪽 원과 바깥쪽 원을 서로 다른 영역으로 매핑해 선형 분리(linearly separable)가 가능하게 만듭니다.
복원 오차(reconstruction error)
차원 축소가 얼마나 잘 되었는지 어떻게 판단할까요? 784차원을 50차원으로 압축했다면, 무엇을 잃었는지 측정해야 합니다.
복원 오차는 다음과 같이 계산합니다.
- 데이터를
k차원으로 사영합니다. X_reduced = X @ W_k
- 다시 복원합니다.
X_hat = X_reduced @ W_k^T
- 평균제곱오차(MSE)를 계산합니다.
mean((X - X_hat)^2)
PCA에서는 복원 오차와 설명분산 사이에 다음과 같은 깔끔한 관계가 성립합니다.
복원 오차 = 포함하지 않은 고유값의 합
총 분산 = 모든 고유값의 합
잃어버린 비율 = (버린 고유값의 합) / (모든 고유값의 합)
각 주성분의 설명분산비는 다음과 같습니다.
explained_ratio_k = eigenvalue_k / sum(all eigenvalues)
누적 설명분산을 주성분 수에 대해 그래프로 그리면 엘보 곡선(elbow curve)이 나타납니다. 적절한 주성분 수는 다음 기준으로 고릅니다.
- 곡선이 평탄해지는 지점, 즉 한계 효용(diminishing returns)이 시작되는 곳
- 누적 분산이 임계값(보통 0.90 또는 0.95)을 넘는 곳
- 다운스트림 태스크(downstream task)의 성능이 평탄해지는 곳
복원 오차는 k 선택 외에도 이상치 탐지(anomaly detection)에 쓸 수 있습니다. 복원 오차가 큰 표본은 학습된 부분공간에 잘 맞지 않는 이상치(outlier)입니다. 운영 시스템(production system)에서 사용하는 PCA 기반 이상치 탐지의 토대가 됩니다.
직접 만들기
Step 1: 처음부터 구현하는 PCA
import numpy as np
class PCA:
def __init__(self, n_components):
self.n_components = n_components
self.components = None
self.mean = None
self.eigenvalues = None
self.explained_variance_ratio_ = None
def fit(self, X):
self.mean = np.mean(X, axis=0)
X_centered = X - self.mean
cov_matrix = np.cov(X_centered, rowvar=False)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
sorted_idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[sorted_idx]
eigenvectors = eigenvectors[:, sorted_idx]
self.components = eigenvectors[:, :self.n_components].T
self.eigenvalues = eigenvalues[:self.n_components]
total_var = np.sum(eigenvalues)
self.explained_variance_ratio_ = self.eigenvalues / total_var
return self
def transform(self, X):
X_centered = X - self.mean
return X_centered @ self.components.T
def fit_transform(self, X):
self.fit(X)
return self.transform(X)
Step 2: 합성 데이터(synthetic data)로 검증하기
np.random.seed(42)
n_samples = 500
t = np.random.uniform(0, 2 * np.pi, n_samples)
x1 = 3 * np.cos(t) + np.random.normal(0, 0.2, n_samples)
x2 = 3 * np.sin(t) + np.random.normal(0, 0.2, n_samples)
x3 = 0.5 * x1 + 0.3 * x2 + np.random.normal(0, 0.1, n_samples)
X_synthetic = np.column_stack([x1, x2, x3])
pca = PCA(n_components=2)
X_reduced = pca.fit_transform(X_synthetic)
print(f"Original shape: {X_synthetic.shape}")
print(f"Reduced shape: {X_reduced.shape}")
print(f"Explained variance ratios: {pca.explained_variance_ratio_}")
print(f"Total variance captured: {sum(pca.explained_variance_ratio_):.4f}")
Step 3: MNIST 숫자를 2차원으로 보기
from sklearn.datasets import fetch_openml
mnist = fetch_openml("mnist_784", version=1, as_frame=False, parser="auto")
X_mnist = mnist.data[:5000].astype(float)
y_mnist = mnist.target[:5000].astype(int)
pca_mnist = PCA(n_components=50)
X_pca50 = pca_mnist.fit_transform(X_mnist)
print(f"50 components capture {sum(pca_mnist.explained_variance_ratio_):.2%} of variance")
pca_2d = PCA(n_components=2)
X_pca2d = pca_2d.fit_transform(X_mnist)
print(f"2 components capture {sum(pca_2d.explained_variance_ratio_):.2%} of variance")
Step 4: scikit-learn과 비교하기
from sklearn.decomposition import PCA as SklearnPCA
from sklearn.manifold import TSNE
sklearn_pca = SklearnPCA(n_components=2)
X_sklearn_pca = sklearn_pca.fit_transform(X_mnist)
print(f"\nOur PCA explained variance: {pca_2d.explained_variance_ratio_}")
print(f"Sklearn PCA explained variance: {sklearn_pca.explained_variance_ratio_}")
diff = np.abs(np.abs(X_pca2d) - np.abs(X_sklearn_pca))
print(f"Max absolute difference: {diff.max():.10f}")
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
X_tsne = tsne.fit_transform(X_mnist)
print(f"\nt-SNE output shape: {X_tsne.shape}")
Step 5: UMAP 비교
try:
from umap import UMAP
reducer = UMAP(n_components=2, n_neighbors=15, min_dist=0.1, random_state=42)
X_umap = reducer.fit_transform(X_mnist)
print(f"UMAP output shape: {X_umap.shape}")
except ImportError:
print("umap-learn을 설치하세요: pip install umap-learn")
사용해보기
분류기(classifier) 학습 전에 PCA를 전처리(preprocessing)로 사용합니다.
from sklearn.decomposition import PCA as SklearnPCA
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X_train, X_test, y_train, y_test = train_test_split(
X_mnist, y_mnist, test_size=0.2, random_state=42
)
results = {}
for k in [10, 30, 50, 100, 200]:
pca_k = SklearnPCA(n_components=k)
X_tr = pca_k.fit_transform(X_train)
X_te = pca_k.transform(X_test)
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_tr, y_train)
acc = accuracy_score(y_test, clf.predict(X_te))
var_captured = sum(pca_k.explained_variance_ratio_)
results[k] = (acc, var_captured)
print(f"k={k:>3d} accuracy={acc:.4f} variance={var_captured:.4f}")
성능(performance)은 784차원에 도달하기 전에 평탄해집니다(plateau). 그 평탄해지는 지점이 운영 지점입니다.
산출물 만들기
이 lesson의 검수 대상은 아래 두 가지입니다.
code/dim_reduction.py: PCA, 커널 PCA, 복원 오차, 합성 데이터 데모를 포함한 실행 가능한 예제
outputs/skill-dimensionality-reduction.md: 목적과 제약에 맞는 차원 축소 기법을 고르는 skill
연습문제
- (쉬움) PCA 클래스가
inverse_transform을 지원하도록 수정합니다. MNIST 숫자를 10, 50, 200개의 주성분으로 복원(reconstruct)하고 원본과의 평균제곱오차(mean squared difference)를 출력합니다.
- (중간) 같은 MNIST 부분집합에 대해 퍼플렉서티 값을 5, 30, 100으로 바꾸어 가며 t-SNE를 실행합니다. 출력이 어떻게 바뀌나요? 퍼플렉서티가 군집의 조밀도(cluster tightness)에 영향을 주는 이유는 무엇인가요?
- (어려움)
sklearn.datasets.make_classification을 사용해 50개 특징 중 5개만 정보를 담은(informative) 데이터셋을 만듭니다. PCA를 적용한 뒤, 설명분산 곡선(explained variance curve)이 데이터의 실질 차원(effective dimension)이 5라는 사실을 잘 보여 주는지 확인합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 차원의 저주(curse of dimensionality) | "특징이 너무 많다" | 차원이 커질수록 거리, 부피, 데이터 밀도가 직관과 다르게 변한다. 이를 보상하려면 모델은 지수적으로 더 많은 데이터를 필요로 한다 |
| PCA | "차원을 줄인다" | 좌표계를 분산이 최대인 방향에 맞게 회전하고 분산이 작은 축을 버리는 방법 |
| 주성분(principal component) | "중요한 방향" | 공분산 행렬의 고유벡터. 데이터 분산이 큰 특징 공간상의 방향 |
| 설명분산비(explained variance ratio) | "주성분이 담은 정보량" | 총 분산 중 하나의 주성분이 포착하는 비율. 상위 k개의 비율을 합하면 k개 주성분이 보존하는 분산량을 알 수 있다 |
| 공분산 행렬(covariance matrix) | "특징 사이의 상관" | 항목 (i, j)가 특징 i와 j가 함께 움직이는 정도를 나타내는 대칭 행렬. 대각 원소는 각 특징의 분산이다 |
| t-SNE | "군집 그래프" | 쌍별 이웃 확률(pairwise neighborhood probability)을 보존해 고차원 데이터를 2차원으로 매핑하는 비선형 방법. 시각화용이며 전처리용은 아니다 |
| UMAP | "더 빠른 t-SNE" | 위상적 데이터 분석(topological data analysis)에 기반한 비선형 방법. 국소 구조와 일부 전역 구조를 모두 보존하며 t-SNE보다 확장성이 좋다 |
| 퍼플렉서티(perplexity) | "t-SNE의 조절 다이얼" | 각 점이 고려하는 실효 이웃 수를 제어한다. 값이 낮으면 매우 국소적인 구조에, 값이 높으면 더 넓은 패턴에 집중한다 |
| 다양체(manifold) | "데이터가 놓인 표면" | 고차원 공간 안에 내재된(embedded) 저차원 표면. 3차원에 구겨진 종이는 2차원 다양체다 |
더 읽을거리