갈루아의 반서재

비지도 학습 (2) - k평균으로 손글씨 숫자 군집화


k 평균


k평균은 데이터 점을 뚜렷한 그룹인 군집으로 분할하는 분할 알고리즘에 속한다.  k평균의 주요개념은 군집 내 평균과 군집내 점들의 제곱 거리를 최소로 만들기 위해 각 점들의 군집을 찾는 것이다. 이 기법은 나누고자 하는 군집의 개수를 미리 알고 있다고 가정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> %matplotlib inline
>>> 
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> 
>>> from sklearn.datasets import load_digits
>>> from sklearn.preprocessing import scale
>>> digits = load_digits()
>>> data = scale(digits.data)
>>> 
>>> def print_digits(images,y,max_n=10):
>>>     fig = plt.figure(figsize=(1212))
>>>     fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
>>>     i = 0
>>>     while i <max_n and i <images.shape[0]:
>>>         p = fig.add_subplot(2020, i + 1, xticks=[], yticks=[])
>>>         p.imshow(images[i], cmap=plt.cm.bone)
>>>         p.text(014str(y[i]))
>>>         i = i + 1
>>> 
>>> print_digits(digits.images, digits.target, max_n=10)
cs

 sklearn.preprocessing.scale(X, axis=0, with_mean=True, with_std=True, copy=True)
    특정 축을 따라 표준화시키는 것. 평균으로 중심을 가져가고 구성요소별로 단위 변화에 맞게 조절하는 것     http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.scale.html#sklearn.preprocessing.scale

subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=None)
    Subplot 파라메터 업데이트 http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.subplots_adjust        

 left

 The left side of the subplots of the figure

 right

 The right side of the subplots of the figure

 bottom

 The bottom of the subplots of the figure

 top

 The top of the subplots of the figure

 wspace

 The amount of width reserved for blank space between subplots

 hspace

 The amount of height reserved for white space between subplots


add_subplot
참조 링크 https://plot.ly/matplotlib/subplots/


출력된 숫자는 다음과 같다.

목적범주가 부여된 숫자가 포함된 데이터셋을 볼 수 있지만, 평가할 때까지 이 정보를 사용하지 않는다. 숫자의 유사성을 기반으로 숫자를 모을 수 있다면 예상대로 10개 군집을 형성할 것이다.

다음과 같이 훈련데이터와 테스트데이터로 나눈다.

1
2
3
4
5
6
>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test, images_train, images_test = train_test_split(data, digits.target, digits.images, test_size=0.25, random_state=42)
>>>
>>> n_samples, n_features = X_train.shape
>>> n_digits = len(np.unique(y_train))
>>> labels = y_train
cs


k 평균 알고리즘의 절차


1. 무작위로 군집 중앙점의 초기 집합을 선택한다.

2. 각 데이터의 점은 가장 가까운 군집 중앙점을 찾고 그 군집이 데이터점의 군집이 된다.

3. 군집의 데이터점의 평균인 새로운 군집 중앙점을 구하고 군집 원소가 안정화될 때까지 계속한다. 즉 각 반복에서 데이터 점이 군집을 변경하지 않을 때까지 계속한다.


일반적인 접근법은 초기 집합인 군집 중앙점 간의 제곱 거리의 합을 최소화하는 초기 집합을 선택한다(scikit-learn에서 n-init 매개변수는 알고리즘에서 시도할 다른 초기 군집 중앙점을 설정할 수 있다). 예제를 통해 살펴보자.

1
2
3
4
5
6
7
>>> from sklearn import cluster
>>> clf = cluster.KMeans(init='k-means++', n_clusters=10, random_state=42)
>>> clf.fit(X_train)
>>>
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=10, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=42, tol=0.0001, verbose=0)
cs

fit 메소드에 훈련 데이터만 입력하며 군집의 개수를 명시해야 한다. 에제에서 숫자의 군집 개수를 알고 있기 때문에 군집개수는 이미 알고 있다. 분류기의 labels_ 속성의 값을 출력하면 각 훈련 인스턴스에 연관된 군집 번호의 리스트를 얻을 수 있다.

class sklearn.cluster.KMeans(n_clusters=8, init='k-means++', n_init=10, max_iter=300, tol=0.0001, precompute_distances='auto', verbose=0, random_state=None, copy_x=True, n_jobs=1, algorithm='auto')

http://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html#sklearn.cluster.KMeans

n_clusters : 기본값은 8이다. 군집의 갯수 설정.
init : 초기화 메소드로 기본값은 ‘k-means++’ 이다.

‘k-means++’ : selects initial cluster centers for k-mean clustering in a smart way to speed up convergence. ‘random’: choose k observations (rows) at random from data for the initial centroids. If an ndarray is passed, it should be of shape (n_clusters, n_features) and gives the initial centers.

n_init : 서로 다른 중심점에서 k-means 알고리즘이 실행되는 횟수로, 기본값은 10이다. 관성의 관점에서 n_init 번 연속 실행 결과에서 가장 좋은 결과가 최종결과가 된다.

fit(X, y=None) : k-means 군집 계산

predict(X) : X가 속한 각각의 샘플 중 가장 근접한 군집 예측.

군집 번호는 실제 숫자와 상관이 없으며 분류하기 위해 범주를 사용하지 않는 점을 기억해야 한다. 이미지는 속성의 유사성으로 모였다. 테스트데이터를 통해 살펴보자. 훈련 데이터의 군집을 예측하기 위해 분류기의 predict 메소드를 사용한다.

1
>>> y_pred=clf.predict(X_test)

cs

군집이 어떤지 살펴보자.

1
2
3
4
5
6
>>> def print_cluster(images, y_pred, cluster_number):
>>>      images = images[y_pred==cluster_number]
>>>      y_pred = y_pred[y_pred==cluster_number]
>>>      print_digits(images, y_pred,max_n=10)
>>> for i in range(10):
>>>      print_cluster(images_test, y_pred, i)
cs

일부 군집은 매우 잘 구별되었다. 군집번호 2는 숫자 0에 해당한다. 군집번호 7은 어떠한가? 이는 뚜렷하지 않다. 이미지를 잘 분별하지 못했음을 알 수 있다.


성능평가


비교할 목적 범주가 없기 때문에 정확률과 같은 평가지표들은 사용할 수 없다. 평가하기 위해 무엇을 의미하든 간에 '실제' 군집을 알 필요가 있다. 예를 들어 각 군집은 숫자가 적혀 있는 인스턴스를 포함한다. 이를 알기 때문에, 군집과 예측한 군집 간의 Adjusted Rand Index를 계산할 수 있다. Rand Index 는 정확도에 대한 유사도 측정이다. 그러나 범주는 부여된 다른 이름을 가진다. 즉 범주 이름은 변경되어도 색인은 변하지 않는다. 어드저스티드 인덱스는 우연히 일치된 결과를 제거할 수 있다. 두 집합이 정확히 같다면 랜드 인덱스는 1이고, 같은 군집이 공유한 인스턴스가 없으면 0이 된다.

1
2
3
4
>>> from sklearn import metrics
>>> print "Adjusted rand score: {:.2}".format(metrics.adjusted_rand_score(y_test, y_pred))
>>>
Adjusted rand score: 0.57
cs

다음과 같이 혼돈 매트릭스를 출력할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> print metrics.confusion_matrix(y_test, y_pred)
>>>
[[ 0  0 43  0  0  0  0  0  0  0]
 [20  0  0  7  0  0  0 10  0  0]
 [ 5  0  0 31  0  0  0  1  1  0]
 [ 1  0  0  1  0  1  4  0 39  0]
 [ 1 50  0  0  0  0  1  2  0  1]
 [ 1  0  0  0  1 41  0  0 16  0]
 [ 0  0  1  0 44  0  0  0  0  0]
 [ 0  0  0  0  0  1 34  1  0  5]
 [21  0  0  0  0  3  1  2 11  0]
 [ 0  0  0  0  0  2  3  3 40  0]]
 
cs

테스트데이터의 숫자 0은 완벽하게 범주 2로 부여되었다. 숫자 8이 문제가 된다. 21개 인스턴스는 범주 0으로, 11개는 범주 8로 부여되었다. 어쨌든 결과는 좋지 않다.

k 군집이 어떻게 생겼는지 그래프로 보고자 한다면, 2차원으로 군집들을 그릴 수 있다. 점을 meshgrid로 도식화하고 (차원 축소후에) 부여된 군집을 계산한 후 도식화한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> from sklearn import decomposition
>>> pca = decomposition.PCA(n_components=2).fit(X_train)
>>> reduced_X_train = pca.transform(X_train)
>>> 
>>> h = .01     
>>> 
>>> x_min, x_max = reduced_X_train[:, 0].min() + 1, reduced_X_train[:, 0].max() - 1
>>> y_min, y_max = reduced_X_train[:, 1].min() + 1, reduced_X_train[:, 1].max() - 1
>>> xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
>>> kmeans = cluster.KMeans(init='k-means++', n_clusters=n_digits, n_init=10)
>>> kmeans.fit(reduced_X_train)
>>> Z = kmeans.predict(np.c_[xx.ravel(), yy.ravel()])
>>> 
>>> Z = Z.reshape(xx.shape)
>>> plt.figure(1)
>>> 
>>> plt.clf()
>>> plt.imshow(Z, interpolation='nearest', extent=(xx.min(), xx.max(), yy.min(), yy.max()), cmap = plt.cm.Paired, aspect ='auto', origin='lower')
>>> plt.plot(reduced_X_train[:, 0], reduced_X_train[:,1], 'k.', markersize=2)
>>> 
>>> centeroids = kmeans.cluster_centers_
>>> plt.scatter(centeroids[:,0], centeroids[:,1], marker='.', s=169, linewidth=3, color='w', zorder=10)
>>> plt.title('K-means clustering on the digits dataset (PCAreduced data)\nCenteroids are marked with white dots')
>>> plt.xlim(x_min, x_max)
>>> plt.ylim(y_min, y_max)
>>> plt.xticks(())
>>> plt.yticks(())
>>> plt.show()
cs


numpy.arange([start], stop[, step], dtype=None) : 특정 간격을 가진 값 반환(시작값은 포함되나 종료값은 제외됨).

numpy.meshgrid(*xi, **kwargs) : 좌표 벡터로부터 좌표 행렬 반환
    https://docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.meshgrid.html

numpy.c_ : 마지막 축을 기준으로 하나로 결합시키는 것. 예를 들면 다음과 같다.
    >>> np.c_[np.array([[1,2,3]]), 0, 0, np.array([[4,5,6]])]
    array([[1, 2, 3, 0, 0, 4, 5, 6]])

numpy.ravel(a, order='C') : 입력된 원소를 포함하는 1-D 배열을 반환한다. 입력된 배열과 동일한 타입으로 반환된다.

reshape(-1, order=order) 과 같다. 예를 들면 다음과 같다.
>>> x = np.array([[1, 2, 3], [4, 5, 6]])
>>> print(np.ravel(x))
[1 2 3 4 5 6]

https://docs.scipy.org/doc/numpy/reference/generated/numpy.ravel.html

imshow: extent and aspect http://stackoverflow.com/questions/13384653/imshow-extent-and-aspect

colormap https://scipy.github.io/old-wiki/pages/Cookbook/Matplotlib/Show_colormaps

pyplot.plot(*args, **kwargs)

    plot(x, y)        # plot x and y using default line style and color
    plot(x, y, 'bo')  # plot x and y using blue circle markers
    plot(y)           # plot y using x as index array 0..N-1
    plot(y, 'r+')     # ditto, but with red plusses

http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot