주성분 분석 (Principal Component Analysis)¶
차원과 차원 축소¶
- 지금까지 우리는 데이터가 가진 속성을 특성이라고 불렀다.
- 과일 사진의 경우 10,000개의 픽셀이 있기 때문에 10,000개의 특성이 있는 셈이다.
- 머신러닝에서는 이런 특성을 차원(dimension)이라고도 부른다.
- 이 차원을 줄일 수 있다면 저장 공간을 크게 절약할 수 있을 것이다.
- 이를 위해 비지도 학습 작업 중 하나인 차원 축소(dimensionality reduction) 알고리즘을 다루어 보자.
- 차원 축소는 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기도 줄이고 지도학습 모델의 성능을 향상시킬 수 있는 방법이다.
- 또한 줄어든 차원에서 다시 원본 차원으로 손실을 최대한 줄이면서 복원할 수도 있다.
- 이 절에서는 대표적인 차원 축소 알고리즘인 주성분 분석(principal component analysis)를 배워보자. 줄여서 PCA라고 한다.
PCA 소개¶
- PCA는 데이터에 있는 분산이 큰 방향을 찾는 것으로 이해할 수 있다.
- 분산은 데이터가 널리 퍼져있는 정도를 말한다.
- 분산이 큰 방향이란 데이터를 잘 표현하는 어떤 벡터라고 생각할 수 있다.
- 2차원 그래프에서 일련의 점들에 대해 가장 잘 설명하는 직선이 있을 때, 이 직선이 원점에서 출발한다는 두 원소로 이루어진 벡터로 설명할 수 있다.
- 이 벡터를 주성분(principal component)라고 한다.
- 주성분 벡터는 원본 데이터에 있는 어떤 방향이다.
- 따라서 주 성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같다.
- 하지만 원본 데이터는 주성분을 사용해 차원을 줄일 수 있다.
- 예를 들어 직선에서 떨어진 점을 직선에 projection하면 직선 상의 한 점을 얻을 수 있다.
- 주성분은 가장 분산이 큰 방향이기 때문에 projection하여 바꾼 데이터는 원본이 가지고 있는 특성을 가장 잘 나타내고 있을 것이다.
- 첫 번째 주성분을 찾은 다음 이 벡터에 수직이고 분산이 가장 큰 다음 방향을 찾는다.
- 이 벡터가 두 번째 주성분이다.
- 일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있다.
- 이해가 안간다면 https://darkpgmr.tistory.com/110를 참고하자.
PCA 클래스¶
In [1]:
import urllib.request
url = 'https://bit.ly/fruits_300_data'
filename = 'fruits_300.npy'
urllib.request.urlretrieve('https://bit.ly/fruits_300_data', 'fruits_300.npy')
Out[1]:
In [2]:
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
- 사이킷런은 sklearn.decomposition 모듈 아래 PCA 클래스로 주성분 분석 알고리즘을 제공한다.
- PCA 클래스의 객체를 만들 때 n_components 매개변수에 주성분의 개수를 지정해야 한다.
- k-means와 마찬가지로 비지도 학습이므로 fit() 메서드에 타깃값을 제공하지 않는다.
In [3]:
from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)
Out[3]:
- PCA 클래스가 찾은 주성분은 components_ 속성에 저장되어 있다.
- 이 배열의 크기를 확인해보자.
In [4]:
print(pca.components_.shape)
- ncomponents=50으로 지정했으므로 pca.components 배열의 첫 번째 차원은 50이다.
- 즉, 50개의 주성분을 찾은 것이다.
- 두 번째 차원은 항상 원본 데이터의 특성 개수와 같은 10,000이다.
- 원본 데이터와 차원이 같으므로 주성분은 100*100 크기의 이미지처럼 출력해 볼 수 있다.
- 이전 절에서 사용했던 draw_fruits() 함수를 이용하여 주성분을 그림으로 그려보자.
In [5]:
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()
In [6]:
draw_fruits(pca.components_.reshape(-1, 100, 100))
- 이 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다.
- 한편으로는 데이터셋에 있는 어떤 특징을 잡아낸 것처럼 생각할 수도 있다.
- 주성분을 찾았으므로 원본 데이터를 주성분에 projection하여 특성의 개수를 10,000개에서 50개로 줄일 수 있다.
- 마치 원본 데이터를 각 주성분으로 분해하는 것으로 생각할 수 있다.
- PCA의 transfrom() 메서드를 사용해 원본 데이터의 차원을 50으로 줄여보자.
In [7]:
print(fruits_2d.shape)
In [8]:
fruits_pca = pca.transform(fruits_2d)
In [9]:
print(fruits_pca.shape)
- fruit_2d는 (300, 10000) 크기의 배열이었다.
- 10,000개의 픽셀 (특성)을 가진 300개의 이미지이다.
- 50개의 주성분을 찾은 PCA 모델을 사용해 이를 (300, 50) 크기의 배열로 변환했다.
- 이제 fruits_pca 배열은 50개의 특성을 가진 데이터이다.
- 데이터가 무려 1/200로 줄어들었다.
- 다시 원상 복구할 수도 있을까?
원본 데이터 재구성¶
- 앞서 10,000개의 특성을 50개로 줄였다.
- 이로 인해 어느 정도 손실이 발생할 수밖에 없다.
- 하지만 최대한 분산이 큰 방향으로 데이터를 투영했기 때문에 원본 데이터를 상당 부분 재구성 할 수 있다.
- PCA 클래스는 이를 위해 inverse_transform() 메서드를 제공한다.
In [10]:
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
- 예상대로 10,000개의 특성이 복원되었다.
- 이 데이터를 100*100 크기로 바꾸어 100개씩 나누어 출력해보자.
In [11]:
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
In [12]:
for start in [0, 100, 200]:
draw_fruits(fruits_reconstruct[start:start+100])
print("\n")
- 거의 모든 과일이 잘 복원되었다.
- 일부 흐리고 번진 부분이 있지만 불과 50개의 특성을 10,000개로 늘린 것임을 감안한다면 놀라운 일이다.
- 이 50개의 특성이 분산을 가장 잘 보존하도록 변환된 것이기 때문이다.
- 그럼 50개의 특성은 얼마나 분산을 보존하고 있는 것일까?
Explained Variance¶
- 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지를 기록한 값을 설명된 분산(explained variance)라고 한다.
- PCA 클래스의 explained_variance_ratio_에 각 주성분의 설명된 분산 비율이 기록되어 있다.
- 당연히 첫 번째 주성분의 설명된 분산이 가장 크다.
- 이 분산 비율을 모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있다.
In [13]:
print(np.sum(pca.explained_variance_ratio_))
- 92%가 넘는 분산을 유지하고 있다.
- 앞에서 50개의 특성에서 원본 데이터를 복원했을 때 원본 이미지의 품질이 높았던 이유를 여기에서 찾을 수 있다.
- 설명된 분산의 비율을 그래프로 그려보면 적절한 주성분의 개수를 찾는데 도움이 된다.
In [14]:
plt.plot(pca.explained_variance_ratio_)
Out[14]:
- 처음 10개의 주성분이 대부분의 분산을 표현하고 있다.
다른 알고리즘과 함께 사용하기¶
- 과일 사진 원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해보고 어떤 차이가 있는지 알아보자.
- 3개의 과일 사진을 분류해야 하므로 간단히 로지스틱 회귀 모델을 사용하겠다.
In [15]:
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
- 지도 학습 모델을 사용하려면 타깃값이 있어야 한다.
- 사과를 0, 파인애플을 1, 바나나를 2로 지정한다.
In [16]:
target = np.array([0] * 100 + [1] * 100 + [2] * 100)
- 먼저 원본 데이터인 fruits_2d를 사용해보자.
- 로지스틱 회귀 모델에서 성능을 가늠해 보기 위해 cross_validate()로 교차 검증을 수행한다.
In [17]:
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 정도로 매우 높다.
- 특성이 10,000개나 되기 때문에 300개의 샘플에서는 금방 과대적합된 모델을 만들기 쉽다.
- fit_time은 1.6초 정도 걸렸다.
- 이 값을 PCA로 축소했을 때의 값과 비교해보자.
In [18]:
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
- 50개의 특성만 사용했음에도 정확도가 100%이고 시간도 20배 이상 감소했다.
- PCA로 훈련 데이터의 차원을 축소하면 저장 공간뿐만 아니라 머신러닝 모델의 훈련 속도도 높일 수 있다.
- 앞서 PCA 클래스를 사용할 때 n_components 매개변수에 주성분의 개수를 지정했다.
- 이 대신 원하는 설명된 분산의 비율을 입력할 수도 있다.
- PCA 클래스는 지정된 비율에 도달할 때 까지 자동으로 주성분을 찾는다.
- 설명된 분산의 50%에 달하는 주성분을 찾도록 PCA 모델을 만들어보자.
In [19]:
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)
Out[19]:
- 간단히 주성분의 개수 대신 0~1 사이의 비율을 실수로 입력하면 된다.
In [20]:
print(pca.n_components_)
- 2개의 특성만으로 원본 데이터에 있는 분산의 50%를 설명할 수 있다 !
- 이 모델로 원본 데이터를 변환해보자.
- 주성분이 2개이므로 변환된 데이터의 크기는 (300, 2)가 될 것이다.
In [21]:
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
In [22]:
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
- 놀랍게도 2개의 특성만 사용했음에도 99%의 정확도를 보인다.
- 이번에는 차원 축소된 데이터를 이용해 k-means 알고리즘으로 클러스터를 찾아보자.
In [23]:
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_pca)
Out[23]:
In [24]:
print(np.unique(km.labels_, return_counts=True))
- fruit_pca로 찾은 클러스터는 각각 91개, 99개, 110개의 샘플을 포함한다.
- 앞선 2절에서 원본 데이터를 사용했을 때와 거의 비슷한 결과이다.
- KMeans가 찾은 레이블을 사용해 과일을 출력해보겠다.
In [25]:
for label in range(0, 3):
draw_fruits(fruits[km.labels_ == label])
print("\n")
- 앞선 절에서의 결과가 비슷하게 파인애플과 사과는 조금 혼동되는 면이 있다.
- 훈련 데이터의 차원을 줄이면 또 하나 얻을 수 있는 장점은 시각화이다.
- 3개 이하로 차원을 줄이면 화면에 출력하기 비교적 쉽다.
- fruit_pca 데이터는 2개의 특성이 있기 때문에 2차원으로 표현할 수 있다.
- 앞에서 찾은 km.labels_ 를 사용해 클러스터별로 산점도를 나눠 그려보자.
In [26]:
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()
- 각 클러스터의 산점도가 아주 잘 구분된다.
- 2개의 특성만을 사용했는데 로지스틱 회귀 모델의 교차 검증 점수가 99%에 달하는 이유를 알 것 같다.
- 사과와 파인애플 클러스터의 경계가 가깝게 붙어있다.
- 이 두 클러스터의 샘플은 몇 개가 혼동을 일으키기 쉬울 것 같다.
- 데이터를 시각화하면 예상치 못한 통찰을 얻을 수 있다.
- 그런 면에서 차원 축소는 매우 유용한 도구 중 하나이다.
In [27]:
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:90% !important; }</style>"))
'혼자 공부하는 머신러닝 + 딥러닝' 카테고리의 다른 글
[혼공머신] 13. K-Means Clustering (0) | 2021.12.18 |
---|---|
[혼공머신] 12. Clustering Algorithm (0) | 2021.12.18 |
[혼공머신] 11. Ensemble Learning (0) | 2021.12.18 |
[혼공머신] 10. Cross Validation & Grid Search (0) | 2021.12.18 |
[혼공머신] 9. Decision Tree (0) | 2021.12.18 |