AI/혼자공부하는머신러닝딥러닝

[ML] 06-2 k-평균

inthyes 2024. 1. 23. 20:06

k-평균 알고리즘 소개

k-평균 알고리즘은 처음에 랜덤하게 클러스터 중심을 정하고 클러스터를 만든다. 그다음 클러스터의 중심을 이동하고 다시 클러스터를 만드는 식으로 반복해서 최적의 클러스터를 구성하는 알고리즘이다.

 

클러스터 중심은 k-평균 알고리즘이 만든 클러스터에 속한 샘플의 특성 평균값이다. 센트로이드라고 부르며 가장 가까운 클러스터 중심을 샘플의 또 다른 특성으로 사용하거나 새로운 샘플에 대한 예측으로 활용할 수 있다.

 

알고리즘의 작동 방식은 아래와 같다.

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

KMeans 클래스

!wget https://bit.ly/fruits_300_data -O fruits_300.npy

 

k-평균 모델을 훈련하기 위해 크기의3차원 배열을 2차원 배열로 변경한다.

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

 

n_clusters 매개변수를 이용해 클러스터 개수를 지정할 수 있다. 비지도학습이기 때문에 fit()메서드를 타깃 데이터를 사용하지 않고 수행한다.

from sklearn.cluster import KMeans
km = KMeans(n_clusters = 3, random_state = 42)
km.fit(fruits_2d)

 

labels_속성에는 군집된 결과가 저장된다. 배열의 길이는 샘플 개수와 같고 이 배열은 각 샘플이 어떤 레이블에 해당되는지를 나타낸다.

print(km.labels_)

 

unique()를 통해 각 클러스터에 해당하는 샘플 수를 출력할 수 있다.

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

 

유틸리티 함수 draw_fruits()를 만들어 그림으로 출력해보자.

draw_fruits() 함수는 3차원 배열(샘플 개수, 너비, 높이)를 입력받아 가로로 10개씩 이미지를 출력한다.

2중 for 반복문을 사용하여 첫 번째 행을 따라 이미지를 그리고 두 번째 행의 이미지를 그리는 식으로 진행한다.

import matplotlib.pyplot as plt
def draw_fruits(arr, ratio = 1):
  n = len(arr) # 샘플 개수
  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_ == 0과 같이 쓰면 km.labels_ 배열에서 0인 위치는 True, 그 외는 모두 False가 되고, True인 위치의 원소만 모두 추출하도록 사용 가능하다.

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

레이블 0에 해당하는 원소를 출력해본 결과 다수의 파인애플과 9개의 사과, 2개의 바나나가 혼합되어 있음을 확인할 수 있다.

k-평균 알고리즘이 이 샘플들을 완벽하게 구별해내지 못했지만 훈련 데이터에 타깃 레이블을 제공하지 않았음에도 스스로 비슷한 샘플들을 잘 모았다고 판단할 수 있다.

클러스터 중심

KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 조정된다.

이 배열은 fruits_2d 샘플의 클러스터 중심이기 때문에 이미지로 출력하려면 100 * 100크기의 2차원 배열로 바꿔야 한다.

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

KMeans 의 transform() 메서드는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해주는 역할을 한다. 

transform() 메서드는 fit() 과 마찬가지로 2차원 배열을 기대하기 때문에 슬라이싱 연산자를 사용해서 (1, 10000) 크기의 배열을 전달한다.

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

결과값을 통해 첫 번째 클러스터(레이블 0)의 거리가 가장 작다는 것을 확인할 수 있다.

 

predict() 메서드를 활용하여 가장 가까운 클러스터 중심을 예측 클래스로 출력해보자.

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

레이블 0을 예측하였다. 클러스터 중심을 그려보았을 때 레이블 0은 파인애플이었으므로 이 샘플은 파인애플일 것이라는 예측이 가능하다.

draw_fruits(fruits[100:101])

 

k-평균 알고리즘은 반복적으로 클러스터 중심을 옮기면서 최적의 클러스터를 찾는다.

n_iter_ 속성에는 알고리즘이 반복(이동)한 횟수가 저장된다.

print(km.n_iter_)

최적의 k 찾기

엘보우 방법은 최적의 클러스터 개수를 정하는 방법 중 하나이다. 이너셔는 클러스터 중심과 샘플 사이 거리의 제곱 합을 말한다. 클러스터 개수에 따라 이너셔 감소가 꺾이는 지점이 적절한 클러스터 개수 k가 될 수 있다.

 

클러스터 개수 k를 2~6까지 바꿔가며 KMeans 클래스를 5번 훈련하고 fit()메서드로 모델을 훈련한 뒤 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()

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