본문 바로가기

데이터/머신러닝

[혼공] ch 6. 비지도 학습

1. 군집 알고리즘 

 

1) 타깃을 모르는 비지도 학습 

 

비지도 학습(Unsupervised learning)

- 타깃이 없을 때 사용하는 머신러닝 알고리즘 

 

2) 과일 사진 데이터 준비하기 

 

- 사과, 바나나, 파인애플을 담고 있는 흑백 사진

- 넘파이 배열의 기본 저장 포맷인 npy 파일로 저장

 

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

 

- wget 명령은 원격 주소에서 데이터를 다운로드하여 저장 

 

import numpy as np
import matplotlib.pyplot as plt


fruits = np.load('fruits_300.npy')
print(fruits.shape)

 

- 넘파이에서 npy 파일을 로드하기 위해 load() 메서드에 파일 이름 전달 

- fruits의 배열 크기 확인

 

- 배열의 첫 번째 차원(300)은 샘플의 개수를 나타내고, 두 번째 차원(100)은 이미지 높이, 세 번째 차원(100)은 이미지 너비이다. 

 

- 첫 번째 이미지의 첫 번째 행을 출력

print(fruits[0, 0, :])

 

 

- 넘파이 배열은 흑백 사진을 담고 있으므로 0~255까지의 정숫값을 가진다. 

 

- 맷플롯립의 imshow() 함수를 사용하면 넘파이 배열로 저장된 이미지를 쉽게 그릴 수 있다. 

- 흑백 이미지이므로 camp 매개변수에 'gray'로 지정 

 

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

 

 

- 0에 가까울수록 검게 나타나고 높은 값은 밝게 표시된다. 

 

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

 

 

- 맷플롯립의 subplots() 함수를 사용하면 여러 개의 그래프를 배열처럼 쌓을 수 있도록 도와준다, 

- subplots() 함수의 두 매개변수는 그래프를 쌓을 행과 열을 지정한다. 

- subplost(1,2)처럼 하나의 행과 2개의 열을 지정 

 

- 반환된 axs는 2개의 서브 그래프를 담고 있는 배열

 

 

3) 픽셀값 분석하기 

 

- fruits 데이터를 사과, 파인애플, 바나나로 각각 나누기

- 넘파이 배열을 나눌 때 100*100 이미지를 펼쳐서 길이가 10,000인 1차원 배열로 만들기 

- frutis 배열에서 순서대로 100개씩 선택하기 위해 슬라이싱 연산자 사용

- 그 다음 reshape() 메서드를 사용해 두 번째 차원(100)과 세 번째 차원(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)
print(pineapple.shape)
print(banana.shape)

 

 

- apple, pineapple, banana 배열의 크기는 (100,10000)이다. 

 

- apple, pineapple, banana 배열에 들어있는 샘플의 픽셀 평균값 계산

- 넘파이 mean() 메서드 사용 

 

print(apple.mean(axis=1))

 

 

 

- 맷플롯립의 hist() 함수를 사용하여 히스토그램 그리기 

 

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

 

 

- 바나나는 픽셀 평균값만으로 사과나 파인애플과 구분 가능하다. 

- 사과와 파인애플은 많이 겹쳐져 있어서 픽셀 평균값만으로 구분이 어렵다. 

 

- 샘플의 평균값이 아니라 픽셀별 평균값을 비교 (전체 샘플에 대해 각 픽셀의 평균값 계산) 

- 맷플롯립의 bar() 함수를 사용해 픽셀 10,000개에 대한 평균값을 막대 그래프로 구리기

- 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개의 그래프를 보면 과일마다 값이 높은 구간이 다르다. 

- 사과는 사진 아래쪽으로 갈수록 값이 높아지고, 파인애플 그래프는 비교적 고르면서 높다, 

- 바나나는 확실히 중앙의 픽셀값이 높다. 

 

- 픽셀 평균값을 100*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()

 

 

 

 

4) 평균값과 가까운 사진 고르기

 

- 사과 사진의 평균값인 apple_mean 과 가까운 사진 고르기 

- 절댓값 오차 사용

- fruits 배열에 있는 모든 샘플에서 apple_mean을 뺀 절댓값의 평균을 계산

 

abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
print(abs_mean.shape)

 

 

- abs_diff는 (300,100,100) 크기의 배열

- abs_mean은 각 샘플의 오차 평균이므로 크기가 (300,)인 1차원 배열 

 

- apple_mean과 오차가 가장 작은 샘플 100개 고르기

- np.argsort() 함수작은 것에서 큰 순서대로 나열한 abs_mean 배열의 인덱스를 반환

- 이 인덱스 중에서 처음 100개를 선택해 10*10 격자를 이루어진 그래프 그리기 

 

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

 

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

 

- 비슷한 샘플끼리 그룹으로 모으는 작업을 군집(clustering)이라고 한다. 군집 알고리즘에서 만든 그룹을 클러스터(cluster)라고 부른다. 

 

 

2. K-평균 

 

1) K-평균 알고리즘 

 

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

 

2) KMeans 클래스 

 

- 넘파이 np.load() 함수를 사용해 npy 파일을 읽어 넘파이 배열을 준비

- K-평균 모델을 훈련하기 위해 (샘플 개수, 너비, 높이) 크기의 3차원 배열을 (샘플 개수, 너비*높이) 크기를 가진 2차원 배열로 변경 

 

import numpy as np

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

 

- 사이킷런의 k- 평균 알고리즘은 sklearn.cluster 모듈 아래 KMeans 클래스에 구현되어 있다. 

- n_clusters는 클러스터의 개수를 지정

 

from sklearn.cluster import KMeans

km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)

 

- 군집된 결과는 KMeans 클래스 객체의 labels_ 속성에 저장

- labels_ 배열의 길이는 샘플 개수와 같다. 

- 이 배열은 각 샘플이 어떤 레이블에 해당되는지 나타낸다. 

 

print(km.labels_)

 

 

 

- 레이블 0,1,2로 모은 샘플의 개수를 확인

 

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

 

 

- 각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력하기 위한 draw_fruits()

- (샘플 개수, 너비, 높이)의 3차원 배열을 입력받아 가로로 10개씩 이미지를 출력

- 샘플 개수에 따라 행과 열의 개수를 계산하고 figsize를 지정

- figsize는 ratio 매개변수에 비례하여 커진다. 

- 그 다음 2중 for 반복문을 사용하여 첫 번째 행에 따라 이미지를 그린다. 

 

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 함수를 사용해 레이블이 0인 과일 사진 모두 그리기 

 

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

 

 

 

 

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

 

 

 

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

 

 

 

- 레이블이 1인 클러스터는 바나나로만 이루어져 있고, 레이블이 2인 클러스터는 사과로만 이루어져 있다. 

하지만 레이블이 0인 클러스터는 파인애플에 사과 9개와 바나나 2개가 섞여 있다. 

 

- K-평균 알고리즘이 이 샘플들을 완벽하게 구별해내지는 못함 

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

 

3) 클러스터 중심 

 

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

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

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

 

 

 

 

- KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해 주는 transform() 메서드를 가지고 있다. 

 

- 인덱스가 100인 샘플에 transform() 메서드 적용 

 

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

첫 번째 클러스터까지의 거리가 가장 작음

 

- KMeans 클래스는 가장 가까운 클러스터 중심을 예측 클래스로 출력하는 predict() 메서드를 제공 

 

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

 

레이블 0으로 예측

 

draw_fruits(fruits[100:101])

 

 

- 알고리즘이 반복한 횟수는 KMeans 클래스의 n_iter_ 속성에 저장된다. 

 

print(km.n_iter_)

 

 

 

4) 최적의 k 찾기 

 

- K-평균 알고리즘의 단점 중 하나는 클러스터 개수를 사전에 지정해야 한다는 것이다. 

 

엘보우(elbow) 방법

 

- 최적의 클러스터 개수를 정하는 방법 중 하나 

- 클러스터 중심과 클러스터의 속한 샘플 사이의 거리의 제곱 합을 이너셔(inertia)라고 부른다. 

- 이너셔는 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값

- 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법 

 

 

 

- KMeans 클래스는 자동으로 이너셔를 계산해서 inertia_ 속성을 제공

- 클러스터 개수 k를 2 ~6 까지 바꿔가며 KMeans 클래스를 5번 훈련

- fit() 메서드로 모델을 훈련한 후 inertia_ 속성에 저장된 이너셔값을 inertia 리스트에 추가 

- 마지막으로 inertia 리스트에 저장된 값을 그래프로 출력 

 

inertia = []
for k in range(2, 7):
    km = KMeans(n_clusters=k, n_init='auto', 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에서 그래프의 기울기가 조금 바뀜

 

 

3. 주성분 분석 

 

1) 차원과 차원 축소 

 

차원(dimension)

- 데이터가 가진 속성

- 10,000개의 특성은 결국 10,000개의 차원 

 

차원 축소(dimensionality reduction)

- 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법 

 

 

2) 주성분 분석

 

주성분 분석(principal component analysis) 

- 차원 축소 알고리즘의 하나로 데이터에서 가잔 분산이 큰 방향을 찾는 방법 

- 분산은 데이터가 널리 퍼져있는 정도

- 분산이 큰 방향이란 데이터를 잘 표현하는 어떤 벡터 

 

ex)

- 이 데이터는 x1, x2 2개의 특성이 있다. 

- 길게 늘어진 대각선 방향이 분산이 가장 크다고 알 수 있다. 

- 직선이 원점에서 출발한다면 두 원소로 이루어진 벡터로 쓸 수 있다.  -> 이 벡터를 주성분(principal component)이라고 부름

- 이 주성분 벡터는 원본 데이터에 있는 어떤 방향 

- 주성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성개수와 같다. 

- 하지만 원본 데이터는 주성분을 사용해 차원을 줄일 수 있다. 

 

샘플 데이터 s(4,2)를 주성분에 직각으로 투영하면 1차원 데이터 p(4,5)를 만들 수 있다.

 

- 주성분이 가장 분산이 큰 방향이기 때문에 주성분에 투영하여 바꾼 데이터는 원본이 가지고 있는 특성을 가장 잘 나타내고 있을 것이다. 

 

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

 

 

3) PCA 클래스 

 

- 사이킷런은 sklearn.decomposition 모듈 아래 PCA 클래스로 주성분 분석 알고리즘을 제공

- PCA 클래스의 객체를 만들 때 n_components 매개변수에 주성분의 개수를 지정해야 한다. 

 

from sklearn.decomposition import PCA

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

print(pca.components_.shape)

 

- 첫 번째 차원은 50이며 50개의 주성분을 찾은것, 두 번째 차원은 항상 원본 데이터의 특성개수와 같은 10,000이다. 

 

- draw_fruits() 함수를 사용해서 100*100 크기의 이미지처럼 출력

 

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

 

 

- 이 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것

- 주성분을 찾았으므로 원본 데이터를 주성분에 투영하여 특성의 개수를 10,000개에서 50개로 줄일 수 있다. 

 

- PCA의 transform() 메서드를 사용해 원본 데이터의 차원을 50으로 줄이기 

 

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

 

 

4) 원본 데이터 재구성 

 

 

- PCA의 inverse_transform() 메서드를 이용해 50개의 차원으로 축소한 fruits_pca 데이터를 전달해 10,000개의 특성을 복원 

 

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

 

 

- 데이터를 100*100 크기로 바꾸어 100개씩 나누어 출력

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

 

 

 

 

5) 설명된 분산 

 

설명된 분산(explained variance)

 

- 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값

- PCA 클래스의 explained variance_ratio에 각 주성분의 설명된 분산 비율이 기록되어 있다. 

 

print(np.sum(pca.explained_variance_ratio_))

 

92%가 넘는 분산을 유지

 

 

- 맷플롯립의 plot() 함수로 설명된 분산을 그래프로 출력

 

plt.plot(pca.explained_variance_ratio_)

 

 

 

 

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

 

- 과일 사진 원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해보고 어떤 차이강 ㅣㅆ는지 알아보기 

- 3개의 과일 사진을 분류해야 하므로 로지스틱 회귀 모델 사용 

 

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()

## 사과를 0, 파인애플을 1, 바나나를 2로 지정
target = np.array([0] * 100 + [1] * 100 + [2] * 100)

 

 

- 로지스틱 회귀 모델에서 성능을 가늠해 보기 위해 cross-validate()로 교차 검증 수행

 

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']))

 

 

- 교차 검증의 점수는 0.997로 매우 높다. 

- fit_time 항목에 각 교차 검증 폴드의 훈련 시간이 기록되어있고 0.94초 걸렸다. 

 

- 이 값을 PCA로 축소한 fruits_pca를 사용했을 때와 비교 

 

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

 

 

- 정확도가 100%이고 훈련 시간은 0.03초로 20배 이상 감소하였다. 

- PCA로 훈련 데이터의 차원을 축소하면 저장 공간뿐만 아니라 머신러닝 모델의 훈련 속도도 높일 수 있다. 

 

- 설명된 분산의 50%에 달하는 주성분을 찾도록 PCA 모델 만들기 

 

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

 

print(pca.n_components_)

 

- 2개의 특성만으로 원본 데이터에 있는 분산의 50%를 찾을 수 있다. 

 

- 이 모델로 원본 데이터를 변환

- 주성분이 2개이므로 변환된 데이터의 크기는 (300,2)가 될 것이다. 

 

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

 

 

- 2개의 특성만 사용했을 때 교차 검증의 결과 

 

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

 

 

 

- 차원 축소된 데이터를 사용해 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))

 

 

- fruits_pca로 찾은 클러스터는 각각 110개, 99개, 91개의 샘플을 포함하고 있다. 

 

- KMeans가 찾은 레이블을 사용해 과일 이미지를 출력

 

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

 

 

- 훈련 데이터의 차원을 줄이면 또 하나 얻을 수 있는 장점은 시각화 

- fruits_pca 데이터는 2개의 특성이 있기 때문에 2차원으로 표현할 수 있다. 

 

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