8장 차원 축소

주요 내용

기본 설정

8.1 차원의 저주

고차원 공간의 특징

차원의 저주

차원 축소

장점과 단점

8.2 차원축소 기법 소개

8.2.1 사영 기법

특잇값 분해를 이용한 사영 기법을 설명하기 위해 3차원 공간에 아래와 같이 분포된 60개의 점을 2차원으로 사영하는 예제를 이용한다.

언급된 60개의 3차원 데이터는 아래와 같이 (60, 3) 모양의 행렬 X로 생성한다.

참고: 생성되는 데이터의 x, y 좌표는 사인, 코사인 함수를 조합하며, 수학적으로 큰 의미는 없다.

PCA(주성분 분석)

데이터셋의 주성분은 데이터 샘플들의 분산을 최대로 유지하는 축을 가리키며 차원의 개수만큼 주성분이 존재한다. 분산을 최대로 유지하는 축이 첫째 주성분이며, 이후의 축은 이전 축에 수직이면서 동시에 남은 분산을 최대한 보존하는 축으로 지정한다. 이 과정을 차원 수 만큼의 주성분을 찾을 때까지 반복한다.

차원축소

차원축소는 PCA를 통해 찾은 주성분의 일부로 구성된 초평면(하이퍼플레인hyperplane)으로 데이터 샘플을 사영(projection)하는 과정을 의미한다.

첫째부터 $d$ 번째 까지의 주성분을 축으로 사용해서 생성되는 $d$ 차원의 공간으로 데이터셋이 사영된다. 따라서 3차원(3D) 데이터를 2차원(2D) 공간으로 보내려면 예를 들어 아래 그림의 회색 평면을 구성하는 두 개의 축에 해당하는 주성분을 찾아야 한다.

특잇값 분해(SVD)

데이터셋의 분산에 대한 주성분은 데이터셋 행렬의 특잇값 분해(SVD, singular value decomposition)를 이용하여 쉽게 구할 수 있다. 사이킷런의 PCA 모델이 특잇값 분해를 이용하여 주성분을 바로 계산할 수 있지만 여기서는 먼저 특잇값 분해를 사용하여 직접 주성분을 구하는 과정을 살펴본다.

주의사항: 평균값(mean)이 $0$이라는 가정하에 특잇값 분해를 진행해야 하기에 아래 코드를 먼저 실행한다.

특잇값 분해의 내용은 다음과 같다.

평균값이 0인 데이터셋 $X$가 주어졌을 때 아래 조건을 만족시키는 세 개의 행렬 $U$, $\Sigma$, $V$가 존재한다.

$$ X = U\, \Sigma \, V^{\!T} $$

참고: 위키백과: 특잇값 분해

특잇값 분해를 진행하는 numpy.linalg.svd() 함수를 이용하여 X_centered에 대해 특잇값 분해를 진행한 결과는 다음과 같다.

각 행렬의 모양을 확인하면 다음과 같다.

앞서 언급한 대로 s는 원래 (60, 3) 모양의 대각 행렬 이어야 하지만 여기서는 대각선 상에 위치한 세 개의 수만 포함하고 있다.

참고: (60, 3) 모양의 대각선은 길이가 3이다.

특잇값 분해가 제대로 이루어진 것인지 아래와 같이 검증할 수 있다.

주의사항: 행렬의 곱셈을 진행하려면 먼저 s를 (60, 3) 모양의 대각 행렬로 만들어야 한다.

이제 아래와 같이 특잇값 분해가 제대로 이루어졌음을 확인할 수 있다.

참고: 컴퓨터를 이용한 부동소수점 연산은 일반적으로 100% 정확하지 않다. 따라서 약간의 오차를 감안하여 두 부동소수점 값을 비교해야 한다. numpy.allclose() 함수는 지정된 오차범위 안에서 두 부동소수점의 일치여부를 판단한다. 보다 자세한 설명은 numpy.allclose 문서를 참조한다.

주성분은 앞서 특잇값 분해로 얻은 행렬 $V$의 열 벡터에 해당한다. 0번 열이 첫째 주성분, 1번 열이 둘째 주성분, 2번 열이 셋재 주성분 벡터이다. 아래 그림은 첫째 주성분 벡터를 보여준다.

첫째와 둘째 주성분 벡터를 함께 그리면 다음과 같다.

2차원으로 사영하기

3차원(3D) 공간의 데이터를 2차원(2D)으로 보내려면 앞서 보여준 두 개의 주성분(principal components)을 선택한 후에 두 축에 의해 생성되는 초평면hyperplane에 사영해야 한다. 2차원으로 사영하려면 따라서 행렬 $V$의 0번과 1번 두 개의 열을 선택한 후에 X_centered를 두 개의 주성분으로 구성된 행렬과 곱한다.

주의사항: Vt.T 는 $(V^{\!T})^{T}$, 즉 $V$를 가리킨다.

2차원에 사영된 데이터셋은 다음과 같다.

사영된 데이터셋을 이어서 소개하는 사이킷런의 PCA 모델을 이용하여 구한 값과의 비교를 위해 잠시 기억해둔다.

사이킷런의 PCA 모델 활용

앞서 특잇값 분해를 직접 활용하여 2차원으로 사영하는 과정을 사이킷런의 PCA 모델을 이용하여 간단하게 해결할 수 있다.

참고: 평균값을 0으로 맞추는 것도 PCA 모델이 알아서 처리한다.

참고: 사영된 데이터의 축별 평균은 거의 0이다.

주의사항: 사이킷런을 이용하여 PCA를 진행할 때 주성분 축이 반대 방향으로 지정되는 경우가 발생할 수 있으며 여기서도 그런 일이 벌어졌음을 아래 코드가 알려준다.

실제로 훈련된 PCA가 알고 있는 주성분과 앞서 특잇값 분해로 얻은 주성분이 서로 반대방향을 가리키고 있다.

재구성 오차

아래 코드는 2차원으로 사영된 데이터를 다시 3차원으로 복원한다.

3차원으로 복원된 데이터셋은 다음과 같다.

그런데 2차원 공간으로 사영되면서 정보손실이 있었기에 3차원으로 복원한 값과 원래의 3차원 값 사이에 오차가 존재한다.

재구성 오차(reconstruction error)는 사영 전과 사영 후 원래 공간으로 복원한 데이터 사이의 평균제곱오차로 계산한다.

아래 그래프에서 빨강 선으로 표시된 부분이 각 데이터 샘플에 대한 재구성 오차를 가리킨다.

참고: inverse_transform() 메서드가 내부에서는 사영할 때 사용된 행렬의 전치행렬을 사영된 데이터셋에 곱한다.

앞서 구한 inverse_transform() 메서드의 결과와 비교하기 위해선 다시 데이터셋의 평균값을 빼주어야 한다.

그런데 PCA 모델의 mean_ 속성에 동일한 값이 저장되어 있다.

이제 두 값을 비교하면 다음과 같이 일치한다.

설명 분산 비율

설명 분산 비율은 각 주성분의 축에 대한 데이텃셈의 분산 비율을 의미한다. PCA 객체의 explained_variance_ratio_ 속성에 사영에 사용된 주성분별 설명 분산 비율이 저장되어 있다.

즉, 2차원으로 사영한 결과 1.1% 정도의 분산을 잃는다.

참고: 특잇값 분해로 생성된 행렬 s를 이용하여 모든 주성분에 대한 설명 분산 비율을 계산할 수 있다.

사영 그래프 코드

지금까지 사용된 그래프를 그리는 코드는 다음과 같다.

사영기법의 한계

사영기법이 차원축소의 최선이 아닌 경우가 존재한다. 이어서 설명하는 롤케이크의 경우처럼 데이터셋을 구성하는 저차원 공간이 꼬이거나 뒤집혀 있는 경우 사영기법을 적용하면 오히려 문제를 보다 어렵게 만들 수 있다.

롤케이크(스위스 롤) 그리기

아래 코드는 롤케이크(swill roll) 데이터셋을 생성한다.

롤케이크 사영하기와 펼치기

롤케이크 모양의 데이터셋을 2차원으로 사영(아래 그림 왼편)해도 분류를 깨끗하게 실행할 수 없다.

반면에 아래 그림 오른편처럼 돌돌 말린 데이터셋을 평평하게 펼치면 보다 쉽게 분류가 가능해진다.

8.2.2 다양체 학습

다양체(manifold)

롤케이크는 3차원 공간에 존재한다. 반면에 위 그림에서 보듯 롤케이크를 구성하는 데이터셋 자체는 2차원 평면을 구부린 형태를 갖는다. 이처럼 저차원 공간이 구부리고 꼬인 형태로 보다 고차원의 공간에 위치하면 그런 저차원 공간을 다양체(manifold, 매니폴드)라 부른다. 예를 들어 롤케이크는 2차원 다양체가 된다.

다양체 학습(manifold learning)

고차원 공간에 존재하는 데이터셋의 다양체 성질을 학습하는 것의 의미한다.

다양체 가정

"고차원의 데이터셋이 대부분 저차원의 다양체와 비슷한 형태를 갖는다"라는 가정을 다양체 가정 또는 다양체 가설이라 부른다.

다양체 가정과 더불어 저차원의 다양체로 표현할 경우 문제가 보다 쉬어질 것이다라는 가정도 함께 많이 사용된다. 앞서 살펴본 롤케이크의 경우가 이 가정을 만족시킨다. 하지만 이 가정또한 항상 성립하지는 않음을 아래 그래프가 잘 보여준다.

반면에 아래의 경우는 저차원 다양체로 말린 부분을 펼치면 분류 문제가 매우 단순해짐을 잘 보여준다.

8.3 PCA

주성분 분석(PCA, principal component analysis)은 가장 많이 사용되는 차원축소 알고리즘이다. 주성분 분석의 핵심은 훈련 데이터셋의 분산을 최대한 보존하는 방향으로 차원축소를 진행하는 것이다.

8.3.1 분산 보존

아래 그림은 분산을 최대로 보존하는 주성분의 의미를 잘 보여준다.

참고: 2차원 데이터셋이므로 두 개의 주성분만 존재하며, 첫째 주성분이 정해지면 둘째 주성분은 자동으로 정해진다.

8.3.7 PCA 활용: MNIST 데이터 압축

MNIST 데이터셋 구하기

MNIST 데이터셋을 대상으로 차원축소를 진행하기 위해 먼저 MNIST 데이터셋을 불러온다.

fetch_openml() 함수는 지정된 데이터셋과 관련된 다양한 정보를 담은 사전(Bunch 객체) 객체를 반환하며, 특성과 타깃 데이터셋은 각각 다음 키(key)의 값으로 지정되어 있다.

주의 사항: 특성 데이터셋과 타깃 데이터셋이 판다스의 DataFrame 또는 Series 객체로 저장된다. 여기서는 각 데이터셋을 넘파이 어레이로 얻기 위해 as_frame=False 옵션을 지정한다.

아래 코드는 특성 데이터셋과 타깃 데이터셋 넘파이 어레이로 지정한다.

훈련 세트와 테스트 세트로 구분한다.

설명 분산 비율과 차원 축소

차원축소를 위한 적절한 차원을 확인하기 위해 설명 분산 비율이 95%가 되는 지점까지 몇 개의 주성분이 필요한가를 계산한다. MNIST 데이터셋의 경우 설명 분산 비율이 95%가 되도록 하려면 154개의 주성분이 필요함이 아래와 같이 확인된다.

MNIST 데이터셋과 관련된 설명 분산 비율과 차원과의 관계를 그래프로 그리면 다음과 같다.

차원을 직접 지정하는 대신 n_components 옵션에 설명 분산 비율을 지정하면 자동으로 해당 분산 비율을 만족하는 최소 차원으로 데이터셋을 사영할 수 있다.

몇 개의 주성분이 사용되었는가는 n_components_ 속성이 알고 있다.

사영에 사용된 설명 분산 비율의 합은 다음과 같이 95%를 갓 넘는다.

재구성 오차

재구성 오차는 170,000 정도로 계산된다.

앞서 확인한 차원 154를 이용하여 차원축소를 진행하더라도 재구성 오차가 거의 비슷하다.

실제로 원본 손글씨 숫자와 재구성된 숫자 이미지를 비교해보면 화질이 조금 떨어지긴 했지만 숫자 인식에는 전혀 문제가 없다는 것을 확인할 수 있다.

아래 코드는 손글씨 이미지 총 52,500 훈련 샘플 중에서 선택된 25 개의 이미지를 보여준다. 각각의 이미지는 2100개 중에 하나씩 선택되었다.

참고: $52,500 = 25 \times 2100$

아래 코드는 이어서 설명하는 점진적 PCA 기법의 결과와 비교하기 위해 앞서 설명한 PCA 기법으로 압축된 손글씨 이미지 데이터를 X_reduced_pca 로 기억해둔다.

8.3.8 랜덤 PCA

svd_solver='randomized' 옵션을 사용하면 특잇값 분해 알고리즘에 무작위성을 가하여 차원 $d$ 만큼의 주성분을 근사적으로 계산한다.

각 알고리즘의 시간 복잡도를 비교하면 다음과 같다.

따라서 $d$가 $n$보다 많이 작으면 랜덤 PCA가 훨씨 빠르게 작동한다. 이어서 설명하는 점진적 PCA 알고리즘을 소개한 후에 세 알고리즘의 실행속도를 직접 비교할 것이다.

8.3.9 점진적 PCA

주성분 분석 알고리즘은 훈련 세트 전체를 이용한다. 따라서 매우 큰 훈련 세트에 대해서는 메모리와 시간과 관련된 문제가 발생한다. 이 문제를 해결하기 위해 점진적 PCA(IPCA, Incremental PCA)를 사용할 수 있다.

아래 코드는 훈련 세트를 쪼개서 미니 배치 단위로 IncrementalPCA 모델을 훈련하는 방법을 보여준다.

주의사항: 위 방식은 아래와 같이 batch_size 옵션인자와 fit() 메서드를 활용하는 방식과 동일하다.

실제로 사영 결과가 동일하다.

재구성 하기

점진적 PCA를 이용해서 사영된 데이터를 재구성한 결과를 비교해서 역시 숫자 인식에는 전혀 문제가 없어 보인다.

아래 코드는 앞서 설명한 다른 PCA의 결과와 비교하기 위해 점진적 PCA로 사영된 데이터를 기억해 둔다.

PCA와 IPCA 비교

특잇값 분해 알고리즘에 필요한 훈련 세트의 평균값은 동일한 값이 계산되었다.

하지만 사영 결과는 다르다. 점진적 PCA 기법의 사영 결과는 상당히 좋기는 하지만 완벽하진 않다.

memmap() 함수 사용하기

넘파이의 memmap() 함수를 이용하여 매우 큰 데이터셋을 마치 실제로 메모리에 적재한 것인양 처리하는 memmap 객체를 생성해서 활용할 수 있다. 처리 과정이 좀 복잡하기는 하지만 몇 개의 지정된 단계를 거치면 된다.

참고: memmap은 memory-map의 줄임말이다. 실제로는 컴퓨터 저장장치에 저장된 파일을 마치 메모리상의 어레이로 불러온 효과를 보이는 어레이 객체를 가리킨다.

X_mm의 자료형은 memmap이다.

크기는 훈련 세트의 크기와 동일하다.

시간 복잡도 비교

앞서 소개된 세 종류의 PCA 알고리즘의 시간 복잡도를 예제를 통해 비교해보자. 그러면 랜덤 PCA, 일반 PCA, 점진적 PCA 순으로 점점 느려진다. 방식은 2차원, 10차원, 154차원으로 사영하는 기준을 따른다.

아래 코드는 일반 PCA와 랜덤 PCA를 직접 비교한다. 비교는 차원은 고정시킨 채 훈련 세트의 크기를 증가시키는 방식이며 일반 PCA 방식이 훨씬 빠름을 보여준다.

아래 코드는 일반 PCA와 랜덤 PCA를 다른 방식으로 비교한다. 이번에는 훈련 세트의 크기와 사영 공간의 차원은 고정시킨 채 훈련 데이터의 차원을 늘린다. 즉, 원 데이터의 차원과 사영 공간의 차원 사이의 차이를 점점 키우는 방식을 이용한다. 그러면 두 차원의 차이가 커질 수록 랜덤 PCA 방식이 훨씬 빠름을 잘 보여준다.

8.4 커널 PCA

기본적으로 선형 분류 또는 회귀만을 지원하는 서포트 벡터 머신(SVM)에 커널 기법을 적용하여 비선형 분류와 회귀를 지원하도록 하는 아이디어를 PCA 기법에 적용한다.

커널 기법의 기본 아이디어는 데이터를 고차원으로 보낼 때 갑자기 선형 분류/회귀가 가능해질 수 있다는 것이다. 그런데 동일한 이유로 고차원으로 보내진 데이터셋의 주성분 찾기가 보다 쉬워질 수 있다. 실제로 롤케이크 데이터의 경우 선형 주성분 찾는 일이 매우 힘들어 보이지만 커널 기법을 이용해서 고차원으로 보내질 경우 선형 주성분 찾는 일이 가능해짐을 아래 가운데에 있는 그래프를 통해 확인할 수 있다.

다양한 커널 기법 적용 예제

아래 코드는 세 종류의 커널을 활용한 결과를 그래프로 보여준다.

아래 코드는 앞서 rbf 커널 기법으로 2차원으로 사영된 데이터셋을 다시 3차원으로 복원한 결과를 보여준다.

8.4.1 커널 선택과 하이퍼파라미터 조정

kPCA는 비지도 학습이다. 따라서 어떤 방식, 어떤 커널이 보다 좋은지 평가할 수 있는 기준이 따로 없다. 하지만 차원축소 이후에 지도학습된 결과를 비교하여 최적의 차원축소 기법과 하이퍼파라미터를 찾을 수는 있다.

예를 들어 아래 코드는 그리드 탐색을 이용하여 최적의 커널 기법과 gamma 옵션의 조합을 찾아낸다.

그리드 탐색 결과 RBF 커널을 gamma=0.043의 조합이 최선임을 알아냈다.

찾아낸 최적의 조합을 이용할 때의 재구성 오차가 거의 0에 가깝게 나온다.

8.5 LLE

국소적 선형 임베딩(LLE, locally linear embedding)은 다양체 학습에 의존하는 비선형 차원축소 기법이다. 아래 코드는 롤케이크를 LLE 방식으로 2차원으로 사영한 결과를 보여준다.

8.6 기타 차원축소 기법

아래 코드는 기타 차원축소 기법을 사용하는 방식을 보여준다.

사영 기법

다양체 학습 기법

아래 코드는 롤케이크를 MDS, Isomap, t-SNE 방식으로 2차원으로 차원축소한 결과를 보여준다.

MNIST 데이터셋 시각화

연습문제 해법

1. to 8.

부록 A 참조.

9.

Exercise: Load the MNIST dataset (introduced in chapter 3) and split it into a training set and a test set (take the first 60,000 instances for training, and the remaining 10,000 for testing).

The MNIST dataset was loaded earlier.

Exercise: Train a Random Forest classifier on the dataset and time how long it takes, then evaluate the resulting model on the test set.

Exercise: Next, use PCA to reduce the dataset's dimensionality, with an explained variance ratio of 95%.

Exercise: Train a new Random Forest classifier on the reduced dataset and see how long it takes. Was training much faster?

Oh no! Training is actually more than twice slower now! How can that be? Well, as we saw in this chapter, dimensionality reduction does not always lead to faster training time: it depends on the dataset, the model and the training algorithm. See figure 8-6 (the manifold_decision_boundary_plot* plots above). If you try a softmax classifier instead of a random forest classifier, you will find that training time is reduced by a factor of 3 when using PCA. Actually, we will do this in a second, but first let's check the precision of the new random forest classifier.

Exercise: Next evaluate the classifier on the test set: how does it compare to the previous classifier?

It is common for performance to drop slightly when reducing dimensionality, because we do lose some useful signal in the process. However, the performance drop is rather severe in this case. So PCA really did not help: it slowed down training and reduced performance. :(

Let's see if it helps when using softmax regression:

Okay, so softmax regression takes much longer to train on this dataset than the random forest classifier, plus it performs worse on the test set. But that's not what we are interested in right now, we want to see how much PCA can help softmax regression. Let's train the softmax regression model using the reduced dataset:

Nice! Reducing dimensionality led to over 2× speedup. :) Let's check the model's accuracy:

A very slight drop in performance, which might be a reasonable price to pay for a 2× speedup, depending on the application.

So there you have it: PCA can give you a formidable speedup... but not always!

10.

Exercise: Use t-SNE to reduce the MNIST dataset down to two dimensions and plot the result using Matplotlib. You can use a scatterplot using 10 different colors to represent each image's target class.

The MNIST dataset was loaded above.

Dimensionality reduction on the full 60,000 images takes a very long time, so let's only do this on a random subset of 10,000 images:

Now let's use t-SNE to reduce dimensionality down to 2D so we can plot the dataset:

Now let's use Matplotlib's scatter() function to plot a scatterplot, using a different color for each digit:

Isn't this just beautiful? :) This plot tells us which numbers are easily distinguishable from the others (e.g., 0s, 6s, and most 8s are rather well separated clusters), and it also tells us which numbers are often hard to distinguish (e.g., 4s and 9s, 5s and 3s, and so on).

Let's focus on digits 3 and 5, which seem to overlap a lot.

Let's see if we can produce a nicer image by running t-SNE on these 3 digits:

Much better, now the clusters have far less overlap. But some 3s are all over the place. Plus, there are two distinct clusters of 2s, and also two distinct clusters of 5s. It would be nice if we could visualize a few digits from each cluster, to understand why this is the case. Let's do that now.

Exercise: Alternatively, you can write colored digits at the location of each instance, or even plot scaled-down versions of the digit images themselves (if you plot all digits, the visualization will be too cluttered, so you should either draw a random sample or plot an instance only if no other instance has already been plotted at a close distance). You should get a nice visualization with well-separated clusters of digits.

Let's create a plot_digits() function that will draw a scatterplot (similar to the above scatterplots) plus write colored digits, with a minimum distance guaranteed between these digits. If the digit images are provided, they are plotted instead. This implementation was inspired from one of Scikit-Learn's excellent examples (plot_lle_digits, based on a different digit dataset).

Let's try it! First let's just write colored digits:

Well that's okay, but not that beautiful. Let's try with the digit images:

Exercise: Try using other dimensionality reduction algorithms such as PCA, LLE, or MDS and compare the resulting visualizations.

Let's start with PCA. We will also time how long it takes:

Wow, PCA is blazingly fast! But although we do see a few clusters, there's way too much overlap. Let's try LLE:

That took a while, and the result does not look too good. Let's see what happens if we apply PCA first, preserving 95% of the variance:

The result is more or less the same, but this time it was almost 4× faster.

Let's try MDS. It's much too long if we run it on 10,000 instances, so let's just try 2,000 for now:

Meh. This does not look great, all clusters overlap too much. Let's try with PCA first, perhaps it will be faster?

Same result, and no speedup: PCA did not help (or hurt).

Let's try LDA:

This one is very fast, and it looks nice at first, until you realize that several clusters overlap severely.

Well, it's pretty clear that t-SNE won this little competition, wouldn't you agree? We did not time it, so let's do that now:

It's twice slower than LLE, but still much faster than MDS, and the result looks great. Let's see if a bit of PCA can speed it up:

Yes, PCA roughly gave us over 2x speedup, without damaging the result. We have a winner!