머신러닝,딥러닝

군집분석, 그리고 2D 이미지 데이터 군집분석을 위한 K-means clustering

kimjy 2021. 12. 14. 14:52

들어가기에 앞서, 연구실에서 2D 이미지 데이터를 군집분석하고, 그 특성을 비교하는 연구를 짧게 진행한 바가 있다. 연구를 시작하기 전에도 군집분석에 대한 개념은 있었지만 데이터를 어떻게 전처리하여야 하는지, 또 K는 어떻게 설정하여야 하는 지에 대한 것은 잘 알지 못하였었다. 다행히도 다른 학생이 도와준 까닭에 작게라도 개념을 잡을 수 있었고, 따라서 본 포스팅에서는 연구를 통해 배웠던 개념을 정리하고자 한다. 다만, 아직 머신러닝에 대해 조예가 깊지 않은 까닭으로 좋은 포스팅은 아니라는 것에 아쉬움이 있다. 

 군집분석이란 비지도학습의 일종으로, 데이터들의 특성을 반영하여 군집화하는 분석방법이다. 널리 사용되는 방법은 k-means, DBSCAN 등이 있다. 본 포스팅에서는 2D 이미지에 대해 전처리 및 k-means 클러스터링을 수행하고 최종적으로 이를 평가하는 방법에 대해 간략히 포스팅하겠다.

import numpy as np
import pandas as pd
from numpy import savetxt
from netCDF4 import Dataset

import matplotlib.pyplot as plt
import seaborn as sns

import os
import glob
from os import listdir

from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans

from pylab import plot,axis,show,pcolor,colorbar,bone

k-means 클러스터링을 위한 패키지는 위와 같은 패키지를 사용했다.

 

A.shape, B.shape
>>> ((58, 79), (58, 79))

A, B 두 채널을 갖고 있는 특성의 이미지를 사용하였고, 원래는 각자 고유한 변수명이 있지만 보안의 이유로 A, B라고 하겠다. 이미지의 크기는 58x79이고 총 320개의 데이터를 사용하였다. 데이터의 수가 너무 적은 것은 아닌가에 대한 걱정은 든다.

 

데이터 전처리

현재 데이터는 58x79의 크기를 갖는 데이터로, 데이터의 차원수는 약 2500차원이다. 따라서 차원을 축소시켜야 군집분석이 잘 이루어질 것이라고 판단한다. 따라서 stride를 주는 방법과, PCA 분석을 통해 차원축소를 진행하였다.

stride 방법

stride 방법은 "이미지의 인접한 픽셀은 거의 동일한 특성을 갖는다"라는 가정에서 시작한다. 따라서 stride를 적용하면 핵심정보의 손실은 적은 채로 차원축소가 가능할 것이다. stride는 아래와 같이 간단히 구현할 수 있다.

A_ = np.array(A)[::2][::2].T[::2][::2]
B_ = np.array(B)[::2][::2].T[::2][::2]
A_.shape, B_.shape
>>> ((20,15), (20,15))

stride를 통해서 2500차원의 데이터가 300차원을 갖는 데이터로 변환된 것을 확인할 수 있다.

 

변수 Scaling

변수 scaling는 normalization과 비슷한 개념으로 이해하였다. 우리가 다루는 많은 데이터들은 각각의 scaling이 존재할 것이라고 생각한다. 예를 들어 설명하면 조금 더 이해가 쉬울 것이라고 생각하는데, 가령 키와 나이를 사용하여 군집분석을 수행한다고 가정하겠다. 일반적으로 키는 150~190정도의 범위를 갖을 수 있고, 나이는 0~100의 범위를 갖을 수 있을 것이다. 따라서 두 변수의 scale이 다르기 때문에 군집분석에 있어서 동일한 가중치를 주기가 힘들다. 따라서 normalization을 수행해서 평균은 0, 분산은 1로 변환한다면, 변수를 동일한 가중치로 군집분석을 수행할 수 있을 것이라 기대할 수 있다.

A_flatten = A_.flatten()
B_flatten = B_.flatten()

scaler = StandardScaler()
A_scaled = scaler.fit_transform(A_flatten)
B_scaled = scaler.fit_transform(B_flatten)

 

Outlier 처리

변수 scaling 후에는 outlier 처리를 수행하면 좋을 것 같다. outlier는 이상치라고 번역할 수 있는데, outlier를 포함하여 군집분석을 수행한다면, outlier가 군집분석에 좋지않은 영향을 미칠 수 있기 때문에 outlier를 처리하였다. outlier를 없애거나 이상치 이하의 값으로 변경하는 방법이 있을 수 있는데, 본 프로젝트의 데이터는 수가 많이 않으므로 이상치 데이터는 threshold값으로 변경하였다. threshold는 z 값 기준으로 -3~3 사이로 수행하였다.

A_scaled=np.where(A_scaled>3, 3, A_scaled)
B_scaled=np.where(B_scaled<-3, -3, B_scaled)
print(A_scaled.max(), A_scaled.min(), B_scaled.max(), B_scaled.min())
>>> 3.0, -3.0, 3.0, -3.0

 

PCA를 통한 차원축소

데이터 전처리의 마지막 단계로 PCA를 통한 차원축소를 진행하였다. Explained variance ratio는 0.8로 설정하였고, 이를 통하여 29개의 차원을 같는 데이터를 최종적으로 얻을 수 있었다. 아래는 PCA를 수행할 수 있는 코드이다.

data = np.hstack(A_scaled, B_scaled)
pca = PCA(n_component=data.shape[1], random_state=123)
data_reduced = pd.DataFrame(pca.fit_transform(data))
col_names = ["PC"+ str(i) for i in range(data.shape[1])]
data_reduced.columns = col_names
print(data_reduced.shape)
>>> (600, 29)

 

K-Means clustering

K-Means clustering은 아래와 같은 코드로 구현이 가능하다.

k_dict={}
iner = pd.DataFrame(data=[], index=n_cluster_list, columns=['inertia'])
for i in range(4, 20, 2):
    model = KMeans(n_cluster=i, n_init=5, random_state=123)
    model.fit(reduced_df)
    iner.loc[i]=model.inertia_
    k_dict[i]=model
    
kmeans_inertia, kmeans_dict = kmeans_inertia(4,20,2,pca_df)

그 후 k 값과 inertia 값의 관계를 확인하기 위해 아래와 같은 plot을 그렸다. inertia는 cluster 간의 거리 합을 나타내는데, inertia 값이 급격히 떨어지는 구간의 K값을 사용하면 좋다. 하지만 이 군집분석에서는 elbow에 해당하는 구간을 찾기 힘들기 때문에 silhouette score를 통해서 k 값을 찾아보도록 하였다. 

 

Silhouette Score

실루엣 스코어는 최적의 k 값을 찾을 수 있는 방안 중 하나로, -1에 가까울수록 군집결과가 타당하지 못하고 1에 가까울수록 군집결과가 타당하다. 따라서 k 값중에서 1에 제일 가까운 k 값을 찾는 것이 타당하다. 이 데이터에서는 k가 14일 경우와 18일 경우에 실루엣 스코어가 가장 크게 나타났다. 데이터의 수가 320개인데 k값이 14와 18이면, 너무 과적합되는 것은 아닌가에 대한 의문이 발생하였다. 아마 데이터의 수를 늘리거나, 다른 클러스터링 방법을 사용하면 어떨 까한다.

for i in np.arange(4,20,2):
    ss = silhouette_score(pca_df,kmeans_dict[i].labels_)
    print("K={} : Silhouette Score : {}".format(i,round(ss,3)))
    
>>> K=4 : Silhouette Score : 0.184
>>> K=6 : Silhouette Score : 0.205
>>> K=8 : Silhouette Score : 0.211
>>> K=10 : Silhouette Score : 0.222
>>> K=12 : Silhouette Score : 0.23
>>> K=14 : Silhouette Score : 0.246
>>> K=16 : Silhouette Score : 0.242
>>> K=18 : Silhouette Score : 0.246

 

정리하면서..

본 포스팅에서는 작성하지 않았지만, 데이터 수를 더 늘렸을 때에는 과적합 문제가 발생하지 않았다. 아마 데이터의 수가 너무 작기 때문에 발생하는 과적합 문제라고 생각한다. 본 포스팅을 작성하면서 배운 것은 아래와 같다.

  • 클러스터링을 위한 전처리 방법과, 클러스터링을 수행하는 코드, 그리고 평가하는 방법을 기록함으로써 군집분석에 대한 프로세스를 복기하였다. 또, 나중에 군집분석을 수행할 때 사용할 수 있을 것 같다.
  • 과적합 문제에서 데이터의 수가 얼마나 중요한지에 대한 것을 다시 한 번 깨달았다. 데이터 분석에서 데이터의 양 그리고 질이 얼마만큼이나 중요한 가를 다시 한 번 생각하게 되었다.