
기본 숙제#
k-평균 알고리즘 작동 방식 설명하기
k-평균 알고리즘은 데이터를 k개의 클러스터로 그룹화하는 비지도 학습 알고리즘입니다.
- 초기 중심점 설정: k개의 클러스터 중심점(centroid)을 랜덤하게 배치합니다.
- 데이터 할당: 각 데이터 포인트를 가장 가까운 중심점의 클러스터에 할당합니다.
- 중심점 업데이트: 각 클러스터에 속한 데이터들의 평균 위치로 중심점을 이동시킵니다.
- 수렴까지 반복: 중심점이 더 이상 움직이지 않거나 변화가 미미할 때까지 2-3단계를 반복합니다.
핵심 개념
- 거리 측정: 주로 유클리드 거리를 사용하여 가장 가까운 중심점을 찾습니다
- 목표: 클러스터 내 분산을 최소화하고 클러스터 간 분산을 최대화
- k값 선택: 적절한 클러스터 개수는 엘보우 방법이나 실루엣 분석으로 결정
장점으로는 구현이 간단하고 빠르며, 구형 클러스터에 효과적입니다. 단점은 k값을 미리 정해야 하고, 초기값에 민감하며, 비구형 클러스터 처리에 한계가 있습니다.
내용 정리#
이제 5주차와 6주차를 하면 블로그 내용 정리는 끝나는 것 같네요.
아직 챕터는 더 있는거 같은데.. 아마 내용이 많거나 실습이 어렵거나.. 어떠한 이유가 있을거라 생각되네요.
군집 알고리즘#
고객이 올린 사진을 자동으로 분류하면 좋을 것 같은 이슈가 생겼네요. 타깃을 알지 못하는 상태에서 사진을 종류대로 모을 수 있는 방법에 대해 학습할 것 같습니다.

위 내용을 보니 몇년 전에 문득 봤던 이런 짤이 떠오르네요.
이제는 AI가 모든 분야에서 언급되고 있네요. 제조업에서는 품질 검사와 설비 관리, 의료 분야에서는 영상 진단, 농업에서도 작물 모니터링에 AI가 활용되고 있습니다.
요즘은 거의 모든 서비스가 “AI 기반"이라는 수식어를 달고 나오는 느낌이네요.
타깃을 모르는 비지도 학습#
위 처럼 타깃을 모르는 사진을 모르는 사진을 종류별로 분류하고 싶을 때 사용하는 머신러닝 알고리즘을 비지도 학습 이라고 합니다.
과일 사진 데이터 준비하기#
이제는 코랩에 실제 이미지를 넣어서 작업을 하는 것 같습니다.
1
| !wget https://bit.ly/fruits_300_data -O fruits_300.npy
|
위와 같이 입력하면 파일 탭에 fruits_300.npy
로 저장된 것을 확인할 수 있습니다.
npy
확장자가 생소해 찾아보니 아래와 같습니다.
NumPy 라이브러리에서 사용하는 바이너리 파일 형식입니다.
.npy 파일의 특징
- NumPy 배열(ndarray)을 효율적으로 저장하는 바이너리 형식
- Python의 pickle보다 빠르고 안전함
- 배열의 shape, dtype, 데이터를 모두 보존
- 플랫폼 독립적 (endianness 처리)
머신러닝 모델의 가중치 데이터를 저장하는데 쓰는 것 같아요.
간단하게 생각하면 개랑 고양이 동물 사진을 비교해야 하면 해당 이미지 데이터가 있는 파일이라고 보면 될 것 같아요.
각각의 이미지의 데이터(이미지 히스토그램 데이터 같은)를 저장하고 있는 데이터라고 보면 좋을 것 같아요.
결국 지금 실습에서는 과일의 이름은 모르지만 비슷하게 생긴 것을 분류하고 싶기 때문에 아래와 같이 과일 300개 npy 파일을 통해서 학습을 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 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, :]) # 첫 번째 이미지의 첫 번째 행 픽셀 100개의 값
[ 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]
|
numpy
라이브러리를 통해 과일의 shape
함수를 호출하면 (300, 100, 100) 의 결과를 볼 수 있습니다.
첫번째 차원(300)은 샘플의 개수, 두 번째 차원(100)은 이미지 높이, 세 번째 차원(100)은 이미지 너비를 나타냅니다.
위에서 첫번째 과일 값을 출력하면 100개의 픽셀값을 확인할 수 있습니다. 이 값을 그림으로 표현하면 사과가 출력되는 것도 확인할 수 있습니다.
1
2
| plt.imshow(fruits[0], cmap='gray')
plt.show()
|

컴퓨터의 기준에서는 0을 기준으로 보는 것보다 255를 기준으로 보는게 출력값을 유의미하게 알 수 있습니다. 그렇기 때문에 흑백 이미지의 경우는 위와 같이 반전하여 사용합니다. 그런데 저렇게 있으면 사람이 판단하기 어렵기 때문에 gray_r
을 지정하면 색상을 반전할 수 있습니다.
1
2
3
4
5
6
7
8
| plt.imshow(fruits[0], cmap='gray_r')
plt.show()
fig, axs = plt.subplots(1, 3)
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
axs[2].imshow(fruits[299], cmap='gray_r')
plt.show()
|

용과 같은게 있네요. 어쨋든 위와 같이 matplotlib.pyplot
의 subplots()
함수를 이용하면 조금 더 보기 좋게 배열처럼 볼 수 있습니다.
픽셀값 분석하기#
아래 처럼 2,3 차원의 데이터를 합쳐서 픽셀값을 분석하기 편하게 바꿉니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| apple = fruits[0:100].reshape(-1, 100*100) # 사과 100개에 대해 두 번째 차원과 세 번때 차원을 합침(10000)
pineapple = fruits[100:200].reshape(-1, 100*100)
banana = fruits[200:300].reshape(-1, 100*100)
print(apple.shape) # 사과 배열 크기 (100, 10000)
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]
|
사과 샘플 100개에 대한 평균값을 계산했으며, 평균값이 어떻게 분포되는지 확인해봅니다.
1
2
3
4
5
| 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()
|

이미지를 보면 바나나는 구분이 쉽지만, 사과와 파인애플은 영역(크기)도 비슷하기에 샘플의 평균값은 비슷하게 분포되는 것을 확인할 수 있습니다.
이를 해결하기 위해 각각의 픽셀에 대한 평균값으로 해결해 볼 수 있습니다. 아래와 같이 막대그래프 및 이미지로 비교해봅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| fig, axs = plt.subplots(1, 3, figsize=(20, 5))
# subplots 함수를 사용하여 bar 그래프 생성(axis=0으로 지정하여 행에 대한 평균을 계산)
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()
|

위와 같이 픽셀 10,000개에 대한 평균값으로 사과, 파인애플, 바나나의 데이터가 표현하는 패턴이 차이가 있음을 확인할 수 있습니다.
평균값과 가까운 사진 고르기#
사과, 파인애플, 바나나 중에 사과만 골라보는 실습을 합니다. abs()
함수를 이용해 이전에 가져온 사과 평균값을 비교합니다.
300개의 샘플 중에 이 오차가 가장 적은 100개를 선정하면 아래와 같이 모두 사과가 나온 것을 확인할 수 있습니다.
추가로 비슷한 방식으로 바나나, 파인애플도 가능할 것 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # 바나나로 대체해보기
abs_diff = np.abs(fruits - banana_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
# print(abs_mean.shape)
apple_index = np.argsort(abs_mean)[:100]
fig, axs = plt.subplots(10, 10, figsize=(5,5))
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 - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
# print(abs_mean.shape)
apple_index = np.argsort(abs_mean)[:100]
fig, axs = plt.subplots(10, 10, figsize=(5,5))
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()
|

사진을 보면 바나나의 경우는 마지막에 사과 비슷한게 몇개가 낀거 같네요. 추측해보자면 이전에 차트에서 바나나 중에 픽셀 값이 사과와 근접한게 있는데 이와 관련해서 평균값이 사과를 더 바나나 같다 생각하지 않을까 합니다.
실제로 바나나 기준으로 300개를 모두 나열 해보니 아래와 같이 바나나 2개가 정면샷과 후면샷을 찍은 걸 볼 수 있네요.

이런 문제를 해결하기 위해서 바나나의 특징에 맞는 특성을 잘 맞춰 코드를 추가하거나 라벨링 데이터를 왕창 늘리면 좋지 않을까 생각(난 지금 찍먹중이니..)합니다.
위와 같이 비슷한 샘플끼리 그룹으로 모으는 작업을 군집이라고 합니다. 군집은 대표적인 비지도 학습 작업 중 하나이며, 군집 알고리즘에서 만든 그룹을 클러스터라고 부릅니다.
k-평균#
1절에서는 각 픽셀의 평균값을 구하여 가장 가까운 사진을 골랐습니다. 이 경우는 이미 사진의 과일 항목을 알기 때문에 각 과일의 평균을 구할 수 있습니다. 하지만 진짜 비지도 학습에서는 사진에 어떤 과일이 들어 있는지 모릅니다.
이럴 경우 k-평균
군집 알고리즘이 평균값을 자동으로 찾아줍니다. 이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심
또는 센트로이드
라고 부릅니다.
k-평균 알고리즘은 다음과 같은 작동 방식입니다.
- 무작위로 k개의 클러스터 중심을 정합니다.
- 각 샘플에서 가장 ㄱ가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정합니다.
- 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경합니다.
- 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복합니다.
KMeans 클래스#
사이킷런의 k-평균 알고리즘은 sklearn.cluster
모듈 아래 KMeans
클래스에 구현 돼 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import numpy as np
fruits = np.load('fruits_300.npy')
# 3차원 배열을(샘플 개수, 너비 * 높이) 크기를 가진 2차원 배열로 변경
fruits_2d = fruits.reshape(-1, 100*100)
# sklearn.cluster 모듈 아래 KMeans클래스로 사이킷런의 k-평균 알고리즘이 구현됨
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
# 비지도 학습이므로 fit() 메서드에서 타깃 데이터를 사용하지 않음
km.fit(fruits_2d)
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 0 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]
print(np.unique(km.labels_, return_counts=True))
# (array([0, 1, 2], dtype=int32), array([112, 98, 90]))
|
0,1,2 인덱스에 각각 112, 98, 90개의 이미지를 분류했네요.
모두 출력해보면 총 300개를 분류했지만 파인애플에 사과랑 바나나가 조금 섞여 있는 것을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import matplotlib.pyplot as plt
def draw_fruits(arr, ratio=1):
sampleCount = len(arr)
# 한 줄에 10개씩 이미지를 그리기 샘플 개수를 10으로 나누어 전체 행 개수를 계산합니다.
rows = int(np.ceil(sampleCount/10))
# 행이 1개 이면 열 개수는 샘플 개수입니다. 그렇지 않으면 10개입니다.
cols = sampleCount if rows < 2 else 10
fig, axs = plt.subplots(rows, cols, figsize=(5, 5), squeeze=False)
for i in range(rows):
for j in range(cols):
if i*10 + j < sampleCount:
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])
|

k-평균 알고리즘이 해당 샘플들을 완벽하게 구별해내지는 못했네요. 훈련 데이터에 타깃 레이블을 제공하지 않아도 어느 정도 비슷한 샘플을 모으는 것 같습니다.
클러스터 중심#
KMeans 클래스가 최종적으로 찾은 클러스터 중심은 cluster_center_
속성에 저장 돼 있습니다.
1
2
3
4
5
6
7
8
9
10
11
| draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio=3) # 파인애플, 바나나, 사과
# 훈련 데이터 샘플에서 클러스터 중심까지 거리로 변환해 주는 transform 함수
# 특정 샘플(100번째)과 각 클러스터 중심점 간의 거리 계산
print(km.transform(fruits_2d[100:101])) # [[3400.24197319 8837.37750892 5279.33763699]]
print(km.predict(fruits_2d[100:101])) # 100번째 샘플이 어느 클러스터에 속하는지 예측
draw_fruits(fruits[100:101]) # 실제 100번째 과일 이미지를 표시
print(km.n_iter_) # k-평균 알고리즘이 수렴하는데 걸린 반복 횟수 4번
|

책에서는 2번째 인덱스를 파인애플이라 표기했지만 제가 실습할 때는 0번째 인덱스가 파인애플이라 표기됐네요.
k-평균 알고리즘은 반복적으로 클러스터 중심을 옮기면서 최적의 클러스터를 찾습니다. 해당 알고리즘이 반복한 횟수는 n_iter_
에 저장되고 저는 4가 출력됐네요.
최적의 k 찾기#
k-평균 알고리즘의 단점 중 하나는 클러스터 개수를 사전에 지정해야 한다는 점입니다.
군집 알고리즘에서 적절한 k 값을 찾기 위한 완벽한 방법은 없다고 합니다. 몇 가지 도구가 있지만 저마다 장단점이 있습니다.
해당 k 값을 찾기 위해 엘보우
방법에 대해 알아보겠습니다. 엘보우 방법은 클러스터 개수를 늘려가면서 이너셔(클래스터의 샘플이 얼마나 가깝게 있는지를 나타내는 값)의 변화를 관찰하여 최적의 클러스터 개수를 찾는 방법입니다.
클러스터 개수를 증가시키면서 이너셔를 그래프로 그리면 감소하는 속도가 꺽이는 지점이 있습니다.
이 지점부터는 클러스터 개수를 늘려도 클러스터에 잘 밀집된 정도가 크게 개선되지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
| inertia = []
# 2~5까지 k값을 바꿔가며 KMeans 클래스를 5번 훈련
for k in range(2, 6):
km = KMeans(n_clusters=k, n_init='auto', random_state=42)
km.fit(fruits_2d)
inertia.append(km.inertia_)
plt.plot(range(2, 6), inertia)
plt.xlabel('k')
plt.ylabel('inertia')
plt.show()
|

그래프에서는 k값이 3일 때를 기준으로 이너셔의 값이 k가 증가해도 상대적으로 낮은 것을 확인할 수 있습니다.
위처럼 엘보우 지점보다 클러스터 개수가 많아지면 이너셔의 변화가 줄어들면서 군집 효과도 줄어듭니다.
하지만 위 그래프에서는 이런 지점이 명확하지 않다고 하네요. 혹시나 해서 2 ~ 80(더 늘리면 오래 걸림 이슈)까지 클러스터를 해봤는데도 사실 잘 모르겠네요.

주성분 분석#
너무 많은 사진이 등록되어 디스크 공간이 부족하여 이를 해결하기 위해 차원 축소에 대해 학습합니다.
차원과 차원 축소#
데이터가 가진 속성을 특성이라고 부릅니다. 과일 사진의 경우 10,000개의 픽셀이 있기 때문에 10,000개의 특성이 있습니다.
머신러닝에서는 이런 특성을 차원이라고 부릅니다. 해당 차원을 줄일 수 있따면 저장 공간을 크게 절약할 수 있습니다.
이를 위해 차원축소 알고리즘을 다뤄봅니다. 차원 축소 알고리즘 중 대표적인게 주성분 분석(Principal Component Analysis) 입니다. PCA로 줄여부르기도 합니다.
주성분 분석 소개#
주성분 분석(PCA, Principal Component Analysis)은 고차원 데이터를 저차원으로 축소하면서 데이터의 중요한 정보를 최대한 보존하는 기법입니다.
주여 특징은 다음과 같습니다.
- 차원 축소: 고차원 → 저차원으로 변환하여 시각화와 계산 효율성 향상
- 정보 보존: 가장 중요한 정보를 우선적으로 보존
- 노이즈 제거: 중요하지 않은 성분을 제거하여 데이터 품질 향상
- 선형 변환: 원본 특성들의 선형 조합으로 새로운 축 생성
주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어들게 됩니다.
PCA 클래스#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
# PCA 클래스 사용
from sklearn.decomposition import PCA
# 50개 주성분 정의
pca = PCA(n_components=50)
pca.fit(fruits_2d)
print(pca.components_.shape) # (50, 10000)
# 50개의 주성분을 찾았으며, 두 번쨰 차원은 항상 원본 데이터의 특성 개수와 같음
import matplotlib.pyplot as plt
# 50개의 주성분을 이미지로 시각화
draw_fruits(pca.components_.reshape(-1, 100, 100))
|

위와 같이 주성분을 이미지로 시각화하면 가장 분산이 큰 방향을 순서대로 나타냅니다.
50개의 주성분을 찾은 PCA 모델을 사용해 기존 배열의 값을 변경합니다.
1
2
3
4
5
| print(fruits_2d.shape) # (300, 10000)
fruits_pca = pca.transform(fruits_2d)
# 주성분 50개로 교체
print(fruits_pca.shape) # (300, 50)
|
이렇게 되면 10,000개의 크기에서 50개로 200배 정도 크기를 줄일 수 있습니다.
원본 데이터 재구성#
PCA 클래스에는 원본 데이터를 복원할 수 있는 inverse_transform
메서드를 제공합니다.
1
2
3
4
5
6
7
8
| fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape) # (300, 10000)
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
draw_fruits(fruits_reconstruct[start:start+100])
print("\n")
|

50개의 특성이 분산을 가장 잘 보존하도록 변환된 것이기 때문에 어느 정도 이미지가 복원된 것을 확인할 수 있습니다.
설명된 분산#
주성분이 원본 데이터의 분산을 얼마나 잘 나타내는지 기록한 값을 설명된 분산이라고 합니다.
1
2
3
| print(np.sum(pca.explained_variance_ratio_)) # 0.9213663445284296
plt.plot(pca.explained_variance_ratio_)
|
92%가 넘는 분산을 유지하기 때문에 원본 데이터를 복원했을 때 원본 이미지의 품질이 높게 나올 수 있었습니다.

다른 알고리즘과 함께 사용하기#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| from sklearn.linear_model import LogisticRegression
lr = LogisticRegression() # 로지스틱 회귀 분류기 생성
# 타겟 레이블 생성
target = np.array([0] * 100 + [1] * 100 + [2] * 100)
from sklearn.model_selection import cross_validate
# 원본 데이터(10000차원)로 교차검증 - 정확도와 훈련시간 측정
scores = cross_validate(lr, fruits_2d, target)
print(np.mean(scores['test_score'])) # 평균 정확도: 0.9966666666666667 (99.67%)
print(np.mean(scores['fit_time'])) # 평균 훈련시간: 1.2789854049682616초
# PCA 적용된 데이터(50차원)로 교차검증 - 차원축소 효과 비교
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score'])) # 평균 정확도: 0.9966666666666667 (동일한 성능)
print(np.mean(scores['fit_time'])) # 평균 훈련시간: 0.0359138011932373초 (더 빨리 훈련)
# 분산 기준으로 PCA 적용 - 전체 분산의 50%를 보존하는 주성분 개수 자동 결정
pca = PCA(n_components=0.5) # 50% 분산 보존
pca.fit(fruits_2d)
print(pca.n_components_) # 자동 결정된 주성분 개수: 2개 (50% 분산 보존에 2개면 충분)
# 2차원으로 극단적 축소
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape) # (300, 2) - 10000차원 → 2차원으로 5000배 축소
# 2차원 데이터로 교차검증 - 극단적 차원축소의 성능 확인
scores = cross_validate(lr, fruits_pca, target)
print(np.mean(scores['test_score'])) # 평균 정확도: 0.99 (99%) - 약간 감소
print(np.mean(scores['fit_time'])) # 평균 훈련시간: 0.053063583374023435초
|
위와 같이 PCA로 축소한 데이터를 지도 학습에 적용해봤습니다. 로지스틱 회귀 모델을 이용하여 실습했으며, 차원을 극단적으로 줄여도 PCA로 만든 주성분을 이용하여 훈련 시간을 개선할 수 있다는 점을 배웠습니다.
실제 이미지 분류에서는 이전과 동일하게 파인애플에 사과와 바나나가 몇 개 들어가 있는 현상은 그대로네요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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(['pineapple', 'banana', 'apple'])
plt.show()
|


그림을 보면 사과와 파인애플 클러스터의 경계가 모호한 점이 있어서 잘못 분류된게 아닐까 생각됩니다.