Post

[ML기초세션]_6주차

세션 전 학습 내용

교재: p.286 ~ p. 337

[6-1] 군집 알고리즘

핵심 키워드: 비지도 학습, 히스토그램, 군집

과일 사진 데이터 준비하기

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

코랩의 코드 셀에서 ‘!’로 시작하면 코랩은 이후 명령을 파이썬 코드가 아니라 리눅스 셸(shell) 명령으로 이해한다. wget 명령은 원격 주소에서 데이터를 다운로드 하여 저장한다.

이렇게 가져온 파일에서 데이터를 로드해보자.

1
2
3
4
5
6
7
import numpy as np
import matplotlib.pyplot as plt

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

print(fruits.shape)
# (300, 100, 100) -> 300개 샘플, 100(h)x100(w) 크기의 이미지

fruits는 넘파이 배열이고 fruits_300.npy 파일에 들어 있는 모든 데이터를 담고 있다.

  • imshow(): 넘파이 배열로 저장된 이미지를 쉽게 그릴 수 있다.
    1
    2
    
    plt.imshow(fruits[0], cmap = 'gray')
    plt.show()
    

    'img1'

알고리즘이 어떤 출력을 만들기 위해 곱셈, 덧셈을 한다. 픽셀값이 0이면 출력도 0이 되어 의미가 없다. 픽셀값이 높으면 출력값도 커지기 때문에 의미를 부여하기 좋다. 만약 눈에 보기 좋게 출력하려면 cmap 매개변수를 ‘gray_r’로 지정한다.

바나나와 파인애플도 출력해보자.

  • subplots(): 여러 개의 그래프를 배열처럼 쌓을 수 있도록 도와준다. 이 함수의 두 매개변수는 그래프를 쌓을 행과 열을 지정한다.
    1
    2
    3
    4
    
    fig, axs = plt.subplots(1,2)
    axs[0].imshow(fruits[100], cmap='gray_r')
    axs[1].imshow(fruits[200], cmap='gray_r')
    plt.show()
    

    'img2'

픽셀값 분석하기

100 x 100 이미지를 펼쳐서 길이가 10,000인 1차원 배열로 만들어보자. 이렇게 펼치면 이미지로 출력하긴 어렵지만 배열을 계산할 때 편리하다.

'img3'

fruits 배열에서 순서대로 100개씩 선택하기 위해 슬라이싱 연산자를 사용한다. 그다음 reshape() method를 사용해 두 번째 차원(100)과 세 번째 차원(100)을 10,000으로 합친다. 첫 번째 차원을 -1로 지정하면 자동으로 남은 차원을 할당한다. 여기서는 첫 번째 차원이 샘플 개수다.

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

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

이번엔 넘파이의 mean() method를 사용해 apple, pineapple, banana 배열에 들어있는 샘플의 픽셀 평균값을 계산해보자.

axis=0으로 설정하면 첫 번째 축인 행을 따라 계산한다.

axis=1으로 지정하면 두 번째 축인 열을 따라 계산한다.

'img4'

샘플은 모두 가로로 값을 나열했으니 axis=1으로 지정하여 평균을 계산해보자. 평균을 계산하는 넘파이 np.mean() 함수를 사용해도 되지만 넘파이 배열은 이런 함수들을 method로도 제공한다. apple 배열의 mean() method로 각 샘플의 픽셀 평균값을 계산해보자.

1
print(apple.mean(axis=1))

사과 샘플 100개에 대한 픽셀 평균값을 맷플롯립의 hist() 함수를 이용하여 히스토그램으로 나타내보자.

1
2
3
4
5
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()

'img5'

평균 픽셀값으로 바나나는 구별되는 것으로 보이지만 사과와 파인애플은 겹쳐 있어서 구분하기가 어려워 보인다.

샘플의 평균값이 아니라 픽셀별 평균값을 비교해보자.

1
2
3
4
5
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()

'img6'

왼쪽부터 순서대로 사과, 파인애플, 바나나 그래프다.

픽셀 평균값을 100 x 100 크기로 바꿔서 이미지처럼 출력하여 위 그래프와 비교하면 더 좋다.

1
2
3
4
5
6
7
8
9
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()

'img7'

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

abs_diff는 (300, 100, 100) 크기의 배열이다. 따라서 각 샘플에 대한 평균을 구하기 위해 axis에 2,3번째 차원 지정한다. 이렇게 계산한 abs_mean은 각 샘플의 오차 평균이므로 크기가 (300,)인 1차원 배열이다.

1
2
3
4
5
6
7
8
9
10
11
12
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
print(abs_mean.shape)
# (300,)

# 값이 작은 순서대로 100개를 골라보자. 이중 처음 100개를 선택해 10 x 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')

코드 설명: 1. subplots()로 10 x 10, 총 100개의 서브 그래프를 만든다. 2. 그래프가 많기에 전체 그래프 크기를 figsize=(10,10)으로 조금 크게 지정(기본값은 (8,6)). 3. 2중 for문을 순회하며 10개의 행과 열에 이미지를 출력한다. 4. axs는 (10,10) 크기의 2차원 배열이므로 i, j 두 첨자를 사용해서 서브 그래프 위치 지정한다. 5. 깔끔하게 이미지만 그리기 위해 axis(‘off’)를 사용하여 좌표축 그리지 않음.

'img8'

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

  • 군집: 비슷한 샘플끼리 그룹으로 모으는 작업, 대표적인 비지도 학습 작업 중 하나

  • 클러스터: 군집 알고리즘에서 만든 그룹

우리는 이미 사과, 파인애플, 바나나가 있다는 것을 알고 있었기에, 타깃값을 알고 있었기에 각 타깃의 사진 평균값을 계산해 가장 가까운 과일을 찾을 수 있었다. 반면, 실제 비지도 학습에서는 타깃값을 모르기에 이처럼 샘플의 평균값을 미리 구할 수 없다.


키워드

  • 비지도 학습: 머신러닝의 한 종류로, 훈련 데이터의 타깃이 없다. 타깃이 없기에 외부의 도움 없이 스스로 유용한 무언가를 학습해야 한다. 대표적인 예시로 군집, 차원 축소 등이 있다.

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

  • 군집: 비슷한 샘플끼리 하나의 그룹으로 모으는 대표적인 비지도 학습 작업이다. 군집 알고리즘으로 모은 샘플 그룹을 클러스터라고 부른다.


[6-2] k-means

핵심 키워드: k-평균, 클러스터 중심, 엘보우 방법

k-means 알고리즘 소개

k-means 알고리즘 작동 방식

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

'img9' 클러스터의 순서나 번호는 의미가 없다

KMeans 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
!wget https://bit.ly/fruits_300_data -O fruits_300.npy

import numpy as np
# 3차원 배열을 2차원 배열로 변경
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

# KMeans 클래스에서 클러스터 개수 3개 지정, 훈련 시킬 때 타깃 데이터 없음
from sklearn.cluster import KMeans

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

# 레이블 0, 1, 2로 모은 샘플의 개수 확인
print(np.unique(km.labels_, return_counts=True))
# (array([0, 1, 2], dtype=int32), array([111,  98,  91]))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()
1
draw_fruits(fruits[km.labels_==0])

'img10'

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

'img11'

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

'img12'

타깃 데이터를 제공하지 않았음에도 k-평균 알고리즘이 비슷한 샘플들을 어느정도 잘 모았다.

클러스터 중심

KMeans 클래스가 최종저긍로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있다. 이 배열은 fruits_2d 샘플의 클러스터 중심이기 때문에 이미지로 출력하려면 100 x 100 크기의 2차원 배열로 바꿔야 한다.

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

'img13'

  • transform(): 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환 -> StandardScaler처럼 특성값을 변환하는 도구로 사용할 수 있다는 의미
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 훈련 데이터 샘플과 클러스터 중심 사이 거리 출력, 1개 샘플만 전달했기에 크기가 (1, 클러스터 개수)인 2차원 배열 출력
print(km.transform(fruits_2d[100:101]))
# [[3393.8136117  8837.37750892 5267.70439881]]

# 가장 가까운 클러스터 중심 예측
print(km.predict(fruits_2d[100:101]))
# [0]

# 인덱스가 100인 해당 데이터 출력
draw_fruits(fruits[100:101])
# (파인애플 이미지)

# 알고리즘이 최적의 클러스터를 찾으면서 반복한 횟수
print(km.n_iter_)
# 4

최적의 k 찾기

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

적절한 k 값을 찾기 위한 완벽한 방법은 없지만 가장 대표적인 엘보우 방법에 대해 알아보자.

  • 이너셔(inertia): k-평균 알고리즘에서 측정한 클러스터 중심과 샘플 사이 거리의 제곱 합, 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값.

일반적으로 클러스터 개수가 늘어나면 클러스터 개개의 크기는 줄어들기 때문에 이너셔도 줄어든다.

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

'img14'

k-평균 알고리즘의 클러스터 중심까지 거리를 특성으로 사용할 수도 있다. 그렇게 함으로써 훈련 데이터의 차원을 크게 줄일 수 있다. 데이터셋의 차원을 줄이면 지도 학습 알고리즘의 속도를 크게 높일 수 있다.

클러스터 중심: k-평균 알고리즘이 만든 클러스터에 속한 샘플의 특성 평균값. centroid라고도 불리며 최근접 클러스터 중심을 또 다른 특성으로 사용하거나 새로운 샘플에 대한 예측으로 활용할 수 있다.


핵심 패키지와 함수

scikit-learn

  • KMeans: k-평균 알고리즘 클래스다.
  • n_clusters에는 클러스터 개수 지정, 기본값은 8. 처음에 랜덤하게 센트로이드를 초기화하기에 여러 번 반복하여 이너셔를 기준으로 가장 좋은 결과를 선택한다.
  • n_init은 반복 횟수를 지정한다. 기본값은 10.
  • max_iter는 k-평균 알고리즘의 한 번 실행에서 최적의 센트로이드를 찾기 위해 반복할 수 있는 최대 횟수다. 기본값은 200.


[6-3] 주성분 분석

핵심 키워드: 차원 축소, 주성분 분석, 설명된 분산

차원과 차원 축소

지금까지 우리는 데이터가 가진 속성을 특성이라 불렀다. 과일 사진의 경우 10,000개의 픽셀이 있기에 10,000개의 특성이 있는 셈. 머신러닝에서는 이런 특성을 차원(dimension)이라고도 부르는데 10,000개의 특성은 10,000개의 차원이라는 뜻. 이 차원을 줄일 수 있다면 저장 공간을 크게 절약할 수 있을 것이다.

'img15'

  • 차원 축소(dimensionality reduction): 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법이다.

  • 주성분 분석(Principal Component Analysis, PCA): 데이터에 있는 분산이 큰 방향을 찾는 것, 분산이 큰 방향을 데이터로 잘 표현하는 벡터.

'img16'

note: 실제로 사이킷런의 PCA 모델을 훈련하면 자동으로 특성마다 평균값을 빼서 원점에 맞춰준다. 따라서 우리가 수동으로 데이터를 원점에 맞출 필요가 없다.

주성분 벡터는 원본 데이터에 있는 어떤 방향이다. 따라서 주성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같다. 하지만 원본 데이터는 주성분을 사용해 차원을 줄일 수 있다. 예를 들면 다음과 같은 샘플 데이터 s(4,2)를 주성분에 직각으로 투영하면 1차원 데이터 p(4,5)를 만들 수 있다.

'img17'

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

'img18'

PCA 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 데이터셋 준비
!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 클래스 import
from sklearn.decomposition import PCA

# n_components에 주성분 개수 지정
pca = PCA(n_components=50)
# 비지도 학습이기에 fit() 메서드에 타깃값 제공 X
pca.fit(fruits_2d)

# PCA 클래스가 찾은 주성분 배열 크기 출력
print(pca.components_.shape)
# (50, 10000)

# 이미지로 주성분 출력
draw_fruits(pca.components_.reshape(-1, 100, 100))

'img19'

1
2
3
4
5
6
print(fruits_2d.shape)
# (300, 10000)

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
# (300, 50)

fruits_2d는 10,000개의 특성(픽셀)을 가졌지만 fruits_pca 배열은 50개의 특성 뿐이다. 1/200의 효과!

원본 데이터 재구성

앞에서 10,000개의 특성을 50개로 줄였으니 손실이 발생할 수밖에 없으나, 최대한 분산이 큰 방향으로 데이터를 투영했기에 원본 데이터의 상당 부분을 재구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
# inverse_transform() method를 이용한 특성 복원
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
# (300, 10000)

# 이 데이터를 100 x 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")

'img20'

설명된 분산

  • 설명된 분산(explained variance): 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값
1
2
3
4
print(np.sum(pca.explained_variance_ratio_))
# 0.9215651897863715

plt.plot(pca.explained_variance_ratio_)

'img21'

그래프를 보면 처음 10개의 주성분이 대부분의 분산을 표현하고 있음을 볼 수 있다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 로지스틱 회귀 모델 사용
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()


# 파이썬 리스트 활용 타깃값 지정 - 사과:0, 파인애플:1, 바나나:2
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']))
# 0.9966666666666667
# 1.819899892807007


# pca로 축소한 50개 특성 사용
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
# 1.0
# 0.032833099365234375

50개의 특성만 사용했는데도 정확도가 100%이고 훈련시간은 1.82초에서 0.03초로 줄었다

앞서 n_components 매개변수에 주성분의 개수를 지정했는데 이 대신 원하는 설명된 분산의 비율을 입력할 수도 있다. PCA 클래스는 지정된 비율에 도달할 때까지 자동으로 주성분을 찾는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)

print(pca.n_components_)
# 2

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
# (300, 2)

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

단 2개의 특성만을 사용해 99%의 정확도를 달성했다!

이번엔 차원 축소된 데이터를 사용해 k-평균 알고리즘으로 클러스터를 찾아보자.

1
2
3
4
5
6
7
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([110,  99,  91]))

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

이 레이블을 활용해 과일 이미지를 출력해보자

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

'img22'

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

1
2
3
4
5
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()

'img23'

각 클러스터의 산점도가 잘 구분되는 것을 볼 수 있다. 이 그림을 보면 사과와 파인애플 클러스터의 경계가 가깝다. 이로 인해 일부 데이터가 혼동을 일으키기 쉬울 것 같다. 이처럼 데이터를 시각화 하면 예상치 못한 통찰을 얻을 수 있다.


키워드

  • 차원 축소: 원본 데이터의 특성을 적은 수의 새로운 특성으로 변환하는 비지도 학습의 한 종류다. 차원 축소는 저장 공간을 줄이고 시각화하기 쉽다. 또한 다른 알고리즘의 성능을 높일 수도 있다.

  • 주성분 분석: 차원 축소 알고리즘의 하나로 데이터에서 가장 분산이 큰 방향을 찾는 방법이다. 이런 방향을 주성분이라고 부른다. 원본 데이터를 주성분에 투영하여 새로운 특성을 만들 수 이싿. 일반적으로 주성분은 원본 데이터의 특성 개수보다 작다.

  • 설명된 분산: 주성분 분석에서 주성분이 얼마나 원본 데이터의 분산을 잘 나타낸는지 기록한 것이다. 사이킷런의 PCA 클래스는 주성분 개수나 설명된 분산의 비율을 지정하여 주성분 분석을 수행할 수 있다.


핵심 패키지와 함수

scikit-learn

  • PCA: 주성분 분석을 수행하는 클래스
  • n_components: 주성분 개수 지정, 기본값은 None으로 샘플 개수와 특성 개수 중 작은 것의 값을 사용한다.
  • random_state: 넘파이 난수 시드 값을 지정할 수 있다.
  • components_: 훈련 세트에서 찾은 주성분이 저장된다.
  • explained_variance_: 설명된 분산 저장, explained_variance_ratio_: 설명된 분산의 비율 저장
  • inverse_transform(): transform() method로 차원을 축소시킨 데이터를 다시 원본 차원으로 복원한다.


발표 내용 복습

심화 발제

심화발제 원본

'img24' 'img25' 'img26'

This post is licensed under CC BY 4.0 by the author.