[혼공머신] 5주차 06-2 k-평균
본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.
K-Means 알고리즘 작동 방식
이번에는 진짜 unsupervised learning을 시도해볼 것이다. 대표적인 알고리즘으로 k-평균(K-Means) 알고리즘이 있다. 우리가 지난번에는 mean을 직접 계산했다면 여기서는 mean을 자동으로 구해 기준을 만들어 clustering을 진행한다.
이렇게 말해서는 K-Means 알고리즘을 이해하기 어려울 것이라고 생각한다. 하지만 이름에서 살펴볼 수 있듯, KNN 알고리즘과 비슷한 방식으로 작동한다. 이미지가 점으로 좌표평면에 위치하고 있다고 생각했을 때, 랜덤으로 $K$개의 중심점을 지정한다. 그리고 그 중심점에서 가까운 sample들을 하나의 cluster로 묶는다. 이때, 처음 묶은 cluster에는 여러 것들이 섞여 있을 것이다. 그 다음 그 cluster 내에서 mean을 구해 그 mean에 가까워지도록 중심점을 이동시킨다.
지난번에 단순히 mean을 통해서 바나나와 사과를 구별했을 경우를 예로 든다면 만약 바나나가 2개, 사과가 1개인 cluster가 만들어졌다면 바나나의 mean에 가깝도록 중심점이 이동하게 될 것이다. 이 상황에서 또 중심점을 갱신한다면 사과로부터 중심점이 멀어져 사과는 배제될 수 있을 것이다. 이러한 방식으로 cluster 내에 유사한 mean을 가진 sample들만 존재하도록 중심점을 계속 갱신해가면서 최적의 cluster을 만드는 것이다. 이제 한 번 실제로 K-Means 알고리즘을 살펴보도록 하자.
K-Means
우선, 지난번에 사용했던 데이터를 불러오고, 2차원 배열 형태로 변경해준다. 그리고 scikit-learn을 통해서 아주 손쉽게 K-Means 학습을 진행한다. 당연히 unsupervised learning이기에 input data만 존재하고 output에 대한 data는 존재하지 않는다. 그리고 학습 결과 몇 개의 cluster로 나뉘었는지, 그리고 각 cluster별로 몇 개의 sample이 있는지 확인해보자. 확인해본 결과 올바르게 3개의 cluster로 나뉘었음을 확인할 수 있다. 그런데 개수를 살펴보니 완전 정확하게 clustering이 진행이 되지는 않은 것 같다. 이제 한 번 어떻게 clustering이 되었는지 살펴보기로 하자.
1
2
3
4
5
6
7
8
9
10
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
data = np.load('fruits_300.npy')
data_2d = data.reshape(-1, 100*100)
kmeans = KMeans(n_clusters=3, random_state=42)
kmeans.fit(data_2d)
np.unique(kmeans.labels_, return_counts=True)
(array([0, 1, 2]), array([112, 98, 90], dtype=int64))
우리의 한빛미디어는 clustering된 sample들을 이미지로 나열해서 볼 수 있도록 코드로 만들어 놓았다. 이를 실행해보면 아래와 같이 0 라벨의 cluster로 구분된 sample들을 확인할 수 있다. 대부분 파인애플이나 사과랑 바나나가 약간 첨가되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import matplotlib.pyplot as plt
def draw_fruit(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_fruit(data[kmeans.labels_==0])
바나나의 경우, 다른 것이 바나나로 clustering된 것이 없이 바나나만 포함된 것을 확인할 수 있다.
1
draw_fruit(data[kmeans.labels_==1])
사과도 바나나와 같이 다른 것 없이 사과만 포함된 것을 확인할 수 있다.
1
draw_fruit(data[kmeans.labels_==2])
클러스터 중심
지난번에 우리는 직접 사과, 파인애플, 바나나의 mean을 계산하여 기준으로 사용하였다. 이 K-Means에서도 우리가 했던 것처럼 기준으로 정해 사용하는 것이 존재한다. 그것을 우리는 클러스터 중심이라고 하며, 아래와 같이 확인할 수 있다.
1
draw_fruit(kmeans.cluster_centers_.reshape(-1, 100, 100), ratio=3)
클러스터의 중심이 있다면 이 중심으로부터 다른 값들이 얼마나 떨어져 있는지도 구할 수 있을 것이다. 이 거리를 통해서 어떤 것이 가까이 있는지, 어떤 것이 멀리 있는지 확인해 가장 가까운 cluster에 sample을 할당할 수 있다. 한 가지 예시를 살펴보자. 아래와 같이 특정 sample에 대해서 각 cluster에 대한 거리를 확인할 수 있다. 거리를 통해 살펴봤을 때, 가장 가까운 cluster는 라벨이 2인 cluster, 즉 사과이다.
1
kmeans.transform(data_2d[0:1])
array([[4661.3649493 , 8642.61845541, 3094.2021583 ]])
그럼 과연 이 sample이 사과인지 살펴보도록 하자. 그렇다. 사과이다.
1
draw_fruit(data[0:1])
최적의 k 찾기
사실 우리가 한 가지 언급하지 않고 넘어간 것이 있다. 바로 $K$의 존재이다. 이전에 KNN 알고리즘에서도 $K$가 존재했고, 그때는 우리가 직접 설정해 주었었다. 이는 K-Means 알고리즘에서도 동일하다. 언급하지는 않았지만 위의 코드를 다시 살펴보면 $K=3$으로 설정해준 것을 확인할 수 있을 것이다. 이제까지 살펴본 과일 sample을 살펴보면 사실 이것은 classificiation이라고 할 수 있다. 명확한 정답이 있으며, 기준이 있다. 하지만, unsupervised learning에서는 이렇게 확실한 정답이 존재하기는 힘들다. 예를 들어, 마케팅을 할 때, 사용자의 특성에 맞게 마케팅 방식을 정하고자 하는 경우, 사용자를 특성에 따라 clustering해야 할 것이다. 이러한 경우에는 명확한 기준이 없이 수많은 방법으로 clustering할 수 있을 것이다. 이와 같이 다양한 방법으로 clustering할 수 있으나, 이러한 상황에서도 최적의 $K$를 찾기 위해 노력은 할 수 있을 것이다.
최적의 $K$를 찾기 위한 방법으로 엘보우(Elbow) 방법이 있다. K-Means는 KNN과 비슷하게 cluster의 중심과 sample 사이의 거리를 계산할 수 있다. 이때, 이 거리의 제곱합을 이너셔(Inertia)라고 부르며, 얼마나 cluster 안에 sample들이 가깝게 모여있는지를 나타낸다. sample들이 가깝게 모여있을수록 잘 clustering되어 있음을 의미한다는 것은 쉽게 이해할 수 있을 것이다. 따라서 inertia가 작을수록 좋을 것이다. 하지만, inertia가 무조건 낮은 것이 좋은 것은 아니다. 극단적으로 보자면 sample 수만큼 cluster의 중심이 존재하는 경우 inertia가 minimum이 될 것이다. 따라서 우리는 inertia를 minimize 하되, 어느 정도 minimize를 하는데, 감소하는 속도가 줄어 $K$를 늘리더라도 엄청난 성능 개선의 효과가 없는 경우 중단을 하는 방식을 사용할 것이다. 이를 코드로 살펴보도록 하자. 아래의 plot을 보면 3.0까지는 매우 가파르게 감소하나 그 이후로는 완만하게 감소하는 것을 확인할 수 있다. 따라서 우리는 최적의 $K$값으로 3을 사용하게 되는 것이다.
1
2
3
4
5
6
7
8
9
inertia = []
for k in range(2, 7):
kmeans = KMeans(n_clusters=k, n_init='auto', random_state=42)
kmeans.fit(data_2d)
inertia.append(kmeans.inertia_)
plt.plot(range(2, 7), inertia)
plt.xlabel('k')
plt.ylabel('inertia')
plt.show()