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(): 넘파이 배열로 저장된 이미지 쉽게 그림.
❗ 보통 흑백 샘플 이미지는 바탕이 밝고 물체가 짙은 색! 그러나 위 사진은 사진으로 찍은 이미지를 넘파이 배열로 변환할 때 반전시킨 것.
=> why? 컴퓨터는 255에 가까운 곳에 집중하기 때문이다.
1
2
3
plt.imshow(fruits[0], cmap='gray_r')
plt.show()
# gray_r: 반전 시킴.
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개의 열)
픽셀값 분석하기
🔖 평균 계산하기
❗ 이 예시에서 넘파이 배열을 나눌 때 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: 행 방향으로 계산.
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(): 어떤 과일의 히스토그램인지 범례를 만들 수 있음.
- 바나나: 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()
픽셀별로 평균낸것을 그림으로 그려보자!
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()
평균값과 가까운 사진 고르기
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()
군집과 클러스터
- 군집: 비슷한 샘플끼리 그룹으로 모으는 작업.
- 클러스터: 군집 알고리즘에서 만든 그룹.
6-2 K-평균
- K-평균: 군집 알고리즘이 평균값을 자동으로 찾아준다.
- 클러스터 중심(센트로이드): 클러스터의 중심에 위치한 평균값.
K-평균 알고리즘 소개
🔖 작동 방식
- 무작위로 K개의 클러스터 중심을 정함.
- 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정.
- 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경.
- 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복.
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])
# 불리언 인덱싱 이용.
클러스터 중심
🔖 중심 구하기
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)
🔖 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])
🔖 n_iter_
- n_iter_: 알고리즘이 반복된 횟수(최적의 클러스터를 몇번만에 찾았나?)
1
2
print(km.n_iter_)
# 4
❗ n_init: 반복 횟수(랜덤하게 센트로이드 초기화)
최적의 K 찾기
- K-평균 알고리즘의 단점 중 하나는 클러스터 개수를 사전에 지정해야 한다는 것!
🔖 엘보우
- 엘보우: 클러스터 개수를 늘려가면서 이녀서의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법.
- 이너셔: 클러스터 중심과 클러스터에 속한 샘플 사이의 거리(클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지 나타내는 값)
- 클러스터 갯수 증가 -> 이너셔 감소
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()
6-3 주성분 분석
차원과 차원 축소
- 차원: 특성(과일 사진의 경우 10000개의 픽셀이 10000개의 특성)
1
2
3
💡 2차원 배열과 1차원 배열의 차원은 다른 건가요?
다차원 배열에서 차원은 배열의 축 개수가 됩니다. 가령 2차원 배열일 때는 행과 열이 차원입니다. 하지만 배열, 즉 벡터일 경우에는 원소의 개수를 말합니다.
- ⭐차원 축소: 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법.
❗줄어든 차원에서 다시 원본 차원으로 손실을 최대한 줄이면서 복원할 수도 있습니다.
❗이번 절에서는 대표 차원 축소 알고리즘 주성분 분석에 대해 알아보겠습니다.
주성분 분석(PCA) 소개
- 주성분 분석: 데이터에 있는 분산(데이터가 널리 퍼져있는 정도)이 큰 방향을 찾는 것.
=> 분산이 큰 방향을 데이터로 잘 표현하는 벡터(주성분) 찾기! - 주성분 벡터: 원본 데이터에 있는 어떤 방향.
- 주성분 벡터의 원소 개수는 원본 데이터셋에 있는 특성 개수와 같다.
🔖 첫번째 주성분
이 직선이 원점에서 출발한다면 두 원소로 이루어진 벡터(주성분)로 쓸 수 있다.
🔖 두번째 주성분
첫번째 주성분 벡터에 수직이고 분산이 가장 큰 다음 방향.
❗주성분은 원본 특성의 개수만큼 찾을 수 있다.(원본 특성의 개수와 샘플 개수 중 작은 값만큼 찾을 수 있다.)
🔖 주성분 이용해 차원 줄이기
샘플 데이터 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))
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")
❗주성분을 최대로 사용했다면 완벽하게 원본 데이터 재구성 가능!
설명된 분산
- 설명된 분산: 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값.
1
2
3
4
5
6
print(np.sum(pca.explained_variance_ratio_))
# explained_variance_ratio_: 각 주성분의 설명된 분산 비율이 기록되어 있음. 이 분산 비율을 모두 더하면 총 분산 비율이 나옴.
# 0.921510012593063
plt.plot(pca.explained_variance_ratio_)
# 설명된 분산의 비율을 그래프로 그려보자.
다른 알고리즘과 함께 사용하기
로지스틱 회귀 모델
❗과일 사진 원본 데이터와 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")
🔖 산점도 그리기
- 차원을 줄이면 시각화 하기 편해짐.
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()