혼공 ML+DL

5주차 Chapter 06 (비지도 학습)

채영sw 2024. 2. 27. 09:25

Chapter 06

6-1. 군집 알고리즘

  • 히스토그램(histogram) : 값이 발생한 빈도를 그래프로 표시한 것으로 보통 x축이 값의 구간(계급)이고, y축은 발생 빈도(도수)임.
  • 군집(clustering) : 비슷한 샘플끼리 그룹으로 모으는 작업으로 대표적인 비지도 학습 작업 중 하나

- 타깃을 모르는 비지도 학습

#사진 데이터 준비하기 -> 파일 다운로드
!wget https://bit.ly/fruits_300_data -O fruits_300.npy

코랩은 ! 로 시작하면 이후 명령을 파이썬 코드가 아니라 리눅스 셀(shell) 명령으로 이해함.

wget 명령 : 우너격 주소에서 데이터를 다운로드하여 저장

-O 옵션 : 저장할 파일 이름 지정

#데이터 로드
import numpy as np
import matplotlib.pyplot as plt
fruits = np.load('fruits_300.npy')
#배열의 크기 화인
print(fruits.shape)
(300, 100, 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]
#imshow 함수로 넘파이 배열로 저장된 이미지 그리기
plt.imshow(fruits[0], cmap='gray') # 흑백 이미지
plt.show()

0에 가까울수록 검게 나타남

이 흑백 이미지는 사진으로 찍은 이미지를 넘파이 배열로 변환할 때 반전시킨 것 (컴퓨터는 255에 가까운 바탕에 집중)

# cmap='gray_r' 다시 반전하여 우리 눈에 보기 좋게 출력
plt.imshow(fruits[0], cmap='gray_r')
plt.show()

이 그림에선 밝은 부분이 0에 가깝고 짙은 부분이 255에 가까운 값

 

fig, axs = plt.subplots(1, 2) # subplots : 여러 개의 그래프를 배열처럼 쌓을 수 있도록 도움(행, 열 지정)
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
plt.show()

 

- 픽셀값 분석하기

#reshape 함수를 이용해 두 번째 차원과 세 번째 차원을 100000으로 합침
#첫 번째 차원을 -1로 지정하면 자동으로 남은 차원을 할당

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 - 가로 계산)
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]

 

# 맷플롯립의 hist() 함수로 히스토그램 그리기
plt.hist(np.mean(apple, axis=1), alpha=0.8) #alpha 값이 1보다 작으면 투명도를 줄 수 있음
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']) #legend 함수를 사용해 범례 만들 수 있음
plt.show()

 

#샘플의 평균값이 아닌 픽셀별 평균값 비교
fig, axs = plt.subplots(1, 3, figsize=(20, 5))
axs[0].bar(range(10000), np.mean(apple, axis=0)) #bar() 함수를 사용해 평균값을 막대 그래프로 그리기
axs[1].bar(range(10000), np.mean(pineapple, axis=0)) #픽셀의 평균 구하기 - axis=0 으로 설정
axs[2].bar(range(10000), np.mean(banana, axis=0))
plt.show()

#픽셀 평균값을 100 X 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()

 

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

#절댓값 오차 사용
abs_diff = np.abs(fruits - apple_mean) #abs_diff는 (300,100,100) 크기의 배열
abs_mean = np.mean(abs_diff, axis=(1,2)) #각 샘플에 대한 평균을 구하기 위해 axis 1, 2 모두 지정
print(abs_mean.shape)
(300,)
#값이 작은 순서대로 100개 고르기
apple_index = np.argsort(abs_mean)[:100] # np.argsort() 함수 : 작은 것에서 큰 순서대로 내열한 배열의 인덱스 반환
fig, axs = plt.subplots(10, 10, figsize=(10,10)) # subplots: 10X10 총 100개의 서브 그래프 만들기, figsize: 그래프의 크기(기본값 8,6)
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') # 깔끔하게 이미지만 그리기 위해 좌표축을 그리지 않음
plt.show()

abs_diff = np.abs(fruits - banana_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))

banana_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[banana_index[i*10 + j]], cmap='gray_r')
        axs[i, j].axis('off')
plt.show()

 

 

6-2. k-평균

  • k-평균 알고리즘(k-means algorithm) : 처음에 랜덤하게 클러스터 중심을 정하여 클러스터를 만들고 그다음 클러스터의 중심을 이동하여 다시 클러스터를 결정하는 식으로 반복해서 최적의 클러스터를 구성하는 알고리즘.
    • 평균값을 자동으로 찾아줌. 이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심 또는 센트로이드(centroid) 라고 부름.

k-평균 알고리즘의 작동 방식

1. 무작위로 k개의 클러스터 중심을 정한다.

2. 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.

3. 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.

4. 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.

 

- KMeans 클래스

!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) #k평균 모델을 훈련하기 위해 3차원 배열을 (샘플 개수, 너비X높이) 크기를 가진 2차원 배열로 변경
from sklearn.cluster import KMeans

km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d) #비지도 학습이므로 fit() 함수에서 타깃 데이터를 사용하지 않음

 

# 군집된 결과, labels_의 길이는 샘플 개수로 각 샘플이 어떤 레이블에 해당되는지 나타냄
print(km.labels_)
[2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 0 2 0 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 0 0 2 2 2 2 2 2 2 2 0 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 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 0 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 0 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([111,  98,  91]))

 

import matplotlib.pyplot as plt

def draw_fruits(arr, ratio=1): # figsize는 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()
#배열에서 값이 0인 위치는 True 그외는 False, True인 위치의 원소만 모두 추출
draw_fruits(fruits[km.labels_==0])

 

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

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

 

- 클러스터 중심

최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있음

이 배열은 fruits_2d 샘플의 클러스터 중심이기 때문에 이미지로 출력하려면 100X100 크기의 2차원 배열로 바꿔야 함.

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

KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해 주는 transform() 메서드를 가지고 있음

-> 특성값을 변환하는 도구로 사용 가능

print(km.transform(fruits_2d[100:101])) #슬라이싱 연산자를 이용해 (1,10000) 크기의 배열 전달
[[3393.8136117  8837.37750892 5267.70439881]]
# 가장 가까운 클러스터 중심을 예측하여 출력(가장 작은 수)
print(km.predict(fruits_2d[100:101]))
[0]
#확인
draw_fruits(fruits[100:101])

# 알고리즘이 반복한 횟수
print(km.n_iter_)
4

 

 

- 최적의 k 찾기

  • 이너셔(inertia) : k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있는데 이 거리의 제곱 합을 이너셔라고 함. 즉 클러스터의 샘플이 얼마나 가깝게 있는지를 나타내는 값임.
  • 엘보우(elbow) : 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법

#KMeans 클래스는 자동으로 이너셔를 계산해서 inertia_ 속성으로 제공
inertia = []
for k in range(2, 7):
    km = KMeans(n_clusters=k, n_init='auto', random_state=42)
    km.fit(fruits_2d)
    inertia.append(km.inertia_)

plt.plot(range(2, 7), inertia)
plt.xlabel('k')
plt.ylabel('inertia')
plt.show()

엘보우 지점보다 클러스터 개수가 많아지면 이너셔의 변화가 줄어들면서 군집 효과도 줄어듦. 

 

6-3. 주성분 분석

  • 차원 축소(dimensionality reduction) : 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고 지도 하급 모델의 성능을 향상시킬 수 있는 방법, 줄어든 차원을 다시 원본 차원으로 손실을 최대한 줄이면서 복원할수 있음. 
  • 주성분 분석(principal component analysis, PCA) : 차원 축소 알고리즘의 하나로 데이터에서 가장 분산이 큰 방향을 찾는 방법이며 이런 방향을 주성분이라 함. 원본 데이터를 주성분에 투영하여 새로운 특성을 만들 수 있음.

주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어듦!

일반적으로 원본 특성의 개수만큼 찾을 수 있음.

 

- PCA 클래스

# 과일 사진 데이터 다운로드 및 넘파이 배열로 적재
!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)
from sklearn.decomposition import PCA

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

 

#PCA 클래스가 찾은 주성분은 components_ 속성에 저장되어 있음
print(pca.components_.shape)
(50, 10000) #두 번째 차원은 항상 원본 데이터의 특성 개수와 같음

 

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()
draw_fruits(pca.components_.reshape(-1, 100, 100))

원본 데이터를 주성분에 투영하여 특성의 개수를 10000개에서 50개로 줄일 수 있음

print(fruits_2d.shape)
fruits_pca = pca.transform(fruits_2d) #transform 함수를 사용해 원본 데이터의 차원 줄이기
print(fruits_pca.shape)
(300, 10000)
(300, 50)

 

 

- 원본 데이터 재구성

#특성 복원
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100) #100X100 크기로 바꾸어 100개씩 나누어 출력
for start in [0, 100, 200]:
    draw_fruits(fruits_reconstruct[start:start+100])
    print("\n")

 

- 설명된 분산

  • 설명된 분산(explained variance) :주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값

PCA 클래스의 explained_variance_ratio_ 에 각 주성분의 설명된 분산 비율이 기록되어 있음. 첫 번째 주성분의 설명된 분산이 가장 크며, 이 분산 비율을 모두 더하면 50개의 주성분으로 표현하고 있는 총 분산 비율을 얻을 수 있음.

print(np.sum(pca.explained_variance_ratio_))
0.9215651897863715
#설명된 분산의 비율을 그래프로 그려 적절한 주성분의 개수를 찾는 데 도움을 받을 수 있음
plt.plot(pca.explained_variance_ratio_)

 

 

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

 

로지스틱 회귀

#로지스틱 회귀
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()

 

지도 학습 모델을 사용하려면 타깃값이 있어야 함.

target = np.array([0] * 100 + [1] * 100 + [2] * 100)
from sklearn.model_selection import cross_validate
#먼저 원본 데이터인 fruits_2d 사용
scores = cross_validate(lr, fruits_2d, target) #cross_validate로 교차 검증 수행
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
0.9966666666666667 #교차 검증 점수
1.819899892807007 #교차 검증 폴드의 훈련 시간
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
1.0
0.032833099365234375

 

차원 축소는 저장 공간뿐만 아니라 머신러닝 모델의 훈련 속도도 높일 수 있음

pca = PCA(n_components=0.5) #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 #2개의 특성을 사용했을 뿐인데 정확도 높음
0.03713240623474121

 

k- 평균 알고리즘

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]))
#KMeans가 찾은 레이블을 사용해 과일 이미지 출력
for label in range(0, 3):
    draw_fruits(fruits[km.labels_ == label])
    print("\n")

 

훈련 데이터의 차원을 줄이면 시각화하기 비교적 쉬워짐

 

fruits_pca : 2개의 특성이 있기 때문에 2차원으로 표현 가능

# [km.labels_를 사용해 클러스터별로 나누어 산점도 그리기
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()