Post

6.비지도 학습

6.비지도 학습

6-1 군집 알고리즘

타깃을 모르는 비지도 학습

  • 비지도 학습: 타깃이 없을 때 사용하는 머신러닝 알고리즘.

과일 사진 데이터 준비하기

  • 사과(100개), 바나나(100개), 파인애플(100개)을 담고 있는 흑백 사진
  • 넘파이 배열의 기본 저장 포맷인 npy 파일로 저장되어 있음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
!wget https://bit.ly/fruits_300 -O fruits_300.npy
# '!' 문자로 시작하면 코랩은 이후 명령을 파이썬 코드가 아니라 리눅스 셸 명령으로 이해한다.
# wget: 원격 주소에서 데이터를 다운로드하여 저장.
# -O 옵션에서 저장할 파일 이름을 지정

import numpy as np
import matplotlib.pyplot as plt

fruits = np.load('fruits_300.npy')
# npy 파일 로드

print(fruits.shape)
# (300, 100, 100)
# 첫번째 차원: 샘플의 개수 / 두번째 차원: 이미지 높이 / 세번째 차원: 이미지 너비 => 이미지 크기는 100 x 100
# 각 픽셀은 넘파이 배열의 원소 하나에 대응.

print(fruits[0, 0, :])
# [  1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1    2   2   2   2   2   2   1   1   1   1   1   1   1   1   2   3   2   1    2   1   1   1   1   2   1   3   2   1   3   1   4   1   2   5   5   5    19 148 192 117  28   1   1   2   1   4   1   1   3   1   1   1   1   1    2   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 1   1   1   1   1   1   1   1   1   1]
# 0에 가까울수록 검게 나타나고 높은 값은 밝게 표시된다.

plt.imshow(fruits[0], cmap='gray')
plt.show()
# imshow(): 넘파이 배열로 저장된 이미지 쉽게 그림.

alt text

❗ 보통 흑백 샘플 이미지는 바탕이 밝고 물체가 짙은 색! 그러나 위 사진은 사진으로 찍은 이미지를 넘파이 배열로 변환할 때 반전시킨 것.
=> why? 컴퓨터는 255에 가까운 곳에 집중하기 때문이다.

1
2
3
plt.imshow(fruits[0], cmap='gray_r')
plt.show()
# gray_r: 반전 시킴.

alt text

1
2
3
4
5
6
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(): 여러 개의 그래프를 배열처럼 쌓음. 두 매개변수는 그래프를 쌓을 행과 열을 지정(위에서는 1개의 행, 2개의 열)

alt text

픽셀값 분석하기

🔖 평균 계산하기

❗ 이 예시에서 넘파이 배열을 나눌 때 100 x 100 이미지를 펼처서 길이가 10,000인 1차원 배열로 만들자! => 계산하기 편함.

1
2
3
4
5
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)
# 배열의 크기는 (100, 10000)
1
2
3
4
5
💡 axis 인수?

- axis: 배열의 '축'을 의미.
    - axis = 1: 열 방향을 계산.
    - axis = 0: 행 방향으로 계산.

alt text

1
2
3
4
print(apple.mean(axis=1))
# np.mean() 함수를 사용해도 되지만 넘파이 배열은 이런 함수를 메서드로 제공.
# 2차원 배열을 1차원 배열로 치환했으니 axis = 1로 지정하여 평균 계산.
# [ 88.3346  97.9249  87.3709  98.3703  92.8705  82.6439  94.4244  95.5999    90.681   81.6226  87.0578  95.0745  93.8416  87.017   97.5078  87.2019  88.9827 100.9158  92.7823 100.9184 104.9854  88.674   99.5643  97.2495  94.1179  92.1935  95.1671  93.3322 102.8967  94.6695  90.5285  89.0744  97.7641  97.2938 100.7564  90.5236 100.2542  85.8452  96.4615  97.1492  90.711  102.3193  87.1629  89.8751  86.7327  86.3991  95.2865  89.1709  96.8163  91.6604  96.1065  99.6829  94.9718  87.4812  89.2596  89.5268  93.799   97.3983  87.151   97.825  103.22    94.4239  83.6657  83.5159  102.8453  87.0379  91.2742 100.4848  93.8388  90.8568  97.4616  97.5022  82.446   87.1789  96.9206  90.3135  90.565   97.6538  98.0919  93.6252  87.3867  84.7073  89.1135  86.7646  88.7301  86.643   96.7323  97.2604  81.9424  87.1687  97.2066  83.4712  95.9781  91.8096  98.4086 100.7823  101.556  100.7027  91.6098  88.8976]

🔖 히스토그램으로 그리기

1
2
3
💡 히스토그램이란?

- 히스토그램: 값이 발생한 빈도를 그래프로 표시한 것. 보통 x축이 값의 구간이고, y축은 발생 빈도입니다.
1
2
3
4
5
6
7
8
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(): 어떤 과일의 히스토그램인지 범례를 만들 수 있음.

alt text

  • 바나나: 40아래 집중 / 사과, 파인애플: 90~100

❗ 구분 어려움!

🔖 샘플의 평균값x 픽셀별 평균값 비교!

1
2
3
4
5
6
7
fig, axs = plt.subplots(1, 3, figsize=(20,5))
# 그래프를 여러개 그리기 위해 이용(1 x 3 table을 그리겠다는 의미)
axs[0].bar(range(10000), np.mean(apple, axis=0))
axs[0].bar(range(10000), np.mean(pineapple, axis=0))
axs[0].bar(range(10000), np.mean(banana, axis=0))
# 행을 따라 계산. 픽셀별 평균!
plt.show()

alt text

픽셀별로 평균낸것을 그림으로 그려보자!

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

alt text

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis(1,2))
# 먼저 행을 따라 평균내고, 열을 따라 평균 내는 것.

print(abs_mean.shape)
# (300,)

apple_index = np.argsort(abs_mean)[:100]
# np.argsort(): 작은 것에서 큰 순서대로 나열한 배열의 인덱스를 반환.

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')
        # axis('off'): 좌표축은 그리지x
plt.show()

alt text

군집과 클러스터

  • 군집: 비슷한 샘플끼리 그룹으로 모으는 작업.
  • 클러스터: 군집 알고리즘에서 만든 그룹.

6-2 K-평균

  • K-평균: 군집 알고리즘이 평균값을 자동으로 찾아준다.
  • 클러스터 중심(센트로이드): 클러스터의 중심에 위치한 평균값.

K-평균 알고리즘 소개

🔖 작동 방식

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

alt text

KMeans 클래스

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
!wget https://bit.ly/fruits_300 -O fruits_300.npy

import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
# (샘플 개수, 너비, 높이) -> (샘플 개수, 너비x높이)

from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
# n_cluster: 클러스터 개수 지정

km.fit(fruits_2d)
# target_data는 필요x

print(km.labels_)
# labels_: 각 샘플이 어떤 레이블에 해당되는지 나타냄.
# [0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 2 2 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

print(np.unique(km.labels_, return_counts=True))
# unique: 넘파이 배열에서 중복된 값을 제거하고, 중복되지 않은 값들로 이루어진 새로운 배열을 반환
# return_counts: True로 지정하면, 중복되지 않은 값들이 각각 몇 번씩 등장하는지의 빈도를 함께 반환해줍니다. 
# (array([0, 1, 2], dtype=int32), array([ 91,  98, 111]))

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개 입니다.
    # np.ceil: 무조건 올림하는 함수
    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(fruits[km.labels_==0])
draw_fruits(fruits[km.labels_==1])
draw_fruits(fruits[km.labels_==2])
# 불리언 인덱싱 이용.

alt text alt text alt text

클러스터 중심

🔖 중심 구하기

1
2
3
4
5
6
print(km.cluster_centers_)
print(km.cluster_centers_.shape)
# [[1.01098901 1.01098901 1.01098901 ... 1.         1.         1.        ][1.10204082 1.07142857 1.10204082 ... 1.         1.         1.        ][1.         1.         1.         ... 1.         1.         1.        ]]
# (3, 10000)

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

alt text

🔖 transform()

  • transform(): 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해줌(StandardScaler 클래스처럼 특성값을 변환하는 도구로 사용할 수 있다.)
1
2
3
print(km.transform(fruits_2d[100:101]))
# [[5267.70439881 8837.37750892 3393.8136117 ]]
# transform()도 2차원 배열을 기대한다. fruits_2d[100]으로 하면 (1000, )크기의 배열이 되므로 에러 발생!

🔖 predict()

1
2
3
4
5
print(km.predict(fruits_2d[100:101]))
# [2]
# 가장 거리가 가까운 세 번째 클러스터로 예측된다.

draw_fruits(fruits[100:101])

alt text

🔖 n_iter_

  • n_iter_: 알고리즘이 반복된 횟수(최적의 클러스터를 몇번만에 찾았나?)
1
2
print(km.n_iter_)
# 4

❗ n_init: 반복 횟수(랜덤하게 센트로이드 초기화)

최적의 K 찾기

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

🔖 엘보우

  • 엘보우: 클러스터 개수를 늘려가면서 이녀서의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법.
  • 이너셔: 클러스터 중심과 클러스터에 속한 샘플 사이의 거리(클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지 나타내는 값)
    • 클러스터 갯수 증가 -> 이너셔 감소

alt text

1
2
3
4
5
6
7
8
inertia = []
for k in range(2, 7):
    km = KMeans(n_clusters=k, random_state=42)
    km.fit(fruits_2d)
    inertia.append(km.inertia_)
    # inertia_ 속성으로 이녀서 값 제공
plt.plot(range(2,7), inertia)
plt.show()

alt text

6-3 주성분 분석

차원과 차원 축소

  • 차원: 특성(과일 사진의 경우 10000개의 픽셀이 10000개의 특성)
1
2
3
💡 2차원 배열과 1차원 배열의 차원은 다른 건가요?

다차원 배열에서 차원은 배열의 축 개수가 됩니다. 가령 2차원 배열일 때는 행과 열이 차원입니다. 하지만 배열, 즉 벡터일 경우에는 원소의 개수를 말합니다.

alt text

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

❗줄어든 차원에서 다시 원본 차원으로 손실을 최대한 줄이면서 복원할 수도 있습니다.
❗이번 절에서는 대표 차원 축소 알고리즘 주성분 분석에 대해 알아보겠습니다.

주성분 분석(PCA) 소개

  • 주성분 분석: 데이터에 있는 분산(데이터가 널리 퍼져있는 정도)이 큰 방향을 찾는 것.
    => 분산이 큰 방향을 데이터로 잘 표현하는 벡터(주성분) 찾기!
  • 주성분 벡터: 원본 데이터에 있는 어떤 방향.
    • 주성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같다.

🔖 첫번째 주성분

alt text

이 직선이 원점에서 출발한다면 두 원소로 이루어진 벡터(주성분)로 쓸 수 있다.

alt text

🔖 두번째 주성분

첫번째 주성분 벡터에 수직이고 분산이 가장 큰 다음 방향.

alt text

❗주성분은 원본 특성의 개수만큼 찾을 수 있다.(원본 특성의 개수와 샘플 개수 중 작은 값만큼 찾을 수 있다.)

🔖 주성분 이용해 차원 줄이기

alt text

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

❗주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어든다.

PCA 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
!wget https://bit.ly/fruits_300 -O fruits_300.npy
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

from sklearn.decomposition import PCA
pca = PCA(n_components = 50)
# n_components: 주성분의 개수
pca.fit(fruits_2d)

print(pca.components_.shape)
# components_: 클래스가 찾은 주성분
# (50, 10000)

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

alt text

1
2
3
4
5
6
print(fruits_2d.shape)
# (300, 10000)
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
# (300, 50)
# transform(): 원본 데이터의 차원을 줄임(주성분으로 분해)

원본 데이터 재구성

1
2
3
4
5
6
7
8
9
fruits_inverse = pca.inverse_transform(fruits_pca)
# inverse_transform(): 원본 데이터를 재구성하는 함수
print(fruits_inverse.shape)
# (300, 10000)

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

alt text

❗주성분을 최대로 사용했다면 완벽하게 원본 데이터 재구성 가능!

설명된 분산

  • 설명된 분산: 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값.
1
2
3
4
5
6
print(np.sum(pca.explained_variance_ratio_))
# explained_variance_ratio_: 각 주성분의 설명된 분산 비율이 기록되어 있음. 이 분산 비율을 모두 더하면 총 분산 비율이 나옴.
# 0.921510012593063

plt.plot(pca.explained_variance_ratio_)
# 설명된 분산의 비율을 그래프로 그려보자.

alt text

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

로지스틱 회귀 모델

❗과일 사진 원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해 보고 어떤 차이가 있는지 보자!

1
2
3
4
5
6
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()

target = np.array([0]*100 + [1]*100 + [2]*100)
# 파인애플:0 / 바나나:1 / 사과:2

🔖 원본 데이터 이용

1
2
3
4
5
6
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
# 3.0091545581817627(각 교차 검증 폴드의 훈련 시간)

🔖 PCA 이용

1
2
3
4
5
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
# 1.0(상승)
# 0.11161208152770996(감소)

🔖 PCA 이용(n_components를 0.5로!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pca = PCA(n_components=0.5)
# n_components: 주성분의 개수 대신 원하는 설명된 분산의 비율을 입력할 수도 있음. 지정된 비율에 도달할 때까지 자동으로 주성분을 찾음.
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.06778483390808106(감소)

K-평균 알고리즘

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

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

alt text

🔖 산점도 그리기

  • 차원을 줄이면 시각화 하기 편해짐.
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(['pineapple','banana','apple'])
plt.show()

alt text

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

Trending Tags