참고: 핵심 설명과 코드는 🔑로 표시되었으며 굳이 알아둘 필요가 없는 코드는 ✋로 표시되었다.
# Python ≥3.5 is required
import sys
assert sys.version_info >= (3, 5)
# Scikit-Learn ≥0.20 is required
import sklearn
assert sklearn.__version__ >= "0.20"
# Common imports
import numpy as np
import os
# to make this notebook's output stable across runs
np.random.seed(42)
# To plot pretty figures
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
# Where to save the figures
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "unsupervised_learning"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
os.makedirs(IMAGES_PATH, exist_ok=True)
def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
print("Saving figure", fig_id)
if tight_layout:
plt.tight_layout()
plt.savefig(path, format=fig_extension, dpi=resolution)
from sklearn.datasets import load_iris
data = load_iris()
X = data.data
y = data.target
data.target_names
array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
아래 코드는 분류와 군집화의 차이를 보여주는 그림을 그린다.
plt.figure(figsize=(9, 3.5))
# 왼편 그림: 분류
plt.subplot(121)
plt.plot(X[y==0, 2], X[y==0, 3], "yo", label="Iris setosa")
plt.plot(X[y==1, 2], X[y==1, 3], "bs", label="Iris versicolor")
plt.plot(X[y==2, 2], X[y==2, 3], "g^", label="Iris virginica")
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.legend(fontsize=12)
# 오른편 그림: 군집화
plt.subplot(122)
plt.scatter(X[:, 2], X[:, 3], c="k", marker=".")
plt.xlabel("Petal length", fontsize=14)
plt.tick_params(labelleft=False)
save_fig("classification_vs_clustering_plot")
plt.show()
Saving figure classification_vs_clustering_plot
꽃잎 길이와 너비만으로는 두 개의 군집으로만 구분이 가능해 보인다. 하지만 꽃잎의 길이와 너비와 더불어 꽃받침의 길이와 너비까지 포함한 네 개의 특성을 모두 사용하여 후반부에서 가우시안 혼합 모델을 이용하여 세 개의 군집으로 나눌 수 있다.
from sklearn.mixture import GaussianMixture
y_pred = GaussianMixture(n_components=3, random_state=42).fit(X).predict(X)
실제 타깃을 이용한 분류와 직접 비교하면 다음과 같으며, 버시컬러와 버지니카 품종의 군집이 거의 정확하게 구분된 것을 확인할 수 있다.
plt.figure(figsize=(9, 3.5))
# 왼편 그림: 실제 품종별 분류
plt.subplot(121)
plt.plot(X[y==0, 2], X[y==0, 3], "yo", label="Iris setosa")
plt.plot(X[y==1, 2], X[y==1, 3], "bs", label="Iris versicolor")
plt.plot(X[y==2, 2], X[y==2, 3], "g^", label="Iris virginica")
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.legend(fontsize=12)
# 오른편 그림: 3개의 군집
plt.subplot(122)
plt.plot(X[y_pred==2, 2], X[y_pred==2, 3], "yo", label="Cluster 1") # 2번 군집: 세토사
plt.plot(X[y_pred==0, 2], X[y_pred==0, 3], "bs", label="Cluster 2") # 0번 군집: 버시컬러
plt.plot(X[y_pred==1, 2], X[y_pred==1, 3], "g^", label="Cluster 3") # 1번 군집: 버지니카
plt.xlabel("Petal length", fontsize=14)
plt.legend(loc="upper left", fontsize=12)
plt.tick_params(labelleft=False)
save_fig("classification_vs_clustering_plot")
plt.show()
Saving figure classification_vs_clustering_plot
군집화의 정확도를 확인하기 위해 군집별로 가장 많이 포함된 품종, 즉, 품종의 최빈값(mode)을 확인한다.
아래 코드는 사이파이(scipy)의 통계 모듈에 포함되어 있는 mode()
함수를 이용하여
각 군집별 최빈값을 확인한 후에 해당 최빈값과 군집 인덱스를 연결(mapping)한다.
from scipy import stats
mapping = {}
for class_id in np.unique(y): # 품종 아이디: 0, 1, 2
mode, _ = stats.mode(y_pred[y==class_id]) # mode: 지정된 품종이 가장 많이 포함된 군집 인덱스
mapping[mode[0]] = class_id # 군집 인덱스와 품종 연결
최종 결과는 다음과 같다.
mapping
{2: 0, 0: 1, 1: 2}
mapping
을 이용하여 군집 인덱스를 품종 인덱스로 변경한 후에 군집화의 정확도를
측정하면 96.7% 가 나온다.
참고: 사용하는 사이킷런 버전에 따라 조금씩 다른 결과가 나올 수 있다.
y_pred = np.array([mapping[cluster_id] for cluster_id in y_pred])
np.sum(y_pred==y) / len(y_pred)
0.9666666666666667
먼저 2,000개의 데이터 샘플을 생성한다. 생성되는 데이터는 지정된 5개의 센터를 중심으로 지정된 표준편차를 따르는 원 모양의 데이터 군집을 이룬다. 또한 각각의 군집은 거의 동일한 크기를 갖는다.
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])
make_blobs()
함수가 앞서 설명한 방식으로 데이터를 생성한다.
참고: 각 샘플에 대한 실제 군집 인덱스가 타깃으로 함께 제공되지만 여기서는 사용하지 않는다.
from sklearn.datasets import make_blobs
X, _ = make_blobs(n_samples=2000, centers=blob_centers,
cluster_std=blob_std, random_state=7)
산점도를 그리면 다음과 같다.
def plot_clusters(X, y=None):
plt.scatter(X[:, 0], X[:, 1], c=y, s=1)
plt.xlabel("$x_1$", fontsize=14)
plt.ylabel("$x_2$", fontsize=14, rotation=0)
plt.figure(figsize=(8, 4))
plot_clusters(X)
save_fig("blobs_plot")
plt.show()
Saving figure blobs_plot
사이킷런 KMeans
모델의
fit()
메서드는 각 군집의 중심(센트로이드)을 잡은 다음에 모든 샘플에 대해 가장 가까운
센트로이드를 중심으로 하는 군집을 이루도록 한다.
다만, 센트로이드를 몇 개로 할 것인지 먼저 지정해야 하며,
여기서는 5개를 사용하여 훈련시킨다.
from sklearn.cluster import KMeans
k = 5
kmeans = KMeans(n_clusters=k, random_state=42)
kmeans.fit(X)
KMeans(n_clusters=5, random_state=42)
훈련 결과 kmeans.labels_
속성에 각 훈련 샘플에 대한 군집 인덱스가 저장된다.
참고: 앞서 붓꽃 데이터 군집화에서 살펴본 것처럼 데이터가 생성될 때 지정된 실제 군집 인덱스가 아닌 훈련 과정에서 무작위로 지정된 인덱스임에 주의해야 한다.
kmeans.labels_
array([0, 4, 1, ..., 2, 1, 4])
각 군집의 중심, 즉 5개의 센트로이드의 좌표는 다음과 같다.
kmeans.cluster_centers_
array([[-2.80037642, 1.30082566], [ 0.20876306, 2.25551336], [-2.79290307, 2.79641063], [-1.46679593, 2.28585348], [-2.80389616, 1.80117999]])
predict()
메서드를 이용하여 새로운 데이터에 대한 군집 예측도 가능하다.
X_new = np.array([[0, 2], [3, 2], [-3, 3], [-3, 2.5]])
kmeans.predict(X_new)
array([1, 1, 2, 2])
군집을 나누는 결정경계를 그리면 보로노이 다이어그램이 생성된다.
plot_data()
함수는 여기서만 사용되는 기본값이 지정된 산점도를 그린다.
# 산점도 그리기
def plot_data(X):
plt.plot(X[:, 0], X[:, 1], 'k.', markersize=2)
plot_centroids()
함수는 센트로이드를 시각화한다.
weights=None
옵션: 특정 가중치 이상의 센트로이드만 그리도록 하는 설정.
나중에 가우시안 혼합 모델을 시각화할 때 사용됨.# 센트로이드 그리기
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)
plot_decision_boundaries()
함수는 결정경계를 시각화한다.
clusterer
: 훈련된 군집화 모델 객체X
: 훈련 세트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_)
# 기타: x, y 축 레이블
if show_xlabels:
plt.xlabel("$x_1$", fontsize=14)
else:
plt.tick_params(labelbottom=False)
if show_ylabels:
plt.ylabel("$x_2$", fontsize=14, rotation=0)
else:
plt.tick_params(labelleft=False)
plt.figure(figsize=(8, 4))
plot_decision_boundaries(kmeans, X)
save_fig("voronoi_plot")
plt.show()
Saving figure voronoi_plot
경계 근처의 일부 데이터가 잘못된 군집에 포함되긴 하였지만 전반적으로 군집이 잘 형성되었다.
지금까지 살펴 보았듯이 k-평균 모델 객체의 labels_
속성은 각 샘플에 대해 가장 가까운
센트로이드를 중심으로 하는 군집의 (작위적으로 지정된) 인덱스를 저장하며,
이를 이용하여 predict()
메서드는 샘플이 속하는 군집의 인덱스를 반환한다.
이런 방식의 군집화가 하드 군집화(hard clustering)이다.
반면에 소프트 군집화(soft clustering)는 샘플과 각 군집 사이의 관계를 점수로 부여한다.
점수는 예를 들어 각 군집과 샘플사이의 거리 또는 5장에서 소개한 가우시안 방사기저 함수를 이용한
유사도 점수 등이 사용될 수 있다.
k-평균 모델 객체의 transform()
메서드는 샘플과 각 센트로이드 사이의 (유클리드) 거리를 점수로
사용한다.
아래 코드는 네 개의 새 데이터를 변환하는 것을 보여준다.
kmeans.transform(X_new)
array([[2.88633901, 0.32995317, 2.9042344 , 1.49439034, 2.81093633], [5.84236351, 2.80290755, 5.84739223, 4.4759332 , 5.80730058], [1.71086031, 3.29399768, 0.29040966, 1.69136631, 1.21475352], [1.21567622, 3.21806371, 0.36159148, 1.54808703, 0.72581411]])
✋ 앞서 설명한 대로 각 샘플에 대한 반환값은 각 센트로이드로부터의 (유클리드) 거리임을 아래와 같이 확인할 수 있다.
np.tile()
: 주어진 어레이를 타일 모양의 지정된 형식으로 복제해서 이어붙이는 함수np.linalg.norm(np.tile(X_new, (1, k)).reshape(-1, k, 2) - kmeans.cluster_centers_, axis=2)
array([[2.88633901, 0.32995317, 2.9042344 , 1.49439034, 2.81093633], [5.84236351, 2.80290755, 5.84739223, 4.4759332 , 5.80730058], [1.71086031, 3.29399768, 0.29040966, 1.69136631, 1.21475352], [1.21567622, 3.21806371, 0.36159148, 1.54808703, 0.72581411]])
k-평균 모델의 transform()
메서드는 결론적으로 기존 $n$-차원의 데이터셋을
비선형적으로 $k$-차원의 데이터셋으로 변환하는
비선형 차원축소 기법으로 사용될 수 있다.
참고: 국소적 선형 임베딩 처럼 차원축소 기법을 군집화에 활용할 수도 있다.
k-평균 알고리즘은 가장 빠르며 가장 간단한 군집화 알고리즘 중 하나이다.
KMeans
클래스¶KMeans
클래스의 기본 옵션 중 몇 가지는 다음과 같다.
init='k-means++'
: 초기화 알고리즘. 'random'
을 사용할 경우 무작위 지정.'k-means++'
: 센트로이드 간의 거리를 최대한 크게하는 알고리즘. 기본값으로 사용됨.n_init=10
: 센트로이드 반복 지정 횟수. 기본값은 10.algorithm='elkan'
: k-평균 알고리즘. 'full'
: 앞서 설명한 알고리즘'elkan'
: 개선된 알고리즘'auto'
: 기본적으로 'elkan'
선택.max_iter=300
: 초기화된 센트로이드 위치 반복 업데이트 횟수.여기서는 예시를 위해 아래 옵션을 대신 사용한다.
init="random"
n_init=1
algorithm="full"
max_iter
: 1, 2, 3kmeans_iter1 = KMeans(n_clusters=5, init="random", n_init=1,
algorithm="full", max_iter=1, random_state=0)
kmeans_iter2 = KMeans(n_clusters=5, init="random", n_init=1,
algorithm="full", max_iter=2, random_state=0)
kmeans_iter3 = KMeans(n_clusters=5, init="random", n_init=1,
algorithm="full", max_iter=3, random_state=0)
kmeans_iter1.fit(X)
kmeans_iter2.fit(X)
kmeans_iter3.fit(X)
KMeans(algorithm='full', init='random', max_iter=3, n_clusters=5, n_init=1, random_state=0)
각 단계별 결정경계와 센트로이드의 변화는 다음과 같다.
plt.figure(figsize=(10, 8))
# 맨 위 왼편
plt.subplot(321)
plot_data(X)
plot_centroids(kmeans_iter1.cluster_centers_, circle_color='r', cross_color='w')
plt.ylabel("$x_2$", fontsize=14, rotation=0)
plt.tick_params(labelbottom=False)
plt.title("Update the centroids (initially randomly)", fontsize=14)
# 맨 위 오른편
plt.subplot(322)
plot_decision_boundaries(kmeans_iter1, X, show_xlabels=False, show_ylabels=False)
plt.title("Label the instances", fontsize=14)
# 가운데 왼편
plt.subplot(323)
plot_decision_boundaries(kmeans_iter1, X, show_centroids=False, show_xlabels=False)
plot_centroids(kmeans_iter2.cluster_centers_)
# 가운데 오른편
plt.subplot(324)
plot_decision_boundaries(kmeans_iter2, X, show_xlabels=False, show_ylabels=False)
# 맨 아래 왼편
plt.subplot(325)
plot_decision_boundaries(kmeans_iter2, X, show_centroids=False)
plot_centroids(kmeans_iter3.cluster_centers_)
# 맨 아래 오른편
plt.subplot(326)
plot_decision_boundaries(kmeans_iter3, X, show_ylabels=False)
save_fig("kmeans_algorithm_plot")
plt.show()
Saving figure kmeans_algorithm_plot
초기화가 무작위로 이루어질 경우 적절하지 않은 군집화를 얻을 수 있다. 아래 코드는 두 개의 나쁜 경우를 잘 보여준다.
plot_clusterer_comparison()
함수는 두 개의 결정경계 그래프를 동시에 그려준다.
def plot_clusterer_comparison(clusterer1, clusterer2, X, title1=None, title2=None):
clusterer1.fit(X)
clusterer2.fit(X)
plt.figure(figsize=(10, 3.2))
plt.subplot(121)
plot_decision_boundaries(clusterer1, X)
if title1:
plt.title(title1, fontsize=14)
plt.subplot(122)
plot_decision_boundaries(clusterer2, X, show_ylabels=False)
if title2:
plt.title(title2, fontsize=14)
아래 두 개의 그래프는 바로 이전의 경우처럼 센트로이드 초기화를 무작위로 딱 한 번 사용한 결과를 보여준다. 두 경우 모두 적절치 않는 모델을 생성한다.
kmeans_rnd_init1 = KMeans(n_clusters=5, init="random", n_init=1,
algorithm="full", random_state=2)
kmeans_rnd_init2 = KMeans(n_clusters=5, init="random", n_init=1,
algorithm="full", random_state=5)
plot_clusterer_comparison(kmeans_rnd_init1, kmeans_rnd_init2, X,
"Solution 1", "Solution 2 (with a different random init)")
save_fig("kmeans_variability_plot")
plt.show()
Saving figure kmeans_variability_plot
비지도 학습인 k-평균 모델의 성능을 관성(inertia)을 이용하여 측정한다.
inertia_
속성에 저장됨.kmeans.inertia_
211.5985372581683
✋ 아래 코드는 관성이 각 훈련 샘플과 가장 가까운 센트로이드 사이의 거리를 모두 합한 값임을 확인해준다.
X_dist = kmeans.transform(X) # 각 샘플과 센트로이드들 사이의 거리
np.sum(X_dist[np.arange(len(X_dist)), kmeans.labels_]**2) # 팬시 인덱싱 활용
211.59853725816873
score()
메서드는 관성의 음숫값을 반환한다.
이유는 "더 높은 점수가 더 좋다"의 원칙을 따라야 하기 때문이다.
kmeans.score(X)
-211.59853725816828
무작위 초기와 문제를 해결하기 위해 k-평균 알고리즘의 초기화를 여러 번 실행한 다음에 가장 낮은
관성을 보이는 모델을 최종 모델로 선택하면 되며, 실제로 이 옵션(n_init=10
)이
앞서 설명한 대로 KMeans 모델의 기본 하이퍼파라미터 값으로 설정되어 있다.
아래에서 확인할 수 있듯이 기본 하이퍼파라미터를 사용한 kmeans
의 관성이
한 번의 센트로이드 초기화를 사용하는 모델들의 관성보다 낮다.
kmeans_rnd_init1.inertia_
219.84385402233193
kmeans_rnd_init2.inertia_
236.95563196978736
실제로 n_init=10
로 설정할 경우 앞서 살펴본 좋은 모델과 비슷한 결과를 얻는다.
kmeans_rnd_10_inits = KMeans(n_clusters=5, init="random", n_init=10,
algorithm="full", random_state=2)
kmeans_rnd_10_inits.fit(X)
KMeans(algorithm='full', init='random', n_clusters=5, random_state=2)
plt.figure(figsize=(8, 4))
plot_decision_boundaries(kmeans_rnd_10_inits, X)
plt.show()
관성 점수도 거의 최저다.
kmeans_rnd_10_inits.inertia_
211.60576489487653
센트로이드 무작위 초기화 문제의 보다 근본적인 해결책이 아서(David Arthur)와 바실비츠기(Sergei Vassilvitskii)의 2006년 논문에서 제시되었다. 기본 아이디어는 기존 센트로이드들과의 거리가 멀 수록 다음 센트로이드로 선택될 확률이 높아지도록 하는 것이다.
$c_1, \dots, c_{i-1}$이 이미 선택되었가고 가정.
아래의 확률로 새로운 센트로이드 $c_i$로 $\mathbf{x}_i$ 선택:
$$\frac{D(\mathbf{x}_i)^2}{\sum\limits_{j=1}^{m}{D(\mathbf{x}_j)}^2}$$
$D(\mathbf{x}_j)$: $\mathbf{x}_j$와 이미 선택된 $c_1, \dots, c_{i-1}$ 중에서 가장 가까운 센트로이드 사이의 거리
$$D(\mathbf{x}_j) = \min_{k<i} \| x_j - c_k \|$$
확률 계산으로 인해 초기화 비용이 좀 더 많이 들어가긴 하지만 결과적으로 초기화 횟수(n_init
)를
획기적으로 줄일 수 있는 장점이 보다 크다.
따라서 사이킷런의 KMeans
모델의 기본값으로 사용된다.
데이터를 생성할 때 사용된 군집의 실제 중심과 kmeans
모델이
찾아낸 군집의 센트로이드가 매우 비슷함을 아래 코드에서 확인할 수 있다.
주의사항: 좌표의 순서가 다름에 주의하라.
blob_centers
array([[ 0.2, 2.3], [-1.5, 2.3], [-2.8, 1.8], [-2.8, 2.8], [-2.8, 1.3]])
kmeans.cluster_centers_
array([[-2.80037642, 1.30082566], [ 0.20876306, 2.25551336], [-2.79290307, 2.79641063], [-1.46679593, 2.28585348], [-2.80389616, 1.80117999]])
init
하이퍼파라미터 활용¶센트로이드에 대한 좋은 후보값을 알 수 있다면 init
하이퍼파라미터의 값으로
센트로이드의 좌표를 지정하여 훈련하면 보다 좋은 결과를 얻게됨을 아래 코드가 잘 보여준다.
주의사항: 아래 5개의 좌표가 앞서 언급한 blob_centers
의 좌표와 유사하다.
여기서도 순서는 중요하지 않다.
good_init = np.array([[-3, 3], [-3, 2], [-3, 1], [-1, 2], [0, 2]])
kmeans = KMeans(n_clusters=5, init=good_init, n_init=1, random_state=42)
kmeans.fit(X)
kmeans.inertia_
211.62337889822365
k-평균 알고리즘은 각 훈련 샘플과 센트로이드 사이의 거리르 계산하여 가장 짧은 거리의 센트로이드를 중심으로 하는 군집에 해당 샘플을 연결한다. 하지만 엘칸(Charles Elkan)의 2003 논문이 거리 계산을 획기적으로 줄이는 개선된 알고리즘을 제시한다.
사이킷런의 KMeans
모델은 algorithm='elkan'
을 기본 옵션으로 사용한다.
그런데 엘칸 알고리즘은 희소 데이터(sparse data)에 대해서는 잘 작동하지 않아
모든 거리를 계산하는 algorithm='full'
옵션이 희소 데이터에 대해 자동으로 선택된다.
아래 코드는 두 방식의 시간차이를 보여준다. 하지만 데이터셋이 크지 않기 때문에 시간차이가 크지 않아 보인다.
%timeit -n 50 KMeans(algorithm="elkan", random_state=42).fit(X)
54 ms ± 905 µs per loop (mean ± std. dev. of 7 runs, 50 loops each)
%timeit -n 50 KMeans(algorithm="full", random_state=42).fit(X)
97.8 ms ± 908 µs per loop (mean ± std. dev. of 7 runs, 50 loops each)
사이킷런의 MiniBatchKMeans
모델은 미니배치 학습을 지원한다.
batch_size=100
: 배치 크기 지정. 기본값은 100.from sklearn.cluster import MiniBatchKMeans
minibatch_kmeans = MiniBatchKMeans(n_clusters=5, random_state=42)
minibatch_kmeans.fit(X)
MiniBatchKMeans(n_clusters=5, random_state=42)
minibatch_kmeans.inertia_
211.93186531476786
memmap
클래스 활용¶8장 주성분 분석에서 소개한 넘파이 memmap
클래스를 이용하여 MNIST 데이터셋을
대상으로 미니배치 k-평균 모델을 훈련해보자.
먼저 MNIST 데이터셋을 불러온다.
import urllib.request
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)
mnist.target = mnist.target.astype(np.int64) # 타깃의 자료형을 변환해줄 필요 있음
훈련 세트와 테스트 세트로 구분한다.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
mnist["data"], mnist["target"], random_state=42)
memmap
객체로 지정한다.
filename = "my_mnist.data"
X_mm = np.memmap(filename, dtype='float32', mode='write', shape=X_train.shape)
X_mm[:] = X_train
MiniBatchKMeans
모델을 훈련한다.
minibatch_kmeans = MiniBatchKMeans(n_clusters=10, batch_size=10, random_state=42)
minibatch_kmeans.fit(X_mm)
MiniBatchKMeans(batch_size=10, n_clusters=10, random_state=42)
partial_fit()
활용¶데이터셋이 너무 크면 memmap
클래스조차 활용하지 못할 수 있다.
이럴 때는 메모리가 아닌 다른 저장 장치로부터 필요한 만큼의 데이터 배치(묶음)를
불러오는 함수를 이용하여 수동으로 미니배치 학습을 구현해야 한다.
즉, 아래 내용을 직접 구현해야 한다.
아래 함수는 지정된 크기 만큼의 데이터를 무작위로 선택해서 전달한다.
def load_next_batch(batch_size):
return X[np.random.choice(len(X), batch_size, replace=False)]
다음 조건에 맞게 미니배치 모델을 훈련한다.
k = 5 # 센트로이드 개수
n_init = 10 # 센트로이드 초기화 횟수
n_iterations = 100 # 센트로이드 조정 횟수
batch_size = 100 # 배치 크기
init_size = 500 # k-평균++ 알고리즘의 초기화 후보에 사용될 데이터셋 크기
아래 코드는 초기화를 반복하면서 최적의 모델을 업데이트한다.
partial_fit()
메서드를 사용함에 주의해야 한다.
np.random.seed(42)
evaluate_on_last_n_iters = 10 # 센트로이드 조정 마지막 10단계 모델의 관성 누적합 저장 기준
best_kmeans = None # 최고 모델 저장
for init in range(n_init): # 초기화 반복
# 미니배티 k-평균 모델 초기화 및 partial_fit() 훈련
minibatch_kmeans = MiniBatchKMeans(n_clusters=k, init_size=init_size)
X_init = load_next_batch(init_size)
minibatch_kmeans.partial_fit(X_init)
# 센트로이드 조정 마지막 10단계 모델의 관성 누적합 저장
minibatch_kmeans.sum_inertia_ = 0
# 센트로이드 조정
for iteration in range(n_iterations):
X_batch = load_next_batch(batch_size)
minibatch_kmeans.partial_fit(X_batch)
# 누적 관성 계산
if iteration >= n_iterations - evaluate_on_last_n_iters:
minibatch_kmeans.sum_inertia_ += minibatch_kmeans.inertia_
# 최저 누적 관성 모델 업데이트
if (best_kmeans is None or
minibatch_kmeans.sum_inertia_ < best_kmeans.sum_inertia_):
best_kmeans = minibatch_kmeans
훈련된 모델의 점수는 매우 높은 편이다.
best_kmeans.score(X)
-211.70999744411446
미니배치 k-평균 알고리즘이 일반 k-평균 알고림즘 보다 훨씬 빠르다.
%timeit KMeans(n_clusters=5, random_state=42).fit(X)
27.1 ms ± 370 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit MiniBatchKMeans(n_clusters=5, random_state=42).fit(X)
12.2 ms ± 825 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
반면에 성능은 많이 떨어진다. 이 사실을 확인해야 할 군집수와 관련해서 확인해보자.
먼저 군집수를 1에서 100까지 변화시키면서 일반 k-평균 모델과 미니배치 k-평균 모델이 훈련에 필요한 시간과 훈련된 모델의 관성을 측정한다.
from timeit import timeit
times = np.empty((100, 2))
inertias = np.empty((100, 2))
for k in range(1, 101):
kmeans_ = KMeans(n_clusters=k, random_state=42)
minibatch_kmeans = MiniBatchKMeans(n_clusters=k, random_state=42)
print("\r{}/{}".format(k, 100), end="")
times[k-1, 0] = timeit("kmeans_.fit(X)", number=10, globals=globals())
times[k-1, 1] = timeit("minibatch_kmeans.fit(X)", number=10, globals=globals())
inertias[k-1, 0] = kmeans_.inertia_
inertias[k-1, 1] = minibatch_kmeans.inertia_
100/100
군집수를 늘릴 때 훈련시간과 관성의 변화를 그래프로 그리면 다음과 같다.
plt.figure(figsize=(10,4))
# 왼편 그림
plt.subplot(121)
plt.plot(range(1, 101), inertias[:, 0], "r--", label="K-Means") # 빨강 파선
plt.plot(range(1, 101), inertias[:, 1], "b.-", label="Mini-batch K-Means") # 파랑 실선
plt.xlabel("$k$", fontsize=16)
plt.title("Inertia", fontsize=14)
plt.legend(fontsize=14)
plt.axis([1, 100, 0, 100])
# 오른편 그림
plt.subplot(122)
plt.plot(range(1, 101), times[:, 0], "r--", label="K-Means") # 빨강 파선
plt.plot(range(1, 101), times[:, 1], "b.-", label="Mini-batch K-Means") # 파랑 실선
plt.xlabel("$k$", fontsize=16)
plt.title("Training time (seconds)", fontsize=14)
plt.axis([1, 100, 0, 6])
save_fig("minibatch_kmeans_vs_kmeans")
plt.show()
Saving figure minibatch_kmeans_vs_kmeans
지금까지 사용한 예제에 대해 군집수를 5보다 작거나 크게 하면 아래와 같은 일이 발생한다.
kmeans_k3 = KMeans(n_clusters=3, random_state=42) # 3개의 군집
kmeans_k8 = KMeans(n_clusters=8, random_state=42) # 8개의 군집
plot_clusterer_comparison(kmeans_k3, kmeans_k8, X, "$k=3$", "$k=8$")
save_fig("bad_n_clusters_plot")
plt.show()
Saving figure bad_n_clusters_plot
별로 좋아보이지 않는다. 그런데 관성은 군집수가 커질수록 줄어든다.
kmeans_k3.inertia_
653.2223267580945
kmeans_k8.inertia_
118.44108623570082
실제로 군집수가 많아질 수록 관성은 줄어든다. 이유는 센트로이드 수가 늘어날 수록 각 샘플과 센트로이드 사이의 거리는 줄어들 수밖에 없기 때문이다. 아래 코드가 이 사실을 잘 보여준다.
kmeans_per_k = [KMeans(n_clusters=k, random_state=42).fit(X)
for k in range(1, 10)]
inertias = [model.inertia_ for model in kmeans_per_k]
plt.figure(figsize=(8, 3.5))
# 군집수와 관성 관계
plt.plot(range(1, 10), inertias, "bo-")
plt.xlabel("$k$", fontsize=14)
plt.ylabel("Inertia", fontsize=14)
# 주석 작성: Elbow 단어와 화살표 표시
plt.annotate('Elbow',
xy=(4, inertias[3]),
xytext=(0.55, 0.55),
textcoords='figure fraction',
fontsize=16,
arrowprops=dict(facecolor='black', shrink=0.1)
)
plt.axis([1, 8.5, 0, 1300])
save_fig("inertia_vs_k_plot")
plt.show()
Saving figure inertia_vs_k_plot
팔꿈치(elbow)에 해당하는 위치인 $k=4$, 즉, 네 개의 군집이 좋아 보인다. 군집이 네 개보다 작으면 별로이고, 4개보다 많아도 별로 좋아지지 않아 보인다. 하지만 아래 그림에서 볼 수 있듯이 왼쪽 하단 두 개의 군집이 하나의 군집으로 처리되기 때문이다. 그럼에도 불구하고 꽤 좋은 군집화 모델임엔 틀림없다.
plot_decision_boundaries(kmeans_per_k[4-1], X)
plt.show()
실루엣 점수(silhouette score)는 각 훈련 샘플에 대한 실루엣 계수(silhouette coefficient)의 평균값이다. 실루엣 계수는 아래와 같이 계산된다.
$$\frac{b - a}{\max(a, b)}$$실루엣 계수는 -1과 1 사이의 값이며, 의미는 다음과 같다.
아래 코드는 군집수가 증가할 때 실루엣 점수의 변화를 보여준다.
from sklearn.metrics import silhouette_score
silhouette_scores = [silhouette_score(X, model.labels_)
for model in kmeans_per_k[1:]]
plt.figure(figsize=(8, 3))
plt.plot(range(2, 10), silhouette_scores, "bo-")
plt.xlabel("$k$", fontsize=14)
plt.ylabel("Silhouette score", fontsize=14)
plt.axis([1.8, 8.5, 0.55, 0.7])
save_fig("silhouette_score_vs_k_plot")
plt.show()
Saving figure silhouette_score_vs_k_plot
$k=4$가 여전히 매우 좋아 보인다. 하지만 관성의 경우와는 달리 $k=5$도 역시 꽤 좋다는 것을 알 수 있다.
군집별로 각 샘플의 실루엣 계수를 오름차순으로 정렬한 그래프인 실루엣 다이어그램(silhouette diagram)이 보다 많은 정보를 전달한다.
from sklearn.metrics import silhouette_samples
from matplotlib.ticker import FixedLocator, FixedFormatter
plt.figure(figsize=(11, 9))
for k in (3, 4, 5, 6):
plt.subplot(2, 2, k - 2)
y_pred = kmeans_per_k[k - 1].labels_
silhouette_coefficients = silhouette_samples(X, y_pred)
padding = len(X) // 30
pos = padding
ticks = []
for i in range(k):
coeffs = silhouette_coefficients[y_pred == i]
coeffs.sort()
color = mpl.cm.Spectral(i / k)
plt.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
plt.gca().yaxis.set_major_locator(FixedLocator(ticks))
plt.gca().yaxis.set_major_formatter(FixedFormatter(range(k)))
if k in (3, 5):
plt.ylabel("Cluster")
if k in (5, 6):
plt.gca().set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])
plt.xlabel("Silhouette Coefficient")
else:
plt.tick_params(labelbottom=False)
plt.axvline(x=silhouette_scores[k - 2], color="red", linestyle="--")
plt.title("$k={}$".format(k), fontsize=16)
save_fig("silhouette_analysis_plot")
plt.show()
Saving figure silhouette_analysis_plot
$k=5$인 경우가 가장 좋아 보인다. 이유는 모든 군집이 거의 비슷한 크기이고, 모든 군집의 칼날이 실루엣 점수(빨강 파선)넘어서고 있기 때문이다.
k-평균의 가장 큰 단점은 최적의 군집수를 확인하기 위해 알고리즘을 여러 번 실행해야 한다는 점이다. 또한 군집의 크기와 밀도가 서로 다른거나 원형이 아닌 경우 k-평균 모델이 제대로 작동하지 않을 수 있다.
아래 코드는 타원 모양의 군집으로 이루어진 데이터셋을 생성한다.
X1, y1 = make_blobs(n_samples=1000, centers=((4, -4), (0, 0)), random_state=42)
X1 = X1.dot(np.array([[0.374, 0.95], [0.732, 0.598]]))
X2, y2 = make_blobs(n_samples=250, centers=1, random_state=42)
X2 = X2 + [6, -8]
X = np.r_[X1, X2]
y = np.r_[y1, y2]
산점도를 그리면 아래와 같다.
plot_clusters(X)
먼저 알고 있는 센트로이드 정보를 이용하여 좋은 k-평균 모델을 훈련한다.
kmeans_good = KMeans(n_clusters=3, init=np.array([[-1.5, 2.5], [0.5, 0], [4, 0]]), n_init=1, random_state=42)
kmeans_good.fit(X)
KMeans(init=array([[-1.5, 2.5], [ 0.5, 0. ], [ 4. , 0. ]]), n_clusters=3, n_init=1, random_state=42)
이번엔 센트로이드를 무작위로 지정한다.
kmeans_bad = KMeans(n_clusters=3, random_state=42)
kmeans_bad.fit(X)
KMeans(n_clusters=3, random_state=42)
두 모델의 훈련 결과는 다음과 같다. 오른편 모델은 형편없다. 반면에 왼편 모델은 보다 좋지만 그래도 25%정도의 데이터가 오른쪽 군집에 잘못 할당되었다.
plt.figure(figsize=(10, 3.2))
plt.subplot(121)
plot_decision_boundaries(kmeans_good, X)
plt.title("Inertia = {:.1f}".format(kmeans_good.inertia_), fontsize=14)
plt.subplot(122)
plot_decision_boundaries(kmeans_bad, X, show_ylabels=False)
plt.title("Inertia = {:.1f}".format(kmeans_bad.inertia_), fontsize=14)
save_fig("bad_kmeans_plot")
plt.show()
Saving figure bad_kmeans_plot
특성 스케일링을 하면 군집 구분이 보다 명확해지고 보다 원형에 가까운 군집이 생성되어 k-평균 모델의 성능이 보다 좋아질 수 있다.
이미지 색상 분할은 유사한 색상을 동일한 군집에 연결하는 기법을 의미한다. 색상 분할 과정을 살펴보기 위해 무당벌레 이미지를 하나 다운로드한다.
# 무당벌레 이미지 다운로드
images_path = os.path.join(PROJECT_ROOT_DIR, "images", "unsupervised_learning")
os.makedirs(images_path, exist_ok=True)
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
filename = "ladybug.png"
print("Downloading", filename)
url = DOWNLOAD_ROOT + "images/unsupervised_learning/" + filename
urllib.request.urlretrieve(url, os.path.join(images_path, filename))
Downloading ladybug.png
('.\\images\\unsupervised_learning\\ladybug.png', <http.client.HTTPMessage at 0x2bfbf3f1370>)
다운로드된 이미지는 533 x 800
픽셀 크기의 칼라 사진이다.
from matplotlib.image import imread
image = imread(os.path.join(images_path, filename))
image.shape
(533, 800, 3)
k-평균 모델 훈련을 위해 이미지 픽셀을 일차원으로 변환한다.
즉, (533, 800, 3)
모양의 어레이를 (426400, 3)
모양의 어레이로 변환한다.
참고: 533 * 800 = 426,400
X = image.reshape(-1, 3)
X.shape
(426400, 3)
아래 코드는 8개의 군집을 이용한 색상 분할과정을 보여준다. 먼저, k-평균 모델을 설정한다.
n_clusters=8
: 8개의 군집 사용kmeans = KMeans(n_clusters=8, random_state=42).fit(X)
찾아낸 8개의 센트로이드는 다음과 같다.
kmeans.cluster_centers_
array([[0.6125396 , 0.38346282, 0.09190764], [0.0210796 , 0.10577151, 0.00556033], [0.9840446 , 0.9394471 , 0.02596566], [0.20604077, 0.37261233, 0.05183628], [0.60494506, 0.63328826, 0.3915232 ], [0.89790714, 0.7377076 , 0.03252882], [0.3422731 , 0.52337915, 0.15179527], [0.09099391, 0.2427601 , 0.01513617]], dtype=float32)
각 훈련 샘플에 대한 레이블(kmeans.labels_
)을 이용하여 군집별 센트로이드의 색상으로 통일시킨다.
이를 위해 넘파이 어레이에 대한 팬시 인덱싱을 활용한다.
segmented_img = kmeans.cluster_centers_[kmeans.labels_]
이미지를 원래의 크기로 다시 변환한다.
segmented_img = segmented_img.reshape(image.shape)
아래 코드는 앞서 설명한 방식을 다양한 군집수에 대해 적용한 결과를 비교할 수 있는 그림을 보여준다.
segmented_imgs = []
n_colors = (10, 8, 6, 4, 2)
for n_clusters in n_colors:
kmeans = KMeans(n_clusters=n_clusters, random_state=42).fit(X)
segmented_img = kmeans.cluster_centers_[kmeans.labels_]
segmented_imgs.append(segmented_img.reshape(image.shape))
plt.figure(figsize=(10,5))
plt.subplots_adjust(wspace=0.05, hspace=0.1)
# 원본 이미지
plt.subplot(231)
plt.imshow(image)
plt.title("Original image")
plt.axis('off')
# 색상 분할된 이미지 5개
for idx, n_clusters in enumerate(n_colors):
plt.subplot(232 + idx)
plt.imshow(segmented_imgs[idx])
plt.title("{} colors".format(n_clusters))
plt.axis('off')
save_fig('image_segmentation_diagram', tight_layout=False)
plt.show()
Saving figure image_segmentation_diagram
훈련된 k-평균 객체의 transform()
메서드는 주어진 데이터에 대해 각 센트로이드부터의 거리로 이루어진
어레이를 생성한다.
즉, n
차원의 데이터셋을 k
차원의 데이터셋으로 변환한다.
만약에 k < n
이라면 이는 비선형 차원축소로 간주될 수 있으며 이어지는 지도학습에
유용하게 활용될 수 있다.
여기서는 MNIST 손글씨 데이터셋을 이용하여 군집화를 차원축소 기법으로 활용하는 방식을 소개한다.
다만, 여기서 사용되는 데이터셋은 8 x 8
크기의 픽셀을 갖는 1,797개의 축소된 MNIST 데이터셋을 사용한다.
from sklearn.datasets import load_digits
X_digits, y_digits = load_digits(return_X_y=True)
먼저 훈련 세트와 테스트 세트로 구분한다.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_digits, y_digits, random_state=42)
로지스틱 회귀 모델을 훈련한 다음에 테스트 세트에 대한 성능을 평가하면, 정확도 평균값이 96.8888% 정도로 나온다.
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train, y_train)
LogisticRegression(max_iter=5000, multi_class='ovr', random_state=42)
log_reg.score(X_test, y_test)
0.9688888888888889
아래 파이프라인은 50개의 군집으로 먼저 군집화를 한 다음에 로지스틱 회귀 모델을 훈련한다.
주의사항: 파이파라인으로 구성된 예측기의 fit()
메서드를 호출하면
최종 예측기를 제외한 나머지 예측기의 fit_transform()
메서드가 호출된다.
또한 k-평균 모델의 transform()
메서드는 앞서 설명한 방식으로 차원축소를 진행한다.
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
("kmeans", KMeans(n_clusters=50, random_state=42)),
("log_reg", LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)),
])
pipeline.fit(X_train, y_train)
Pipeline(steps=[('kmeans', KMeans(n_clusters=50, random_state=42)), ('log_reg', LogisticRegression(max_iter=5000, multi_class='ovr', random_state=42))])
파이프라인 모델의 정확도 평균값은 97.7%로 28%의 성능향상이 발생한다.
pipeline.score(X_test, y_test)
0.98
1 - (1 - 0.977777) / (1 - 0.968888)
0.28570969400874346
그리드 탐색을 이용하여 최적의 군집수를 알아낼 수 있다. 즉, 파이프라인의 성능이 최고가 되도록 하는 군집수는 다음과 같다.
아래 코드는 그리드 참색을 이용하여 2개부터 100개 사이의 군집을 사용할 때 이어지는 로지스틱 회귀의 성능을 평가하여 최적의 군집수를 알아낸다.
경고: 사용하는 컴퓨터 성능에 따라 아래 코드는 20분 이상 걸릴 수 있다.
from sklearn.model_selection import GridSearchCV
param_grid = dict(kmeans__n_clusters=range(2, 100))
grid_clf = GridSearchCV(pipeline, param_grid, cv=3, verbose=2)
grid_clf.fit(X_train, y_train)
Fitting 3 folds for each of 98 candidates, totalling 294 fits [CV] kmeans__n_clusters=2 ............................................ [CV] ............................. kmeans__n_clusters=2, total= 0.1s [CV] kmeans__n_clusters=2 ............................................
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers. [Parallel(n_jobs=1)]: Done 1 out of 1 | elapsed: 0.0s remaining: 0.0s
[CV] ............................. kmeans__n_clusters=2, total= 0.1s [CV] kmeans__n_clusters=2 ............................................ [CV] ............................. kmeans__n_clusters=2, total= 0.1s [CV] kmeans__n_clusters=3 ............................................ [CV] ............................. kmeans__n_clusters=3, total= 0.1s [CV] kmeans__n_clusters=3 ............................................ [CV] ............................. kmeans__n_clusters=3, total= 0.1s [CV] kmeans__n_clusters=3 ............................................ [CV] ............................. kmeans__n_clusters=3, total= 0.1s [CV] kmeans__n_clusters=4 ............................................ [CV] ............................. kmeans__n_clusters=4, total= 0.1s [CV] kmeans__n_clusters=4 ............................................ [CV] ............................. kmeans__n_clusters=4, total= 0.2s [CV] kmeans__n_clusters=4 ............................................ [CV] ............................. kmeans__n_clusters=4, total= 0.2s [CV] kmeans__n_clusters=5 ............................................ [CV] ............................. kmeans__n_clusters=5, total= 0.2s [CV] kmeans__n_clusters=5 ............................................ [CV] ............................. kmeans__n_clusters=5, total= 0.2s [CV] kmeans__n_clusters=5 ............................................ [CV] ............................. kmeans__n_clusters=5, total= 0.2s [CV] kmeans__n_clusters=6 ............................................ [CV] ............................. kmeans__n_clusters=6, total= 0.2s [CV] kmeans__n_clusters=6 ............................................ [CV] ............................. kmeans__n_clusters=6, total= 0.2s [CV] kmeans__n_clusters=6 ............................................ [CV] ............................. kmeans__n_clusters=6, total= 0.2s [CV] kmeans__n_clusters=7 ............................................ [CV] ............................. kmeans__n_clusters=7, total= 0.2s [CV] kmeans__n_clusters=7 ............................................ [CV] ............................. kmeans__n_clusters=7, total= 0.2s [CV] kmeans__n_clusters=7 ............................................ [CV] ............................. kmeans__n_clusters=7, total= 0.3s [CV] kmeans__n_clusters=8 ............................................ [CV] ............................. kmeans__n_clusters=8, total= 0.3s [CV] kmeans__n_clusters=8 ............................................ [CV] ............................. kmeans__n_clusters=8, total= 0.3s [CV] kmeans__n_clusters=8 ............................................ [CV] ............................. kmeans__n_clusters=8, total= 0.3s [CV] kmeans__n_clusters=9 ............................................ [CV] ............................. kmeans__n_clusters=9, total= 0.4s [CV] kmeans__n_clusters=9 ............................................ [CV] ............................. kmeans__n_clusters=9, total= 0.3s [CV] kmeans__n_clusters=9 ............................................ [CV] ............................. kmeans__n_clusters=9, total= 0.4s [CV] kmeans__n_clusters=10 ........................................... [CV] ............................ kmeans__n_clusters=10, total= 0.5s [CV] kmeans__n_clusters=10 ........................................... [CV] ............................ kmeans__n_clusters=10, total= 0.5s [CV] kmeans__n_clusters=10 ........................................... [CV] ............................ kmeans__n_clusters=10, total= 0.5s [CV] kmeans__n_clusters=11 ........................................... [CV] ............................ kmeans__n_clusters=11, total= 0.6s [CV] kmeans__n_clusters=11 ........................................... [CV] ............................ kmeans__n_clusters=11, total= 0.6s [CV] kmeans__n_clusters=11 ........................................... [CV] ............................ kmeans__n_clusters=11, total= 0.5s [CV] kmeans__n_clusters=12 ........................................... [CV] ............................ kmeans__n_clusters=12, total= 0.7s [CV] kmeans__n_clusters=12 ........................................... [CV] ............................ kmeans__n_clusters=12, total= 0.7s [CV] kmeans__n_clusters=12 ........................................... [CV] ............................ kmeans__n_clusters=12, total= 0.8s [CV] kmeans__n_clusters=13 ........................................... [CV] ............................ kmeans__n_clusters=13, total= 0.8s [CV] kmeans__n_clusters=13 ........................................... [CV] ............................ kmeans__n_clusters=13, total= 0.8s [CV] kmeans__n_clusters=13 ........................................... [CV] ............................ kmeans__n_clusters=13, total= 0.9s [CV] kmeans__n_clusters=14 ........................................... [CV] ............................ kmeans__n_clusters=14, total= 0.9s [CV] kmeans__n_clusters=14 ........................................... [CV] ............................ kmeans__n_clusters=14, total= 1.0s [CV] kmeans__n_clusters=14 ........................................... [CV] ............................ kmeans__n_clusters=14, total= 0.9s [CV] kmeans__n_clusters=15 ........................................... [CV] ............................ kmeans__n_clusters=15, total= 1.1s [CV] kmeans__n_clusters=15 ........................................... [CV] ............................ kmeans__n_clusters=15, total= 1.0s [CV] kmeans__n_clusters=15 ........................................... [CV] ............................ kmeans__n_clusters=15, total= 1.0s [CV] kmeans__n_clusters=16 ........................................... [CV] ............................ kmeans__n_clusters=16, total= 1.2s [CV] kmeans__n_clusters=16 ........................................... [CV] ............................ kmeans__n_clusters=16, total= 1.1s [CV] kmeans__n_clusters=16 ........................................... [CV] ............................ kmeans__n_clusters=16, total= 1.1s [CV] kmeans__n_clusters=17 ........................................... [CV] ............................ kmeans__n_clusters=17, total= 1.4s [CV] kmeans__n_clusters=17 ........................................... [CV] ............................ kmeans__n_clusters=17, total= 1.2s [CV] kmeans__n_clusters=17 ........................................... [CV] ............................ kmeans__n_clusters=17, total= 1.2s [CV] kmeans__n_clusters=18 ........................................... [CV] ............................ kmeans__n_clusters=18, total= 1.4s [CV] kmeans__n_clusters=18 ........................................... [CV] ............................ kmeans__n_clusters=18, total= 1.3s [CV] kmeans__n_clusters=18 ........................................... [CV] ............................ kmeans__n_clusters=18, total= 1.4s [CV] kmeans__n_clusters=19 ........................................... [CV] ............................ kmeans__n_clusters=19, total= 1.5s [CV] kmeans__n_clusters=19 ........................................... [CV] ............................ kmeans__n_clusters=19, total= 1.3s [CV] kmeans__n_clusters=19 ........................................... [CV] ............................ kmeans__n_clusters=19, total= 1.3s [CV] kmeans__n_clusters=20 ........................................... [CV] ............................ kmeans__n_clusters=20, total= 1.6s [CV] kmeans__n_clusters=20 ........................................... [CV] ............................ kmeans__n_clusters=20, total= 1.4s [CV] kmeans__n_clusters=20 ........................................... [CV] ............................ kmeans__n_clusters=20, total= 1.5s [CV] kmeans__n_clusters=21 ........................................... [CV] ............................ kmeans__n_clusters=21, total= 1.7s [CV] kmeans__n_clusters=21 ........................................... [CV] ............................ kmeans__n_clusters=21, total= 1.7s [CV] kmeans__n_clusters=21 ........................................... [CV] ............................ kmeans__n_clusters=21, total= 1.6s [CV] kmeans__n_clusters=22 ........................................... [CV] ............................ kmeans__n_clusters=22, total= 1.6s [CV] kmeans__n_clusters=22 ........................................... [CV] ............................ kmeans__n_clusters=22, total= 1.8s [CV] kmeans__n_clusters=22 ........................................... [CV] ............................ kmeans__n_clusters=22, total= 1.7s [CV] kmeans__n_clusters=23 ........................................... [CV] ............................ kmeans__n_clusters=23, total= 1.8s [CV] kmeans__n_clusters=23 ........................................... [CV] ............................ kmeans__n_clusters=23, total= 1.9s [CV] kmeans__n_clusters=23 ........................................... [CV] ............................ kmeans__n_clusters=23, total= 1.7s [CV] kmeans__n_clusters=24 ........................................... [CV] ............................ kmeans__n_clusters=24, total= 1.9s [CV] kmeans__n_clusters=24 ........................................... [CV] ............................ kmeans__n_clusters=24, total= 1.8s [CV] kmeans__n_clusters=24 ........................................... [CV] ............................ kmeans__n_clusters=24, total= 1.9s [CV] kmeans__n_clusters=25 ........................................... [CV] ............................ kmeans__n_clusters=25, total= 1.8s [CV] kmeans__n_clusters=25 ........................................... [CV] ............................ kmeans__n_clusters=25, total= 2.0s [CV] kmeans__n_clusters=25 ........................................... [CV] ............................ kmeans__n_clusters=25, total= 1.8s [CV] kmeans__n_clusters=26 ........................................... [CV] ............................ kmeans__n_clusters=26, total= 2.1s [CV] kmeans__n_clusters=26 ........................................... [CV] ............................ kmeans__n_clusters=26, total= 2.0s [CV] kmeans__n_clusters=26 ........................................... [CV] ............................ kmeans__n_clusters=26, total= 1.9s [CV] kmeans__n_clusters=27 ........................................... [CV] ............................ kmeans__n_clusters=27, total= 2.0s [CV] kmeans__n_clusters=27 ........................................... [CV] ............................ kmeans__n_clusters=27, total= 2.1s [CV] kmeans__n_clusters=27 ........................................... [CV] ............................ kmeans__n_clusters=27, total= 2.0s [CV] kmeans__n_clusters=28 ........................................... [CV] ............................ kmeans__n_clusters=28, total= 2.2s [CV] kmeans__n_clusters=28 ........................................... [CV] ............................ kmeans__n_clusters=28, total= 2.0s [CV] kmeans__n_clusters=28 ........................................... [CV] ............................ kmeans__n_clusters=28, total= 2.2s [CV] kmeans__n_clusters=29 ........................................... [CV] ............................ kmeans__n_clusters=29, total= 2.5s [CV] kmeans__n_clusters=29 ........................................... [CV] ............................ kmeans__n_clusters=29, total= 2.3s [CV] kmeans__n_clusters=29 ........................................... [CV] ............................ kmeans__n_clusters=29, total= 2.0s [CV] kmeans__n_clusters=30 ........................................... [CV] ............................ kmeans__n_clusters=30, total= 2.1s [CV] kmeans__n_clusters=30 ........................................... [CV] ............................ kmeans__n_clusters=30, total= 2.1s [CV] kmeans__n_clusters=30 ........................................... [CV] ............................ kmeans__n_clusters=30, total= 1.9s [CV] kmeans__n_clusters=31 ........................................... [CV] ............................ kmeans__n_clusters=31, total= 2.2s [CV] kmeans__n_clusters=31 ........................................... [CV] ............................ kmeans__n_clusters=31, total= 2.2s [CV] kmeans__n_clusters=31 ........................................... [CV] ............................ kmeans__n_clusters=31, total= 2.0s [CV] kmeans__n_clusters=32 ........................................... [CV] ............................ kmeans__n_clusters=32, total= 2.5s [CV] kmeans__n_clusters=32 ........................................... [CV] ............................ kmeans__n_clusters=32, total= 2.1s [CV] kmeans__n_clusters=32 ........................................... [CV] ............................ kmeans__n_clusters=32, total= 2.2s [CV] kmeans__n_clusters=33 ........................................... [CV] ............................ kmeans__n_clusters=33, total= 2.2s [CV] kmeans__n_clusters=33 ........................................... [CV] ............................ kmeans__n_clusters=33, total= 2.3s [CV] kmeans__n_clusters=33 ........................................... [CV] ............................ kmeans__n_clusters=33, total= 2.2s [CV] kmeans__n_clusters=34 ........................................... [CV] ............................ kmeans__n_clusters=34, total= 2.4s [CV] kmeans__n_clusters=34 ........................................... [CV] ............................ kmeans__n_clusters=34, total= 2.4s [CV] kmeans__n_clusters=34 ........................................... [CV] ............................ kmeans__n_clusters=34, total= 2.2s [CV] kmeans__n_clusters=35 ........................................... [CV] ............................ kmeans__n_clusters=35, total= 2.3s [CV] kmeans__n_clusters=35 ........................................... [CV] ............................ kmeans__n_clusters=35, total= 2.4s [CV] kmeans__n_clusters=35 ........................................... [CV] ............................ kmeans__n_clusters=35, total= 2.1s [CV] kmeans__n_clusters=36 ........................................... [CV] ............................ kmeans__n_clusters=36, total= 2.3s [CV] kmeans__n_clusters=36 ........................................... [CV] ............................ kmeans__n_clusters=36, total= 2.5s [CV] kmeans__n_clusters=36 ........................................... [CV] ............................ kmeans__n_clusters=36, total= 2.3s [CV] kmeans__n_clusters=37 ........................................... [CV] ............................ kmeans__n_clusters=37, total= 2.7s [CV] kmeans__n_clusters=37 ........................................... [CV] ............................ kmeans__n_clusters=37, total= 2.3s [CV] kmeans__n_clusters=37 ........................................... [CV] ............................ kmeans__n_clusters=37, total= 2.5s [CV] kmeans__n_clusters=38 ........................................... [CV] ............................ kmeans__n_clusters=38, total= 2.2s [CV] kmeans__n_clusters=38 ........................................... [CV] ............................ kmeans__n_clusters=38, total= 2.1s [CV] kmeans__n_clusters=38 ........................................... [CV] ............................ kmeans__n_clusters=38, total= 2.3s [CV] kmeans__n_clusters=39 ........................................... [CV] ............................ kmeans__n_clusters=39, total= 2.3s [CV] kmeans__n_clusters=39 ........................................... [CV] ............................ kmeans__n_clusters=39, total= 2.5s [CV] kmeans__n_clusters=39 ........................................... [CV] ............................ kmeans__n_clusters=39, total= 2.1s [CV] kmeans__n_clusters=40 ........................................... [CV] ............................ kmeans__n_clusters=40, total= 2.2s [CV] kmeans__n_clusters=40 ........................................... [CV] ............................ kmeans__n_clusters=40, total= 2.5s [CV] kmeans__n_clusters=40 ........................................... [CV] ............................ kmeans__n_clusters=40, total= 2.4s [CV] kmeans__n_clusters=41 ........................................... [CV] ............................ kmeans__n_clusters=41, total= 2.3s [CV] kmeans__n_clusters=41 ........................................... [CV] ............................ kmeans__n_clusters=41, total= 2.3s [CV] kmeans__n_clusters=41 ........................................... [CV] ............................ kmeans__n_clusters=41, total= 2.3s [CV] kmeans__n_clusters=42 ........................................... [CV] ............................ kmeans__n_clusters=42, total= 2.3s [CV] kmeans__n_clusters=42 ........................................... [CV] ............................ kmeans__n_clusters=42, total= 2.5s [CV] kmeans__n_clusters=42 ........................................... [CV] ............................ kmeans__n_clusters=42, total= 2.4s [CV] kmeans__n_clusters=43 ........................................... [CV] ............................ kmeans__n_clusters=43, total= 2.6s [CV] kmeans__n_clusters=43 ........................................... [CV] ............................ kmeans__n_clusters=43, total= 2.7s [CV] kmeans__n_clusters=43 ........................................... [CV] ............................ kmeans__n_clusters=43, total= 2.4s [CV] kmeans__n_clusters=44 ........................................... [CV] ............................ kmeans__n_clusters=44, total= 2.8s [CV] kmeans__n_clusters=44 ........................................... [CV] ............................ kmeans__n_clusters=44, total= 2.6s [CV] kmeans__n_clusters=44 ........................................... [CV] ............................ kmeans__n_clusters=44, total= 2.4s [CV] kmeans__n_clusters=45 ........................................... [CV] ............................ kmeans__n_clusters=45, total= 2.5s [CV] kmeans__n_clusters=45 ........................................... [CV] ............................ kmeans__n_clusters=45, total= 2.6s [CV] kmeans__n_clusters=45 ........................................... [CV] ............................ kmeans__n_clusters=45, total= 2.7s [CV] kmeans__n_clusters=46 ........................................... [CV] ............................ kmeans__n_clusters=46, total= 2.4s [CV] kmeans__n_clusters=46 ........................................... [CV] ............................ kmeans__n_clusters=46, total= 2.6s [CV] kmeans__n_clusters=46 ........................................... [CV] ............................ kmeans__n_clusters=46, total= 2.6s [CV] kmeans__n_clusters=47 ........................................... [CV] ............................ kmeans__n_clusters=47, total= 2.5s [CV] kmeans__n_clusters=47 ........................................... [CV] ............................ kmeans__n_clusters=47, total= 2.4s [CV] kmeans__n_clusters=47 ........................................... [CV] ............................ kmeans__n_clusters=47, total= 2.8s [CV] kmeans__n_clusters=48 ........................................... [CV] ............................ kmeans__n_clusters=48, total= 2.4s [CV] kmeans__n_clusters=48 ........................................... [CV] ............................ kmeans__n_clusters=48, total= 2.7s [CV] kmeans__n_clusters=48 ........................................... [CV] ............................ kmeans__n_clusters=48, total= 2.7s [CV] kmeans__n_clusters=49 ........................................... [CV] ............................ kmeans__n_clusters=49, total= 2.5s [CV] kmeans__n_clusters=49 ........................................... [CV] ............................ kmeans__n_clusters=49, total= 2.5s [CV] kmeans__n_clusters=49 ........................................... [CV] ............................ kmeans__n_clusters=49, total= 2.8s [CV] kmeans__n_clusters=50 ........................................... [CV] ............................ kmeans__n_clusters=50, total= 2.6s [CV] kmeans__n_clusters=50 ........................................... [CV] ............................ kmeans__n_clusters=50, total= 2.5s [CV] kmeans__n_clusters=50 ........................................... [CV] ............................ kmeans__n_clusters=50, total= 2.6s [CV] kmeans__n_clusters=51 ........................................... [CV] ............................ kmeans__n_clusters=51, total= 2.7s [CV] kmeans__n_clusters=51 ........................................... [CV] ............................ kmeans__n_clusters=51, total= 2.6s [CV] kmeans__n_clusters=51 ........................................... [CV] ............................ kmeans__n_clusters=51, total= 2.9s [CV] kmeans__n_clusters=52 ........................................... [CV] ............................ kmeans__n_clusters=52, total= 2.5s [CV] kmeans__n_clusters=52 ........................................... [CV] ............................ kmeans__n_clusters=52, total= 2.6s [CV] kmeans__n_clusters=52 ........................................... [CV] ............................ kmeans__n_clusters=52, total= 2.7s [CV] kmeans__n_clusters=53 ........................................... [CV] ............................ kmeans__n_clusters=53, total= 2.7s [CV] kmeans__n_clusters=53 ........................................... [CV] ............................ kmeans__n_clusters=53, total= 2.6s [CV] kmeans__n_clusters=53 ........................................... [CV] ............................ kmeans__n_clusters=53, total= 2.7s [CV] kmeans__n_clusters=54 ........................................... [CV] ............................ kmeans__n_clusters=54, total= 2.5s [CV] kmeans__n_clusters=54 ........................................... [CV] ............................ kmeans__n_clusters=54, total= 2.5s [CV] kmeans__n_clusters=54 ........................................... [CV] ............................ kmeans__n_clusters=54, total= 3.0s [CV] kmeans__n_clusters=55 ........................................... [CV] ............................ kmeans__n_clusters=55, total= 2.4s [CV] kmeans__n_clusters=55 ........................................... [CV] ............................ kmeans__n_clusters=55, total= 3.0s [CV] kmeans__n_clusters=55 ........................................... [CV] ............................ kmeans__n_clusters=55, total= 2.8s [CV] kmeans__n_clusters=56 ........................................... [CV] ............................ kmeans__n_clusters=56, total= 2.8s [CV] kmeans__n_clusters=56 ........................................... [CV] ............................ kmeans__n_clusters=56, total= 2.7s [CV] kmeans__n_clusters=56 ........................................... [CV] ............................ kmeans__n_clusters=56, total= 2.7s [CV] kmeans__n_clusters=57 ........................................... [CV] ............................ kmeans__n_clusters=57, total= 2.6s [CV] kmeans__n_clusters=57 ........................................... [CV] ............................ kmeans__n_clusters=57, total= 3.2s [CV] kmeans__n_clusters=57 ........................................... [CV] ............................ kmeans__n_clusters=57, total= 2.7s [CV] kmeans__n_clusters=58 ........................................... [CV] ............................ kmeans__n_clusters=58, total= 2.8s [CV] kmeans__n_clusters=58 ........................................... [CV] ............................ kmeans__n_clusters=58, total= 2.7s [CV] kmeans__n_clusters=58 ........................................... [CV] ............................ kmeans__n_clusters=58, total= 2.7s [CV] kmeans__n_clusters=59 ........................................... [CV] ............................ kmeans__n_clusters=59, total= 2.8s [CV] kmeans__n_clusters=59 ........................................... [CV] ............................ kmeans__n_clusters=59, total= 2.8s [CV] kmeans__n_clusters=59 ........................................... [CV] ............................ kmeans__n_clusters=59, total= 2.8s [CV] kmeans__n_clusters=60 ........................................... [CV] ............................ kmeans__n_clusters=60, total= 2.9s [CV] kmeans__n_clusters=60 ........................................... [CV] ............................ kmeans__n_clusters=60, total= 3.0s [CV] kmeans__n_clusters=60 ........................................... [CV] ............................ kmeans__n_clusters=60, total= 2.9s [CV] kmeans__n_clusters=61 ........................................... [CV] ............................ kmeans__n_clusters=61, total= 3.0s [CV] kmeans__n_clusters=61 ........................................... [CV] ............................ kmeans__n_clusters=61, total= 3.0s [CV] kmeans__n_clusters=61 ........................................... [CV] ............................ kmeans__n_clusters=61, total= 2.4s [CV] kmeans__n_clusters=62 ........................................... [CV] ............................ kmeans__n_clusters=62, total= 2.9s [CV] kmeans__n_clusters=62 ........................................... [CV] ............................ kmeans__n_clusters=62, total= 3.0s [CV] kmeans__n_clusters=62 ........................................... [CV] ............................ kmeans__n_clusters=62, total= 2.7s [CV] kmeans__n_clusters=63 ........................................... [CV] ............................ kmeans__n_clusters=63, total= 3.0s [CV] kmeans__n_clusters=63 ........................................... [CV] ............................ kmeans__n_clusters=63, total= 3.0s [CV] kmeans__n_clusters=63 ........................................... [CV] ............................ kmeans__n_clusters=63, total= 2.8s [CV] kmeans__n_clusters=64 ........................................... [CV] ............................ kmeans__n_clusters=64, total= 3.3s [CV] kmeans__n_clusters=64 ........................................... [CV] ............................ kmeans__n_clusters=64, total= 3.3s [CV] kmeans__n_clusters=64 ........................................... [CV] ............................ kmeans__n_clusters=64, total= 2.7s [CV] kmeans__n_clusters=65 ........................................... [CV] ............................ kmeans__n_clusters=65, total= 2.8s [CV] kmeans__n_clusters=65 ........................................... [CV] ............................ kmeans__n_clusters=65, total= 3.0s [CV] kmeans__n_clusters=65 ........................................... [CV] ............................ kmeans__n_clusters=65, total= 2.7s [CV] kmeans__n_clusters=66 ........................................... [CV] ............................ kmeans__n_clusters=66, total= 3.2s [CV] kmeans__n_clusters=66 ........................................... [CV] ............................ kmeans__n_clusters=66, total= 2.8s [CV] kmeans__n_clusters=66 ........................................... [CV] ............................ kmeans__n_clusters=66, total= 2.6s [CV] kmeans__n_clusters=67 ........................................... [CV] ............................ kmeans__n_clusters=67, total= 2.7s [CV] kmeans__n_clusters=67 ........................................... [CV] ............................ kmeans__n_clusters=67, total= 2.7s [CV] kmeans__n_clusters=67 ........................................... [CV] ............................ kmeans__n_clusters=67, total= 2.8s [CV] kmeans__n_clusters=68 ........................................... [CV] ............................ kmeans__n_clusters=68, total= 2.9s [CV] kmeans__n_clusters=68 ........................................... [CV] ............................ kmeans__n_clusters=68, total= 2.8s [CV] kmeans__n_clusters=68 ........................................... [CV] ............................ kmeans__n_clusters=68, total= 2.8s [CV] kmeans__n_clusters=69 ........................................... [CV] ............................ kmeans__n_clusters=69, total= 2.8s [CV] kmeans__n_clusters=69 ........................................... [CV] ............................ kmeans__n_clusters=69, total= 2.8s [CV] kmeans__n_clusters=69 ........................................... [CV] ............................ kmeans__n_clusters=69, total= 3.0s [CV] kmeans__n_clusters=70 ........................................... [CV] ............................ kmeans__n_clusters=70, total= 2.6s [CV] kmeans__n_clusters=70 ........................................... [CV] ............................ kmeans__n_clusters=70, total= 2.8s [CV] kmeans__n_clusters=70 ........................................... [CV] ............................ kmeans__n_clusters=70, total= 2.9s [CV] kmeans__n_clusters=71 ........................................... [CV] ............................ kmeans__n_clusters=71, total= 2.6s [CV] kmeans__n_clusters=71 ........................................... [CV] ............................ kmeans__n_clusters=71, total= 3.3s [CV] kmeans__n_clusters=71 ........................................... [CV] ............................ kmeans__n_clusters=71, total= 2.9s [CV] kmeans__n_clusters=72 ........................................... [CV] ............................ kmeans__n_clusters=72, total= 2.6s [CV] kmeans__n_clusters=72 ........................................... [CV] ............................ kmeans__n_clusters=72, total= 2.6s [CV] kmeans__n_clusters=72 ........................................... [CV] ............................ kmeans__n_clusters=72, total= 2.8s [CV] kmeans__n_clusters=73 ........................................... [CV] ............................ kmeans__n_clusters=73, total= 2.6s [CV] kmeans__n_clusters=73 ........................................... [CV] ............................ kmeans__n_clusters=73, total= 2.8s [CV] kmeans__n_clusters=73 ........................................... [CV] ............................ kmeans__n_clusters=73, total= 2.6s [CV] kmeans__n_clusters=74 ........................................... [CV] ............................ kmeans__n_clusters=74, total= 2.8s [CV] kmeans__n_clusters=74 ........................................... [CV] ............................ kmeans__n_clusters=74, total= 3.2s [CV] kmeans__n_clusters=74 ........................................... [CV] ............................ kmeans__n_clusters=74, total= 2.9s [CV] kmeans__n_clusters=75 ........................................... [CV] ............................ kmeans__n_clusters=75, total= 2.7s [CV] kmeans__n_clusters=75 ........................................... [CV] ............................ kmeans__n_clusters=75, total= 3.0s [CV] kmeans__n_clusters=75 ........................................... [CV] ............................ kmeans__n_clusters=75, total= 2.7s [CV] kmeans__n_clusters=76 ........................................... [CV] ............................ kmeans__n_clusters=76, total= 2.4s [CV] kmeans__n_clusters=76 ........................................... [CV] ............................ kmeans__n_clusters=76, total= 2.8s [CV] kmeans__n_clusters=76 ........................................... [CV] ............................ kmeans__n_clusters=76, total= 2.8s [CV] kmeans__n_clusters=77 ........................................... [CV] ............................ kmeans__n_clusters=77, total= 2.6s [CV] kmeans__n_clusters=77 ........................................... [CV] ............................ kmeans__n_clusters=77, total= 3.0s [CV] kmeans__n_clusters=77 ........................................... [CV] ............................ kmeans__n_clusters=77, total= 3.1s [CV] kmeans__n_clusters=78 ........................................... [CV] ............................ kmeans__n_clusters=78, total= 2.7s [CV] kmeans__n_clusters=78 ........................................... [CV] ............................ kmeans__n_clusters=78, total= 2.8s [CV] kmeans__n_clusters=78 ........................................... [CV] ............................ kmeans__n_clusters=78, total= 2.7s [CV] kmeans__n_clusters=79 ........................................... [CV] ............................ kmeans__n_clusters=79, total= 2.9s [CV] kmeans__n_clusters=79 ........................................... [CV] ............................ kmeans__n_clusters=79, total= 2.8s [CV] kmeans__n_clusters=79 ........................................... [CV] ............................ kmeans__n_clusters=79, total= 3.0s [CV] kmeans__n_clusters=80 ........................................... [CV] ............................ kmeans__n_clusters=80, total= 2.7s [CV] kmeans__n_clusters=80 ........................................... [CV] ............................ kmeans__n_clusters=80, total= 3.0s [CV] kmeans__n_clusters=80 ........................................... [CV] ............................ kmeans__n_clusters=80, total= 2.6s [CV] kmeans__n_clusters=81 ........................................... [CV] ............................ kmeans__n_clusters=81, total= 2.4s [CV] kmeans__n_clusters=81 ........................................... [CV] ............................ kmeans__n_clusters=81, total= 2.7s [CV] kmeans__n_clusters=81 ........................................... [CV] ............................ kmeans__n_clusters=81, total= 2.8s [CV] kmeans__n_clusters=82 ........................................... [CV] ............................ kmeans__n_clusters=82, total= 2.5s [CV] kmeans__n_clusters=82 ........................................... [CV] ............................ kmeans__n_clusters=82, total= 2.9s [CV] kmeans__n_clusters=82 ........................................... [CV] ............................ kmeans__n_clusters=82, total= 2.9s [CV] kmeans__n_clusters=83 ........................................... [CV] ............................ kmeans__n_clusters=83, total= 2.7s [CV] kmeans__n_clusters=83 ........................................... [CV] ............................ kmeans__n_clusters=83, total= 2.8s [CV] kmeans__n_clusters=83 ........................................... [CV] ............................ kmeans__n_clusters=83, total= 2.9s [CV] kmeans__n_clusters=84 ........................................... [CV] ............................ kmeans__n_clusters=84, total= 2.7s [CV] kmeans__n_clusters=84 ........................................... [CV] ............................ kmeans__n_clusters=84, total= 2.8s [CV] kmeans__n_clusters=84 ........................................... [CV] ............................ kmeans__n_clusters=84, total= 2.8s [CV] kmeans__n_clusters=85 ........................................... [CV] ............................ kmeans__n_clusters=85, total= 2.6s [CV] kmeans__n_clusters=85 ........................................... [CV] ............................ kmeans__n_clusters=85, total= 3.1s [CV] kmeans__n_clusters=85 ........................................... [CV] ............................ kmeans__n_clusters=85, total= 2.8s [CV] kmeans__n_clusters=86 ........................................... [CV] ............................ kmeans__n_clusters=86, total= 2.8s [CV] kmeans__n_clusters=86 ........................................... [CV] ............................ kmeans__n_clusters=86, total= 2.9s [CV] kmeans__n_clusters=86 ........................................... [CV] ............................ kmeans__n_clusters=86, total= 2.7s [CV] kmeans__n_clusters=87 ........................................... [CV] ............................ kmeans__n_clusters=87, total= 2.8s [CV] kmeans__n_clusters=87 ........................................... [CV] ............................ kmeans__n_clusters=87, total= 2.8s [CV] kmeans__n_clusters=87 ........................................... [CV] ............................ kmeans__n_clusters=87, total= 2.9s [CV] kmeans__n_clusters=88 ........................................... [CV] ............................ kmeans__n_clusters=88, total= 2.8s [CV] kmeans__n_clusters=88 ........................................... [CV] ............................ kmeans__n_clusters=88, total= 2.6s [CV] kmeans__n_clusters=88 ........................................... [CV] ............................ kmeans__n_clusters=88, total= 3.0s [CV] kmeans__n_clusters=89 ........................................... [CV] ............................ kmeans__n_clusters=89, total= 2.9s [CV] kmeans__n_clusters=89 ........................................... [CV] ............................ kmeans__n_clusters=89, total= 3.0s [CV] kmeans__n_clusters=89 ........................................... [CV] ............................ kmeans__n_clusters=89, total= 2.8s [CV] kmeans__n_clusters=90 ........................................... [CV] ............................ kmeans__n_clusters=90, total= 3.3s [CV] kmeans__n_clusters=90 ........................................... [CV] ............................ kmeans__n_clusters=90, total= 2.8s [CV] kmeans__n_clusters=90 ........................................... [CV] ............................ kmeans__n_clusters=90, total= 2.9s [CV] kmeans__n_clusters=91 ........................................... [CV] ............................ kmeans__n_clusters=91, total= 2.8s [CV] kmeans__n_clusters=91 ........................................... [CV] ............................ kmeans__n_clusters=91, total= 3.0s [CV] kmeans__n_clusters=91 ........................................... [CV] ............................ kmeans__n_clusters=91, total= 3.0s [CV] kmeans__n_clusters=92 ........................................... [CV] ............................ kmeans__n_clusters=92, total= 2.6s [CV] kmeans__n_clusters=92 ........................................... [CV] ............................ kmeans__n_clusters=92, total= 2.6s [CV] kmeans__n_clusters=92 ........................................... [CV] ............................ kmeans__n_clusters=92, total= 2.7s [CV] kmeans__n_clusters=93 ........................................... [CV] ............................ kmeans__n_clusters=93, total= 2.7s [CV] kmeans__n_clusters=93 ........................................... [CV] ............................ kmeans__n_clusters=93, total= 2.9s [CV] kmeans__n_clusters=93 ........................................... [CV] ............................ kmeans__n_clusters=93, total= 2.6s [CV] kmeans__n_clusters=94 ........................................... [CV] ............................ kmeans__n_clusters=94, total= 2.7s [CV] kmeans__n_clusters=94 ........................................... [CV] ............................ kmeans__n_clusters=94, total= 3.1s [CV] kmeans__n_clusters=94 ........................................... [CV] ............................ kmeans__n_clusters=94, total= 2.5s [CV] kmeans__n_clusters=95 ........................................... [CV] ............................ kmeans__n_clusters=95, total= 2.8s [CV] kmeans__n_clusters=95 ........................................... [CV] ............................ kmeans__n_clusters=95, total= 2.8s [CV] kmeans__n_clusters=95 ........................................... [CV] ............................ kmeans__n_clusters=95, total= 2.9s [CV] kmeans__n_clusters=96 ........................................... [CV] ............................ kmeans__n_clusters=96, total= 2.7s [CV] kmeans__n_clusters=96 ........................................... [CV] ............................ kmeans__n_clusters=96, total= 2.7s [CV] kmeans__n_clusters=96 ........................................... [CV] ............................ kmeans__n_clusters=96, total= 3.1s [CV] kmeans__n_clusters=97 ........................................... [CV] ............................ kmeans__n_clusters=97, total= 2.9s [CV] kmeans__n_clusters=97 ........................................... [CV] ............................ kmeans__n_clusters=97, total= 3.1s [CV] kmeans__n_clusters=97 ........................................... [CV] ............................ kmeans__n_clusters=97, total= 2.6s [CV] kmeans__n_clusters=98 ........................................... [CV] ............................ kmeans__n_clusters=98, total= 2.8s [CV] kmeans__n_clusters=98 ........................................... [CV] ............................ kmeans__n_clusters=98, total= 2.8s [CV] kmeans__n_clusters=98 ........................................... [CV] ............................ kmeans__n_clusters=98, total= 3.1s [CV] kmeans__n_clusters=99 ........................................... [CV] ............................ kmeans__n_clusters=99, total= 2.6s [CV] kmeans__n_clusters=99 ........................................... [CV] ............................ kmeans__n_clusters=99, total= 2.7s [CV] kmeans__n_clusters=99 ........................................... [CV] ............................ kmeans__n_clusters=99, total= 2.7s
[Parallel(n_jobs=1)]: Done 294 out of 294 | elapsed: 10.9min finished
GridSearchCV(cv=3, estimator=Pipeline(steps=[('kmeans', KMeans(n_clusters=50, random_state=42)), ('log_reg', LogisticRegression(max_iter=5000, multi_class='ovr', random_state=42))]), param_grid={'kmeans__n_clusters': range(2, 100)}, verbose=2)
최적의 군집수는 57이며, 로지스틱 회귀의 정확도는 98%가 된다.
grid_clf.best_params_
{'kmeans__n_clusters': 57}
grid_clf.score(X_test, y_test)
0.98
준지도학습은 약간의 레이블이 있는 샘플이 있고 대부분의 샘플엔 레이블이 없는 데이터셋에 대한 지도학습 기법이다. 준지도학습 설명을 위해 미니 MNIST 데이터셋을 계속 이용한다.
먼저 무작위로 선정된 50개의 샘플만을 대상으로 로지스틱 회귀모델을 훈련하면 정확도 평균값이 83.33% 정도로 낮게 나온다. 이유는 훈련 세트가 작아서 훈련이 제대로 되지 않기 때문이다.
참고: train_test_split()
함수는 무작위성을 이용한다.
n_labeled = 50
log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", random_state=42)
log_reg.fit(X_train[:n_labeled], y_train[:n_labeled])
log_reg.score(X_test, y_test)
0.8333333333333334
무작위로 50개의 샘플을 선택해서 훈련하는 대신에 50개의 군집으로 군집화한 뒤에 각 군집의 센트로이드에 가장 가까운 이미지 50개를 대상으로 훈련해보았을 때 로지스틱 회귀 모델의 성능을 확인해보자.
참고: 센트로이드에 가장 가까운 이미지를 대표이미지라 부른다.
먼저 50개의 군집으로 나눈 후 변환한다.
k = 50
kmeans = KMeans(n_clusters=k, random_state=42)
X_digits_dist = kmeans.fit_transform(X_train)
변환된 훈련세트는 50개의 특성을 가지며, 샘플별로 50개 군집의 센트로이드 사이의 거리를 특성값으로 갖는다. 따라서 특성별로 최소값을 갖는 인덱스가 50개 군집의 센트로이드에 가장 가까운 샘플을 가리킨다. 이 성질을 이용하여 대표이미지를 아래와 같이 선정한다.
representative_digit_idx = np.argmin(X_digits_dist, axis=0) # 50개의 대표이미지 인덱스 확인
X_representative_digits = X_train[representative_digit_idx] # 50개의 대표이미지 지정
✋ 선정된 50개의 대표이미지에 포함된 숫자는 다음과 같다.
plt.figure(figsize=(8, 2))
for index, X_representative_digit in enumerate(X_representative_digits):
plt.subplot(k // 10, 10, index + 1)
plt.imshow(X_representative_digit.reshape(8, 8), cmap="binary", interpolation="bilinear")
plt.axis('off')
save_fig("representative_images_diagram", tight_layout=False)
plt.show()
Saving figure representative_images_diagram
실제 타깃(레이블)은 다음과 같다.
y_train[representative_digit_idx]
array([0, 1, 3, 2, 7, 6, 4, 6, 9, 5, 1, 2, 9, 5, 2, 7, 8, 1, 8, 6, 3, 1, 5, 4, 5, 4, 0, 3, 2, 6, 1, 7, 7, 9, 1, 8, 6, 5, 4, 8, 5, 3, 3, 6, 7, 9, 7, 8, 4, 9])
위 정보를 이용하여 50개의 대표이미지에 대한 레이블을 지정한다.
y_representative_digits = y_train[representative_digit_idx]
50개의 대표이미지를 이용하여 로지스틱 회귀 모델을 훈련시킨 결과 성능이 92.4% 이상으로 좋아졌다. 이를 통해 무작위로 선정된 샘플의 레이블을 이용하는 것 보다 대표(샘플)을 군집화를 이용하여 선정한 후에 학습하면 보다 좋은 성능의 모델이 생성됨을 확인하였다.
log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_representative_digits, y_representative_digits)
log_reg.score(X_test, y_test)
0.9244444444444444
동일한 군집에 속하는 샘플의 레이블을 대표이미지의 레이블로 지정하는 레이블 전파 방식을 활용할 때의 성능을 확인해보자.
아래 코드는 군집별로 샘플의 레이블을 대표이미지의 레이블로 지정한다.
y_train_propagated = np.empty(len(X_train), dtype=np.int32)
for i in range(k):
y_train_propagated[kmeans.labels_==i] = y_representative_digits[i]
로지스틱 회귀 모델의 훈련후 성능은 93.8% 정도로 조금 향상된다.
log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train, y_train_propagated)
log_reg.score(X_test, y_test)
0.9377777777777778
군집 전체에 레이블을 전파하면 이상치 등에 대한 레이블 전파도 이루어지기에 모델의 성능을 약화시킬 수 있다. 따라서 군집별로 센트로이드에 가까운 20%의 샘플에 대해서만 레이블을 전파하고, 나머지 샘플은 무시한다.
아래 코드는 각 샘플에 대해 해당 샘플이 속한 군집 센트로이드까지의 거리를 확인한다.
X_cluster_dist = X_digits_dist[np.arange(len(X_train)), kmeans.labels_]
아래 코드는 군집별로 센트로이드 근접도가 상위 20%가 아닌 샘플들을 센트로이드와의 거리를 -1로 지정하는 방식을 이용하여 제외시킨다.
percentile_closest = 20
for i in range(k):
in_cluster = (kmeans.labels_ == i) # 군집별 샘플 대상
cluster_dist = X_cluster_dist[in_cluster]
cutoff_distance = np.percentile(cluster_dist, percentile_closest) # 군집별 센트로이드 근접도 상위 20% 경곗값
above_cutoff = (X_cluster_dist > cutoff_distance) # 군집별 센트로이드 근접도 상위 20% 이내 샘플 대상
X_cluster_dist[in_cluster & above_cutoff] = -1
아래 코드는 군집별로 센트로이드 근접도가 상위 20% 안에 드는 샘플만 훈련세트로 추출한다.
partially_propagated = (X_cluster_dist != -1)
X_train_partially_propagated = X_train[partially_propagated]
y_train_partially_propagated = y_train_propagated[partially_propagated]
선정된 샘플만을 이용하여 로지스틱 회귀 모델을 훈련한 후에 성능을 평가해보자.
log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train_partially_propagated, y_train_partially_propagated)
log_reg.score(X_test, y_test)
0.9222222222222223
성능이 (책 설명과는 달리) 오히려 좀 떨어졌다.
참고: 아마도 샘플 수가 너무 적기 때문일 것으로 추정된다. 원래 훈련 세트가 보다 크다면 성능이 올라갈 것으로 기대된다.
센트로이드 근접도가 상위 20% 이내인 샘플들을 대상으로 전파된 레이블의 정확도는 98.9%에 달한다.
np.mean(y_train_partially_propagated == y_train[partially_propagated])
0.9896193771626297
반면에 전체 훈련 세트에 대해 레이블 전파를 했을 경우의 정확도는 94.4% 정도이다.
np.mean(y_train_propagated == y_train)
0.9435783221974758
DBSCAN 알고리즘은 데이터셋의 밀도를 이용하여 군집을 구성하며, 하나의 군집은 연속된 핵심 샘플(core samples)들의 $\varepsilon$-이웃을 모두 합친 결과이다.
참고: $\varepsilon$은 그리스어 알파벳이며 엡실론이라 읽는다.
핵심 샘플인지 여부는 반경 $\varepsilon$과 min_samples
두 개의 하이퍼파라미터에 의해 결정되며,
하나의 핵심 샘플이 다른 핵심 샘플의 $\varepsilon$ 반경 안에 들어갈 때 연속된 핵심 샘플이라 한다.
따라서 하나의 군집은 핵심 샘플의 이웃의 이웃 관계로 이루어진 샘플들의 집합이다.
DBSCAN 알고리즘은 모든 군집이 충분한 밀도의 샘플들로 구성되고 군집 사이는 저밀도 지역으로
구분될 수 있을 때 잘 작동한다.
아래 예제는 moons
데이터셋을 이용하여 DBSCAN 알고리즘의 작동과정을 보여준다.
from sklearn.datasets import make_moons
moons
데이터셋 생성 X, y = make_moons(n_samples=1000, noise=0.05, random_state=42)
eps=0.05
: 엡실론 반경min_samples
: \varepsilon
반경 안에 있어야 하는 샘플 수. from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.05, min_samples=5)
dbscan.fit(X)
DBSCAN(eps=0.05)
총 7개의 군집이 형성되었다.
np.unique(dbscan.labels_)
array([-1, 0, 1, 2, 3, 4, 5, 6], dtype=int64)
일부 샘플의 군집 인덱스가 -1인 이유는 해당 샘플들이 이상치(outlier)로 간주되었음을 의미한다.
dbscan.labels_[:10]
array([ 0, 2, -1, -1, 1, 0, 0, 0, 2, 5], dtype=int64)
핵심 샘플은 총 808개로 지정되었다.
len(dbscan.core_sample_indices_)
808
처음 10개의 핵심 샘플의 인덱스는 다음과 같다.
dbscan.core_sample_indices_[:10]
array([ 0, 4, 5, 6, 7, 8, 10, 11, 12, 13], dtype=int64)
핵심 샘플 자체는 components_
속성이 기억한다.
처음 3개의 핵심 샘플은 다음과 같다.
dbscan.components_[:3]
array([[-0.02137124, 0.40618608], [-0.84192557, 0.53058695], [ 0.58930337, -0.32137599]])
이제 이웃의 반경을 0.2로 키워보자.
dbscan2 = DBSCAN(eps=0.2)
dbscan2.fit(X)
DBSCAN(eps=0.2)
그러면 군집수가 2로 줄어들고 이상치도 사라진다.
np.unique(dbscan2.labels_)
array([0, 1], dtype=int64)
plot_dbscan()
함수는 군집화된 샘플을 보여준다.
군집은 색으로 구분되며 각 샘플을 핵심 샘플, 이상치, 기타(non-core samples)로 구분한다.
def plot_dbscan(dbscan, X, size, show_xlabels=True, show_ylabels=True):
# 코어 샘플 구분용 마스크 설정
core_mask = np.zeros_like(dbscan.labels_, dtype=bool)
core_mask[dbscan.core_sample_indices_] = True
# 이상치 구분용 마스크 설정
anomalies_mask = dbscan.labels_ == -1
# 기타 샘플용 마스크 설정
non_core_mask = ~(core_mask | anomalies_mask)
# 핵심 샘플
cores = dbscan.components_
# 이상치
anomalies = X[anomalies_mask]
# 기타
non_cores = X[non_core_mask]
# 핵심 샘플 산점도: 각 샘플과 함께 반경도 함께 표현
plt.scatter(cores[:, 0], cores[:, 1],
c=dbscan.labels_[core_mask], marker='o', s=size, cmap="Paired")
plt.scatter(cores[:, 0], cores[:, 1], marker='*', s=20, c=dbscan.labels_[core_mask])
# 이상치 산점도: 빨강 X 표시
plt.scatter(anomalies[:, 0], anomalies[:, 1], c="r", marker="x", s=100)
# 기타 샘플 산점도: 책과는 달리 명확한 구분을 위해 검은 점으로 표시
# plt.scatter(non_cores[:, 0], non_cores[:, 1], c=dbscan.labels_[non_core_mask], marker=".")
plt.scatter(non_cores[:, 0], non_cores[:, 1], c="k", marker=".")
if show_xlabels:
plt.xlabel("$x_1$", fontsize=14)
else:
plt.tick_params(labelbottom=False)
if show_ylabels:
plt.ylabel("$x_2$", fontsize=14, rotation=0)
else:
plt.tick_params(labelleft=False)
plt.title("eps={:.2f}, min_samples={}".format(dbscan.eps, dbscan.min_samples), fontsize=14)
아래 코드는 위 두 경우를 비교하는 그림을 그려준다. 오른편 그림에서는 기타의 경우는 존재하지 않는다. 즉, 모든 샘플이 핵심 샘플이다. 반면에 왼편 그림에서는 세 종류의 샘플이 표시된다.
# 핵심 샘플 수
len(dbscan.components_)
808
# 이상치 수
(dbscan.labels_==-1).sum()
77
plt.figure(figsize=(9, 3.2))
# 왼편 그림: 반경 0.05
plt.subplot(121)
plot_dbscan(dbscan, X, size=100)
# 오른편 그림: 반경 0.2
plt.subplot(122)
plot_dbscan(dbscan2, X, size=600, show_ylabels=False)
save_fig("dbscan_plot")
plt.show()
Saving figure dbscan_plot
DBSCAN 모델을 predict()
메서드를 지원하지 않으며, 다만 fit_predict()
메서드만 지원한다.
이유는 DBSCAN 모델의 특성상 예측을 하려면 예측에 사용되는 데이터 샘플을 포함하여 핵심 샘플을
다시 계산해야 하기 때문인데, 이는 예측할 때마다 매 번 학습을 다시 해야 한다는 것을 의미한다.
반면에 주어진 샘플의 가장 가까운 군집을 예측하는 일은 간단하게 구현된다.
예를 들어, k-최근접 이웃 분류기인 KNeighborsClassifier
모델을 훈련하면된다.
이 방식을 설명하기 위해 앞서 훈련한 dbscan2
모델(위 그림의 오른편 모델)을 사용한다.
dbscan = dbscan2
k-최근접 이웃 분류 모델은 주어진 샘플 근처의 k개의 샘플에 대한 타깃값들의 최빈값(mode)을 주어진 샘플의 예측값으로 지정한다.
아래 코드는 학습된 DBSCAN 모델의 핵심 샘플만을 이용하여 k-최근겆 이웃 분류기를 훈련한다. 이웃 샘플의 수는 50으로 정한다.
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=50)
knn.fit(dbscan.components_, dbscan.labels_[dbscan.core_sample_indices_])
KNeighborsClassifier(n_neighbors=50)
훈련된 k-최근접 이웃 분류기를 이용하여 새로운 샘플들에 대한 클래스를 예측한다.
X_new = np.array([[-0.5, 0], [0, 0.5], [1, -0.1], [2, 1]])
knn.predict(X_new)
array([1, 0, 1, 0], dtype=int64)
k-최근접 이웃 분류기는 각 클래스에 속할 확률도 계산한다.
아래 결과에서 볼 수 있듯이 predict_proba()
메서드는
0번과 1번 클래스에 속할 확률을 각 샘플에 대해 계산한다.
knn.predict_proba(X_new)
array([[0.18, 0.82], [1. , 0. ], [0.12, 0.88], [1. , 0. ]])
아래 코드는 네 개의 새로운 샘플에 대한 예측값을 포함하여 두 군집의 결정경계를 잘 보여준다.
plt.figure(figsize=(6, 3))
# 결정경계 그리기: knn 모델의 predict() 메서드 활용
plot_decision_boundaries(knn, X, show_centroids=False)
# 네 개의 샘플 표기: 파랑 + 기호
plt.scatter(X_new[:, 0], X_new[:, 1], c="b", marker="+", s=200, zorder=10)
save_fig("cluster_classification_plot")
plt.show()
Saving figure cluster_classification_plot
앞서 사용된 훈련 세트에는 이상치가 없어서 모든 샘플에 대해 군집(클래스)을 할당한다. 하지만 모든 군집으로부터 일정 거리 이상 떨어진 샘플을 간단하게 이상치로 처리할 수 있다.
아래 코드는 k-최근접 이웃 분류기의 kneighbors()
메서드를 이용하여 가장 가까운
이웃 샘플로부터의 거리를 계산한다.
kneighbors()
메서드는 n_neighbors
로 지정된 수 만큼의 가장 가까운 데이터들의 인덱스와
거리로 이루어진 두 개의 어레이를 반환한다.y_dist, y_pred_idx = knn.kneighbors(X_new, n_neighbors=1)
y_pred = dbscan.labels_[y_pred_idx] # 가장 가까운 샘플의 (클래스) 레이블
y_pred[y_dist > 0.2] = -1 # 거리가 0.2 이상인 경우 이상치 처리
y_pred.ravel()
array([-1, 0, 1, -1], dtype=int64)
주의사항: 저자의 주피터 노트북에 사용된 아래 코드는 엄밀히 말해 정확하지 않음.
이유는 dbscan.core_sample_indices_
와 dbscan.labels_
가 다른 경우
엉뚱한 답을 내 놓을 수도 있기 때문임.
y_pred = dbscan.labels_[dbscan.core_sample_indices_][y_pred_idx]
DBSCAN 모델은 군집의 밀집도가 변하는 데이터셋에 대해서는 잘 작동하지 않는다. 이런 한계를 극복하기 위해 다양한 $\varepsilon$을 이용하여 최적의 군집을 찾는 HDBSCAN 모델을 활용할 수 있다. 간단한 사용법은 다음과 같으며 보다 자세한 사항은 scikit-learn-contrib: hdbscan 프로젝트를 참고한다.
주의사항 hdbscan
모듈을 먼저 설치해야 한다.
conda.exe install -c conda-forge hdbscan
conda install -c conda-forge hdbscan
pip.exe install hdbscan
pip install hdbscan
!pip install hdbscan
# 훈련 세트
data, _ = make_blobs(1000, centers=5)
# hdbscan 모델 훈련
from hdbscan import HDBSCAN
clusterer = HDBSCAN(min_cluster_size=10, gen_min_span_tree=True)
clusterer.fit(data)
# 생성된 군집
np.unique(clusterer.labels_)
array([-1, 0, 1, 2, 3, 4], dtype=int64)
샘플 사이의 유사도 행렬을 이용하여 저차원으로 차원축소를 진행한 후에 k-평균 등의 군집화를 진행하는 기법이다. 유사도 행렬은 가우시안 rbf(방사기저함수) 등을 이용하여 계산하며, 차원축소는 8장에서 소개한 커널 PCA와 유사한 방식으로 이루어진다.
사이킷런의 SpectralClustering
모델은 군집수를 지정해야 하며,
간단한 사용법은 다음과 같다.
from sklearn.cluster import SpectralClustering
gamma
는 8장에서 소개한 rbf 함수에 사용되는 $\gamma$ 계수에 해당한다.
gamma
값이 커질 수록 보다 가까운 샘플들 사이이의 유사도가 강조된다.
아래 코드는 유사도를 달리한 군집화의 결과를 보여준다.
sc1 = SpectralClustering(n_clusters=2, gamma=100, random_state=42)
sc1.fit(X)
SpectralClustering(gamma=100, n_clusters=2, random_state=42)
sc2 = SpectralClustering(n_clusters=2, gamma=1, random_state=42)
sc2.fit(X)
SpectralClustering(gamma=1, n_clusters=2, random_state=42)
def plot_spectral_clustering(sc, X, size, alpha, show_xlabels=True, show_ylabels=True):
plt.scatter(X[:, 0], X[:, 1], marker='o', s=size, c='gray', cmap="Paired", alpha=alpha)
plt.scatter(X[:, 0], X[:, 1], marker='o', s=30, c='w')
plt.scatter(X[:, 0], X[:, 1], marker='.', s=10, c=sc.labels_, cmap="Paired")
if show_xlabels:
plt.xlabel("$x_1$", fontsize=14)
else:
plt.tick_params(labelbottom=False)
if show_ylabels:
plt.ylabel("$x_2$", fontsize=14, rotation=0)
else:
plt.tick_params(labelleft=False)
plt.title("RBF gamma={}".format(sc.gamma), fontsize=14)
오른편 그래프의 경우 gamma=1
에 해당하며 잘못된 군집화를 보여준다.
반면에 왼편 그래프는 gamma=100
에 해당하며 제대로된 군집화를 보여준다.
plt.figure(figsize=(9, 3.2))
plt.subplot(121)
plot_spectral_clustering(sc1, X, size=500, alpha=0.1)
plt.subplot(122)
plot_spectral_clustering(sc2, X, size=4000, alpha=0.01, show_ylabels=False)
plt.show()
병합 군집화는 계층 군집화(hierchical clustering)의 주요 기법이다. 가장 작은 단위의 군집은 개별 샘플 하나로 이루어지며, 두 개의 군집을 합치며 점점 군집의 수를 줄여 나간다. 군집화 대상인 두 개의 군집은 병합했을 때 두 샘플 사이의 연결 거리(linkage distance)가 최소가 되도록 유도하며 이를 위해 탐욕 알고리즘을 사용한다.
참고: 사이킷런의 계층 군집화
아래 코드는 사이킷런의 AgglomerativeClustering
클래스를 이용하여
병합 군집화 모델을 훈련하는 과정을 간단하게 보여준다.
연결 거리는 다음 옵션 종류에 따라 다르게 측정되며 기본 옵션은 'ward'
이다.
linkage
: 'ward'
, 'complete'
, 'average'
, 'single'
from sklearn.cluster import AgglomerativeClustering
X = np.array([0, 2, 5, 8.5]).reshape(-1, 1)
X
array([[0. ], [2. ], [5. ], [8.5]])
agg = AgglomerativeClustering(linkage="complete").fit(X)
아래 함수는 훈련된 군집화 모델이 제공하는 속성의 리스트를 생성한다.
def learned_parameters(estimator):
return [attrib for attrib in dir(estimator)
if attrib.endswith("_") and not attrib.startswith("_")]
병합 군집 모델의 경우 아래 속성이 제공된다.
learned_parameters(agg)
['children_', 'labels_', 'n_clusters_', 'n_connected_components_', 'n_features_in_', 'n_leaves_']
예를 들어, children_
속성은 병합과정을 묘사하는 트리(tree)를 $(m-1, 2)$ 모양의 어레이로 설명한다.
어레이에 사용된 숫자 $i$ 의 의미는 다음과 같다.
agg.children_
array([[0, 1], [2, 3], [4, 5]])
위 결과의 의미는 다음과 같다.
childeren_
의 0번 인덱스에 위치한 [0, 1]
에 포함된 0번, 1번 노드를 자식 노드로 갖는 노드.childeren_
의 1번 인덱스에 위치한 [2, 3]
에 포함된 2번, 3번 노드를 자식 노드로 갖는 노드.childeren_
의 2번 인덱스에 위치한 [4, 5]
에 포함된 4번, 5번 노드를 자식 노드로 갖는 노드이자 루트 노드.
즉, 전체 샘플로 구성된 한 개의 클러스를 나타냄.두 개의 군집을 생성하였기에 4번, 5번 노드가 각각 서로 다른 군집을 가리킨다.
agg.labels_
array([1, 1, 0, 0], dtype=int64)
가우시안 혼합 모델은 데이터셋이 여러 개의 가우시안 분포가 혼합된 분포를 따른다는 가정하는 확률 모델이다. 하나의 가우시안 분포를 따르는 샘플들이 하나의 군집을 생성하며, 일반적으로 타원형 모습을 띈다. 타원의 모양, 크기, 밀집도, 방향이 다양한 여러 개의 가우시안 분포를 따르는 데이터 셋을 군집화한다.
아래 코드는 가우시안 혼합 모델을 지원하는 사이킷런의 GaussianMixture
클래스의
활용법을 설명하기 위해 사용되는 데이터셋을 생성한다.
X1
: 두 개의 군집으로 이루어진 데이터셋X2
: 다른 군집에 비해 밀도가 낮음# 군집 2개
X1, y1 = make_blobs(n_samples=1000, centers=((4, -4), (0, 0)), random_state=42)
X1 = X1.dot(np.array([[0.374, 0.95], [0.732, 0.598]]))
# 셋째 군집
X2, y2 = make_blobs(n_samples=250, centers=1, random_state=42)
X2 = X2 + [6, -8]
# 데이터셋 병합
X = np.r_[X1, X2]
y = np.r_[y1, y2]
GaussianMixture
모델을 적용한다.
from sklearn.mixture import GaussianMixture
gm = GaussianMixture(n_components=3, n_init=10, random_state=42)
gm.fit(X)
GaussianMixture(n_components=3, n_init=10, random_state=42)
갸중치(weights_
)는 군집별 크기 비를 나타내며, 거의 제대로 학습되었다.
gm.weights_
array([0.39054348, 0.2093669 , 0.40008962])
학습된 군집별 평균값과 공분산은 다음과 같다.
gm.means_
array([[ 0.05224874, 0.07631976], [ 3.40196611, 1.05838748], [-1.40754214, 1.42716873]])
gm.covariances_
array([[[ 0.6890309 , 0.79717058], [ 0.79717058, 1.21367348]], [[ 1.14296668, -0.03114176], [-0.03114176, 0.9545003 ]], [[ 0.63496849, 0.7298512 ], [ 0.7298512 , 1.16112807]]])
기댓값-최대화(EM, expectation-maximization) 알고리즘이 4번 반복만에 수렴을 확인할 수 있다.
gm.converged_
True
gm.n_iter_
4
predict()
와 predict_proba()
¶predict()
메서드: 군집 인덱스를 직접 지정predict_proba()
메서드: 군집별 속할 확률gm.predict(X)
array([0, 0, 2, ..., 1, 1, 1], dtype=int64)
gm.predict_proba(X)
array([[9.77227791e-01, 2.27715290e-02, 6.79898914e-07], [9.83288385e-01, 1.60345103e-02, 6.77104389e-04], [7.51824662e-05, 1.90251273e-06, 9.99922915e-01], ..., [4.35053542e-07, 9.99999565e-01, 2.17938894e-26], [5.27837047e-16, 1.00000000e+00, 1.50679490e-41], [2.32355608e-15, 1.00000000e+00, 8.21915701e-41]])
생성모델은 학습된 정보를 이용하여 새로운 샘플을 생성할 수 있으며, 속한 군집에 대한 정보도 함께 제공한다.
X_new, y_new = gm.sample(6)
X_new
array([[-0.8690223 , -0.32680051], [ 0.29945755, 0.2841852 ], [ 1.85027284, 2.06556913], [ 3.98260019, 1.50041446], [ 3.82006355, 0.53143606], [-1.04015332, 0.7864941 ]])
군집 인덱스 순서대로 샘플을 생성한다.
y_new
array([0, 0, 1, 1, 1, 2])
생성된 샘플들의 군집별 비는 군집별 가중치를 따른다.
from collections import Counter
X_new, y_new = gm.sample(1000)
Counter(y_new)
Counter({0: 377, 1: 207, 2: 416})
score_samples()
와 확률밀도¶샘플별로 확률밀도함수(PDF, probability density function)의 로그를 계산한다.
gm.score_samples(X)
array([-2.60674489, -3.57074133, -3.33007348, ..., -3.51379355, -4.39643283, -3.8055665 ])
np.exp(gm.score_samples(X_new))
array([1.31639021e-01, 2.96960925e-02, 1.07634613e-01, 1.12503697e-01, 2.17324126e-02, 3.22192929e-02, 8.05586623e-02, 3.71389975e-02, 4.61959839e-02, 5.03675976e-02, 1.19512749e-01, 6.84431993e-02, 1.11923123e-01, 2.11008615e-02, 7.95154261e-02, 4.69492918e-02, 2.00451856e-02, 5.88440087e-02, 1.04078068e-01, 1.31913878e-01, 4.09799771e-02, 7.16252879e-02, 2.77195832e-02, 1.22260905e-01, 9.21260157e-02, 5.28444834e-02, 9.45852830e-02, 8.16135629e-02, 1.22196182e-01, 4.09359870e-02, 3.97613059e-02, 8.37120648e-02, 1.05571293e-01, 3.98534829e-02, 4.08397392e-02, 4.69898979e-03, 1.32262839e-01, 1.94025472e-02, 1.27262102e-01, 4.06930201e-02, 8.98815435e-02, 8.64395294e-02, 1.06151021e-01, 8.64172062e-02, 1.03952537e-01, 4.49821725e-02, 1.28355206e-01, 1.35095683e-01, 5.06827431e-02, 9.54188832e-02, 1.26366536e-01, 2.30658745e-02, 1.33871902e-01, 2.61271081e-02, 7.32032328e-03, 1.30327530e-01, 7.02991897e-02, 5.44200670e-02, 6.71002120e-02, 1.94130708e-02, 1.08225865e-02, 7.48081004e-02, 1.21717881e-01, 4.49531906e-02, 7.19993064e-02, 2.78941892e-02, 9.83459656e-02, 4.67202404e-02, 5.75600875e-02, 4.07065545e-02, 9.88258160e-02, 3.23333144e-02, 1.15844074e-01, 1.26643682e-01, 1.07968142e-01, 1.03022451e-01, 2.17502834e-02, 5.65677690e-02, 6.43129760e-02, 5.06291792e-02, 8.00954728e-02, 1.64389983e-02, 1.01760062e-01, 6.92906929e-02, 1.30624545e-01, 9.48543873e-02, 4.82939527e-02, 4.70852576e-03, 7.90077230e-02, 7.10714710e-02, 1.05029000e-01, 1.23714632e-01, 3.37920638e-02, 8.73148986e-02, 6.25110496e-02, 1.26930080e-01, 9.40790293e-02, 7.22539193e-02, 1.11208350e-01, 4.42892368e-02, 3.97957413e-02, 1.06477900e-01, 1.72069430e-02, 6.18610325e-02, 7.11826269e-02, 9.91845008e-02, 1.01391815e-01, 1.23443829e-01, 1.66575678e-03, 2.98764885e-02, 6.88699387e-02, 7.76281260e-02, 8.59185100e-02, 1.04705030e-01, 1.12224901e-01, 1.13960863e-02, 2.11069854e-02, 7.80096361e-02, 1.01958807e-01, 8.39568310e-02, 6.68137224e-02, 1.00922912e-01, 2.68972249e-02, 4.25403227e-02, 8.56519721e-03, 4.35069292e-02, 4.84010225e-02, 9.31380291e-02, 9.17448913e-02, 2.12547553e-03, 6.22606428e-02, 1.31630095e-02, 1.25319848e-01, 1.75134585e-02, 7.05218374e-02, 7.83073716e-02, 1.13664116e-01, 1.28604059e-01, 1.10753453e-01, 1.83316178e-02, 2.12166899e-03, 1.15488101e-01, 1.09908719e-01, 1.20554088e-01, 8.17153845e-02, 1.02422586e-01, 8.82884605e-02, 8.17683046e-02, 8.58936070e-02, 8.70940585e-02, 1.37865548e-01, 5.14832817e-02, 1.17035495e-01, 7.42110145e-02, 7.08757813e-02, 5.91506493e-02, 1.04780054e-01, 1.30547428e-01, 1.15720611e-01, 1.15354087e-02, 4.35641955e-02, 5.18566197e-02, 9.37231999e-02, 9.29608832e-02, 1.10121201e-01, 8.53039368e-02, 9.52599426e-02, 1.08702053e-01, 9.71384989e-02, 1.18452491e-01, 1.21338447e-01, 2.51502386e-02, 1.05916386e-01, 4.45451847e-02, 9.48121166e-02, 8.41433491e-02, 1.33196408e-01, 9.35812882e-02, 3.74735263e-02, 1.27357125e-01, 1.00875977e-01, 1.35322561e-01, 1.02538337e-01, 1.22590951e-01, 3.05528091e-02, 1.01749888e-02, 9.32413121e-02, 9.09886424e-03, 8.33510327e-02, 1.81403942e-02, 1.03844965e-01, 2.26027105e-02, 7.47698416e-02, 8.31849143e-02, 1.16350920e-02, 6.19823739e-02, 1.42712741e-02, 6.41501433e-02, 3.77177176e-02, 1.38685829e-01, 1.03610964e-01, 7.98530239e-02, 1.20709617e-01, 5.72274564e-02, 2.03317810e-02, 9.92505808e-02, 4.13766220e-02, 6.90477744e-02, 3.56119471e-03, 8.40472925e-02, 4.41641619e-02, 6.99056859e-02, 1.68756618e-02, 1.54625757e-02, 1.03611883e-01, 4.47530472e-02, 1.42095097e-02, 3.64447614e-02, 1.28815601e-01, 2.05421191e-02, 4.91330168e-02, 8.34431863e-02, 7.04841435e-02, 8.78251541e-02, 7.64239844e-02, 8.90343484e-02, 8.10324615e-02, 5.93969807e-02, 2.01206597e-02, 1.07000853e-01, 1.34736012e-01, 8.62297909e-02, 1.03006184e-01, 1.03915331e-02, 1.86091088e-02, 3.47602055e-02, 1.11931430e-01, 6.47917099e-04, 8.74925695e-02, 4.05904004e-02, 4.02904621e-02, 6.42864775e-02, 2.26597123e-02, 4.64683905e-02, 9.55988292e-02, 3.76103481e-02, 8.16934549e-02, 3.93575057e-02, 1.46560698e-02, 4.66527457e-02, 1.00861294e-01, 8.13732923e-02, 9.82729761e-02, 1.33614898e-01, 8.97721629e-02, 8.87590266e-02, 7.59104278e-02, 1.04143098e-01, 3.23411263e-02, 9.52732795e-02, 2.32085553e-02, 4.04335315e-02, 8.91830892e-02, 2.84723677e-02, 6.67905178e-02, 1.12898178e-01, 9.07123373e-02, 7.07925600e-02, 5.16893314e-02, 4.26703291e-02, 9.94777271e-03, 1.15058527e-01, 1.07938202e-01, 1.36636997e-01, 6.79129433e-02, 1.20421450e-01, 1.12918340e-01, 1.17486929e-01, 1.56164971e-02, 9.00086014e-02, 6.20369764e-02, 1.78125505e-02, 2.76899438e-02, 3.41035654e-02, 1.28421893e-01, 8.54256073e-03, 1.00273936e-01, 2.56847632e-02, 1.20215355e-01, 8.89702352e-03, 1.32359998e-01, 8.78589986e-02, 6.81257309e-02, 1.23575492e-01, 1.17003103e-01, 7.11052050e-02, 8.33831506e-02, 1.17457639e-01, 6.80915895e-02, 3.79281060e-02, 2.19712151e-02, 4.96755971e-02, 1.06691983e-02, 1.11818638e-01, 7.08814715e-02, 6.91187409e-03, 1.06018864e-01, 1.20522063e-01, 9.17108814e-02, 9.54992116e-03, 3.42145214e-02, 8.62424441e-02, 7.61724069e-02, 1.15667945e-02, 1.25346928e-01, 2.55467277e-02, 9.85915088e-02, 3.17427198e-02, 3.27255709e-02, 4.39964384e-02, 1.30019164e-01, 6.17028045e-03, 1.06178909e-01, 1.33111496e-02, 7.31017334e-02, 5.05105640e-03, 1.38788219e-01, 1.34679645e-01, 1.02326025e-01, 1.19807019e-01, 1.07936784e-01, 4.34636220e-02, 3.44853575e-03, 7.43894606e-03, 1.22570274e-01, 4.29889144e-02, 1.67758762e-02, 3.99017096e-02, 5.65344645e-02, 1.07721239e-01, 5.25533760e-02, 1.25981788e-01, 9.76194968e-02, 6.65688991e-02, 6.53436746e-02, 1.10915549e-01, 6.02060166e-02, 8.89844399e-02, 7.01237131e-02, 4.22554600e-02, 3.67334571e-02, 1.39018249e-02, 4.91362824e-03, 1.35092315e-01, 1.10952175e-01, 7.54981007e-02, 1.36345545e-01, 1.30062106e-01, 3.07440887e-02, 4.41975669e-02, 1.34489194e-01, 1.30662178e-01, 8.96135287e-02, 8.44271552e-02, 3.00146460e-02, 7.30541669e-02, 8.28615531e-02, 2.07199295e-02, 1.96451639e-02, 1.14893610e-01, 9.32568265e-02, 1.02579796e-01, 4.44703865e-02, 3.95923427e-02, 5.03137783e-02, 3.67909355e-03, 1.20760795e-01, 1.72794833e-02, 2.68429499e-02, 5.73899254e-02, 1.28985106e-02, 6.65032614e-03, 5.07383597e-03, 2.12103710e-02, 1.63129163e-02, 7.66756593e-03, 2.37629796e-02, 2.62519109e-02, 1.28811469e-02, 2.46421527e-02, 1.52345785e-02, 2.03384377e-02, 8.01463013e-03, 2.84821293e-02, 1.36154458e-02, 1.02186939e-02, 2.52827411e-02, 3.18896717e-02, 1.79930893e-02, 2.85500030e-02, 2.76863269e-02, 2.77671603e-02, 9.75423653e-03, 2.84737798e-02, 1.57250870e-02, 6.21731590e-03, 2.07336766e-02, 1.80314714e-02, 1.03418843e-02, 8.29548291e-02, 1.00515455e-02, 1.07172340e-02, 2.80824858e-02, 8.47729638e-03, 1.43567465e-02, 3.10343940e-02, 6.29971689e-03, 1.92410987e-02, 2.53863566e-02, 1.57959707e-02, 2.78830477e-02, 1.51850839e-03, 2.26190130e-02, 2.25265900e-02, 1.28470541e-02, 1.05037325e-02, 2.01983648e-02, 2.24288791e-02, 9.91250286e-03, 5.54050909e-03, 2.45377960e-02, 2.89721941e-02, 3.58588034e-03, 2.28472659e-02, 9.43792112e-03, 6.37497931e-03, 2.72246990e-02, 1.57501958e-02, 1.00378285e-01, 1.64153096e-02, 4.78847168e-03, 2.52315838e-02, 1.45253547e-02, 2.00944105e-02, 1.05131855e-02, 1.54419938e-02, 3.11370735e-02, 1.10656507e-02, 2.75997909e-02, 1.40904406e-02, 3.13053275e-02, 9.58380130e-03, 2.66856093e-03, 2.97178748e-02, 8.91835309e-03, 3.05589846e-02, 2.60754154e-02, 2.83503369e-02, 1.05916646e-02, 2.47592911e-02, 4.89939058e-03, 2.54263147e-02, 1.58959056e-02, 4.66284914e-03, 9.97383347e-04, 2.55411370e-02, 1.47432183e-02, 2.64250948e-02, 9.61135577e-03, 2.57092140e-02, 7.92612591e-03, 7.84709933e-03, 3.09533050e-02, 2.91384015e-02, 5.14344085e-03, 1.70402250e-02, 2.71150199e-02, 2.45492475e-02, 1.08037154e-02, 2.43485959e-02, 1.12698650e-02, 5.01623358e-03, 1.14588326e-02, 2.51034381e-02, 1.81170251e-02, 4.84721323e-03, 1.52690050e-03, 6.81847992e-03, 2.14378600e-02, 1.99746955e-02, 1.63171222e-02, 1.70390902e-02, 3.11355999e-02, 3.88480787e-03, 2.45331748e-02, 1.85783301e-02, 2.90343737e-02, 2.30134223e-02, 1.45790605e-02, 2.58441527e-02, 2.31580548e-02, 1.76283937e-02, 1.59578209e-02, 1.51001648e-02, 4.08569872e-02, 2.76048534e-02, 2.29629123e-02, 2.77117439e-02, 2.36149923e-02, 3.21653864e-03, 2.79168060e-02, 3.16646202e-02, 1.56719341e-02, 7.67461490e-03, 2.39035969e-02, 8.22907921e-03, 5.15351104e-03, 1.79887201e-02, 1.36443615e-02, 1.79542382e-02, 2.12806528e-02, 7.44734195e-03, 1.09747196e-02, 2.42673372e-02, 2.73066728e-02, 2.18930450e-02, 1.60410897e-02, 2.57053427e-02, 4.09945474e-04, 3.63282212e-03, 1.59684326e-02, 7.03759425e-03, 2.69429370e-02, 2.84526450e-02, 1.39731906e-02, 1.72091858e-02, 8.67123273e-03, 1.71676337e-02, 2.82574245e-02, 3.50770635e-03, 2.70510784e-02, 1.84341112e-02, 3.02742590e-02, 3.18128325e-02, 2.44636287e-02, 2.43265005e-02, 4.32162344e-03, 1.98369545e-02, 2.92575117e-04, 5.86934354e-02, 1.00605837e-02, 1.51884888e-02, 4.20731215e-03, 3.06094091e-02, 1.94149336e-02, 2.95243335e-02, 6.70359576e-02, 1.80109475e-02, 1.03319056e-02, 1.35435126e-02, 4.95690715e-03, 2.29396240e-02, 3.01753885e-02, 2.77291582e-02, 2.21334827e-02, 1.94644087e-02, 7.40785679e-03, 2.08897966e-02, 1.18528609e-02, 3.13114173e-02, 3.14383216e-02, 6.20492330e-03, 7.44911116e-03, 1.92158810e-03, 1.08450256e-02, 1.84652724e-02, 1.99295191e-03, 1.87132571e-02, 1.01815031e-04, 3.13930527e-03, 1.58168965e-03, 1.56031138e-02, 1.85936079e-02, 2.30146654e-02, 1.39740537e-01, 1.01864214e-01, 3.34398889e-02, 4.70083915e-02, 5.25704506e-02, 2.69198896e-02, 5.54237492e-02, 8.47275320e-02, 7.57217419e-02, 5.75110699e-02, 1.11636357e-01, 3.59294343e-02, 1.20826800e-01, 5.41447887e-02, 1.27376189e-01, 1.20555113e-01, 1.40493046e-01, 8.83416372e-02, 1.13942264e-02, 5.11140617e-02, 7.28713202e-02, 6.18831926e-02, 1.07930999e-01, 9.94899027e-03, 6.62934793e-02, 1.05711907e-01, 1.37221085e-01, 1.39130616e-01, 7.68623392e-03, 4.71906262e-02, 5.03206161e-03, 2.65996590e-02, 2.95652635e-02, 3.17998050e-02, 1.28330591e-01, 7.10493942e-02, 5.39394464e-02, 1.04604693e-01, 4.08503208e-02, 9.41645334e-02, 1.79959084e-02, 3.18821324e-02, 3.18370433e-02, 1.09634528e-01, 1.20111052e-01, 7.96973805e-02, 5.10992637e-02, 9.29925008e-02, 5.00352279e-02, 6.44677284e-02, 7.37547786e-02, 6.67746446e-02, 8.84784498e-03, 1.31756847e-01, 1.37914710e-01, 6.93259973e-02, 4.26517521e-02, 2.29338306e-02, 7.73955773e-03, 5.83661011e-03, 9.90495900e-02, 1.39621968e-01, 8.84722239e-02, 1.28982521e-01, 6.88509576e-02, 1.04202712e-01, 8.14215821e-02, 1.40715169e-01, 6.97497964e-02, 1.00734536e-01, 4.58444205e-02, 5.95510857e-02, 7.62141300e-02, 1.14892146e-01, 1.22153777e-01, 1.24754488e-01, 2.00618936e-02, 8.96333985e-02, 5.65780677e-02, 2.07569032e-02, 4.52980329e-02, 1.31540277e-01, 1.11426105e-01, 1.28679849e-01, 1.38972636e-01, 6.29847351e-02, 2.97919581e-02, 3.49033156e-03, 5.39492308e-02, 1.41080480e-02, 9.11420950e-02, 4.82881876e-03, 1.12308130e-01, 4.63858161e-02, 1.19017058e-01, 8.70628271e-02, 6.79458449e-02, 9.10450467e-02, 8.62813473e-02, 3.20483646e-02, 1.50378537e-02, 4.66694213e-02, 6.55399700e-02, 2.21236317e-02, 9.93203588e-02, 1.14356650e-02, 1.02209842e-01, 1.78275388e-02, 1.05939298e-01, 1.00659175e-01, 1.83748544e-02, 1.29464170e-01, 3.54798435e-02, 5.51976035e-02, 7.04728423e-02, 5.38269422e-02, 6.43765186e-02, 1.66014925e-02, 1.29499741e-01, 4.99200360e-02, 1.21780477e-02, 7.44172950e-02, 1.35568567e-01, 3.84789648e-02, 6.85217742e-02, 9.26228037e-02, 1.19324307e-02, 9.25074326e-03, 1.32211506e-01, 9.62748221e-02, 8.20208954e-02, 1.33391429e-01, 3.75049136e-02, 9.52607889e-02, 5.67662747e-02, 5.97418544e-02, 1.05126987e-01, 1.03085542e-01, 6.53834784e-02, 3.15228573e-02, 4.63581978e-03, 2.55007754e-02, 9.85699100e-02, 4.88546983e-02, 6.59102426e-02, 9.59376681e-02, 1.00030488e-01, 3.22976613e-02, 6.23875101e-02, 5.71277272e-02, 1.39503917e-01, 8.38506467e-02, 1.27977970e-01, 8.62995706e-02, 1.01164752e-01, 2.92568093e-02, 8.88603293e-02, 1.10407344e-02, 1.98145370e-02, 4.58472521e-03, 1.18035197e-01, 5.73112741e-03, 8.10268237e-03, 6.99144393e-02, 8.93307000e-02, 1.00736767e-01, 1.25211835e-01, 6.99924127e-02, 4.70737508e-02, 7.20154614e-02, 1.91621161e-02, 1.39261305e-01, 3.67060433e-02, 1.49379960e-02, 5.63897675e-02, 8.66044708e-02, 7.26187312e-02, 7.24661941e-03, 6.67643131e-02, 2.92274616e-02, 1.32166221e-01, 3.54503641e-02, 1.07060650e-01, 2.15550070e-03, 2.92639372e-02, 3.19064975e-02, 1.05213367e-01, 8.17216502e-02, 4.94541717e-02, 1.37198227e-01, 6.80473875e-02, 1.28259328e-01, 6.71755247e-02, 4.45385428e-02, 1.06464398e-01, 5.04517020e-03, 1.03144157e-01, 1.22956660e-01, 3.61322078e-02, 4.67684028e-02, 5.37363118e-02, 1.26185976e-01, 1.93884514e-02, 7.74265583e-02, 7.13339499e-02, 8.38830264e-02, 1.00990851e-01, 1.29574599e-01, 9.95909996e-02, 2.34301151e-03, 8.50575359e-02, 5.21786401e-03, 1.39970839e-02, 1.07984369e-01, 5.38017544e-02, 8.30431736e-02, 7.95511619e-02, 3.73322188e-02, 1.29430208e-01, 3.97877518e-02, 9.50305142e-02, 8.46147414e-04, 1.01490143e-01, 6.64749906e-02, 4.97350663e-02, 7.38084203e-02, 1.35894526e-02, 5.60716539e-02, 9.76371395e-02, 8.74913062e-02, 6.73364148e-02, 1.22695402e-01, 8.15635650e-02, 9.49155351e-03, 5.19195199e-02, 1.26682447e-01, 7.30528023e-02, 8.51003802e-02, 2.43343914e-03, 1.12855728e-01, 1.29790393e-01, 1.27263092e-01, 5.82619500e-02, 4.43001535e-02, 5.87212831e-02, 1.23465276e-02, 6.06918497e-02, 7.52412937e-02, 8.51478100e-02, 1.27915903e-01, 8.52116301e-02, 3.62523460e-02, 2.10990815e-02, 7.79220569e-02, 1.29720117e-01, 8.87910777e-02, 3.47046495e-02, 1.23844336e-01, 1.30782003e-01, 1.05879342e-01, 3.89357118e-02, 2.13160926e-02, 6.78628083e-02, 1.21388265e-01, 1.17454059e-01, 6.21789267e-02, 1.25724571e-01, 1.84712467e-02, 8.67990256e-02, 7.57250412e-02, 9.28348832e-03, 1.21488991e-01, 1.30832811e-01, 9.95525232e-02, 1.19324889e-01, 8.86689340e-02, 5.04007729e-02, 8.79773000e-02, 2.42947406e-02, 1.29181163e-01, 1.34428846e-01, 2.77079242e-02, 4.57300796e-02, 2.07496009e-02, 1.38030947e-01, 7.15028263e-02, 8.90178221e-02, 7.71780800e-02, 8.37765540e-02, 2.89921848e-03, 2.13562920e-02, 4.80881593e-02, 8.85655574e-02, 1.61666026e-02, 2.78560371e-02, 3.89322002e-02, 9.57208641e-02, 1.31340008e-02, 1.20002662e-01, 3.34356235e-02, 1.13797718e-01, 4.47497027e-02, 1.16675726e-01, 1.01026261e-01, 5.22022869e-02, 3.40005319e-02, 4.85141320e-02, 1.01145975e-01, 5.44797839e-02, 2.41163856e-02, 1.10941766e-01, 1.03723775e-01, 9.74678053e-02, 7.88893048e-02, 2.30592128e-02, 3.48783321e-02, 7.99527193e-03, 1.19353734e-01, 2.70230434e-02, 1.23933773e-01, 2.56714658e-02, 9.12943831e-02, 1.07626721e-02, 8.45813835e-02, 1.39158355e-01, 1.11128038e-01, 1.25875532e-01, 2.87135750e-02, 1.40069978e-01, 1.06611371e-01, 8.57737987e-02, 1.09324001e-01, 7.27733347e-02, 1.37685583e-01, 2.38374008e-02, 1.20583535e-01, 8.19722413e-02, 4.34252539e-02, 8.03182806e-02, 2.36300568e-02, 4.52604723e-02, 6.81508481e-02, 5.17501368e-02, 1.30531583e-01, 1.00428893e-01, 2.23152558e-02, 1.51163407e-02, 4.37880176e-02, 7.13731731e-02, 7.46615160e-02, 9.90674018e-02, 2.25277307e-02, 1.17603357e-01, 2.47052764e-02, 2.39662130e-02, 5.51633250e-02, 1.35311745e-01, 9.27187093e-02, 1.25817726e-01, 1.21259874e-01, 1.27227661e-01, 1.30951832e-01, 3.79341435e-02, 1.15146027e-01, 1.04436538e-01, 6.60716583e-02, 1.26620386e-01, 2.17821380e-02, 6.83307843e-02, 2.60390922e-03, 1.01310665e-01, 2.40263389e-02, 8.51419253e-02, 1.24166830e-01, 1.50139087e-02, 7.69576860e-02, 1.07364694e-02, 5.51270131e-03, 1.12858511e-02, 5.84411374e-02, 1.25629173e-01, 1.38041295e-03, 1.22194051e-01, 7.77036376e-02, 1.24341623e-01, 1.28005052e-01, 1.08384921e-01, 1.31058087e-01, 7.45785700e-02, 1.31879683e-02, 7.54549781e-02, 4.58437682e-02, 1.04862247e-04, 7.86197212e-02, 1.34949200e-01, 9.98064938e-02, 1.20158187e-01, 1.36493087e-01, 7.26361476e-02, 4.16200759e-03, 1.11397521e-01, 1.30521296e-01, 4.77725085e-02, 9.61293215e-02, 3.39511486e-02, 7.97535456e-02, 7.83602190e-02, 9.47334089e-02, 1.04393814e-01, 7.68890205e-02, 5.40313026e-02, 7.46492753e-02, 9.53936240e-02, 1.05264993e-01, 1.10925161e-01, 9.80903996e-02])
아래 코드는 확률밀도함수와 x축으로 감싸인 면적이 1임을 증명한다. 증명 방식은 구분구적법(segmentation method)을 이용한 정적분 계산이다.
먼저 x축과 y축 모두 -10부터 10 사이의 구간을 1/100 단위로 구분하여 가로세로 20인 정사각형 영역을 2000x2000개의 작은 격자로 잘개 쪼갠다. -10부터 10 사이의 구간을 택한 이유는 해당 영역 이외에서는 확률밀도가 거의 0기 때문이다.
resolution = 100
grid = np.arange(-10, 10, 1 / resolution)
xx, yy = np.meshgrid(grid, grid)
X_full = np.vstack([xx.ravel(), yy.ravel()]).T
이제 각 격자(정사각형)의 한쪽 모서리에서의 확률밀도를 해당 격자의 면적과 곱한 값을 모두 더하면 거의 1이다.
score_samples()
메서드의 반환값은 확률밀도의 로그임에 주의하라.# exp() 함수를 적용하여 log() 함수를 상쇄시킴
pdf = np.exp(gm.score_samples(X_full))
# 격자의 크기를 확률밀도와 곱하기
pdf_probas = pdf * (1 / resolution) ** 2
pdf_probas.sum()
0.9999999999271592
plot_gaussian_mixture()
함수는 가우시안 혼합 모델이 알아낸
군집 결정경계와 샘플의 로그밀도 등고선(density contours)을 그린다.
from matplotlib.colors import LogNorm
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))
# score_samples가 기본적으로 음수이기에 양수로 변환함
Z = -clusterer.score_samples(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z,
norm=LogNorm(vmin=1.0, vmax=30.0),
levels=np.logspace(0, 2, 12))
plt.contour(xx, yy, Z,
norm=LogNorm(vmin=1.0, vmax=30.0),
levels=np.logspace(0, 2, 12),
linewidths=1, colors='k')
# 결정경계 그리기: 빨강 파선
Z = clusterer.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contour(xx, yy, Z, 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$", fontsize=14)
if show_ylabels:
plt.ylabel("$x_2$", fontsize=14, rotation=0)
else:
plt.tick_params(labelleft=False)
군집 경계와 로그밀도 등고선이 제대로 그려진다.
plt.figure(figsize=(8, 4))
plot_gaussian_mixture(gm, X)
save_fig("gaussian_mixtures_plot")
plt.show()
Saving figure gaussian_mixtures_plot
covariance_type
속성과 군집 형태¶군집에 속하는 데이터들의 가우시안 분포를 결정하는
공분산 행렬은 공분산 유형(covariance_type
) 옵션에 따라 달라진다.
"full"
: 어떤 제한도 가하지 않음. 즉 군집은 임의의 크기와 모양의 타원 형태를 가질 수 있음."tied"
: 모든 군집이 동일한 모양과 크기의 타원 형태이어야 함."spherical"
: 모든 군집이 원 형태를 가져야 함. 지름은 다를 수 있음."diag"
: 모든 군집이 임의의 크기와 모양의 타원 형태를 가져야 함.
단, 타원의 축이 기본 축과 평행해야 함.
즉, 공분산 행력이 대각행렬이어야 함. 아래 코드는 다양한 방식으로 가우시안 혼합 모델을 훈련시킨다.
gm_full = GaussianMixture(n_components=3, n_init=10, covariance_type="full", random_state=42)
gm_tied = GaussianMixture(n_components=3, n_init=10, covariance_type="tied", random_state=42)
gm_spherical = GaussianMixture(n_components=3, n_init=10, covariance_type="spherical", random_state=42)
gm_diag = GaussianMixture(n_components=3, n_init=10, covariance_type="diag", random_state=42)
gm_full.fit(X)
gm_tied.fit(X)
gm_spherical.fit(X)
gm_diag.fit(X)
GaussianMixture(covariance_type='diag', n_components=3, n_init=10, random_state=42)
compare_gaussian_mixtures()
함수는 서로 다른 옵션을 사용하는 두 모델의 결과를
비교하는 그래프를 그린다.
def compare_gaussian_mixtures(gm1, gm2, X):
plt.figure(figsize=(9, 4))
plt.subplot(121)
plot_gaussian_mixture(gm1, X)
plt.title('covariance_type="{}"'.format(gm1.covariance_type), fontsize=14)
plt.subplot(122)
plot_gaussian_mixture(gm2, X, show_ylabels=False)
plt.title('covariance_type="{}"'.format(gm2.covariance_type), fontsize=14)
"tied"
방식과 "spherical"
방식 사이의 차이는 다음과 같다.
compare_gaussian_mixtures(gm_tied, gm_spherical, X)
save_fig("covariance_type_plot")
plt.show()
Saving figure covariance_type_plot
"full"
방식과 "diag"
방식 사이의 차이는 다음과 같다.
compare_gaussian_mixtures(gm_full, gm_diag, X)
plt.tight_layout()
plt.show()
지정된 밀도보다 낮은 저밀도 지역에 위치한 샘플을 이상치로 간주할 수 있다. 밀도 기준은 경우에 따라 다르게 지정된다. 예를 들어, 아래 코드는 제조 결함률이 4% 정도라고 알려져 있다면 밀도 임곗값을 4%의 제조 결함률이 이상치로 판명되도록 정한다.
densities = gm.score_samples(X)
density_threshold = np.percentile(densities, 4) # 4%를 이상치로 처리하는 밀도 임곗값
anomalies = X[densities < density_threshold] # 이상치
아래 코드는 밀도 4% 이하의 지역에 위차한 이상치를 표시한다.
plt.figure(figsize=(8, 4))
plot_gaussian_mixture(gm, X)
plt.scatter(anomalies[:, 0], anomalies[:, 1], color='r', marker='*') # 이상치 표시: 빨강 별표
plt.ylim(top=5.1)
save_fig("mixture_anomaly_detection_plot")
plt.show()
Saving figure mixture_anomaly_detection_plot
k-평균 모델의 경우와는 달리 군집이 임의의 타원 형태를 띌 수 있기 때문에 적절한 군집수를 선택하기 위해 관성(이너셔), 실루엣 점수 등을 사용할 수 없다. 대신에 BIC 또는 AIC와 같은 이론적 정보 기준(theoretical information criterion)을 사용할 수 있다.
BIC(베이즈 정보 기준, Bayesian Information criterion)와 AIC(아카이케 정보 기준, Akaike information criterion) 모두 값이 낮을 수록 좋은 모델이다. 이는 모델이 학습해야 하는 파라미터가 적을 수록 불리함을 의미하며 결국 군집수를 적게하는 방향으로 유도한다. 또한 모델의 가능도 함수의 값이 클 수록 좋으며 따라서 데이터를 보다 잘 대표하는 모델일 수록 두 기준값이 낮아진다.
학습된 모델의 BIC와 AIC는 각각 bic()
, aic()
메서드가 계산한다.
gm.bic(X)
8189.662685850679
gm.aic(X)
8102.437405735641
모델이 학습해야 하는 파라미터는 아래 세 종류이다.
참고: $n\times n$ 공분산 행렬의 자유도(파라미터 수)는 $n^2$이 아니라 $1 + 2 + \dots + n = \dfrac{n (n+1)}{2}$이다.
# p = (군집수 - 1) + (군집수 * 차원) + (군집수 * 차원 * (차원+1) // 2)
n_clusters = 3
n_dims = 2
n_params_for_weights = n_clusters - 1 # 군집별 가중치
n_params_for_means = n_clusters * n_dims # 군집별 평균값
n_params_for_covariance = n_clusters * n_dims * (n_dims + 1) // 2 # 군집별 공분산
n_params = n_params_for_weights + n_params_for_means + n_params_for_covariance
# 각 샘플에 대한 최대 로그 가능도를 모두 더한 값
max_log_likelihood = gm.score(X) * len(X)
bic = np.log(len(X)) * n_params - 2 * max_log_likelihood
aic = 2 * n_params - 2 * max_log_likelihood
bic, aic
(8189.662685850679, 8102.437405735641)
군집수를 달리하면서 BIC와 AIC의 변화를 관측해보자. 아래 코드는 군집수를 1 개에서 10 개까지 변화시키면서 10개의 가우안 혼합 모델을 훈련한다.
gms_per_k = [GaussianMixture(n_components=k, n_init=10, random_state=42).fit(X)
for k in range(1, 11)]
bics = [model.bic(X) for model in gms_per_k]
aics = [model.aic(X) for model in gms_per_k]
훈련된 모델의 BIC와 AIC의 변화를 그래프로 그리면 다음과 같다.
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$", fontsize=14)
plt.ylabel("Information Criterion", fontsize=14)
plt.axis([1, 9.5, np.min(aics) - 50, np.max(aics) + 50])
plt.annotate('Minimum',
xy=(3, bics[2]),
xytext=(0.35, 0.6),
textcoords='figure fraction',
fontsize=14,
arrowprops=dict(facecolor='black', shrink=0.1)
)
plt.legend()
save_fig("aic_bic_vs_k_plot")
plt.show()
Saving figure aic_bic_vs_k_plot
아래 코드는 군집수뿐만 아니라 공분산 유형(covariance_type
)을 달리하면서
가장 좋은 BIC를 갖는 가우시안 혼합 모델을 찾는다.
min_bic = np.infty
for k in range(1, 11):
for covariance_type in ("full", "tied", "spherical", "diag"):
bic = GaussianMixture(n_components=k, n_init=10,
covariance_type=covariance_type,
random_state=42).fit(X).bic(X)
if bic < min_bic:
min_bic = bic
best_k = k
best_covariance_type = covariance_type
3개의 군집과 full
공분산 유형이 가장 좋은 모델을 생성한다.
best_k
3
best_covariance_type
'full'
가능도(likelihood) 함수는 파라미터가 알려지지 않은 분포에서 특정 확률변수 값 $\mathbf{x}$가 주어졌을 때 파라미터의 변화에 따라 해당 값이 나타날 확률을 계산한다. 반면에 최대 가능도 추정치(MLE, maximal likelihood estimate)는 주어진 확률변수 값이 발생할 확률을 최대로 하는 확률분포의 파라미터이다.
가능도 함수의 최댓값을 찾는 대신에 가능도 함숫값의 로그값을 최대화시키는 작업을 사용하곤 한다. 이유는 두 함숫값을 최대화 하는 파라미터가 동일한 반면에 함수의 로그값의 최댓값을 구하는 일이 경우에 따라 훨씬 쉬어지기 때문이다.
아래 코드는 두 개의 가우시안 분포가 혼합된 확률분포 모델을 생성한다.
from scipy.stats import norm
# x축 구간: -6에서 4.
xx = np.linspace(-6, 4, 101)
# y축 구간: 1에서 2
ss = np.linspace(1, 2, 101)
# 지정된 구간을 100x100 개의 격자로 쪼갬
XX, SS = np.meshgrid(xx, ss)
# 확률밀도 함숫(pdf)값 지정: 적절한 스케일 사용.
ZZ = 2 * norm.pdf(XX, 1, SS) + norm.pdf(XX, -4, SS)
ZZ = ZZ / ZZ.sum(axis=1)[:,np.newaxis] / (xx[1] - xx[0])
아래 코드는 책의 그림 9-20 (338쪽)을 그린다.
from matplotlib.patches import Polygon
plt.figure(figsize=(8, 4.5))
x_idx = 85 # x=2.5의 인덱스
s_idx = 30 # theta=1.3의 인덱스
# 좌상단 그림: 모델의 파라미터 함수 f(x;theta)
plt.subplot(221)
plt.contourf(XX, SS, ZZ, cmap="GnBu")
plt.plot([-6, 4], [ss[s_idx], ss[s_idx]], "k-", linewidth=2)
plt.plot([xx[x_idx], xx[x_idx]], [1, 2], "b-", linewidth=2)
plt.xlabel(r"$x$")
plt.ylabel(r"$\theta$", fontsize=14, rotation=0)
plt.title(r"Model $f(x; \theta)$", fontsize=14)
# 우상단 그림: x=2.5일 때의 가능도 함수 L(theta|x=2.5) = f(x=2.5;theta)
plt.subplot(222)
plt.plot(ss, ZZ[:, x_idx], "b-")
max_idx = np.argmax(ZZ[:, x_idx]) # MLE
max_val = np.max(ZZ[:, x_idx])
plt.plot(ss[max_idx], max_val, "r.") # MLE 표시(빨강 점)
plt.plot([ss[max_idx], ss[max_idx]], [0, max_val], "r:")
plt.plot([0, ss[max_idx]], [max_val, max_val], "r:")
plt.text(1.01, max_val + 0.005, r"$\hat{L}$", fontsize=14)
plt.text(ss[max_idx]+ 0.01, 0.055, r"$\hat{\theta}$", fontsize=14)
plt.text(ss[max_idx]+ 0.01, max_val - 0.012, r"$Max$", fontsize=12)
plt.axis([1, 2, 0.05, 0.15])
plt.xlabel(r"$\theta$", fontsize=14)
plt.grid(True)
plt.text(1.99, 0.135, r"$=f(x=2.5; \theta)$", fontsize=14, ha="right")
plt.title(r"Likelihood function $\mathcal{L}(\theta|x=2.5)$", fontsize=14)
# 좌하단 그림: theta=1.3일 때의 확률밀도함수(pdf) f(x;theta=1.3)
plt.subplot(223)
plt.plot(xx, ZZ[s_idx], "k-")
plt.axis([-6, 4, 0, 0.25])
plt.xlabel(r"$x$", fontsize=14)
plt.grid(True)
plt.title(r"PDF $f(x; \theta=1.3)$", fontsize=14)
verts = [(xx[41], 0)] + list(zip(xx[41:81], ZZ[s_idx, 41:81])) + [(xx[80], 0)]
poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
plt.gca().add_patch(poly)
# 우하단 그림: x=2.5일 때 로그 가능도 함수 log L(theta|x=2.5)
plt.subplot(224)
plt.plot(ss, np.log(ZZ[:, x_idx]), "b-")
max_idx = np.argmax(np.log(ZZ[:, x_idx]))
max_val = np.max(np.log(ZZ[:, x_idx]))
plt.plot(ss[max_idx], max_val, "r.")
plt.plot([ss[max_idx], ss[max_idx]], [-5, max_val], "r:")
plt.plot([0, ss[max_idx]], [max_val, max_val], "r:")
plt.axis([1, 2, -2.4, -2])
plt.xlabel(r"$\theta$", fontsize=14)
plt.text(ss[max_idx]+ 0.01, max_val - 0.05, r"$Max$", fontsize=12)
plt.text(ss[max_idx]+ 0.01, -2.39, r"$\hat{\theta}$", fontsize=14)
plt.text(1.01, max_val + 0.02, r"$\log \, \hat{L}$", fontsize=14)
plt.grid(True)
plt.title(r"$\log \, \mathcal{L}(\theta|x=2.5)$", fontsize=14)
save_fig("likelihood_function_plot")
plt.show()
Saving figure likelihood_function_plot
베이즈 가우시안 혼합 모델은 군집수를 미리 정하기 보다는 필요한 만큼의 군집만을
선택해서 사용한다.
사이킷런의 BayesianGaussianMixture
모델은 불필요한 군집에 0 또는 거의 0에
가까운 가중치(weights)를 가하는 방식으로 필요한 만큼의 군집만 사용한다.
from sklearn.mixture import BayesianGaussianMixture
모델을 생성할 때 군집수를 충분히 크다고 생각하는 수로 지정해야 한다.
bgm = BayesianGaussianMixture(n_components=10, n_init=10, random_state=42)
bgm.fit(X)
C:\Users\gslee\anaconda3\lib\site-packages\sklearn\mixture\_base.py:265: ConvergenceWarning: Initialization 10 did not converge. Try different init parameters, or increase max_iter, tol or check for degenerate data. warnings.warn('Initialization %d did not converge. '
BayesianGaussianMixture(n_components=10, n_init=10, random_state=42)
모델이 적절한 군집화를 완료하지 못했다는 경고가 뜨는 경우에는
초기화 횟수(n_init
) 또는 기댓값-최대화(EM) 알고리즘 반복 횟수(max_iter
)를
키운다.
max_iter
를 기본값인 100 대신 150을 사용한다.bgm = BayesianGaussianMixture(n_components=10, n_init=10, max_iter=150, random_state=42)
bgm.fit(X)
BayesianGaussianMixture(max_iter=150, n_components=10, n_init=10, random_state=42)
n_init
를 15로 지정한다.bgm = BayesianGaussianMixture(n_components=10, n_init=15, random_state=42)
bgm.fit(X)
BayesianGaussianMixture(n_components=10, n_init=15, random_state=42)
3개의 군집만 필요하단는 사실을 아래와 같이 확인한다. 또한 군집 크기의 비가 4:4:2로 제대로 예측되었다.
np.round(bgm.weights_, 2)
array([0. , 0.39, 0.2 , 0.4 , 0. , 0. , 0. , 0. , 0. , 0. ])
군집 결정경계는 다음과 같다.
plt.figure(figsize=(8, 5))
plot_gaussian_mixture(bgm, X)
plt.show()
사전 믿음(prior belief), 즉 군집수에 대한 사전 지식에 대한 믿음을
weight_concentration_prior
속성으로 지정할 수 있다.
0보다 큰 값을 사용하며 0에 가까울 수록 적은 군집수가 사용되었음을 암시한다.
지정하지 않으면 1/n_components
가 기본값으로 사용된다.
하지만 훈련세트가 크면 별 의미가 없으며, 아래와 같은 그림은 사전 믿음의 차이가
매우 크고 훈련세트가 작은 경우에만 그려진다.
다음 코드는 사전확률을 극단적으로 차이를 두어 두 모델을 생성한다. 또한 73개의 샘플만을 이용하여 훈련한다.
# 사전 믿음: 0.01
bgm_low = BayesianGaussianMixture(n_components=10, max_iter=1000, n_init=1,
weight_concentration_prior=0.01, random_state=42)
# 사전 믿음: 10,000
bgm_high = BayesianGaussianMixture(n_components=10, max_iter=1000, n_init=1,
weight_concentration_prior=10000, random_state=42)
# 훈련 세트 크기: 73
nn = 73
bgm_low.fit(X[:nn])
bgm_high.fit(X[:nn])
BayesianGaussianMixture(max_iter=1000, n_components=10, random_state=42, weight_concentration_prior=10000)
사전 믿음이 0.01인 경우 두 개의 군집만 찾는다.
np.round(bgm_low.weights_, 2)
array([0.49, 0.51, 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ])
사전 믿음이 10,000인 경우 세, 네의 군집을 찾는다.
np.round(bgm_high.weights_, 2)
array([0.43, 0.01, 0.01, 0.11, 0.01, 0.01, 0.01, 0.37, 0.01, 0.01])
plt.figure(figsize=(9, 4))
# 왼편 그림
plt.subplot(121)
plot_gaussian_mixture(bgm_low, X[:nn])
plt.title("weight_concentration_prior = 0.01", fontsize=14)
# 오른편 그림
plt.subplot(122)
plot_gaussian_mixture(bgm_high, X[:nn], show_ylabels=False)
plt.title("weight_concentration_prior = 10000", fontsize=14)
save_fig("mixture_concentration_prior_plot")
plt.show()
Saving figure mixture_concentration_prior_plot
주의사항: 실행결과가 책과 다르다. 또한 훈련세트 크기를 조금만 다르게 해도 전혀 다른 결과를 얻는다. 이는 버그가 아니며 앞서 언급한 대로 사전 믿음이 주는 불안정성을 반영할 뿐이다.
가우시안 혼합 모델은 데이터셋을 타원 형탱의 군집으로 분할하려 한다. 따라서 moons 데이터셋과 같은 경우 제대로된 군집화를 진행하지 못한다.
X_moons, y_moons = make_moons(n_samples=1000, noise=0.05, random_state=42)
bgm = BayesianGaussianMixture(n_components=10, n_init=10, random_state=42)
bgm.fit(X_moons)
BayesianGaussianMixture(n_components=10, n_init=10, random_state=42)
아래 코드는 moons 데이터셋에 베이즈 가우시안 혼합 모델을 적용하였더니 8개의 군집을 찾는 것을 보여준다. 하지만 밀도 고등선은 이상치 탐지에 활용되기에는 충분하다.
plt.figure(figsize=(9, 3.2))
plt.subplot(121)
plot_data(X_moons)
plt.xlabel("$x_1$", fontsize=14)
plt.ylabel("$x_2$", fontsize=14, rotation=0)
plt.subplot(122)
plot_gaussian_mixture(bgm, X_moons, show_ylabels=False)
save_fig("moons_vs_bgm_plot")
plt.show()
Saving figure moons_vs_bgm_plot
부록 A 참조
Exercise: The classic Olivetti faces dataset contains 400 grayscale 64 × 64–pixel images of faces. Each image is flattened to a 1D vector of size 4,096. 40 different people were photographed (10 times each), and the usual task is to train a model that can predict which person is represented in each picture. Load the dataset using the sklearn.datasets.fetch_olivetti_faces()
function.
from sklearn.datasets import fetch_olivetti_faces
olivetti = fetch_olivetti_faces()
print(olivetti.DESCR)
.. _olivetti_faces_dataset: The Olivetti faces dataset -------------------------- `This dataset contains a set of face images`_ taken between April 1992 and April 1994 at AT&T Laboratories Cambridge. The :func:`sklearn.datasets.fetch_olivetti_faces` function is the data fetching / caching function that downloads the data archive from AT&T. .. _This dataset contains a set of face images: http://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html As described on the original website: There are ten different images of each of 40 distinct subjects. For some subjects, the images were taken at different times, varying the lighting, facial expressions (open / closed eyes, smiling / not smiling) and facial details (glasses / no glasses). All the images were taken against a dark homogeneous background with the subjects in an upright, frontal position (with tolerance for some side movement). **Data Set Characteristics:** ================= ===================== Classes 40 Samples total 400 Dimensionality 4096 Features real, between 0 and 1 ================= ===================== The image is quantized to 256 grey levels and stored as unsigned 8-bit integers; the loader will convert these to floating point values on the interval [0, 1], which are easier to work with for many algorithms. The "target" for this database is an integer from 0 to 39 indicating the identity of the person pictured; however, with only 10 examples per class, this relatively small dataset is more interesting from an unsupervised or semi-supervised perspective. The original dataset consisted of 92 x 112, while the version available here consists of 64x64 images. When using these images, please give credit to AT&T Laboratories Cambridge.
olivetti.target
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39])
Exercise: Then split it into a training set, a validation set, and a test set (note that the dataset is already scaled between 0 and 1). Since the dataset is quite small, you probably want to use stratified sampling to ensure that there are the same number of images per person in each set.
from sklearn.model_selection import StratifiedShuffleSplit
strat_split = StratifiedShuffleSplit(n_splits=1, test_size=40, random_state=42)
train_valid_idx, test_idx = next(strat_split.split(olivetti.data, olivetti.target))
X_train_valid = olivetti.data[train_valid_idx]
y_train_valid = olivetti.target[train_valid_idx]
X_test = olivetti.data[test_idx]
y_test = olivetti.target[test_idx]
strat_split = StratifiedShuffleSplit(n_splits=1, test_size=80, random_state=43)
train_idx, valid_idx = next(strat_split.split(X_train_valid, y_train_valid))
X_train = X_train_valid[train_idx]
y_train = y_train_valid[train_idx]
X_valid = X_train_valid[valid_idx]
y_valid = y_train_valid[valid_idx]
print(X_train.shape, y_train.shape)
print(X_valid.shape, y_valid.shape)
print(X_test.shape, y_test.shape)
(280, 4096) (280,) (80, 4096) (80,) (40, 4096) (40,)
To speed things up, we'll reduce the data's dimensionality using PCA:
from sklearn.decomposition import PCA
pca = PCA(0.99)
X_train_pca = pca.fit_transform(X_train)
X_valid_pca = pca.transform(X_valid)
X_test_pca = pca.transform(X_test)
pca.n_components_
199
Exercise: Next, cluster the images using K-Means, and ensure that you have a good number of clusters (using one of the techniques discussed in this chapter).
from sklearn.cluster import KMeans
k_range = range(5, 150, 5)
kmeans_per_k = []
for k in k_range:
print("k={}".format(k))
kmeans = KMeans(n_clusters=k, random_state=42).fit(X_train_pca)
kmeans_per_k.append(kmeans)
k=5 k=10 k=15 k=20 k=25 k=30 k=35 k=40 k=45 k=50 k=55 k=60 k=65 k=70 k=75 k=80 k=85 k=90 k=95 k=100 k=105 k=110 k=115 k=120 k=125 k=130 k=135 k=140 k=145
from sklearn.metrics import silhouette_score
silhouette_scores = [silhouette_score(X_train_pca, model.labels_)
for model in kmeans_per_k]
best_index = np.argmax(silhouette_scores)
best_k = k_range[best_index]
best_score = silhouette_scores[best_index]
plt.figure(figsize=(8, 3))
plt.plot(k_range, silhouette_scores, "bo-")
plt.xlabel("$k$", fontsize=14)
plt.ylabel("Silhouette score", fontsize=14)
plt.plot(best_k, best_score, "rs")
plt.show()
best_k
100
It looks like the best number of clusters is quite high, at 120. You might have expected it to be 40, since there are 40 different people on the pictures. However, the same person may look quite different on different pictures (e.g., with or without glasses, or simply shifted left or right).
inertias = [model.inertia_ for model in kmeans_per_k]
best_inertia = inertias[best_index]
plt.figure(figsize=(8, 3.5))
plt.plot(k_range, inertias, "bo-")
plt.xlabel("$k$", fontsize=14)
plt.ylabel("Inertia", fontsize=14)
plt.plot(best_k, best_inertia, "rs")
plt.show()
The optimal number of clusters is not clear on this inertia diagram, as there is no obvious elbow, so let's stick with k=100.
best_model = kmeans_per_k[best_index]
Exercise: Visualize the clusters: do you see similar faces in each cluster?
def plot_faces(faces, labels, n_cols=5):
n_rows = (len(faces) - 1) // n_cols + 1
plt.figure(figsize=(n_cols, n_rows * 1.1))
for index, (face, label) in enumerate(zip(faces, labels)):
plt.subplot(n_rows, n_cols, index + 1)
plt.imshow(face.reshape(64, 64), cmap="gray")
plt.axis("off")
plt.title(label)
plt.show()
for cluster_id in np.unique(best_model.labels_):
print("Cluster", cluster_id)
in_cluster = best_model.labels_==cluster_id
faces = X_train[in_cluster].reshape(-1, 64, 64)
labels = y_train[in_cluster]
plot_faces(faces, labels)
Cluster 0
Cluster 1
Cluster 2
Cluster 3
Cluster 4
Cluster 5
Cluster 6
Cluster 7
Cluster 8
Cluster 9
Cluster 10
Cluster 11
Cluster 12
Cluster 13
Cluster 14
Cluster 15
Cluster 16
Cluster 17
Cluster 18
Cluster 19
Cluster 20
Cluster 21
Cluster 22
Cluster 23
Cluster 24
Cluster 25
Cluster 26
Cluster 27
Cluster 28
Cluster 29
Cluster 30
Cluster 31
Cluster 32
Cluster 33
Cluster 34
Cluster 35
Cluster 36
Cluster 37
Cluster 38
Cluster 39
Cluster 40
Cluster 41
Cluster 42
Cluster 43
Cluster 44
Cluster 45
Cluster 46
Cluster 47
Cluster 48
Cluster 49
Cluster 50
Cluster 51
Cluster 52
Cluster 53
Cluster 54
Cluster 55
Cluster 56
Cluster 57
Cluster 58
Cluster 59
Cluster 60
Cluster 61
Cluster 62
Cluster 63
Cluster 64
Cluster 65
Cluster 66
Cluster 67
Cluster 68
Cluster 69
Cluster 70
Cluster 71
Cluster 72
Cluster 73
Cluster 74
Cluster 75
Cluster 76
Cluster 77
Cluster 78
Cluster 79
Cluster 80
Cluster 81
Cluster 82
Cluster 83
Cluster 84
Cluster 85
Cluster 86
Cluster 87
Cluster 88
Cluster 89
Cluster 90
Cluster 91
Cluster 92
Cluster 93
Cluster 94
Cluster 95
Cluster 96
Cluster 97
Cluster 98
Cluster 99
About 2 out of 3 clusters are useful: that is, they contain at least 2 pictures, all of the same person. However, the rest of the clusters have either one or more intruders, or they have just a single picture.
Clustering images this way may be too imprecise to be directly useful when training a model (as we will see below), but it can be tremendously useful when labeling images in a new dataset: it will usually make labelling much faster.
Exercise: Continuing with the Olivetti faces dataset, train a classifier to predict which person is represented in each picture, and evaluate it on the validation set.
from sklearn.ensemble import RandomForestClassifier
clf = RandomForestClassifier(n_estimators=150, random_state=42)
clf.fit(X_train_pca, y_train)
clf.score(X_valid_pca, y_valid)
0.9
Exercise: Next, use K-Means as a dimensionality reduction tool, and train a classifier on the reduced set.
X_train_reduced = best_model.transform(X_train_pca)
X_valid_reduced = best_model.transform(X_valid_pca)
X_test_reduced = best_model.transform(X_test_pca)
clf = RandomForestClassifier(n_estimators=150, random_state=42)
clf.fit(X_train_reduced, y_train)
clf.score(X_valid_reduced, y_valid)
0.75
Yikes! That's not better at all! Let's see if tuning the number of clusters helps.
Exercise: Search for the number of clusters that allows the classifier to get the best performance: what performance can you reach?
We could use a GridSearchCV
like we did earlier in this notebook, but since we already have a validation set, we don't need K-fold cross-validation, and we're only exploring a single hyperparameter, so it's simpler to just run a loop manually:
from sklearn.pipeline import Pipeline
for n_clusters in k_range:
pipeline = Pipeline([
("kmeans", KMeans(n_clusters=n_clusters, random_state=42)),
("forest_clf", RandomForestClassifier(n_estimators=150, random_state=42))
])
pipeline.fit(X_train_pca, y_train)
print(n_clusters, pipeline.score(X_valid_pca, y_valid))
5 0.4125 10 0.525 15 0.5375 20 0.6375 25 0.65 30 0.6375 35 0.675 40 0.7375 45 0.725 50 0.75 55 0.7375 60 0.725 65 0.7375 70 0.725 75 0.725 80 0.775 85 0.7375 90 0.7375 95 0.75 100 0.75 105 0.75 110 0.7375 115 0.7375 120 0.75 125 0.75 130 0.725 135 0.75 140 0.7625 145 0.7375
Oh well, even by tuning the number of clusters, we never get beyond 80% accuracy. Looks like the distances to the cluster centroids are not as informative as the original images.
Exercise: What if you append the features from the reduced set to the original features (again, searching for the best number of clusters)?
X_train_extended = np.c_[X_train_pca, X_train_reduced]
X_valid_extended = np.c_[X_valid_pca, X_valid_reduced]
X_test_extended = np.c_[X_test_pca, X_test_reduced]
clf = RandomForestClassifier(n_estimators=150, random_state=42)
clf.fit(X_train_extended, y_train)
clf.score(X_valid_extended, y_valid)
0.825
That's a bit better, but still worse than without the cluster features. The clusters are not useful to directly train a classifier in this case (but they can still help when labelling new training instances).
Exercise: Train a Gaussian mixture model on the Olivetti faces dataset. To speed up the algorithm, you should probably reduce the dataset's dimensionality (e.g., use PCA, preserving 99% of the variance).
from sklearn.mixture import GaussianMixture
gm = GaussianMixture(n_components=40, random_state=42)
y_pred = gm.fit_predict(X_train_pca)
Exercise: Use the model to generate some new faces (using the sample()
method), and visualize them (if you used PCA, you will need to use its inverse_transform()
method).
n_gen_faces = 20
gen_faces_reduced, y_gen_faces = gm.sample(n_samples=n_gen_faces)
gen_faces = pca.inverse_transform(gen_faces_reduced)
plot_faces(gen_faces, y_gen_faces)
Exercise: Try to modify some images (e.g., rotate, flip, darken) and see if the model can detect the anomalies (i.e., compare the output of the score_samples()
method for normal images and for anomalies).
n_rotated = 4
rotated = np.transpose(X_train[:n_rotated].reshape(-1, 64, 64), axes=[0, 2, 1])
rotated = rotated.reshape(-1, 64*64)
y_rotated = y_train[:n_rotated]
n_flipped = 3
flipped = X_train[:n_flipped].reshape(-1, 64, 64)[:, ::-1]
flipped = flipped.reshape(-1, 64*64)
y_flipped = y_train[:n_flipped]
n_darkened = 3
darkened = X_train[:n_darkened].copy()
darkened[:, 1:-1] *= 0.3
darkened = darkened.reshape(-1, 64*64)
y_darkened = y_train[:n_darkened]
X_bad_faces = np.r_[rotated, flipped, darkened]
y_bad = np.concatenate([y_rotated, y_flipped, y_darkened])
plot_faces(X_bad_faces, y_bad)
X_bad_faces_pca = pca.transform(X_bad_faces)
gm.score_samples(X_bad_faces_pca)
array([-1.79997468e+07, -2.26427421e+07, -3.96415646e+07, -4.60254380e+07, -3.13975227e+07, -1.39330251e+07, -2.90882963e+07, -1.06298693e+08, -1.20931144e+08, -7.49740718e+07])
The bad faces are all considered highly unlikely by the Gaussian Mixture model. Compare this to the scores of some training instances:
gm.score_samples(X_train_pca[:10])
array([1163.02020938, 1149.16682072, 1148.47710555, 1170.67602773, 1088.46009527, 1075.7170049 , 1075.71700925, 1088.46008902, 1096.42609678, 1119.68627018])
Exercise: Some dimensionality reduction techniques can also be used for anomaly detection. For example, take the Olivetti faces dataset and reduce it with PCA, preserving 99% of the variance. Then compute the reconstruction error for each image. Next, take some of the modified images you built in the previous exercise, and look at their reconstruction error: notice how much larger the reconstruction error is. If you plot a reconstructed image, you will see why: it tries to reconstruct a normal face.
We already reduced the dataset using PCA earlier:
X_train_pca
array([[ 3.7807992e+00, -1.8547927e+00, -5.1440420e+00, ..., -1.3563001e-01, -2.1408510e-01, 6.1194517e-02], [ 1.0148863e+01, -1.5275445e+00, -7.6698363e-01, ..., 1.2393168e-01, -1.3526660e-01, -2.3265788e-02], [-1.0015284e+01, 2.8772824e+00, -9.1987586e-01, ..., 7.2610505e-02, -2.9626514e-03, 1.2489169e-01], ..., [ 2.4758759e+00, 2.9559698e+00, 1.2998563e+00, ..., -2.0908976e-02, 3.4845721e-02, -1.5432714e-01], [-3.2203169e+00, 5.3489785e+00, 1.3942686e+00, ..., 5.7551935e-02, -2.2830766e-01, 1.5557502e-01], [-9.2287689e-01, -3.6470294e+00, 2.2608802e+00, ..., 1.3684936e-01, -6.9123939e-02, 6.2689997e-02]], dtype=float32)
def reconstruction_errors(pca, X):
X_pca = pca.transform(X)
X_reconstructed = pca.inverse_transform(X_pca)
mse = np.square(X_reconstructed - X).mean(axis=-1)
return mse
reconstruction_errors(pca, X_train).mean()
0.00019205351
reconstruction_errors(pca, X_bad_faces).mean()
0.004707354
plot_faces(X_bad_faces, y_gen_faces)
X_bad_faces_reconstructed = pca.inverse_transform(X_bad_faces_pca)
plot_faces(X_bad_faces_reconstructed, y_gen_faces)