본문 바로가기

카테고리 없음

[ML 세션] 혼공머신 Chapter 06. 비지도 학습

Chapter 06. 비지도 학습

06-1 군집 알고리즘

✅ 타깃을 모르는 비지도 학습

타깃이 없을 때 사용하는 머신러닝 알고리즘을 비지도 학습이라고 합니다. 사람이 가르쳐 주지 않아도 데이터에 있는 무언가를 학습하는 거죠.

✅ 과일 사진 데이터 준비하기

더보기

!는 뭔가요?

코랩의 코드 셀에서 '!' 문자로 시작하면 코랩은 이후 명령을 파이썬 코드가 아니라 리눅스 셀(shell) 명령으로 이해합니다.

print(fruits.shape)
#-> (300, 100, 100)

fruits 배열의 첫 번째 차원(300)은 샘플의 개수를 나타내고, 두 번째 차원(100)은 이미지 높이, 세 번째 차원(100)은 이미지 너비입니다. 이미지 크기는 100 * 100입니다. 각 픽셀은 넘파이 배열의 원소 하나에 대응합니다.

보통 흑백 샘플 이미지는 바탕이 밝고 물체가 짙은 색입니다. 하지만 이미지를 넘파이 배열로 변환할 때 반전시킬 수 있습니다. 사진의 흰 바탕(높은 값)은 검은색(낮은 값)으로 만들고 실제 물체가 있어 짙은 부분(낮은 값)은 밝은 색(높은 값)으로 바꿀 수 있습니다.

더보기

컴퓨터는 왜 255에 가까운 바탕에 집중하나요❓

알고리즘이 어떤 출력을 만들기 위해 곱셈, 덧셈을 합니다. 픽셀값이 0이면 출력도 0이 되어 의미가 없습니다. 픽셀값이 높으면 출력값도 커지기 때문에 의미를 부여하기 좋습니다.

✅ 픽셀값 분석하기

더보기

axis 인수가 뭔가요?

아래 그림의 axis는 배열의 '축'을 의미합니다. 다음의 apple 2차원 배열에서 axis=1일 때는 열 방향으로 계산하고, axis=0일 때는 행 방향으로 계산합니다.

더보기

히스토그램이 뭔가요?

히스토그램은 값이 발생한 빈도를 그래프로 표시한 것입니다. 보통 x축이 값의 구간(계급)이고, y축은 발생 빈도(도수)입니다.

히스토그램을 보면 바나나 사진의 평균값은 40 아래에 집중되어 있습니다. 사과와 파인애플은 90~100 사이에 많이 모여 있네요. 여기서 바나나는 바로 구분할 수 있지만 사과와 파인애플은 많이 겹쳐있어서 픽셀값만으로는 구분하기 쉽지 않습니다.

 

다른 방법으로 픽셀별 평균값을 비교하는 방법이 있습니다. axis=0으로 지정하여 픽셀 10,000개에 대한 평균값을 막대그래프로 그리면 다음과 같습니다.

✅ 평균값과 가가운 사진 고르기

사과 사진의 평균값인 apple_mean과 가장 가까운 사진을 절댓값 오차를 사용하여 골라보겠습니다. fruits 배열에 있는 모든 샘플에서 apple_mean을 뺀 절댓값의 평균을 계산하면 됩니다.

apple_mean과 가장 가까운 사진 100개를 골랐더니 모두 사과입니다.

 

흑백 사진에 있는 픽셀값을 사용해 과일 사진을 모으는 작업을 해 보았습니다. 이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을 군집이라고 합니다. 군집은 대표적인 비지도 학습 작업 중 하나입니다. 군집 알고리즘에서 만든 그룹을 클러스터라고 부릅니다.

 

하지만 우리는 이미 사과, 파인애플, 바나나가 있다는 것을 알고 있었습니다. 실제 비지도 학습에서는 타깃값을 모르기 때문에 이처럼 샘플의 평균값을 미리 구할 수 없습니다.

✅ 비슷한 샘플끼리 모으기

타깃값이 없을 때 데이터에 있는 패턴을 찾거나 데이터 구조를 파악하는 머신러닝 방식을 비지도 학습이라고 합니다. 타깃이 없기 때문에 알고리즘을 직접적으로 가르칠 수가 없죠. 대신 알고리즘은 스스로 데이터가 어떻게 구성되어 있는지 분석합니다.

#과일 사진 데이터 준비하기
!wget https://bit.ly/fruits_300 0- fruits_300.npy

import numpy as np
import matplotlib.pyplot as plt

fruits = np.load('fruits_300.2')

print(fruits.shape)

print(fruits[0, 0, :])

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

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

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

#픽셀 값 분석하기
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)

print(apple.mean(axis=1))

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

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

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

#평균값과 가까운 사진 고르기
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
print(abs_mean.shape)

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

06-2 k-평균

사진에 어떤 과일이 들어 있는지 알지 못할 때 어떻게 평균값을 구할 수 있을까요? 바로 k-평균 군집 알고리즘이 평균값을 자동으로 찾아줍니다. 이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심 또는 센트로이드라고 부릅니다.

 

✅ k-평균 알고리즘 소개

k-평균 알고리즘의 작동 방식은 다음과 같습니다.

➡️ 무작위로 k개의 클러스터 중심을 정합니다.

➡️ 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정합니다.

➡️ 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경합니다.

➡️ 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복합니다.

✅ KMeans 클래스

사이킷런의 k-평균 알고리즘은 sklearn.cluster 모듈 아래 KMeans 클래스에 구현되어 있습니다. 이 클래스에서 설정할 매개변수는 클러스터 개수를 지정하는 n_clusters입니다.

✅ 클러스터 중심

KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있습니다.

✅ 최적의 k 찾기

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

 

사실 군집 알고리즘에서 적절한 k 값을 찾기 위한 완벽한 방법은 없습니다. 여기서는 적절한 클러스터 개수를 찾기 위한 대표적인 방법인 엘보우 방법에 대해 알아보겠습니다.

 

k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있습니다. 이 거리의 제곱 합을 이너셔라고 부릅니다. 이너셔는 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값으로 생각할 수 있습니다. 엘보우 방법은 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법입니다.

!wget https://bit.ly/fruits_300 0- fruits_300.npy

import numpy as np
fruits = np.load('fruits_300')
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))

import matplotlib.pyplot as plt
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(fruits[km.labels_==0])
  
draw_fruits(fruits[km.labels_==1])

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

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

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

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

draw_fruits(fruits[100:101])

print(km.n_iter_)

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

06-3 주성분 분석

✅ 차원과 차원 축소

지금까지 우리는 데이터가 가진 속성을 특성이라 불렀습니다. 머신러닝에서는 이런 특성을 차원이라고도 부릅니다. 10,000개의 특성은 결국 10,000개의 차원이라는 건데 이 차원을 줄일 수 있다면 저장 공간을 크게 절약할 수 있을 것입니다.

배열과 벡터에서의 차원

✅ 주성분 분석 소개

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

직관적으로 우리는 길게 늘어진 대각선 방향이 분산이 가장 크다고 알 수 있습니다. 이 직선이 원점에서 출발한다면 두 원소로 이루어진 벡터로 쓸 수 있습니다. 예를 들어 다음 그림의(2, 1)처럼 나타낼 수 있겠죠

이 벡터를 주성분이라고 부릅니다. 이 주성분 벡터를 원본 데이터에 있는 어떤 방향입니다. 따라서 주성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같습니다. 하지만 원본 데이터는 주성분을 사용해 차원을 줄일 수 있습니다.

주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어듭니다!!

 

첫 번째 주성분을 찾은 다음 이 벡터에 수직이고 분산이 가장 큰 다음 방향을 찾습니다. 이 벡터가 두번째 주성분입니다. 여기서는 2차원이기 때문에 두 번째 주성분의 방향은 다음처럼 하나뿐입니다.

일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있습니다.

✅ PCA 클래스

사이킷런은 sklearn.decomposition 모듈 아래 PCA 클래스로 주성분 분석 알고리즘을 제공합니다. PCA 클래스의 객체를 만들 때 n_components 매개변수에 주성분의 개수를 지정해야 합니다.

✅ 원본 데이터 재구성

10,000개의 특성을 50개로 줄이면 어느 정도 손실이 발생할 수밖에 없습니다. 하지만 최대한 분산이 큰 방향으로 데이터를 투영했기 때문에 원본 데이터를 상당 부분 재구성할 수 있습니다. PCA 클래스는 이를 위해 inverse_transform() 메서드를 제공합니다.

✅ 설명된 분산

주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값을 설명된 분산이라고 합니다. PCA 클래스의 explained_variance_ratio_에 각 주성분의 설명된 분산 비율이 기록되어 있습니다. 당연히 첫 번째 주성분의 설명된 분산이 가장 큽니다.

✅ 다른 알고리즘과 함께 사용하기

과일 사진 원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해 보고 어떤 차이가 있는지 알아보겠습니다. 3개의 과일 사진을 분류해야 하므로 간단히 로지스틱 회귀 모델을 사용하겠습니다.

 

50개의 특성만 사용했을 때 정확도가 100%이고 훈련 시간은 0.03초로 20배 이상 감소했습니다. PCA로 훈련 데이터의 차원을 축소하면 저장 공간뿐만 아니라 머신러닝 모델의 훈련 속도도 높일 수 있습니다.

 

각 클러스터의 산점도가 아주 잘 구분됩니다. 이 그림을 보면 사과와 파인애플 클러스터의 경계가 가깝게 붙어 있습니다. 이 두 클러스터의 샘플은 몇 개가 혼동을 일으키기 쉬울 것 같군요.

!wget https://bit.ly/fruits_300 0- fruits_300.npy
import numpy as np
fruits = np.load('fruits_300.1')
fruits_2d = fruits.reshape(-1, 100*100)

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

print(pca.components_.shape)

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

print(fruits_2d.shape)

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

fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)

fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
  draw_fruits(fruits_reconstruct[start:start+100])
  print("\n")

print(np.sum(pca.explained_variance_ratio_))

plt.plot(pca.explained_variance_ratio_)

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()

target = np.array([0]*100 + [1]*100 + [2]*100)

from sklearn.model_selection import cross_validate
scores = cross_validate(lr, fruits_2d, target)
print(np.mean(scores['test_score']))
print (np.mean(scores['fit_time']))

scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))

pca = PCA(n_components=0.5)
pca.fit(fruits_2d)

print(pca.n_components_)

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

scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))

from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_pca)
print(np.unique(km.labels_, return_counts=True))

for label in range(0, 3):
  draw_fruits(fruits[km.labels_ == label])
  print("\n")

for label in range(0, 3):
  data = fruits_pca[km.labels_ == label]
  plt.scatter(data[:,0], data[:,1])
plt.legend(['pineapple', 'banana', 'apple'])
plt.show()