k-Means 알고리즘을 이용한 군집화(Cluster)

다음과 같은 데이터가 존재한다고 합시다.

위의 데이터는 아래의 코드로 생성된 것입니다.

import sklearn
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

blob_centers = np.array(
    [[ 0.5,  0.5 ],
     [ 1.5,  0.5 ],
     [ 0.5,  1.5],
     [ 1.5,  1.5]])
blob_std = np.array([0.4, 0.3, 0.1, 0.1])

X, y = make_blobs(n_samples=2000, centers=blob_centers, cluster_std=blob_std, random_state=3224)

def plot_data(X, y):
    plt.scatter(X[:, 0], X[:, 1], c=y, marker='.', s=10)

plot_data(X, y)
plt.show()

위의 데이터는 4개로 구룹지을 수 있다는 것을 코드를 통해 알 수 있습니다. 이제 이 데이터를 k-Means를 이용하여 4개로 군집화하는 코드는 살펴보면 다음과 같습니다.

k = 4
kmeans = KMeans(n_clusters=k)
y_pred = kmeans.fit_predict(X)

위의 코드를 통해 kmeans 모델은 새로운 샘플 데이터에 대해서 4개의 그룹중 어떤 그룹에 포함되는지 예측할 수 있습니다. 이에 대한 시각화 코드는 다음과 같습니다.

def plot_decision_boundaries(clusterer, X, y, resolution=500):
    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="gist_stern")
    plt.contour(Z, extent=(mins[0], maxs[0], mins[1], maxs[1]), linewidths=1, colors='k')
    plot_data(X, y)

plot_decision_boundaries(kmeans, X, y)
plt.show()

위 코드의 결과는 다음과 같습니다.

실제 k-means를 적용할 시에는 군집화할 개수를 알 수 없는 경우가 대부분입니다. 위의 코드에서는 k 값인데요. 이 값을 효과적으로 결정하기 위해서는 실루엣 다이어그램(Silhouette Diagram)을 통해 효과적으로 파악할 수 있습니다.

차원 축소

머신러닝으로 풀고자 하는 문제에 대한 입력값, 즉 특성은 그 상황에 따라 몇개에서 수백, 수천만개 이상으로 이루어질 수 있습니다. 그렇다고 이 모든 특성이 문제 해결을 위해 중요한 것은 아닙니다. 어떤 특성은 문제 해결에 미치는 영향이 미미하거나 아예 없는 경우도 있습니다. 이럴때 문제 해결에 미미한 영향을 가지는 특성은 제거하는 것은 해당 문제를 풀 수 있는 가능성을 높여준다는 점에서 매우 의미가 큽니다. 문제 해결의 가능성을 높여지는 것 뿐만이 아니라 불필요한 특성을 줄여줌으로써 학습 속도가 향상되며, 더 적은 학습 데이터만으로도 높은 정확도의 결과를 얻을 수 있습니다. 또한 특성을 2개 또는 3개로 줄임으로써 우리에게 익숙한 2차원과 3차원의 공간에 데이터를 시각화하여 어떤 통찰을 얻을 수도 있습니다.

이 글은 특성수를 줄이는, 즉 차원을 축소하는 방법에 몇가지를 언급합니다. 구체적인 예로 다음과 같은 3개의 특성으로 구성된 3차원의 데이터를 특성 2개인 2차원의 데이터로 축소함에 있어서, 최대한 원래 데이터가 가지고 있는 좋은 특성을 유지하도록 하는데, 이는 통계학적으로는 분석을 최대한 보존하는 방향이기도 합니다. 먼저 특성을 줄일 대상이 되는 원본 데이터를 구성하고 이를 3차원으로 시각화하는 코드는 다음과 같습니다.

from sklearn.datasets import make_swiss_roll
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

X, t = make_swiss_roll(n_samples=500, noise=0.1, random_state=3224)

fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=t, cmap=plt.cm.hot)
plt.show()

위 코드의 결과는 다음과 같습니다.

데이터는 사이킷런에서 제공하는 데이터셋 중 스위스롤로 스펀지 케익을 롤케익처럼 둘둘 말아 놓은 형태인데요, 이를 평면으로 펴서 시각화하는 코드와 그 결과는 다음과 같습니다.

plt.scatter(t, X[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

스위스롤 데이터셋에 대한 차원축소에 대한 최고의 결과는 바로 위의 결과라고도 할 수 있습니다. 하지만 이러한 해석은 2차원상의 시각화라는 관점에서만 국한된 결과입니다.

이제 실제 차원축소에 대한 다양한 방법을 코드로 살펴보겠습니다. 먼저 PCA입니다.

from sklearn.decomposition import PCA
X_pca_reduced = PCA(n_components=2, random_state=42).fit_transform(X)
plt.scatter(X_pca_reduced[:, 0], X_pca_reduced[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

다음은 PCA의 비선형 버전인 Kernel PCA입니다.

from sklearn.decomposition import KernelPCA

lin_pca = KernelPCA(n_components=2, kernel="linear")
rbf_pca = KernelPCA(n_components=2, kernel="rbf", gamma=0.05)
sig_pca = KernelPCA(n_components=2, kernel="sigmoid", gamma=0.001, coef0=1)

plt.figure(figsize=(11, 4))
for subplot, pca, title in (
        (131, lin_pca, "Linear kernel"), 
        (132, rbf_pca, "RBF kernel, $\gamma=0.05$"), 
        (133, sig_pca, "Sigmoid kernel, $\gamma=10^{-3}, r=1$")):
    X_reduced = pca.fit_transform(X)
    if subplot == 132:
        X_reduced_rbf = X_reduced
    
    plt.subplot(subplot)
    plt.title(title, fontsize=14)
    plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=t, cmap=plt.cm.hot)
    plt.xlabel("$z_1$", fontsize=18)
    if subplot == 131:
        plt.ylabel("$z_2$", fontsize=18, rotation=0)

plt.show()

위의 코드는 3가지 종류의 커널에 대한 PCA 결과를 표현하고 있습니다.

다음은 LLE 알고리즘을 사용한 차원축소입니다. LLE는 Locally Linear Embedding으로 국소적인(지역적인) 샘플 데이터간 거리를 최대한 보존하며 차원을 축소합니다. 코드 및 그 결과는 아래와 같습니다.

from sklearn.manifold import LocallyLinearEmbedding

lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10, random_state=3224)
X_reduced = lle.fit_transform(X)

plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

다음은 MDS로 다차원 스케일링(Multi-Dimensional Scaling)의 약자로, 샘플 데이터 간의 거리르 보존하면서 차원을 축소합니다. 코드 예와 그 결과는 다음과 같습니다.

from sklearn.manifold import MDS
mds = MDS(n_components=2, random_state=3224)
X_reduced_mds = mds.fit_transform(X)

plt.scatter(X_reduced_mds[:, 0], X_reduced_mds[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

다음은 Isomap을 이용한 차원축소이며, 이는 각 샘플에서 가장 가까운 샘플 간의 거리, 보다 정확히는 Geodesic Distance를 유지하면서 차원을 축소합니다. 코드와 그 결과는 다음과 같습니다.

from sklearn.manifold import Isomap
isomap = Isomap(n_components=2)
X_reduced_isomap = isomap.fit_transform(X)

plt.scatter(X_reduced_isomap[:, 0], X_reduced_isomap[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

더 많은 차원축소 방법이 존재하지만 이 글에서는 마지막으로 t-SNE을 이용한 차원감소를 소개합니다. 주로 시각화에 많이 사용되며 군집화된 결과를 시각적으로 표현합니다. 코드와 결과는 다음과 같습니다.

from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, random_state=3224)
X_reduced_tsne = tsne.fit_transform(X)

plt.scatter(X_reduced_tsne[:, 0], X_reduced_tsne[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

t-SNE를 이용한 다른 글은 아래의 링크를 참조하시기 바랍니다.

잠재벡터(Latent) z의 공간분포 시각화(Visualization)

간단한 tar 파일 사용

tar는 많은 파일을 하나로 묶어 주는 프로그램 입니다. 묶어줄때 압축 옵션을 지정하면 압축도 해줍니다 아래의 이미지를 토대로 예를 들어 보겠습니다..

위의 그림 중 TILES_20200626와 vworld 디렉토리의 모든 파일을 해당 디렉토리 구조를 유지하면서 tiles.tar이라는 파일 하나로 묶고자할때 아래처럼 tar 명령어를 수행합니다.

tar -cvf tiles.tar ./TILES_20200626 ./vworld

하나로 묶여진 tar 파일을 원하는 컴퓨터에 복사해 다시 복원하는 tar 명령은 다음과 같습니다.

tar -xvf tiles.tar

옵션으로 z를 지정하면 압축을 수행합니다. 위의 경우 이미 압축된 이미지 파일들을 하나로 묶는 경우이므로 압축 옵션을 지정하지 않았습니다.

FingerEyes-Xr의 편집 이벤트

FingerEyes-Xr의 공간 데이터 편집시 발생하는 이벤트는 1개입니다. Xr.Events.EditingCompleted로 사용자가 선택한 도형을 편집한 뒤에 발생하는 이벤트입니다. 등록은 다음과 같습니다.

map.addEventListener(Xr.Events.EditingCompleted, onMapEditingCompleted);

그리고 이벤트에 대한 콜백 함수인 onMapEditingCompleted 함수는 아래와 같이 작성할 수 있습니다.

function onMapEditingCompleted (e) 
{
    let map = e.map;
    let type = e.editCommandType;
    let rowId = e.rowId;

    if(type === Xr.edit.AddPartCommand.TYPE) {
        // 여러 개의 요소를 갖는 도형에 대해 1개의 새로운 요소가 추가될 때 ...
    } else if(type === Xr.edit.AddVertexCommand.TYPE) {
        // 도형에 대해 정점이 하나 추가될 때 ...
    } else if(type === Xr.edit.MoveCommand.TYPE) {
        // 도형 전체가 이동될 때 ...
    } else if(type === Xr.edit.MoveControlPointCommand.TYPE) {
        // 도형의 제어점이 이동되었을 때 ...
    } else if(type === Xr.edit.NewCommand.TYPE) {
        // 새로운 도형이 생성되었을 때 ...
    } else if(type === Xr.edit.RemoveCommand.TYPE) {
        // 기존의 도형을 제거했을 때 ...
    } else if(type === Xr.edit.RemovePartCommand.TYPE) {
        // 도형을 구성하는 하나의 요소를 제거했을 때 ...
    } else if(type === Xr.edit.RemoveVertexCommand.TYPE) {
        // 도형을 구성하는 정점을 제거했을 때 ...
    }
}

이벤트 함수로 넘겨지는 이벤트 객체인 e의 map은 편집이 이루어진 지도 객체를 의미하며, rowId는 편집 대상이 되는 Row의 ID 값입니다. 그리고 editCommandType은 위의 코드의 if 문에서 언급한 주석의 내용일 때를 파악하기 위해 사용됩니다.

추가로, 위의 편집 이벤트를 위해 선행되어야할 것은 도형에 대한 편집 행위의 시발을 발생해줘야 한다는 것입니다. 아래는 그래픽 레이어에 사각형을 새롭게 생성하는 것에 대한 편집의 시작 코드입니다.

let gl = new Xr.layers.GraphicLayer("gl_community");

map.layers().add(gl);
map.edit().targetGraphicLayer(gl);

map.userMode(Xr.UserModeEnum.EDIT);
map.edit().newRectangle(0);

마지막 코드에서 newRectangle 함수의 인자값인 0은 새롭게 생성할 도형이 가질 id 값입니다.

만약 새로운 도형이 추가되면 onMapEditingCompleted 이벤트의 Xr.edit.NewCommand.TYPE 조건에 걸리게 되는데, 이 조건에서 추가된 도형의 상세 정보를 얻기 위한 코드 예시는 다음과 같습니다.

function onMapEditingCompleted(e) {
    let map = e.map;
    let type = e.editCommandType;
    let rowId = e.rowId;
            
    if ( ... ) {
        ...
    } else if (type === Xr.edit.NewCommand.TYPE) {
        let row = map.edit().targetGraphicLayer().row(rowId);
        let data = row.graphicData().data();

        if (data instanceof Xr.data.RectangleShapeData) {
            console.log("RECTANGLE:", data.minX, data.minY, data.maxX, data.maxY);
        } else if (data instanceof Xr.data.EllipseShapeData) {
            console.log("ELLIPSE:", data.cx, data.cy, data.rx, data.ry);
        } else if (data instanceof Xr.data.PointShapeData) {
            console.log("POINT:", data.x, data.y);
        } else if (data instanceof Xr.data.PolylineShapeData) {
            let cntParts = data.length;
            console.log("POLYLINE:");
            for (let iPart = 0; iPart < cntParts; iPart++) {
                let part = data[iPart];
                let cntVtx = part.length;
                for (let iVtx = 0; iVtx < cntVtx; iVtx++) {
                    console.log(part[iVtx].x + ", " + part[iVtx].y);
                }
            }
        } else if (data instanceof Xr.data.PolygonShapeData) {
            console.log("POLYGON:");
            let cntParts = data.length;
            for (let iPart = 0; iPart < cntParts; iPart++) {
                let part = data[iPart];
                let cntVtx = part.length;
                for (let iVtx = 0; iVtx < cntVtx; iVtx++) {
                    console.log(part[iVtx].x + ", " + part[iVtx].y);
                }
            }
        }
    } else if ( ... ) {
        ...
    }
}

마지막으로 위의 편집 이벤트가 적용된 실제 편집 기능에 대한 동영상은 아래와 같습니다.