Date:     Updated:

카테고리:

태그: , , ,


💡 ‘혼자 공부하는 머신러닝 딥러닝(박해선 저)’ 책을 읽고 공부한 내용을 요약한 페이지입니다.
책에 나오는 코드를 그대로 쓰지 않고, 코드와 파라미터를 변형하고 조정해가며 다른 결과를 출력하며 공부했습니다.


06 비지도 학습


06-1 군집 알고리즘

타깃을 모르는 사진을 종류별로 분류하려할 때, 이것은 비지도 학습(unsupervised learning) 이다.

!wget https://bit.ly/fruits_300_data -O fruits_300.npy
#!으로 시작하면 파이썬 코드가 아니라 리눅스 셀 명령으로 이해
import numpy as np
import matplotlib.pyplot as plt
fruits = np.load('fruits_300.npy') #load(): 넘파이에서 npy를 로드하기 위한 메서드
print(fruits.shape) #(300, 100, 100)
#첫번째 차원(300)은 샘플의 개수를, 두번째 차원(100)은 이미지 높이를, 세번째 차원(100)은 이미지 너비를 의미한다.

plt.imshow(fruits[0], cmap='gray_r') #0에 가까울수록 밝게 나타남
plt.show()

fig, axs = plt.subplots(1,2)
#subplots()함수의 두 매개변수는 그래프를 쌓을 행과 열을 지정.
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
plt.show()

넘파이 배열로 나눌 때 100*100이미지를 펼쳐서 길이가 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(apple.mean(axis=1)) #axis를 0으로 하면 행을 따라 계산하고 1로 하면 열을 따라 계산함.

이제 사과, 파인애플, 바나나 각 샘플의 픽셀 평균값을 계산해보고, 이를 히스토그램으로 나타내보자.

plt.hist(np.mean(apple, axis=1), alpha=0.8) #alpha매개변수를 1보다 작게 하면 투명도를 준다.
plt.hist(np.mean(pineapple, axis=1), alpha=0.8)
plt.hist(np.mean(banana, axis=1), alpha=0.8)
plt.legend(['apple', 'pineapple', 'banana']) #legend()함수로 범례 지정
plt.show()

바나나는 픽셀 평균값이 대체로 40이하이지만, 사과와 파인애플은 평균값이 서로 겹친다. 고로 픽셀값만으로 이 둘을 비교하리란 어렵다.

샘플의 픽셀 평균값이 아니라 픽셀별 평균값을 비교해보는 것이 좋다.

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()

위 사진은 픽셀 평균값을 100*100으로 재반환하여 이미지로 출력한 것이다. 이 대표 이미지와 가까운 사진을 골라낸다면 사과, 파인애플, 바나나를 구분할 수 있을 것이다.

대표이미지와 샘플 각각의 오차를 계산한다. 그리고 이 오차가 가장 적은 것부터 차례대로 인덱스를 매긴다. 그 인덱스를 원본 샘플 데이터에 적용해서 오차가 작은 것이 정말 그 과일이 맞는지 확인해보자.

abs_diff = np.abs(fruits - banana_mean) #바나나 대표이미지와 각 샘플간의 차이를 3차원 배열로
abs_mean = np.mean(abs_diff, axis=(1,2)) # 각 샘플에 대한 평균을 구하여 1차원으로 만듦

banana_index = np.argsort(abs_mean)[:100] 
#np.argsort()함수는 작은 것에서 큰 순서대로 나열한 abs_mean의 인덱스를 반환
fig, axs = plt.subplots(10, 10, figsize=(10,10))
for i in range(10): #10*10 형태로 만들려는 이중 반복문
  for j in range(10):
    axs[i,j].imshow(fruits[banana_index[i*10+j]], cmap='gray_r')
    axs[i,j].axis('off')
plt.show()

바나나 대표 이미지와 오차가 작은 100개를 골랐더니 2개를 제외하곤 모두 바나나이다. 꽤 정확하다! 이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을 군집(clustering)이라고 하고, 군집 알고리즘에서 만든 그룹을 클러스터라고 부른다.

06-1 핵심 키워드

  • 비지도 학습: 머신러닝의 한 종류로 훈련 데이터에 타깃이 없다.
  • 군집: 비슷한 샘플끼리 하나의 그룹으로 모으는 대표적인 비지도 학습 작업


06-2 k평균

앞선 절에선 사과, 파인애플, 바나나 사진임을 미리 알고 있었기에 각각의 대표이미지를 뽑아놓고 가장 오차가 적은 과일을 찾을 수 있었다. 하지만 진짜 비지도학습에서는 사진에 어떤 과일이 들어있는지 알지 못한다.

이런 경우 k-평균(k-mean)군집 알고리즘이 평균값을 자동으로 찾아준다. 이 평균값이 클러스터의 중심에 위치하므로 클러스터 중심(cluster center) 또는 센트로이드(centroid) 라고 불린다.

k-평균 알고리즘의 작동방식은 다음과 같다.

  1. 무작위로 k개의 클러스터 중심을 정한다.(위 그림에서 c1, c2, c3)
  2. 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.
  3. 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.
  4. 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.
!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) #2차원 배열로 전환
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
#n_clusters매개변수는 클러스터 개수를 지정한다.
km.fit(fruits_2d)
#비지도 학습이므로 fit() 메서드에 타깃 데이터를 넣지 않는다.
print(km.labels_) #각 샘플이 어떤 레이블에 해당되는지 나타내는 속성
print(np.unique(km.labels_, return_counts=True)) #3개의 레이블이 각각 몇개인지 확인
#결과값
(array([0, 1, 2], dtype=int32), array([ 91,  98, 111]))
#각각 91개, 98개, 111개이다.

이제 3개의 클러스터가 어떤 이미지를 모았는지 그림으로 출력해보자.

import matplotlib.pyplot as plt
def draw_fruits(arr, ratio=1):
  n = len(arr) #n은 샘플 개수
  rows = int(np.ceil(n/10)) #한줄에 10개씩 출력할거라 샘플을 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(fruits[km.labels_==0])

draw_fruits(fruits[km.labels_==1])

draw_fruits(fruits[km.labels_==2])

레이블2 클러스터는 파인애플에 사과 9개와 바나나 2개가 섞여있다. k-평균 알고리즘이 이 샘플들을 완벽하게 구별해내지는 못했다.

KMeans클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers 속성에 저장되어 있다.

draw_fruits(km.cluster_centers_.reshape(-1,100,100), ratio=3)
#이 배열은 fruits_2d 샘플의 클러스터 중심이기에 이미지로 출력하려면 100*100 크기로 바꿔야 함

앞선 절에서 사과, 바나나, 파인애플의 픽셀 평균값을 출력했던 것과 매우 비슷하다.

우리는 타깃값을 사용하진 않았지만 n_clusters를 3으로 지정하는 편법을 사용했다. 마치 과일이 3종류인 것을 미리 알고있던 양 말이다. 실전에서는 클러스터 개수를 전혀 알 수 없다. 이럴 때 최적의 클러스터 개수는 어떻게 찾을 수 있을까?

적절한 클러스터 개수를 찾는 가장 대표적 방법은 엘보우(elbow) 이다. k-평균 알고리즘은 클러스터 중심과 샘플 사이의 거리를 잴 수 있다. 이 거리의 제곱 합을 이너셔(inertia) 라고 부르는데, 이는 클러스터에 속한 샘플이 얼마나 가까이 모여있는지를 나타내는 값이다.

일반적으로 클러스터 개수가 늘어나면 클러스터 개개의 크기가 줄어듦에 따라 이너셔도 줄어든다. 엘보우 방법은 클러스터 개수를 늘려가며 이너셔의 변화를 관찰하여 최적의 개수를 찾는 방법이다.

inertia=[]
for k in range(2,7): #클러스터 개수를 2~6까지 바꿔가며 반복
  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()

위 그래프에서 꺾이는 지점(k=3부근)이 바로 이너셔의 변화가 꺾이는 타이밍, 즉 군집 효과가 줄어드는 때이다.

06-2 핵심 키워드

  • k-평균 알고리즘: 처음에 랜덤하게 클러스터 중심을 정하고 중심을 이동하고 다시 클러스터를 재편성하는 과정을 반복하여 최적의 클러스터를 구성하는 알고리즘
  • 클러스터 중심: k-평균 알고리즘이 만든 클러스터에 속한 샘플의 특성 평균값.
  • 엘보우 방법: 최적의 클러스터 개수를 정하는 방법. 클러스터 개수에 따라 이너셔 감소가 꺾이는 지점이 적절한 클러스터 개수가 된다.

06-2 핵심 패키지와 함수

  • scikit-learn
    • KMeans: k-평균 알고리즘 클래스
      • n_clusters매개변수는 클러스터 개수를 지정. 기본값은 8.


06-3 주성분 분석

차원이 여러 개일 때 이를 줄일 수 있으면 저장 공간을 크게 절약할 수 있을 것이다. 이를 위해 차원 축소(dimensionality reduction) 알고리즘을 다뤄보자. 차원 축소는 데이터를 잘 나타내는 일부 특성을 선택한다.

또한 줄어든 차원에서 손실을 줄이며 다시 원본 차원으로 복원할 수도 있다. 그러한 대표적인 알고리즘이 바로 주성분 분석(PCA, principal component analysis)이다.

데이터에 있는 분산이 큰 방향이 바로 주성분 벡터이다. 이것이 첫번째 주성분이고, 이 벡터에 수직이고 분산이 가장 큰 다음 방향이 두번째 주성분이 된다.

일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있다. 이제 사이킷런으로 과일 사진 데이터에서 주성분 분석을 해보자.

#데이터 준비
!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)
from sklearn.decomposition import PCA
pca =  PCA(n_components=50)
#n_components매개변수에 주성분의 개수 지정
pca.fit(fruits_2d)
#k-평균과 마찬가지로 비지도학습이므로 fit()메서드에 타깃데이터 넣지 않음
#주성분을 100*100 크기의 이미지처럼 출력
import matplotlib.pyplot as plt
def draw_fruits(arr, ratio=1):
  n = len(arr)
  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()

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

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape) #(300, 50)
#원본데이터가 (300,10000)배열이었는데 (300,50)으로 줄였다!

이제 원본 데이터로 원상 복구해보자.

fruits_inverse = pca.inverse_transform(fruits_pca)
#inverse_transform()메서드로 원상 복구
print(fruits_inverse.shape) #(300, 10000)으로 복구됨
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_))
#0.9215634154222491
plt.plot(pca.explained_variance_ratio_)
plt.show()

처음 5~6개의 주성분이 대부분의 분산을 설명하고 있다.

과일 사진 원본데이터와 PCA로 축소한 데이터를 로지스틱 회귀 모델에 사용해보고 어떤 차이가 있는지 알아보자.

from sklearn.linear_model import LogisticRegression
lr=LogisticRegression()
target = np.array([0]*100+[1]*100+[2]*100)
#사과를 0, 파인애플을 1, 바나나를 2로.

#교차검증 수행
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
1.3571013450622558

#PCA로 차원축소한 데이터
scores=cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
#결과값
1.0
0.03344817161560058

결과값을 보면 PCA로 축소한 데이터의 검증 점수가 1.0으로 압도적이고, 훈련시간 역시 0.033초로 40배 가량 줄어들었다.

06-3 핵심 키워드

  • 차원 축소: 원본 데이터의 특성을 적은 수의 새로운 특성으로 변환하는 비지도학습의 한 종류
  • 주성분 분석: 데이터에서 가장 분산이 큰 방향을 찾는 차원 축소 알고리즘
  • 설명된 분산: 주성분이 얼마나 원본 데이터의 분산을 잘 나타내는지 기록한 값

06-3 핵심 패키지와 함수

  • scikit-learn
    • PCA: 주성분 분석을 수행하는 클래스
      • n_componenets는 주성분의 개수를 지정
      • componenets_속성에는 훈련 세트에서 찾은 주성분이 저장됨
      • explained_variance_속성에는 설명된 분산이 저장됨
      • inverse_transform()메서드는 차원을 축소시킨 데이터를 다시 원본 차원으로 복원