티스토리 뷰

직전 포스팅에서는 사과, 바나나, 파인애플 사진에 있는 각 픽셀의 평균값을 구하여 그 평균값과 오차가 가장 적은 사진으로 각 과일을 구분했다. 

 

하지만, 이 경우에는 처음부터 이건 사과야~ 하고 정답을 미리 알려준 사실상 지도 학습이었다고 할 수 있는데, 진짜 비지도 학습에서는 사진에 어떤 과일이 들어 있는 지 알지 못한다.

 

각 사진이 어떤 과일인 지 알지 못하는 상황에서 컴퓨터가 어떻게 하면 사과인 사진만을 골라 평균값을 계산하게 할 수 있을까?

 

이럴 때 사용하는 것이 바로 k-평균 군집 알고리즘이다. 

 

바로 한번 알아보도록 하자.


k-평균 알고리즘은 클러스터 중심이라는 개념을 사용한다.

 

무작위로 k개의 클러스터 중심을 정하고, 각 클러스터 중심과 가장 가까운 데이터들 끼리 군집화를 시켜 하나의 클러스터 샘플로 지정하는 것이다. 

 

k개의 클러스터 샘플이 만들어졌으면, 다시 클러스터에 속한 샘플의 평균값을 구하여 클러스터 중심을 해당 평균값으로 변경하고, 더 이상 클러스터 중심에 변화가 없을 때까지 이를 반복하는 것이다.

 

이를 그림으로 나타내면 아래와 같다.

출처: https://velog.io/@jhlee508/%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-K-%ED%8F%89%EA%B7%A0K-Means-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98

 

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

 

이제 코드와 함께 알아보자.


우선, 마찬가지로 이미지 데이터 셋을 불러와. 2차원 배열로 변경한다.

import numpy as np

fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

 

사이킷런의 k-평균 알고리즘은 sklearn.cluster 모듈 아래 KMeans 클래스에 구현이 되어있다.

 

n_clusters 매개변수로 생성할 클러스터의 개수를 지정할 수 있다. 구분할 이미지가 3종류이니, 3으로 지정한다.

 

지금까지 봤던 다른 알고리즘과 동일하게 fit으로 데이터를 학습시키지만, 비지도 학습이기 때문에 따로 타깃 데이터는 지정하지 않아도 된다.

from sklearn.cluster import KMeans

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

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

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

위와 같이 데이터가 3개의 클러스터로 레이블을 가지고 분류가 된 것을 확인할 수 있다.

 

이제 각 클러스터가 어떤 이미지를 가지고 있는지를 자동으로 출력하기 위한 함수를 만들어보려고 한다. 

ratio매개변수에 따라 그래프 figsize를 조절하며 클러스터 배열에 있는 값을 자동으로 그려주는 함수이다.

import matplotlib.pyplot as plt

def draw_fruits(arr, ratio=1):
    # n = 샘플의 개수
    n = len(arr)

    # 한 줄에 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:
                axs[i, j].imshow(arr[i*10+j], cmap='gray_r')
            axs[i, j].axis('off')

    plt.show()

 

위에서 만든 함수와 불리언 인덱싱(km.labels_==n) 을 사용하여 km.labels_ 에서 값이 n인 위치를 제외하고 모두 False로 바꿔 이미지를 출력한다.

# 레이블 0으로 클러스터링된 91개의 이미지 출력
draw_fruits(fruits[km.labels_==0])

# 레이블 1으로 클러스터링된 이미지
draw_fruits(fruits[km.labels_==1])

# 레이블 2으로 클러스터링 이미지
draw_fruits(fruits[km.labels_==2])

결과를 확인해보면, 레이블이 1인 클러스터는 바나나로만 이뤄져 있는 걸 알 수 있다.

 

반면에 파인애플을 모아놓은 클러스터를 보면 다른 종류의 과일이 조금씩 섞여있다. k-평균 알고리즘이 이 샘플들을 완벽하게 구별해내지는 못하지만, 훈련 데이터에 타깃 레이블을 전혀 제공하지 않았음에도 스스로 비슷한 샘플들을 아주 잘 모았다.

 

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

# 클러스터 중심
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio=3)

위 이미지는 군집 알고리즘에서 각 픽셀의 평균값을 출력한 것과 매우 유사하다!

 


또한 KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변화해주는 transform() 메서드를 가지고 있다.

fransform() 메서드 또한 fit()과 마찬가지로 2차원 배열을 줘야하므로, 배열을 슬라이싱하여 (1, 10000) 크기로 전달해야 한다.

print(km.transform(fruits_2d[100:101]))
# [[3393.8136117  8837.37750892 5267.70439881]]

위 결과를 보면, 인덱스가 100인 샘플은 0번 클러스터 중심과의 거리가 3394 정도로 제일 가까운 걸 확인할 수 있다.

 

그럼 예측 값은? 

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

0번 클러스터와 가장 가까우니, 예측값 또한 0번 클러스터로 예측한다!

 

해당 샘플을 그려보면, 당연히 파인애플이 나온다.

draw_fruits(fruits[100:101])


최적의 k 찾기

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

 

어떻게 하면 적절한 클러스터의 개수를 찾을 수 있을까?

 

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

이너셔 그래프를 확인해보면 알 수 있듯, 3 정도에서 이너셔의 감소 속도가 꺾인다. 

 

엘보우 방식을 사용하여 최적의 k를 찾아낸 뒤, k-평균 알고리즘 모델을 구현하면 실전에서도 괜찮은 성능을 기대할 수 있다.

 

 

 

끝!

Comments