지난 글에서 PCA로 선형 차원 축소를 배웠다. PCA는 빠르고 재현 가능하지만, 비선형 구조(곡면, 군집)는 잘 포착하지 못한다는 한계가 있다. 784차원 MNIST 손글씨 데이터나 수천 차원의 LLM 임베딩을 2D로 펼쳐봤을 때, 숫자별 군집이 선명하게 보이면 데이터 구조가 건강하다는 신호다. 이 역할을 하는 것이 t-SNE와 UMAP이다. 둘 다 비선형 차원 축소 알고리즘으로, 고차원의 지역 구조(Local Structure)를 저차원에 충실히 재현하는 데 특화되어 있다.
PCA만으로 부족한 이유
PCA는 전역 분산을 최대화하는 선형 투영을 찾는다. 따라서 비선형 매니폴드(Swiss Roll, 동심원 등)에는 한계가 있다.
import numpy as np
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# MNIST 손글씨 데이터 (8x8 = 64차원)
digits = load_digits()
X = digits.data # (1797, 64)
y = digits.target
# PCA로 2차원 축소
pca = PCA(n_components=2, random_state=42)
X_pca = pca.fit_transform(X)
# 클래스 분리 품질 확인
from sklearn.metrics import silhouette_score
sil_pca = silhouette_score(X_pca, y)
print(f"PCA 2D 실루엣 계수: {sil_pca:.4f}")
# 보통 0.1~0.2 수준 — 군집이 많이 겹침
t-SNE: 확률 분포로 이웃 구조 보존
t-SNE(t-Distributed Stochastic Neighbor Embedding)는 2008년 Laurens van der Maaten과 Geoffrey Hinton이 발표했다. 핵심 아이디어는 다음과 같다.
고차원 공간: 각 점의 이웃 확률을 가우시안 분포로 모델링
p(j|i) = exp(-||xᵢ-xⱼ||² / 2σᵢ²) / Σₖ≠ᵢ exp(-||xᵢ-xₖ||² / 2σᵢ²)
저차원 공간: 이웃 확률을 t-분포로 모델링 (꼬리가 두꺼워 먼 점을 더 멀리 밀어냄)
q(j|i) = (1 + ||yᵢ-yⱼ||²)⁻¹ / Σₖ≠ᵢ (1 + ||yᵢ-yₖ||²)⁻¹
두 분포의 KL-Divergence를 최소화해 저차원 좌표를 최적화한다.
from sklearn.manifold import TSNE
import time
# t-SNE 기본 사용
start = time.time()
tsne = TSNE(
n_components=2,
perplexity=30, # 유효 이웃 수 (5~50 권장)
learning_rate='auto',
n_iter=1000,
random_state=42,
n_jobs=-1
)
X_tsne = tsne.fit_transform(X)
print(f"t-SNE 완료: {time.time()-start:.1f}초")
sil_tsne = silhouette_score(X_tsne, y)
print(f"t-SNE 2D 실루엣 계수: {sil_tsne:.4f}")
# 보통 0.4~0.6 수준 — PCA보다 훨씬 선명한 군집
perplexity: t-SNE의 핵심 하이퍼파라미터
perplexity는 각 점의 "유효 이웃 수"를 제어한다. 직관적으로는 각 가우시안의 너비 σᵢ를 결정한다.
# perplexity 값에 따른 결과 차이
for perp in [5, 15, 30, 50, 100]:
tsne = TSNE(n_components=2, perplexity=perp,
random_state=42, n_iter=500)
X_t = tsne.fit_transform(X)
sil = silhouette_score(X_t, y)
print(f"perplexity={perp:3d}: 실루엣={sil:.4f}")
# perplexity 가이드:
# 작은 값 (5~10): 지역 구조 강조, 군집 내 세부 구조 보임
# 중간 값 (20~50): 균형 (권장 범위)
# 큰 값 (>100): 전역 구조 강조, 군집 간 간격 의미 있음
# 데이터 크기의 1/10 ~ 1/5 정도가 경험적 출발점
UMAP: 빠르고 전역 구조도 보존
UMAP(Uniform Manifold Approximation and Projection)은 2018년 발표된 알고리즘으로, 위상 수학(Algebraic Topology)에 기반한다. t-SNE의 단점인 느린 속도와 전역 구조 소실을 개선했다.
# umap-learn 설치 필요: pip install umap-learn
import umap
start = time.time()
reducer = umap.UMAP(
n_components=2,
n_neighbors=15, # 지역/전역 균형 (5~50 권장)
min_dist=0.1, # 군집 내 점 간격 (0.0~0.99)
metric='euclidean',
random_state=42
)
X_umap = reducer.fit_transform(X)
print(f"UMAP 완료: {time.time()-start:.1f}초")
# t-SNE보다 5~10배 빠름
sil_umap = silhouette_score(X_umap, y)
print(f"UMAP 2D 실루엣 계수: {sil_umap:.4f}")
# 새 데이터 변환 가능 (t-SNE는 불가)
X_new_umap = reducer.transform(X[:10])
print(f"새 데이터 변환: {X_new_umap.shape}")
n_neighbors와 min_dist: UMAP 핵심 파라미터
# n_neighbors: 지역 vs 전역 구조 균형
# 작은 값 (5~10): 지역 구조 강조, 세밀한 군집
# 큰 값 (50~200): 전역 구조 강조, 큰 그림
# min_dist: 저차원 공간에서 점들의 최소 거리
# 작은 값 (0.0~0.1): 군집 내 점이 빽빽하게 뭉침
# 큰 값 (0.5~0.99): 점들이 더 넓게 퍼짐
for n_nbrs in [5, 15, 30, 50]:
for min_d in [0.0, 0.1, 0.5]:
red = umap.UMAP(n_neighbors=n_nbrs, min_dist=min_d,
random_state=42)
Xu = red.fit_transform(X)
sil = silhouette_score(Xu, y)
print(f"n_neighbors={n_nbrs:2d}, "
f"min_dist={min_d}: {sil:.4f}")
실전 워크플로: PCA → t-SNE/UMAP
고차원 데이터 직접 적용보다 PCA로 먼저 줄이는 것이 표준 패턴이다.
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
import umap
# 단계 1: PCA로 50차원으로 축소 (노이즈 제거 + 속도 향상)
pca50 = PCA(n_components=50, random_state=42)
X_pca50 = pca50.fit_transform(X)
print(f"PCA 누적 분산: "
f"{pca50.explained_variance_ratio_.sum():.1%}")
# 단계 2a: t-SNE로 2D 시각화
tsne = TSNE(n_components=2, perplexity=30,
random_state=42, n_jobs=-1)
X_2d_tsne = tsne.fit_transform(X_pca50)
# 단계 2b: UMAP으로 2D 시각화
reducer = umap.UMAP(n_components=2, n_neighbors=15,
min_dist=0.1, random_state=42)
X_2d_umap = reducer.fit_transform(X_pca50)
# 단계 3: 시각화 (클래스별 색상)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
for ax, X_2d, title in zip(
axes,
[X_2d_tsne, X_2d_umap],
['t-SNE', 'UMAP']
):
scatter = ax.scatter(X_2d[:, 0], X_2d[:, 1],
c=y, cmap='tab10', s=5, alpha=0.7)
ax.set_title(title)
plt.colorbar(scatter, ax=axes[1])
plt.savefig('/tmp/tsne_umap_viz.png', dpi=120, bbox_inches='tight')
t-SNE vs UMAP vs PCA 비교
| 항목 | PCA | t-SNE | UMAP |
|---|---|---|---|
| 기반 원리 | 선형 투영 | 확률 분포 (KL) | 위상 수학 |
| 지역 구조 보존 | 보통 | 우수 | 우수 |
| 전역 구조 보존 | 우수 | 취약 | 좋음 |
| 속도 (10만 샘플) | 초 단위 | 수십 분 | 수 분 |
| 재현성 | 완벽 | 낮음 | 중간 |
| 새 데이터 변환 | 가능 | 불가 | 가능 |
| 하이퍼파라미터 | 거의 없음 | perplexity | n_neighbors, min_dist |
| 주 용도 | 전처리, 노이즈 제거 | 탐색적 시각화 | 시각화 + 임베딩 |
시각화 해석 시 주의사항
비선형 차원 축소의 결과를 잘못 해석하면 오류를 범할 수 있다.
# ⚠️ 흔한 실수들
# 1. 군집 간 거리 해석 금지
# t-SNE 2D에서 A군집이 B군집보다 C군집에 가깝다고 해서
# 고차원에서도 그렇지는 않다
# 2. 군집 크기 해석 주의
# t-SNE는 밀도를 균일하게 만드는 경향 → 실제 밀도 반영 안 됨
# 3. perplexity/n_neighbors 변경 시 구조 변화
# 여러 하이퍼파라미터로 실행해서 일관된 패턴만 신뢰
# 올바른 사용:
# - "이 두 클래스는 저차원에서도 분리된다" ✓
# - "특성 A와 B가 서로 유사한 군집을 형성한다" ✓
# - "군집 1이 군집 2보다 3배 크다" ✗ (t-SNE에서)
LLM 임베딩 시각화 예시
t-SNE와 UMAP은 LLM 연구에서도 핵심 도구다.
# sentence-transformers로 임베딩 후 UMAP 시각화
from sentence_transformers import SentenceTransformer
sentences = [
"고양이가 소파 위에 앉아있다",
"강아지가 공원에서 뛰어논다",
# ... 수백 개의 문장
]
model = SentenceTransformer('jhgan/ko-sroberta-multitask')
embeddings = model.encode(sentences) # (N, 768)
# UMAP으로 2D 시각화
reducer = umap.UMAP(n_neighbors=10, min_dist=0.05,
random_state=42)
emb_2d = reducer.fit_transform(embeddings)
# 유사한 문장들이 2D에서 가까이 모이는지 확인
print(f"임베딩 차원: {embeddings.shape}")
print(f"2D 축소 완료: {emb_2d.shape}")
t-SNE와 UMAP은 데이터 탐색, 모델 디버깅, 임베딩 품질 검증의 필수 도구다. 특히 LLM 시대에 수백~수천 차원의 임베딩을 다루는 일이 많아지면서 그 중요성이 더욱 커졌다. 이번 글로 지도 학습의 분류·회귀 알고리즘과 비지도 학습의 군집화·차원 축소를 모두 다뤘다. 다음 단계로는 신경망과 딥러닝의 세계가 기다린다.
지난 글: PCA: 고차원 데이터를 압축하는 차원 축소
읽어주셔서 감사합니다. 😊

