지난 글에서 PCA로 선형 차원 축소를 배웠다. PCA는 빠르고 재현 가능하지만, 비선형 구조(곡면, 군집)는 잘 포착하지 못한다는 한계가 있다. 784차원 MNIST 손글씨 데이터나 수천 차원의 LLM 임베딩을 2D로 펼쳐봤을 때, 숫자별 군집이 선명하게 보이면 데이터 구조가 건강하다는 신호다. 이 역할을 하는 것이 t-SNE와 UMAP이다. 둘 다 비선형 차원 축소 알고리즘으로, 고차원의 지역 구조(Local Structure)를 저차원에 충실히 재현하는 데 특화되어 있다.
여기서 지역 구조란 "각 점의 가까운 이웃이 누구인가"라는 정보이고, 전역 구조는 "군집과 군집이 서로 얼마나 떨어져 있는가"라는 정보다. 두 알고리즘은 앞의 것을 지키려고 뒤의 것을 어느 정도 포기한다. 그 포기가 어디서 생기는지를 알아야 그림을 읽을 때 속지 않는다. 이 글은 PCA가 막히는 자리에서 출발해 t-SNE의 확률 모형, perplexity, UMAP의 이웃 그래프를 차례로 보고, 실제로 돌리는 순서와 해석의 함정으로 끝난다. 숫자는 전부 scikit-learn에 들어 있는 digits 데이터(8×8 손글씨 1,797장)를 직접 돌려 얻은 값이다.
선형 투영의 한계
전역 분산의 제약
PCA는 전역 분산, 곧 데이터 전체가 가장 넓게 퍼진 방향을 찾아 그 축들로 투영한다. 투영은 평면에 그림자를 떨어뜨리는 일이라 선형이다. 원래 공간에서 곧은 선은 그림자에서도 곧고, 멀리 떨어진 두 점은 대개 그림자에서도 떨어져 있다. 이 성질 덕분에 PCA는 전역 구조를 잘 지킨다.
문제는 "가장 넓게 퍼진 방향"이 "군집을 가장 잘 가르는 방향"과 다를 수 있다는 데 있다. 분산은 모든 점을 한꺼번에 보고 계산하는 값이라, 어느 점이 어느 점과 이웃인지는 전혀 따지지 않는다. 열 개의 군집이 64차원 곳곳에 흩어져 있을 때, 그 군집들을 전부 갈라 주는 평면 하나가 존재하리라는 보장은 없다.
digits 실루엣
직접 재 보면 이 한계가 숫자로 나온다. digits를 PCA로 2차원에 내리면 두 축이 설명하는 분산이 전체의 28.5%다. 나머지 71.5%는 그림자에서 사라진다. 그리고 군집이 얼마나 잘 갈렸는지를 재는 실루엣 계수는 0.105가 나왔다. 실루엣 계수는 점마다 "같은 군집 안의 평균 거리"와 "가장 가까운 다른 군집까지의 평균 거리"를 견줘 −1에서 1 사이로 나타낸 값이고, 0 근처면 군집이 서로 겹쳐 있다는 뜻이다.
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
X, y = load_digits(return_X_y=True) # (1797, 64)
pca = PCA(n_components=2, random_state=42)
X_pca = pca.fit_transform(X)
print(pca.explained_variance_ratio_.sum()) # 0.285
print(silhouette_score(X_pca, y)) # 0.105
0.105라는 값은 산점도로 보면 몇몇 숫자만 가장자리에 떨어져 나가고, 나머지 숫자 대부분이 가운데에서 한 덩어리로 겹친 모양이다. 64차원에서는 멀쩡히 떨어져 있던 군집들이, 그림자로 떨어지는 순간 같은 자리에 포개진다.
휘어진 매니폴드
더 극단적인 예가 스위스 롤이다. 종이 한 장을 둘둘 말아 3차원에 놓은 모양인데, 데이터가 실제로 사는 곳은 2차원 종이다. 이렇게 고차원 안에 낮은 차원으로 휘어 들어앉은 면을 매니폴드라 부른다. 스위스 롤을 어느 평면으로 투영하든 말린 겹과 겹이 포개진다. 종이 위에서는 멀리 있던 두 점이 그림자에서는 바로 옆에 선다.
동심원도 같다. 안쪽 원과 바깥 원은 어느 방향으로 눌러도 선분 위에서 겹친다. 이런 구조를 풀려면 "평면을 하나 골라 누른다"는 발상을 버리고 "가까운 이웃끼리는 가깝게 놓는다"는 규칙만 남겨야 한다. 이웃 관계를 보존하는 이 방향이 비선형 차원 축소이고, t-SNE와 UMAP이 그 대표다.
t-SNE의 두 분포
가우시안 이웃 확률
t-SNE(t-Distributed Stochastic Neighbor Embedding)는 2008년 Laurens van der Maaten과 Geoffrey Hinton이 발표했다. 이웃 관계를 거리 그대로 쓰지 않고 확률로 바꾼다는 것이 출발점이다. 점 가 이웃을 하나 고른다면 를 고를 확률을 가우시안으로 둔다.
가까운 점일수록 확률이 크고, 멀어지면 지수적으로 0에 가까워진다. 분모는 확률을 다 더해 1이 되게 맞추는 몫이다. 는 점마다 따로 정하는 가우시안의 폭인데, 이것을 어떻게 정하는지가 다음 절의 perplexity다. 실제 계산에서는 와 를 평균 내 대칭인 로 만들어 쓴다.
저차원 쪽에서도 같은 방식으로 점 가 를 이웃으로 고를 확률 를 만든다. 다만 여기서는 가우시안이 아니라 자유도 1의 t분포를 쓴다.
고차원은 가우시안, 저차원은 t분포라는 비대칭이 이 알고리즘 이름의 "t"다. 학습은 저차원 좌표 를 움직여 가 를 닮게 만드는 일이다.
밀집 문제
왜 굳이 저차원에서 분포를 바꾸는가. 차원이 줄면 이웃을 놓을 자리가 모자라기 때문이다. 10차원에서는 서로 같은 거리에 있는 점을 11개까지 놓을 수 있지만, 2차원 평면에서는 정삼각형의 세 꼭짓점, 곧 3개가 한계다. 고차원에서 "적당히 떨어진" 점들을 2차원에 그 거리 그대로 놓으려 하면 공간이 부족해 전부 가운데로 밀려 들어온다. 이것을 밀집 문제(crowding problem)라 한다.
t분포는 꼬리가 두꺼워 이 문제를 푼다. 거리를 받아 "얼마나 가까운가"를 돌려주는 함수를 핵이라 부르는데, 같은 거리에서 두 핵의 값을 견주면 차이가 분명하다. 가우시안 과 t분포 을 재 보면, 거리 1에서는 0.368과 0.5로 1.4배 차이지만 거리 2에서는 0.018과 0.2로 11배, 거리 3에서는 0.00012와 0.1로 약 810배까지 벌어진다.
고차원에서 가우시안이 "중간 정도의 이웃"에게 주던 작은 확률을, 저차원의 t분포는 훨씬 먼 거리에서야 같은 값으로 내준다. 그러니 중간 거리의 점들은 2차원에서 더 멀리 떨어져 놓여야 확률이 맞는다. 결과적으로 군집 안은 조밀하게, 군집 사이는 넓게 벌어진다. t-SNE 그림의 군집이 유난히 또렷한 이유가 이것이다.
KL 발산의 비대칭
가 를 얼마나 닮았는지는 KL 발산으로 잰다. 두 분포의 차이를 재는 값인데, 대칭이 아니다.
항마다 가 무게로 곱해진다는 점을 보자. 고차원에서 가까운 두 점()을 저차원에서 멀리 놓아 이 되면 그 항은 다. 반대로 고차원에서 먼 두 점()을 저차원에서 가깝게 놓아 가 되면 그 항은 에 그친다. 같은 크기의 실수인데 벌점이 스무 배 가까이 다르다.
그래서 t-SNE는 "이웃을 떼어 놓는 실수"는 강하게 막고 "먼 점을 가깝게 놓는 실수"에는 관대하다. 지역 구조는 지키고 전역 구조는 흘려보낸다는 성격이 목적 함수에 그대로 새겨져 있다. 나중에 군집 사이 거리를 읽지 말라는 경고가 나오는 뿌리가 여기다.
from sklearn.manifold import TSNE
tsne = TSNE(
n_components=2,
perplexity=30, # 기본값 30
learning_rate="auto",
max_iter=1000, # 예전 이름은 n_iter
random_state=42,
)
X_tsne = tsne.fit_transform(X)
print(silhouette_score(X_tsne, y)) # 약 0.55
scikit-learn 1.5부터 반복 횟수 인자 이름이 n_iter에서 max_iter로 바뀌었고, 옛 이름은 이후 버전에서 빠졌다. 옛 코드를 그대로 붙이면 인자 오류가 난다. 같은 digits에서 실루엣이 0.105에서 0.55 안팎으로 오른다. PCA의 다섯 배다.
perplexity
σ 이분 탐색
를 모든 점에 똑같이 주면 곤란하다. 밀집한 영역의 점은 작은 폭으로도 이웃이 수십 개 잡히고, 성긴 영역의 점은 같은 폭으로 이웃이 하나도 안 잡힌다. 그래서 t-SNE는 폭 대신 "이웃을 몇 개쯤 보겠다"를 정하고, 점마다 그 수에 맞는 폭을 찾는다. 그 수가 perplexity다.
는 이웃 확률 분포의 엔트로피, 곧 확률이 얼마나 고르게 퍼져 있는지를 재는 값이다. 이웃 개에게 확률을 똑같이 나눠 주면 perplexity가 정확히 가 된다. 확률이 0.4, 0.3, 0.2, 0.1로 쏠린 이웃 넷이라면 엔트로피는 1.846비트이고 perplexity는 약 3.6이다. 이웃은 넷이지만 "고르게 친 유효 이웃 수"는 3.6개라는 뜻이다.
폭 가 커지면 먼 점에도 확률이 가서 엔트로피가 오르고, 작아지면 가장 가까운 한두 점에 확률이 몰려 엔트로피가 내려간다. 이 관계가 한 방향으로만 움직이므로, 목표 perplexity를 정해 두고 폭을 반으로 쪼개 가며 맞추는 이분 탐색으로 점마다 를 찾는다. 결과적으로 밀집한 곳의 점은 폭이 좁고, 성긴 곳의 점은 폭이 넓다. 이 점이 뒤에서 "군집 크기를 믿지 말라"는 경고와 이어진다.
값에 따른 모양 변화
같은 digits에 perplexity만 바꿔 돌린 결과다.
for perp in [5, 15, 30, 50, 100]:
X_t = TSNE(perplexity=perp, random_state=42).fit_transform(X)
print(perp, round(silhouette_score(X_t, y), 3))
| perplexity | 실루엣 | 시간 |
|---|---|---|
| 5 | 0.429 | 3.1초 |
| 15 | 0.554 | 3.2초 |
| 30 | 0.552 | 3.4초 |
| 50 | 0.556 | 4.0초 |
| 100 | 0.520 | 4.8초 |
5에서는 이웃을 너무 적게 보아 한 숫자의 군집이 작은 섬 여러 개로 쪼개진다. 같은 "3"이라도 필체가 비슷한 몇 장끼리만 뭉치고, 그 섬들 사이의 연결은 무시된다. 15에서 50까지는 실루엣이 0.55 근처에서 거의 평평하다. 100이 되면 폭이 넓어져 군집 경계 근처의 점들이 이웃 군집 쪽 확률까지 받으면서 경계가 조금 흐려진다. 이웃을 많이 볼수록 계산할 거리도 늘어나 시간이 함께 오른다.
평평한 구간이 넓다는 것 자체가 정보다. 결과가 perplexity에 크게 휘둘리지 않는 구간이 있고, 그 구간에서 공통으로 보이는 군집은 믿을 만하다는 뜻이기 때문이다.
경험칙과 한계
흔히 쓰는 출발점은 5~50 사이이고, scikit-learn의 기본값은 30이다. perplexity는 반드시 표본 수보다 작아야 한다. 이웃 수가 전체 점 수를 넘을 수는 없기 때문이다. 표본이 수백 개뿐이면 30도 이미 전체의 상당 부분이라 군집 사이 구분이 흐려지고, 수십만 개면 30은 아주 좁은 동네만 보는 셈이라 군집이 잘게 부서지기 쉽다.
그래서 "데이터 크기의 몇 분의 일"같은 공식을 찾기보다 값을 서너 개 바꿔 돌리고 공통된 모양을 보는 편이 낫다. 실루엣 같은 지표는 참고일 뿐이다. 실루엣은 저차원 좌표에서 군집이 얼마나 떨어졌는지를 재므로, 군집을 인위적으로 벌려 놓는 t-SNE에게는 후하게 나온다. 그림이 또렷하다는 것과 데이터에 실제로 그런 구조가 있다는 것은 다른 이야기다.
UMAP의 이웃 그래프
k-최근접 그래프
UMAP(Uniform Manifold Approximation and Projection)은 2018년 Leland McInnes 등이 발표했다. 이론적 뼈대는 위상수학에서 왔지만, 실제로 하는 일은 그래프 두 장을 맞추는 것이다. 먼저 고차원에서 점마다 가장 가까운 이웃 개를 찾아 선으로 잇는다. 이것이 k-최근접 그래프다. t-SNE가 모든 점 쌍에 확률을 주는 것과 달리, UMAP은 처음부터 이웃 개 밖은 아예 보지 않는다.
선마다 무게를 준다. 점 에서 가장 가까운 이웃까지의 거리를 라 하면, 이웃 로 가는 선의 무게는 다음과 같다.
가장 가까운 이웃은 거리에서 를 빼면 0이 되므로 무게가 언제나 1이다. 어느 점도 외톨이가 되지 않게 하는 장치다. 는 t-SNE의 perplexity 맞추기와 비슷하게 점마다 이분 탐색으로 정한다.
그런데 가 의 이웃이라고 도 의 이웃인 것은 아니다. 그래서 두 방향 무게를 합친다. 가 0.8, 가 0.3이면 합친 무게는 이다. 둘 중 하나라도 이어져 있으면 선이 남고, 둘 다 강하면 1에 가까워진다. 확률에서 "둘 중 하나라도 일어날 확률"을 구하는 식과 같은 꼴이다.
그다음 2차원에 점을 흩어 놓고, 저차원에서도 같은 방식으로 무게를 매긴 그래프가 고차원 그래프를 닮도록 좌표를 움직인다. 목적 함수는 교차 엔트로피, 곧 선마다 "고차원 무게와 저차원 무게가 얼마나 어긋났나"를 이어진 쪽과 안 이어진 쪽 둘 다에서 재어 더한 값인데, t-SNE의 KL 발산과 달리 "이어진 선을 끊는 실수"와 함께 "안 이어진 점을 붙이는 실수"에도 벌점이 붙는다. 이 둘째 항 덕분에 UMAP은 t-SNE보다 군집 사이 배치를 조금 더 지키는 편이다. 다만 "조금 더"이지 전역 거리를 보존한다는 뜻은 아니다.
n_neighbors와 min_dist
UMAP의 두 핵심 인자는 서로 다른 단계에 걸린다. n_neighbors(기본 15)는 첫 단계, 곧 고차원 그래프를 만들 때 이웃을 몇 개 볼지를 정한다. 작으면 좁은 동네만 보아 세밀한 군집이 갈라지고, 크면 넓게 보아 큰 그림이 살아난다. perplexity와 비슷한 역할이다.
min_dist(기본 0.1)는 마지막 단계, 곧 저차원에 점을 놓을 때 점끼리 얼마나 붙을 수 있는지를 정한다. 고차원 그래프는 전혀 건드리지 않는다. 0이면 같은 군집의 점들이 한 점에 가깝게 뭉치고, 0.5쯤이면 군집 안이 널찍하게 퍼진다. digits에서 두 인자를 바꿔 본 실루엣은 이렇다.
| n_neighbors | min_dist 0.0 | 0.1 | 0.5 |
|---|---|---|---|
| 5 | 0.655 | 0.641 | 0.542 |
| 15 | 0.649 | 0.637 | 0.529 |
| 50 | 0.642 | 0.616 | 0.499 |
min_dist를 올릴수록 실루엣이 내려가는 것은 군집이 나빠져서가 아니라 군집 안이 넓게 펼쳐져서다. 실루엣은 군집 안 거리가 작을수록 높게 나오므로, 뭉쳐 그리면 점수가 오른다. 군집의 안쪽 구조, 예컨대 같은 숫자 안에서 필체가 어떻게 갈리는지를 보고 싶다면 오히려 min_dist를 올려야 한다. 점수가 높은 설정이 목적에 맞는 설정은 아니다.
import umap # pip install umap-learn
reducer = umap.UMAP(
n_components=2,
n_neighbors=15, # 기본값
min_dist=0.1, # 기본값
random_state=42,
)
X_umap = reducer.fit_transform(X)
print(silhouette_score(X_umap, y)) # 약 0.64
transform
UMAP에는 t-SNE에 없는 transform()이 있다. 학습에 안 쓴 새 점을 이미 만든 지도 위에 올리는 기능이다. 구조를 보면 이유가 보인다. 새 점이 들어오면 학습 데이터 중 가장 가까운 이웃을 찾아 무게를 매기고, 그 이웃들의 2D 좌표를 무게로 평균 낸 자리에서 출발한다. 그다음 기존 점들은 고정한 채 새 점의 좌표만 조금 다듬는다. 고차원 그래프라는 중간 산물이 남아 있으니 새 점을 그 그래프에 끼워 넣을 수 있는 것이다.
scikit-learn의 t-SNE는 fit_transform()만 있다. 최적화가 모든 점의 좌표를 한꺼번에 움직여 끝나므로, 새 점을 넣으려면 전체를 다시 돌려야 하고 그러면 기존 점의 좌표도 다 바뀐다. 매일 새 문서가 쌓이는 검색 시스템에서 임베딩 지도를 유지해야 한다면 이 차이가 곧 선택 기준이 된다.
X_new_2d = reducer.transform(X[:10]) # 학습한 지도 위에 올린다
print(X_new_2d.shape) # (10, 2)
전처리와 재현성
50차원 PCA
실무에서는 원본을 바로 t-SNE나 UMAP에 넣지 않고 PCA로 50차원쯤까지 먼저 줄이는 것이 표준 순서다. 이유가 둘이다. 하나는 계산이다. 두 알고리즘 모두 첫걸음이 이웃 찾기이고, 이웃 찾기는 거리 계산이며, 거리 계산은 차원에 비례해 비싸진다. 768차원을 50차원으로 줄이면 거리 한 번에 드는 곱셈이 15분의 1 남짓으로 준다. 다른 하나는 잡음이다. 분산이 아주 작은 뒤쪽 성분들은 대개 측정 잡음이고, 이것이 거리 계산에 섞이면 이웃 관계를 흐린다.
digits는 원래 64차원이라 50차원 PCA가 분산의 99.95%를 남긴다. 사실상 아무것도 버리지 않으니 이 데이터에서는 효과가 거의 없다. 이 단계가 실제로 힘을 쓰는 것은 수백~수천 차원의 이미지 특징이나 문장 임베딩이다. 누적 분산을 찍어 보고 90% 안팎이 남는지 확인하는 습관을 들이면 된다.
from sklearn.decomposition import PCA
pca50 = PCA(n_components=50, random_state=42)
X_50 = pca50.fit_transform(X)
print(pca50.explained_variance_ratio_.sum())
X_2d = TSNE(perplexity=30, random_state=42).fit_transform(X_50)
재현성
두 알고리즘 모두 무작위 초기값과 확률적 최적화를 쓴다. random_state를 고정하면 같은 기계, 같은 라이브러리 버전에서는 같은 그림이 나온다. 하지만 그것이 전부다. 라이브러리 버전이 바뀌면 기본값이 바뀌기도 하고(scikit-learn의 t-SNE는 1.2부터 초기화 기본값이 무작위에서 PCA로 바뀌었다), 부동소수점 연산 순서가 달라지는 기계에서는 작은 차이가 반복을 거치며 커진다.
UMAP은 random_state를 주면 병렬 처리를 끄고 한 스레드로 돈다. 병렬 스레드가 좌표를 갱신하는 순서가 매번 달라지면 결과를 고정할 수 없기 때문이다. 재현성을 얻는 대신 속도를 내준다. 탐색 중에는 비워 두고, 보고서에 실을 그림만 고정해서 뽑는 식으로 나누는 것이 현실적이다.
그림을 남에게 보일 때 최소한 적어 둘 것은 넷이다.
- 라이브러리와 버전
- perplexity 또는
n_neighbors·min_dist - 앞 단계 PCA 차원 수
random_state
이 넷이 없으면 같은 그림을 다시 만들 방법이 없고, 그림에서 읽은 결론을 누구도 확인할 수 없다.
문장 임베딩 시각화
LLM 시대에 두 알고리즘이 가장 자주 쓰이는 곳은 임베딩 점검이다. 문장을 768차원 벡터로 바꾸는 모델이 있다면, 같은 주제의 문장이 정말 가까이 모이는지를 눈으로 확인하고 싶어진다. 순서는 같다. 임베딩을 뽑고, PCA로 50차원까지 내리고, UMAP으로 2D에 펼친 뒤, 사람이 붙여 둔 주제 라벨로 색을 칠한다.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("jhgan/ko-sroberta-multitask")
emb = model.encode(sentences) # (N, 768)
emb_50 = PCA(n_components=50).fit_transform(emb)
emb_2d = umap.UMAP(n_neighbors=10, min_dist=0.05,
random_state=42).fit_transform(emb_50)
색이 라벨대로 뭉치면 임베딩이 주제를 구분하고 있다는 좋은 신호다. 더 쓸모 있는 것은 어긋나는 자리다. 스포츠 군집 한가운데 경제 기사 하나가 앉아 있으면, 라벨이 틀렸거나 그 기사가 실제로 구단 매각 이야기일 수 있다. 두 주제가 한 덩어리로 섞여 있으면 모델이 그 둘을 못 가르는 것이고, 검색에서 그 두 주제가 서로 끼어드는 오류가 날 자리를 미리 본 셈이다.
해석의 함정
군집 사이 거리
앞에서 본 KL 발산의 비대칭이 여기서 값을 치른다. t-SNE는 먼 점을 가깝게 놓아도 벌점이 거의 없으므로, 군집 A가 C보다 B에 가깝게 그려졌다는 사실에는 근거가 없다. 최적화가 어느 국소 최적점에 떨어졌느냐에 따라 군집들의 배치는 돌려지고 뒤바뀐다. random_state만 바꿔도 "3 옆에 5가 있던 그림"이 "3 옆에 8이 있는 그림"으로 바뀌는 일이 흔하다.
UMAP은 교차 엔트로피의 둘째 항 덕분에 사정이 조금 낫지만, 그래프가 이웃 개까지만 보고 만들어졌다는 한계는 그대로다. 두 군집 사이에 이웃 선이 하나도 없으면 둘이 얼마나 먼지에 대한 정보가 애초에 그래프에 없다. 군집 사이 거리를 말하고 싶다면 고차원에서 군집 중심 사이 거리를 직접 재야 한다.
군집 크기
perplexity 절에서 본 대로 t-SNE는 점마다 폭 를 따로 맞춘다. 빽빽한 군집은 폭이 좁고, 성긴 군집은 폭이 넓다. 그래서 두 군집 모두 저차원에서는 비슷한 크기로 펼쳐진다. 고차원에서 한 군집이 다른 군집보다 열 배 넓게 퍼져 있어도 그림에서는 비슷한 원 두 개로 보일 수 있다. UMAP도 이웃 거리를 점마다 와 로 정규화하므로 같은 경향이 있다. "이 군집이 저 군집보다 크다", "이 군집은 흩어져 있다"는 그림으로 말할 수 없는 문장이다.
교차 확인
그림에서 결론을 뽑을 때는 설정을 세 번 이상 바꿔 보고 매번 남는 패턴만 쓴다. perplexity를 15·30·50으로, 또는 n_neighbors를 5·15·50으로 돌리고, random_state도 몇 개 바꾼다. 이 모든 그림에서 두 라벨이 늘 떨어져 있다면 "이 두 부류는 저차원에서도 분리된다"고 말해도 된다. 한 그림에서만 보이는 작은 섬은 결론에 쓰지 않는다.
말해도 되는 것과 안 되는 것을 나눠 두면 이렇다.
- 말해도 되는 것: 두 라벨이 모든 설정에서 떨어진다, 한 라벨 안에 여러 설정에서 늘 두 덩어리가 보인다
- 말하면 안 되는 것: 군집 1이 군집 2보다 세 배 크다, A가 C보다 B에 가깝다, 군집 안이 흩어져 있어 품질이 나쁘다
세 방법의 선택
지금까지를 한 표로 모으면 다음과 같다. 속도는 규모에 따라 크게 달라지는데, digits 1,797장에서는 둘 다 몇 초 안에 끝났다. 표본이 수십만으로 커지면 UMAP 쪽이 훨씬 가볍다는 것이 널리 보고된 경향이다.
| 항목 | PCA | t-SNE | UMAP |
|---|---|---|---|
| 기반 원리 | 선형 투영 | 확률 분포 (KL) | 이웃 그래프 |
| 지역 구조 보존 | 보통 | 우수 | 우수 |
| 전역 구조 보존 | 우수 | 취약 | 보통 |
| digits 실루엣 | 0.105 | 약 0.55 | 약 0.64 |
| 재현성 | 결정적 | 시드 고정 필요 | 시드 고정 필요 |
| 새 데이터 변환 | 가능 | 불가 | 가능 |
| 하이퍼파라미터 | 성분 수 | perplexity | n_neighbors, min_dist |
| 주 용도 | 전처리, 잡음 제거 | 탐색적 시각화 | 시각화 + 임베딩 |
실무에서는 PCA를 앞에 두고, 새 점을 계속 올려야 하거나 데이터가 크면 UMAP을, 한 번 찍는 탐색용 그림이고 지역 구조를 가장 또렷하게 보고 싶으면 t-SNE를 고르면 된다. 어느 쪽이든 그림은 가설을 세우는 자리이지 결론을 내리는 자리가 아니다.
군집을 눈으로 확인하고 나면 다음 물음은 모델이 그 구조를 정말 배웠는지를 숫자로 재는 일이다. 그림이 좋아 보여도 평가를 잘못 나누면 성능이 부풀려진다. 다음 글에서는 데이터를 학습·검증·시험으로 나누고 교차 검증으로 성능을 정직하게 재는 법을 다룬다.
읽어주셔서 감사합니다. 😊

