본문 바로가기

AI/기계학습(Machine Learning)

[기계학습/ML]15. 비지도학습 - 주성분 분석

728x90
반응형

Pupbani는 과일 분류 비지도학습 모델을 완성하였다.

하지만 이 모델을 사용하여 분류한 사진들을 저장하다보니 용량이 너무 많아져 저장 공간이 부족해졌다.

마케팅 팀장은 Pupbani에게 나중에 분류에 영향을 끼지지 않으면서 업로드된 사진의 용량을 줄이는 방법은 없을까 하고 물어 봤다.

Pupbani는 그 질문에 차원축소를 사용해보겠다고 답한 후 차원축소 알고리즘을 작성하러 떠났다.

차원과 차원축소

지금까지 우리는 데이터가 가진 속성을 특성이라고 불렀다.

과일 사진의 경우 10,000개의 픽셀(100 x 100)이 있기 때문에 10,000개의 특성이 있다고 생각하면 된다.

머신러닝에서는 이러한 특성을 차원(dimension)이라고도 부른다.

 

차원의 저주(Curse of Dimentionality)

출처 : https://chulhongsung.github.io/ml/차원의저주/

  • 1차원에서 특성이 3개가 있다고 가정
  • 2차원에서는 이 특성이 3^2가 된다.
  • 차원이 늘어날 수록 특성의 개수가 기하급수 적으로 증가한다.
  • 특성의 개수가 늘어날 수록 동일 데이터를 설명하는 빈 공간이 증가함.
  • 이러한 특징들 때문에 모델링 과정에서의 공간적, 시간적 자원이 많이 들어감.

이러한 차원을 줄일 수 있다면 저장 공간을 크게 절약할 수 있다.

차원을 줄이는 방법으로 비지도 학습 중 하나인 차원 축소 알고리즘을 사용한다.차원 축소 알고리즘은 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기는 줄이고 지도 학습 모델의 성능을 향상 시킬 수 있다.줄어든 차원을 원본 손실을 최소로 하면서 복원할 수 있다.대표적인 알고리즘은 PCA(Principal Component Analysis, 주성분 분석)이다.

PCA 주성분 분석

주성분 분석은 데이터에 있는 분산이 큰 방향을 찾는 것 이다.

분산은 데이터가 널리 퍼져 있는 정도를 말한다.

다음의 데이터가 있을 때

이 데이터는 2개의 특성을 가지고 있다.

이 데이터의 분산이 가장 큰 방향을 표시 해 보겠다.

이 직선이 원점에서 출발한다면 두 원소로 이루어진 벡터로 쓸 수 있다.

ex) (2,1)

(2,1)와 같은 벡터를 주성분(Principal Component)라고 부른다.

이 주성분 벡터는 원본 데이터에 있는 어떤 방향이다.

벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같다.

원본 데이터를 주성분에 직각으로 투영하여 1차원 데이터로 차원을 축소할 수 있다.

그림과 같이 S(2,4)를 주성분에 직각으로 투영하여 P(4.5)라는 1차원 데이터로 차원 축소했다.

 

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

이렇게 찾은 벡터가 두번째 주성분이다.

일반적으로 주성분은 원본 특성의 개수 만큼 찾을 수 있다.

이제 PCA를 사용해서 차원 축소를 해보자.

PCA 클래스

데이터를 준비한다.

2차원 배열의 형태로 만들어 줘야한다.

import numpy as np

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

PCA 클래스를 불러온다.

  • PCA 클래스는 sklearn.decomposition 모듈아래 있다.
  • 객체 생성 시 n_components 매개변수에 주성분 개수를 지정해야 한다.
from sklearn.decomposition import PCA

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

PCA 클래스가 찾은 주성분은 components_ 속성에 저장되어 있다.

print(pca.components_.shape)

n_components를 50으로 지정했기 때문에 첫번째 값인 차원이 50개(= 주성분이 50개)이다.

두번째 차원은 항상 원본 데이터의 특성 개수와 같은 10,000개이다.

 

n_components에는 원하는 설명된 분산의 비율을 입력할 수도 있다.

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

비율은 0~1 사이의 값을 넣으면 된다.

0.5는 설명된 분산의 50%에 달하는 주성분을 찾도록 설정한 것이다.

PCA 클래스는 50%에 달하는 주성분을 찾을 때까지 자동으로 주성분을 찾는다. 

몇 개의 주성분을 찾았는지는 n_components_를 출력하면 알 수 있다.

 

 

주성분을 이미지로 출력해 보자.

import matplotlib.pyplot as plt

def draw_fruits(arr, ratio=1):
    n = len(arr)    # n은 샘플 개수입니다
    # 한 줄에 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:    # 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))

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

transform() 메서드를 사용해 원본 데이터의 차원을 줄일 수 있다.

print("차원 축소 전 데이터 형태:",fruits_2d.shape)
fruits_pca = pca.transform(fruits_2d)
print("차원 축소 후 데이터 형태:",fruits_pca.shape)

원본 데이터 재구성

앞에서 차원을 축소한 데이터를 원본 데이터로 재구성할 수 있다.

차원 축소로 인해 어느정도 손실은 발생할 수 밖에 없다.

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

PCA 클래스의 inverse_transform() 메서드를 통해 재구성을 할 수 있다.

fruits_inverse = pca.inverse_transform(fruits_pca)
print("원본 데이터로 재구성된 데이터 :",fruits_inverse.shape)

복원된 데이터를 시각화 해보자.

fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
    draw_fruits(fruits_reconstruct[start:start+100])
    print("\n")

 

설명된 분산

주성분이 원본 데이터 분산을 얼마나 잘 나타내는지 기록한 값을 "설명된 분산(Explained Variance)"이라고 한다.

PCA 클래스의 explained_variance_ratio_에 기록되어 있다.

print(np.sum(pca.explained_variance_ratio_))

92%가 넘는 분산을 유지하고 있다.

 

설명된 분산을 시각화 해보자.

plt.plot(pca.explained_variance_ratio_)

그래프를 보면 처음 10개의 주성분이 대부분의 분산을 표현하고 있다.

그 다음 부터는 각 주성분이 설명하고 있는 분산은 비교적 작다.

 

다른 알고리즘과 함께 사용하기

차원 축소된 데이터를 사용하여 지도학습 모델을 학습해보자.

로지스틱 회귀 모델을 사용해보겠다.

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()

타겟 데이터를 생성한다.(0: 사과, 1: 파인애플, 2: 바나나)

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']))
print("원본 데이터 훈련 시간 :",np.mean(scores['fit_time']))

점수는 약 0.997, 훈련 시간은 1.819초 정도로 길게 나왔다.

 

PCA로 차원 축소한 데이터로 학습을 해보겠다.

scores = cross_validate(lr, fruits_pca, target)
print("차원 축소 데이터 테스트 세트 점수 :",np.mean(scores['test_score']))
print("차원 축소 데이터 훈련 시간 :",np.mean(scores['fit_time']))

특성을 50개만 사용했는데 점수가 100%이고 훈련시간도 0.024 정도로 매우빠르다.

 

위에서 2개의 주성분을 찾은 데이터로 테스트를 해보자.

scores = cross_validate(lr, fruits_pca, target)
print("차원 축소 데이터 테스트 세트 점수 :",np.mean(scores['test_score']))
print("차원 축소 데이터 훈련 시간 :",np.mean(scores['fit_time']))

놀랍게도 2개의 주성분만으로도 99%의 정확도가 나온다.

 

이번에는 PCA로 차원축소한 데이터로 K-Means를 사용해 클러스터를 찾아보자.

from sklearn.cluster import KMeans

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

result = np.unique(km.labels_, return_counts=True)
print(f"0번 샘플 개수:  {result[1][0]}개")
print(f"1번 샘플 개수:  {result[1][1]}개")
print(f"2번 샘플 개수:  {result[1][2]}개")

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

 

이제 이 데이터를 산점도로 그려보자.

데이터 특성의 수가 적기 떄문에 화면에 출력이 비교적 쉽다.

for label in range(0, 3):
    data = fruits_pca[km.labels_ == label]
    plt.scatter(data[:,0], data[:,1])
plt.legend(['사과', '바나나', '파인애플'],loc="upper center")
plt.show()

산점도 아주 잘 구분되게 그려진다.

이렇게 데이터 시각화를 통해 데이터에 대한 통찰을 얻을 수 있다.

  • 사과와 파인애플 데이터는 서로 가까이 붙어 있어 몇 개가 혼동 될 수 있을 것 같다.

Pupbani는 차원 축소를 통해 데이터의 용량을 줄이고 나아가 학습 모델의 성능까지 끌어올리는 결과를 달성하였다.

728x90
반응형