6장에서 k-평균 알고리즘을 공부하면서 제공된 정답 데이터가 없음에도 불구하고, 최적의 클러스터 개수를 찾아 학습하는 과정이 재미있고 흥미로웠다. (신기했다는 표현이 더 맞을 것 같다.) 그치만 주성분 분석의 내용은 이해했지만 세부 원리는 잘 이해가 되지 않았다. 과제를 제출하고 320~322 쪽을 다시 읽어보고, 관련 자료도 좀 더 찾아봐야겠다.
그리고 이번 장은 공부하면서 "이건 업무에 적용할 수 있지 않을까?"라는 생각을 했다. (물론 더 깊게 공부해봐야 겠지만...) 현업에서 활용할 만한 부분이 있을 것 같은 느낌이 드니 공부할 때도 좀 더 즐겁고 집중하며 볼 수 있었다. 어떻게 쓸 수 있을지 고민해봐야겠다.
이제 마지막 6주차가 남았다. 다음주부터 리프레시 휴가로 설날까지 쭉 쉬니 7~9장까지 딥러닝 내용을 쭉 완독해봐야 겠다.
import numpy as np
import matplotlib.pyplot as plt
# 다운로드 한 fruits_300.npy 파일 불러오기
fruits = np.load('fruits_300.npy')
print(f"""# 배열 크기 확인
배열 크기 : {fruits.shape}
- 첫 번째 차원 : 샘플의 개수
- 두 번째 차원 : 이미지 높이
- 세 번째 차원 : 이미지 너비
""")
# 배열 크기 확인
배열 크기 : (300, 100, 100)
- 첫 번째 차원 : 샘플의 개수
- 두 번째 차원 : 이미지 높이
- 세 번째 차원 : 이미지 너비
# 첫 번째 이미지 출력
print("# 첫 번째 이미지 출력")
plt.imshow(fruits[0], cmap='gray')
plt.show()
# 첫 번째 이미지 출력
# 첫 번째 이미지의 첫 번째 행 출력
print(f"""# 첫 번째 이미지의 첫 번째 행 출력
{fruits[0, 0, :]}
- 값이 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에 가까우면 검게 나타나고, 높은 값은 밝게 표시된다.
- 원래는 사과가 어두운 색이고, 바탕이 밝은 색이지만, 넘파이 배열로 변환할 때 반전되었다.
- 알고리즘이 출력을 만들기 위해 덧셈, 곱셈을 하는데 픽셀값이 0이 되면 의미가 없다.
그리고 픽셀값이 높을 수록 출력값이 커져서 의미를 부여하기 좋다.
우리의 관심사는 "사과"이기 때문에 사과의 값을 큰 값(밝은색)으로 반전한다.
# 첫 번째 이미지 값을 재반전 하여 출력
print("# 첫 번째 이미지 재반전 출력")
plt.imshow(fruits[0], cmap='gray_r')
plt.show()
# 첫 번째 이미지 재반전 출력
# matplotlib의 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()
# 픽셀값 분석하기
# - 사과, 파인애플, 바나나 100개씩
# - 하나의 100x100 이미지를 펼쳐서 길이가 10,000인 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(f"""# 배열의 크기
apple: {apple.shape}
pineapple: {pineapple.shape}
banana: {banana.shape}
""")
# 픽셀 평균값에 대한 히스토그램
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)
print(f""" 결과 해석
- 바나나 사진의 평균값은 40에 집중되어 있음
- 사과와 파인애플 사진의 평균값은 90~100 사이에 모여 있음
- 따라서 픽셀 평균값만으로도 바나나를 구분할 수 있고, 사과와 파인애플은 구분하기 어려움
""")
결과 해석
- 바나나 사진의 평균값은 40에 집중되어 있음
- 사과와 파인애플 사진의 평균값은 90~100 사이에 모여 있음
- 따라서 픽셀 평균값만으로도 바나나를 구분할 수 있고, 사과와 파인애플은 구분하기 어려움
# 픽셀별 평균값 구하기
# - 전체 샘플에 대한 각 픽셀의 평균 계산 > 과일마다 모양이 다르므로 픽셀값이 높은 위치가 다름
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()
###
# 평균값과 가까운 사진 고르기
###
print("# 사과 사진 100개 찾기")
# 사과 샘플에 대한 평균
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
print(abs_mean.shape)
# 사과 샘플 평균값과 오차가 가장 작은 샘플 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()
# 사과 사진 100개 찾기
(300,)
군집(Clustering)
비슷한 샘플끼리 그룹으로 모으는 작업
대표적인 비지도 학습 작업 중 하나
군집 알고리즘에서 만든 그룹을 클러스터(Cluster)라고 한다.
k-평균(k-means) 군집 알고리즘
개요
자동으로 평균값을 찾아주는 군집 알고리즘
평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centroid)라고 부른다.
작동 방식
무작위로 k개의 클러스터 중심을 정한다.
각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.
클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.
클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.
KMeans 클래스
# 데이터 다운로드
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
# npy 파일을 읽어 넘파이 배열 생성
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
# 사이킷런의 k-평균 알고리즘 : KMeans 클래스
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)
print(f"""# 군집 결과 - 각 샘플이 어떤 레이블에 해당하는지 나타낸다. (n_clusters=3이기 때문에 0, 1, 2 중 하나의 값으로 표시)
{km.labels_}
""")
print(f"""# 각 군집 별 개수
{np.unique(km.labels_, return_counts=True)}
""")
import matplotlib.pyplot as plt
def draw_fruits(arr, ratio=1):
# 샘플 개수
n = len(arr)
# 한 줄에 10개씩 이미지 출력
rows = int(np.ceil(n/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()
# 레이블이 0인 과일 사진 출력
draw_fruits(fruits[km.labels_==0])
# 레이블이 1인 과일 사진 출력
draw_fruits(fruits[km.labels_==1])
# 레이블이 2인 과일 사진 출력
draw_fruits(fruits[km.labels_==2])
클러스터 중심
KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_centers_ 속성에 저장되어 있다.
KMeans 클래스는 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해주는 transform() 메서드를 제공한다.
# 인덱스가 100인 샘플에 transform() 메서드 적용
print(f"""# 인덱스 100인 샘플의 각 레이블 별 클러스터 중심까지의 거리
{km.transform(fruits_2d[100:101])}
# 인덱스 100인 샘플의 가장 가까운 클러스터 중심 레이블
{km.predict(fruits_2d[100:101])}
# 인덱스 100인 샘플 이미지""")
draw_fruits(fruits[100:101])
print(f"\n# k-평균 알고리즘이 최적의 클러스터를 찾기 위해 반복한 횟수 : {km.n_iter_}")
# 인덱스 100인 샘플의 각 레이블 별 클러스터 중심까지의 거리
[[3393.8136117 8837.37750892 5267.70439881]]
# 인덱스 100인 샘플의 가장 가까운 클러스터 중심 레이블
[0]
# 인덱스 100인 샘플 이미지
# k-평균 알고리즘이 최적의 클러스터를 찾기 위해 반복한 횟수 : 4
최적의 k 찾기
k-평균 알고리즘의 단점
클러스터 개수를 사전에 지정해야 함
하지만 실전에서는 몇 개의 클러스터가 있는지 알 수 없음
군집 알고리즘에서 적절한 k 값을 찾기 위한 완벽한 방법은 없음
이너셔(inertia)
클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값
클러스터 중심과 클러스터에 속한 샘플 사이의 거리의 제곱 합
엘보우(elbow) 방법
클러스터 개수를 늘려가면서 이너셔(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.xlabel('k')
plt.ylabel('inertia')
plt.show()
print("# 최적의 클러스터 개수 : 3 (그래프의 기울기가 가장 바뀐 지점)")
# 최적의 클러스터 개수 : 3 (그래프의 기울기가 가장 바뀐 지점)
주성분 분석
차원
데이터가 가진 속성 = 특성 = 차원(dimension)
1차원 배열 : 1차원 배열은 벡터라고 하고, 원소의 개수를 차원이라고 한다.
다차원 배열 : 배열의 축 개수를 차원이라고 한다. 2차원 배열은 행과 열이 차원이다.
차원을 줄이면 저장 공간을 절약할 수 있다. → 차원 축소
차원 축소 (dimensionality reduction)
특성이 많으면 선형 모델의 성능이 높아지고, 훈련 데이터에 쉽게 과대적합 된다.
차원 축소
데이터를 가장 잘 나타내는 일부 특성을 선택하여 데이터 크기를 줄인다.
지도 학습 모델의 성능을 향상시킬 수 있는 방법
대표적인 차원 축소 알고리즘 : 주성분 분석(PCA, Principal Component Analysis)
주성분 분석 (PCA, Principal Component Analysis)
데이터에 있는 분산이 큰 방향을 찾는 것
분산이 큰 방향 = 데이터를 잘 표현하는 어떤 벡터 = 주성분(principal component)
주성분을 사용하여 원본 데이터의 차원을 줄일 수 있다.
주성분은 원본 차원과 같고, 주성분으로 바꾼 데이터는 차원이 줄어든다.
일반적으로 원본 특성의 개수만큼 주성분을 찾을 수 있다.
PCA 클래스
사이킷런의 PCA 클래스를 사용해 주성분 분석을 할 수 있다.
비지도 학습이므로 fit() 메서드에 타깃값을 제공하지 않는다.
# 과일 사진 데이터 다운로드
!wget https://bit.ly/fruits_300_data -O fruits_300.npy
output
# 차원 축소
before : (300, 10000)
after : (300, 50)
원본 데이터 재구성
PCA 클래스의 inverse_transform() 메서드 사용
fruits_inverse = pca.inverse_transform(fruits_pca)
print(f"""# 원본 데이터 재구성
before : {fruits_pca.shape}
after : {fruits_inverse.shape}
""")
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
draw_fruits(fruits_reconstruct[start:start+100])
print("\n")
# 원본 데이터 재구성
before : (300, 50)
after : (300, 10000)
설명된 분산(explained variance)
설명된 분산 : 주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값
explained_variance_ratio_ 값으로 주성분의 설명된 분산 비율을 볼 수 있다.
첫 번째 주성분의 설명된 분산이 가장 크다.
분산 비율을 모두 더하면 총 분산 비율을 얻을 수 있다.
print(f"# 총 분산 비율 : {np.sum(pca.explained_variance_ratio_) * 100}%\n")
print("# 설명된 분산을 그래프로 출력")
plt.plot(pca.explained_variance_ratio_)
plt.show()
print("=> 처음 10개의 주성분이 대부분의 분산을 표현한다.")
# 총 분산 비율 : 92.15811232595493%
# 설명된 분산을 그래프로 출력
=> 처음 10개의 주성분이 대부분의 분산을 표현한다.
다른 알고리즘과 함께 사용하기
과일 사진 원본 데이터와 PCA로 축소한 데이터를 지도 학습에 적용해 보고, 어떤 차이가 있는지 알아보기 → 로지스틱 회귀 모델 사용
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(max_iter=1000)
# 타깃값 생성 (0: 사과, 1: 파인애플, 2: 바나나)
target = np.array([0]*100 + [1]*100 + [2]*100)
print(f"""# 타깃값 생성
{target}
""")
from sklearn.model_selection import cross_validate
# 원본 데이터
scores = cross_validate(lr, fruits_2d, target)
print(f"""# 원본 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : {np.mean(scores['test_score'])}
fit_time : {np.mean(scores['fit_time'])}
""")
# PCA로 축소한 데이터
scores = cross_validate(lr, fruits_pca, target)
print(f"""# PCA로 축소한 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : {np.mean(scores['test_score'])}
fit_time : {np.mean(scores['fit_time'])}
""")
print("=> PCA로 특성을 50개로 축소한 데이터의 정확도가 더 높거나 같고, 훈련 시간도 훨씬 빠르다.")
# 원본 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : 0.9966666666666667
fit_time : 1.981255006790161
# PCA로 축소한 데이터 - 로지스틱 회귀 모델 교차 검증 결과
test_score : 0.9966666666666667
fit_time : 0.04311800003051758
=> PCA로 특성을 50개로 축소한 데이터의 정확도가 더 높고, 훈련 시간도 훨씬 빠르다.
# 직접 주성분 개수를 지정하는 대신 <설명된 분산의 비율>을 지정하면,
# PCA 클래스는 지정된 비율에 도달할 때까지 자동으로 주성분을 찾는다.
pca = PCA(n_components=0.5)
pca.fit(fruits_2d)
print(f"# 자동으로 찾은 주성분 개수 : {pca.n_components_}\n")
# 원본 데이터 변환
fruits_pca = pca.transform(fruits_2d)
print(f"# 변환된 데이터 크기 : {fruits_pca.shape}")
# 교차 검증
scores = cross_validate(lr, fruits_pca, target)
print(f"교차 검증 점수 : {np.mean(scores['test_score'])}")
print(f"교차 검증 수행 시간 : {np.mean(scores['fit_time'])}")
# 자동으로 찾은 주성분 개수 : 2
# 변환된 데이터 크기 : (300, 2)
교차 검증 점수 : 0.9966666666666667
교차 검증 수행 시간 : 0.04672560691833496
# 차원 축소된 데이터를 사용해 k-평균 알고리즘으로 클러스터 찾기
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42, n_init='auto')
km.fit(fruits_pca)
print(np.unique(km.labels_, return_counts=True))