[혼공머신] 비지도 학습

Jinyeong Choi·2024년 8월 27일
0

ML

목록 보기
6/6
post-thumbnail

비지도 학습

타깃을 모르는 비지도 학습

타깃을 모르는 사진을 종류별로 분류. 타깃이 없을 때 사용하는 머신러닝 알고리즘.
사람이 가르쳐 주지 않아도 데이터에 있는 무언가를 학습하는 것. 대표적인 비지도 학습 작업은 군집, 차원 축소 등이다.

군집 cluster

비슷한 샘플끼리 하나의 그룹으로 모으는 대표적인 비지도 학습 작업으로, 군집 알고리즘으로 모은 샘플 그룹을 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()

k-평균 군집 알고리즘

평균값을 자동으로 찾으며, 그 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centroid)라고 부른다.
k-평균 알고리즘의 작동 방식을 이해하고 대상을 구분하는 비지도 학습 모델을 생성한다.

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_)으로 변환

💡 가장 가까운 거리에 있는 클러스터 중심을 샘플의 예측값으로 사용할 수 있다!

엘보우 elbow 방법

군집 알고리즘에서 적절한 k값(클러스터 개수)을 찾기 위한 대표적 방법.

이너셔 inertia

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가 된다.

주성분 분석 PCA

대표적인 차원 축소 알고리즘으로, 데이터에서 가장 분산이 큰 방향을 찾는 방법이다. 이런 방향을 주성분이라고 하며, 원본 데이터를 주성분에 투영하여 새로운 특성을 만들 수 있다. 일반적으로 주성분은 원본 데이터에 있는 특성 개수보다 작다.

차원 축소

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

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

profile
Hang in there

0개의 댓글

관련 채용 정보