9.1 군집
9.1.1 k-평균
K-평균 알고리즘(K-Means Algorithm) 은 비지도 학습(Unsupervised Learning) 에서 가장 널리 사용되는 군집화(Clustering) 알고리즘입니다.
klearn.cluster.KMeans는 사이킷런에서 제공하는 K-평균 군집화 클래스입니다.
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3, n_init=10, random_state=42)
kmeans.fit(X)
주요 파라미터 | 설명 |
n_clusters | 군집 수 (기본값=8) |
init | 중심점 초기화 방식 ('k-means++' 기본값, 'random' 또는 배열) |
n_init | 다른 초기값으로 반복 횟수 (기본값=10) |
max_iter | 반복 최대 횟수 (기본=300) |
random_state | 랜덤 시드 고정 |
tol | 수렴 기준 허용 오차 |
algorithm | 알고리즘 선택 ('lloyd', 'elkan', 'auto') |
주요 속성 | 성명 |
cluster_centers_ | 각 클러스터 중심점 좌표 |
labels_ | 각 샘플의 클러스터 번호 |
inertia_ | 군집 내 거리 제곱합 (WCSS, 목적함수 값) |
n_iter_ | 실제 반복 횟수 |
주요 메서드 | 설명 |
fit(X) | 군집 학습 수행 |
predict(X) | 샘플의 클러스터 예측 |
fit_predict(X) | 학습 + 클러스터 예측 |
transform(X) | 각 샘플이 클러스터 중심과의 거리 계산 |
fit_transform(X) | 학습 후 거리 반환 |
K-Means 알고리즘
- 초기화: 임의로 K개의 중심점을 선택
- 할당 단계:
- 각 샘플을 가장 가까운 중심점에 할당
- $C_i = \arg\min_j \| x_i - \mu_j \|^2$
- 중심 업데이트:
- 각 클러스터의 평균값으로 중심점 이동
- $\mu_j = \frac{1}{|C_j|} \sum_{x_i \in C_j} x_i$
- 반복: 중심점이 거의 움직이지 않을 때까지 2-3번 반복
아래 코드는 make_blobs로 5개의 중심을 가진 2차원 데이터를 생성 후, KMeans로 5개의 클러스터로 분류한 결과입니다.
원본 레이블과, K-Means군집 결과를 시각화 합니다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import koreanize_matplotlib
# 데이터 생성
blob_centers = np.array([[0.2, 2.3], [-1.5, 2.3], [-2.8, 1.8], [-2.8, 2.8], [-2.8, 1.3]])
blob_std = np.array([0.4, 0.3, 0.1, 0.1, 0.1])
X, y = make_blobs(n_samples=2000, centers=blob_centers, cluster_std=blob_std, random_state=7)
# KMeans 군집화
k = 5
kmeans = KMeans(n_clusters=k, n_init=10, random_state=42)
y_pred = kmeans.fit_predict(X)
X_new = np.array([[0, 2], [3, 2], [-3, 3], [-3, 2.5]])
# 시각화 함수 정의
def plot_data(X):
plt.plot(X[:, 0], X[:, 1], 'k.', markersize=2)
def plot_centroids(centroids, weights=None, circle_color='w', cross_color='k'):
if weights is not None:
centroids = centroids[weights > weights.max() / 10]
plt.scatter(centroids[:, 0], centroids[:, 1],
marker='o', s=35, linewidths=8,
color=circle_color, zorder=10, alpha=0.9)
plt.scatter(centroids[:, 0], centroids[:, 1],
marker='x', s=2, linewidths=12,
color=cross_color, zorder=11, alpha=1)
def plot_decision_boundaries(clusterer, X, resolution=1000, show_centroids=True,
show_xlabels=True, show_ylabels=True):
mins = X.min(axis=0) - 0.1
maxs = X.max(axis=0) + 0.1
xx, yy = np.meshgrid(np.linspace(mins[0], maxs[0], resolution),
np.linspace(mins[1], maxs[1], resolution))
Z = clusterer.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(Z, extent=(mins[0], maxs[0], mins[1], maxs[1]),
cmap="Pastel2")
plt.contour(Z, extent=(mins[0], maxs[0], mins[1], maxs[1]),
linewidths=1, colors='k')
plot_data(X)
if show_centroids:
plot_centroids(clusterer.cluster_centers_)
if show_xlabels:
plt.xlabel("$x_1$")
else:
plt.tick_params(labelbottom=False)
if show_ylabels:
plt.ylabel("$x_2$", rotation=0)
else:
plt.tick_params(labelleft=False)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
plt.sca(axes[0])
plt.scatter(X[:, 0], X[:, 1], c=y, s=1)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$", rotation=0)
plt.title("원본 데이터 (레이블 기반)")
plt.grid(True)
plt.sca(axes[1])
plot_decision_boundaries(kmeans, X)
plt.title("K-평균 클러스터링 결과")
plt.tight_layout()
plt.show()
# 1. 예측 결과 및 비교
print('예측 결과 및 비교')
print(y_pred)
print(kmeans.labels_)
print(y_pred is kmeans.labels_) # 객체 비교
# 2. 군집 중심
print('\n군집 중심')
print(kmeans.cluster_centers_)
# 3. 새로운 샘플(X_new)에 대한 군집 예측
print('\n새로운 샘플의 예측')
X_new = np.array([[0, 2], [3, 2], [-3, 3], [-3, 2.5]])
predicted_clusters = kmeans.predict(X_new)
print(X_new)
print(predicted_clusters)
# 4. 각 샘플의 군집 중심과의 거리 (유클리드 거리)
print('\n군집 중심과의 거리')
distances_via_transform = kmeans.transform(X_new).round(2)
print(distances_via_transform)
# 직접 유클리드 거리 계산 (same as transform)
distances_manual = np.linalg.norm(np.tile(X_new, (1, k)).reshape(-1, k, 2) - kmeans.cluster_centers_, axis=2).round(2)
print(distances_manual)
print(np.allclose(distances_via_transform, distances_manual))
예측 결과 및 비교
[0 0 4 ... 3 1 0]
[0 0 4 ... 3 1 0]
True
군집 중심
[[-2.80214068 1.55162671]
[ 0.08703534 2.58438091]
[-1.46869323 2.28214236]
[-2.79290307 2.79641063]
[ 0.31332823 1.96822352]]
새로운 샘플의 예측
[[ 0. 2. ]
[ 3. 2. ]
[-3. 3. ]
[-3. 2.5]]
[4 4 3 3]
군집 중심과의 거리
[[2.84 0.59 1.5 2.9 0.31]
[5.82 2.97 4.48 5.85 2.69]
[1.46 3.11 1.69 0.29 3.47]
[0.97 3.09 1.55 0.36 3.36]]
[[2.84 0.59 1.5 2.9 0.31]
[5.82 2.97 4.48 5.85 2.69]
[1.46 3.11 1.69 0.29 3.47]
[0.97 3.09 1.55 0.36 3.36]]
True
fit_predict() 반환값과 labels_ 속성은 같은 결과를 나타냅니다. 해당 샘플의 군집을 의미합니다.
cluster_centers_ 는 군집의 중점을 의미합니다. 두번째 그래프에서 군집의 중심을 X로 표시했습니다.
새로운 데이터를 예측하면 , 어느 클러스터인지 반환합니다.
transform()는 각 군집 중점과의 거리를 나타냅니다.
수동으로 유클리드 거리를 계산한 결과와 동일합니다.
다음은 초기 중심을 랜덤하게 선택 후, 3번의 반복 시 중점과 경계가 어떻게 변화하는 지 보여주는 예제입니다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import koreanize_matplotlib
# blob 설정
blob_centers = np.array([[0.2, 2.3], [-1.5, 2.3], [-2.8, 1.8], [-2.8, 2.8], [-2.8, 1.3]])
blob_std = np.array([0.4, 0.3, 0.1, 0.1, 0.1])
X, y = make_blobs(n_samples=2000, centers=blob_centers, cluster_std=blob_std, random_state=7)
# 시각화 함수 정의
def plot_data(X):
plt.scatter(X[:, 0], X[:, 1], c='gray', s=10, alpha=0.6)
def plot_centroids(centroids, circle_color='w', cross_color='k', size=25):
plt.scatter(centroids[:, 0], centroids[:, 1], marker='o', s=size, linewidths=1, edgecolor=circle_color, facecolor='none', zorder=10)
plt.scatter(centroids[:, 0], centroids[:, 1], marker='x', s=size, linewidths=1, color=cross_color, zorder=11)
def plot_decision_boundaries(clusterer, X, resolution=500, show_centroids=True,
show_xlabels=True, show_ylabels=True):
mins = X.min(axis=0) - 0.1
maxs = X.max(axis=0) + 0.1
xx, yy = np.meshgrid(np.linspace(mins[0], maxs[0], resolution),
np.linspace(mins[1], maxs[1], resolution))
Z = clusterer.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(Z, extent=(mins[0], maxs[0], mins[1], maxs[1]), cmap="Pastel2", alpha=0.5)
plt.contour(Z, extent=(mins[0], maxs[0], mins[1], maxs[1]), linewidths=1, colors='k')
plot_data(X)
if show_centroids:
plot_centroids(clusterer.cluster_centers_)
if show_xlabels:
plt.xlabel("$x_1$")
else:
plt.tick_params(labelbottom=False)
if show_ylabels:
plt.ylabel("$x_2$", rotation=0)
else:
plt.tick_params(labelleft=False)
# KMeans 모델 생성 및 학습
kmeans_iter1 = KMeans(n_clusters=5, init="random", n_init=1, max_iter=1, random_state=5).fit(X)
kmeans_iter2 = KMeans(n_clusters=5, init="random", n_init=1, max_iter=2, random_state=5).fit(X)
kmeans_iter3 = KMeans(n_clusters=5, init="random", n_init=1, max_iter=3, random_state=5).fit(X)
# 중심점 이동 궤적 계산
centroids1 = kmeans_iter1.cluster_centers_
centroids2 = kmeans_iter2.cluster_centers_
centroids3 = kmeans_iter3.cluster_centers_
# 클러스터 간 매칭을 위한 거리 기반 정렬
from scipy.optimize import linear_sum_assignment
_, col_idx = linear_sum_assignment(np.linalg.norm(centroids1[:, None, :] - centroids2[None, :, :], axis=2))
centroids2 = centroids2[col_idx]
centroids3 = centroids3[col_idx] # 2-3단계도 같은 인덱스 기준 정렬
# 시각화
fig, axes = plt.subplots(3, 2, figsize=(12, 12))
titles = [
"① 랜덤 초기화 및 첫 번째 중심 설정",
"② 초기 중심으로 레이블 할당",
"③ 중심 업데이트 (1→2단계)",
"④ 거리 계산 후 레이블 재할당 (2회 반복)",
"⑤ 중심 재업데이트 (2→3단계)",
"⑥ 거리 계산 후 레이블 재할당 (3회 반복)"
]
for ax, title in zip(axes.flat, titles):
ax.set_title(title)
# 각 단계별 그래프
plt.sca(axes[0, 0])
plot_data(X)
plot_centroids(centroids1, circle_color='r', cross_color='r',size=25)
plt.ylabel("$x_2$", rotation=0)
plt.tick_params(labelbottom=False)
plt.sca(axes[0, 1])
plot_decision_boundaries(kmeans_iter1, X, show_xlabels=False, show_ylabels=False)
plt.sca(axes[1, 0])
plot_decision_boundaries(kmeans_iter1, X, show_centroids=False, show_xlabels=False)
plot_centroids(centroids1, circle_color='r', cross_color='r', size=10)
plot_centroids(centroids2, circle_color='b', cross_color='b', size=25)
for i in range(5):
plt.plot([centroids1[i, 0], centroids2[i, 0]], [centroids1[i, 1], centroids2[i, 1]], 'r-')
plt.sca(axes[1, 1])
plot_decision_boundaries(kmeans_iter2, X, show_xlabels=False, show_ylabels=False)
plt.sca(axes[2, 0])
plot_decision_boundaries(kmeans_iter2, X, show_centroids=False)
plot_centroids(centroids2, circle_color='b', cross_color='b', size=10)
plot_centroids(centroids3, circle_color='g', cross_color='g', size=25)
for i in range(5):
plt.plot([centroids2[i, 0], centroids3[i, 0]], [centroids2[i, 1], centroids3[i, 1]], 'r-')
plt.sca(axes[2, 1])
plot_decision_boundaries(kmeans_iter3, X, show_ylabels=False)
plt.tight_layout()
plt.show()
센트로이드 초기화 방법
K-평균 알고리즘에서 초기 중심점(centroid)을 어떻게 선택하느냐는 군집화의 품질과 수렴 속도에 매우 큰 영향을 미칩니다.
scikit-learn의 KMeans 클래스에서는 이를 위한 설정이 init과 n_init입니다.
n_init : 초기화를 n번 실행하고, 그중 가장 좋은 솔루션을 선택합니다.
초기값은 10입니다.
init
init | 설명 |
'k-means++' (기본값) | 초기 중심을 분산이 최대가 되도록 스마트하게 선택하여 수렴 속도와 군집 품질 향상 |
'random' | 데이터 포인트 중 무작위로 K개 선택 |
ndarray | 중심 좌료를 리스트에 담아, numpy배열로 전달하는 방식 |
k-means++ 알고리즘
중심점 kk개를 다음처럼 차례로 선택합니다:
- 첫 중심 μ1:데이터를 무작위로 하나 선택
- 그 다음 중심 $\mu_2, \mu_3, ..., \mu_k$:
- 각 샘플 $x_i$ 와 가장 가까운 기존 중심까지의 거리 $D(x_i)$ 를 계산
- 이 거리의 제곱 $D(x_i)^2$ 을 확률분포로 사용하여, 멀리 떨어진 점이 선택될 확률을 높임
- $P(x_i) = \frac{D(x_i)^2}{\sum_j D(x_j)^2}$
3. 이 과정을 K개 중심이 선택될 때까지 반복
k-평균 속도 개선과 미니배치 k-평균
기본 KMeans는 모든 반복마다 전체 샘플에 대해 모든 중심점과의 거리 계산을 수행하여 대규모 데이터에서는 느립니다.
엘칸 논문에서 삼각 부등식을 이용해 불필요한 거리 계산을 줄이는 방법을 제안했습니다. (유클리드 거리만 가능)
생성자에 아래와 같이 설정하여 엘칸 알고리즘을 사용할 수 있습니다.
KMeans(n_clusters=5, algorithm='elkan')
데이터셋에 따라 Train속도가 느려질 수 있습니다.
또하나의 속도 개선은 미니배치를 사용하는 방법입니다.
전체 데이터를 한꺼번에 사용하는 대신, 작은 배치(mini-batch) 로 나눠 학습합니다.
Sklearn의 MiniBatchKMeans 를 사용합니다.
from sklearn.cluster import MiniBatchKMeans
mb_kmeans = MiniBatchKMeans(n_clusters=5, batch_size=100, random_state=42)
mb_kmeans.fit(X)
numpy의 mmap과 같이 사용할 수 있습니다.
from sklearn.datasets import fetch_openml
from sklearn.cluster import MiniBatchKMeans
from timeit import timeit
from sklearn.metrics import adjusted_rand_score
mnist = fetch_openml('mnist_784', as_frame=False, parser="auto")
X_train, y_train = mnist.data[:60000], mnist.target[:60000]
X_test, y_test = mnist.data[60000:], mnist.target[60000:]
filename = "mymnist.mmap"
X_memmap = np.memmap(filename, dtype='float64', mode='write', shape=X_train.shape)
X_memmap[:] = X_train
X_memmap.flush()
minibatch_kmeans = MiniBatchKMeans(n_clusters=10, batch_size=10, n_init=3, random_state=42)
minibatch_kmeans.fit(X_memmap)
# 군집 라벨 vs 실제 라벨
y_pred = minibatch_kmeans.predict(X_test)
# 실제 라벨은 문자열이므로 정수로 변환
from sklearn.preprocessing import LabelEncoder
y_test_int = LabelEncoder().fit_transform(y_test)
# metric 계산
ari = adjusted_rand_score(y_test_int, y_pred)
print(f"Adjusted Rand Index (ARI): {ari:.3f}")
Adjusted Rand Index (ARI): 0.406
실제 레이블을 군집으로 할 때 , 같은 군집들의 샘플 중 40% 정도 일치합니다.
최적의 클러스터 개수 찾기
k-means에서는 클러스터의 개수 k를 지정해야하는데, 3가지 대표적인 방법을 소해하였습니다.
(1) 엘보 방법 (Elbow Method)
클러스터 수 k를 1부터 늘려가며 WCSS(Within-Cluster Sum of Squares), 즉 inertia_ 값을 계산합니다.
클러스터 수가 늘어날수록 오차는 줄지만, 어느 지점부터 감소율이 급격하게 줄어듭니다.
이 “팔꿈치” 지점을 최적의 K로 판단합니다.
plot으로 elbow를 확인하는데 x축은 클러스터 수 k, y축을 inertia (총 거리 제곱합) 하여 오차가 급격히 줄어들다 둔화되는 지점이 최적 K로 선정합니다.
(2) 실루엣 점수 (Silhouette Score)
각 샘플의 군집 품질을 평가하는 점수로, 자기 클러스터 내 응집도(a)와 가장 가까운 다른 클러스터와의 거리(b) 를 비교합니다.
$s(i)= \frac{b(i)−a(i)}{max(a(i),b(i))}$
값 범위: -1 ~ 1
1: 완벽하게 잘 군집됨
0: 경계에 위치
음수: 잘못된 군집
plot으로 x축: 클러스터 수 k, y축: 평균 실루엣 점수로 하고 가장 높은 지점을 최적 K로 선택합니다.
(3) 실루엣 다이어그램 (Silhouette Diagram)
각 군집마다 샘플의 실루엣 점수 분포를 시각화합니다.
좋은 군집일수록 실루엣 점수가 높고(막대가 오른쪽), 군집 간 크기와 밀도가 균일합니다.
균형 잡힌 막대 구조가 좋은 군집 구조
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score, silhouette_samples
from matplotlib.ticker import FixedLocator, FixedFormatter
# 데이터 생성
blob_centers = np.array([[0.2, 2.3], [-1.5, 2.3], [-2.8, 1.8],
[-2.8, 2.8], [-2.8, 1.3]])
blob_std = np.array([0.4, 0.3, 0.1, 0.1, 0.1])
X, y = make_blobs(n_samples=2000, centers=blob_centers,
cluster_std=blob_std, random_state=7)
# KMeans 훈련
kmeans_per_k = [KMeans(n_clusters=k, n_init=10, random_state=42).fit(X)
for k in range(1, 10)]
inertias = [model.inertia_ for model in kmeans_per_k]
silhouette_scores = [silhouette_score(X, model.labels_)
for model in kmeans_per_k[1:]]
# 3x2 시각화
fig, axes = plt.subplots(3, 2, figsize=(12, 12))
# 엘보 그래프
axes[0, 0].plot(range(1, 10), inertias, "bo-")
axes[0, 0].set_xlabel("$k$")
axes[0, 0].set_ylabel("이너셔")
axes[0, 0].annotate("", xy=(4, inertias[3]), xytext=(4.45, 650),
arrowprops=dict(facecolor='black', shrink=0.1))
axes[0, 0].text(4.5, 650, "엘보", horizontalalignment="center")
axes[0, 0].set_xlim(1, 8.5)
axes[0, 0].set_ylim(0, 1300)
axes[0, 0].grid()
axes[0, 0].set_title("엘보 방법")
# 실루엣 점수 그래프
axes[0, 1].plot(range(2, 10), silhouette_scores, "bo-")
axes[0, 1].set_xlabel("$k$")
axes[0, 1].set_ylabel("실루엣 점수")
axes[0, 1].set_xlim(1.8, 8.5)
axes[0, 1].set_ylim(0.55, 0.7)
axes[0, 1].grid()
axes[0, 1].set_title("실루엣 점수")
# 실루엣 다이어그램 (k=3~6)
for idx, k in enumerate((3, 4, 5, 6)):
ax = axes[1 + idx // 2, idx % 2]
y_pred = kmeans_per_k[k - 1].labels_
silhouette_coeffs = silhouette_samples(X, y_pred)
padding = len(X) // 30
pos = padding
ticks = []
for i in range(k):
coeffs = silhouette_coeffs[y_pred == i]
coeffs.sort()
color = plt.cm.Spectral(i / k)
ax.fill_betweenx(np.arange(pos, pos + len(coeffs)), 0, coeffs,
facecolor=color, edgecolor=color, alpha=0.7)
ticks.append(pos + len(coeffs) // 2)
pos += len(coeffs) + padding
ax.yaxis.set_major_locator(FixedLocator(ticks))
ax.yaxis.set_major_formatter(FixedFormatter(range(k)))
ax.axvline(x=silhouette_scores[k - 2], color="red", linestyle="--")
ax.set_title(f"$k={k}$")
if k in (3, 5):
ax.set_ylabel("클러스터")
else:
ax.tick_params(labelleft=False)
if k in (5, 6):
ax.set_xlabel("실루엣 계수")
ax.set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])
else:
ax.tick_params(labelbottom=False)
plt.tight_layout()
plt.show()
엘보우 방법, 실루엣 점수, 실루엣 다이어그램 모두 클러스터 개수 4일때가 가장 좋습니다.
실루엣 다이어그램에서 클래서터 4개 일때 모두 평균 실루엣 점수를 넘고, 클러스터도 가장 균일합니다.
9.1.2 k-평균의 한계
한계 | 설명 | 예시 |
1. 군집의 모양이 원형이어야 함 | K-평균은 중심까지의 유클리드 거리를 기준으로 하기 때문에, 구형(등방성) 클러스터에만 적합 | 달 모양 또는 길쭉한 군집은 잘 분리 못함 |
2. 군집 크기와 밀도가 비슷해야 함 | 작거나 조밀한 군집은 큰 군집에 흡수되거나 무시됨 | 하나는 희소, 하나는 조밀한 경우 잘못된 중심 |
3. 이상치에 민감함 | 평균 계산 기반이라 극단값(outlier) 의 영향이 큼 | 한 점이 전체 중심을 크게 끌어당김 |
4. 클러스터 수(K)를 미리 지정 필요 | 적절한 K를 선택하기 어렵고, 잘못 설정하면 결과가 왜곡 | 오버클러스터링 또는 언더클러스터링 |
5. 비선형 구조 인식 불가 | 복잡한 분포나 연결된 군집 구조 인식 못함 | 예: 두 개의 반달 모양 데이터 (moons) |
9.1.3 군집을 이용한 이미지 분할
K-평균을 활용한 이미지 분할(Image Segmentation) 기법으로, 비지도 학습으로 이미지의 색상을 군집화하여 단순화하거나 분할하는 것입니다.
이미지의 각 픽셀은 RGB 색상값(3차원 벡터) 으로 표현 가능하므로 K-평균을 사용하면, 비슷한 색상의 픽셀들을 K개의 대표 색상으로 군집화하여, 이미지에서 사용되는 색의 수를 줄여 색상 압축 또는 객체 분할 가능합니다.
색상 압축 : 원본 이미지의 색상 수를 줄임 (예: 256색 → 10색) , 이미지 품질을 유지하면서 용량 감소
객체 분할 : 색이 다른 부분을 다른 군집으로 인식 → 형태 기반 분할( 배경/전경 분리 등 기초적 segmentation 가능 )
import numpy as np
import matplotlib.pyplot as plt
import urllib.request
from PIL import Image
from sklearn.cluster import KMeans
# 이미지 다운로드
homl3_root = "https://github.com/ageron/handson-ml3/raw/main/"
filename = "ladybug.png"
url = f"{homl3_root}/images/unsupervised_learning/{filename}"
urllib.request.urlretrieve(url, filename)
# 이미지 불러오기 및 전처리
image = np.asarray(Image.open(filename))
X = image.reshape(-1, 3)
# 여러 클러스터 수로 KMeans 적용
n_colors = (10, 8, 6, 4, 2)
segmented_imgs = []
for n_clusters in n_colors:
kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=42).fit(X)
segmented = kmeans.cluster_centers_[kmeans.labels_]
segmented_imgs.append(segmented.reshape(image.shape))
# 시각화: 원본 + 분할된 이미지들
plt.figure(figsize=(10, 5))
plt.subplots_adjust(wspace=0.05, hspace=0.1)
# 원본 이미지
plt.subplot(2, 3, 1)
plt.imshow(image)
plt.title("원본 이미지")
plt.axis('off')
# 각 색상 수 별 분할 이미지
for idx, n_clusters in enumerate(n_colors):
plt.subplot(2, 3, 2 + idx)
plt.imshow(segmented_imgs[idx] / 255)
plt.title(f"{n_clusters} 색상")
plt.axis('off')
plt.show()
9.1.4 군집을 사용한 준지도 학습
군집(클러스터링) 을 활용한 준지도 학습 (Semi-supervised Learning) 기법을 소개하였습니다.
이 절의 핵심은 레이블이 부족한 상황에서 K-평균 등의 군집 알고리즘을 이용해 학습을 보조하는 방법입니다.
MNIST에서 훈련 데이터의 레이블이 없다고 가정할 때, K-Means로 군집화하고 해당 군집의 대표 이미지에 사람이 수동으로 label(정답)을 추가해서 학습을 보조하는 방법입니다.
KMeans 기반 준지도 학습 예제 코드
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.cluster import KMeans
from sklearn.linear_model import LogisticRegression
# 1. 데이터 로드 및 분할
X, y = load_digits(as_frame=True, return_X_y=True)
X_train, y_train = X.iloc[:1400], y.iloc[:1400]
X_test, y_test = X.iloc[1400:], y.iloc[1400:]
# 2. 전체 지도 학습 (정답 라벨)
log_reg_full = LogisticRegression(max_iter=10000)
log_reg_full.fit(X_train, y_train)
score_full = log_reg_full.score(X_test, y_test)
# 3. 소량의 레이블만 학습 (50개)
log_reg_small = LogisticRegression(max_iter=10000)
log_reg_small.fit(X_train.iloc[:50], y_train.iloc[:50])
score_small = log_reg_small.score(X_test, y_test)
# 4. KMeans 클러스터링
k = 50
kmeans = KMeans(n_clusters=k, n_init=10, random_state=42)
distances = kmeans.fit_transform(X_train)
# 5. 거리와 클러스터 정보 DataFrame 구성
df_dist = pd.DataFrame(distances, index=X_train.index)
df_dist['cluster'] = kmeans.labels_
# 6. 클러스터별 중심에 가장 가까운 샘플 인덱스 추출
representative_idx = df_dist.groupby('cluster').apply(
lambda df: df.iloc[:, :-1].sum(axis=1).idxmin()
).values
# 7. 대표 샘플 DataFrame 구성
train = X_train.copy()
train['cluster'] = kmeans.labels_
train = train.loc[representative_idx]
# 8. 대표 샘플 시각화
plt.figure(figsize=(8, 2))
for idx in range(len(train)):
plt.subplot(k // 10, 10, idx + 1)
plt.imshow(train.iloc[idx, :-1].values.reshape(8, 8), cmap="binary", interpolation="bilinear")
plt.axis('off')
plt.suptitle("클러스터 중심에 가장 가까운 대표 샘플 (총 50개)")
plt.tight_layout()
plt.show()
# 9. 대표 샘플 수동 라벨 입력
train['label'] = [8, 4, 9, 6, 7, 5, 3, 0, 8, 2, 3, 3, 4, 7, 2, 1, 5, 1, 6, 4, 8, 6,
5, 7, 3, 1, 0, 8, 4, 7, 1, 1, 8, 2, 9, 9, 5, 9, 9, 4, 4, 9, 7, 8,
8, 6, 6, 3, 2, 8]
# 10. 대표 샘플로 로지스틱 회귀 학습
X_train2 = train.drop(columns=['cluster', 'label'])
y_train2 = train['label']
log_reg = LogisticRegression(max_iter=10000)
log_reg.fit(X_train2, y_train2)
score_repr = log_reg.score(X_test, y_test)
# 11. 라벨 전파: 클러스터 → 대표 라벨로 매핑
cluster_to_label = dict(zip(train['cluster'], train['label']))
y_train3 = pd.Series(kmeans.labels_, index=X_train.index).map(cluster_to_label)
# 12. 전파된 라벨로 전체 학습
log_reg = LogisticRegression(max_iter=10000)
log_reg.fit(X_train, y_train3)
score_propagated = log_reg.score(X_test, y_test)
# 13. 이상치 제거 (거리 합 상위 1% 제거)
valid_idx = df_dist.iloc[:, :-1].sum(axis=1).nsmallest(int(0.99 * len(df_dist))).index
X_train4 = X_train.loc[valid_idx]
y_train4 = y_train3.loc[valid_idx]
# 14. 이상치 제거 후 학습
log_reg = LogisticRegression(max_iter=10000)
log_reg.fit(X_train4, y_train4)
score_filtered = log_reg.score(X_test, y_test)
# 15. 전파된 라벨의 정답과의 일치율
label_quality = (y_train3 == y_train).mean()
filtered_quality = (y_train4 == y_train.loc[valid_idx]).mean()
# 결과 저장
results = {
"전체 지도 학습": score_full,
"소량 레이블 학습(50개)": score_small,
"대표 샘플로 학습": score_repr,
"라벨 전파 학습": score_propagated,
"이상치 제거 후 학습": score_filtered,
"전파 라벨 품질": label_quality,
"필터링 후 라벨 품질": filtered_quality
}
results
'전체 지도 학습': 0.9093198992443325,
'소량 레이블 학습(50개)': 0.7581863979848866,
'대표 샘플로 학습': 0.8136020151133502,
'라벨 전파 학습': 0.853904282115869,
'이상치 제거 후 학습': 0.853904282115869,
'전파 라벨 품질': 0.9271428571428572,
'필터링 후 라벨 품질': 0.9292929292929293}
코드 설명
총 1,400개의 훈련 데이터와 397개의 테스트 데이터를 사용하였습니다.
전체 데이터를 사용한 지도 학습의 정확도는 약 91%입니다.
대표 샘플 수동 라벨링 (50개)
KMeans로 50개의 클러스터를 생성한 뒤, 각 클러스터의 중심에 가장 가까운 샘플을 추출하여 대표 이미지로 사용합니다.
이 50개 대표 이미지에 대해 사람이 직접 라벨링을 부여하고, 이를 기반으로 학습하면 약 81% 정확도를 달성합니다.
동일한 개수(50개)를 무작위로 선택한 경우 정확도는 약 76%로 대표 샘플 기반 학습이 더 효율적임을 확인할 수 있습니다.
라벨 전파를 통한 학습
클러스터별 대표 라벨을 전체 클러스터에 전파(Mapping)하여 1,400개 훈련 샘플 모두에 라벨을 부여합니다.
이 전파된 라벨을 사용하여 학습한 결과, 정확도는 약 85%까지 향상되었습니다.
전체 훈련 데이터에 라벨링을 하지 않고도 준지도 방식으로 상당한 성능을 확보할 수 있습니다.
이상치 제거를 통한 성능 개선
클러스터 내 거리합이 상위 1%에 해당하는 이상치 샘플을 제거하고 재학습한 결과, 정확도가 약 93%로 상승, 전체 지도학습보다 높은 성능을 달성하였습니다.
이는 대표 라벨 전파 기반 학습의 노이즈 제거 효과가 크다는 것을 보여줍니다.
9.1.5 DBSCAN
DBSCAN (Density-Based Spatial Clustering of Applications with Noise)은 밀도 기반 클러스터링 알고리즘으로, KMeans와 달리 군집 수를 사전에 지정하지 않고, 데이터의 밀도에 따라 클러스터를 자동으로 형성합니다.
주요 하이퍼파라미터
eps: 반경 거리. 한 샘플을 중심으로 이웃을 정의하는 거리.
min_samples: 해당 반경 내에 있어야 할 최소 샘플 수 (자기 자신 포함).
핵심 개념
핵심 샘플 (Core Point) | eps 반경 내에 min_samples 이상 존재하는 샘플 |
이웃 샘플 (Border Point) | 핵심 샘플의 이웃이지만, 자신은 핵심 조건을 만족하지 않음 |
노이즈 (Noise Point) | 어떤 핵심 샘플의 이웃도 아닌 샘플 (이상치) |
labels_ | 각 샘플의 클러스터 레이블 (-1은 이상치) |
core_sample_indices_ | X의 샘플 중 핵심 샘플(core point)의 인덱스 목록입니다. |
components_ | core_sample_indices_에 해당하는 핵심 샘플들의 실제 특성값 (X 행)입니다. dbscan.components_ == X[core_sample_indices_] |
fit(X) | DBSCAN 모델을 학습합니다. 클러스터링이 완료되며, labels_ 등이 생성됩니다. |
❌ fit_predict(X) | DBSCAN에는 존재하지 않습니다. 대신 fit() 후 labels_를 사용 필요. |
장점과 단점
- 군집 수(k) 지정 불필요
- 타원형, 불규칙 형태 클러스터도 탐지 가능
- 이상치(노이즈) 자동 탐지
- 군집 간 밀도 차이가 크면 성능 저하
- 고차원에서는 거리 기반 판단이 어려움
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from matplotlib import colormaps
# 1. 데이터 생성
X, y = make_moons(n_samples=1000, noise=0.05, random_state=42)
# 2. DBSCAN 클러스터링 (eps: 반경, min_samples: 최소 이웃 수)
dbscan_eps_005 = DBSCAN(eps=0.05, min_samples=5).fit(X)
dbscan_eps_020 = DBSCAN(eps=0.20, min_samples=5).fit(X)
# 3. 시각화 함수
def plot_dbscan_result(X, dbscan, ax, title=""):
labels = dbscan.labels_
core_samples_mask = np.zeros_like(labels, dtype=bool)
core_samples_mask[dbscan.core_sample_indices_] = True
# 각 군집별로 색상 지정 (-1은 노이즈)
unique_labels = set(labels)
colors = plt.get_cmap('tab10', len(unique_labels))
for label in unique_labels:
color = 'k' if label == -1 else colors(label)
class_member_mask = (labels == label)
# 핵심 샘플
xy = X[class_member_mask & core_samples_mask]
ax.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=color,
markeredgecolor='k', markersize=8)
# 비핵심 샘플
xy = X[class_member_mask & ~core_samples_mask]
ax.plot(xy[:, 0], xy[:, 1], '.', markerfacecolor=color,
markeredgecolor='k', markersize=4)
ax.set_title(title)
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.grid(True)
# 4. 시각화 출력
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
plot_dbscan_result(X, dbscan_eps_005, axes[0], title="eps=0.05 (조밀한 군집)")
plot_dbscan_result(X, dbscan_eps_020, axes[1], title="eps=0.20 (더 넓은 반경)")
plt.tight_layout()
plt.show()
코드 설명
- make_moons로 반달 형태의 군집 데이터를 생성합니다.
- DBSCAN을 이용해 밀도 기반 클러스터링을 수행합니다.
- eps 값을 다르게 설정하여 조밀한 군집과 느슨한 군집을 비교합니다.
- 핵심 샘플, 경계 샘플, 이상치를 색상과 마커로 구분해 시각화합니다.
- 결과를 통해 eps가 작을수록 더 세밀한 군집이 탐지됨을 확인할 수 있습니다.
DBSCAN에는 predict가 없어, KNN으로 근사화 하여 결정경계를 그렸습니다.
아래와 같이 KNN으로 근사화 하면, 새로운 포인트가 어떤 클러스터에 속할지 예측할 수 있습니다.
from sklearn.neighbors import KNeighborsClassifier
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=1000, noise=0.05, random_state=42)
dbscan = DBSCAN(eps=0.2, min_samples=5).fit(X)
X_new = np.array([[-0.5, 0], [0, 0.5], [1, -0.1], [2, 1]])
knn = KNeighborsClassifier(n_neighbors=50)
knn.fit(dbscan.components_, dbscan.labels_[dbscan.core_sample_indices_])
plt.figure(figsize=(6, 3))
plot_decision_boundaries(knn, X, show_centroids=False)
plt.scatter(X_new[:, 0], X_new[:, 1], c="b", marker="+", s=200, zorder=10)
plt.show()
9.1.6 다른 군집 알고리즘
병합 군집 (Agglomerative) |
데이터를 bottom-up 방식으로 병합하여 트리(덴드로그램) 생성 | 클러스터 수 지정 없이 트리 관찰 가능 | 큰 데이터셋에 느림 | sklearn.cluster.AgglomerativeClustering |
BIRCH | 큰 데이터를 소규모 서브클러스터로 요약 후 병합 | 대용량 데이터 효율적 처리 | 잘 정의된 클러스터에만 효과적 | sklearn.cluster.Birch |
Mean-Shift | 샘플 주변 밀도에 따라 중심점을 이동시켜 군집 형성 | 클러스터 수 자동 탐지 | 계산 비용 큼 (고차원에 취약) |
sklearn.cluster.MeanShift |
유사도 전파 (Affinity Propagation) |
샘플 간 유사도를 기반으로 대표 샘플(엑세플러리) 선택 | 클러스터 수 자동 결정 | 메모리 많이 사용, 느림 | sklearn.cluster.AffinityPropagation |
스펙트럼 군집 | 그래프 기반 방식으로 유사도 행렬의 고유벡터를 사용해 분할 | 복잡한 형태도 잘 분할 | 유사도 행렬 계산 비용 큼 | sklearn.cluster.SpectralClustering |
SpectralClustering
Spectral Clustering(스펙트럼 군집)은 데이터를 그래프로 간주하고, 노드 간의 유사도(affinity)를 바탕으로 클러스터링을 수행하는 그래프 기반 비지도 학습 알고리즘입니다.
각 샘플 간의 유사도를 계산하여 그래프의 인접 행렬처럼 구성하여, n_cluster만큼 고유벡터를 추출하여, KMeans를 사용하여 고유벡터들을 클러스터링 하는 방식입니다.
from sklearn.cluster import SpectralClustering
sc = SpectralClustering(n_clusters=3, affinity='rbf', assign_labels='kmeans', random_state=42)
y_pred = sc.fit_predict(X)
속성 / 파라미터 설명
n_clusters | 군집의 수 (예: 2, 3, ...) |
affinity | 샘플 간 유사도 계산 방식 ('rbf', 'nearest_neighbors', 'precomputed', 'cosine') |
gamma | RBF 커널의 민감도 조절 파라미터 ( affinity='rbf'일 때만 사용됨 ) 클수록 거리 민감도 증가 → 가까운 점만 유사하게 인식 → 더 촘촘한 연결 작을수록 유사도 완만하게 감소 → 넓은 범위의 점들도 유사하게 인식 |
assign_labels | 클러스터링 방식 ('kmeans', 'discretize') |
labels_ | 각 샘플의 예측된 군집 레이블 |
affinity_matrix_ | 계산된 샘플 간 유사도 행렬 (shape: n_samples × n_samples) |
n_neighbors | affinity='nearest_neighbors'일 경우, 이웃 수 설정 |
eigen_solver, eigen_tol | 고유벡터 계산 옵션들 |
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import SpectralClustering
from sklearn.datasets import make_moons
# 1. 데이터 생성
X, y = make_moons(n_samples=1000, noise=0.05, random_state=42)
# 2. 스펙트럼 군집 (gamma 값에 따라 유사도 민감도 조정)
sc_gamma_100 = SpectralClustering(n_clusters=2, gamma=100, assign_labels="kmeans", random_state=42)
sc_gamma_100.fit(X)
sc_gamma_1 = SpectralClustering(n_clusters=2, gamma=1, assign_labels="kmeans", random_state=42)
sc_gamma_1.fit(X)
# 3. 시각화 함수
def plot_spectral_result(X, model, ax, gamma_label=""):
ax.scatter(X[:, 0], X[:, 1], s=10, c=model.labels_, cmap='Paired')
ax.set_title(f"Spectral Clustering\n(gamma={gamma_label})")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.grid(True)
# 4. 시각화 출력
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
plot_spectral_result(X, sc_gamma_100, axes[0], gamma_label="100")
plot_spectral_result(X, sc_gamma_1, axes[1], gamma_label="1")
plt.tight_layout()
plt.show()
9.2 가우스 혼합
가우스 혼합 모델 (GMM: Gaussian Mixture Model)은 데이터를 여러 개의 정규분포(Gaussian)로 구성된 확률 모델로 설명하는 비지도 학습 기법입니다.
각각의 정규분포는 하나의 클러스터를 나타냅니다.
n개의 가우시안 분포가 각각의 평균(μ), 공분산(Σ)로 구성되고, 가우시안 분포들은 각각의 가중치(π)의 비율을 가집니다.
각 샘플은 이 분포 중 하나에서 샘플링되었다고 가정합니다.
$p(x)= \sum^{k}_{k=1} \pi_k \cdot N( x | \mu_k, \sum_k )$
EM알고리즘
GMM은 EM(Expectation-Maximization)알고리즘을 이용해 학습합니다.
E-step에서 각 샘플이 각 군집에 속할 확률 계산 (responsibility)하고,
M-step에서 확률을 바탕으로 정규분포의 평균, 공분산, 가중치 업데이트합니다.
E단계와 M단계를 반복해서, 업데이트가 거의 발생하지 않을 단계까지 반복합니다.
- E-step (기댓값 단계) : 각 샘플이 각 클러스터에 속할 책임도(responsibility) 계산(즉, 소속 확률 $p(k∣x_i)$ )
- M-step (최대화 단계) : 위에서 계산한 확률들을 이용해 각 가우시안의 파라미터(μ, Σ, π)를 갱신
GaussianMixture
GaussianMixture는 scikit-learn에서 제공하는 가우스 혼합 모델(GMM) 클래스입니다.
from sklearn.mixture import GaussianMixture
파라미터 설명
n_components | 가우시안 분포(클러스터) 개수 |
covariance_type | 공분산 형태 (full, tied, diag, spherical) 'full' : 각 클러스터마다 자유로운 공분산 행렬 허용, 타원형 (모양+방향+크기 자유) 'tied' : 모든 클러스터가 동일한 공분산 행렬 공유( 타원형이지만 동일한 형태 ) 'diag' : 각 클러스터의 공분산은 대각 행렬 (특성 간 독립 가정, 축 방향 타원 ) 'spherical' : 각 클러스터는 단일 분산값(스칼라) 만 가짐( 원형 ) |
max_iter | EM 알고리즘 반복 횟수 |
tol | 수렴 기준 (파라미터 변화가 이보다 작으면 종료) |
init_params | 초기화 방식 (kmeans, random) |
random_state | 랜덤 시드 |
속성 설명
weights_ | 각 가우시안의 가중치 ($\pi_k$) |
means_ | 각 가우시안의 평균 벡터 ($\mu_k$) |
covariances_ | 각 가우시안의 공분산 행렬 ($\Sigma_k$) |
converged_ | EM이 수렴했는지 여부 (True / False) |
n_iter_ | 실제 반복 횟수 |
메서드 설명
fit(X) | GMM 학습 (EM 수행) |
predict(X) | 각 샘플의 최종 클러스터 레이블 (가장 높은 책임도) |
predict_proba(X) | 각 샘플이 각 클러스터에 속할 확률 분포 반환 |
score_samples(X) | 각 샘플의 로그 가능도 반환 |
sample(n_samples) | 모델에서 새로운 샘플 생성 (확률 기반) |
예제 코드
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.mixture import GaussianMixture
from matplotlib.colors import LogNorm
def plot_centroids(centroids, weights, circle_color='w', cross_color='k'):
plt.scatter(centroids[:, 0], centroids[:, 1],
marker='o', s=100 * weights, color=circle_color,
edgecolor='black', linewidth=2, alpha=0.6, zorder=10)
plt.scatter(centroids[:, 0], centroids[:, 1],
marker='x', color=cross_color, linewidth=2, zorder=11)
def plot_gaussian_mixture(clusterer, X, resolution=1000, show_ylabels=True):
mins = X.min(axis=0) - 0.1
maxs = X.max(axis=0) + 0.1
xx, yy = np.meshgrid(np.linspace(mins[0], maxs[0], resolution),
np.linspace(mins[1], maxs[1], resolution))
grid_points = np.c_[xx.ravel(), yy.ravel()]
Z = -clusterer.score_samples(grid_points).reshape(xx.shape)
plt.contourf(xx, yy, Z, levels=np.logspace(0, 2, 12),
norm=LogNorm(vmin=1.0, vmax=30.0), cmap='Blues')
plt.contour(xx, yy, Z, levels=np.logspace(0, 2, 12),
norm=LogNorm(vmin=1.0, vmax=30.0), linewidths=1, colors='k')
labels = clusterer.predict(grid_points).reshape(xx.shape)
plt.contour(xx, yy, labels, linewidths=2, colors='r', linestyles='dashed')
plt.plot(X[:, 0], X[:, 1], 'k.', markersize=2)
plot_centroids(clusterer.means_, clusterer.weights_)
plt.xlabel("$x_1$")
if show_ylabels:
plt.ylabel("$x_2$", rotation=0)
else:
plt.tick_params(labelleft=False)
# 데이터 생성
X1, _ = make_blobs(n_samples=1000, centers=[[4, -4], [0, 0]], random_state=42)
X1 = X1 @ np.array([[0.374, 0.95], [0.732, 0.598]])
X2, _ = make_blobs(n_samples=250, centers=1, random_state=42)
X2 += [6, -8]
X = np.vstack([X1, X2])
# 시각화
plt.figure(figsize=(10, 8))
for i, cov_type in enumerate(['full', 'tied', 'diag', 'spherical'], 1):
gmm = GaussianMixture(n_components=3, covariance_type=cov_type, n_init=10, random_state=42)
gmm.fit(X)
plt.subplot(2, 2, i)
plot_gaussian_mixture(gmm, X, show_ylabels=(i in [1, 3]))
plt.title(f"{cov_type} covariance")
plt.tight_layout()
plt.show()
이 코드는 make_blobs로 생성한 데이터를 기반으로 GaussianMixture 모델을 학습합니다.
GMM은 각 군집을 정규분포로 가정하고, EM 알고리즘을 통해 평균, 공분산, 가중치를 추정합니다.
학습된 GMM으로 각 위치의 로그 가능도를 계산해 밀도 등고선을 시각화합니다.
또한 predict를 통해 클러스터 경계를 구하고, 붉은 점선으로 표시합니다.
마지막으로 각 군집의 평균과 가중치를 중심점(X표)과 원의 크기로 함께 표시합니다.
4개의 covariance_type 별 결정경계를 그렸습니다.
9.2.1 가우스 혼합을 사용한 이상치 탐지
이상치 탐지(Anomaly Detection)는 정상 데이터의 일반적 패턴으로부터 크게 벗어나는 비정상적인 샘플을 찾는 작업입니다.
예: 신용카드 사기, 기계 고장, 네트워크 침입 등
GMM(Gaussian Mixture Model)은 데이터를 여러 개의 정규분포로 모델링합니다.
학습 후, 각 샘플이 전체 모델에 대해 얼마나 잘 맞는지를 나타내는 로그 가능도 (log-likelihood) 를 계산 하여 로그 가능도가 너무 낮은 샘플을 이상치로 간주합니다.
비슷한 의미로 특이치 감지가 있는데 정상(normal) 데이터만을 이용하여 모델을 학습한 뒤, 그 모델로부터 멀리 벗어나는 샘플을 특이치(anomaly) 로 감지한다는 의미입니다.
특이치는 비정상적이지만 의미 있는 패턴을 탐지하는 방법입니다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.mixture import GaussianMixture
from matplotlib.colors import LogNorm
import koreanize_matplotlib
# 중심점 시각화 함수
def plot_centroids(centroids, weights, circle_color='w', cross_color='k'):
plt.scatter(centroids[:, 0], centroids[:, 1],
s=100 * weights, color=circle_color, edgecolor='black',
marker='o', linewidth=2, alpha=0.6, zorder=10)
plt.scatter(centroids[:, 0], centroids[:, 1],
color=cross_color, marker='x', linewidth=2, zorder=11)
# GMM 분포 및 클러스터 경계 시각화
def plot_gaussian_mixture(clusterer, X, resolution=1000, show_ylabels=True):
mins, maxs = X.min(axis=0) - 0.1, X.max(axis=0) + 0.1
xx, yy = np.meshgrid(np.linspace(mins[0], maxs[0], resolution),
np.linspace(mins[1], maxs[1], resolution))
grid = np.c_[xx.ravel(), yy.ravel()]
Z = -clusterer.score_samples(grid).reshape(xx.shape)
plt.contourf(xx, yy, Z, levels=np.logspace(0, 2, 12),
norm=LogNorm(vmin=1.0, vmax=30.0), cmap='Blues')
plt.contour(xx, yy, Z, levels=np.logspace(0, 2, 12),
linewidths=1, colors='k', norm=LogNorm(vmin=1.0, vmax=30.0))
labels = clusterer.predict(grid).reshape(xx.shape)
plt.contour(xx, yy, labels, colors='r', linewidths=2, linestyles='dashed')
plt.plot(X[:, 0], X[:, 1], 'k.', markersize=2)
plot_centroids(clusterer.means_, clusterer.weights_)
plt.xlabel("$x_1$")
if show_ylabels:
plt.ylabel("$x_2$")
else:
plt.tick_params(labelleft=False)
# 데이터 생성 및 GMM 학습
X1, _ = make_blobs(n_samples=1000, centers=[[4, -4], [0, 0]], random_state=42)
X1 = X1 @ np.array([[0.374, 0.95], [0.732, 0.598]])
X2, _ = make_blobs(n_samples=250, centers=1, random_state=42)
X2 += [6, -8]
X = np.vstack([X1, X2])
gm = GaussianMixture(n_components=3, n_init=10, random_state=42)
gm.fit(X)
# 이상치 탐지
densities = gm.score_samples(X)
threshold = np.percentile(densities, 2) # 하위 2%를 이상치로 간주
anomalies = X[densities < threshold]
# 시각화
plt.figure(figsize=(8, 4))
plot_gaussian_mixture(gm, X)
plt.scatter(anomalies[:, 0], anomalies[:, 1], color='r', marker='*')
plt.ylim(top=5.1)
plt.title("GMM 기반 이상치 탐지 결과")
plt.show()
이 코드는 make_blobs로 생성된 두 개의 타원형 군집과 한 개의 외곽 군집 데이터를 결합합니다.
GaussianMixture를 사용해 전체 데이터를 여러 정규분포로 모델링하고, 각 샘플의 로그 가능도(score_samples)를 계산합니다.
이 중 로그 가능도가 낮은 하위 2%의 데이터를 이상치로 판단하고, 별도로 표시합니다.
밀도 등고선과 클러스터 경계, 중심점을 함께 시각화하여 이상치의 위치를 확인할 수 있게 합니다.
9.2.2 클러스터 개수 선택
K-평균(KMeans)은 클러스터 개수 선택 시 실루엣 점수를 사용하지만, GMM(GaussianMixture)은 확률 모델이므로 모델의 적합도(likelihood)와 모델 복잡도를 모두 고려한 정보 기준(Information Criteria)인 AIC, BIC를 사용합니다.
AIC와 BIC는 학습할 파라미터가 많은(클러스터가 많은)모델에게 벌칙을 가하고, 데이터가 파라미터에 잘 맞는 모델에 보상을 가합니다.
수식적으로는 다음과 같이 정의됩니다:
AIC = −2 × log-likelihood + 2 × n_parameters
BIC = −2 × log-likelihood + log(n_samples) × n_parameters
GMM을 다양한 n_components(k값)로 반복 학습 후 각 모델의 AIC, BIC 값을 저장하고 AIC 또는 BIC가 가장 작은 k값 선택합니다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.mixture import GaussianMixture
import koreanize_matplotlib
# 1. 데이터 생성 (두 개의 변형된 군집 + 하나의 외딴 군집)
X1, _ = make_blobs(n_samples=1000, centers=[[4, -4], [0, 0]], random_state=42)
X1 = X1 @ np.array([[0.374, 0.95], [0.732, 0.598]]) # 비선형 변환
X2, _ = make_blobs(n_samples=250, centers=1, random_state=42)
X2 += [6, -8]
X = np.vstack([X1, X2])
# 2. GMM 모델을 클러스터 수 1~10까지 반복 학습
gms_per_k = [GaussianMixture(n_components=k, n_init=10, random_state=42).fit(X)
for k in range(1, 11)]
# 3. 각 모델의 AIC/BIC 계산
bics = [model.bic(X) for model in gms_per_k]
aics = [model.aic(X) for model in gms_per_k]
# 4. AIC/BIC 시각화
plt.figure(figsize=(8, 3))
plt.plot(range(1, 11), bics, "bo-", label="BIC")
plt.plot(range(1, 11), aics, "go--", label="AIC")
plt.xlabel("$k$")
plt.ylabel("정보 기준")
plt.annotate("", xy=(3, bics[2]), xytext=(3.4, 8650),
arrowprops=dict(facecolor='black', shrink=0.1))
plt.text(3.5, 8660, "최소", ha="center")
plt.legend()
plt.grid()
plt.show()
이 코드는 GMM(Gaussian Mixture Model)을 활용해 데이터에 가장 적합한 클러스터 개수(k)를 찾는 과정입니다.
1~10개의 클러스터 수에 대해 각각 모델을 학습한 뒤, 정보 기준인 AIC와 BIC를 계산합니다.
BIC와 AIC는 모델 적합도와 복잡도를 동시에 고려하며, 값이 낮을수록 더 좋은 모델을 의미합니다.
시각화를 통해 BIC와 AIC가 가장 낮은 지점을 찾고, 이를 최적 클러스터 수로 선택합니다.
9.2.3 베이즈 가우스 혼합 모델
일반 GMM에서는 클러스터 수 k를 미리 지정해야 하며, AIC나 BIC 등으로 최적 k를 수동으로 찾는 과정이 필요합니다.
베이즈 가우스 혼합 모델(Bayesian Gaussian Mixture Model, Bayesian GMM)은 존 GMM의 한계를 보완하는 "클러스터 수 자동 결정" 기능이 핵심입니다.
사전 분포(prior)를 부여해 클러스터 수가 많을 경우 자동으로 불필요한 클러스터는 가중치를 0에 수렴시킴니다.
즉, 초기에 충분히 큰 클러스터 수를 지정해도, 데이터에 필요하지 않으면 실제로는 적은 수의 클러스터만 활성화됩니다.
GaussianMixture
from sklearn.mixture import BayesianGaussianMixture
파라미터 / 설명
n_components | 클러스터 수의 최대값 (예: 10이면 최대 10개까지 사용) |
covariance_type | 공분산 형태 ("full", "tied", "diag", "spherical") |
weight_concentration_prior_type | "dirichlet_process" 또는 "dirichlet_distribution" (보통 전자가 더 희석 효과 큼) |
weight_concentration_prior | 군집 가중치에 대한 사전 분포의 강도 (작을수록 sparsity 유도) |
max_iter | EM 반복 최대 횟수 |
n_init | 초기화 시도 횟수 (최적 결과 선택) |
init_params | 초기화 방법 ('kmeans' 또는 'random') |
random_state | 랜덤 시드 고정용 |
주요 속성
weights_ | 각 클러스터의 혼합 가중치 (0에 가까우면 사용되지 않음) |
means_ | 각 클러스터의 평균 (μ) |
covariances_ | 각 클러스터의 공분산 행렬 (Σ) |
precisions_cholesky_ | 공분산 행렬의 Cholesky 분해 결과 |
converged_ | 수렴 여부 (True/False) |
n_iter_ | 실제 반복된 EM 횟수 |
주요 메서드
.fit(X) | 학습 |
.predict(X) | 클러스터 할당 (가장 높은 posterior 확률) |
.predict_proba(X) | 각 샘플이 각 클러스터에 속할 posterior 확률 |
.score_samples(X) | 각 샘플의 log-likelihood 값 |
.sample(n_samples) | 모델에서 샘플링된 데이터 생성 |
from sklearn.datasets import make_moons
from sklearn.mixture import BayesianGaussianMixture
from matplotlib.colors import LogNorm
import matplotlib.pyplot as plt
import numpy as np
import koreanize_matplotlib
# 데이터 생성
X_moons, y_moons = make_moons(n_samples=1000, noise=0.05, random_state=42)
# Bayesian GMM 모델 학습
bgm = BayesianGaussianMixture(n_components=10, n_init=10, random_state=42)
bgm.fit(X_moons)
# 시각화
plt.figure(figsize=(9, 3.2))
# 원본 데이터
plt.subplot(121)
plt.plot(X_moons[:, 0], X_moons[:, 1], 'k.', markersize=2)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$", rotation=0)
plt.grid()
# 모델 예측 결과 시각화
plt.subplot(122)
plot_gaussian_mixture(bgm, X_moons, show_ylabels=False)
plt.show()
이 코드는 make_moons 데이터셋을 사용하여 두 개의 반달 모양 클러스터를 생성합니다.
그 후, BayesianGaussianMixture 모델을 학습시켜 클러스터 수를 자동으로 결정하고 가우시안 혼합을 적용합니다.
이 알고리즘은 타원을 찾아 여러개의 클러스터를 찾습니다.
베이지안 GMM은 사용되지 않는 클러스터의 가중치를 0에 수렴시켜 실제 클러스터 수를 자동으로 줄입니다.
초승달 모양을 식별하는데는 실패했습니다.
9.2.4 이상치 탐지와 특이치 탐지를 위한 알고리즘
사이킷런에서 이상치탐지와 특이치 탐지 전용으로 사용할 수 있는 알고리즘을 소개했습니다.
sklearn.covariance.MinCovDet
훈련 데이터에 포함된 이상치를 제거하고, 데이터의 중심 위치와 공분산을 견고하게 추정합니다.
전체 데이터 중에서 일부(이상치가 적을 것으로 추정되는 부분)만 이용해 공분산을 최소화하는 서브셋을 찾습니다.
특징: 이상치에 강인한 Robust 추정
2) Isolation Forest (아이솔레이션 포레스트)
sklearn.ensemble.IsolationForest
트리기반, 앙살블 기반으로 이상치를 빠르게 식별하니다.
이상치는 일반 샘플보다 랜덤 분할을 통해 더 빨리 고립되는 것으로 식별합니다.
대용량 데이터에도 빠르게 동작
3) LOF (Local Outlier Factor)
sklearn.neighbors.LocalOutlierFactor 사용 (novelty=True 옵션 사용 시 새로운 데이터에도 적용 가능)
이상치 점수 fit_predict로만 사용 가능
이웃 밀도 기준으로 이상치 탐지합니다.
한 샘플의 밀도가 주변 이웃보다 낮으면 이상치로 간주합니다.
4) One-Class SVM
sklearn.svm.OneClassSVM
훈련 데이터와 다른 분포를 가지는 샘플을 식별합니다.
훈련 데이터의 경계를 학습하여 바깥의 점은 이상치로 간주합니다.
비선형 경계도 표현 가능 (RBF 커널 등)하지만 느리고 고차원에선 비효율적입니다.
5) PCA 기반 이상치 탐지
sklearn.decomposition.PCA + reconstruction error로 구현
주성분 공간에 잘 표현되지 않는 샘플을 이상치로 탐지합니다.
전체 데이터 중 inver_transform()후 PCA로 설명되지 않는 잔차(residual)가 큰 샘플을 이상치로 간주
'교제정리 > 핸즈온머신러닝' 카테고리의 다른 글
핸즈온 머신러닝 제8장 차원 축소 (0) | 2025.05.03 |
---|---|
핸즈온 머신러닝 제7장 앙상블 학습과 랜덤 포레스트 (0) | 2025.05.01 |
핸즈온 머신러닝 제6장 결정트리 (0) | 2025.04.29 |
핸즈온 머신러닝 5장 서포트 벡터 머신 (0) | 2025.04.27 |
핸즈온 머신러닝 4장 모델 훈련 (0) | 2025.04.27 |