본문 바로가기

문돌이 존버/데이터 분석

핸즈온 머신러닝 2 복습하기(챕터 8: 차원 축소)

반응형

CH 8. 차원 축소

많은 경우 머신러닝 문제는 훈련 샘플 각각이 수천 심지어 수백만 개의 특성을 가지고 있다. 이는 훈련을 느리게 하고 좋은 솔루션을 찾기 힘든데, 이를 차원의 저주(curse of dimensionality)라고 한다.

차원을 축소시키면 일부 정보가 유실될 수 밖에 없어, 훈련 속도는 빨라지지만 시스템 성능이 조금 나빠질 수 있다는 양면성이 존재한다. 따라서 차원 축소를 실행하기 전 훈련이 너무 느린지 원본 데이터로 시스템을 훈련시켜봐야 한다.

(주의)
차원이 축소된다고 해서 언제나 학습 시간을 줄여주는 것은 아니다. 데이터셋, 알고리즘, 모델에 따라 상황이 달라진다.

8.2.1 투영

대부분의 실전 문제는 훈련 샘플이 모든 차원에 걸쳐 균일하게 퍼져 있지 않다. 많은 특성은 거의 변화가 없는 반면, 다른 특성들은 서로 강하게 연관되어 있다. 결과적으로 모든 훈련 샘플이 사실 고차원 공간 안의 저차원 부분 공간에 놓여 있다고 볼 수 있다. 이런 저차원 공간에 수직으로 투영하면 차원이 줄어든 데이터셋을 얻을 수 있다.

아래 예에선 3차원 데이터를 2차원으로 줄인 것이다.

8.2.2 매니폴드 학습

스위스 롤은 2D 매니폴드의 한 예이다. 간단히 말해 2D 매니폴드는 고차원 공간에서 휘어지거나 뒤틀린 2D 모양이며, 일반적으로 d차원 매니폴드는 국부적으로 d차원 초평면으로 보일 수 있는 n차원 공간의 일부이다(d<n). 따라서 스위스 롤의 경우 d=2이고 n=3이다.

말이 어려울 수 있는데, 쉽게 말하면 실제로는 n차원 데이터셋인데, 부분부분 자세히 살펴보면 d차원이라는 것이다. 따라서 부분부분 데이터를 잘 구분하면 아래 그림의 2D - unrolling처럼 2D 그래프에서도 명확히 구분된다. 

많은 차원 축소 알고리즘이 훈련 샘플이 놓여 있는 매니폴드를 모델링하는 식으로 작동하며, 이를 매니폴드 학습(Manifold Learning)이라고 한다. 대부분 실제 고차원 데이터셋이 더 낮은 저차원 매니폴드에 가깝게 놓여 있다는 매니폴드 가정에 근거한다.

매니폴드 가정은 종종 암묵적으로 다른 가정과 병행되곤 하는데, 그것은 바로 처리해야 할 작업이 저차원의 매니폴드 공간에 표현되면 더 간단해질 것이란 가정이다. 아래 예시를 살펴보자.

1행 그래프를 보면 3D(왼쪽)에서는 결정 경계가 매우 복잡하지만 펼쳐진 매니폴드 공간인 2D(오른쪽)에선 결정 경계가 단순해진다. 반면, 2행 그래프의 3D에선 결정 경계가 수직 평면으로 간단하지만, 펼쳐진 매니폴드에선 더 복잡해졌다.

8.3 PCA

주성분 분석(Principal Component Analysis)은 가장 인기 있는 차원 축소 알고리즘이다. 데이터에 가장 가까운 초평면(hyperplane)을 정의한 다음 데이터를 이 평면에 투영시키는 것이다.

8.3.1 분산 보존

PCA는 분산이 최대한 보존되는 축을 선택한다. 그 이유는 분산이 정보가 가장 적게 손실되므로 합리적이기 때문이다. 이를 다른 방식으로 설명하면 원본 데이터셋과 투영된 것 사이의 평균 제곱 거리를 최소화하는 축이다.

아래 그림에선 실선 c1 축에 투영된 결과(오른쪽 첫 번째 그래프)가 분산이 가장 크고, 이후 파선에 투영된 결과(두 번째 그래프), c2 축에 투영된 결과(마지막 그래프) 순으로 분산이 보존되고 있다.

8.3.2 주성분

PCA는 초기에 분산이 최대인 축을 찾고, 이후에 남은 분산을 최대한 보존하는 두 번째 축을 찾는다. $i$ 번째 축을 정의하는 단위 벡터를 $i$ 번째 주성분이라고 한다. 학습셋의 주성분을 찾는 방법은 특잇값 분해(Singular Value Decomposition)라는 행렬 분해 기술이다. 학습셋 행렬 X를 3개 행렬의 점곱인 $U \times \sum \times V^T$로 분해하고, 이때 모든 주성분은 $V$에 아래와 같이 담겨 있다.

X_centered = X - X.mean(axis=0)
U, s, Vt = np.linalg.svd(X_centered) # m x m, m x n, n x n
c1 = Vt.T[:, 0]
c2 = Vt.T[:, 1]

print(U.shape)
print(s.shape)
print(Vt.shape)

8.3.3 d차원으로 투영하기

초평면에 학습셋을 투영하기 위해서는 행렬 X와 첫 d개의 주성분을 담은(즉, V의 첫 d열로 구성된) 행렬 $W_d$를 점곱하면 된다.

$X_{d-proj} = X \times W_d$
W2 = Vt.T[:, :2]
X2D = X_centered.dot(W2)

8.3.4 사이킷런 사용하기

사이킷런의 PCA 모델은 SVD 분해 방법을 사용하여 구현한다. 아래는 PCA 모델을 통해 데이터셋 차원을 2로 줄이는 코드이다.

from sklearn.decomposition import PCA

pca = PCA(n_components=2)
X2D = pca.fit_transform(X)

아래는 주성분을 확인하는 코드이고, components_ 변수를 사용하면 주성분이 행 벡터로 포함되어 있기 때문에 아래처럼 transpose를 취해줘야 한다. 

pca.components_.T
# pca.components_.T[:, 0]이 첫 번째 주성분

8.3.5 설명된 분산의 비율

explained_variance_ratio_ 변수에 저장된 주성분의 설명된 분산 비율도 유용한 정보 중 하나다. 해당 값은 각 주성분의 축을 따라 있는 데이터셋의 분산 비율을 나타낸다.

pca.explained_variance_ratio_

이는 데이터셋 분산의 84.2%가 첫 번째 축에 놓여 있고 14.6%가 두 번째 축에 놓여 있음을 알려준다.

8.3.6 적절한 차원 수 선택하기

축소할 차원 수를 임의로 정하기보다는 충분한 분산(ex: 95%)이 될 때까지 더해야 할 차원 수를 선택하는 쪽을 더 선호한다. 아래는 차원을 축소하지 않고 PCA를 계산한 뒤 학습셋의 분산을 95%로 유지하는 데 필요한 최소한의 차원 수를 계산한다.

numpy의 cumsum에 대한 자세한 설명은 공식 홈페이지를 참고하자
pca = PCA()
pca.fit(X_train)
# numpy cumsum: 1차원 array의 경우 누적 합을 리턴
cumsum = np.cumsum(pca.explained_variance_ratio_)
d = np.argmax(cumsum >= 0.95) + 1

보존하려는 분산의 비율을 n_components에 0 ~ 1 사이로 설정할 수도 있다.

pca = PCA(n_components=0.95)
X_reduced = pca.fit_transform(X_train)

8.3.7 압축을 위한 PCA

압축된 데이터셋에서 PCA 투영의 변환을 반대로 적용하여 원래 차원으로 되돌릴 수 있다. 물론 투영에서 일정량의 정보(1 - 설명된 분산 비율 합)를 잃어버렸기 때문에 이렇게 해도 원본 데이터셋을 얻을 수는 없다. 이런 차이를 재구성 오차(reconstruction error)라고 부르기도 한다.

$X_{recovered} = X_{d-proj} \times W_{d}^T$
pca = PCA(n_components=154)
X_reduced = pca.fit_transform(X_train)
X_recovered = pca.inverse_transform(X_reduced)

8.3.8 점진적 PCA

PCA 구현 문제는 SVD 알고리즘을 실행하기 위해 전체 학습셋을 메모리에 올려야 한다는 것이다. 하지만 점진적 PCA(Icremental PCA) 알고리즘이 개발되면서 학습셋을 미니배치로 나눈 뒤 IPCA 알고리즘에 한 번에 하나씩 주입한다.

from sklearn.decomposition import IncrementalPCA

n_batches = 100
inc_pca = IncrementalPCA(n_components=154)
for X_batch in np.array_split(X_train, n_batches):
    inc_pca.partial_fit(X_batch)

X_reduced = inc_pca.transform(X_train)

또 다른 방법은 numpy의 memmap 파이썬 클래스를 사용해 하드 디스크의 이진 파일에 저장된 매우 큰 배열을 메모리에 들어 있는 것처럼 다루는 것이다. 해당 파이썬 클래스는 필요할 때 데이터를 메모리에 적재한다.

8.3.9 랜덤 PCA

사이킷런에는 또 다른 옵션으로 랜덤 PCA(Randomized PCA)를 제공한다. 이 방식은 확률적인 알고리즘으로, 첫 d개의 주성분에 대한 근삿값을 빠르게 찾는다.

계산 복잡도가 $O(m \times n^2) + O(n^3)$이 아니라 $O(m \times d^2) + O(d^3)$이다.
rnd_pca = PCA(n_components=154, svd_solver="randomized")
X_reduced = rnd_pca.fit_transform(X_train)

8.4 커널 PCA

샘플을 매우 높은 고차원 공간(특성 공간, feature space)으로 암묵적으로 매핑하여 서포트 벡터 머신의 비선형 분류와 회귀를 가능하게 하는 수학적 기법인 커널 트릭이 있다. 이를 PCA에 적용해 차원 축소를 위한 복잡한 비선형 투형을 수행할 수 있으며, 커널 PCA(Kernel PCA)라고 한다.

from sklearn.decomposition import KernalPCA

rbf_pca = KernelPCA(n_components=2, kernel='rbf', gamma=0.04)
X_reduced = rbf_pca.fit_transform(X)

8.4.1 커널 선택과 하이퍼파라미터 튜닝

kPCA는 비지도 학습이기 때문에 좋은 커널과 하이퍼파라미터를 선택하기 위한 명확한 성능 측정 기준이 없다. 하지만 차원 축소는 종종 지도 학습의 전처리 단계로도 활용되므로 그리드 탐색을 사용하여 주어진 문제에서 성능이 가장 좋은 커널과 하이퍼파라미터를 선택할 수 있다.

from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline

clf = Pipeline([
        ('kpca', KernelPCA(n_components=2)),
        ('log_reg', LogisticRegression()) # 분류 모델 적용
    ])
    
 param_grid = [{
         'kpca__gamma': np.linspace(0.03, 0.05, 10),
         'kpca__kernel': ['rbf', 'sigmoid']
     }]
     
grid_search = GridSearch(clf, param_grid, cv=3)
grid_search.fit(X, y)

print(grid_search.best_params_)

완전한 비지도 학습 방법으로, 가장 낮은 재구성 오차를 만드는 커널과 하이퍼파라미터를 선택하는 방식도 있다. 커널 트릭으로 학습셋을 특성 맵(feature map) $\boldsymbol{\varphi}$를 사용한 무한 차원의 특성 공간에 매핑한 다음, 변환된 데이터셋을 선형 PCA를 사용해 2D로 투영한 것과 수학적으로 동일하다.

축소된 공간에 있는 샘플에 대해 선형 PCA를 역전시키면 재구성된 데이터 포인트는 원본 공간이 아닌 특성 공간에 놓이게 된다. 이 특성 공간은 무한 차원이기 때문에 재구성된 포인트를 계산할 수 없고 재구성에 따른 실제 에러를 계산할 수 없다. 다행히 재구성된 포인트에 가깝게 매핑된 원본 공간의 포인트를 찾을 수 있는데, 이를 재구성 원상(pre-image)이라 부른다. 따라서 이러한 재구성 원상의 오차를 최소화하는 커널과 하이퍼파라미터를 선택할 수 있는 것이다.

재구성을 하는 한 가지 방법은 투영된 샘플을 학습셋으로, 원본 샘플을 타겟으로 하는 지도 학습 회귀 모델을 훈련시키는 것이다. 사이킷런에선 fit_inverse_transform=True 로 지정하면 이를 자동으로 수행한다.

from sklearn.metrics import mean_squared_error

rbf_pca = KernelPCA(n_components=2, kernel='rbf', gamma=0.0433,
                    fit_inverse_transform=True)
X_reduced = rbf_pca.fit_transform(X)
X_preimage = rbf_pca.inverse_transform(X_reduced) # fit_inverse_transform=True일 때만 생성됨

print(mean_sqaured_error(X, X_preimage)

8.5 LLE

지역 선형 임베딩(Locally Linear Embedding)은 또 다른 강력한 비선형 차원 축소(nonlinear dimensionality reduction) 기술이다. 각 훈련 샘플이 가장 가까운 이웃에 얼마나 선형적으로 연관되어 있는지 측정하고, 국부적인 관계가 가장 잘 보존되는 학습셋의 저차원 표현을 찾는다.

from sklearn.manifold import LocallyLinearEmbedding

lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10)
X_reduced = lle.fit_transform(X)
LEE가 작동하는 방식

각 훈련 샘플 $x_{(i)}$에 대해 가장 가까운 k개의 샘플을 찾는다.
이 이웃에 대한 선형 함수로 $x_{(i)}$를 재구성한다. 더 구체적으로 말하면 $x_{(i)}$와 $\sum_{j=1}^m w_{i, j}x^{(j)}$ 사이의 제곱 거리가 최소가 되는 $w_{i, j}$를 찾는 것이다. 이때, $x^{(j)}$가 $x^{(i)}$의 가장 가까운 k개 이웃 중 하나가 아닐 경우에는 $w_{i, j}=0$이 된다.

이를 수식으로 정리하면 아래와 같다.
$\hat{W}=\underset{w}{argmin} \sum_{i=1}^m(x^{(i)} - \sum_{j=1}^m w_{i, j}x^{(j)})^2$
[조건]
1. $w_{i, j}=0$, $x^{(j)}$가 $x^{(i)}$의 최근접 이웃 k개 중 하나가 아닐 때
2. $\sum_{j=1}^m w_{i, j}=1$, $i=1, 2, ..., m$ 일 때

위 단계를 거치면 가중치 행렬 $\hat{W}$은 학습 샘플 사이에 있는 지역 선형 관계를 담고 있다. 이제 두 번째 단계는 가능한 한 이 관계가 보존되도록 학습 샘플을 d차원 공간(d < n)으로 매핑하는 것이다. 만약 $z^{(i)}$가 d차원 공간에서 $x^{(i)}$의 상(image)이라면 가능한 $z^{(i)}$와 $\sum_{j=1}^m \hat{w_{i, j}}z^{(j)}$ 사이의 거리가 최소화되어야 한다.

첫 번째 단계와 비슷해 보이지만, 샘플을 고정하고 최적의 가중치를 찾는 대신, 반대로 가중치를 고정하고 저차원의 공간에서 샘플 이미지의 최적 위치를 찾는다.

$\hat{Z}=\underset{z}{
argmin} \sum_{i=1}^m(z^{(i)} - \sum_{j=1}^m w_{i, j}z^{(j)})^2$

이외에도 사이킷런은 다양한 차원 축소 기법을 제공한다.

1. 다차원 스케일링(Multidimensional Scaling, MDS)
- 샘플 간 거리를 보존하면서 차원을 축소한다.

2. Isomap
- 각 샘플을 가장 가까운 이웃과 연결하는 식으로 그래프를 생성한다. 이후 샘플 간의 지오데식 거리(geodesic  distance, 두 노드 사이의 최단 경로를 이루는 노드 수)를 유지하면서 차원을 축소한다.

3. t-SNE(t-Distributed Stochastic Neighbor Embedding)
- 주로 시각화에 많이 사용되며 특히 고차원 공간에 있는 샘플 군집을 시각화할 때 사용된다.

4. 선형 판별 분석(Linear Discriminant Analsys, LDA)
- 사실 분류 알고리즘이지만 학습 과정에서 클래스 사이를 가장 잘 구분하는 축을 학습한다. 투영을 통해 가능한 한 클래스를 멀리 떨어지게 유지시키므로 SVM 분류기 같은 다른 분류 알고리즘을 적용하기 전 차원을 축소시키는 데 좋다.
# MDS
from sklearn.manifold import MDS
mds = MDS(n_components=2, random_state=42)
X_reduced_mds = mds.fit_transform(X)

# Isomap
from sklearn.manifold import Isomap
isomap = Isomap(n_components=2)
X_reduced_isomap = isomap.fit_transform(X)

# t-SNE
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, random_state=42)
X_reduced_tsne = tsne.fit_transform(X)

# LDA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis(n_components=2)
X_mnist = mnist["data"]
y_mnist = mnist["target"]
lda.fit(X_mnist, y_mnist)
X_reduced_lda = lda.transform(X_mnist)
728x90
반응형