Machine Learning/Basic

[6-2] k-평균

jwjwvison 2021. 4. 13. 10:00

이 포스팅은 혼자 공부하는 머신러닝 + 딥러닝을 공부하고 정리한것 입니다.


 이전 포스팅에서는 각 과일들에 있는 각 픽셀의 평균값을 구해서 가장 가까운 사진을 골랐다. 이 경우에는 사과, 파인애플, 바나나 사진임을 미리 알고 있었기 때문에 각 과일의 평균을 구할 수 있었다. 하지만 진짜 비지도학습에서는 사진에 어떤 과일이 들어 있는지 알지 못한다.

 

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

 

k-평균 알고리즘 소개

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

   1. 무작위로 k개의 클러스터 중심을 정한다.

   2. 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.

   3. 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.

   4. 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.

 

 

 

 k-평균 알고리즘은 처음에는 랜덤하게 클러스터 중심을 선택하고 점차 가장 가까운 샘플의 중심으로 이동하는 비교적 간단한 알고리즘이다.

 

 

KMeans 클래스

 k- 평균모델을 훈련하기 위해 (샘플 개수, 너비, 높이) 크기의 3차원 배열을 (샘플 개수, 너비 x 높이) 크기를 가진 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)

 

 비지도 학습이므로 fit() 메서드에서 타깃 데이터를 사용하지 않는다.

 

from sklearn.cluster import KMeans

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

 

군집된 결과는 KMeans 클래스 객체의 labels_ 속성에 저장된다. labels_ 길이는 샘플 개수로 각 샘플이 어떤 레이블에 해당되는지 나타낸다. n_clusters=3 으로 지정했기 때문에 labels_ 배열의 값은 0,1,2 중 하나이다.

print(km.labels_)

 

 

print(np.unique(km.labels_,return_counts=True))

 

 

 

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    # 행이 1개면 열의 개수는 샘플 개수. 그렇지 않으면 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() 함수는 3차원 배열을 입력받아 가로로 10개씩 이미지를 출력한다.

 

이 함수를 사용해 레이블이 0인 과일 사진을 모두 그려보겠다. km.labels_==0과 같이 쓰면 km.labels_ 배열에서 값이 0인 위치는 True, 그 외는 모두 False가 된다. 

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

 

 

레이블 0으로 클러스터링된 91개의 이미지를 모두 출력했다. 이 클러스터는 모두 사과가 올바르게 모였다.

 

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

 

 

 

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

 

 

 레이블이 1인 클러스터는 바나나로만 이루어져 있다. 하지만 레이블이 2인 클러스터는 파인애플에 사과 9개 바나나 2개가 섞여있다. k-평균 알고리즘이 이 샘플들을 완벽하게 구별해내지는 못했다. 하지만 훈련 데이터에 타깃 레이블을 전혀 제공하지 않았음에도 스스로 비슷한 샘플들을 잘 모았다.

 

 

클러스터 중심

draw_fruits(km.cluster_centers_.reshape(-1,100,100),ratio=3)

 KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있다. 이 배열은 fruits_2d 샘플의 클러스터 중심이기 때문에 이미지로 출력하려면 100x100 크기의 2차원 배열로 바꿔야 한다.

 

이전 포스팅에서 사과, 바나나, 파인애플의 픽셀 평균값을 출력했던 것과 매우 비슷하다.

 

 

 KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 변환해 주는 transform() 메서드를 가지고 있다. fit() 메서드와 마찬가지로 2차원 배열을 기대한다. fruits_2d[100] 처럼 쓰면 (10000,) 크기의 배열이 되므로 에러가 발생한다.

print(fruits_2d[1:2].shape)
print(fruits_2d[1].shape)

 

 

print(km.transform(fruits_2d[100:101]))

 

 

 

 하나의 샘플을 전달했기 때문에 반환된 배열은 크기가 (1,클러스터 개수)인 2차원 배열이다.  첫번째 클러스터(레이블 0), 두번째 클러스터(레이블 1)가 각각 첫 번째 원소, 두 번째 원소의 값이다. 세 번째 클러스터까지의 거리가 3393.8로 가장 작다. 이 샘플은 레이블 2에 속할것이다. KMeans 클래스는 가장 가까운 클러스터 중심을 예측 클래스로 출력하는 predict() 메서드를 제공한다.

print(km.predict(fruits_2d[100:101]))

 

 

 

draw_fruits(fruits[100:101])

 

 

 

 k-평균 알고리즘은 앞에서 설명했듯이 반복적으로 클러스터 중심을 옮기면서 최적의 클러스터를 찾는다. 알고리즘이 반복한 횟수는 KMeans클래스의 n_iter_ 속성에 저장된다.

print(km.n_iter_)

 

 

 

최적의 k 찾기

 k-평균 알고리즘의 단점 중 하나는 클러스터 개수를 사전에 지정해야 한다는 것이다. 실전에서는 몇개의 클러스터가 있는지 알 수 없다. 어떻게 하면 적절한 k값을 찾을 수 있는지 알아보자.

 

 k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있다. 이 거리의 제곱 합을 이너셔(inertia)라고 부른다. 이너셔는 클러스터에 속한 샘플이 얼마나 가까이 모여 있는지를 나타내는 값으로 생각할 수 있다. 일반적으로 클러스터 개수가 늘어나면 클러스터 개개의 크기는 줄어들어 이너셔도 줄어든다. 엘보우 방법은 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법이다.

 

 클러스터 개수를 증가시키면서 이너셔를 그래프로 그리면 감소하는 속도가 꺽이는 지점이 있다. 이 지점부터는 클러스터 개수를 늘려도 클러스터에 잘 밀집된 정도가 크게 개선되지 않는다. 즉 이너셔가 크게 줄어들지 않는다.

 

 

 

 과일 데이터셋을 사용해 이너셔를 계산해 보자. KMeans 클래스는 자동으로 이너셔를 계산해서 inertia_ 속성으로 제공한다.

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

 

 

 

 

 

결론


 이전 포스팅에서는 과일 종류별로 픽셀 평균값을 계산했다. 하지만 실전에서는 어떤 과일 사진이 들어올지 모른다. 따라서 타깃값을 모르는 척하고 자동으로 사진을 클러스터로 모을 수 있는 군집 알고리즘이 필요하다.

 

 이번 포스팅에서는 대표적인 군집 알고리즘인 k-평균 알고리즘을 사용했다. k-평균은 비교적 간단하고 속도가 빠르며 이해하기도 쉽다. k-평균 알고리즘을 구현한 사이킷런의 KMeans 클래스는 각 샘플이 어떤 클러스터에 소속되어 있는지 labels_ 속성에 저장한다.

 

 각 샘플에서 각 클러스터까지의 거리를 하나의 특성으로 활용할 수도 있다. 이를 위해 KMeans 클래스는 transform() 메서드를 제공한다. 또한 predict() 메서드에서 새로운 샘플에 대해 가장 가까운 클러스터를 예측값으로 출력한다.

 

 k-평균 알고리즘은 사전에 클러스터 개수를 미리 지정해야 한다. 사실 데이터를 직접 확인하지 않고서는 몇 개의 클러스터가 만들어 질지 알기 어렵다. 그 방법은 이너셔를 사용하는 것이다. 이너셔가 더이상 크게 줄어들지 않는다면 클러스터 개수를 더 늘리는 것은 효과가 없다. 이를 엘보우 방법이라고 부른다.