참고 문헌:
1. 서강대 AI MBA 데이터마이닝 강의교재 (2023)
2. Müller, A. C., & Guido, S. (2016). *Introduction to Machine Learning with Python*. 1st Edition. O’Reilly Media, Inc., Sebastopol, CA. ISBN: 978-1449369415.
# Python 3.12.9 와 3.13.2 기준으로 작성되었습니다.
1. 비지도 학습의 개요 및 유형
1) 비지도 학습(Unsupervised Learning)의 개념
- 비지도 학습은 레이블이 없는 데이터를 활용하여 구조적 패턴을 발견하는 학습 방식이다.
- 대표적인 유형은 변환(transformation) 과 클러스터링(clustering) 이 있다.
2) 변환(Transformation)과 차원 축소(Dimensionality Reduction)
- 변환은 데이터의 새로운 표현을 찾는 과정이며, 주성분 분석(PCA), 비음수 행렬 분해(NMF), t-SNE 등의 방법이 존재한다.
- 차원 축소는 다차원 데이터를 더 적은 차원으로 변환하여 시각화나 데이터 압축에 활용된다.
3) 클러스터링(Clustering)
- 데이터의 유사성을 기준으로 군집을 형성하는 알고리즘이다.
- K-평균(K-means), DBSCAN, 병합 클러스터링(Agglomerative Clustering) 등의 기법이 있다.
4) 비지도 학습의 평가 문제
- 지도 학습처럼 정확한 정답이 없기 때문에 결과의 유용성을 평가하기 어렵다.
- 수작업 분석과 정성적 해석이 필요하며, 탐색적 데이터 분석(EDA)에서 유용하다.
2. 데이터 전처리와 스케일링(Preprocessing and Scaling)
1) 데이터 전처리의 중요성
- 일부 알고리즘(예: SVM, 신경망)은 데이터의 스케일에 민감하기 때문에, 적절한 스케일링이 필요하다.
- 데이터 정규화 및 변환을 통해 머신러닝 성능을 향상할 수 있다.
2) 스케일링 기법
- StandardScaler: 평균을 0, 분산을 1로 정규화(정규 분포 가정).
- MinMaxScaler: 데이터를 0과 1 사이로 변환.
- RobustScaler: 이상치(outlier) 영향을 줄이기 위해 중앙값과 사분위수를 이용.
- Normalizer: 데이터 포인트를 정규화하여 길이가 1이 되도록 변환.

3) 훈련 데이터와 테스트 데이터의 동일한 변환 적용 필요성
- 훈련 데이터에서 구한 평균과 표준편차를 테스트 데이터에 동일하게 적용해야 한다.
- `fit_transform()`을 훈련 데이터에 적용하고, 테스트 데이터에는 `transform()`을 사용.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
# 데이터 로드 및 분할
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=1
)
# 데이터 크기 출력
print("Training data shape:", X_train.shape)
print("Test data shape:", X_test.shape)
# Min-Max 스케일링 적용
scaler = MinMaxScaler()
scaler.fit(X_train)
# 데이터 변환
X_train_scaled = scaler.transform(X_train)
# 변환 전후 데이터 속성 비교
print("\n[Before Scaling]")
print("Per-feature minimum:\n", X_train.min(axis=0))
print("Per-feature maximum:\n", X_train.max(axis=0))
print("\n[After Scaling]")
print("Per-feature minimum:\n", X_train_scaled.min(axis=0))
print("Per-feature maximum:\n", X_train_scaled.max(axis=0))
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
# 1. 합성 데이터 생성
X, _ = make_blobs(n_samples=50, centers=5, random_state=4, cluster_std=2)
# 2. 훈련 및 테스트 데이터 분할
X_train, X_test = train_test_split(X, test_size=0.1, random_state=5)
# 3. 데이터 시각화 (원본 데이터)
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].scatter(X_train[:, 0], X_train[:, 1], c="blue", label="Training set", s=60)
axes[0].scatter(X_test[:, 0], X_test[:, 1], marker='^', c="red", label="Test set", s=60)
axes[0].legend(loc='upper left')
axes[0].set_title("Original Data")
# 4. MinMaxScaler를 사용하여 데이터 스케일링 (올바른 방식)
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 5. 스케일링된 데이터 시각화
axes[1].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1], c="blue", label="Training set", s=60)
axes[1].scatter(X_test_scaled[:, 0], X_test_scaled[:, 1], marker='^', c="red", label="Test set", s=60)
axes[1].set_title("Scaled Data (Proper)")
# 6. 잘못된 스케일링 (테스트 세트를 별도로 스케일링) - 올바르지 않은 방식
test_scaler = MinMaxScaler()
test_scaler.fit(X_test)
X_test_scaled_badly = test_scaler.transform(X_test)
# 7. 잘못된 스케일링된 데이터 시각화
axes[2].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1], c="blue", label="Training set", s=60)
axes[2].scatter(X_test_scaled_badly[:, 0], X_test_scaled_badly[:, 1], marker='^', c="red", label="Test set", s=60)
axes[2].set_title("Improperly Scaled Data (Incorrect)")
# 공통 축 라벨 설정
for ax in axes:
ax.set_xlabel("Feature 0")
ax.set_ylabel("Feature 1")
plt.show()

4) 전처리 후 머신러닝 모델 성능 변화
- 스케일링을 적용한 후 SVM(Support Vector Machine)의 성능이 크게 향상됨(0.63 → 0.97).
import numpy as np
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.datasets import load_breast_cancer
# 1. 데이터 로드 및 분할
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0
)
# 2. SVM 모델 학습 (원본 데이터)
svm = SVC(C=100)
svm.fit(X_train, y_train)
print("Test set accuracy (Original Data): {:.2f}".format(svm.score(X_test, y_test)))
# 3. Min-Max 스케일링 적용 (0-1 정규화)
minmax_scaler = MinMaxScaler()
minmax_scaler.fit(X_train)
X_train_minmax_scaled = minmax_scaler.transform(X_train)
X_test_minmax_scaled = minmax_scaler.transform(X_test)
# 4. Min-Max 스케일링 데이터로 SVM 학습 및 평가
svm.fit(X_train_minmax_scaled, y_train)
print("Test set accuracy (MinMax Scaled Data): {:.2f}".format(svm.score(X_test_minmax_scaled, y_test)))
# 5. StandardScaler 적용 (평균 0, 분산 1 정규화)
std_scaler = StandardScaler()
std_scaler.fit(X_train)
X_train_std_scaled = std_scaler.transform(X_train)
X_test_std_scaled = std_scaler.transform(X_test)
# 6. StandardScaler 데이터로 SVM 학습 및 평가
svm.fit(X_train_std_scaled, y_train)
print("Test set accuracy (Standard Scaled Data): {:.2f}".format(svm.score(X_test_std_scaled, y_test)))
# Test set accuracy (Original Data): 0.94
# Test set accuracy (MinMax Scaled Data): 0.97
# Test set accuracy (Standard Scaled Data): 0.96
3. 차원 축소(Dimensionality Reduction), 특징 추출(Feature Extraction), 및 매니폴드 학습(Manifold Learning)
1) 주성분 분석(PCA, Principal Component Analysis)
- 데이터의 방향성을 파악하여 정보를 가장 잘 보존하는 새로운 축으로 변환.
- 차원을 축소하면서도 최대한 많은 분산을 유지.
✅ PC1은 데이터의 분산을 가장 잘 설명하는 방향
✅ PC2는 PC1과 직교하면서 두 번째로 분산을 잘 설명하는 방향
✅ 이 과정을 반복하면 더 높은 차원의 데이터에서도 추가 주성분(PC3, PC4, ...)을 찾을 수 있음
# pip install mglearn
import mglearn
import matplotlib.pyplot as plt
# 이 함수는 2차원 공간에서 PCA가 어떻게 작동하는지 설명하는 그림을 출력한다.
mglearn.plots.plot_pca_illustration()
plt.show()

※그림 설명
좌측 상단: 원본 데이터 (Original data)
- 데이터가 2차원 공간에 분포되어 있으며, 서로 상관관계를 가지고 있는 모습.
- Component 1 (첫 번째 주성분, PC1): 데이터 분산이 가장 큰 방향.
- Component 2 (두 번째 주성분, PC2): 첫 번째 주성분과 직교(90도)하면서 그다음으로 분산이 큰 방향.
우측 상단: 변환된 데이터 (Transformed data)
- PCA를 적용한 후, 데이터를 주성분 축(PC1, PC2)에 정렬.
- 원래 데이터 공간에서 기울어져 있던 데이터가, 새로운 PCA 좌표계에서 수평 및 수직 방향으로 변환됨.
- X축(PC1) 방향으로 분산이 크고, Y축(PC2) 방향으로 분산이 작음.
좌측 하단: 두 번째 주성분 제거 (Transformed data w/ second component dropped)
- 두 번째 주성분(PC2)을 제거하고 첫 번째 주성분(PC1)만 남긴 상태.
- 데이터가 1차원 공간으로 축소됨 → 정보가 손실되지만, 가장 중요한 방향(PC1)만 유지됨.
- PCA 차원 축소의 핵심 개념을 보여줌.
우측 하단: 첫 번째 주성분만 사용하여 원래 공간으로 복원 (Back-rotation using only first component)
- 첫 번째 주성분만 사용하여 다시 원래 좌표계로 변환(역변환).
- 데이터가 단일 직선상에 놓이게 되며, 일부 정보(PC2 방향의 분산)는 손실됨.
- 원래 데이터보다 단순해졌지만, 가장 중요한 정보는 유지됨.
핵심 요약
- PCA는 데이터를 주성분(PC1, PC2, …) 방향으로 변환하여 더 적은 차원으로 축소할 수 있는 기법.
- PC1 방향이 가장 중요한 정보(분산이 큰 방향)를 담고 있음.
- 불필요한 차원을 제거하면 일부 정보가 손실되지만, 차원을 줄여 데이터 압축 및 해석 가능성을 높일 수 있음.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
import mglearn
# 1. 유방암 데이터 로드
cancer = load_breast_cancer()
# 2. 악성(malignant)과 양성(benign) 데이터 분리
malignant = cancer.data[cancer.target == 0] # 악성 (0)
benign = cancer.data[cancer.target == 1] # 양성 (1)
# 3. 히스토그램 플롯 생성
fig, axes = plt.subplots(15, 2, figsize=(12, 20)) # 15x2 서브플롯 생성
ax = axes.ravel() # 2D 배열을 1D로 변환하여 순회 가능하게 함
for i in range(30):
# 각 특징에 대한 히스토그램 생성
_, bins = np.histogram(cancer.data[:, i], bins=50) # 50개 구간(bin) 생성
ax[i].hist(malignant[:, i], bins=bins, color=mglearn.cm3(0), alpha=0.5, label="Malignant")
ax[i].hist(benign[:, i], bins=bins, color=mglearn.cm3(2), alpha=0.5, label="Benign")
# 서브플롯 제목(특징명) 설정
ax[i].set_title(cancer.feature_names[i])
ax[i].set_yticks([]) # Y축 눈금 제거
# 4. 공통 X축, Y축 레이블 설정
ax[0].set_xlabel("Feature magnitude")
ax[0].set_ylabel("Frequency")
ax[0].legend(loc="best") # 범례 추가
# 5. 레이아웃 조정 및 플롯 표시
fig.tight_layout()
plt.show()

히스토그램 시각화 설명(*위 코드를 실행 후 그림을 보며 읽으면 더 이해가 잘 될 것임)
히스토그램을 활용하여 유방암 데이터셋의 각 특징(feature)이 악성(빨간색)과 양성(파란색) 샘플에서 어떻게 분포되는지 분석할 수 있다.
1. 히스토그램 개요
- 각 서브플롯은 하나의 특징(feature) 에 대한 분포를 보여준다.
- X축: 해당 특징의 값 범위 (Feature Magnitude)
- Y축: 해당 값이 등장한 빈도 (Frequency)
- 두 개의 히스토그램이 겹쳐서 표현됨:
- 보라색 (Malignant, 악성 종양)
- 연두색(Benign, 양성 종양)
2. 유용한 특징 vs. 그렇지 않은 특징
- 명확히 구분되는 특징 (예: "worst concave points")
- 악성과 양성이 분리되어 분포됨 → 암을 분류하는 데 유용한 특징.
- 겹치는 특징 (예: "smoothness error")
- 악성과 양성의 분포가 거의 동일 → 단독으로는 유용하지 않은 특징.
3. 히스토그램의 한계
- 단일 특징(1D)만 분석 가능 → 다중 특징 간의 상호작용을 반영하지 못함.
- 어떤 특징은 단독으로는 별로 유용하지 않지만, 다른 특징과 결합하면 더 중요한 역할을 할 수 있음.
PCA(주성분 분석)를 활용하는 이유
히스토그램은 개별 특징만 보여주기 때문에, 다중 특징 간의 관계(상호작용)를 분석하기 어렵다.
이를 해결하기 위해 PCA(주성분 분석, Principal Component Analysis) 를 사용하면:
- 주요 특징 간의 관계를 반영한 새로운 차원(PC1, PC2)을 생성할 수 있다.
- 30차원의 데이터를 2차원으로 축소하여 시각적으로 쉽게 분석할 수 있다.
- 데이터를 가장 잘 분류하는 새로운 방향(Principal Components, 주성분)을 찾을 수 있다.
PCA를 활용한 2D 시각화 (산점도)
PCA를 활용하여 첫 번째 & 두 번째 주성분(PC1 & PC2)을 계산한 후, 데이터셋을 2D 공간에 시각화할 수 있다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
import mglearn
# 1. 유방암 데이터 로드 및 스케일링
cancer = load_breast_cancer()
scaler = StandardScaler()
X_scaled = scaler.fit_transform(cancer.data)
# 2. PCA 적용 (첫 번째 & 두 번째 주성분만 유지)
pca = PCA(n_components=2)
pca.fit(X_scaled)
X_pca = pca.transform(X_scaled)
# 3. 차원 축소 후 데이터 크기 출력
print("Original shape: {}".format(X_scaled.shape)) # 원본 데이터 형태 (569, 30)
print("Reduced shape: {}".format(X_pca.shape)) # 차원 축소 후 형태 (569, 2)
#Original shape: (569, 30)
# Reduced shape: (569, 2)
# 4. PCA 결과 시각화 (클래스별 색상 구분)
plt.figure(figsize=(8, 8))
mglearn.discrete_scatter(X_pca[:, 0], X_pca[:, 1], cancer.target)
plt.legend(cancer.target_names, loc="best")
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
plt.title("PCA of Breast Cancer Dataset")
plt.gca().set_aspect("equal")
plt.show()
# 5. PCA 주성분 분석
print("PCA component shape: {}".format(pca.components_.shape)) # (2, 30)
print("PCA components:\n", pca.components_)
# 6. PCA 주성분 가중치 시각화
plt.figure(figsize=(12, 6))
plt.matshow(pca.components_, cmap='viridis', fignum=1)
plt.colorbar()
plt.yticks([0, 1], ["First component", "Second component"])
plt.xticks(range(len(cancer.feature_names)), cancer.feature_names, rotation=60, ha='left')
plt.xlabel("Feature")
plt.ylabel("Principal Components")
plt.title("PCA Component Weights")
plt.show()


* PCA + SVM 코드: PCA가 반드시 좋은 것은 아니다를 보여주는 코드
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_breast_cancer
# 1. 데이터 로드 및 전처리
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0, test_size=0.2
)
# 데이터 스케일링 (평균 0, 분산 1로 변환)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 2. PCA 없이 SVM 실행
svm_no_pca = SVC(kernel='linear', C=1)
svm_no_pca.fit(X_train_scaled, y_train)
accuracy_no_pca = svm_no_pca.score(X_test_scaled, y_test)
# 3. PCA 적용 (주성분 2개 선택)
pca = PCA(n_components=2)
X_train_pca = pca.fit_transform(X_train_scaled)
X_test_pca = pca.transform(X_test_scaled)
# 4. PCA 적용 후 SVM 실행
svm_pca = SVC(kernel='linear', C=1)
svm_pca.fit(X_train_pca, y_train)
accuracy_pca = svm_pca.score(X_test_pca, y_test)
# 결과 출력
results = {
"SVM without PCA": accuracy_no_pca,
"SVM with PCA (n=2)": accuracy_pca,
}
print(results)
# {'SVM without PCA': 0.9824561403508771, 'SVM with PCA (n=2)': 0.9210526315789473}
다음은 주성분 분석(PCA)을 활용하여 얼굴 이미지의 핵심 특징, 즉 Eigenfaces를 추출하는 방식이다. 얼굴 이미지는 수많은 픽셀로 구성되어 있어 개별 픽셀의 정보만으로는 얼굴의 본질적인 특징을 파악하기 어렵다. 대신, PCA를 통해 고차원의 이미지 데이터를 낮은 차원의 공간으로 변환하면서 중요한 구조적 패턴(예를 들어, 얼굴의 윤곽이나 주요 구성 요소)을 추출할 수 있다. 여기서는 'Labeled Faces in the Wild' 데이터셋을 사용하여 유명 인사들의 얼굴 이미지를 불러오고, 각 이미지를 그레이스케일로 사용하며 크기를 축소하여 빠른 처리가 가능하도록 한다. 이렇게 전처리된 데이터를 바탕으로 PCA를 적용하면, 원본 이미지의 잡음과 불필요한 정보를 줄이고 얼굴 인식 등 후속 분석에 유용한 저차원 특징 벡터를 얻을 수 있다.
from sklearn.datasets import fetch_lfw_people
import matplotlib.pyplot as plt
# LFW 데이터셋을 불러오되, 최소 20개 이상의 얼굴 이미지가 있는 인물만 선택하고, 이미지를 0.7배로 축소
people = fetch_lfw_people(min_faces_per_person=20, resize=0.7)
# 데이터셋에 포함된 첫 번째 이미지의 형태 확인
image_shape = people.images[0].shape
# 2행 5열 서브플롯 생성 (총 10개의 이미지를 시각화)
fix, axes = plt.subplots(2, 5, figsize=(15, 8), subplot_kw={'xticks': (), 'yticks': ()})
# 각 이미지와 해당 라벨(인물 이름)을 순회하며 서브플롯에 출력
for target, image, ax in zip(people.target, people.images, axes.ravel()):
ax.imshow(image) # 이미지 출력
ax.set_title(people.target_names[target]) # 해당 인물의 이름을 제목으로 표시
plt.show()

데이터분포를 확인해보면, 균일하지 않는 분포(a bit skewed)이므로, 데이터를 균일화한 후 PCA유무에 따른 정확도를 측정해보았다.
import numpy as np
# NumPy 1.20이후 np.bool을 bool로 사용해야 한다.
# 각 인물별로 최대 50개의 샘플만 선택하기 위해 마스크 생성 (np.bool 대신 bool 사용)
mask = np.zeros(people.target.shape, dtype=bool)
for target in np.unique(people.target):
mask[np.where(people.target == target)[0][:50]] = 1
X_people = people.data[mask]
y_people = people.target[mask]
# 픽셀 값 스케일링 (0~255 -> 0~1)
X_people = X_people / 255.
# 1-NN(K-Nearest Neighbors) 분류기를 적용
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
# 데이터 분할 (stratify 옵션으로 클래스 비율 유지)
X_train, X_test, y_train, y_test = train_test_split(
X_people, y_people, stratify=y_people, random_state=0)
# 1-NN 분류기 생성 및 학습 후 테스트 성능 확인
knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train, y_train)
print("Test set score of 1-nn: {:.2f}".format(knn.score(X_test, y_test)))
# PCA를 적용
from sklearn.decomposition import PCA
# PCA를 이용하여 차원을 100으로 축소(whiten: 정규화)
pca = PCA(n_components=100, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
print("X_train_pca.shape: {}".format(X_train_pca.shape))
# 축소된 데이터로 1-NN 분류기 학습 및 테스트 성능 확인
knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train_pca, y_train)
print("Test set accuracy: {:.2f}".format(knn.score(X_test_pca, y_test)))
# 실행결과
# Test set score of 1-nn: 0.22
# X_train_pca.shape: (1547, 100)
# Test set accuracy: 0.30
원래 픽셀 공간에서 두 얼굴을 비교하면, 각 픽셀 값이 직접 비교되기 때문에 한 픽셀만 조금 옮겨도 엄청 다른 결과가 나온다. 반면에 PCA는 얼굴 이미지의 중요한 변화 요인들을 뽑아서, 그 주성분 공간에서 거리를 측정하게 된다. 특히 whitening 옵션을 사용하면, 각 주성분의 분산을 동일하게 맞춰서 데이터가 회전할 뿐만 아니라 크기도 조정되므로, 데이터의 분포가 타원형이 아니라 원형으로 바뀌어 얼굴 간 유사도를 보다 균형 있게 비교할 수 있게 된다.
import mglearn
mglearn.plots.plot_pca_whitening()

PCA로 뽑은 컴포넌트들은 얼굴 이미지의 픽셀 기반 특징을 나타내지만, 완벽하게 이해하기 어렵다. 아래 이미지를 봐도 컴포넌트들이 어떤 특징을 표현한 것인지 쉽게 확인할 수 없다. 이처럼 알고리즘이 해석하는 방식은 우리(사람)의 인식과 꽤 다르다
fix, axes = plt.subplots(3, 5, figsize=(15, 12),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (component, ax) in enumerate(zip(pca.components_, axes.ravel())):
ax.imshow(component.reshape(image_shape),cmap='viridis')
ax.set_title("{}. component".format((i + 1)))

PCA를 이해하는 또 다른 방법은 일부 주성분만 사용해 원본 데이터를 재구성하는 것이다. 예를 들어, 얼굴 이미지에서 주성분 몇 개만 남기고 inverse_transform을 사용해 원래의 특성 공간으로 복원하면, N 개의 컴포넌트로 재구성된 얼굴을 시각화할 수 있다.
import matplotlib.pyplot as plt
import mglearn
from sklearn.datasets import fetch_lfw_people
from sklearn.model_selection import train_test_split
# LFW 얼굴 이미지 데이터셋 불러오기 (최소 20개 이상의 얼굴 이미지가 있는 인물만 선택, 이미지 크기 축소)
people = fetch_lfw_people(min_faces_per_person=20, resize=0.7)
image_shape = people.images[0].shape # (height, width)
# 데이터와 타겟 설정 (데이터는 각 이미지의 픽셀 값이 일렬로 펼쳐진 형태)
X = people.data
# 학습용/테스트용 데이터 분할 (클래스 비율 유지)
X_train, X_test, y_train, y_test = train_test_split(
X, people.target, stratify=people.target, random_state=0)
# mglearn의 plot_pca_faces 함수는 내부적으로 PCA를 적용해
# 다양한 수의 주성분으로 재구성한 얼굴 이미지를 시각화해준다.
mglearn.plots.plot_pca_faces(X_train, X_test, image_shape)
plt.show()

import matplotlib.pyplot as plt
import mglearn
from sklearn.datasets import fetch_lfw_people
from sklearn.model_selection import train_test_split
from sklearn.decomposition import PCA
from sklearn.neighbors import KNeighborsClassifier
# 얼굴 데이터셋 불러오기
people = fetch_lfw_people(min_faces_per_person=20, resize=0.7)
X, y = people.data, people.target
# 학습/테스트 데이터 분할 (클래스 비율 유지)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
# PCA 적용 (2개 주성분, whitening 적용)
pca = PCA(n_components=2, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
# 1-NN 분류기 학습 및 평가
knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train_pca, y_train)
print("Test set accuracy: {:.2f}".format(knn.score(X_test_pca, y_test)))
# 주성분 공간에 데이터 시각화
mglearn.discrete_scatter(X_train_pca[:, 0], X_train_pca[:, 1], y_train)
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
plt.show()

2) 비음수 행렬 분해(NMF, Non-Negative Matrix Factorization)
NMF는 오직 0 이상의 값만 사용하는 행렬 분해 기법으로, 두 개의 컴포넌트를 사용하면 모든 데이터가 이 두 벡터의 양의 조합으로 표현됩니다. 컴포넌트 수가 줄어들면 PCA와 달리 완전히 다른 컴포넌트들이 생성되며, 컴포넌트의 순서에 의미가 없습니다. 또한, 무작위 초기화 때문에 복잡한 경우 결과가 달라질 수 있습니다.
- 모든 요소가 0 이상(양수)인 행렬로 데이터를 분해.
- 음수가 존재할 수 없는 데이터(예: 이미지, 오디오)에서 특징을 추출하는 데 효과적.
- PCA와 달리 각 요소를 해석하기 쉬움.
# nmf 특성을 알려주는 그래프
import mglearn
mglearn.plots.plot_nmf_illustration()


다음은 15개 컴포넌트를 추출한 후 각 컴포넌트를 이미지 형태로 시각화하는 것이다. 실제 이미지는 15개 comonent의 가중치 조합으로 표현될 수 있다.
from sklearn.decomposition import NMF
nmf = NMF(n_components=15, random_state=0)
nmf.fit(X_train)
X_train_nmf = nmf.transform(X_train)
X_test_nmf = nmf.transform(X_test)
fix, axes = plt.subplots(3, 5, figsize=(15, 12),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (component, ax) in enumerate(zip(nmf.components_, axes.ravel())):
ax.imshow(component.reshape(image_shape))
ax.set_title("{}. component".format(i))

3번 컴포넌트(compn=3)의 값을 기준으로 X_train_nmf를 내림차순 정렬하여 상위 10개의 이미지를 보자. 3번 컴포넌트가 강하게 표현된 사진이 추출되었다.
import numpy as np
compn = 3
# sort by 3rd component, plot first 10 images
inds = np.argsort(X_train_nmf[:, compn])[::-1]
fig, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (ind, ax) in enumerate(zip(inds, axes.ravel())):
ax.imshow(X_train[ind].reshape(image_shape))

마찬가지로 8번 컴포넌트가 얼마나 강하게 나타나는지를 확인해 보자.
import numpy as np
compn = 8
# sort by 7th component, plot first 10 images
inds = np.argsort(X_train_nmf[:, compn])[::-1]
fig, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (ind, ax) in enumerate(zip(inds, axes.ravel())):
ax.imshow(X_train[ind].reshape(image_shape))

이러한 패턴 추출은 오디오, 유전자 발현, 텍스트 등 가법적 구조의 데이터에 적합하고, 합성 데이터에서는 세 소스의 조합으로 신호를 표현하는 예제로 설명할 수 있다.
S = mglearn.datasets.make_signals() # S.shape (2000, 3)
plt.figure(figsize=(6, 1))
plt.plot(S, '-')
plt.xlabel("Time")
plt.ylabel("Signal")

세 개의 원래 신호(S)가 100개의 측정 장치를 통해 혼합되어 관측 데이터(X)가 생성된 후, NMF와 PCA를 사용해 혼합 신호로부터 원래 신호를 복원하는 과정을 살펴보자.
# 100차원 측정 공간으로 데이터를 혼합
# A: 100×3 크기의 임의의 혼합 행렬 (각 행은 측정 장치의 가중치를 의미)
# X: 혼합 신호, S와 A의 전치 행렬의 내적로 계산 (X.shape = (2000, 100))
A = np.random.RandomState(0).uniform(size=(100, 3))
X = np.dot(S, A.T)
print("Shape of measurements: {}".format(X.shape))
# 출력: Shape of measurements: (2000, 100)
# NMF를 이용하여 3개의 구성 요소(원래 신호)를 복원
nmf = NMF(n_components=3, random_state=42)
S_ = nmf.fit_transform(X)
print("Recovered signal shape: {}".format(S_.shape))
# 출력: Recovered signal shape: (2000, 3)
# 비교를 위해 PCA도 적용
pca = PCA(n_components=3)
H = pca.fit_transform(X)
# 복원된 신호(또는 활동성)를 시각화
models = [X, S, S_, H]
names = ['Observations (first three measurements)',
'True sources',
'NMF recovered signals',
'PCA recovered signals']
fig, axes = plt.subplots(4, figsize=(8, 4), gridspec_kw={'hspace': .5},
subplot_kw={'xticks': (), 'yticks': ()})
for model, name, ax in zip(models, names, axes):
ax.set_title(name)
ax.plot(model[:, :3], '-') # 각 모델의 처음 3개 열(측정값 혹은 신호)을 플롯
plt.show()

3) 매니폴드 학습(Manifold Learning)과 t-SNE
매니폴드 학습 알고리즘은 데이터의 내재된 구조를 파악하여 시각적으로 표현하는 데 주로 활용되며, 보통 2차원과 같이 적은 수의 특성으로 데이터를 변환한다. t-SNE와 같은 알고리즘은 학습 데이터에 대해 새로운 저차원 표현을 생성하지만, 학습 후에는 새로운 데이터에 동일한 변환을 적용할 수 없으므로 테스트 데이터에는 사용할 수 없다. 이처럼 t-SNE는 원본 공간에서 서로 가까운 데이터 간의 거리를 더욱 가깝게, 먼 데이터 간의 거리는 멀게 유지함으로써 데이터의 국소적인 이웃 관계를 효과적으로 보존하는 2차원 표현을 찾아내는 역할을 한다.
- 데이터의 고차원 구조를 보존하면서 저차원 공간에 임베딩.
- t-SNE(t-distributed Stochastic Neighbor Embedding): 데이터를 2D 또는 3D로 변환하여 시각화.
- 숫자 이미지 데이터셋에서 t-SNE를 활용하여 클래스 간 분포를 효과적으로 구별.
load_digits 데이터셋을 PCA 방법과 t-SNE 을 사용했을 때, 어떤 차이가 있는지 알아보자. 아래 결과를 보면, t-SNE를 사용했을 때 숫자들이 확실히 구분된 것을 알 수 있다.
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
digits = load_digits()
fig, axes = plt.subplots(2, 5, figsize=(10, 5),
subplot_kw={'xticks': (), 'yticks': ()})
for ax, img in zip(axes.ravel(), digits.images):
ax.imshow(img, cmap='gray')
plt.show()

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
digits = load_digits()
# PCA 모델 생성 및 학습
pca = PCA(n_components=2)
pca.fit(digits.data)
# 데이터를 첫 두 주성분으로 변환
digits_pca = pca.transform(digits.data)
# 새로운 색상 리스트 (matplotlib의 tab10 색상 팔레트)
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]
plt.figure(figsize=(10, 10))
plt.xlim(digits_pca[:, 0].min(), digits_pca[:, 0].max())
plt.ylim(digits_pca[:, 1].min(), digits_pca[:, 1].max())
for i in range(len(digits.data)):
# 각 숫자를 텍스트로 표시
plt.text(digits_pca[i, 0], digits_pca[i, 1], str(digits.target[i]),
color=colors[digits.target[i]],
fontdict={'weight': 'bold', 'size': 9})
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
plt.show()

from sklearn.manifold import TSNE
tsne = TSNE(random_state=42)
# TSNE는 transform 메서드가 없으므로 fit_transform을 사용합니다.
digits_tsne = tsne.fit_transform(digits.data)
plt.figure(figsize=(10, 10))
plt.xlim(digits_tsne[:, 0].min(), digits_tsne[:, 0].max() + 1)
plt.ylim(digits_tsne[:, 1].min(), digits_tsne[:, 1].max() + 1)
for i in range(len(digits.data)):
# 산점도 대신 텍스트로 숫자 레이블을 표시합니다.
plt.text(digits_tsne[i, 0], digits_tsne[i, 1], str(digits.target[i]),
color=colors[digits.target[i]],
fontdict={'weight': 'bold', 'size': 9})
plt.xlabel("t-SNE feature 0")
plt.ylabel("t-SNE feature 1")
plt.show()

4. 클러스터링(Clustering)
1) K-평균 클러스터링(K-Means Clustering)
- 가장 일반적인 클러스터링 기법 중 하나로, 데이터 포인트를 K개의 군집으로 나눔.
- 중심점을 반복적으로 조정하여 군집을 형성.
- 단점: 데이터가 구형(Gaussian) 분포를 이루지 않을 경우 부적절.
하기 그림은 K-Means 방법을 보여준다. 3개의 집단으로 분류한다고 가정하고 설명해보면,
1) 처음 3개의 대표점을 무작위로 찾는다.
2) 이후 3개의 지점과 각 데이터의 거리를 계산하여, 데이터와 가장 가까운 거리의 대표점으로 해당 데이터를 할당한다.
3) 데이터 할당이 모두 완료되면 다시 3개의 집단별로 중심점을 찾고, 2,3번 과정을 반복하여, 중심점 이동이 없을 때까지 반복한다.
mglearn.plots.plot_kmeans_algorithm()

mglearn.plots.plot_kmeans_boundaries()

실제 iris 데이터셋을 사용해보자.
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
# Iris 데이터 로드
iris = load_iris()
X = iris.data # 4개의 특성
# 시각화를 위해 petal length와 petal width (세 번째와 네 번째 특성) 선택
X_selected = X[:, 2:4]
# k-means 클러스터링 수행 (클러스터 개수: 3)
kmeans = KMeans(n_clusters=3, random_state=42)
kmeans.fit(X_selected)
clusters = kmeans.labels_
centers = kmeans.cluster_centers_
# 클러스터링 결과 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X_selected[:, 0], X_selected[:, 1], c=clusters, cmap='viridis', s=50, alpha=0.7)
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='X', s=200, label='Centroids')
plt.xlabel("Petal Length (cm)")
plt.ylabel("Petal Width (cm)")
plt.title("Iris Clustering using K-Means")
plt.legend()
plt.show()

k-means는 각 클러스터를 단일 중심으로 대표하고, 그 중심들 사이의 중간 지점을 경계로 사용하기 때문에, 클러스터의 형태가 볼록하고 단순한 경우에만 적합하다. 데이터 분포가 복잡하거나 비선형적인 경우, k-means는 올바른 클러스터링 결과를 얻기 어렵다.
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs, make_moons
from sklearn.cluster import KMeans
# Generate convex clusters data (3 clusters) using make_blobs
X_blobs, y_true_blobs = make_blobs(n_samples=300, centers=3, cluster_std=0.60, random_state=0)
kmeans_blobs = KMeans(n_clusters=3, random_state=0)
y_kmeans_blobs = kmeans_blobs.fit_predict(X_blobs)
centers_blobs = kmeans_blobs.cluster_centers_
# Generate non-convex (moon-shaped) data using make_moons
X_moons, y_true_moons = make_moons(n_samples=300, noise=0.05, random_state=0)
kmeans_moons = KMeans(n_clusters=2, random_state=0)
y_kmeans_moons = kmeans_moons.fit_predict(X_moons)
centers_moons = kmeans_moons.cluster_centers_
# Create a figure with 1 row and 2 columns to plot both graphs
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# Plot for convex clusters
axes[0].scatter(X_blobs[:, 0], X_blobs[:, 1], c=y_kmeans_blobs, s=50, cmap='viridis')
axes[0].scatter(centers_blobs[:, 0], centers_blobs[:, 1], c='red', s=200, alpha=0.75, marker='X')
axes[0].set_title("k-means on Convex Clusters")
axes[0].set_xlabel("Feature 1")
axes[0].set_ylabel("Feature 2")
# Plot for non-convex (moon-shaped) clusters
axes[1].scatter(X_moons[:, 0], X_moons[:, 1], c=y_kmeans_moons, s=50, cmap='viridis')
axes[1].scatter(centers_moons[:, 0], centers_moons[:, 1], c='red', s=200, alpha=0.75, marker='X')
axes[1].set_title("k-means on Non-Convex (Moon-Shaped) Data")
axes[1].set_xlabel("Feature 1")
axes[1].set_ylabel("Feature 2")
plt.tight_layout()
plt.show()

K-means는 벡터 양자화로 볼 수 있으며, 각 데이터 포인트를 가장 가까운 클러스터 중심으로만 표현한다. 반면, PCA와 NMF는 데이터를 여러 구성 요소들의 선형 결합으로 분해한다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_olivetti_faces
from sklearn.model_selection import train_test_split
from sklearn.decomposition import NMF, PCA
from sklearn.cluster import KMeans
# 1. Olivetti Faces 데이터셋을 로드합니다.
# faces.images는 (n_samples, 64, 64) 형태의 배열입니다.
faces = fetch_olivetti_faces()
# 2. 각 이미지를 1차원 벡터로 변환합니다.
# 전체 이미지를 (n_samples, 64*64) 모양의 배열로 만듭니다.
X_people = faces.images.reshape((faces.images.shape[0], -1))
# 3. 각 이미지에 해당하는 레이블(사람의 번호)를 y_people에 저장합니다.
y_people = faces.target
# 4. 원래 이미지의 형태 (64, 64)를 저장합니다.
image_shape = faces.images[0].shape
# 5. 데이터를 학습 세트와 테스트 세트로 분할합니다.
# stratify=y_people 옵션을 사용하여 각 클래스의 비율을 유지합니다.
# random_state=0을 설정하여 결과의 재현성을 보장합니다.
X_train, X_test, y_train, y_test = train_test_split(
X_people, y_people, stratify=y_people, random_state=0)
# 6. NMF 모델을 생성합니다.
# n_components=100으로 설정하여 100개의 비음수 성분을 추출합니다.
# random_state=0으로 재현성을 확보합니다.
nmf = NMF(n_components=100, random_state=0)
# 7. 학습 데이터(X_train)를 사용해 NMF 모델을 학습시킵니다.
nmf.fit(X_train)
# 8. PCA 모델을 생성합니다.
# n_components=100으로 설정하여 100개의 주성분을 추출합니다.
pca = PCA(n_components=100, random_state=0)
# 9. 학습 데이터(X_train)를 사용해 PCA 모델을 학습시킵니다.
pca.fit(X_train)
# 10. k-means 모델을 생성합니다.
# n_clusters=100으로 설정하여 100개의 클러스터(즉, 클러스터 중심)를 찾습니다.
kmeans = KMeans(n_clusters=100, random_state=0)
# 11. 학습 데이터(X_train)를 사용해 k-means 모델을 학습시킵니다.
kmeans.fit(X_train)
# 12. 각 기법을 사용하여 테스트 데이터(X_test)를 재구성합니다.
# 12-1. PCA를 사용한 재구성
# 먼저 X_test 데이터를 PCA의 주성분 공간으로 투영한 후,
# inverse_transform으로 원래의 차원으로 재구성합니다.
X_reconstructed_pca = pca.inverse_transform(pca.transform(X_test))
# 12-2. k-means를 사용한 재구성
# 먼저 kmeans.predict를 사용하여 X_test의 각 샘플이 속하는 클러스터 번호를 예측합니다.
# 그런 후, 해당 클러스터 번호에 대응하는 학습 데이터에서 얻은 클러스터 중심을 재구성 결과로 사용합니다.
X_reconstructed_kmeans = kmeans.cluster_centers_[kmeans.predict(X_test)]
# 12-3. NMF를 사용한 재구성
# 먼저 X_test 데이터를 NMF를 통해 100차원으로 변환한 후,
# 학습된 nmf.components_와 행렬곱(np.dot)을 수행하여 원래의 차원으로 재구성합니다.
X_reconstructed_nmf = np.dot(nmf.transform(X_test), nmf.components_)
# 13. 추출된 성분(components) 시각화: 3행 5열 서브플롯 생성
# 각 열은 한 개의 성분을 나타내며, 행별로 각각 k-means, PCA, NMF에서 추출한 성분을 표시합니다.
fig, axes = plt.subplots(3, 5, figsize=(8, 8), subplot_kw={'xticks': (), 'yticks': ()})
fig.suptitle("Extracted Components")
# 14. zip(axes.T, ...)를 사용하여 각 열(=한 성분)에 대해 반복합니다.
# axes.T: 서브플롯 배열의 전치(transpose)를 취하여 열 단위로 접근합니다.
# kmeans.cluster_centers_: k-means에서 학습한 100개의 클러스터 중심 (각각 하나의 성분으로 볼 수 있음)
# pca.components_: PCA에서 추출한 100개의 주성분
# nmf.components_: NMF에서 추출한 100개의 성분
for ax, comp_kmeans, comp_pca, comp_nmf in zip(
axes.T, kmeans.cluster_centers_, pca.components_, nmf.components_):
# 14-1. 첫 번째 행(ax[0])에 k-means 성분(클러스터 중심)을 원래 이미지 형태로 재구성하여 흑백(gray)으로 출력
ax[0].imshow(comp_kmeans.reshape(image_shape), cmap='gray')
# 14-2. 두 번째 행(ax[1])에 PCA 성분을 image_shape로 재구성하고, 'viridis' 컬러맵 사용
ax[1].imshow(comp_pca.reshape(image_shape), cmap='viridis')
# 14-3. 세 번째 행(ax[2])에 NMF 성분을 image_shape로 재구성하여 흑백(gray)으로 출력
ax[2].imshow(comp_nmf.reshape(image_shape), cmap='gray')
# 15. 각 행의 첫 번째 열에 y축 레이블을 추가하여 어느 행이 어떤 방법의 성분인지 표시합니다.
axes[0, 0].set_ylabel("kmeans")
axes[1, 0].set_ylabel("pca")
axes[2, 0].set_ylabel("nmf")
plt.tight_layout()
plt.show()
# 16. 재구성 결과 시각화: 4행 5열 서브플롯 생성
# 각 열은 하나의 테스트 샘플에 대해 원본 이미지, k-means, PCA, NMF로 재구성한 이미지를 순서대로 보여줍니다.
fig, axes = plt.subplots(4, 5, subplot_kw={'xticks': (), 'yticks': ()}, figsize=(8, 8))
fig.suptitle("Reconstructions")
# 17. zip(axes.T, ...)를 사용하여 각 열(=각 테스트 샘플)에 대해 반복합니다.
# X_test: 원본 테스트 이미지 데이터
# X_reconstructed_kmeans: k-means를 사용한 재구성 결과
# X_reconstructed_pca: PCA를 사용한 재구성 결과
# X_reconstructed_nmf: NMF를 사용한 재구성 결과
for ax, orig, rec_kmeans, rec_pca, rec_nmf in zip(
axes.T, X_test, X_reconstructed_kmeans, X_reconstructed_pca, X_reconstructed_nmf):
# 17-1. 첫 번째 행에 원본 이미지를 image_shape로 재구성하여 흑백(gray)으로 출력
ax[0].imshow(orig.reshape(image_shape), cmap='gray')
# 17-2. 두 번째 행에 k-means 재구성 결과를 image_shape로 재구성하여 흑백(gray)으로 출력
ax[1].imshow(rec_kmeans.reshape(image_shape), cmap='gray')
# 17-3. 세 번째 행에 PCA 재구성 결과를 image_shape로 재구성하여 흑백(gray)으로 출력
ax[2].imshow(rec_pca.reshape(image_shape), cmap='gray')
# 17-4. 네 번째 행에 NMF 재구성 결과를 image_shape로 재구성하여 흑백(gray)으로 출력
ax[3].imshow(rec_nmf.reshape(image_shape), cmap='gray')
# 18. 각 행의 첫 번째 열에 y축 레이블을 추가하여 어느 행이 어떤 재구성 결과인지 표시합니다.
axes[0, 0].set_ylabel("original")
axes[1, 0].set_ylabel("kmeans")
axes[2, 0].set_ylabel("pca")
axes[3, 0].set_ylabel("nmf")
plt.tight_layout()
plt.show()


K-means를 활용한 벡터 양자화는 입력 차원보다 더 많은 클러스터를 사용할 수 있기 때문에, 2차원 데이터를 PCA나 NMF로 1차원으로 축소할 때 나타나는 구조적 정보 손실 문제를 방지할 수 있다. 또한 더 많은 클러스터 중심을 활용하여 데이터를 더욱 세밀하고 풍부하게 인코딩할 수 있다.
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
kmeans = KMeans(n_clusters=10, random_state=0)
kmeans.fit(X)
y_pred = kmeans.predict(X)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=60, cmap='Paired')
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], s=60,
marker='^', c=range(kmeans.n_clusters), linewidth=2, cmap='Paired')
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
print("Cluster memberships:\n{}".format(y_pred))

distance_features = kmeans.transform(X)
print("Distance feature shape: {}".format(distance_features.shape))
print("Distance features:\n{}".format(distance_features))
# 결과
# Distance feature shape: (200, 10)
# Distance features:
# [[0.53664613 1.15017588 0.93237626 ... 1.48034956 0.002907 1.07736639]
# [1.74138152 0.60592307 1.00666225 ... 2.52921971 1.20779969 2.23716489]
# [0.75710543 1.93145038 0.91586549 ... 0.78321505 0.87573753 0.71838465]
# ...
# [0.9274342 1.73811046 0.57899268 ... 1.11471941 0.83358544 1.04125672]
# [0.3227627 1.97647071 1.47861069 ... 0.81425026 0.84551232 0.28446737]
# [1.63322944 0.47226506 1.02289983 ... 2.46626118 1.09767675 2.14812753]]
K-means는 알고리즘이 직관적이고 속도가 빠르며 대용량 데이터에 대해서도 확장성이 뛰어난 장점이 있으나, 초기 클러스터 중심의 랜덤 초기화에 따라 결과가 달라질 수 있고, 클러스터의 형태가 구형으로 제한되며, 클러스터의 개수를 사전에 미리 지정해야 하는 단점이 있다.
2) 병합 클러스터링(Agglomerative Clustering)
(1) 기본 개념 및 작동 원리
- 시작 단계: 각 데이터 포인트를 독립적인 하나의 클러스터로 설정하여 시작함.
- 병합 단계: 매 단계마다 가장 가까운 두 클러스터를 병합하여 점진적으로 큰 클러스터를 형성함.
- scikit-learn의 구현에서는 원하는 군집(cluster) 개수를 미리 지정해야 하며, 지정된 개수가 되면 병합을 중지함.
(2) 병합 기준(linkage criteria)
병합할 클러스터의 유사도를 측정하는 방식으로, scikit-learn에서는 다음 세 가지 방식을 지원함.
- ward (기본값)
- 클러스터 내 분산 증가가 최소가 되는 클러스터를 병합함.
- 비교적 크기가 균등한 클러스터를 생성함.
- average (평균 연결법)
- 두 클러스터 내 모든 점 사이의 평균 거리가 최소가 되는 클러스터를 병합함.
- complete (완전 연결법)
- 두 클러스터 내 점들 간 최대 거리가 최소가 되는 클러스터를 병합함.
(3) 알고리즘 진행 예시
- 초기 단계에서는 주로 단일 포인트 클러스터들이 병합되어 2개 포인트로 구성된 클러스터를 형성함.
- 이후 점진적으로 더 큰 클러스터가 만들어지며, 최종적으로 사용자가 지정한 클러스터 개수에 도달하면 알고리즘이 종료됨.
(4) 예측 및 시각화 특징
- 새 데이터 예측:
- 병합 군집은 클러스터링된 데이터에 대해서만 클러스터 소속을 결정할 수 있음.
- 새로운 데이터 포인트에 대한 예측(predict)은 불가능하므로, scikit-learn에서는 `fit_predict` 메서드를 사용하여 훈련 데이터의 클러스터 소속을 얻음.
- 계층적 구조(hierarchical clustering):
- 병합 군집은 모든 포인트가 하나의 클러스터로 병합되는 과정에서 중간 단계들이 계층적으로 구성되어 있어 "계층적 군집(hierarchical clustering)"이라고 불림.
- 덴드로그램(Dendrogram):
- 계층적 군집화의 시각화 도구로, 클러스터가 병합되는 전체 과정을 나무(tree) 형태로 표현함.
- x축은 데이터 포인트, y축은 클러스터 간 거리(유사도)를 나타내며, 가지(branch)의 길이가 클수록 병합된 클러스터 간 거리가 멀다는 것을 의미함.
- scikit-learn에는 덴드로그램 기능이 없으며, SciPy의 `dendrogram` 함수를 사용하여 쉽게 생성할 수 있음.
(5) 병합 군집의 한계점
- 데이터 형태가 복잡할 경우(예: two_moons 데이터셋처럼 비선형적이고 복잡한 모양) 클러스터를 효과적으로 구분하지 못할 수 있음.
- 이러한 문제를 해결할 수 있는 알고리즘으로 DBSCAN이 존재함.
import mglearn
mglearn.plots.plot_agglomerative_algorithm()

iris 데이터에 agglomerative algorithm를 적용해보자.
import matplotlib.pyplot as plt
import mglearn
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import load_iris
# Iris 데이터 로드
iris = load_iris()
X = iris.data # 특성 데이터
# Agglomerative Clustering 적용 (3개의 클러스터)
agg = AgglomerativeClustering(n_clusters=3)
assignment = agg.fit_predict(X)
# 첫 번째와 두 번째 특성을 사용하여 시각화
plt.figure(figsize=(8, 6))
mglearn.discrete_scatter(X[:, 0], X[:, 1], assignment)
plt.xlabel("Feature 0 (Sepal Length)")
plt.ylabel("Feature 1 (Sepal Width)")
plt.title("Agglomerative Clustering on Iris Dataset")
plt.show()

import matplotlib.pyplot as plt
import scipy.cluster.hierarchy as sch
from sklearn.datasets import load_iris
# Iris 데이터 로드
iris = load_iris()
X = iris.data # 특성 데이터
# 계층적 클러스터링을 위한 linkage 계산
linkage_matrix = sch.linkage(X, method='ward')
# 덴드로그램 그리기 (p=10 적용, 개별 샘플 대신 클러스터 그룹 표시)
plt.figure(figsize=(12, 6))
sch.dendrogram(linkage_matrix, truncate_mode='lastp', p=10, show_leaf_counts=False)
plt.xlabel("Cluster Groups")
plt.ylabel("Cluster Distance")
plt.title("Truncated Dendrogram (p=10) for Agglomerative Clustering on Iris Dataset")
plt.show()

파란색 선으로 구분할 경우 2개의 군집으로 나눌 수 있으며, 주황색선과 녹색선으로 구분할 경우 위치에 따라 3개 이상의 군집으로 나눌 수 있다.이처럼 덴드로그램을 그려보면 해당 데이터셋을 몇 개의 그룹으로 분류할 것인지 결정할 수 있다.
3) DBSCAN(Density-Based Spatial Clustering of Applications with Noise)
(1) DBSCAN 개요
- 밀도 기반 군집화 알고리즘으로, 사전에 클러스터 개수를 지정할 필요가 없음.
- 복잡한 모양의 클러스터도 탐지 가능하며, 노이즈 데이터(이상치)를 식별 가능.
- K-평균(k-means) 및 병합 군집(Agglomerative Clustering)보다 느리지만 대규모 데이터에도 적용 가능.
(2) DBSCAN의 핵심 개념
- 밀도가 높은 영역(dense region)이 클러스터를 형성하고, 밀도가 낮은 영역이 클러스터를 구분한다고 가정함.
- 데이터 포인트를 다음 세 가지 유형으로 분류:
- 코어 포인트(Core points): 주변 `eps` 거리 내에 `min_samples` 개 이상의 데이터가 있는 포인트.
- 경계 포인트(Boundary points): 코어 포인트 근처에 있지만, 자기 자신은 코어 포인트가 아닌 데이터.
- 노이즈(Noise): 어느 클러스터에도 속하지 않는 데이터.
(3) DBSCAN의 작동 원리
1. 아무 데이터 포인트나 선택하여 시작.
2. 선택한 포인트에서 `eps` 이내에 있는 점들의 개수를 확인.
- 개수가 `min_samples`보다 적으면 노이즈로 분류 (`-1` 레이블 할당).
- 개수가 `min_samples` 이상이면 새로운 클러스터 생성.
3. 클러스터가 확장됨:
- 새롭게 할당된 코어 포인트의 `eps` 이내에 있는 모든 데이터도 같은 클러스터에 포함.
- 새롭게 추가된 코어 포인트가 또 다른 코어 포인트를 포함하면 계속 확장됨.
4. 더 이상 확장할 수 없는 경우, 새로운 점을 선택하여 위 과정을 반복.
5. 모든 데이터가 처리되면 군집화 완료.
(4) DBSCAN의 주요 하이퍼파라미터
- `eps` (epsilon, 거리 반경)
- 두 점이 "가깝다"고 판단하는 거리 기준.
- 너무 작으면 대부분의 데이터가 노이즈로 분류됨.
- 너무 크면 모든 데이터가 하나의 클러스터로 묶일 수 있음.
- `min_samples` (최소 샘플 개수)
- 코어 포인트가 되기 위한 최소한의 이웃 데이터 개수.
- 값을 증가시키면 더 밀집된 클러스터만 유지되고, 밀도가 낮은 영역은 노이즈로 분류됨.
(5) 파라미터 설정에 따른 DBSCAN의 특징
- `eps` 증가 → 클러스터 크기가 커지고, 여러 클러스터가 하나로 합쳐질 가능성이 높아짐.
- `eps` 감소 → 데이터 간 거리를 더 엄격하게 계산하여 더 작은 클러스터를 형성함.
- `min_samples` 증가 → 클러스터의 최소 크기를 증가시켜, 작은 클러스터를 제거하고 노이즈를 늘림.
- `min_samples` 감소 → 더 작은 클러스터도 인정하여 군집화가 더 많이 이루어짐.
(6) DBSCAN의 장점과 단점
장점
- 클러스터 개수를 사전에 지정할 필요 없음.
- 밀도 기반 접근으로 인해 복잡한 형태의 클러스터도 탐지 가능.
- 노이즈(이상치)를 자동으로 식별할 수 있음.
단점
- `eps`와 `min_samples` 값을 적절하게 설정해야 함.
- 밀도가 일정하지 않은 경우(밀도 변화가 심한 데이터)에는 성능이 떨어질 수 있음.
- 고차원 데이터에서는 거리 계산 비용이 커지므로 속도가 느려질 수 있음.
(7) DBSCAN 적용 사례
a) 기본 데이터셋 적용
- `eps`와 `min_samples` 기본값 사용 시, 모든 데이터가 노이즈로 분류될 수 있음.
- 파라미터를 조정하면 제대로 된 클러스터가 형성됨.
b) two_moons 데이터셋 적용
- DBSCAN은 두 개의 반원 형태의 클러스터를 올바르게 탐지함.
- `eps`를 줄이면 클러스터 개수가 증가하고, `eps`를 늘리면 모든 데이터가 하나의 클러스터로 합쳐질 수 있음.
c) 스케일링의 중요성
- `StandardScaler` 또는 `MinMaxScaler`를 적용하면 `eps` 값을 설정하기 더 쉬워짐.
- 스케일링을 하지 않으면 특징값의 크기가 달라 `eps` 값을 조정하는 것이 어려울 수 있음.
다시 한 번 정리하면, DBSCAN은 밀도 기반 클러스터링 기법으로, 데이터의 분포를 기반으로 군집을 형성하며, 사전에 클러스터 개수를 설정할 필요 없이 복잡한 형태의 클러스터를 탐지할 수 있는 장점이 있다. 특히, 일반적인 군집화 알고리즘과 달리 노이즈 데이터를 구분할 수 있어 이상치 탐지에도 효과적으로 활용될 수 있다.DBSCAN의 성능을 최적화하기 위해서는 eps와 min_samples 파라미터를 적절히 조정하는 것이 필수적이며, 데이터의 특성이 다를 경우 스케일링을 고려하는 것이 중요하다. 또한, 이 알고리즘은 동일한 데이터를 여러 번 실행하더라도 클러스터링 결과가 동일하게 유지되지만, 경계에 위치한 데이터 포인트의 경우 방문 순서에 따라 클러스터 할당이 달라질 가능성이 있다.DBSCAN은 K-means나 Agglomerative Clustering보다 더 유연한 군집화 방법을 제공하지만, 파라미터 설정이 까다로울 수 있으며, 데이터의 밀도가 일정하지 않을 경우 클러스터링 성능이 저하될 수 있다는 한계가 있다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
# 1. 데이터 로드 (Iris 데이터셋 활용)
iris = load_iris()
X = iris.data # Iris 데이터의 특성 (feature) 사용
# 2. 데이터 스케일링 (DBSCAN은 거리 기반이므로 StandardScaler 적용)
scaler = StandardScaler() # 평균을 0, 표준편차를 1로 조정
X_scaled = scaler.fit_transform(X) # 데이터를 스케일링하여 변환
# 3. DBSCAN 클러스터링 적용
# - eps: 반경 (클러스터 내 점들 간 최대 거리)
# - min_samples: 코어 포인트가 되기 위한 최소한의 이웃 개수
dbscan = DBSCAN(eps=0.5, min_samples=5)
clusters = dbscan.fit_predict(X_scaled) # 클러스터 할당
# 4. 클러스터링 결과 출력
# - 클러스터 할당 결과를 출력하며, -1은 노이즈(군집에 속하지 않는 데이터)를 의미함
print("Cluster memberships:\n", clusters)
# 5. 클러스터링 결과 시각화
plt.figure(figsize=(8, 6))
# X_scaled[:, 0] (첫 번째 특성)과 X_scaled[:, 1] (두 번째 특성) 기준으로 산점도 출력
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=clusters, cmap="viridis", s=60)
# 그래프 라벨 및 제목 설정
plt.xlabel("Feature 0 (Standardized)")
plt.ylabel("Feature 1 (Standardized)")
plt.title("DBSCAN Clustering on Iris Dataset")
plt.colorbar(label="Cluster ID") # 색상 바를 통해 클러스터 ID 표시
plt.show()
# 결과
Cluster memberships:
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 -1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 -1 -1 0 0 0 0 0 0 0 -1 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 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 -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]

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
# 1. 데이터 생성 (반달 모양 데이터셋)
# - n_samples=200: 샘플 개수 200개
# - noise=0.05: 데이터에 약간의 노이즈 추가
# - random_state=0: 재현 가능한 결과를 위해 난수 고정
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
# 2. 데이터 스케일링 (평균=0, 표준편차=1로 변환하여 DBSCAN 적용 효과 향상)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # 데이터 표준화
# 3. DBSCAN 클러스터링 적용 (eps, min_samples 기본값 사용)
dbscan = DBSCAN(eps=0.3, min_samples=5) # eps 값 조정 가능
clusters = dbscan.fit_predict(X_scaled) # 클러스터 할당
# 4. 클러스터링 결과 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=clusters, cmap="viridis", s=60)
# 그래프 라벨 및 제목 설정
plt.xlabel("Feature 0 (Standardized)")
plt.ylabel("Feature 1 (Standardized)")
plt.title("DBSCAN Clustering on Moons Dataset")
plt.colorbar(label="Cluster ID") # 색상 바 추가하여 클러스터 ID 표시
plt.show()

다음은 DBSCAN의 라벨로 '-1'을 사용할 경우 발생할 수 있는 문제를 보여주는 코드다. '-1'은 노이즈(이상치)를 의미하므로, 사용하지 않는 것을 추천한다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
# 1. 데이터 생성 및 스케일링
X, y = make_moons(n_samples=200, noise=0.1, random_state=42)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 2. DBSCAN 적용
dbscan = DBSCAN(eps=0.2, min_samples=5)
clusters = dbscan.fit_predict(X_scaled)
# 3. 클러스터 결과 확인 (-1 포함 여부 확인)
unique_clusters = np.unique(clusters)
print("Cluster assignments:", unique_clusters)
# 4. 색상 배열을 **고의로 너무 작게 설정하여 IndexError 발생**
colors_with_noise = np.array(["red", "blue", "green"]) # 클러스터 개수를 초과하는 색상 개수로 설정
# 5. IndexError 발생 코드 (filtered_clusters의 값이 colors_with_noise 크기를 초과)
valid_clusters = clusters != -1 # 노이즈 제외
filtered_clusters = clusters[valid_clusters] # 노이즈가 제외된 클러스터 배열
print("Filtered clusters:", np.unique(filtered_clusters)) # 예상 출력: [0, 1, 2, 3, 4] → colors_with_noise 크기를 초과
# 6. IndexError 발생
try:
cluster_colors_valid = colors_with_noise[filtered_clusters] # IndexError 발생 예상
print("Valid cluster colors (excluding noise):", cluster_colors_valid[:10])
except IndexError as e:
print("IndexError:", e) # 오류 발생 예상
# 7. 노이즈 처리 코드 (IndexError 방지 필요)
cluster_colors_fixed = colors_with_noise[np.where(clusters == -1, 2, clusters)] # IndexError 발생 가능
print("Cluster colors with noise handled:", cluster_colors_fixed[:10])
# 8. 클러스터링 결과 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=cluster_colors_fixed, s=60)
plt.xlabel("Feature 0 (Standardized)")
plt.ylabel("Feature 1 (Standardized)")
plt.title("DBSCAN Clustering on Moons Dataset (with Noise)")
plt.show()
colors_with_noise 배열이 3개만 할당되었지만, filtered_clusters에는 3, 4, 5 등의 값이 포함되어 있어 IndexError 발생한다. 이를 방지하기 위해선 다음과 같이 코드를 수정할 수 있다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
# 1. 데이터 생성 및 스케일링
X, y = make_moons(n_samples=200, noise=0.1, random_state=42)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 2. DBSCAN 적용
dbscan = DBSCAN(eps=0.2, min_samples=5)
clusters = dbscan.fit_predict(X_scaled)
# 3. 클러스터 결과 확인 (-1 포함 여부 확인)
unique_clusters = np.unique(clusters)
print("Cluster assignments:", unique_clusters)
# 4. 색상 배열 크기 자동 조정 (IndexError 방지)
num_clusters = max(clusters) + 2 # -1 포함 최대 클러스터 개수
colors_with_noise = np.array(["red", "blue", "green", "orange", "purple", "cyan",
"brown", "pink", "yellow", "black", "lime", "magenta", "gray"])[:num_clusters]
# 5. IndexError 방지 코드 (filtered_clusters 인덱스가 colors_with_noise 범위를 초과하지 않도록 함)
valid_clusters = clusters != -1 # 노이즈 제외
filtered_clusters = clusters[valid_clusters] # 노이즈가 제외된 클러스터 배열
print("Filtered clusters:", np.unique(filtered_clusters))
# 6. 올바른 색상 매핑 (IndexError 방지)
cluster_colors_valid = colors_with_noise[np.minimum(filtered_clusters, len(colors_with_noise) - 1)]
print("Valid cluster colors (excluding noise):", cluster_colors_valid[:10])
# 7. 노이즈 처리 코드 (IndexError 방지)
cluster_colors_fixed = colors_with_noise[np.where(clusters == -1, len(colors_with_noise) - 1, clusters)]
print("Cluster colors with noise handled:", cluster_colors_fixed[:10])
# 8. 클러스터링 결과 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=cluster_colors_fixed, s=60)
plt.xlabel("Feature 0 (Standardized)")
plt.ylabel("Feature 1 (Standardized)")
plt.title("DBSCAN Clustering on Moons Dataset (with Noise)")
plt.show()
# 출력결과
Cluster assignments: [-1 0 1 2 3 4 5 6 7 8 9 10]
Filtered clusters: [ 0 1 2 3 4 5 6 7 8 9 10]
Valid cluster colors (excluding noise): ['brown' 'red' 'blue' 'black' 'green' 'orange' 'cyan' 'green' 'blue'
'purple']
Cluster colors with noise handled: ['magenta' 'brown' 'red' 'blue' 'magenta' 'magenta' 'magenta' 'black'
'green' 'orange']

5. 클러스터링 알고리즘 비교 및 평가
1) 각 알고리즘의 장단점
- K-평균: 간단하고 빠르지만, 군집의 개수를 미리 지정해야 하며 구형 클러스터만 찾을 수 있음.
- 병합 클러스터링: 클러스터 계층 구조를 탐색 가능하지만 계산 비용이 높음.
- DBSCAN: 이상치를 감지하고 복잡한 클러스터 형태를 탐지할 수 있지만, 밀도 기준을 잘 설정해야 함.
2) 클러스터링 평가 방법
클러스터링 평가 지표로 정확도를 사용하는 것은 의미가 없다. 클러스터링시 중요한 점은 군집 내에 어떤 데이터가 모여있는지가 더 중요하다.
- 클러스터링 결과의 해석은 정성적(qualitative) 분석이 필요.
- 클러스터의 응집도(cohesion)와 분리도(separation)를 기준으로 평가.
- 실루엣 점수(Silhouette Score) 등을 활용하여 클러스터 품질을 수치적으로 분석 가능.
실루엣 점수(Silhouette Score)는 클러스터링 결과의 품질을 수치적으로 분석할 수 있는 대표적인 평가 지표 중 하나이다. 이 점수는 각 데이터 포인트가 자신이 속한 클러스터 내에서 얼마나 밀집되어 있는지(응집도)와 다른 클러스터와는 얼마나 잘 분리되어 있는지(분리도)를 동시에 고려하여 계산된다.
실루엣 점수는 −1에서 1 사이의 값을 가지며, 1에 가까울수록 클러스터링이 잘 이루어졌음을 의미한다. 점수가 1에 가까우면 해당 데이터 포인트는 같은 클러스터 내의 다른 점들과 가까우면서, 동시에 다른 클러스터와는 멀리 떨어져 있다는 뜻이다. 반대로 0에 가까운 경우는 클러스터의 경계에 위치해 있어 어느 클러스터에도 속하기 애매한 상태이며, 0보다 작은 경우는 해당 포인트가 현재 속한 클러스터보다 오히려 다른 클러스터에 속하는 것이 더 적절하다는 것을 나타낸다. 이는 클러스터링이 잘못되었음을 시사한다.
실루엣 점수는 직관적이고 일반적인 클러스터링 평가에 효과적이지만, 군집이 복잡한 구조를 가질 경우(예: 반달 모양이나 동심원 구조 등)에는 정확한 평가가 어려울 수 있다. 이는 실루엣 점수가 주로 밀집된 구형 클러스터에 유리한 평가 기준이기 때문이다. 따라서 데이터의 구조가 비선형적이거나 비정형적일 경우에는 실루엣 점수 하나만으로 클러스터 품질을 판단하기보다는 시각화나 다른 보조 지표들과 함께 종합적으로 해석하는 것이 바람직하다.
$$
\text{Silhouette score } s = \frac{b - a}{\max(a, b)}
$$
$a$ : 동일 클러스터 내의 다른 점들과의 평균 거리 (intra-cluster distance)
$b$: 가장 가까운 다른 클러스터의 점들과의 평균 거리 (nearest-cluster distance)
# ==============================================
# 1. 필수 라이브러리 임포트
# ==============================================
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.metrics import adjusted_rand_score
# mglearn이 없다면 대체 컬러맵 사용
try:
import mglearn
cmap = mglearn.cm3
except ImportError:
cmap = plt.cm.Set1 # 간단 대체
# ==============================================
# 2. 두 개의 반달형 데이터 생성
# ==============================================
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
# ==============================================
# 3. 특성 스케일링 (평균 0, 분산 1)
# ==============================================
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# ==============================================
# 4. 시각화 준비
# ==============================================
fig, axes = plt.subplots(1, 4, figsize=(15, 3),
subplot_kw={'xticks': (), 'yticks': ()})
# ==============================================
# 5. 비교할 클러스터링 알고리즘 정의
# ==============================================
algorithms = [
KMeans(n_clusters=2, random_state=0),
AgglomerativeClustering(n_clusters=2),
DBSCAN() # eps, min_samples 기본값 사용
]
# ==============================================
# 6. 랜덤 클러스터(기준선) 생성·플로팅
# ==============================================
rng = np.random.RandomState(0)
random_clusters = rng.randint(low=0, high=2, size=len(X_scaled))
axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1],
c=random_clusters, cmap=cmap, s=60)
axes[0].set_title(f"Random – ARI: {adjusted_rand_score(y, random_clusters):.2f}")
# ==============================================
# 7. 각 알고리즘 적용·플로팅
# ==============================================
for ax, algo in zip(axes[1:], algorithms):
clusters = algo.fit_predict(X_scaled)
ax.scatter(X_scaled[:, 0], X_scaled[:, 1],
c=clusters, cmap=cmap, s=60)
score = adjusted_rand_score(y, clusters)
ax.set_title(f"{algo.__class__.__name__} – ARI: {score:.2f}")
# ==============================================
# 8. 화면에 출력
# ==============================================
plt.tight_layout()
plt.show()

ARI (Adjusted Rand Index, 조정된 랜드 지수) 란 군집 결과(예측된 클러스터)와 실제 레이블(ground truth) 사이의 유사성을 측정하는 지표입니다. Rand Index(RI)는 두 데이터 포인트 쌍이 같은 클러스터에 속하거나 다른 클러스터에 속하는지의 일치도를 기반으로 계산된다. 하지만 RI는 무작위 군집에도 높은 점수를 줄 수 있기 때문에, 이를 보정한 것이 ARI다. ARI 값이 1인 경우 완벽히 일치하는 군집 결과이며, 0인 경우 무작위 군집과 같은 수준을 나타낸다. 음수일 경우는 무작위보다 못한 군집 결과를 의미한다.
$$
\text{ARI} = \frac{\text{RI} - \mathbb{E}[\text{RI}]}{\max(\text{RI}) - \mathbb{E}[\text{RI}]}
$$
# ==============================================
# 1. 필수 라이브러리 임포트
# ==============================================
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.metrics import silhouette_score
# mglearn 색상 없을 경우 대체
try:
import mglearn
cmap = mglearn.cm3
except ImportError:
cmap = plt.cm.Set1
# ==============================================
# 2. 데이터 생성 및 정규화
# ==============================================
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
X_scaled = StandardScaler().fit_transform(X)
# ==============================================
# 3. 시각화용 플롯 설정
# ==============================================
fig, axes = plt.subplots(1, 4, figsize=(15, 3),
subplot_kw={'xticks': (), 'yticks': ()})
# ==============================================
# 4. 랜덤 클러스터링 (참고용)
# ==============================================
random_state = np.random.RandomState(seed=0)
random_clusters = random_state.randint(low=0, high=2, size=len(X_scaled))
axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c=random_clusters,
cmap=cmap, s=60)
score = silhouette_score(X_scaled, random_clusters)
axes[0].set_title(f"Random assignment: {score:.2f}")
# ==============================================
# 5. 클러스터링 알고리즘 적용 및 실루엣 점수 계산
# ==============================================
algorithms = [
KMeans(n_clusters=2, random_state=0),
AgglomerativeClustering(n_clusters=2),
DBSCAN()
]
for ax, algorithm in zip(axes[1:], algorithms):
clusters = algorithm.fit_predict(X_scaled)
# DBSCAN은 noise(-1) 군집 포함 → 예외 처리 필요
if len(set(clusters)) > 1 and np.min(clusters) >= 0:
score = silhouette_score(X_scaled, clusters)
else:
score = -1 # 실루엣 점수 계산 불가
ax.scatter(X_scaled[:, 0], X_scaled[:, 1], c=clusters, cmap=cmap, s=60)
ax.set_title(f"{algorithm.__class__.__name__}: {score:.2f}")
plt.tight_layout()
plt.show()

실루엣 점수는 클러스터링 결과의 품질을 평가하는 데 널리 사용되는 수치적 지표이지만, 그것만으로 클러스터링의 유효성을 완전히 판단하기에는 한계가 있다. 예를 들어, 실루엣 점수 기준으로는 K-means가 가장 높은 점수를 받을 수 있지만, 데이터의 구조나 의미를 고려했을 때는 DBSCAN과 같은 알고리즘이 더 적절한 결과를 제공할 수도 있다. 이는 실루엣 점수가 군집의 응집도와 분리도를 기반으로 하기 때문에, 데이터의 의미적 구조나 사용자의 관심사와의 관련성은 반영하지 못하기 때문이다.
보다 나은 평가 방법으로는 강건성 기반 클러스터링 평가(robustness-based clustering metrics)가 있다. 이 방법은 클러스터링을 수행할 때 데이터에 소량의 노이즈를 추가하거나 알고리즘의 파라미터를 변화시키고, 여러 조건에서 반복적으로 실행하여 결과 간의 일관성을 측정하는 방식이다. 만약 다양한 조건에서도 동일하거나 유사한 군집 결과가 반복적으로 나타난다면, 해당 클러스터링은 신뢰할 수 있는 것으로 간주할 수 있다. 그러나 이러한 강건성 기반 평가 방법은 현재 scikit-learn 라이브러리에 기본적으로 구현되어 있지 않다.
더 중요한 문제는, 아무리 실루엣 점수가 높거나 클러스터링 결과가 강건하더라도, 그 군집이 실제로 사용자가 관심을 가지는 의미 있는 특성을 반영하는지는 별도의 문제라는 점이다. 예를 들어 얼굴 이미지 데이터를 클러스터링했을 때, 사용자가 기대하는 것은 남성과 여성, 노인과 청년, 혹은 수염이 있는 사람과 없는 사람 같은 구분일 수 있다. 그러나 실제 클러스터링 결과는 사진의 정면/측면 방향, 촬영 시간대, 혹은 촬영 기기(iPhone vs Android) 등의 기준에 의해 나뉘었을 가능성도 있다. 이처럼 클러스터링 결과가 의미 있는 정보를 반영하는지를 확인하기 위해서는, 자동화된 수치 평가뿐 아니라 사람에 의한 직접적인 분석과 해석이 필수적이다.
6. 얼굴 이미지 데이터셋(Labeled Faces in the Wild)을 대상으로 다양한 클러스터링 알고리즘을 적용하고 해석
이 실험의 목적은 얼굴 이미지 데이터셋에서 클러스터링 알고리즘을 활용하여 의미 있는 집단을 자동으로 찾아낼 수 있는지를 탐구하는 것이다. 우리가 기대하는 집단은 "웃는 사람", "남성과 여성", "특정 인물" 등과 같은 시멘틱한 기준이지만, 클러스터링 알고리즘은 데이터의 시각적 또는 통계적 유사성을 기반으로 분류할 뿐, 사람이 생각하는 의미 있는 구분을 보장하지는 않는다. 이러한 한계 때문에 결과는 수치적인 지표뿐 아니라 직접적인 시각적 분석이 필요하다.
고차원인 이미지 데이터를 효율적으로 처리하기 위해 PCA(주성분 분석)를 사용하여 100차원으로 축소한다. 이 과정은 연산 속도를 높이고, 얼굴 이미지의 의미 있는 구조를 더 잘 반영하도록 돕는다.
from sklearn.decomposition import PCA
from sklearn.datasets import fetch_lfw_people
# 얼굴 데이터셋 로드: 최소 20장 이상의 이미지가 있는 인물만 포함
people = fetch_lfw_people(min_faces_per_person=20, resize=0.7)
# 이미지 데이터를 벡터로 변환한 형태 (n_samples, n_features)
X_people = people.data
# 이미지의 원래 shape 저장 (시각화에 필요)
image_shape = people.images[0].shape
# PCA 적용 (100차원으로 축소)
pca = PCA(n_components=100, whiten=True, random_state=0)
X_pca = pca.fit_transform(X_people)
DBSCAN은 밀도 기반 클러스터링으로, 클러스터 수를 미리 지정할 필요가 없고 노이즈 탐지가 가능하다. 그러나 기본 설정으로는 모든 포인트가 -1 (노이즈)로 분류되었다. eps 값을 증가시키고 min_samples 값을 줄이면 클러스터가 일부 형성된다.
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
import numpy as np
# eps 값 리스트
eps_values = [1, 3, 5, 7, 9, 11, 13]
# 플롯 설정
n_cols = 4
n_rows = (len(eps_values) + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 8),
subplot_kw={'xticks': (), 'yticks': ()})
axes = axes.ravel()
for i, eps in enumerate(eps_values):
dbscan = DBSCAN(eps=eps, min_samples=3)
labels = dbscan.fit_predict(X_pca)
unique_labels = np.unique(labels)
n_clusters = len(unique_labels) - (1 if -1 in labels else 0)
n_noise = list(labels).count(-1)
axes[i].scatter(X_pca[:, 0], X_pca[:, 1], c=labels, cmap='tab10', s=10)
axes[i].set_title(f"eps={eps}\nclusters={n_clusters}, noise={n_noise}")
# 빈 플롯 제거
for j in range(i+1, len(axes)):
fig.delaxes(axes[j])
plt.tight_layout()
plt.show()

DBSCAN은 eps=7일 때 다수의 노이즈와 13개의 소규모 클러스터를 형성하며, 일반적이지 않은 얼굴(모자, 가림 등)을 효과적으로 이상치로 탐지하였다.
계층적 군집화(Agglomerative Clustering)는 거리 기반으로 군집을 병합해가며, 클러스터 수는 지정해야 한다. n_clusters=10일 때 KMeans보다 다소 불균형한 분포를 보였다.
from sklearn.cluster import AgglomerativeClustering
# Agglomerative Clustering (10개 군집)
agglo = AgglomerativeClustering(n_clusters=10)
labels_agg = agglo.fit_predict(X_pca)
print("Agglomerative 클러스터 크기:", np.bincount(labels_agg))
# Agglomerative 클러스터 크기: [998 880 100 391 158 172 110 40 151 23]
KMeans와의 결과 유사성을 측정하면 ARI(Adjusted Rand Index)는 약 0.13로 낮아, 두 알고리즘의 결과가 상당히 다름을 보여준다.
from sklearn.metrics import adjusted_rand_score
print("ARI:", adjusted_rand_score(labels_km, labels_agg)) # 결과: 0.13
덴드로그램 시각화를 통해 계층 구조를 시각적으로 확인할 수 있다
from scipy.cluster.hierarchy import ward, dendrogram
linkage_array = ward(X_pca)
plt.figure(figsize=(20, 5))
dendrogram(linkage_array, p=7, truncate_mode='level', no_labels=True)
plt.xlabel("sample index")
plt.ylabel("Cluster distance")

클러스터 수를 40개로 늘리면 더 정교한 분류가 가능해진다. 몇몇 군집은 "웃는 여성", "짙은 피부톤", "셔츠가 보이는 사람" 등 명확한 시각적 특징을 공유했다.
# Agglomerative Clustering (40개 군집)
agglo_40 = AgglomerativeClustering(n_clusters=40)
labels_agg_40 = agglo_40.fit_predict(X_pca)
# 일부 흥미로운 클러스터 시각화
for cluster in [10, 13, 19, 22, 36]:
mask = labels_agg_40 == cluster
fig, axes = plt.subplots(1, 10, figsize=(15, 4),
subplot_kw={'xticks': (), 'yticks': ()})
for image, ax in zip(X_people[mask][:10], axes):
ax.imshow(image.reshape(image_shape), vmin=0, vmax=1)





- DBSCAN은 노이즈 탐지와 소규모 군집 형성에 효과적이지만, 전체 데이터에 대한 일반적인 구조는 잘 파악하지 못한다.
- KMeans는 균등한 크기의 클러스터를 제공하지만, 의미 있는 구분과는 거리가 멀 수 있다.
- Agglomerative Clustering은 계층 구조를 제공하고, 더 세분화된 클러스터링이 가능하나 결과 해석에는 주관적 판단이 필요하다.
- 클러스터링의 최종 해석은 반드시 사람의 시각적 분석을 동반해야 하며, 자동화된 지표로는 제한적이라는 점이 이 실험의 핵심 교훈이다.
7. 요약정리
1) 비지도 학습의 역할
- 데이터 탐색과 패턴 인식에 활용됨.
- 지도 학습을 위한 전처리 및 특징 추출에도 사용 가능.
2) 차원 축소와 클러스터링의 중요성
- 차원 축소는 데이터의 복잡성을 줄이면서 중요한 정보를 유지하는 역할을 함.
- 클러스터링은 데이터의 구조를 파악하고 그룹을 형성하는 데 유용.
3) 비지도 학습 알고리즘 활용
- 다양한 데이터셋(예: 숫자 이미지, 얼굴 이미지, 유방암 데이터)에서 실험 가능.
- 탐색적 데이터 분석(EDA)과 데이터 시각화의 필수 도구.
'데이터분석' 카테고리의 다른 글
| Chapter 2. 지도학습(Supervised learning): 부스팅(Boosting) (0) | 2024.09.21 |
|---|---|
| Chapter 2. 지도학습(Supervised learning): 랜덤포레스트(Random Forest) (0) | 2024.09.21 |
| Chapter 2. 지도학습(Supervised learning): 의사결정나무(Decision Trees) (0) | 2024.09.02 |
| Chapter 2. 지도학습(Supervised learning): Naive Bayes classifier (1) | 2024.09.02 |
| Chapter 2. 지도학습(Supervised learning): SVM (0) | 2024.08.27 |