06. 비지도 학습

 

06-1 군집 알고리즘

  • 시작하기 전에


과일의 여러가지 사진을 어떤 과일인지 찾기 위해 자동으로 분류하는 프로그램을 만들어 보려 한다.

 

<타깃을 모르는 비지도 학습>

chapter 2에서 배운 것처럼 타깃을 모르거나 타깃이 없을 때 사용하는 머신러닝 알고리즘을 비지도 학습(unsupervised learning)이라고 한다. 가르쳐 주지 않아도 데이터에 있는 무언가를 학습하도록 하기 위한 방법이 무엇이 있을까?

<과일 사진 데이터 준비하기>

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

처음에 숫자 0 인줄 알았는데, 계속 에러가 나길래 봤더니 open 의 알파벳 O 였다...

import numpy as np
import matplotlib.pyplot as plt

fruits = np.load('fruits_300.npy')
print(fruits.shape)
print(fruits[0, 0, :])

넘파이 배열의 데이터를 가져와서 첫 번째 행을 출력하면 위와 같은 리스트가 나오는 것을 볼 수 있다.

 

plt.imshow(fruits[0], cmap='gray')
plt.show()

imshow() 메소드를 사용하여 cmap 매개변수를 gray로 지정하여 흑백사진으로 이미지를 출력하여 볼 수 있다. 0에 가까울수록 검게 나타나고 높은 값을 밝게 표시된다. 이미지를 이렇게 반전시킨 이유는 우리의 관심사가 바탕이 아닌 물체(과일)에 있기 때문이다. 또한 우리가 바라보는 것과 컴퓨터가 이미지를 인식하고 처리하는 방식은 다르다는 것을 염두에 두어야 한다.

 

plt.imshow(fruits[0], cmap='gray_r')
plt.show()

cmap 매개변수를 gray_r로 지정하여 이미지를 재반전 후 출력하면 우리 눈에 좀 더 편한 이미지가 출력된다. 

 

fig, axs = plt.subplots(1, 2)
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
plt.show()

바나나와 파인애플의 이미지도 위와같은 코드로 출력할 수 있다. subplot() 메서드를 활용하면 여러개의 그래프를 배열처럼 쌓을 수 있다. 

 

<픽셀값 분석하기>

100 x 100의 다차원 배열의 이미지를 펼쳐서 길이가 10,000인 1차원 배열로 만들 수 있다. 이렇게 만들면 데이터를 다룰 때 배열 계산을 훨씬 쉽게 진행할 수 있다.

 

apple = fruits[0:100].reshape(-1, 100*100)
pineapple = fruits[100:200].reshape(-1, 100*100)
banana = fruits[200:300].reshape(-1, 100*100)

print(apple.shape)

reshape() 메서드를 활용하여 두번째 차원과 세번째 차원을 합친 다음 과일의 배열의 크기를 확인해 볼 수 있다.

 

평균값을 확인해본다.

plt.hist(np.mean(apple, axis=1), alpha=0.8)
plt.hist(np.mean(pineapple, axis=1), alpha=0.8)
plt.hist(np.mean(banana, axis=1), alpha=0.8)
plt.legend(['apple', 'pineapple', 'banana'])
plt.show()

히스토그램을 그려 보면 바나나는 대체적으로 40 이하의 평균값을, 사과와 파인애플은 80 - 100 사이의 평균값을 띄는 것을 알 수 있다. 하지만 사과와 파인애플은 많이 겹쳐 있어서 그래프로만 보기에는 구분하기 쉽지 않다.

fig, axs = plt.subplots(1, 3, figsize=(20, 5))
axs[0].bar(range(10000), np.mean(apple, axis=0))
axs[1].bar(range(10000), np.mean(pineapple, axis=0))
axs[2].bar(range(10000), np.mean(banana, axis=0))
plt.show()

막대그래프를 그려보니 과일마다 값이 높은 구간이 다른 것을 알 수 있다. 다시 평균값을 100 x 100 크기로 바꿔 출력해보도록 하겠다.

 

apple_mean = np.mean(apple, axis=0).reshape(100, 100)
pineapple_mean = np.mean(pineapple, axis=0).reshape(100, 100)
banana_mean = np.mean(banana, axis=0).reshape(100, 100)
fig, axs = plt.subplots(1, 3, figsize=(20, 5))
axs[0].imshow(apple_mean, cmap='gray_r')
axs[1].imshow(pineapple_mean, cmap='gray_r')
axs[2].imshow(banana_mean, cmap='gray_r')
plt.show()

각각의 이미지와 가까운 사진을 고르면 각 과일을 구분할 수 있지 않을까?

 

<평균값과 가까운 사진 고르기>

평균값과 가까운 사진을 고르기 위해 3장에서 배운 평균 절댓값 오차를 사용할 수 있다.

abs_mean은 각 샘플의 오차 평균이므로 크기가 300인 1차원 배열임을 알 수 있다.

 

apple_index = np.argsort(abs_mean)[:100]
fig, axs = plt.subplots(10, 10, figsize=(10, 10))
for i in range(10):
  for j in range(10):
    axs[i, j].imshow(fruits[apple_index[i*10 + j]], cmap='gray_r')
    axs[i, j].axis('off')
plt.show()

평균 값과 가장 오차가 적은 순서대로 이미지를 골라 10 x 10 배열로 출력한 결과다. axis() 매개변수를 'on'으로 설정하면 좌표값이 함께 출력된다. 여하튼, 이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을 군집(clustering)이라고 한다. 군집은 대표적인 비지도 학습 작업 중 하나로, 군집 알고리즘에서 만든 그룹을 클러스터(cluster) 라고 한다.

 


06-2  k-평균

  • 시작하기 전에

    앞서 군집화를 할 때, 사과/파인애플/바나나 라는 것을 알고 있었지만, 실제 비지도 학습에서는 그러한 정보가 없이 진행된다. 이런 경우 k-평균(k-means) 군집 알고리즘을 통해 평균값을 자동으로 찾을 수 있다. 이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centeroid) 라고 부른다.

<k-평균 알고리즘>

# 기본미션

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

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

알고리즘 작동 방식을 그림으로 그리면 위와 같다. 클러스터 중심을 랜덤하게 지정한 후 중심에서 가장 가까운 샘플을 하나의 샘플로 묶으면(1) 과일이 분류된 것을(2) 확인할 수 있다. 클러스터에는 순서나 번호는 의미가 없다. 여기서 클러스터 중심을 다시 계산한 다음 가장 가까운 샘플을 묶으면(2) 분류된 것을(3) 확인할 수 있다. 여기서 3의 결과와 2의 결과가 동일하므로 클러스터에 변동이 없다는 것을 캐치하고 k-평균 알고리즘을 종료하면 된다.

<KMeans 클래스>

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)

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

 

k-평균 알고리즘은 KMeans() 클래스에 구현되어 있는데, n_clusters 매개변수를 활용하여 클러스터의 갯수를 지정해줄 수 있다. 주의할 점은, 비지도 학습이기 때문에 fit() 메서드에서 타깃 데이터를 사용하지 않는다는 점이다. 배열을 출력하여 샘플이 0, 1, 2 중 어떤 레이블에 해당되는지 확인하고 각 클러스터의 샘플의 갯수를 출력해 보았다. 

def draw_fruits(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_fruits() 함수를 만들고 레이블이 0인 과일 사진을 모두 그려볼 수 있다. 넘파이 자료형에서는 불리언 배열을 사용해 원소를 선택할 수 있고 이러한 것을 불리언 인덱싱(boolean indexing)이라고 한다. 더 자세한 내용은 4장에서 소개했었다. 즉, True인 원소만 추출하는 것이다.

레이블이 0인 클러스터는 파인애플이 다수이면서도, 사과와 바나나도 일부 섞인 것을 알 수 있다.

<클러스터 중심>

KMeans 클래스가 찾은 최종 중심은 cluster_centers_라는 속성에 저장되어 있다. 이 배열은 2d 이미지 샘플을 중심으로 이루어졌기 때문에 이미지로 출력하려면 100 x 100 크기로 다시 바꿔야 한다. 

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

draw_fruits(fruits[100:101])

 

KMeans 클래스에서도 transform(), predict() 메서드가 있기 때문에 이를 활용하여 가장 가까운 클러스터 중심을 예측 클러스터로 출력하거나 예측할 수 있다. 또 n_iter_ 속성을 출력하여 반복횟수를 출력해볼 수 있다.

<최적의 k 찾기>

k-평균 알고리즘의 단점은 클러스터 개수를 사전에 지정해야 한다는 것이다. 적절한 k 값을 찾기 위한 몇 가지 도구가 있는데 엘보우(elbow) 방법이 대표적이다. k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있다. 이 거리의 합을 이너셔라고 하는데, 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값이다.

일반적으로 클러스터 개수가 늘어나면 클래스터 각각의 크기는 줄어들기 때문에 이너셔도 줄어든다. 엘보우 방법은 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 갯수를 찾는 방법이다.

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.ylavel('inertia')
plt.show()

 

이너셔를 그래프로 그리면 감소하는 속도가 꺾이는 지점이 있는데, 이 지점부터는 클러스터 개수를 늘려도 클러스터에 밀집된 정도가 크게 개선되지 않는다. 이 지점이 마치 팔꿈치 모양과도 같아서 엘보우 방법이라고 부른다고 한다.


06-3  주성분 분석

  • 시작하기 전에

    k-평균 알고리즘으로 나눠진 사진들을 군집이나 분류에 영향을 끼치지 않으면서 용량을 줄여서 저장할 방법이 없을까?


<차원과 차원 축소>

데이터가 가진 속성을 특성이라고 불렀는데, 머신 러닝에서는 이런 특성을 차원(dimension)이라 부른다. 앞서 다뤘던 과일 사진의 경우 10,000개의 픽셀 = 10,000개의 특성 = 10,000개의 차원이라는 뜻이다.

비지도 학습 작업 중 차원 축소(dimensionality reduction) 알고리즘이 있는데, 3장에서 차원이 많아지면 선형 모델의 성능이 높아지고 train model에 과적합 되기 쉽다는 것을 배웠다. 차원 축소는 데이터를 가장 잘 나타내는 특성을 선택하여 데이터의 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법이다. 

대표적인 차원 축소 알고리즘인 주성분 분석(Principal Component Analysis)PCA라고도 부른다.

 

<주성분 분석 소개>

주성분 분석은 데이터에 있는 분산이 큰 방향을 찾는 것이다. 분산은 데이터가 널리 퍼져있는 정도를 말하는데, 분산이 큰 방향을 데이터로 잘 표현하는 벡터라고 생각할 수 있다.

위와 같은 2차원 벡터가 있을 때 대각선 방향이 분산이 가장 크다고 볼 수 있다. 이 벡터를 주성분(principatl component) 이라 부른다. 

주성분 벡터의 원소 개수는 원본 데이터셋의 특성 개수와 같고, 주성분을 사용해 차원을 줄일 수 있다. S(4,2)를 주성분에 직각으로 투영하여 1차원 데이터 P(4,5)를 만들 수 있다. 주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어든다.

첫번째 주성분을 찾고 이 벡터에 수직이고 분산이 가장 큰 다음 방향을 찾으면 두번째 주성분이 된다. 일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있다.

<PCA 클래스>

 

from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)

print(pca.components_.shape)
print(fruits_2d.shape)

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)

주성분 학습도 비지도 학습이기 때문에 fit() 메서드에 타깃값을 제공하지 않는다. 이 클래스가 찾은 주성분은 components_ 속성에 저장되어 있다. 이 배열의 크기를 확인해보니 n_components=50으로 지정했기 때문에 첫번째 차원이 50이고, 즉 50개의 주성분을 찾은 것을 알 수 있다. 두번째 주성분은 항상 원본 데이터의 특성 갯수인 10,000이다.

 

draw_fruits(pca.components_.reshape(-1, 100, 100))

 

원본 데이터와 차원이 같으므로 이미지처럼 출력해볼 수 있다. 위 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다.

 

 

 

# 선택미션

6-3 확인문제 

2. 샘플개수가 1,000개 이고 특성이 100개인 데이터셋의 크기는 (1,000, 100) 이다. 이 데이터를 PCA를 사용해 10개의 주성분을 찾아 변환했을 때 변환된 데이터셋의 크기는 특성 개수만 바뀌므로 (1,000, 10) 이 된다.

3. 2번문제에서 설명된 분산이 가장 큰 주성분은 분산이 가장 큰 방향부터 찾기 때문에 첫번째 주성분이라고 볼 수 있다.


● 마무리

이번 챕터에서는 비지도 학습인 군집, 클러스터링, k-평균, 주성분(PCA) 분석을 배웠다. 비지도 학습에서 차원을 줄일 수 있을거라곤 생각도 못했는데 개인적으로 PCA라는 개념이 낯설면서도 신기하고 재미있고 유용했다. 

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter06_unsupervised_learning.ipynb
1.30MB

 

+ Recent posts