[머신러닝] K-평균 알고리즘

4 분 소요

K-평균 알고리즘

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

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

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

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

KMeans 클래스

!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.cluster import KMeans

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

사이킷런의 k-평균 알고리즘은 sklearn.cluster 모듈 아래 KMeans 클래스에 구현되어 있다. 이 클래스에서 설정할 매개변수는 클러스터 개수를 지정하는 n_clusters이다.

print(km.labels_)
[0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 2 0 2 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 2 2 0 0 0 0 0 0 0 0 2 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 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 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 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 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]

군집 결과는 KMeans 클래스 객체의 labels_ 속성에 저장된다. 이 배열의 길이는 샘플 개수와 같다. n_clusters=3이기 때문에 labels_ 배열의 값은 0,1,2 중 하나이다.

print(np.unique(km.labels_, return_counts=True))
(array([0, 1, 2], dtype=int32), array([ 91,  98, 111]))

레이블 0,1,2로 모은 샘플의 개수를 확인한다.

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

#draw_fruits(fruits[km.labels_==0])
#draw_fruits(fruits[km.labels_==1])
#draw_fruits(fruits[km.labels_==2])

각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력하기 위해 유틸리티 함수 draw_fruits를 만든다.

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

위 코드를 실행하면 클러스터 중심을 이미지로 그려준다.

print(km.n_iter_)
3

k-평균 알고리즘의 반복횟수는 n_iter_ 속성에 저장되있다.

여기서 n_clusters를 3으로 지정한 것은 타깃에 대한 정보를 활용한 셈이다. 실전에서는 클러스터 개수조차 알 수 없다.



최적의 k 찾기

k-평균 알고리즘의 단점 중 하나는 클러스터 개수를 사전에 지정해야 한다는 점이다.

군집 알고리즘에서 적절한 k 값을 찾기 위한 완벽한 방법은 없다. 몇 가지 도구가 있지만 저마다 장단점이 있다.

그 중 대표적인 방법 엘보우(elbow) 방법이 있다.

앞서 본 k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있다. 이 거리의 제곱 합을 이너셔(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.xlabel('k')
plt.ylabel('inertia')
plt.show()

kmeans

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

위 코드에서는 클러스터 개수 k를 2~6까지 바꿔가며 KMeans 클래스를 5번 훈련한다. fit 메소드로 모델을 훈련 후 inertia_ 속성에 저장된 이너셔 값을 inertia 리스트에 추가한다. 마지막에 저장된 값을 그래프로 출력한다.

이 그래프에서 꺾이는 지점이 뚜렷하지 않지만 k=3에서 그래프의 기울기가 조금 바뀐 것을 볼 수 있다. 엘보우 지점보다 클러스터 개수가 많아지면 이너셔의 변화가 줄어들면서 군집효과도 줄어든다. 하지만 이 그래프에서는 이런 지점이 명확하지 않다.