이미지로 존재하는 과일 데이터를 종류별로 분류할 수 있을까? ...사진의 픽셀값을 모두 평균 내면 비슷한 과일끼리 모이지 않을까??
<타깃, 정답이 없는 머신러닝 알고리즘. '비지도 학습'>
<군집 알고리즘>
1. 과일 데이터 준비하기
import numpy as np
import matplotlib.pyplot as plt
!wget https://bit.ly/fruits_300 _0 fruits_300.npy
fruits = np.load('fruits_300')
print(fruits.shape)
# (300, 100, 100)
배열의 첫 번째 차원(300)은 샘플의 개수를 나타내고, 두 번째 차원(100)은 이미지 높이, 세 번째 차원(100)은 이미지 너비이다. 이미지 크기는 100 * 100이다. 각 픽셀은 넘파이 배열의 원소 하나에 대응하고, 각 배열의 크기가 100 * 100임을 알 수 있다.
반전된 배열의 이미지를 출력한 코드인 cmap = 'gray'를 제외하고, 이미지의 밝은 부분이 출력한 픽셀 배열에서 0에 가깝고, 짙은 부분일수록 255에 가까운 값이라는 점을 알 수 있다.
fig, axs = plt.subplots(1, 2) # 맷플롯립의 subplots() 함수. 여러 개의 그래프를 배열처럼 쌓을 수 있게 해줌. (1, 2)는 각각 1
axs[0].imshow(fruits[100], cmap = 'gray_r') # 행과 2열이다. axs[0], axs[1]은 각각의 그래프를 담고 있는 배열로, 파인애플과 바나나를 그린
axs[1].imshow(fruits[200], cmap = 'gray_r') # 다.
plt.show()
3. 픽셀값 분석하기
fruits 데이터는 숫자들로 이루어진 픽셀값의 2차원 배열임을 알았다. 그렇기 때문에, 100 * 100 형태의 이미지를 펼쳐서 길이가 10,000인 1차원 배열을 만드는 것이 가능하다!
마찬가지로 사과, 파인애플, 바나나 순이다. 픽셀의 위치에 따라 값의 크기가 다르기 때문에, 이 대표 이미지와 가까운 사진을 골라낸다면 각각의 과일을 분류할수 있다.
5. 평균값과 가까운 사진 고르기
fruits 배열에 있는 모든 샘플에서 apple_mean을 뺀 절댓값의 평균을 구하면, 사진의 평균값인 apple_mean과 가장 가까운 사진을 고를 수 있다.
abs_diff = np.abs(fruits - apple_mean) # abs_diff 는 (300, 100, 100) 크기의 배열. 각 샘플에 대한 평균을 구하기 위해 axis에
abs_mean = np.mean(abs_diff, axis = (1, 2)) # (1, 2) 즉, 두 번째, 세 번째 차원을 모두 지정해줌.
print(abs_mean.shape)
# (300,) > 계산한 abs_mean은 각 샘플의 오차 평균. 크기가 (300,)인 1차원 배열이 됨.
apple_index = np.argsort(abs_mean)[:100] # argsort() 함수: 가장 작은 것에서부터 큰 순서대로, apple_mean과 오차가 가장 작은 샘플 fig, axs = plt.subplots(10, 10, figsize = (10, 10)) # 100개를 고르고, 해당 인덱스를 반환함.
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()
apple_mean과 가장 가까운 사진 100개를 골랐더니 모두 사과가 나왔다. 이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을'군집화(clustering)'이라고 한다. 대표적인 비지도 학습 작업 중 하나로, 이렇게 군집 알고리즘에서 만들어진 그룹을 일컬어 군집, 클러스터(cluster)라고 부른다.
우리는 이미 사과와 파인애플, 바나나가 데이터셋에 있음을 알고 있었다. 즉 타깃값(정답값)을 알고 있었다는 말과도 같다. 덕분에 각각의 과일이 있는 사진 평균값을 계산해 가장 가까운 과일을 찾을 수 있었다. 하지만 실제 비지도 학습에서는 타깃값을 알지 못하기 때문에 샘플의 평균값을 미리 구할 수 없다. 그렇다면 타깃값을 알지 못한 채 세 과일의 평균값은 구하려면 어떻게 해야할까?
<k - 평균>
비지도 학습에서는 타깃값을 알지 못한다. 어떻게 평균값을 구할 수 있을까? 바로 k -평균 군집 알고리즘이 있다. 이 평균값은 클러스터의 중심에 위치하기 때문에, '클러스터 중심' 혹은 '센트로이드'라고 부른다. 이제 그 알고리즘을 살펴보자.
3개의 클러스터 중심(빨간 점)을 랜덤하게 지정한다. 클러스터 중심에서 가장 가까운 샘플을 하나의 클러스터로 묶는다. 왼쪽 위부터 시계 방향으로 바나나 2개와 사과 1개 클러스터, 바나나 1개와 파인애플 2개 클러스터, 사과 2개와 파인애플 1개 클러스터가 만들어진다.
그다음 클러스터의 중심을 다시 계산해 이동시킨다. 맨 아래 클러스터는 사과 쪽으로 중심이 조금 더 이동하고 왼쪽 위의 클러스터는 바나나 쪽으로 중심이 더 이동하는 식이다. 클러스터 중심을 다시 계산했다면, 다시 가장 가까운 샘플을 다시 클러스터로 묶는다. 이제 3개의 클러스터에는 바나나와 파인애플, 사과가 3개씩 올바르게 묶여있다. 올바르게 묶여있다면, 클러스터 중심을 다시 한가운데로 이동시킨다.
이동된 클러스터 중심에서 다시 한번 가장 가까운 샘플을 클러스터로 묶는다. 중심에서 가장 가까운 샘플은 이전 클러스터와 동일하다. 따라서 만들어진 클러스터에 변동이 없으므로 k-평균 알고리즘을 종료한다.
설명이 장황할 수 있어 내부 과정은 따로 정리했다.k-평균 알고리즘은 처음엔 랜덤하게 클러스터 중심을 선택하고 점차 가장 가까운 샘플의 중심으로 이동하는 간단한 알고리즘이라 비지도학습 과정에서 많이 사용된다.
첫 번째 클러스터(0 레이블)에선 111개의 샘플을 모았고, 두 번째 클러스터(1 레이블)가 98개, 세 번째 클러스터(2 레이블)가 91개의 샘플을 모았다. 각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력해보자.
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(row, 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)는 (샘플의 개수, 너비, 높이)의 3차원 배열을 입력받아 가로로 10개씩 이미지를 출력한다. 개수에 따라 행과 열의 개수를 계산하고, figsizeㄹ르 지정한다. 그다움 2중 for 문을 사용해 먼저 첫 번째 행의 이미지를 그리고, 두 번째 행의 이미지를 그리는 식으로 계속된다. 이제 불리언 인덱싱으로 km.labels_ 배열에서 값이 1인 위치의 과일 사진을 모두 그려보자.
draw_fruits(fruits[km.labels_ == 1])
레이블 1로 클러스팅된 98개의 이미지가 모두 출력되었다. 다른 레이블도 모두 확인해보자.
draw_fruits(fruits[km.labels_ == 2])
draw_fruits(fruits[km.labels_ == 0])
레이블이 0인 클러스터는 파인애플과 바나나, 사과를 완벽하게는 구별하지 못하는 모습이다.
2. 클러스터 중심
KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있다. 이 배열은 fruits_2d 샘플의 클러스터 중심이기 때문에 이미지로 출력하면 100 * 100 크기의 2차원 배열로 바꿔야 한다.
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio = 3)
KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해주는 transform() 메서드가 있다. 특성 공학에서 활용했던 StandardScaler처럼 특성값을 변환하는 도구로 활용이 가능하다. 인덱스가 100인 샘플에 transform() 메서드를 적용해보면, fit()과 마찬가지로 2차원 배열을 넣어야 한다. 슬라이싱 연산자를 사용해 (1, 10000) 크기의 배열을 전달해보자.
하나의 샘플을 전달했기 때문에, 반환된 배열은 크기가 (1, 클러스터 개수)인 2차원 배열이다. 여기서 우리는 책과는 다르게 첫 번째(0번째 인덱스) 클러스터까지의 거리가 3393.8136117로 가장 가까운 것을 알 수 있다. 그렇다면 해당 클러스터의 중심이 위치한 클래스는 어떤 것으로 예측했을까?
print(km.predict(fruits_2d[100:101]))
# [0]
draw_fruits(fruits[100:101])
파인애플
k -평균 알고리즘은 반복적으로 클러스터 중심을 옮기면서 최적의 클러스터를 찾는다. 알고리즘이 반복한 횟수는 KMeans 클래스의 n_iter_속성에 저장된다. 여기서 우리는 비록 타깃값은 사용하지 않았지만, n_cluster를 3으로 지정한 뒤 타깃에 대한 정보를 일부 제공했다. 원래라면 클러스터의 개수조차 알 수 없기에, 최적의 클러스터 개수를 찾는 과정을 거쳐야만 한다.
print(km.n_iter_)
# 4
3. 최적의 k 찾기
k-평균 알고리즘의 단점은 클러스터 개수를 사전에 지정해야 한다는 점이다. 실제로 비지도 학습을 진행할 때 우리는 몇 개의 클러스터가 있는지 알 수 없다. 그렇다면, 어떻게 적절한 k값을 찾아야 할까?
> 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.show()
위 그래프에선 명확하지는 않지만, k = 3에서 그래프의 기울기가 조금 바뀐 것을 알 수 있다. 엘보우 지점보다 클러스터 개수가 많아지면 이너셔의 변화가 줄어들면서 군집 효과도 줄어든다.
<주성분 분석>
우리는 데이터가 가진 속성을 가리켜 '특성'이라고 불렀다. 과일 사진의 경우 10,000개의 픽셀이 있기에, 10,000개의 특성이 있다고도 말한다. 머신러닝에서는 이런 특성을 '차원'(dimension)이라고 부른다. 10,000개의 특성은 결국 10,000개의 차원이라는 건데, 이 차원을 줄인다면 저장공간 또한 크게 절약이 가능하다!
이를 위해 비지도 학습 작업 중 하나인 차원 축소 알고리즘을 다뤄보자. 앞 장에서 특성이 많으면 많을수록 선형 모델의 성능이 높아지고 훈련 데이터에 과대적합되기 쉽다고 배웠다. 차원 축소는 데이터를 가장 잘 나타내는 일부 특성을 선택해 데이터의 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법이다.
또한 줄어든 차원에서 다시 원본 차원으로 손실을 최대한 줄이면서 복원도 가능하다. 이번 장에선 '주성분 분석'이라고도 알려진 Principal component analysis를 배울 것이다. 이를 간단히 'PCA'라고 부른다.
주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값을 '설명된 분산'이라고 한다. PCA 클래스의 explained_variance_ratio_에 각 주성분의 설명된 분산 비율이 기록되어 있는데, 첫 번째 주성분의 설명된 분산이 가장 크다. 이 분산 비율을 모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있다.
92% 가 넘는 분산을 유지하고 있다. 앞서 50개의 특성에서 원본 데이터를 복원했을 때 높은 복원률을 보였던 이유가 여기에 있다. 그래프를 보면 처음 10개의 주성분이 대부분의 분산을 표현하고 있다. 그 다음부터는 각 주성분이 설명하고 있는 분산은 비교적 작다. 원본 데이터를 사용했을 때와는 어떻게 다른지 살펴보자.
# 타 알고리즘과 사용하기/느낀 점
시각화는 알고리즘의 원만한 구현과 진행 현황을 잘 도출시켜준다는 점에서 의미가 크다. 그렇게 함으로써 앞으로의 진행에 또다른 인사이트를 제공하고, 새로운 문제를 발견하는데 큰 도움을 주기 때문이다. 비지도 학습의 과정에서 '시각화'와 데이터의 구체화 중요성을 깨달았다. 물론 책이 던지고자 한 주제와 일맥상통하지는 않지만, 이 또한 기억해야 할 중요한 맹점이다.