14. 주성분 분석

1. 차원과 차원축소

  • 데이터가 가진 속성을 특성이라고 한다. 이전에 다룬 과일 사진의 경우 10000개의 픽셀이 있기 떄문에 10000개의 특성을 가지고 있다고 볼 수 있다. 머신러닝에서는 이런 특성을 차원이라고도 부른다. 즉 10000개의 특성은 10000개의 차원이라고도 볼 수 있다.
  • 이런 차원을 줄일 수 있다면 용량을 아낄 수 있다. 이를 위해 비지도 학습 작업 중 하나인 차원축소알고리즘을 이용할 수 있다. 이전에 특성이 많으면 선형 모델의 성능이 높아지고 훈련 데이터에 쉽게 과대적합된다는 것을 배웠다. 차원 축소는 데이터를 가장 잘 나타내는 일부 특성을 선택해 크기를 줄임과 동시에 학습 모델의 성능을 향상시키는 방법이다.
  • 또한 줄어든 차원에서 다시 원본 차원으로 손실을 최대한 줄이면서 복원도 가능하다. 이번시간에는 대표적인 차원 축소 알고리즘인 주성분 분석을 이용한다. 주성분 분석은 PCA라고도 한다.

2. 주성분 분석

  • 주성분 분석은 데이터에 있는 분산이 큰 방향을 찾는 것으로 이해할 수 있다.
  • 고차원 공간의 표본들을 선형 연관성이 없는 저차원 공간(주성분)의 표본으로 변환하기 위해 직교 변환을 사용한다. 데이터를 한개의 축으로 사상시켰을 때 그 분산이 가장 커지는 축을 첫 번째 주성분, 두 번째로 커지는 축을 두 번째 주성분으로 놓이도록 새로운 좌표계로 데이터를 선형 변환한다.
  • 주성분 분석을 사이킷런을 이용해 수행해보도록 하겠다. 데이터는 이전에 이용했던 데이터를 사용한다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate
from sklearn.cluster import KMeans
fruits = np.load('/Users/janghyeseong/Downloads/fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
pca = PCA(n_components=50)
pca.fit(fruits_2d)
PCA(n_components=50)
  • PCA 클래스의 객채를 만들 때 n_components 매개변수에 주성분 개수를 지정한다.
  • PCA 클래스가 찾은 주성분은 components_속성에 저장되어 있다.
print(pca.components_.shape)
(50, 10000)
  • n_components를 50으로 지정해서 pca.components_배열의 첫 번째 차원이 50으로 나타난다. 즉 50개의 주성분을 찾은 것이다. 두 번째 차원은 항상 원본 데이터의 특성 개수와 같은 10000이다.
  • 원본 데이터와 차원이 같기 때문에 주성분은 100x100 크기의 이미지처럼 출력해볼 수 있다.
  • 저번 시간에 만든 draw_fruits()함수를 사용해서 구현해보면 아래와 같다.
def draw_fruits(arr, ratio=1):
    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))

output_10_0

  • 이 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다.
  • 주성분을 찾았기 떄문에 주성분에 투영해 특성의 개수를 10000개에서 50개로 줄일 수 있다.
  • PCA의 transform()매서드를 이용해 원본 데이터의 차원을 50으로 줄여보자
print(fruits_2d.shape)
(300, 10000)
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
(300, 50)
  • fruits_2d는 10000개의 픽셀값을 가지는 300개의 이미지였다. 주성분 분석을 이용해 차원을 줄여 (300, 50)크기의 배열로 변환했다.
  • 데이터의 차원을 줄였으니 용량도 줄어들었다. 그렇다면 차원을 줄이는것과 반대로 원상복구도 가능할까? 이를 직접 해보도록 하자.

3. 원본 데이터의 재구성

  • 이전까지 10000개의 차원을 50개로 축소하는 과정을 진행했다. 이로인한 손실은 발생할 수밖에 없다. 하지만 최대한 분산이 큰 방향으로 데이터를 투영했기 때문에 원본 데이터 상당부분을 재구성할 수 있다.
  • 이를 위해 PCA는 inverse_transform() 메서드를 제공한다. 앞서 50개의 차원으로 축소한 fruits_pca 데이터를 전달해 10000개의 특성을 복구해보자
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
(300, 10000)
  • 10000개의 특성이 복구되었다. 데이터를 100x100 크기로 바꾸어 100개씩 나누어 출력해보자. 이 데이터는 순서대로 사과, 파인애플, 바나나를 100개씩 담고 있다.
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
    draw_fruits(fruits_reconstruct[start:start+100])
    print('\n')

output_19_0


output_19_2


output_19_4


  • 모든 데이터가 처음과 같이 복구된 것을 확인할 수 있다. 일부는 흐리고 번진 부분이 존재하지만 50개로 축소한것을 다시 10000개로 늘린것을 감안하면 놀라운 일이다.
  • 만약 주성분을 최대로 사용했다면 보다 완벽하게 재구성이 가능했을 것이다. 그렇다면 이전에 50개의 주성분은 얼마나 분산을 보존하고 있던 것인지 알아보자.

4. 설명된 분산

  • 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값을 설명된 분산이라고 한다.
  • PCA 클래스의 explained_variance_ratio_에 각 주성분의 설명된 분산 비율이 기록되어 있다. 이 분산비율을 모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있다.
print(np.sum(pca.explained_variance_ratio_))
0.9215175552398827
  • 92%가 넘는 분산을 유지하고 있다. 앞에서 50개의 특성에서 원본 데이터를 복원했을 때 원본 이미지의 품질이 높았던 이유가 여기 있었다. 설명된 분산의 비율을 그래프로 그려보면 적절한 주성분의 개수를 찾는데 도움이 된다. 맷플롯립의 plot()함수로 설명된 분산을 그래프로 출력해보자.
plt.plot(pca.explained_variance_ratio_)
plt.show()

output_25_0

  • 그래프를 보면 처음 10개의 주성분이 대부분의 분산을 표현하고 있다. 그 다음부터의 각 주성분이 설명하는 분산은 비교적 작다. 이번에는 PCA로 차원 축소된 데이터를 사용해 지도학습 모델을 훈련해보도록 하자. 그리고 원본데이터를 사용했을 때와 비교를 해보겠다.

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

lr = LogisticRegression()
  • 과일 사진 원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해보고 어떤 차이가 있는지 알아보자. 3개의 과일 사진을 분류해야 하므로 간단히 로지스틱 회귀 모델을 사용하자.
  • 지도 학습 모델로 사용하기 위해서는 타깃값이 필요하다. 여기서는 사과를 0, 파인애플을 1, 바나나를 2로 지정한다. 파이썬 리스트와 정수를 곱하면 리스트 안의 원소를 정수만큼 반복한다. 이를 이용하면 타깃 데이터를 만들 수 있다.
target = np.array([0]*100 + [1]*100 + [2]*100)
  • 먼저 원본 데이터를 사용해보자. 로지스틱 회귀 모델에서 성능을 가늠해 보기 위해 cross_validate()로 교차검증을 수행한다.
scores = cross_validate(lr, fruits_2d, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
0.9966666666666667
0.6225533485412598
  • 교차 검증의 점수는 0.997정도로 매우 높게 나타난다 특성이 10000개나 되기 때문에 300개의 샘플에서는 금방 과대적합된 모델이 나오기 쉽다. cross_validate() 함수가 반환하는 딕셔너리에는 fit_time 항목에 각 교차 검증 폴드의 훈련시간이 기록되어있다. 0.94초 걸렸닥 나와있다. 이 값을 PCA로 축소한 fruits_pca를 사용했을때와 비교해보도록 하겠다.
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
1.0
0.03422822952270508
  • 50개의 특성만 사용했는데도 정확도가 100%이며 훈련시간은 0.03초로 매우 빨라진것을 알 수 있다. PCA로 훈련 데이터의 차원을 축소하면 저장 공간 뿐 아니라 머신러닝 모델의 훈련속도도 높일 수 있다.
  • 앞에서 PCA 클래스를 사용할 때 n_components 매개변수에 주성분의 개수를 지정했다. 대신 원하는 설명된 분산의 비율을 입력할 수도 있다. PCA 클래스는 지정된 비율에 도달할 때까지 자동으로 주성분을 찾는다. 설명된 분산의 50%에 달하는 주성분을 찾도록 PCA모델을 만들어보자.
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)
PCA(n_components=0.5)
print(pca.n_components_)
2
  • 2개의 특성만으로도 원본 데이터에 있는 분산의 50%를 표현할 수 있다.
  • 이 모델로 원본 데이터를 변환해보자. 주성분이 2개 이므로 변환된 데이터의 크기는 (300, 2)가 될 것이다.
  • 2개를 사용해서도 교차검증의 결과가 좋을지도 살펴보겠다.
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
(300, 2)
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
/Users/janghyeseong/Desktop/PR/PR2/lib/python3.9/site-packages/sklearn/linear_model/_logistic.py:763: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
/Users/janghyeseong/Desktop/PR/PR2/lib/python3.9/site-packages/sklearn/linear_model/_logistic.py:763: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
/Users/janghyeseong/Desktop/PR/PR2/lib/python3.9/site-packages/sklearn/linear_model/_logistic.py:763: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(


0.9933333333333334
0.05604367256164551


/Users/janghyeseong/Desktop/PR/PR2/lib/python3.9/site-packages/sklearn/linear_model/_logistic.py:763: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
  • 결과를 보게되면 단 2개의 특성으로도 99%의 정확도를 보여준다.
  • 이번에는 축소된 데이터를 이용해 k-평균 알고리즘으로 클러스터를 찾아보자.
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]))
  • 결과는 이전과 거의 비슷하게 나왔다.
  • 훈련 데이터의 차원을 줄이면 시각화에 용이하다는 장점을 얻을 수 있다. 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()

output_44_0

  • 각 클러스터의 산점도가 아주 잘 구분된다. 2개의 특성만으로도 로지스틱회귀모형의 교차검증 점수가 99%에 달하는 결과라고 할 수 있다. 위의 그림을 보면 파인애플과 사과의 산점도가 많이 붙어있는 것을 확인할 수 있다. 그래서 둘이 혼동하는 결과가 생긴것이 아닌가 추측할 수 있다.
  • 데이터를 시각화하면 예상치 못한 통찰을 얻을 수 있다. 그런 면에서 차원 축소는 매우 유용한 도구 중 하나이다.

Categories:

Updated:

Leave a comment