코로나 걸려서 끙끙 앓고 있던 도중
4주차 우수혼공족 되다.
햅삐 >< 열심히 살겠슴미다
Chapter 6 : 비지도 학습 - 비슷한 과일끼리 모으자!
6-1 군집 알고리즘
<타깃을 모르는 비지도 학습>
비지도 학습(unsupervised learning) ? 타깃이 없을 때 사용하는 머신러닝 알고리즘
- 사이킷런이 결정 트리 알고리즘 제공 (DecisionTreeClassifier)
- plot_tree() : 결정 트리를 이해하기 쉬운 트리 그림으로 출력해주는 함수
- 표준화 전처리가 필요 없음!!!!
<과일 사진 데이터 준비하기>
↓ 과일 사진 데이터 준비하기 코드
!wget https://bit.ly/fruits_300 -O fruits_300.npy
! : 리눅스 셸 명령으로 이해하게 해주는 문자
wget : 원격 주소에서 데이터를 다운로드하여 저장
-O : 저장할 파일 이름을 지정 (fruits_300.npy)
import numpy as np
import matplotlib.pyplot as plt
fruits = np.load('fruits_300.npy')
print(fruits.shape) # (300, 100, 100)
첫 번째 차원(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]
'''
픽셀 100개에 들어있는 값을 출력함
0에 가까울수록 검게 나타나고 값이 높을수록 밝게 표시됨
plt.imshow(fruits[0], cmap='gray')
plt.show()
imshow : 넘파이 배열로 저장된 이미지를 그림
- cmap='gray' : 흑백으로 저장
- cmap='gray_r' : 반전 흑백으로 확인
실제 사진은 바탕이 흰색, 사과가 더 짙은색이나, 출력하면 반전됨
이유는 ? 사진으로 찍은 이미지를 넘파이 배열로 변환할 때 반전시켰기 때문!
굳이 이렇게 바꾼 이유는 ?
컴퓨터는 수치로 해석, 0인 값에 초점을 맞추게 되면 아무리 곱하거나 더해도 0이므로
밝은 색인 255에 초점을 맞춰야 함 -> 우리가 확인하고 싶은 건 사과
=> 따라서 사과를 밝은 색으로 해야 하기 위해 색을 반전시킴
↓ cmap='gray_r'을 통해 원래의 색상으로 확인 가능
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() : 여러 개의 그래프를 배열처럼 쌓을 수 있도록 함
<픽셀값 분석하기>
↓ 배열로 계산하기 위해 100 X 100 이미지를, 길이가 10000인 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)
샘플마다 픽셀의 평균값을 계산해야 함
- mean() 메서드 사용
↓ 각 샘플의 픽셀 평균값 계산하는 코드
# 샘플의 평균값 계산
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]
'''
axis=0 : 행 방향으로 계산
axis=1 : 열 방향으로 계산
# 히스토그램으로 그려 평균값 분포 확인
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 : 히스토그램의 투명도 조절
legend() : 히스토그램의 범례 표시
=> 바나나는 사진에 차지하는 영역이 작아 평균값이 작고
사과와 파인애플은 사진에 차지하는 영역이 커 평균값이 큼
↓ 픽셀의 평균값 계산하는 코드
# 픽셀별 평균값 비교 (not 샘플의 평균값)
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()
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()
↓ 절댓값 오차 계산하는 코드
# 절댓값 계산
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1, 2)) # 각 샘플의 오차 평균
print(abs_mean.shape) # (300,)
# apple_mean과 오차가 가장 작은 샘플 100개 선정
apple_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[apple_index[i*10 + j]], cmap='gray_r')
axs[i, j].axis('off')
plt.show()
np.argsort() : 작은 것에서 큰 순서대로 나열한 abs_mean 배열의 인덱스를 반환하는 함수
axis('off') : 축 제거
군집(clustering) ? 비슷한 샘플끼리 그룹으로 모으는 작업
- 위 코드에서와 같이 비슷한 샘플 100개를 모아 출력
- 대표적인 비지도 학습 중 하나
클러스터(cluster) ? 군집 알고리즘에서 만든 그룹
6-2 k-평균
<k-평균 알고리즘 소개>
실제로는 비지도 학습에서 타깃값을 모르기 때문에 샘플의 평균값을 미리 구할 수 없음
그렇다면 타깃값을 모르는데 어떻게 세 과일의 평균값을 찾을 수 있었을까??
=> k-평균(k-means) 군집 알고리즘이 평균값을 자동으로 찾아줌
- 이 평균값을 클러스터 중심(cluster center) OR 센트로이드(centroid) 라고 부름
k-평균(k-means) 알고리즘 작동 방식
1. 무작위로 k개의 클러스터 중심을 정한다.
2. 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.
3. 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.
4. 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.
기본 숙제(필수): k-평균 알고리즘 작동 방식 설명하기
1. 3개의 클러스터 중심을 랜덤하게 지정한다
-> 클러스터 중심에서 가장 가까운 샘플들을 하나의 클러스터로 묶는다
(클러스터에서 순서나 번호는 의미 X)
-> 클러스터의 중심을 다시 계산하여 이동
- 1번에서 2사과 1파인애플의 경우, 클러스터의 중심이 더 많은 사과쪽으로 이동
2. 다시 계산한 클러스터 중심에서 가장 가까운 샘플들을 하나의 클러스터로 묶는다.
-> 클러스터의 중심을 다시 계산하여 이동
3. 다시 계산한 클러스터 중심에서 가장 가까운 샘플들을 하나의 클러스터로 묶는다.
- 2번 클러스터와 동일
-> 만들어진 클러스터에 변동이 없으므로 k-평균 알고리즘을 종료!!
<KMeans 클래스>
사이킷런에 KMeans 클래스 구현되어 있음!!
- n_cluster : 클러스터 개수 지정
↓ KMeans 클래스 사용하는 코드
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)
print(km.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]
'''
비지도 학습이므로 fit() 에서 타깃 데이터를 사용하지 않음!!!
군집된 결과는 KMeans 클래스 객체의 labels_ 속성에 저장됨
# 레이블로 모은 샘플의 개수 확인
print(np.unique(km.labels_, return_counts=True))
# (array([0, 1, 2], dtype=int32), array([ 91, 98, 111]))
첫 번째 클러스터(0) : 91
두 번째 클러스터(1) : 98
세 번째 클러스터(2) : 111
# 각 클러스터가 어떤 이미지를 나타냈는지 그림으로 출력하는 함수 출력
import matplotlib.pyplot as plt
def draw_fruits(arr, ratio=1):
n = len(arr) # 샘플 개수
rows = int(np.ceil(n/10)) # 한 줄에 10개씩 이미지 그림
cols = n if rows < 2 else 10 # 행의 개수==1 ? 열의개수==샘플개수 : 열의개수==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])
# km.labels_ 배열에서 값이 0인 위치는 True, 그 외는 False
draw_fruits(fruits[km.labels_==1])
draw_fruits(fruits[km.labels_==2])
=> 완벽하게 분리되진 않음
<클러스터 중심>
KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers 속성에 저장되어 있음
- transform() : 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해주는 메서드
↓ 코드
# 2차원 배열로 바꿈
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio=3)
# 인덱스가 100인 샘플에 transform() 메서드 적용
print(km.transform(fruits_2d[100:101]))
#[[5267.70439881 8837.37750892 3393.8136117 ]]
print(km.predict(fruits_2d[100:101]))
# [2]
index=2가 3393으로 제일 가까움 -> 파인애플로 예측
<최적의 k 찾기>
k-평균 알고리즘의 단점 : 클러스터 개수를 사전에 지정해야 함
그렇다면 어떻게 최적의 클러스터 개수를 찾을 수 있을까?
=> 엘보우 사용
엘보우(elbow) ? 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법
- 이너셔(inertia) ? 클러스터의 샘플이 얼마나 가깝게 있는지를 나타내는 값
- 클러스터 중심과 클러스터에 속한 샘플 사이의 거리의 제곱합으로 나타냄
- 일반적으로 클러스터 개수 ↑ -> 클러스터 개개의 크기 ↓ -> 이너셔 ↓
- KMeans 클래스에서 자동으로 이너셔를 계산 -> inertia_ 속성으로 제공
이너셔의 그래프에서 꺾이는 지점이 최적의 클러스터 개수 !!
↓ 이너셔를 그래프로 나타내는 코드
inertia = []
for k in range(2, 7): # 클러스터 개수를 2~6까지 바꿔가며 훈련
km = KMeans(n_clusters=k, random_state=42)
km.fit(fruits_2d)
inertia.append(km.inertia_)
plt.plot(range(2, 7), inertia)
plt.show()
6-3 주성분 분석
<차원과 차원 축소>
차원(dimension) ? 특성의 또 다른 이름
- ex) 10,000개의 특성이 있다 == 10,000개의 차원이 있다
차원 축소(dimensionally reduction) ? 데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법
- 대표적인 차원 축소 알고리즘 : 주성분 분석(principal component analysis) (a.k.a PCA)
<주성분 분석 소개>
주성분 분석(principal component analysis) ? 데이터에 있는 분산이 큰 방향을 찾는 방법
- 분산 ? 데이터가 멀리 퍼져있는 정도
위 그래프의 경우, 대각선 방향이 분산이 크다는 것을 알 수 있음
- 화살표의 위치는 상관 X
이 직선을 원점으로 옮긴 후. 두 원소로 이루어진 벡터로 쓸 수 있음
=> 주성분(principal component) 라고 부름
주성분 벡터 : 원본 데이터에 있는 어떤 방향
=> 주성분 벡터의 원소 개수 == 원본 데이터셋에 있는 특성 개수
원본 데이터 : 주성분을 사용하여 차원 축소 가능 -> 투영
<PCA 클래스>
↓ PCA 클래스를 사용하여 차원 축소하는 코드
from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)
print(pca.components_.shape) # (50, 10000)
n_components : 주성분의 개수
PCA 클래스가 찾은 주성분은 components_ 속성에 저장
- 첫 번째 차원 : 주성분의 개수가 50이므로 50
- 두 번째 차원 : 원본 데이터의 특성 개수와 같은 10000
# 주성분을 이미지처럼 출력
draw_fruits(pca.components_.reshape(-1, 100, 100))
원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타냄
# 주성분을 찾았으니, 차원 줄이기
print(fruits_2d.shape) # (300, 10000)
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape) # (300, 50)
<원본 데이터 재구성>
inverse_transform() : 원본 데이터를 상당 부분 재구성하는 메서드
↓ 원본 데이터 재구성하는 코드
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape) # (300, 10000)
# 복원된 데이터를 100개씩 나누어 출력
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
draw_fruits(fruits_reconstruct[start:start+100])
print("\n")
<설명된 분산>
설명된 분산(explained variance) ? 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값
- PCA 클래스의 explained_variance_ratio_에 설명된 분산 비율이 기록되어 있음
- 첫 번째 주성분의 설명된 분산이 가장 큼
↓ 설명된 분산, 총 분산 비율 구하는 코드
# 총 분산 비율
print(np.sum(pca.explained_variance_ratio_))
# 0.9215333578580416
=> 복원했을 때의 원본 이미지의 품질이 높은 이유임!!
<다른 알고리즘과 함께 사용하기>
↓ 로지스틱 회귀 알고리즘과 같이 사용하는 코드
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
target = np.array([0]*100 + [1]*100 + [2]*100)
# cross_validate 사용해서 교차 검증 실행
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
# 1.6808361053466796
# PCA로 축소한 fruits_pca를 사용했을 떄와 비교
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
# 1.0
# 0.07098498344421386
=> 훈련 시간이 1.6 -> 0.07로 대폭 감소됨!!
=> PCA로 훈련 데이터의 차원을 축소하면 저장 공간뿐만 아니라 모델의 훈련 속도도 높일 수 있다!!
# 설명된 분산의 50%에 달하는 주성분 찾기
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개이므로 변환된 데이터의 크기는 (300, 2)
# 교차 검증
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score']))
print(np.mean(scores['fit_time']))
# 0.9933333333333334
# 0.030288887023925782
↓ k-평균 알고리즘으로 클러스터 찾는 코드
# 차원 축소된 데이터를 사용해 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")
# 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()
추가 숙제(선택): Ch.06(06-3) 확인 문제 풀고, 풀이 과정 정리하기
2. 샘플 개수가 1,000개이고 특성 개수는 100개인 데이터셋이 있습니다. 즉 이 데이터 셋의 크기는 (1000, 100)입니다. 이 데이터를 사이킷런의 PCA 클래스를 사용해 10개의 주성분을 찾아 변환했습니다. 변환된 데이터셋의 크기는 얼마일까요?
(1000, 10) / (10, 1000) / (10, 10) / (1000, 1000)
3. 2번 문제에서 설명된 분산이 가장 큰 주성분은 몇 번째인가요?
첫 번째 주성분 / 다섯 번째 주성분 / 열 번째 주성분 / 알 수 없음
풀이 과정 : 주성분 분석은 가장 분산이 큰 뱡향부터 순서대로 찾기 때문에, 당연히 첫 번째 주성분이 가장 클 수밖에 없다.
'혼공머신' 카테고리의 다른 글
[혼공머신] 6주차 : ch7) 딥러닝을 시작합니다 (0) | 2024.08.18 |
---|---|
[혼공머신] 4주차 : ch5) 트리 알고리즘 (0) | 2024.07.28 |
[혼공머신] 3주차 : ch4 (0) | 2024.07.21 |
[혼공머신] 2주차 : ch3 (11) | 2024.07.14 |
[혼공머신] 1주차 : ch1, ch2 (0) | 2024.07.06 |