티스토리 뷰

비지도 학습의 예제들을 다뤄보면서, 과일 사진들을 예제 데이터로 사용하였다.

 

사진 데이터의 경우, 만약 10000개의 픽셀을 분석하여 모델을 만드는 예제라면, 특성이 10000개인 데이터를 다루는 셈이다.

 

머신러닝에서는 이런 특성을 차원이라고 하는데, 차원이 많으면 많을수록 좋은 성능을 내겠지만, 필요 이상으로 많다면, 모델이 과대적합될 가능성이 생기고, 저장 공간을 너무 많이 차지할 수도 있다.

 

그렇기 때문에 차원 축소를 통해 데이터를 가장 잘 나타내는 일부 특성만 선택하여 데이터의 크기를 줄이고 학습 모델의 성능을 향상시킬 수 있다.

 

이번에는 대표적인 차원 축소 알고리즘인 주성분 분석을 다뤄보려고 한다.

 

바로 한번 알아보자.


주성분 분석(Priciple component analysis)은 데이터에 있는 분산이 큰 방향을 찾는 것으로 이해할 수 있다. 간단히 분산은 데이터가 널리 퍼져있는 정도를 나타내는데, 분산이 큰 방향이란 데이터를 잘 표현하고 있는 어떤 벡터라고 생각하면 된다.

 

예를 들어, 아래와 같은 2차원 데이터가 있다면

직관적으로 봤을 때, 아래처럼 길게 늘어진 대각선 방향이 분산이 가장 크다는 것을 알 수 있다.

해당 직선은 (1, 1)을 지나는 벡터라고 말할 수 있으며, 아래와 같이 데이터를 하나 벡터에 투영하여 1차원 벡터로 만들 수 있다.

즉, 2차원 상에 존재하던 데이터인 (7, 10)을 분산이 가장 큰 방향으로 진행하던 벡터에 투영하여 1차원 데이터인 (10, 10)을 얻어낸 것이다.

 

이때, 이 벡터를 주성분이라고 하며 주성분의 차원은 원본 차원과 동일하다. 하지만, 다른 차원에 있던 데이터를 주성분에 투영한 데이터는 차원이 하나 줄어드는 셈이다. 

 

이렇게 첫번째 주성분을 찾은 다음 이 벡터에 수직이고 분산이 가장 큰 다음 방향을 찾는 것이다. 

 

위 예제 데이터는 2차원 데이터이기 때문에 다음 주성분이 될 수 있는 방향은 아래 표시한 방향 밖에 없으며, 일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있다.

 

이제 주성분 분석이 무엇인지 알았으니, 바로 코드와 함께 실습해보자.


마찬가지로 저번 포스팅에서 사용했던 과일 데이터를 가지고 진행한다.

import numpy as np
import matplotlib.pyplot as plt

fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

 

이제 사이킷런이 제공하는 PCA 클래스로 주성분 분석 모델을 한번 만들어보자. 매우 간단하다.

from sklearn.decomposition import PCA

# n_components -> 사용할 주성분의 개수
pca = PCA(n_components=50)
pca.fit(fruits_2d)

print(pca.components_.shape)
# (50, 10000)

n_components를 50으로 지정했기 때문에 pca.components의 형태를 확인했을 때, 배열의 첫번째 차원이 50인 것을 확인할 수 있다. 두번째 차원은 항상 원본 데이터의 특성 개수와 같은 10000이다.

 

이제, 이 주성분을 그림으로 그려보자.

def draw_fruits(arr, ratio=1):
    # n = 샘플의 개수
    n = len(arr)

    # 한 줄에 10개씩 이미지를 그린다. 샘플 개수를 10으로 나누어 전체 행 개수를 계산한다.
    rows = int(np.ceil(n/10))

    # 행이 1개면 열의 개수는 샘플의 개수, 그렇지 않으면 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_fruits(pca.components_.reshape(-1, 100, 100))

위 그림은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다. 즉, 데이터 셋이 있는 어떤 특징을 잡아낸 것처럼 생각할 수도 있다.

 

주성분을 찾았으므로, 원본 데이터를 주성분에 투영하여 특성의 개수를 10000개에서 50개로 줄일 수 있다.

이는 마치 원본 데이터를 각 주성분으로 분해하는 것으로 생각할 수 있다. 

 

PCA의 transform() 메서드를 사용하여 원본 데이터의 차원을 50으로 줄여보자.

print(fruits_2d.shape)
# (300, 10000)

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
# (300, 50)

성공적으로 데이터의 특성이 10000개에서 50개로 줄어든 것을 볼 수 있다.

 

하지만 이로 인해 어느 정도 손실이 발생할 수 밖에 없다. 

 

분산이 최대한 큰 방향으로 데이터를 투영했기 때문에 원본 데이터를 상당 부분 재구성할 수 있다. 

 

PCA 클래스는 이를 위해 inverse_transform() 메서드를 제공하는데, 이는 축소한 차원을 재구성해주는 역할을 한다.

fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
# (300, 10000)

 

데이터를 제대로 재구성 하였는지 확인해보면,

거의 모든 과일이 잘 복원되었다! 이는 축소한 50개의 특성이 데이터의 분산을 가장 잘 보존하도록 변환된 것이기 때문이다.


설명된 분산

주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값을 설명된 분산이라고 한다. PCA 클래스의 explained_variance_ratio_ 에 각 주성분의 설명된 분산 비율이 기록되어 있다. 

 

당연히 첫번째 주성부느이 설명된 분산이 가장 크다. 이 분산 비율을 모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있다.

print(np.sum(pca.explained_variance_ratio_))
# 0.9214883529082143

50개의 특성이 약 92%의 분산을 유지하고 있다.

 

아래와 같이 설명된 분산을 그래프로 나타내면 더욱 확실하게 이해할 수 있다.

보면, 처음 10개의 주성분이 대부분의 분산을 표현하고 있다는 것을 알 수 있다. 

 

이제 이 축소된 데이터를 사용하지 않았을 때와 어떤 차이가 있는 지 확인해보자.


원본과 비교

 

원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해보고 어떤 차이가 있는 지 확인해보자.

 

먼저 지도 학습을 진행하려고 하니, 타깃 데이터를 만들어줘야 하는데, 아래와 같이 레이블을 붙여 만들어주면 된다.

 

지도 학습 알고리즘은 로지스틱 회귀 모델을 사용한다.

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()

target = np.array([0]*100 + [1]*100 + [2]*100)

 

이제 cross_validate로 교차 검증하며 성능을 확인해보자.

from sklearn.model_selection import cross_validate

scores = cross_validate(lr, fruits_2d, target)

print(np.mean(scores['test_score']))
# 0.9966666666666667

print(np.mean(scores['fit_time']))
# 0.35381221771240234

교차 검증의 점수가 0.997 정도로 매우 높다! 특성이 10000개나 되기 때문에 300개의 샘플에서는 금방 과대적합된 모델이 나온다.

 

이제 차원 축소한 데이터를 집어 넣어 확인해보자.

from sklearn.model_selection import cross_validate

scores = cross_validate(lr, fruits_pca, target)

print(np.mean(scores['test_score']))
# 1.0

print(np.mean(scores['fit_time']))
# 0.012211275100708009

?? 50개의 특성만 사용했는데 정확도가 100%가 찍혔다. 훈련 시간은 0.03초로 20배 이상 감소했다. (개쩐다;)


위에서 PCA의 n_components를 입력할 때, 주성분의 개수를 입력했는데, 이 대신 원하는 분산의 비율을 입력하여 얻을 수도 있다. 

pca = PCA(n_components=0.5)
pca.fit(fruits_2d)

print(pca.n_components_)
# 2

fruits_pca = pca.transform(fruits_2d)

scores = cross_validate(lr, fruits_pca, target)

print(np.mean(scores['test_score']))
# 0.99

print(np.mean(scores['fit_time']))
# 0.017415618896484374

위 코드를 보면, 분산의 50%에 달하는 주성분의 개수를 찾고 그 모델로 스코어를 도출했다.

 

보면, 2개의 주성분만 사용했는데도 불구하고 0.99의 정확도를 보인다.

 

이젠 k-평균 알고리즘과 비교해보자.

from sklearn.cluster import KMeans

km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_pca)

print(np.unique(km.labels_, return_counts=True))
# (array([0, 1, 2]), array([110,  99,  91], dtype=int64))

for label in range(0, 3) :
    draw_fruits(fruits[km.labels_ == label])
    print("\n")

확실히 분산의 50%만 사용한 주성분 2개로 분류했더니, 몇 가지 샘플을 헷갈려하는 것을 볼 수 있다.

 

이번엔 산점도로 확인해보자.

for label in range(0, 3) :
    data = fruits_pca[km.labels_ == label]
    plt.scatter(data[:,0], data[:,1])

plt.legend(['apple', 'banana', 'pineapple'])
plt.show()
    

확실히, 사과와 파인애플 클러스터 경계가 가깝게 붙어있어, 두 클러스터 샘플은 혼동을 일으키기 쉬울 거 같다.

 

이는 주성분의 개수를 늘려주면 바로 정확도를 높일 수 있지만, 차원 축소에 대해서는 충분히 다뤄본 거 같으니 넘어가자.

 

 

 

끝!

Comments