[혼공머신] 차원 축소의 주성분 분석 알고리즘

강민우·2022년 2월 12일
0
post-thumbnail

[혼자 공부하는 머신러닝+딥러닝] 책에 기반한 정리글입니다.
전체 소스코드는 아래 Github 링크에서 확인할 수 있습니다.

Github 링크

0. 개요

머신러닝 모델을 만들며 여러 이미지들을 저장할 때 저장공간이 부족할 수 있다. 이때 차원 축소 알고리즘을 사용하는데, 이를 통해 데이터셋의 크기를 줄일 수 있고 비교적 시각화하기 쉽다는 장점이 있다.

이번 편에서는 차원 축소 알고리즘 중 하나인 주성분 분석을 알아본다.

1. 차원 축소란

데이터가 가진 속성을 특성이라고 부른다면 저번 편의 과일 사진은 1만개의 픽셀이 있기 때문에 1만개의 특성이 있는 셈이다. 머신러닝을 이를 차원이라고 부르는데, 즉 1만개의 차원이 있고 이를 줄일 수 있다면 저장 공간을 크게 절약할 수 있다.

차원 축소는 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고, 지도학습 모델 성능을 향상시킬 수 있는 방법이다. 또한 줄어든 차원에서 원본 차원으로 손실을 줄이면서 복원할 수도 있다.

여기서는 대표적인 차원축소 알고리즘인 주성분 분석을 설명한다.

2. 주성분 분석이란?

주성분 분석차원 축소 알고리즘 중 하나로, 데이터에서 가장 분산이 큰 방향을 찾는 방법이다.
이 방향을 주성분이라고 부르는데, 원본 데이터를 주성분에 투영하여 새로운 특성을 만들 수 있다.
일반적으로 주성분은 원본 차원과 같고, 주성분으로 바꾼 데이터는 차원이 줄어든다.
또한 주성분은 원본 특성의 갯수만큼 찾을 수 있고, 원본 특성의 갯수와 샘플 갯수 중 작은 값만큼 찾을 수 있다.

3. PCA클래스 : 주성분 분석 알고리즘 제공

sklearn.decomposition 모듈 아래의 PCA 클래스로 모델에 주성분 분석 알고리즘을 적용한다.

3-1. 데이터 준비하기

넘파이 배열을 다운받아 reshape()를 통해 재배열한다.

!wget https://bit.ly/fruits_300_data -O fruits_300.npy
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

3-2. 주성분 분석 알고리즘으로 차원 축소

n_components 매개변수에 주성분의 갯수를 지정해야 한다.
여기서는 임으로 50으로 지정한다.

from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)
print(pca.components_.shape)
출력 (50, 10000)

PCA가 찾은 주성분은 components_ 속성에 저장된다.
50개의 주성분과 10*10개의 특성을 찾았다.

이전편의 draw_fruits() 함수로 이 주성분을 그림으로 그려본다.

draw_fruits(pca.components_.reshape(-1, 100, 100))

이 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다.

주성분을 찾았기 때문에 원본 데이터를 주성분에 투영하여 10000개의 특성에서 50개로 줄일 수 있다. transform() 메서드를 사용한다.

print(fruits_2d.shape) #원본 데이터

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape) # 차원축소 데이터
출력
(300, 10000)
(300, 50)

특성의 갯수가 50개로 줄어들었다.

3-3. 축소된 데이터로 원본 데이터 재구성

10,000개의 특성에서 50개로 줄어들었기 때문에, 어느정도 손실이 있을 수밖에 없다. 그러나 최대한 분산이 큰 방향으로 데이터를 투영했기 때문에 원본 데이터를 상당부분 재구성할 수 있다.

inverse_transform() 메서드로 10,000개의 특성을 복원한다.

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

10,000개의 특성으로 복구되었다.
이 데이터를 100X100 크기로 바꾸어 그림으로 표현해본다.

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

흐린 부분도 있지만 전체적으로 잘 표현되었다.

3-4. 설명된 분산

설명된 분산이란 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값이다. PCA 클래스의 explained_variance_ratio_ 에 각 주성분의 설명된 분산 비율이 기록되어있다.
첫 번째 주성분의 설명된 분산이 가장 크고, 분산 비율을 모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있다.

print(np.sum(pca.explained_variance_ratio_))
plt.plot(pca.explained_variance_ratio_)
plt.show()
출력 0.921517484011715

50개의 분산에서 92%의 분산을 유지하고 있으며, 그래프로 그려 보면 적절한 주성분의 갯수를 찾는 데 도움이 된다.
처음 10개의 주성분이 대부분을 표현하고 있음을 알 수 있다.

한편, PCA 클래스의 n_components 매개변수에 주성분의 개수가 아닌, 원하는 설명된 분산의 비율을 입력할 수 있다. PCA 클래스는 지정된 비율에 도달할 때까지 자동으로 주성분을 찾는다.

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

print(pca.n_components_) 

fruits_pca = pca.transform(fruits_2d)
scores = cross_validate(lr, fruits_pca, target)

print(np.mean(scores['test_score']))
출력
2    # 2개의 특성으로 원본 데이터 분산의 50% 표현
0.9933333333333334    # 교차 검증 점수

2개의 특성으로도 원본 데이터 분산의 50%를 표현하며, 교차 검증에서 높은 점수를 받을 수 있다.

4. 여러 알고리즘에 차원축소 적용하기

4-1. 로지스틱 회귀 모델

3개의 과일 사진을 분류해야 하므로 로지스틱 회귀 모델을 이용한다.
타깃값을 사과는 0, 파인애플을 1, 바나나를 2로 지정한다.

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()

# 타깃 데이터 만들기
target = np.array([0]*100 + [1]*100 + [2]*100)

먼저 10,000개의 특성으로 모델을 훈련하고 교차검증을 수행한다.

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.9966666666666667
0.9674365997314454

교차 검증의 점수는 매우 높지만, 특성이 10,000개나 되기 때문에 과대적합된 모델을 만들기 쉽다.

차원축소된 데이터로 모델을 다시 훈련한다.

scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
1.0
0.02288055419921875

100%의 정확도와 매우 짧은 훈련시간을 기록했다.

4-2. k-평균 알고리즘

차원 축소된 데이터를 사용해 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], dtype=int32), array([110,  99,  91]))

찾은 클러스터는 각각 110개, 99개, 91개의 샘플을 포함한다.

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


첫 번째 그림에서 사과와 파인애플, 바나나 몇 개가 혼동되지만, 전체적으로 높은 정확도를 보여준다.

훈련 데이터의 차원을 줄이면 시각화가 쉽다는 것도 장점이다. 3개 이하로 차원을 줄이면 화면에 출력하기 쉬운데, fruits_pca는 2개의 특성이 있기 때문에 2차원으로 표현할 수 있다.

앞에서 찾은 km.labels_ 를 사용해 클러스터별로 산점도를 그린다.

for label in range(0, 3) :
  data = fruits_pca[km.labels_ == label]
  plt.scatter(data[:,0], data[:,1])
plt.legend(['pineapple', 'banana', 'apple'])
plt.show()

각 클러스터의 산점도가 잘 표현되는 것을 알 수 있다.

profile
어제보다 성장한 오늘

0개의 댓글