Post

[혼공머신] 5주차 06-3 주성분 분석

[혼공머신] 5주차 06-3 주성분 분석

본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.

차원과 차원 축소

지금까지 우리는 다양한 data를 살펴보았다. Data에는 feature들이 있는데, 예를 들어 회원 data에는 이름, 나이, 성별 등 다양한 feature들이 존재한다. 그런데 한두개라면 상관 없겠으나 지난번에 살펴본 이미지의 경우 feature가 엄청나게 많이 존재한다. 예를 들어 100*100 픽셀의 이미지가 있을 경우 총 10,000개의 픽셀이 존재한다. 이러한 경우, 10,000개의 feature가 존재하게 되는 것이다. 하나의 sample에 10,000개의 feature가 있고, 수많은 data가 존재할 경우, 그 크기는 엄청나게 빠른 속도로 증가할 것이다. 또한, overfitting의 우려도 존재하며, 이를 방지하기 위한 더 많은 data는 더 많은 시간과 돈이 소요된다. 보통 이러한 경우 더 많은 data를 구할 수도 없고 우리는 시간과 돈 둘 다 없기에 크기를 줄일 수 있는 방법을 찾아야 한다.

그런데, 단순히 data 사이즈만 줄이는 경우 underfitting이 발생할 수 있다. 그러면 어떻게 해야 할까? 이를 위해 차원 축소(Dimensionality Reduction)라는 방법이 존재한다. Dimensionality reduction은 data를 가장 잘 나타낼 수 있는 특성만을 선택하는 방법이다. 이번에는 dimensionality reduction의 대표적인 방법인 주성분 분석(PCA, Principal Component Analysis)를 살펴볼 것이다.

주성분 분석

PCA는 data 내의 variance이 가장 큰 방향을 찾는 것이라고 생각하면 된다. 예를 들어 아래와 같이 한 방향으로 길게 분포하고 있는 data가 존재한다고 하자. image

이때, 길게 뻗은 data 방향으로 선을 그을 수 있을 것이다. 이렇게 길게 뻗은 data 방향으로 그은 선을 우리는 주성분(Principal Component)이라고 한다. image

2차원 좌표평면 위에 있는 data들을 이 선 위로 투영시킨다면 모든 data들이 1차원 점이 될 수 있을 것이다. image

만약 첫번째 principal component을 찾은 후에, 이 선, 즉 벡터에 수죽이고 분산이 가장 큰 방향을 찾아 2번째 principal component를 찾는다. 방금 살펴본 것과 같이 일반적으로 principal component은 feature의 개수만큼 찾을 수 있다. 이제 파이썬으로 찾아보자.

PCA

지난번과 마찬가지로 과일 이미지를 가져오고 동일하게 과일 그림을 그리는 함수를 만들어서 이미지를 살펴보자. PCA를 실행하니 아래와 같이 정체를 알 수 없는 그림이 나왔다. 아래의 그림은 가장 variance이 큰 방향으로 principal component를 순서대로 구해서 나타낸 것이다. 이렇게 principal component를 구했으므로, 기존의 10,000개의 feature를 방금 찾은 50개릐 principal component의 feature로 줄여볼 수 있다. transform을 통해서 원본 데이터의 dimension을 줄여보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

data = np.load('fruits_300.npy')
data_2d = data.reshape(-1, 100*100)

pca = PCA(n_components=50)
pca.fit(data_2d)

import matplotlib.pyplot as plt
def draw_fruit(arr, ratio=1):
    n = len(arr)
    rows = int(np.ceil(n/10))
    cols = n if rows < 2 else 10
    fig, axs = plt.subplots(rows, cols, figsize=(cols*ratio, rows*ratio), squeeze=False)
    for i in range(rows):
        for j in range(cols):
            if i * 10 + j < n:
                axs[i, j].imshow(arr[i*10+j], cmap='gray_r')
            axs[i, j].axis('off')
    plt.show()

draw_fruit(pca.components_.reshape(-1, 100, 100))
data_pca = pca.transform(data_2d)

image

그런데 한 가지 궁금한 것이 있다. 만약 dimension을 줄인 다음 원래대로 복원은 불가능할까? 답을 먼저 말하자면 어느 정도 가능하다. 아래의 이미지를 보면, 기존의 과일들을 거의 완벽하게 복원한 것을 확인할 수 있다. 즉, 아까 만든 50개의 principal component가 기존의 data를 매우 잘 설명하고 있는 것이다. 그러면 얼마나 principal component가 기존의 data를 설명하는지 알고 싶을 것이다.

1
2
3
4
5
6
data_inverse = pca.inverse_transform(data_pca)

fruit_reconstruct = data_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
    draw_fruit(fruit_reconstruct[start:start+100])
    print('\n')

image image image

설명된 분산

방금 말한, principal component가 원본 data의 variance을 얼마나 잘 나타내는지를 기록한 것을 설명된 분산(Explained Variance)라고 한다. 아까 우리가 만든 principal component들의 순서는 얼만 이 explained variance가 큰지를 바탕으로 순서대로 구한 것이다. 그럼 이것을 값으로 한 번 살펴보도록 하자. 값을 살펴보니 무려 92%를 설명한다는 것을 확인할 수 있다.

1
np.sum(pca.explained_variance_ratio_)

0.9214952099938081

그리고 여기서 또 한 가지 도움이 되는 것을 찾을 수 있다. 지난번에 K-Means 알고리즘을 살펴보았을 때 최적의 $K$값을 찾는 것을 해본 기억이 있을 것이다. 그때, 값이 감소하는 속도를 바탕으로 최적의 $K$값을 찾았었는데, 여기서도 동일하게 사용할 수 있다. 여기서도 최적의 principal component 개수를 explained variance의 값을 가지고 살펴보도록 하자. 아래의 plot을 살펴보면, 10 전까지는 값이 어느정도 큰데, 그 이후로는 값의 변화가 거의 없는 것을 확인할 수 있다. 따라서 처음 10개의 principal component를 사용하면 어느정도 variance를 표현할 수 있을 것이다.

1
2
plt.plot(pca.explained_variance_ratio_)
plt.show()

image

다른 알고리즘과 사용하기

PCA는 그 자체로도 의미를 찾을 수 있지만, 다른 알고리즘과 함께 사용하면 더 좋은 결과를 얻을 수 있다. 지금은 과일을 classify하는 것을 하고 있기에 간단한 logistic regression 모델을 사용해서 확인해보자. 먼저, logistic regression을 cross validation을 통해 성능을 확인해보도록 하자. Cross validation score를 확인했을 때, 0.997로 엄청나게 높은 것을 확인할 수 있다. 그리고 학습에 소요되는 시간은 0.13초 정도 소요되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate
from sklearn.decomposition import PCA

data = np.load('fruits_300.npy')
data_2d = data.reshape(-1, 100*100)

lr = LogisticRegression()
y = np.array([0]*100 + [1]*100 + [2]*100)

scores = cross_validate(lr, data_2d, y)
print(np.mean(scores['test_score']), np.mean(scores['fit_time']))

0.9966666666666667 0.1372838020324707

이제 PCA를 사용했을 경우 어떻게 되는지 비교해보도록 하자. 아까와 성능은 동일하지만 학습 시간이 0.002초로 엄청나게 감소한 것을 확인할 수 있다. 항상 말하는 것이지만 우리에게 시간은 매우 중요하다. 비용적인 측면에서도 중요하지만, 시간이 길어지면 길어질수록 우리의 의지도 떨어지기 때문이다. 한 번 학습할 때마다 몇시간씩 해야 하면 실시간으로 의지가 떨어진다. 이를 막기 위해서라도 빠른 학습은 매우 중요하다.

1
2
3
4
5
pca = PCA(n_components=50)
pca.fit(data_2d)
data_pca = pca.transform(data_2d)
scores = cross_validate(lr, data_pca, y)
print(np.mean(scores['test_score']), np.mean(scores['fit_time']))

0.9966666666666667 0.002648448944091797

지금싸지 PCA를 사용할 때, principal component의 개수를 직접 설정해서 했었다. 그런데, principal component의 개수를 정하는 또 다른 방법이 있다. PCA를 할 때, 분산의 비율을 입력하여 사용할 수도 있다. 아래와 같이 비율을 입력해서 학습을 실시할 수도 있다.

1
2
3
4
5
pca = PCA(n_components=0.5)
pca.fit(data_2d)
data_pca = pca.transform(data_2d)
scores = cross_validate(lr, data_pca, y)
print(np.mean(scores['test_score']), np.mean(scores['fit_time']))

0.9933333333333334 0.007794666290283203

이제 이렇게 했을 때 몇 개의 feature를 사용했을지 확인해보자. 엄청난 숫자가 나왔다. 무려 2개의 feature만 사용해서 위와 같은 결과를 낸 것이다.

1
pca.n_components_

2

이제 이렇게 축소시킨 data를 가지고 K-Means 알고리즘 학습을 시켜보면 어떨까? K-Means 알고리즘을 통해 학습한 결과 3개의 cluster로 나뉘었으며, 개수도 이전에 실했했던 K-Means 알고리즘의 결과와 비슷하다.

from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3, random_state=42)
kmeans.fit(data_pca)
np.unique(kmeans.labels_, return_counts=True)

(array([0, 1, 2]), array([110, 99, 91], dtype=int64))

그리고 dimensionality reduction의 도움을 받아 visualization이 불가능한 data를 visualization을 할 수 있다. 처음 이미지 data를 살펴보았을 때, feature가 10,000개 있다고 했었다. 즉, 10,000 dimension의 data인데, 4 dimension도 visualization을 할 수 없는데 10,000 dimension은 visualization에 대한 실마리도 찾기 어려울 것이다. 그런데 방금 우리가 PCA를 통해 dimensionality reduction를 했더니 feature가 2개가 나왔다. 2 dimension의 경우 우리가 plot을 통해 매우 간편하게 그릴 수 있다. 한번 그려서 확인해보도록 하자. 아래와 같이 plot을 통해 어떻게 data가 clustering 되었는지 확인할 수 있따. Data를 보니 apple과 pineapple이 가깝게 붙어 있어 이 둘이 섞이게 되어 accuracy가 떨어지게 되는 것을 알 수 있다.

1
2
3
4
5
for label in range(0, 3):
    data = data_pca[kmeans.labels_ == label]
    plt.scatter(data[:, 0], data[:, 1])
plt.legend(['apple', 'banana', 'pineapple'])
plt.show()

image

This post is licensed under CC BY 4.0 by the author.