[11주차] 정답 없는 데이터와 마주하기: Unsupervised Learning 🔍

안녕하세요! 11주차 수업에 오신 것을 환영합니다.
지금까지 우리는 ’정답(y)’이 주어진 상태에서 모델을 학습시키는
Supervised Learning (e.g., 선형/Logistic Regression, Decision Tree)을 다루었습니다.

오늘은 Machine Learning의 또 다른 큰 축인 Unsupervised Learning 을 배웁니다.
Unsupervised Learning은 정답(레이블)이 없는 데이터가 주어졌을 때,
데이터 자체에 내재된 숨겨진 구조나 패턴을 찾아내는 방식입니다.

오늘은 Unsupervised Learning의 두 가지 대표적인 기법,

  1. K-Means 군집화 (Clustering): 데이터의 숨겨진 그룹을 찾는 기법
  2. 주성분 분석 (PCA): 데이터의 차원을 축소하여 시각화하는 기법
    에 대해 학습하고 scikit-learn으로 직접 구현해 보겠습니다.

🧺 모듈 1: K-Means 군집화 (Clustering)

군집화(Clustering)란?

K-Means 알고리즘의 직관적 이해:

K-Means 실습 1: 가상 데이터로 원리 확인

scikit-learn을 이용해 K-Means 알고리즘을 직접 적용해 보겠습니다.
먼저, make_blobs라는 함수를 사용해 군집화 연습용 가상 데이터를 생성합니다.

# 필요한 라이브러리 임포트
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs # 군집화 연습용 데이터 생성기
from sklearn.cluster import KMeans # K-Means 모델
from sklearn.decomposition import PCA # PCA
from sklearn.preprocessing import StandardScaler # 데이터 스케일러
from sklearn.datasets import load_iris # Iris 데이터셋

# 1. 데이터 준비 (정답 없는 데이터 생성)
# n_samples=300: 300개의 샘플 생성
# centers=3: 3개의 그룹(중심)을 가지도록 생성
# cluster_std=2.0: 그룹의 퍼진 정도
# random_state=42: 재현성을 위한 시드 고정
# X는 (300, 2) shape의 2D 데이터, y_true는 실제 정답(0,1,2)
X, y_true = make_blobs(n_samples=300, centers=3,
                       cluster_std=2.0, random_state=42)

# 2. 데이터 탐색 (시각화)
# 비지도 학습이므로, 정답(y_true)을 모른다고 가정하고
# 모든 데이터를 회색(c='gray')으로 시각화합니다.
plt.figure()
plt.scatter(X[:, 0], X[:, 1], c='gray', s=50, alpha=0.7)
plt.title("Unlabeled Data")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.grid(True)
plt.show()

print("우리의 목표: 이 회색 점들을 3개의 그룹으로 분류하는 것입니다.")
<Figure size 640x480 with 1 Axes>
실행 결과
우리의 목표: 이 회색 점들을 3개의 그룹으로 분류하는 것입니다.

K-Means 실습 2: 모델 학습 및 결과 확인

이제 KMeans 모델을 생성하고 학습시킵니다.
핵심 파라미터는 n_clusters (K값) 입니다.

# 1. K-Means 모델 생성
# n_clusters=3 : 우리가 찾으려는 그룹의 개수(K)를 3으로 지정
# random_state=42 : 조장의 초기 무작위 위치를 고정
model_kmeans = KMeans(n_clusters=3, random_state=42)

# 2. 모델 학습 (fit)
# 비지도 학습이므로, 정답(y) 없이 X 데이터만 입력합니다!
# 이 과정에서 내부적으로 '할당'과 '업데이트' 단계가 반복됩니다.
model_kmeans.fit(X)

# 3. 학습 결과 확인
# model.labels_ : 모델이 각 데이터에 할당한 그룹 번호(0, 1, 2)
y_pred = model_kmeans.labels_
# model.cluster_centers_ : 학습이 완료된 3개 조장(Centroid)의 최종 위치
centroids = model_kmeans.cluster_centers_

print("K-Means 학습 완료!")
print(f"예측된 그룹 라벨 (앞 10개): {y_pred[:10]}")
print(f"최종 중심점 위치:\n{centroids}")
K-Means 학습 완료!
예측된 그룹 라벨 (앞 10개): [1 1 0 2 1 2 0 2 0 0]
최종 중심점 위치:
[[-2.75726773  9.07285344]
 [-6.88811639 -7.0878587 ]
 [ 4.85432791  2.04801886]]

학습된 모델이 데이터를 어떻게 그룹화했는지,
model.labels_를 이용해 시각화해 봅시다.

# K-Means 군집화 결과 시각화
plt.figure()
# 데이터를 산점도로 그리되, 색상(c)을 모델이 예측한 그룹(y_pred)으로 지정
plt.scatter(X[:, 0], X[:, 1],
            c=y_pred, s=50, cmap='viridis', alpha=0.7)

# 최종 중심점(Centroid)을 빨간색 'x' 마커로 겹쳐 그리기
plt.scatter(centroids[:, 0], centroids[:, 1],
            c='red', s=200, marker='X', label='Centroids')

plt.title("K-Means Clustering Result (K=3)")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.legend()
plt.grid(True)
plt.show()

print("정답(레이블) 없이도 데이터의 분포만으로 그룹을 성공적으로 찾아냈습니다.")
<Figure size 640x480 with 1 Axes>
실행 결과
정답(레이블) 없이도 데이터의 분포만으로 그룹을 성공적으로 찾아냈습니다.

🧐 모듈 2: 최적의 K값 찾기 (엘보우 메소드)

방금 우리는 정답이 3개인 것을 알고 n_clusters=3으로 설정했습니다.
하지만 실제 Unsupervised Learning 상황에서는 K값이 몇 개가 적절한지 모릅니다.

K값을 어떻게 선택할까요?

엘보우 메소드 (Elbow Method):

# K값을 1부터 10까지 변화시키며 Inertia를 저장할 리스트
inertia_list = []
k_range = range(1, 11)

for k in k_range:
    # 1. 현재 k값으로 K-Means 모델 생성 및 학습
    model = KMeans(n_clusters=k, random_state=42)
    model.fit(X)
    # 2. 학습된 모델의 Inertia 값을 리스트에 추가
    # model.inertia_ 속성에 학습된 Inertia 값이 저장됩니다.
    inertia_list.append(model.inertia_)
print("K값에 따른 Inertia 계산 완료!")

# K값(x축)에 따른 Inertia(y축)를 선 그래프로 시각화
plt.figure()
plt.plot(k_range, inertia_list, marker='o', linestyle='--')
plt.title("Elbow Method")
plt.xlabel("K")
plt.ylabel("Inertia")
plt.grid(True)
plt.show()

print("결과 해석:")
print("- K=1에서 2로, 2에서 3으로 갈 때는 Inertia가 급격히 감소합니다.")
print("- K=3에서 4로 갈 때부터는 감소폭이 완만해집니다.")
print("=> 따라서, 이 그래프의 '팔꿈치(Elbow)' 지점은 K=3이며,")
print("   최적의 클러스터 개수는 3개라고 판단할 수 있습니다.")
K값에 따른 Inertia 계산 완료!
<Figure size 640x480 with 1 Axes>
실행 결과
결과 해석:
- K=1에서 2로, 2에서 3으로 갈 때는 Inertia가 급격히 감소합니다.
- K=3에서 4로 갈 때부터는 감소폭이 완만해집니다.
=> 따라서, 이 그래프의 '팔꿈치(Elbow)' 지점은 K=3이며,
   최적의 클러스터 개수는 3개라고 판단할 수 있습니다.

📉 모듈 3: 차원 축소와 PCA

이제 Unsupervised Learning의 두 번째 주제인 차원 축소(Dimensionality Reduction) 로 넘어갑니다.

차원이란?

왜 차원을 축소할까요?

  1. 시각화: 인간은 3차원까지만 인지할 수 있습니다.
  1. 차원의 저주 (Curse of Dimensionality): 차원이 너무 높아지면(특징이 너무 많으면)
    데이터가 존재하는 공간은 매우 넓어지는 데 비해 데이터는 희박해져(sparse)
    오히려 모델의 성능이 저하될 수 있습니다. (e.g., 불필요한 노이즈 특징)

주성분 분석 (PCA, Principal Component Analysis):

[중요] PCA를 위한 데이터 전처리: 스케일링

PCA는 데이터의 분산(Variance) 을 기반으로 작동합니다.
만약 특징(열)마다 측정 단위(scale)가 다르면 (e.g., 키(150~190) vs. 몸무게(40~100)),
분산이 큰 ‘키’ 특징이 PCA에 과도하게 큰 영향을 미치게 됩니다.

따라서 PCA를 수행하기 전에는, 모든 특징의 중요도를 동등하게 만들기 위해
Standardization 스케일링(Standard Scaling) 을 통해 모든 특징의 평균을 0, 분산을 1로
맞춰주는 전처리 과정이 반드시 필요합니다.

scikit-learn의 StandardScaler 를 사용합니다.

# 1. 붓꽃(Iris) 데이터 로드
iris = load_iris()
# 4개의 특징(4D)을 모두 사용
X_iris = iris.data
y_iris = iris.target

print(f"원본 데이터 Shape: {X_iris.shape}")

# 2. StandardScaler를 이용한 데이터 스케일링
# StandardScaler 객체 생성
scaler = StandardScaler()

# scaler가 데이터의 평균과 표준편차를 학습(fit)하고,
# 그 기준으로 데이터를 변환(transform)하도록 한 번에 수행
X_scaled = scaler.fit_transform(X_iris)

print(f"\n스케일링 후 데이터 (앞 5줄):\n{np.round(X_scaled[:5], 2)}")
print(f"\n스케일링 후 전체 평균 (≈0): {X_scaled.mean():.4f}")
print(f"스케일링 후 전체 표준편차 (≈1): {X_scaled.std():.4f}")
원본 데이터 Shape: (150, 4)

스케일링 후 데이터 (앞 5줄):
[[-0.9   1.02 -1.34 -1.32]
 [-1.14 -0.13 -1.34 -1.32]
 [-1.39  0.33 -1.4  -1.32]
 [-1.51  0.1  -1.28 -1.32]
 [-1.02  1.25 -1.34 -1.32]]

스케일링 후 전체 평균 (≈0): -0.0000
스케일링 후 전체 표준편차 (≈1): 1.0000

PCA 실습 1: 4D → 2D로 차원 축소

이제 스케일링된 4차원 붓꽃 데이터를
PCA를 이용해 2차원으로 축소해 보겠습니다.

# 1. PCA 모델 생성
# n_components=2 : 4개의 특징을 2개의 주성분(2D)으로 압축하겠다고 설정
pca = PCA(n_components=2)

# 2. PCA 모델 학습(fit) 및 데이터 변환(transform)
# 스케일링된 4D 데이터(X_scaled)를 입력받아
# 2D 주성분 공간으로 변환된 데이터(X_pca)를 반환
X_pca = pca.fit_transform(X_scaled)

print("PCA 변환 완료!")
print(f"원본 데이터 Shape: {X_scaled.shape}")
print(f"PCA 변환 후 Shape: {X_pca.shape}")
PCA 변환 완료!
원본 데이터 Shape: (150, 4)
PCA 변환 후 Shape: (150, 2)

PCA 실습 2: 결과 해석

PCA로 차원을 축소하면 정보 손실이 발생합니다.
얼마나 많은 정보가 보존되었는지 확인하는 것이 중요합니다.

explained_variance_ratio_:

# PCA의 '설명된 분산' 비율 확인
variance_ratio = pca.explained_variance_ratio_

print(f"설명된 분산 비율 (PC1, PC2): {variance_ratio}")

# 두 주성분이 설명하는 총 분산(정보량)
total_variance = np.sum(variance_ratio)
print(f"\n총 분산 비율: {total_variance * 100:.2f}%")

print("\n=> 4차원 데이터를 2차원으로 압축했지만,")
print(f"   약 {total_variance * 100:.2f}%를 보존하고 있습니다!")
설명된 분산 비율 (PC1, PC2): [0.72962445 0.22850762]

총 분산 비율: 95.81%

=> 4차원 데이터를 2차원으로 압축했지만,
   약 95.81%를 보존하고 있습니다!

PCA 실습 3: 2D 시각화

이제 PCA로 변환된 2차원 데이터(X_pca)를 산점도로 그려봅시다.
이때, 점의 색상을 원본 붓꽃 품종(y_iris) 으로 지정하여,
2차원 공간에서도 품종 구분이 잘 되는지 확인합니다.

# PCA로 변환된 2D 데이터 시각화
plt.figure()

# X_pca의 0번 열을 x축, 1번 열을 y축으로 사용
# 색상(c)은 원본 데이터의 정답(y_iris)을 사용
plt.scatter(X_pca[:, 0], X_pca[:, 1],
            c=y_iris, cmap='viridis', alpha=0.7)

plt.title("PCA Result")
plt.xlabel("Principal Component 1")
plt.ylabel("Principal Component 2")
plt.colorbar(label='Iris Species')
plt.grid(True)
plt.show()

print("결과 해석:")
print("- 데이터의 분포가 2차원 평면에도 꽤 잘 나타납니다.")
print("- 각 붓꽃 품종(색깔)이 2차원 공간에서도",
      "뚜렷하게 구분되는 것을 볼 수 있습니다.")
print("=> PCA가 데이터의 핵심 구조를 잘 보존하며",
      "차원을 축소했음을 의미합니다.")
<Figure size 640x480 with 2 Axes>
실행 결과
결과 해석:
- 데이터의 분포가 2차원 평면에도 꽤 잘 나타납니다.
- 각 붓꽃 품종(색깔)이 2차원 공간에서도 뚜렷하게 구분되는 것을 볼 수 있습니다.
=> PCA가 데이터의 핵심 구조를 잘 보존하며 차원을 축소했음을 의미합니다.

✨ 마무리

오늘은 정답 없는 데이터를 다루는 Unsupervised Learning을 배웠습니다.

다음 시간에는 지금까지 배운 모델들의 기초가 되는 MLP(Multilayer Perceptron),
즉 Deep Learning의 기본 모델을 실습해 보겠습니다.