본문 바로가기

프로그래밍/AI

[혼공머신 11기] 5주차 스터디 및 과제

반응형

5주차 후기

6장에서 k-평균 알고리즘을 공부하면서 제공된 정답 데이터가 없음에도 불구하고, 최적의 클러스터 개수를 찾아 학습하는 과정이 재미있고 흥미로웠다. (신기했다는 표현이 더 맞을 것 같다.)
그치만 주성분 분석의 내용은 이해했지만 세부 원리는 잘 이해가 되지 않았다.
과제를 제출하고 320~322 쪽을 다시 읽어보고, 관련 자료도 좀 더 찾아봐야겠다.

그리고 이번 장은 공부하면서 "이건 업무에 적용할 수 있지 않을까?"라는 생각을 했다. (물론 더 깊게 공부해봐야 겠지만...)
현업에서 활용할 만한 부분이 있을 것 같은 느낌이 드니 공부할 때도 좀 더 즐겁고 집중하며 볼 수 있었다.
어떻게 쓸 수 있을지 고민해봐야겠다.

이제 마지막 6주차가 남았다.
다음주부터 리프레시 휴가로 설날까지 쭉 쉬니 7~9장까지 딥러닝 내용을 쭉 완독해봐야 겠다.


5주차 과제

기본 미션

k-평균 알고리즘 작동 방식 설명하기

  1. 8개의 점이 존재한다고 가정한다.


  2. 클러스터의 개수인 k 값을 정한다.
  3. 데이터에서 무작위로 k 개의 클러스터 중심(centroid)을 설정한다.


  4. 각 클러스터 중심에서 가까운 데이터들을 클러스터로 묶는다.


  5. (3)번 과정에서 새로 형성된 클러스터의 중심(centroid)를 다시 계산한다.


  6. (3)번과 (4)번 과정을 반복한다.


  7. 다음과 같은 증상이 발생하면 반복을 멈춘다.
    • 새로 형성된 클러스터의 중심이 더 이상 변하지 않을 때
    • 데이터들이 동일한 클러스터에 계속 남을 때
    • 최대 반복 횟수에 다다를 때

참고자료 : The Ultimate Guide to K-Means Clustering

선택 미션

Ch.06(06-3) 확인 문제 풀고, 풀이 과정 정리하기


비지도 학습

  • 지도 학습 (Supervised Learning)
    • 훈련 데이터(Training Data)로부터 하나의 함수를 유추해내기 위한 기계 학습(Machine Learning)의 한 방법이다.
    • 훈련 데이터는 일반적으로 입력 객체에 대한 속성을 벡터 형태로 포함하고 있으며 각각의 벡터에 대해 원하는 결과가 무엇인지 표시되어 있다.
    • 이렇게 유추된 함수 중 연속적인 값을 출력하는 것을 회귀분석(Regression)이라 하고 주어진 입력 벡터가 어떤 종류의 값인지 표식하는 것을 분류(Classification)라 한다.
  • 비지도 학습 (Unsupervised Learning)
    • 타깃(정답)이 없을 때 사용하는 머신러닝 알고리즘
    • 사람이 가르쳐 주지 않아도 데이터에 있는 무언가를 학습한다.

군집 알고리즘

# 과일 사진 데이터 다운로드
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
--2024-02-02 13:49:15--  https://bit.ly/fruits_300_data
Resolving bit.ly (bit.ly)... 67.199.248.11, 67.199.248.10
Connecting to bit.ly (bit.ly)|67.199.248.11|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: https://github.com/rickiepark/hg-mldl/raw/master/fruits_300.npy [following]
--2024-02-02 13:49:15--  https://github.com/rickiepark/hg-mldl/raw/master/fruits_300.npy
Resolving github.com (github.com)... 140.82.113.4
Connecting to github.com (github.com)|140.82.113.4|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://raw.githubusercontent.com/rickiepark/hg-mldl/master/fruits_300.npy [following]
--2024-02-02 13:49:15--  https://raw.githubusercontent.com/rickiepark/hg-mldl/master/fruits_300.npy
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3000128 (2.9M) [application/octet-stream]
Saving to: ‘fruits_300.npy’

fruits_300.npy      100%[===================>]   2.86M  --.-KB/s    in 0.07s   

2024-02-02 13:49:16 (43.6 MB/s) - ‘fruits_300.npy’ saved [3000128/3000128]
import numpy as np
import matplotlib.pyplot as plt

# 다운로드 한 fruits_300.npy 파일 불러오기
fruits = np.load('fruits_300.npy')
print(f"""# 배열 크기 확인
배열 크기 : {fruits.shape}
  - 첫 번째 차원 : 샘플의 개수
  - 두 번째 차원 : 이미지 높이
  - 세 번째 차원 : 이미지 너비
""")
# 배열 크기 확인
배열 크기 : (300, 100, 100)
  - 첫 번째 차원 : 샘플의 개수
  - 두 번째 차원 : 이미지 높이
  - 세 번째 차원 : 이미지 너비
# 첫 번째 이미지 출력
print("# 첫 번째 이미지 출력")
plt.imshow(fruits[0], cmap='gray')
plt.show()
# 첫 번째 이미지 출력
# 첫 번째 이미지의 첫 번째 행 출력
print(f"""# 첫 번째 이미지의 첫 번째 행 출력
{fruits[0, 0, :]}
  - 값이 0에 가까우면 검게 나타나고, 높은 값은 밝게 표시된다.
  - 원래는 사과가 어두운 색이고, 바탕이 밝은 색이지만, 넘파이 배열로 변환할 때 반전되었다.
  - 알고리즘이 출력을 만들기 위해 덧셈, 곱셈을 하는데 픽셀값이 0이 되면 의미가 없다.
    그리고 픽셀값이 높을 수록 출력값이 커져서 의미를 부여하기 좋다.
    우리의 관심사는 "사과"이기 때문에 사과의 값을 큰 값(밝은색)으로 반전한다.
""")
# 첫 번째 이미지의 첫 번째 행 출력
[  1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1
   2   2   2   2   2   2   1   1   1   1   1   1   1   1   2   3   2   1
   2   1   1   1   1   2   1   3   2   1   3   1   4   1   2   5   5   5
  19 148 192 117  28   1   1   2   1   4   1   1   3   1   1   1   1   1
   2   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1
   1   1   1   1   1   1   1   1   1   1]
  - 값이 0에 가까우면 검게 나타나고, 높은 값은 밝게 표시된다.
  - 원래는 사과가 어두운 색이고, 바탕이 밝은 색이지만, 넘파이 배열로 변환할 때 반전되었다.
  - 알고리즘이 출력을 만들기 위해 덧셈, 곱셈을 하는데 픽셀값이 0이 되면 의미가 없다.
    그리고 픽셀값이 높을 수록 출력값이 커져서 의미를 부여하기 좋다.
    우리의 관심사는 "사과"이기 때문에 사과의 값을 큰 값(밝은색)으로 반전한다.
# 첫 번째 이미지 값을 재반전 하여 출력
print("# 첫 번째 이미지 재반전 출력")
plt.imshow(fruits[0], cmap='gray_r')
plt.show()
# 첫 번째 이미지 재반전 출력
# matplotlib의 subplots() 함수를 사용하여 여러 개의 그래프를 배열로 쌓기
fig, axs = plt.subplots(1, 2)
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
plt.show()
# 픽셀값 분석하기
#   - 사과, 파인애플, 바나나 100개씩
#   - 하나의 100x100 이미지를 펼쳐서 길이가 10,000인 1차원 배열로 만들기
apple = fruits[0:100].reshape(-1, 100*100)
pineapple = fruits[100:200].reshape(-1, 100*100)
banana = fruits[200:300].reshape(-1, 100*100)

print(f"""# 배열의 크기
apple: {apple.shape}
pineapple: {pineapple.shape}
banana: {banana.shape}
""")
# 배열의 크기
apple: (100, 10000)
pineapple: (100, 10000)
banana: (100, 10000)
# 픽셀 평균값 구하기
#   - axis = 0 : 행
#   - axis = 1 : 열
print(f"""# 픽셀 평균값
apple: {apple.mean(axis=1)}
pineapple: {pineapple.mean(axis=1)}
banana: {banana.mean(axis=1)}
""")
# 픽셀 평균값
apple: [ 88.3346  97.9249  87.3709  98.3703  92.8705  82.6439  94.4244  95.5999
  90.681   81.6226  87.0578  95.0745  93.8416  87.017   97.5078  87.2019
  88.9827 100.9158  92.7823 100.9184 104.9854  88.674   99.5643  97.2495
  94.1179  92.1935  95.1671  93.3322 102.8967  94.6695  90.5285  89.0744
  97.7641  97.2938 100.7564  90.5236 100.2542  85.8452  96.4615  97.1492
  90.711  102.3193  87.1629  89.8751  86.7327  86.3991  95.2865  89.1709
  96.8163  91.6604  96.1065  99.6829  94.9718  87.4812  89.2596  89.5268
  93.799   97.3983  87.151   97.825  103.22    94.4239  83.6657  83.5159
 102.8453  87.0379  91.2742 100.4848  93.8388  90.8568  97.4616  97.5022
  82.446   87.1789  96.9206  90.3135  90.565   97.6538  98.0919  93.6252
  87.3867  84.7073  89.1135  86.7646  88.7301  86.643   96.7323  97.2604
  81.9424  87.1687  97.2066  83.4712  95.9781  91.8096  98.4086 100.7823
 101.556  100.7027  91.6098  88.8976]
pineapple: [ 99.3239  87.1622  97.193   97.4689  98.8892  97.8819  95.1444  92.9461
  95.8412  96.9487  93.2863 101.2771  91.6511  98.3901  84.3277 100.7017
  99.0229  87.5298  99.4109  91.8568  90.3877  99.5066  95.8498  96.0728
 100.0062  95.5283  95.2715  98.1624  93.1395  99.1666  99.9519  96.732
  94.16   100.7644 101.1263  98.2852  98.5354 101.3809  95.4862  93.1256
  87.6404  93.7146  94.7279  85.5131  95.0937  95.3236  92.7696  94.4375
  99.36    95.8924  97.8221  99.4014  99.325   95.025   97.5771  97.3511
  99.4891  97.366  101.0062  91.2304  95.2824  95.3544 101.4657  97.5239
  98.8419  96.746   98.2922  96.3969  81.9464  93.4927  97.3872  82.4883
  95.3665  95.2541 101.3074  97.2391  95.4544 100.5576 100.2927  94.3088
  95.7401  97.0982  96.9559  92.9114  97.4164 100.769   97.808   99.2481
 101.0643  94.8173  99.2639  98.8539  97.2198  97.0201  94.5039  98.097
  95.0201  98.8078  99.4634 100.0257]
banana: [19.5487 24.4884 36.7517 18.568  53.572  31.5789 51.9062 28.4703 23.7856
 32.1295 29.1737 24.5578 27.7676 41.3082 61.0437 32.6444 38.4187 30.4175
 30.0639 21.4994 32.6018 39.6335 47.8397 57.7484 33.6511 23.5018 49.6817
 44.0855 46.1559 33.4963 36.0099 69.0751 33.9575 32.8786 60.1911 32.9949
 45.3359 56.1694 36.9218 25.6754 32.7901 29.0245 57.7134 44.6563 31.2138
 29.836  40.9228 23.7501 43.1543 32.3716 26.0986 55.6189 22.2269 29.5089
 24.0435 22.6247 24.1709 46.2714 32.0763 42.5076 44.5522 24.0199 27.68
 32.4377 19.2198 22.5083 19.6742 26.1852 25.8368 25.2676 26.4721 34.8503
 28.5235 80.8545 36.9371 47.3975 18.7909 33.0428 37.8222 34.7819 33.6051
 24.3963 32.7988 19.7609 32.538  28.5407 23.0999 26.3778 27.1965 38.7848
 18.7648 30.2297 54.3816 37.9073 32.3126 31.0664 33.1657 37.6051 38.1806
 26.2805]
# 픽셀 평균값에 대한 히스토그램
plt.hist(np.mean(apple, axis=1), alpha=0.8)
plt.hist(np.mean(pineapple, axis=1), alpha=0.8)
plt.hist(np.mean(banana, axis=1), alpha=0.8)

print(f""" 결과 해석
- 바나나 사진의 평균값은 40에 집중되어 있음
- 사과와 파인애플 사진의 평균값은 90~100 사이에 모여 있음
- 따라서 픽셀 평균값만으로도 바나나를 구분할 수 있고, 사과와 파인애플은 구분하기 어려움
""")
 결과 해석
- 바나나 사진의 평균값은 40에 집중되어 있음
- 사과와 파인애플 사진의 평균값은 90~100 사이에 모여 있음
- 따라서 픽셀 평균값만으로도 바나나를 구분할 수 있고, 사과와 파인애플은 구분하기 어려움

# 픽셀별 평균값 구하기
#   - 전체 샘플에 대한 각 픽셀의 평균 계산 > 과일마다 모양이 다르므로 픽셀값이 높은 위치가 다름
fig, axs = plt.subplots(1, 3, figsize=(20,5))
axs[0].bar(range(10000), np.mean(apple, axis=0))
axs[1].bar(range(10000), np.mean(pineapple, axis=0))
axs[2].bar(range(10000), np.mean(banana, axis=0))
plt.show()
# 과일 별 모든 사진을 합쳐서 출력하기
apple_mean = np.mean(apple, axis=0).reshape(100, 100)
pineapple_mean = np.mean(pineapple, axis=0).reshape(100, 100)
banana_mean = np.mean(banana, axis=0).reshape(100, 100)

fig, axs = plt.subplots(1, 3, figsize=(20, 5))
axs[0].imshow(apple_mean, cmap='gray_r')
axs[1].imshow(pineapple_mean, cmap='gray_r')
axs[2].imshow(banana_mean, cmap='gray_r')
plt.show()
###
# 평균값과 가까운 사진 고르기
###
print("# 사과 사진 100개 찾기")
# 사과 샘플에 대한 평균
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
print(abs_mean.shape)

# 사과 샘플 평균값과 오차가 가장 작은 샘플 100개 선정
apple_index = np.argsort(abs_mean)[:100]
fig, axs = plt.subplots(10, 10, figsize=(10,10))
for i in range(10):
  for j in range(10):
    axs[i, j].imshow(fruits[apple_index[i*10 + j]], cmap='gray_r')
    axs[i, j].axis('off')

plt.show()
# 사과 사진 100개 찾기
(300,)
  • 군집(Clustering)
    • 비슷한 샘플끼리 그룹으로 모으는 작업
    • 대표적인 비지도 학습 작업 중 하나
    • 군집 알고리즘에서 만든 그룹을 클러스터(Cluster)라고 한다.

k-평균(k-means) 군집 알고리즘

개요

  • 자동으로 평균값을 찾아주는 군집 알고리즘
  • 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centroid)라고 부른다.

작동 방식

  1. 무작위로 k개의 클러스터 중심을 정한다.
  2. 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.
  3. 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.
  4. 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.

KMeans 클래스

# 데이터 다운로드
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
--2024-02-02 13:50:57--  https://bit.ly/fruits_300_data
Resolving bit.ly (bit.ly)... 67.199.248.11, 67.199.248.10
Connecting to bit.ly (bit.ly)|67.199.248.11|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: https://github.com/rickiepark/hg-mldl/raw/master/fruits_300.npy [following]
--2024-02-02 13:50:57--  https://github.com/rickiepark/hg-mldl/raw/master/fruits_300.npy
Resolving github.com (github.com)... 140.82.113.4
Connecting to github.com (github.com)|140.82.113.4|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://raw.githubusercontent.com/rickiepark/hg-mldl/master/fruits_300.npy [following]
--2024-02-02 13:50:58--  https://raw.githubusercontent.com/rickiepark/hg-mldl/master/fruits_300.npy
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3000128 (2.9M) [application/octet-stream]
Saving to: ‘fruits_300.npy’

fruits_300.npy      100%[===================>]   2.86M  --.-KB/s    in 0.07s   

2024-02-02 13:50:58 (40.7 MB/s) - ‘fruits_300.npy’ saved [3000128/3000128]
# npy 파일을 읽어 넘파이 배열 생성
import numpy as np

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


# 사이킷런의 k-평균 알고리즘 : KMeans 클래스
from sklearn.cluster import KMeans

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

print(f"""# 군집 결과 - 각 샘플이 어떤 레이블에 해당하는지 나타낸다. (n_clusters=3이기 때문에 0, 1, 2 중 하나의 값으로 표시)
{km.labels_}
""")

print(f"""# 각 군집 별 개수
{np.unique(km.labels_, return_counts=True)}
""")
# 군집 결과 - 각 샘플이 어떤 레이블에 해당하는지 나타낸다. (n_clusters=3이기 때문에 0, 1, 2 중 하나의 값으로 표시)
[2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 0 2 0 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 0 0 2 2 2 2 2 2 2 2 0 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1]

# 각 군집 별 개수
(array([0, 1, 2], dtype=int32), array([111,  98,  91]))
import matplotlib.pyplot as plt

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

  # 한 줄에 10개씩 이미지 출력
  rows = int(np.ceil(n/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()
# 레이블이 0인 과일 사진 출력
draw_fruits(fruits[km.labels_==0])
# 레이블이 1인 과일 사진 출력
draw_fruits(fruits[km.labels_==1])
# 레이블이 2인 과일 사진 출력
draw_fruits(fruits[km.labels_==2])

클러스터 중심

  • KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있다.
  • KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해주는 transform() 메서드를 제공한다.
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio=3)
# 인덱스가 100인 샘플에 transform() 메서드 적용
print(f"""# 인덱스 100인 샘플의 각 레이블 별 클러스터 중심까지의 거리
{km.transform(fruits_2d[100:101])}

# 인덱스 100인 샘플의 가장 가까운 클러스터 중심 레이블
{km.predict(fruits_2d[100:101])}

# 인덱스 100인 샘플 이미지""")

draw_fruits(fruits[100:101])

print(f"\n# k-평균 알고리즘이 최적의 클러스터를 찾기 위해 반복한 횟수 : {km.n_iter_}")
# 인덱스 100인 샘플의 각 레이블 별 클러스터 중심까지의 거리
[[3393.8136117  8837.37750892 5267.70439881]]

# 인덱스 100인 샘플의 가장 가까운 클러스터 중심 레이블
[0]

# 인덱스 100인 샘플 이미지
# k-평균 알고리즘이 최적의 클러스터를 찾기 위해 반복한 횟수 : 4

최적의 k 찾기

  • k-평균 알고리즘의 단점
    • 클러스터 개수를 사전에 지정해야 함

      하지만 실전에서는 몇 개의 클러스터가 있는지 알 수 없음
  • 군집 알고리즘에서 적절한 k 값을 찾기 위한 완벽한 방법은 없음
  • 이너셔(inertia)
    • 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값
    • 클러스터 중심과 클러스터에 속한 샘플 사이의 거리의 제곱 합
  • 엘보우(elbow) 방법
    • 클러스터 개수를 늘려가면서 이너셔(inertia)의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법
    • 클러스터 개수를 증가시키면서 이너셔를 그래프로 그리면 감소하는 속도가 꺽이는 지점이 있는데, 이 지점의 클러스터 개수를 최적의 k 값으로 선정
inertia = []
for k in range(2, 7):
  km = KMeans(n_clusters=k, random_state=42)
  km.fit(fruits_2d)
  inertia.append(km.inertia_)

plt.plot(range(2,7), inertia)
plt.xlabel('k')
plt.ylabel('inertia')
plt.show()

print("# 최적의 클러스터 개수 : 3 (그래프의 기울기가 가장 바뀐 지점)")
# 최적의 클러스터 개수 : 3 (그래프의 기울기가 가장 바뀐 지점)

주성분 분석

차원

  • 데이터가 가진 속성 = 특성 = 차원(dimension)
    • 1차원 배열 : 1차원 배열은 벡터라고 하고, 원소의 개수를 차원이라고 한다.
    • 다차원 배열 : 배열의 축 개수를 차원이라고 한다. 2차원 배열은 행과 열이 차원이다.
  • 차원을 줄이면 저장 공간을 절약할 수 있다. → 차원 축소

차원 축소 (dimensionality reduction)

  • 특성이 많으면 선형 모델의 성능이 높아지고, 훈련 데이터에 쉽게 과대적합 된다.
  • 차원 축소
    • 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄인다.
    • 지도 학습 모델의 성능을 향상시킬 수 있는 방법
  • 대표적인 차원 축소 알고리즘 : 주성분 분석(PCA, Principal Component Analysis)

주성분 분석 (PCA, Principal Component Analysis)

  • 데이터에 있는 분산이 큰 방향을 찾는 것
  • 분산이 큰 방향 = 데이터를 잘 표현하는 어떤 벡터 = 주성분(principal component)
  • 주성분을 사용하여 원본 데이터의 차원을 줄일 수 있다.
  • 주성분은 원본 차원과 같고, 주성분으로 바꾼 데이터는 차원이 줄어든다.
  • 일반적으로 원본 특성의 개수만큼 주성분을 찾을 수 있다.

PCA 클래스

  • 사이킷런의 PCA 클래스를 사용해 주성분 분석을 할 수 있다.
  • 비지도 학습이므로 fit() 메서드에 타깃값을 제공하지 않는다.
# 과일 사진 데이터 다운로드
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
--2024-02-02 13:51:33--  https://bit.ly/fruits_300_data
Resolving bit.ly (bit.ly)... 67.199.248.11, 67.199.248.10
Connecting to bit.ly (bit.ly)|67.199.248.11|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: https://github.com/rickiepark/hg-mldl/raw/master/fruits_300.npy [following]
--2024-02-02 13:51:33--  https://github.com/rickiepark/hg-mldl/raw/master/fruits_300.npy
Resolving github.com (github.com)... 140.82.112.4
Connecting to github.com (github.com)|140.82.112.4|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://raw.githubusercontent.com/rickiepark/hg-mldl/master/fruits_300.npy [following]
--2024-02-02 13:51:34--  https://raw.githubusercontent.com/rickiepark/hg-mldl/master/fruits_300.npy
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3000128 (2.9M) [application/octet-stream]
Saving to: ‘fruits_300.npy’

fruits_300.npy      100%[===================>]   2.86M  --.-KB/s    in 0.07s   

2024-02-02 13:51:34 (42.0 MB/s) - ‘fruits_300.npy’ saved [3000128/3000128]
# 과일 사진을 넘파이 배열로 적재
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

# 사이킷런의 주성분 분석
from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)

print(f"""# PCA 클래스가 찾은 주성분 배열의 크기
{pca.components_.shape}
""")

print("# 주성분 그림 출력")
draw_fruits(pca.components_.reshape(-1, 100, 100))
print()
# PCA 클래스가 찾은 주성분 배열의 크기
(50, 10000)

# 주성분 그림 출력
# 원본 데이터를 주성분에 투영하여 차원을 축소 (10,000개 -> 50개)
print("# 차원 축소")
print(f"before : {fruits_2d.shape}")
fruits_pca = pca.transform(fruits_2d)
print(f"after : {fruits_pca.shape}")
output
# 차원 축소
before : (300, 10000)
after : (300, 50)

원본 데이터 재구성

  • PCA 클래스의 inverse_transform() 메서드 사용
fruits_inverse = pca.inverse_transform(fruits_pca)
print(f"""# 원본 데이터 재구성
before : {fruits_pca.shape}
after : {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")
# 원본 데이터 재구성
before : (300, 50)
after : (300, 10000)

설명된 분산(explained variance)

  • 설명된 분산 : 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값
  • explained_variance_ratio_ 값으로 주성분의 설명된 분산 비율을 볼 수 있다.
    • 첫 번째 주성분의 설명된 분산이 가장 크다.
    • 분산 비율을 모두 더하면 총 분산 비율을 얻을 수 있다.
print(f"# 총 분산 비율 : {np.sum(pca.explained_variance_ratio_) * 100}%\n")

print("# 설명된 분산을 그래프로 출력")
plt.plot(pca.explained_variance_ratio_)
plt.show()
print("=> 처음 10개의 주성분이 대부분의 분산을 표현한다.")
# 총 분산 비율 : 92.15811232595493%

# 설명된 분산을 그래프로 출력
=> 처음 10개의 주성분이 대부분의 분산을 표현한다.

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

과일 사진 원본 데이터PCA로 축소한 데이터지도 학습에 적용해 보고, 어떤 차이가 있는지 알아보기 → 로지스틱 회귀 모델 사용

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression(max_iter=1000)

# 타깃값 생성 (0: 사과, 1: 파인애플, 2: 바나나)
target = np.array([0]*100 + [1]*100 + [2]*100)

print(f"""# 타깃값 생성
{target}
""")
# 타깃값 생성
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2]
from sklearn.model_selection import cross_validate

# 원본 데이터
scores = cross_validate(lr, fruits_2d, target)
print(f"""# 원본 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : {np.mean(scores['test_score'])}
fit_time : {np.mean(scores['fit_time'])}
""")

# PCA로 축소한 데이터
scores = cross_validate(lr, fruits_pca, target)
print(f"""# PCA로 축소한 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : {np.mean(scores['test_score'])}
fit_time : {np.mean(scores['fit_time'])}
""")

print("=> PCA로 특성을 50개로 축소한 데이터의 정확도가 더 높거나 같고, 훈련 시간도 훨씬 빠르다.")
# 원본 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : 0.9966666666666667
fit_time : 1.981255006790161

# PCA로 축소한 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : 0.9966666666666667
fit_time : 0.04311800003051758

=> PCA로 특성을 50개로 축소한 데이터의 정확도가 더 높고, 훈련 시간도 훨씬 빠르다.
# 직접 주성분 개수를 지정하는 대신 <설명된 분산의 비율>을 지정하면,
# PCA 클래스는 지정된 비율에 도달할 때까지 자동으로 주성분을 찾는다.
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)
print(f"# 자동으로 찾은 주성분 개수 : {pca.n_components_}\n")

# 원본 데이터 변환
fruits_pca = pca.transform(fruits_2d)
print(f"# 변환된 데이터 크기 : {fruits_pca.shape}")

# 교차 검증
scores = cross_validate(lr, fruits_pca, target)
print(f"교차 검증 점수 : {np.mean(scores['test_score'])}")
print(f"교차 검증 수행 시간 : {np.mean(scores['fit_time'])}")
# 자동으로 찾은 주성분 개수 : 2

# 변환된 데이터 크기 : (300, 2)
교차 검증 점수 : 0.9966666666666667
교차 검증 수행 시간 : 0.04672560691833496
# 차원 축소된 데이터를 사용해 k-평균 알고리즘으로 클러스터 찾기
from sklearn.cluster import KMeans

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

print(np.unique(km.labels_, return_counts=True))
(array([0, 1, 2], dtype=int32), array([110,  99,  91]))
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(['apple', 'banana', 'pineapple'])
plt.show()
반응형