타깃을 모르는 사진을 종류별로 분류. 타깃이 없을 때 사용하는 머신러닝 알고리즘.
사람이 가르쳐 주지 않아도 데이터에 있는 무언가를 학습하는 것. 대표적인 비지도 학습 작업은 군집, 차원 축소 등이다.
비슷한 샘플끼리 하나의 그룹으로 모으는 대표적인 비지도 학습 작업으로, 군집 알고리즘으로 모은 샘플 그룹을 cluster라고 부른다.
# 과일 사진 데이터 준비 (.npy 파일로 저장)
!wget https://bit.ly/fruits_300 -O fruits_300.npy
# numpy에서 npy 파일을 로드하는 법
fruits = np.load('fruits_300.npy')
print(fruits.shape)
# (첫번째차원, 두번째차원, 세번째차원)
# (샘플의개수, 이미지높이, 이미지너비)
# (300, 100, 100)
# 첫 번째 이미지의 첫 번째 행 출력
print(fruits[0, 0, :])
# 흑백 사진을 담고 있으므로 0~255까지의 정숫값을 가짐
# [ 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]
# matplotlib의 imshow()를 사용하여 넘파이 배열로 저장된 이미지를 그리기
# 흑백 이미지이므로 cmap='gray'
plt.imshow(fruits[0], cmap='gray')
# numpy 배열로 변환할 때 색깔이 반전된
plt.show()
plt.imshow(fruits[0], cmap='gray_r')
plt.show()
# subplots(): 그래프를 쌓을 행과 열을 지정
fig, axs = plt.subplots(1, 2)
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
plt.show()
# 픽셀값 분석하기
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)
# (100, 10000)
# apple 샘플 100개에 대한 픽셀 평균값 계산
# 두 번째 축인 열을 따라 계산
print(apple.mean(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]
구간별로 값이 발생한 빈도를 그래프로 표시한 것으로, 보통 x축이 값의 구간이고 y축은 발생 빈도.
# 히스토그램
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()
# 막대그래프
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()
# 픽셀 평균값을 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()
# 평균값과 가까운 사진 고르기
# (300, 100, 100) 크기의 배열
abs_diff = np.abs(fruits - apple_mean)
# 각 샘플에 대한 평균을 구하기 위해 axis에 두 번째, 세 번째 차원을 지정
abs_mean = np.mean(abs_diff, axis=(1, 2))
# 각 샘플의 오차 평균이므로 크기가 (300, )인 1차원 배열
print(abs_mean.shape)
# (300,)
# apple_mean과 가장 가까운 사진 100개 선택
apple_index = np.argsort(abs_mean)[:100]
fig, axis = plt.subplots(10, 10, figsize=(10, 10))
for i in range(10):
for j in range(10):
axis[i, j].imshow(fruits[apple_index[i*10 + j]], cmap='gray_r')
# 좌표축을 그리지 않음
axis[i, j].axis('off')
plt.show()
평균값을 자동으로 찾으며, 그 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centroid)라고 부른다.
k-평균 알고리즘의 작동 방식을 이해하고 대상을 구분하는 비지도 학습 모델을 생성한다.
처음에 랜덤하게 클러스터 중심을 정하고 클러스터를 생성한 후 클러스터의 중심을 이동하고 다시 클러스터를 만드는 식으로 반복하여 최적의 클러스터를 구성하는 알고리즘이다.
k-평균 알고리즘이 만든 클러스터에 속한 샘플의 특성 평균값으로 센트로이드 centroid라고도 불린다. 가장 가까운 클러스터 중심을 샘플의 또 다른 특성으로 사용하거나 새로운 샘플에 대한 예측으로 활용할 수 있다.
import numpy as np
fruits = np.load('fruits_300.npy')
# (샘플개수, 너비, 높이) 크기의 3차원 배열을 (샘플개수, 너비*높이) 크기의 2차원 배열로 변경
fruits_2d = fruits.reshape(-1, 100*100)
# 사이킷런의 k-평균 알고리즘
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)
print(km.labels_)
# n_clusters=3으로 지정했기 때문에 labels_ 배열의 값은 0, 1, 2 중 하나
# [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]
# 레이블 0, 1, 2로 모은 샘플의 개수
print(np.unique(km.labels_, return_counts=True))
# (array([0, 1, 2], dtype=int32), array([ 91, 98, 111]))
첫 번째 클러스터(label 0)가 91개의 샘플을 모았고, 두 번째 클러스터(label 1)가 98개의 샘플을 모았다. 세 번째 클러스터(label 2)는 111개의 샘플을 모았다.
# 각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력
import matplotlib.pyplot as plt
# 함수 정의
def draw_fruits(arr, ratio=1):
# 샘플 개수
n = len(arr)
# 한 줄에 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:
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])
label 0 : only apple
label 1 : only banana
label 2 : 8 apples, 2 bananas, and pineapples
# 클러스터 중심
# fruits_2d 샘플의 클러스터 중심. 이미지로 출력하려면 100*100 크기의 2차원 배열로 변형.
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio=3)
# 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해주는 transform()
print(km.transform(fruits_2d[100:101]))
# 반환된 배열 (1, 클러스터 개수)의 크기인 2차원 배열
# 첫번째 클러스터, 두번째 클러스터, 세번째 클러스터까지의 거리
# [[5267.70439881 8837.37750892 3393.8136117 ]]
# 가장 가까운 클러스터 중심을 예측 클래스로 출력하는 predict()
print(km.predict(fruits_2d[100:101]))
# [2]
draw_fruits(fruits[100:101])
# k-평균 알고리즘은 반복적으로 클러스터 중심을 옮기면서 최적의 클러스터를 찾음
# KMeans 클래스의 n_iter_ 속성에 알고리즘이 반복한 횟수가 저장됨
print(km.n_iter_)
# 데이터셋을 저차원(10000에서 n_iter_)으로 변환
💡 가장 가까운 거리에 있는 클러스터 중심을 샘플의 예측값으로 사용할 수 있다!
군집 알고리즘에서 적절한 k값(클러스터 개수)을 찾기 위한 대표적 방법.
k-평균 알고리즘에서의 클러스터 중심과 클러스터에 속한 샘플 사이의 거리의 제곱 합. 클러스터의 샘플이 얼마나 가깝게 있는지를 나타내는 값.
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.show()
💡 적절한 k값이자 클러스터 개수는 그래프의 꺾이는 부분이다.
클러스터 개수에 따라 이너셔 감소가 꺾이는 지점이 적절한 클러스터 개수 k가 된다.
대표적인 차원 축소 알고리즘으로, 데이터에서 가장 분산이 큰 방향을 찾는 방법이다. 이런 방향을 주성분이라고 하며, 원본 데이터를 주성분에 투영하여 새로운 특성을 만들 수 있다. 일반적으로 주성분은 원본 데이터에 있는 특성 개수보다 작다.
비지도 학습 작업 중 하나. 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법.
from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)
# PCA클래스가 찾은 주성분은 components_에 저장되어 있음
# print(pca.components_)
# n_copmonents=50으로 지정했기 때문에 pca.components_ 배열의 첫 번째 차원이 50.
# 50개의 주성분을 찾음.
# 두 번째 차원은 항상 원본 데이터의 특성 개수와 같은 100*100=10000
print(pca.components_.shape)
# (50, 10000)
draw_fruits(pca.components_.reshape(-1, 100, 100))
print(fruits_2d.shape)
# (300, 10000)
# 특성의 개수를 10000개에서 50개로 줄이기
# 원본 데이터의 차원을 50으로 줄이기
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
# (300, 50)
fruits_pca 배열은 50개의 특성을 가진 데이터가 됨!
# 50개의 차원으로 축소한 fruits_pca 데이터를 전달하여 10000개 특성을 복원
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
# (300, 10000)
# 데이터를 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')
주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 것으로, 사이킷런의 PCA 클래스는 주성분 개수나 설명된 분산의 비율을 지정하여 주성분 분석을 수행할 수 있다.
# 설명된 분산
print(np.sum(pca.explained_variance_ratio_))
# 0.9215576482293821
# 적절한 주성분의 개수를 찾는 방법
plt.plot(pca.explained_variance_ratio_)
처음 10개의 주성분이 대부분의 분산을 표현하고 있으며, 그 다음부터는 각 주성분이 설명하고 있는 분산은 비교적 작다는 것을 알 수 있다.
# 로지스틱 회귀 모델
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
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']))
# 0.9966666666666667
print(np.mean(scores['fit_time']))
# 2.727587032318115
# 차원 축소된 데이터로 교차검증
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
# 1.0 = 정확도 100%
print(np.mean(scores['fit_time']))
# 0.05091152191162109
💡 PCA로 훈련 데이터의 차원을 축소하면 저장 공간 뿐만 아니라 머신러닝 모델의 훈련 속도도 높일 수 있다.
# 설명된 분산의 50%에 달하는 주성분을 찾도록 PCA 모델 생성
# n_components에 원하는 설명된 분산의 비율도 입력 가능
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)
# 찾은 주성분의 개수
print(pca.n_components_)
# 2
2개의 특성만으로 원본 데이터에 있는 분산의 50%를 표현할 수 있다.
# 변환된 데이터의 크기 확인
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
# (300, 2)
# 2개의 특성을 사용했을 때
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
# 0.9933333333333334
print(np.mean(scores['fit_time']))
# 0.07958650588989258
위의 결과는 교차 검증의 결과이며, 로지스틱 회귀 모델이 완전히 수렴하지 못하여 반복 횟수를 증가하라는 Convergence Warning이 출력되나 교차 검증의 결과가 충분히 좋기 때문에 무시해도 된다.
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(fruits[km.labels_ == label])
print('\n')
# 클러스터별로 나누어 산점도 그리기
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()