혼자 공부하는 머신러닝 + 딥러닝

[혼공머신] 13. K-Means Clustering

Seon_ 2021. 12. 18. 19:42
13.K-means clustering

K-means

  • 앞선 절에서는 사과, 파인애플, 바나나 사진임을 미리 알고 있었기에 과일의 평균을 구할 수 있었다.
  • 그러나 실제 비지도 학습에서는 어떤 과일이 들어있는지 알지 못한다.
    • 이런 경우 k-means clustering을 통해 알고리즘이 평균값을 자동으로 찾아준다.
    • 이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centroid)라고도 부른다.

K-means 알고리즘 소개

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

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

KMeans 클래스

In [1]:
import urllib.request
url = 'https://bit.ly/fruits_300_data'
filename = 'fruits_300.npy'
urllib.request.urlretrieve('https://bit.ly/fruits_300_data', 'fruits_300.npy')
Out[1]:
('fruits_300.npy', <http.client.HTTPMessage at 0x1eea12366c8>)
In [2]:
import numpy as np

fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
In [3]:
from sklearn.cluster import KMeans

km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)
Out[3]:
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
       n_clusters=3, n_init=10, n_jobs=None, precompute_distances='auto',
       random_state=42, tol=0.0001, verbose=0)
  • 비지도 학습이므로 fit() 메서드에서 타깃 데이터를 사용하지 않는다.
In [4]:
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_ 속성에 저장된다.
    • labels_의 배열의 길이는 샘플의 개수와 같다.
    • 이 배열은 각 샘플이 어떤 레이블에 해당되는지 나타낸다.
    • nclusters = 3으로 지정했기 때문에 labels 배열의 값은 0, 1, 2 중 하나이다.
    • 레이블값 0, 1, 2와 레이블 순서에는 어떤 의미도 없다.
    • 실제 레이블 0, 1, 2가 어떤 과일 사진을 주로 모았는지 알아보려면 직접 이미지를 출력하는 것이 최선이다.
In [5]:
print(np.unique(km.labels_, return_counts=True))
(array([0, 1, 2]), array([ 91,  98, 111], dtype=int64))
  • 레이블 0이 91개, 레이블 1이 98개, 레이블 2가 111개의 샘플을 모았음을 알 수있다.
    • 각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력하기 위해 간단한 유틸리티 함수 draw_fruits() 를 만들어보자.
In [6]:
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() 함수는 (샘플 개수, 너비, 높이)의 3차원 배열을 입력받아 가로로 10개씩 이미지를 출력한다.

    • 샘플 개수에 따라 행과 열의 개수를 계산하고 figsize를 지정한다.
    • figsize는 ratio 매개변수에 비례하여 커진다.
  • 불리언 인덱싱을 이용해 특정 레이블의 이미지를 출력해보자.

In [7]:
draw_fruits(fruits[km.labels_==0])
draw_fruits(fruits[km.labels_==1])
draw_fruits(fruits[km.labels_==2])
  • 레이블이 0인 클러스터는 파인애플에 사과 9개와 바나나 2개가 섞여있다.
    • k-means 알고리즘이 샘플들을 완벽히 구별해내지는 못했다.
    • 하지만 훈련 데이터에 타깃 레이블을 전혀 제공하지 않았음에도 스스로 비슷한 샘플들을 잘 모았다.

클러스터 중심(Centroid)

  • KMeans 클래스가 최종적으로 찾은 centroid는 clusterscenters 속성에 저장되어 있다.
    • 이 배열은 fruits_2d 샘플의 centroid이므로 이미지로 출력하려면 reshape 해야한다.
In [8]:
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio=3)
  • KMeans 클래스는 훈련 데이터 샘플에서 centroid까지 거리로 변환해주는 transform() 메서드를 가지고 있다.
    • transform() 메서드가 있다는 것은 StandardScaler 클래스처럼 특성값을 변환하는 도구로 사용할 수 있다는 의미이다.
  • 인덱스가 100인 샘플에 transform() 메서드를 적용해보자.
    • fit() 메서드와 마찬가지로 2차원 배열을 기대하므로 슬라이싱 연산자를 이용하여 (1, 10000)의 배열을 전달한다.
In [9]:
print(km.transform(fruits_2d[100:101]))
[[5267.70439881 8837.37750892 3393.8136117 ]]
  • 하나의 샘플을 전달했기 때문에 반환된 배열은 크기가 (1, 클러스터 개수)인 2차원 배열이다.
    • 레이블 0까지의 거리가 3393.8로 가장 작다.
    • 이 샘플은 레이블 0에 속한 것 같다.
    • KMeans 클래스는 가장 가까운 centroid를 예측 클래스로 출력하는 predict() 메서드를 제공한다.
In [10]:
print(km.predict(fruits_2d[100:101]))
[2]
In [11]:
draw_fruits(fruits[100:101])
  • k-means 알고리즘은 앞에서 설명했듯 반복적으로 centroid를 옮기면서 최적의 클러스터를 찾는다.
    • 알고리즘이 반복한 횟수는 KMeans 클래스의 niter 속성에 저장된다.
In [12]:
print(km.n_iter_)
3
  • 이번에 우리는 타깃값을 사용하지 않았지만, 약간의 편법을 이용했다.
    • n_clusters를 3으로 지정한 것은 타깃에 대한 정보를 활용한 셈이다.
    • 실전에서는 클러스터 개수조차 알 수 없다.
    • 그렇다면 n_clusters를 어떻게 지정해야 할까?

최적의 k 찾기

  • K-means 알고리즘의 단점 중 하나는 클러스터 개수를 사전에 지정해야 한다는 것이다.
    • 사실 군집 알고리즘에서 적절한 k 값을 찾기 위한 완벽한 방법은 없다.
    • 몇 가지 도구가 있지만 저마다 장단점이 있다.
    • 여기서는 적절한 클러스터 개수를 찾기 위한 대표적인 방법인 elbow 방법에 대해 알아보겠다.
    • 앞서 본 것 처럼 k-means 알고리즘은 centroid와 클러스터에 속한 샘플 사이의 거리를 잴 수 있다.
    • 이 거리의 제곱 합을 inertia라고 부른다.
    • inertia는 클러스터에 속한 샘플이 얼마나 가깝게 모여있는지를 나타내는 값으로 생각할 수 있다.
    • 일반적으로 클러스터 개수가 늘어나면 클러스터 개개의 크기는 줄어들기 때문에 inertia도 줄어든다.
    • 클러스터 개수를 증가시키면서 inertia를 그래프로 그리면 감소하는 속도가 꺾이는 지점이 있다.
    • 이 지점부터는 클러스터 개수를 늘려도 클러스터에 잘 밀집된 정도가 크게 개선되지 않는다.
    • 즉 inertia가 크게 줄어들지 않는다. 이 지점이 마치 팔꿈치 모양이어서 elbow 방법이라고 부른다.
    • 친절하게도 KMeans 클래스는 자동으로 inertia를 계산해서 inertia_ 속성으로 제공한다.
In [13]:
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()
  • k=3에서 그래프의 기울기가 바뀐 것을 볼 수 있다.
In [14]:
from IPython.core.display import display, HTML

display(HTML("<style>.container { width:90% !important; }</style>"))