

타깃을 모르는 사진들을 분류하려고 한다
이렇게 타깃이 없을 때 사용하는 머신러닝 알고리즘을 비지도 학습이라고 한다
사람이 가르쳐주지 않아도 데이터에 있는 무언가를 학습하는 것을 의미하죠
그래서 이번에는 사과, 바나나, 파인애플을 담고 있는
흑백사진을 이용해서 한 번 진행해보도록 하겠습니다
(해당 데이터는 캐글에서 공개된 데이터셋이다)
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
import numpy as np
import matplotlib.pyplot as plt
fruits = np.load('fruits_300.npy')
print(fruits.shape) # (300, 100, 100)
넘파이에서 npy 파일을 로드하는 방법은 load() 메서드에 파일이름을 넣는 것이다
파일의 크기를 확인해보면 (300, 100, 100)으로 출력이 되는데
첫 번째 차원 300은 샘플의 개수를 의미하고 두 번째는 이미지의 높이
세 번째는 이미지의 너비를 의미한다 정리를 하면 이미지의 크기는 100x100
각 픽셀은 넘파이 배열에 원소 하나에 대응한다
즉 배열의 크기라 100x100이라는 뜻이다
print(fruits[0,0,:])

다음과 같이 출력을 하게 되면 첫 번째 이미지의 첫 번째 행을 모두 확인할 수 있다
그래서 첫번째 행에 있는 픽셀 100개에 있는 값을 출력했다
이 넘파이 배열은 흑백사진을 담고 있으므로 0~255까지의 정수값을 가진다
이번에는 사진을 출력해보자 맷플롯립의 imshow() 함수를 사용하면 쉽게 그릴 수 있다
(흑백 이미지이므로 cmap 매개변수를 'gray'로 지정했다)
plt.imshow(fruits[0],cmap='gray')
plt.show()

픽셀에 있는 값들은 0에 가까울수록 어둡고 숫자가 높을수록 밝은 색깔을 나타낸다
이 흑백이미지는 사진으로 찍은 이미지를 넘파이 배열로 변환할 때 반전시킨것으로
사진의 흰 바탕(높은값)은 검은색(낮은값)으로 만들고
짙은 부분(낮은값)은 밝은색(높은색)으로 바꾸었다
이렇게 바꾼이유는 우리가 관심이 있는 것은 사과이기 때문이다
원래의 흰색 바탕은 중요하지 않지만 컴퓨터는 255에 가까운 바탕에 집중하기에
바탕을 검게 만들고 사진에 짙게 나온 사과를 밝은 색으로 만들었다
(알고리즘이 어떤 출력을 만들 때 곱셉, 덧셈을 하는데 0이면 아무 의미가 없다
하지만 픽셀값이 높으면 출력값도 커지기 때문에 다음과 같이 바꾼다)
우리가 보는 것과 컴퓨터가 처리하는 방식이 다르기 때문에
우리는 종종 흑백 이미지를 반전시켜서 이렇게 사용을 한다
관심 대상의 영역을 높은 값으로 바꾸었지만 맷플롯립으로 출력할 때
바탕이 검게 나오므로 보기에는 좋지 않다
그래서 cmap 매개변수를 'gray_r'로 지정하면 다시 반전하여 우리 눈에는 좋게 출력된다
plt.imshow(fruits[0],cmap='gray_r')
plt.show()

이 그림에는 사과, 바나나, 파인애플이 각각 100개씩 있으니 다른 것도 출력해보자
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 함수를 사용하면 여러개의 그래프를 배열처럼 쌓을 수 있게 도와준다
subplot(1,2)는 한 개의 행에 두 개의 열을 지정했다는 뜻이다
반환된 axs는 2개의 서브 그래프를 담고 있는 배열로 axs[0]는 파인애플을
axs[1]에는 바나나 이미지를 그렸다
해당 데이터는 처음 100개는 사과, 그리고 100개는 파인애플,
마지막 100개는 바나나로 구성되어 있다
이 데이터를 각각의 종류별로 나눠보겠다
넘파이 배열을 나눌 때 100x100 이미지를 펼쳐서 길이가 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)
fruits 배열에서 순서대로 100개씩 선택하기 위해 슬라이싱 연산자를 활용한다
그 다음 reshape() 메서드를 활용해서 두 번째 차원과 세 번째 차원을 합친다
첫번째 차원을 -1로 지정하면 자동으로 나머지 남은 차원을 할당한다
여기서는 첫 번째 차원이 샘플의 개수이다
이제 apple, pineapple, banana 배열의 크기는 (100,10000) 이다
print(apple.shape) # (100, 10000)
이제 각 배열에 들어있는 샘플의 픽셀 평균값을 구해볼 것이다
넘파이의 mean() 메서드를 사용할 것이다
샘플마다 픽셀의 평균값을 계산해야 하므로 평균을 계산할 축을 설정해야 하는데
axis=0으로 설정하면 첫 번째 축인 행을 따라 계산하고
axis=1로 지정하면 두 번째 축인 열을 따라 계산한다
우리가 사진의 픽셀값을 모두 평균 내는 이유는
모두 평균을 내면 비슷한 과일끼리 모일 수 있다고 가정했기 때문이다
샘플은 모두 가로로 값을 나열했으니 axis=1로 지정하여 평균을 계산하자
평균을 계산하는 넘파이 np.mean() 함수를 사용해도 괜찮지만
넘파이 배열은 이런 함수를 메서드로도 제공한다
apple 배열의 mean() 메서드로 각 샘플의 픽셀 평균값을 계산해보자
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()
맷플롯립의 hist() 함수를 사용해서 히스토그램을 그렸다
alpha 매개변수를 1보다 작게 하면 투명도를 줄 수 있다
또 맷플롯립의 legend() 함수를 사용하면
어떤 과일의 히스토그램인지 범례도 만들 수 있다

히스토그램을 보면 바나나 사진의 평균값은 40 아래에 집중되어 있다
사과와 파인애플은 90~100 사이에 많이 모여 있다
이렇게 사과 파인애플은 많이 겹쳐 있어서 픽셀값만으로는 구분하기 쉽지 않다
그렇다면 샘플의 평균값이 아니라 픽셀별 평균값을 비교해보면 어떨까?
전체 샘플에 대해 각 픽셀의 평균을 구하는 것이다
세 과일은 모양이 다르므로 픽셀값이 높은 위치가 다를 것이다
픽셀의 평균을 구하는 것도 간단하다 axis=0으로 지정하면 된다
이번에는 맷플롯립의 bar() 함수를 사용해서 막대그래프를 그려보고
subplots() 함수로 3개의 서브 그래프를 만들었다
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()

순서대로 사과, 파인애플, 바나나 그래프이다
3개의 그래프를 보면 과일마다 높은 구간이 다르다
그렇다면 픽셀 평균값을 100x100 크기로 바꿔서 이미지처럼 출력하여
위 그래프와 비교하면 편하다 픽셀을 평균 낸 이미지를
모든 사진을 합쳐 놓은 대표 이미지로 생각할 수 있다
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()
세 과일은 픽셀 위치에 따라 값의 크기가 차이가 난다
따라서 이 대표 이미지와 가까운 사진을 골라낸다면
사과, 파인애플, 바나나를 구분할 수 있지 않을까?

이제 사과 사진의 평균값인 apple_mean과 가장 가까운 사진을 골라보자
fruits 배열에 있는 모든 샘플에서 apple_mean을 뺀 절댓값의 평균을 계산해볼 것이다
넘파이의 abs() 함수는 절댓값을 계산하는 함수이다
배열을 입력하면 모든 원소의 절댓값을 계산하여 입력과 동일한 크기의 배열을 반환한다
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
print(abs_mean.shape) # (300,)
그리고 나서 abs_diff는 (300,100,100) 크기의 배열이다
따라서 각 샘플에 대한 평균을 구하기 위해 axis에 두 번째, 세 번째 차원을 모두 지정했다
이렇게 계산한 샘플은 오차평균이므로 크기가 (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()
이제는 이 값이 가장 작은 순서대로 100개를 골라보자
np.argsort() 함수는 작은 것에서 큰 순서대로 나열한
abs_mean 배열의 인덱스를 반환한다
이 인덱스 중에서 처음 100개를 선택해 10 x 10 격자로 이루어진 그래프를 그렸다

apple_mean과 가장 가까운 사진을 골랐더니 100개 모두 사과이다 완벽하다
subplot() 함수로 10 x 10, 총 100개의 서브 그래프를 만들었다
그리고 그래프가 많기 때문에
그래프의 크기를 figsize= (10,10)으로 조금 크게 지정했다
그런 다음 이중 for 반복문으로 순회하면서 10개의 행과 열에 이미지를 출력했다
그리고 깔끔하게 이미지만 보이기 위해서
axis('off') 사용하여 좌표축을 그리지 않았다
오늘 이 시간에는 흑백 사진에 있는 픽셀값을 사용해 과일 사진을 모으는 작업을 진행했다
이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을 군집(clustering)이라고 한다
군집은 대표적인 비지도 학습 작업 중 하나로
군집 알고리즘에서 만든 그룹을 클러스터(cluster)라고 한다
하지만 우리는 이미 사과, 파인애플, 바나나가 있었다는 것을 알고 있었다
즉 타깃값을 알고 있었기 때문에 사진 평균값을 계산해서 가장 가까운 과일을 찾을 수 있었다
실제 비지도 학습에서는 타깃값을 모르기 때문에 이처럼 샘플의 평균값을 미리 구할 수 없다
그러면 어떻게 값을 구할까?
바로 k-평균 알고리즘으로 알아보도록 하자
비지도 학습은 머신러닝의 한 종류로 훈련 데이터에 타깃이 없다
타깃이 없기 때문에 외부의 도움없이 스스로 유용한 무언가를 학습해야 한다
대표적인 비지도 학습 작업은 군집, 차원 축소등이 있다
군집은 비슷한 샘플끼리 하나의 그룹으로 모으는 대표적인 비지도 학습 작업
군집 알고리즘으로 모은 샘플 그룹을 우리는 클러스터라고 한다
우리는 방금전에 과일들에 있는 각 픽셀의 평균값을 구해서 가장 가까운 값을 구했다
하지만 이 경우에는 우리가 미리 타깃을 알고 있었기에 과일의 평균을 구할 수 있었다
하지만 비지도 학습에서는 사진에 어떤 과일이 있는지를 알지 못한다
이런 경우에는 K-평균 군집 알고리즘을 사용하면 평균값을 자동으로 찾아준다
이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심 또는
센트로이드(centroid)라고 부른다
k-평균 알고리즘의 작동방식은 다음과 같다
- 무작위로 k개의 클러스터 중심을 정한다
- 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정
- 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다
- 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다
k-평균 알고리즘은 처음에는 랜덤하게 클러스터 중심을 선택하고
점차 가장 가까운 샘플의 중심으로 이동하는 알고리즘이다
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
wget 명령어를 통해서 데이터를 다운받고,
넘파이 np.load() 함수를 사용해서 npy 파일을 읽어서 넘파이 배열을 준비한다
k-평균 모델을 훈련하기 위해 (샘플 개수, 너비, 높이) 크기의 3차원 배열을
(샘플 개수, 너비x높이) 크기를 가진 2차원 배열로 변경했다
사이킷런의 k-평균 알고리즘은 sklearn.cluster
모듈 아래에 KMeans 클래스에 구현되어 있다
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)
클러스터의 개수를 지정하는 매개변수는
n_clusters로 여기에서는 클러스터 개수를 3으로 설정했다
군집된 결과는 KMeans 클래스 객체의 labels_ 속성에 저장된다
labels_ 배열의 길이는 샘플의 개수와 같다
이 배열은 각 샘플이 어떤 레이블에 해당하는지를 나타내주는데
우리가 n_clusters를 3으로 지정했기에 배열의 값은 0,1,2 중에 하나이다
print(km.labels_)

이렇게 출력을 하면 한눈에 보기가 어려우니 0,1,2 각각의 개수를 확인해보자
print(np.unique(km.labels_, return_counts=True))
# (array([0, 1, 2], dtype=int32), array([112, 98, 90]))
첫번째 클러스터는 112개, 두번째는 98개, 마지막은 90개를 모은 것을 알 수 있다
그러면 각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력해보자
import matplotlib.pyplot as plt
def draw_fruits(arr, ratio=1):
n = len(arr) # n은 샘플의 개수
# 한 줄에 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: # 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_==0 이라고 하면
배열에서 값이 0인 위치는 True가 되고, 그 외는 모두 False가 된다
넘파이는 이런 불리언 배열을 사용해서 원소를 선택할 수 있는데
이를 불리언 인덱싱이라고 한다
draw_fruits(fruits[km.labels_==0])

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

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

k-평균 알고리즘이 샘플들을 완벽하게 구분하지는 못했지만
훈련 데이터에 타깃 레이블을 전혀 제공하지 않았음에도
스스로 비슷한 샘플들을 잘 모은 것을 확인할 수 있다
KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 있다
이 배열은 fruits_2d 샘플의 클러스터 중심이기에 이미지로 출력하려면
100x100 2차원 배열로 바꿔야 한다
draw_fruits(km.cluster_centers_.reshape(-1,100,100), ratio=3)

KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해 주는
transform() 메서드를 가지고 있다 transform() 메서드가 있다는 것은
마치 StandardScaler 클래스처럼 특성값을 변환하는 도구로 사용할 수 있다는 의미이다
예를 들어서 인덱스가 100인 샘플에 transform() 메서드를 사용해보자
fit() 메서드와 마찬가지로 2차원 배열을 넣어야한다
print(km.transform(fruits_2d[100:101]))
# [[3400.24197319 8837.37750892 5279.33763699]]
하나의 샘플을 전달했기 때문에 반환된 배열은 크기가 (1, 클러스터 개수)인 2차원 배열이다
첫 번째 클러스터(레이블 0), 두 번째 클러스터(레이블 1), 세 번째 클러스터(레이블3)이
각각 첫 번째 원소, 두 번째 원소의 값, 세 번째 원소의 값이다
이것들 중에서 0이 가장 가깝다 그래서 이 샘플은 0에 속한 것 같다
KMeans 클래스는 가장 가까운 클러스터 중심을
예측 클래스로 출력하는 predict() 메서드를 제공한다
print(km.predict(fruits_2d[100:101])) # [0]
draw_fruits(fruits[100:101])

k-평균 알고리즘은 반복적으로 클러스터의 중심을 옮기면서 최적의 클러스터를 찾는다
그리고 이 알고리즘이 반복된 횟수는 KMeans 클래스의 n_iter_ 속성에 저장된다
print(km.n_iter_) # 4
우리는 클러스터 중심을 특성 공학처럼 사용하여 데이터셋을 저차원으로 바꿀 수 있다
또는 가장 가까운 거리에 있는 클러스터 중심을 샘플의 예측값으로 사용할 수 있다
그런데 우리는 사실 편법을 저질렀다
바로 n_clusters가 3이라고 지정을 했기 때문이다
실전에서는 클러스터의 개수도 알기가 힘들다
그러면 이 클러스터의 개수는 어떻게 지정해야 할까?
사실 군집 알고리즘에서 적절한 k값을 찾기 위한 완벽한 방법은 없다
몇가지 도구가 있지만 다 장단점이 있다
우리는 여기에서 적절한 클러스터 개수를 찾기 위한 대표적인 방법으로
엘보우(ellbow) 방식을 사용해볼 것이다
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('intertia')
plt.show()
KMeans 클래스는 자동으로 이니셔를 계산해서 inertia_ 속성으로 제공하고 있다
그래서 클러스터 개수를 2~6까지 바꿔가면서 KMeans 클래스를 5번 훈련시키고
inertia_ 속성에 저장된 이너셔값을 inertia에 추가했다
마지막으로 그래프로 이를 출력했다

해당 그래프에서는 k=3에서 그래프의 기울기가 바뀐 것을 알 수 있다
엘보우 지점보다 클러스터 개수가 많아지게 되면 이너셔의 변화가 줄어들면서
군집효과도 줄어들게 된다 (하지만 이 그래프에서는 명확하지는 않다)
우리는 과일 종류별로 픽셀 평균값을 계산했었다
하지만 실전에서는 어떤 과일이 들어올지는 아무도 모른다
그래서 타깃값을 모르고 자동으로 클러스터로 모을 수 있는 군집 알고리즘이 필요했다
거기에 대표적인 k-평균 알고리즘을 사용했다
k-평균 알고리즘은 비교적 간단하고 속도가 빠르며 이해하기도 쉽다
k-평균 알고리즘을 구현한 사이킷런의 KMeans 클래스는
각 샘플이 어떤 클러스터에 소속되어 있는지를 labels_ 속성에 저장한다
또 각 샘플에서 각 클러스터까지의 거리를 하나의 특성으로 활용할 수도 있는데
이를 위해 KMeans에서는 transform() 메서드를 제공한다
또한 predict() 메서드를 사용하면 새로운 샘플에 대해
가장 가까운 클러스터를 예측값으로 출력한다
k-평균 알고리즘은 사전에 클러스터 개수를 미리 지정해야 한다
사실 직접 데이터를 확인하지 않고서는 몇 개의 클러스터가 만들어질지 알기 어렵다
최적의 클러스터 개수 k를 알아내는 한 가지 방법은 클러스터가 얼마나 밀집되어 있는지
나타내는 이너셔를 사용하는 것이다 이니셔가 더 이상 크게 줄지 않으면
클러스터 개수를 늘리는 것이 효과가 없다 우리는 이 방법을 엘보우라고 한다
사이킷런의 KMeans 클래스는 자동으로 이니셔를 계산해서 inertia_ 속성으로 제공한다
클러스터 개수를 늘리면서 반복하여 KMeans 알고리즘을 훈련하고
이니셔가 줄어드는 속도가 꺽이는 지점을 최적의 클러스터 개수로 결정한다
k-평균 알고리즘은 처음에 랜덤하게 클러스터 중심을 정하고 클러스터를 만든다
그 다음 클러스터의 중심을 이동하고 다시 클러스터를 만드는 식으로 반복해서
최적의 클러스터를 구성하는 알고리즘이다클러스터 중심은 k-평균 알고리즘이 만든 클러스터에 속한 샘플의 특성 평균값이다
센트로이드라고 부르기도 하며 가장 가까운 클러스터 중심을 샘플의 또 다른 특성으로
사용하거나 새로운 샘플에 대한 예측으로 활용할 수 있다엘보우 방식은 최적의 클러스터 개수를 정하는 방법 중에 하나이다
이너셔는 클러스터 중심과 샘플 사이의 거리의 제곱합이다 클러스터 개수에 따라
이너셔 감소가 꺽이는 지점이 적절한 클러스터 개수 k개가 될 수 있다
이 그래프의 모양을 따서 우리는 엘보우 방식이라고 한다
지금까지 우리는 데이터 가진 속성을 특성이라고 불렀다
과일 사진의 경우 1만개의 픽셀이 있기 때문에 1만개의 특성이 있는 셈이다
머신러닝에서는 이런 특성을 차원(dimension)이라고 부른다
1만개의 특성은 결국 1만개의 차원이라는건데 이 차원을 줄일 수 있다면
저장 공간을 크게 줄일 수 있을 것이다
이를 위해 우리는 비지도 학습 작업 중 하나인 차원 축소 알고리즘을 해볼 것이다
특성이 많으면 선형 모델의 성능이 높아지고
훈련 데이터에 쉽게 과대적합된다는 것을 우리는 배웠다
차원축소는 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고
지도 학습 모델의 성능을 향상시킬 수 있는 매우 좋은 방법이다
또한 줄어든 차원에서 다시 원본 차원으로 손실을 최대한 줄이면서 복원할 수도 있다
그래서 우리는 이번에 대표적인 차원 축소 알고리즘인 주성분 분석
일명 PCA(Principal component analysis)를 배워볼 것이다
주성분 분석은 데이터에 있는 분산이 큰 방향을 찾는 것으로 이해를 할 수 있다
이 때 분산이 큰 방향이란 데이터를 잘 표현하는 어떤 벡터라고 생각을 하면 된다
우리가 2차원 데이터가 있다고 가정을 해보자
이 데이터는 x1 x2 두개의 특성이 있고, 대각선 방향으로 길게 늘어진 형태이다
그러면 이 데이터의 가장 분산이 큰 방향은 어디일까?
직관적으로 우리는 길게 늘어진 대각선 방향이 분산이 가장 크다고 할 수 있다
주성분 벡터는 원본 데이터에 있는 어떤 방향이다
따라서 주성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같다
하지만 원본 데이터는 주성분을 사용해 차원을 줄일 수 있다
주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어든다
주성분이 가장 분산이 큰 방향이기 때문에 주성분에 투영하여 바꾼 데이터는
원본이 가지고 있는 특성을 가장 잘 나타내고 있다
일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있다
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
import numpy as np
fruits =np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
일단 다시 과일 사진을 다운로드 하고 넘파이 배열로 적재를 했다
사이킷런은 sklearn.decomposition 모듈 아래에 PCA 클래스로
주성분 분석 알고리즘을 제공한다
PCA 클래스의 객체를 만들 때 n_components 매개변수에 주성분의 개수를
지정해야 한다 k-평균과 마찬가지로 비지도 학습이기 때문에
fit() 메서드에 타깃값을 제공하지 않는다
from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)
print(pca.components_.shape) # (50, 10000)
PCA 클래스가 찾은 주성분은 components_ 속성에 저장되어 있다
배열의 크기를 확인을 해보면 첫 번째 차원이 50이였다
즉 50개의 주성분을 찾은 것이다
두 번째 차원은 항상 원본 데이터의 특성 개수와 같은 10,000이다
원본 데이터와 차원이 같으므로 주성분을 100 x 100 크기의
이미지처럼 출력해볼 수 있다
위에 사용했던 draw_fruits 함수를 사용해서 출력해보겠다
draw_fruits(pca.components_.reshape(-1,100,100))

이 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다
한편으로는 데이터셋에 있는 어떤 특징을 잡아낸 것으로 생각할 수 있다
주성분으로 찾았으므로 원본 데이터에 주성분을 투영하여
특성의 개수를 10,000개에서 50개로 줄일 수 있다
이는 마치 원본 데이터를 각 주성분으로 분해하는 것으로 생각할 수 있다
PCA의 transform() 메서드를 사용해서 원본 데이터의 차원을 50으로 줄여보겠다
print(fruits_2d.shape) # (300, 10000)
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape) # (300, 50)
원래 fruits_2d는 (300,10000) 크기의 배열로
10000개의 픽셀(특성)을 가진 300개의 이미지였다
50개의 주성분을 찾은 PCA 모델을 사용해 (300,50) 크기의 배열로 변환했다
이제 fruit_pca 배열은 50개의 특성을 가진 데이터이다
이렇게 데이터 차원을 줄였지만 원하면 원상복구도 할 수 있다
한 번 알아보도록 하자
보통 데이터를 줄이면 정보의 손실이 발생한다
하지만 최대한 분산이 큰 방향으로 데이터를 투영했기에
원본 데이터를 상당 부분 재구성할 수가 있다
PCA 클래스를 이를 위해 inverse_transform() 메서드를 제공한다
아까 줄인 데이터를 다시 복구해보도록 하겠다
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape) # (300, 10000)
이 데이터를 100x100 크기로 바꾸어서 100개씩 나누어 출력해보겠다
fruits_reconstruct = fruits_inverse.reshape(-1,100,100)
for start in [0, 100, 200]:
draw_fruits(fruits_reconstruct[start:start+100])
print("\n")



확인을 해보면 거의 모든 과일이 잘 복원된 것을 확인할 수 있다
만약 주성분을 최대로 사용했다면 완벽하게 원본 데이터를 재구성할 수 있었을 것이다
그러면 50개의 특성은 얼마나 분산을 보존하고 있는 것일까?
주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지를
기록한 값을 설명된 분산 (explained variance)라고 한다
PCA 클래스의 explained_variance_ratio_에
각 주성분의 설명된 분산 비율이 기록되어 있다
당연하게 첫 번째 주성분의 설명된 분산이 가장 크고 이 분산 비율을
모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있다
print(np.sum(pca.explained_variance_ratio_)) # 0.921536598807397
92%가 넘는 분산을 유지하고 있다
앞에서 50개의 특서에서 원본 데이터를 복구했을 때
원본 이미지의 품질이 높았던 이유가 여기에 있다
설명된 분산 비율을 그래프로 그려보면
적절한 주성분의 개수를 찾는데 도움이 된다
plt.plot(pca.explained_variance_ratio_)
plt.show()

그래프를 보면 처음 10개의 주성분이 대부분의 분산을 표현하고 있다
그 다음부터는 각 주성분이 설명하고 있는 분산은 비교적 작다
이번에는 PCA로 차원 축소된 데이터를 사용하여 지도 학습 모델을 훈련보겠다
원본 데이터를 사용했을 때와 어떤 차이가 있는지를 알아보자
먼저 3개의 과일 사진을 분류해야 하므로 로지스틱 회귀 모델을 사용해보겠다
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
지도 학습 모델을 사용하려면 타깃값이 있어야 한다
여기에서는 사과를 0, 파인애플 1, 바나나를 2로 지정해보겠다
파이썬 리스트와 정수를 곱하면 리스트 안의 원소를 정수만큼 반복할 수 있다
target = np.array([0]*100 + [1]*100 + [2]*100)
먼저 원본 데이터인 fruits_2d를 사용하고
성능을 가늠해보기 위해서 cross_validate()로 교차 검증을 수행하겠다
from sklearn.model_selection import cross_validate
scores = cross_validate(lr, fruits_2d, target)
print(np.mean(scores['test_score'])) # 0.9966666666666667
print(np.mean(scores['fit_time'])) # 1.7283995151519775
교차 검증의 점수는 매우 높다
특성이 10000개나 되기 때문에 쉽게 과적합 모델이 탄생한다
cross_validate() 함수가 반환하는 딕셔너리에는 fit_time 항목에
각 교차 검증 폴드의 훈련 시간이 기록되어 있다
이 값을 PCA로 축소한 fruits_pca를 사용했을 때와 비교해보겠다
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score'])) # 0.9966666666666667
print(np.mean(scores['fit_time'])) # 0.07323875427246093
50개의 특성을 사용했음에도 정확도가 높고 훈련시간은 더 줄어들었다
PCA로 훈련 데이터의 차원을 축소하면 저장 공간뿐만 아니라
머신러닝 모델의 훈련 속도 높일 수 있다
앞서 PCA 클래스를 사용할 때 n_components 매개변수에
주성분의 개수를 지정했다 이 대신 원하는 설명된 분산의 비율을 입력할 수도 있다
PCA 클래스는 지정된 비율에 도달할 때까지 자동으로 주성분을 찾는다
설명된 분산의 50%에 달하는 주성분을 찾도록 PCA 모델을 만들어 보겠다
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)
print(pca.n_components_) # 2
방법은 간단하다 주성분 개수 대신 0~1 사이의 비율을 실수로 입력하면 된다
2개의 특성만으로 원본 데이터에 있는 분산의 50%를 표현할 수 있다
그러면 이 모델로 원본 데이터를 반환해보자
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score'])) # 0.9966666666666667
print(np.mean(scores['fit_time'])) # 0.07639546394348144
2개의 특성만을 사용했음에도 99% 정확도를 보여주는 것을 확인할 수 있다
이번에는 차원 축소된 데이터를 사용해 k-평균 알고리즘으로 클러스터를 찾아보겠다
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))
# (array([0, 1, 2], dtype=int32), array([112, 98, 90]))
fruits_pca로 찾은 클러스터는 각각 91, 99, 110개의 샘플을 포함하고 있다
이는 전에 원본 데이터를 사용했을 때와 거의 비슷한 결과이다
Kmeans가 찾은 레이블을 사용해서 과일 이미지를 출력해보겠다
for label in range(0,3):
draw_fruits(fruits[km.labels_ == label])
print("\n")



훈련 데이터의 차원을 줄이면 또 하나 얻을 수 있는 장점은 시각화이다
3개 이하로 차원을 줄이면 화면에 출력하기가 비교적 쉽다
fruits_pca 데이터는 2개의 특성이 있기 때문에 2차원으로 표현할 수 있다
그러면 앞에서 찾은 km.labels_를 사용해 클러스터별로 나누어 산점도를 그려보겠다
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()

각 클러스터의 산점도가 잘 구분되는 것을 확인할 수 있다
그림을 보면 사과와 파인애플의 클러스터의 경계가 가깝게 붙어 이싿
이 두 클러스터의 샘플은 몇 개가 혼동을 일으키기 쉬울 것 같다
이렇게 데이터를 시각화하면 예상치 못한 통찰을 얻을 수 있다
이런 차원에서 차원 축소는 매우 유용한 개념이다
우리는 대표적인 비지도 학습 문제 중 하나인 차원 축소에 대해 알아봤다
차원 축소를 사용하면 데이터셋의 크기를 줄일 수 있고 비교적 시각화하기 쉽다
또 차원 축소된 데이터를 지도 학습 알고리즘이나 다른 비지도 학습 알고리즘에
재사용하여 성능을 높이거나 훈련 속도를 빠르게 만들 수 있다
우리는 사이킷런의 PCA 클래스를 사용하여 과일 사진 데이터의 특성을 50개로
크게 줄였다 특성 개수는 작지만 변환된 데이터는 원본 데이터에 있는 분산의
90% 이상을 표현했다 우리는 이를 설명된 분산이라고 부른다
PCA 클래스는 자동으로 설명된 분산을 계산하여 제공한다
또한 주성분의 개수를 명시적으로 지정하는 대신 설명된 분산의 비율을 지정하여
원하는 비율만큼 주성분의 개수를 찾을 수 있다
PCA 클래스는 변환된 데이터에서 원본 데이터를 복원하는 메서드도 제공한다
변환된 데이터가 원본 데이터의 분산을 모두 유지하고 있지 않다면
완벽하게 복원되지 않는다 하지만 적은 특성으로도 상당 부분의 디테일을 복원할 수 있다

비지도 학습을 하다보면 데이터가 어떤 데이터인지 모르는 경우가 많다
그래서 군집 알고리즘을 사용해서 데이터를 분류하는데
이 때 사용되는 대표 알고리즘이 k-평균 알고리즘이다
해당 알고리즘은 각 데이터 평균값을 찾아 데이터를 분류하는데
먼저 처음에는 랜덤하게 클러스터 중심을 정하고 클러스터를 만든다
그 다음 각 샘플에서 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다
클러스터에 속한 샘플의 평균 값으로 클러스터를 변경한다
이러한 과정을 반복해서 최적의 클러스터를 구하는 방식이다
n_clusters에 클러스터 개수를 지정한다 기본값은 8이다
처음에 랜덤하게 센트로이드를 초기화하기에 여러 번 반복해서 이너셔를 기준으로
가장 좋은 결과를 선택한다 n_init는 이 반복횟수를 지정하고 기본 값은 10이다
max_iter은 k-평균 알고리즘의 한 번 실행에서 최적의 센트로이드를 찾기 위해
반복할 수 있는 최대 횟수이고 기본값은 200이다
그냥 337p에 있는 문제를 다 설명해보겠다
1번 문제
특성이 20개인 대량의 데이터 셋이 있다 이 데이터에 찾을 수 있는 주성분 개수는?
일반적으로 주성분은 원본의 특성 개수보다 작다 그러므로
보기 중에 10이 유일하게 20보다 작으므로 10이다
2번 문제
샘플 개수가 1000개이고 특성 개수가 100개인 데이터셋이 있다
즉, 이 데이터셋의 크기는 (1000,100)이다
이 데이터를 사이킷런의 PCA 클래스를 사용해 10개의 주성분으로 줄였다면
변환된 데이터셋의 크기는?
말할 필요가 없다 (1000,10)
3번 문제
2번 문제에서 설명된 분산이 가장 큰 주성분은 몇 번째인가?
첫 번째 주성분이다
분산이 큰 순서대로 주성분을 만들게 되기 때문이다