LaTeX new commands defined for this notebook: $ \newcommand{\bx}{\boldsymbol{x}} \newcommand{\br}{\boldsymbol{r}} \newcommand{\bmu}{\boldsymbol{\mu}} \newcommand{\bzero}{\boldsymbol{0}} $
\bx
$\bx$,\br
$\br$,\bmu
$\bmu$,\bzero
$\bzero$
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
mpl.style.use('seaborn-white')
올드페이스풀(Old Faithful)은 미국 와이오밍 주 옐로스톤 국립공원 안에 있는 아주 유명한 간헐천(geyser)이다. 총 272개의 관측 데이터로 이루어져 있는 올드페이스풀 데이터(download)는 두 가지 컬럼으로 구성되어 있는데, 첫 번째는 분출 지속시간이고 두 번째는 다음 분출까지의 시간이다. 모든 데이터의 시간 단위는 분(min)이다. 그림 A.5는 분출 지속시간과 다음 분출까지의 시간을 각각 $x$축과 $y$축으로 표시한 것이다.
from pandas import read_csv
data = read_csv('data/Old_Faithful.csv', header=None).values
from sklearn.preprocessing import scale
data_scaled = scale(data)
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8.5,4))
kwargs = dict(markersize=5, markerfacecolor='none')
# *data.T extracts data[:,0], data[:,1]
ax1.plot(*data.T, 'o', markeredgecolor='C2', **kwargs)
ax1.set(xlabel='Duration of eruption (min)', ylabel='Time to the next eruption (min)',
xlim=(1, 6), ylim=(40, 100), title='Old Faithful data set')
ax2.plot(*data_scaled.T, 'o', markeredgecolor='C4', **kwargs)
ax2.set(aspect='equal', xticks=np.arange(-2, 2.1), yticks=np.arange(-2, 2.1),
xlim=(-2, 2), ylim=(-2, 2), title='Normalized Old Faithful data set');
Figure A.5 Plot of the time to the next eruption in minutes (vertical axis) versus the duration of the eruption in minutes (horizontal axis) for the Old Faithful data set.
$D$차원 실수값을 가지는 확률변수 $\bx$에 대해 $N$개의 관측 데이터 $\{\bx_1,\dotsc,\bx_N\}$가 주어졌을 때, 이 데이터를 $K$개의 클러스터로 나누고자 한다. (단, 클러스터의 수 $K$는 미리 주어졌다고 하자.)
$K$-평균 알고리즘은 목적함수 $J$를 최소화하는 클러스터 라벨 $\{\br_n\}_{n=1}^N$과 클러스터 센터 $\{\bmu_k\}_{k=1}^K$를 찾아 준다.
먼저 임의로 클러스터 센터 $\{\bmu_k\}$를 선택한다.
(E step) 첫 번째 단계에서는 센터 $\{\bmu_k\}$를 고정한 상태에서 $J$를 최소화하는 라벨 $\{\br_n\}$을 찾는다.
(M step) 두 번째 단계에서는 라벨 $\{\br_n\}$을 고정한 상태에서 $J$를 최소화하는 센터 $\{\bmu_k\}$를 찾는다.
위의 두 단계를 반복해서 라벨과 센터를 찾아간다. 더 이상 라벨과 센터가 변하지 않거나 정해진 반복횟수를 넘게 되면 멈춘다.
$K$-평균 알고리즘에서 목적함수 $J$는 특정한 극솟값으로 수렴하지만 이 값이 최솟값이라는 보장은 없다.
# K-means clustering algorithm (function)
# http://scikit-learn.org/stable/modules/generated/sklearn.cluster.k_means.html
from sklearn.cluster import k_means
# Row-wise (squared) Euclidean norm of X
# https://github.com/scikit-learn/scikit-learn/blob/0.19.2/sklearn/utils/extmath.py#L55
from sklearn.utils.extmath import row_norms
def k_means_one_step(X, centers, n_clusters=2):
# E step of the K-means EM algorithm
# https://github.com/scikit-learn/scikit-learn/blob/0.19.2/sklearn/cluster/k_means_.py#L573
from sklearn.cluster.k_means_ import _labels_inertia
# M step of the K-means EM algorithm
# https://github.com/scikit-learn/scikit-learn/blob/0.19.2/sklearn/cluster/_k_means.pyx#L273
from sklearn.cluster._k_means import _centers_dense
# These arguments must be allocated before sending to _labels_inertia
sample_weight = np.ones(X.shape[0], dtype=X.dtype)
x_squared_norms = row_norms(X, squared=True)
distances = np.zeros(shape=(X.shape[0],), dtype=X.dtype)
from distutils.version import StrictVersion
from sklearn import __version__ as sklearn_version
if StrictVersion(sklearn_version) < "0.19.2":
# E step of the K-means EM algorithm
labels_, inertia_ = _labels_inertia(X,
x_squared_norms, centers, precompute_distances=False, distances=distances)
# M step of the K-means EM algorithm
centers_new = _centers_dense(X, labels_, n_clusters, distances)
else:
# E step of the K-means EM algorithm
labels_, inertia_ = _labels_inertia(X, sample_weight,
x_squared_norms, centers, precompute_distances=False, distances=distances)
# M step of the K-means EM algorithm
centers_new = _centers_dense(X, sample_weight, labels_, n_clusters, distances)
return centers_new, labels_, inertia_
def compute_inertia(X, labels, centers, n_clusters=2):
from functools import reduce
return reduce(np.add, # generator is a little bit faster than list comprehension
(np.sum(row_norms(X[labels == i] - centers[i], squared=True)) for i in range(n_clusters)))
def draw_scatter(ax, X, label, centers, title=None, bisector=True):
kwargs = dict(markersize=5, markerfacecolor='none', alpha=0.5)
if label is None:
ax.plot(*X.T, 'o', markeredgecolor='C2', **kwargs)
else:
ax.plot(*X[labels == 0].T, 'o', markeredgecolor='C0', **kwargs)
ax.plot(*X[labels == 1].T, 'o', markeredgecolor='C3', **kwargs)
if bisector:
centers_mid = centers.mean(axis=0)
bisector_l = lambda x: centers_mid[1] - (x-centers_mid[0]) * \
(centers[1,0]-centers[0,0]) / (centers[1,1]-centers[0,1])
ax.plot([-2, 2], [bisector_l(-2), bisector_l(2)], color='C4', alpha=0.5)
ax.scatter(*centers.T, s=100, c=['C0', 'C3'], marker='x')
ax.set(aspect='equal', xlim=(-2, 2), ylim=(-2, 2), xticks=[-2, 0, 2], yticks=[-2, 0, 2],
title=title)
fig = plt.figure(figsize=(14,7))
num = 4
ax1 = fig.add_subplot(2, num, 1)
ax2 = fig.add_subplot(2, num, num+2)
ax3 = fig.add_subplot(2, num, 2*num)
iax = [fig.add_subplot(2, num, i+2) for i in range(num)]
centers_init = np.asarray([[-1.5, 1], [1.5, -1]])
draw_scatter(ax1, data_scaled, None, centers_init, title='Old Faithful data set')
inertia_l = []
centers_old = centers_init
for i in range(num):
centers, labels, inertia = k_means_one_step(data_scaled, centers_old)
# Append the inertia of E step (according to centers_old & labels)
inertia_l.append(inertia)
# Append the inertia of M step (according to centers_new & labels)
inertia_l.append(compute_inertia(data_scaled, labels, centers))
draw_scatter(iax[i], data_scaled, labels, centers_old, title='$n={}$'.format(i+1))
centers_old = centers
# The classical EM-style algorithm='full' (not default) and max_iter=300 (default)
# If init is given as an ndarray, n_init=1 will be set automatically.
# To avoid RuntimeWarning, n_init=1 is given explicitly.
centers, labels, inertia = k_means(data_scaled, 2, init=centers_init, n_init=1, algorithm='full')
draw_scatter(ax2, data_scaled, labels, centers, title='Final convergence of $K$-means', bisector=False)
ax3.plot(np.arange(1, num+1, 0.5), inertia_l, color='C2')
ax3.scatter(np.arange(1, num+1, 0.5), inertia_l, s=100, c='none', edgecolors=['C0', 'C3'], marker='o')
ax3.set(xlabel='$n$', ylabel='$J$', xticks=np.arange(1, num+1), yticks=[0, 500, 1000],
title='Plot of the cost function $J$');
Figure 9.1 Illustration of the $K$-means algorithm using the re-scaled Old Faithful data set.
Figure 9.2 Plot of the cost function $J$ given by (9.1) after each E step (blue points; old centers and new labels) and M step (red points; new labels and new centers) of the $K$-means algorithm for the example shown in Figure 9.1.
$K$-평균 알고리즘은 손실(lossy) 데이터 압축을 하는데 사용할 수 있다.
주어진 $N$개의 데이터를 $k$개의 클러스터로 분류한 후, 원래의 데이터 대신 $N$개의 클러스터 라벨과 $K$개의 클러스터 센터만 저장한다. 만약 $K\ll N$이면 데이터 저장 공간을 상당히 아끼게 된다.
각 데이터는 가장 가까운 클러스터 센터로 압축된다고 볼 수 있다. 새로운 데이터가 주어지면, 가장 가까운 클러스터 센터 $\bmu_k$를 찾은 다음 그에 해당하는 클러스터 라벨 $k$만 저장한다.
이러한 기법을 벡터 양자화(vector quantization)라 부르고, 특히 클러스터 센터를 코드북(code-book) 벡터라 부른다.
# K-means clustering algorithm (function)
# http://scikit-learn.org/stable/modules/generated/sklearn.cluster.k_means.html
from sklearn.cluster import k_means
# Reads an image from the specified file. Returns a numpy array.
# https://imageio.readthedocs.io/en/stable/userapi.html#imageio.imread
from imageio import imread
def vector_quantization(fname, cluster_l):
image = imread(fname) # support PNG only
channels = image.shape[-1]
fig, iax = plt.subplots(ncols=len(cluster_l)+1, figsize=(2.5*len(cluster_l)+6,4))
iax[0].imshow(image)
iax[0].set_title('Original image')
data = image.reshape((-1, channels))
for i, k in enumerate(cluster_l):
centers, labels, _ = k_means(data, k)
# NOTE: numpy.choose only work for k < 32; a restriction on the size of choices
# data_n = np.choose(np.tile(labels, (channels,1)).T, centers)
# To avoid this restriction, we use a direct way
data_n = np.zeros_like(data)
for j in range(data_n.shape[0]):
data_n[j] = centers[labels[j]]
iax[i+1].imshow(data_n.reshape(image.shape))
iax[i+1].set_title('$K={}$'.format(k))
for ax in iax:
ax.set_axis_off()
%time vector_quantization('data/originalA.png', [2, 3, 10, 50])
%time vector_quantization('data/originalB.png', [2, 3, 10, 50])
Figure 9.3 Two examples of the application of the $K$-means clustering algorithm to image segmentation showing the initial images together with their $K$-means segmentations obtained using various values of $K$. This also illustrates of the use of vector quantization for data compression, in which smaller values of $K$ give higher compression at the expense of poorer image quality.
원본 이미지가 $N$개의 픽셀로 이루어져 있고, 각 픽셀은 8비트 {R, G, B} 색상으로 표현된다면, 전체 이미지를 전송하는데 $24N$ 비트가 필요하다. 그림 9.3의 원본 이미지는 $N=240\times180=43,200$ 픽셀로 이루어져 있다.
원본 이미지에 대해 $K$-평균 알고리즘을 적용한 후, 원본 이미지 전체를 전송하는 대신 라벨만을 전송한다고 하자. 이 경우 픽셀 당 $\log_2 K$ 비트가 든다. 또한 $K$개의 센터를 전송하는데 $24K$ 비트가 필요하므로, 이미지를 전송하는데 드는 비트의 수는 모두 $24K + N \log_2 K$ (rounding up to the nearest integer) 이다.
그림 9.3의 경우, 압축 이미지를 전송하는데 43,248 비트 ($K=2$), 68,542 비트 ($K=3$), 143,747 비트 ($K=10$), 245,015 비트 ($K=50$) 가 필요하다. 원본이미지 대비 압축률은 각각 4.2% ($K=2$), 6.6% ($K=3$), 13.9% ($K=10$), 23.6% ($K=50$) 이다.
압축 수준과 이미지 품질 사이에는 트레이드오프(trade-off)가 있다. 만약 좋은 품질의 압축을 원한다면, 픽셀 단위로 $K$-평균 알고리즘을 적용하는 대신 인접한 픽셀들로 이루어진 작은 블록 (예를 들어 $5\times5$) 단위로 $K$-평균 알고리즘을 적용하는 것을 고려해 보자. 자연스런 이미지에 나타나는 인접한 픽셀들 사이의 연관성을 살릴 수 있다.