K-Means (K-Nearest Neighbor) 구현해보기

요즘 네X버 기사 페이지를 보면 이 기사를 보는 연령과 성별 구성이 어떻게 되는지가 나와있다. 엄밀하게 따져보면 리플을 달았던 사람들의 연령과 성별이고, 로그인을 안 한 사람은 배제가 되기는 하겠지만, 어찌됐건 특정 종류의 기사를 많이 보는 연령과 성별을 눈으로 확인할 수 있는 좋은 데이터임에는 틀림이 없다.

그럼 로그인을 안 하는 경우에는 어떻게 구분을 해야할까? 여러가지 그룹화 (Clustering) 방법이 있겠지만, 여기서는 데이터 셋을 k개 그룹으로 쪼개는 방법을 활용해보자. 참고로 여기서 말하는 k는 특정 숫자가 아니고, 사용자가 임의로 정하는 숫자다.

 

1. IRIS 데이터 예시

1.1. 데이터 정리 & k-Means 테스트

df <- iris[, -ncol(iris)]
df <- scale(df)
df <- as.data.frame(df)

R로 하는 온라인 예제는 보통 R에 끼여서 들어오는 iris 데이터 셋을 이용한다. 그 데이터를 불러와서 마지막 열을 제거하고, scale을 조절해준 다음에, data.frame으로 설정한다.


require(dbscan)
kNNdistplot(df, k= 5)
abline(h = 0.8, col = "red")

여기서는 k-Means 혹은 k-Nearest Neighbor (kNN) 알고리즘을 활용하기 위해 dbscan이라는 패키지를 활용한다. 명령어 사용법은 지극히 간단하다. 데이터 셋 (df)을 불러내고, k 값을 지정한다. 아래 그래프에서 빨간 직선은 abline으로 그은 것이다.

plot of chunk dbscan exercises

1.2. dbscan에 세부설정 해 보기


require(dbscan)
db_clusters_iris <- dbscan(df, eps=0.8, minPts=5)
print(db_clusters_iris)

 

다시 dbscan 패키지 (kNN용 패키지)를 불러온 다음에, 한 개 묶음의 반원 크기 최대치 (epsilon, eps로 지정)를 지정하고, 그 원 안에 들어가는 최소한의 점의 숫자 (minPoints, minPts로 지정)를 설정해준다. 위의 예시에서는 scale된 데이터 값의 0.8 사이즈 안에 있는 그룹, 한 그룹에 최소한 5개의 데이터 포인트가 있을 것을 지정했다


require(factoextra)
fviz_cluster(db_clusters_iris, df, ellipse = FALSE, geom = "point")

factoextra라는 패키지에 있는 fviz_cluster라는 함수를 활용했다. ellipse를 FALSE로 지정한 것은 주변에 원을 그리지 않기 위해서고, geom에서 “point”는 점을 선으로 묶는 옵션을 배제하기 위해서 지정했다. 아래 그래프를 보니 Dim1이 -2 근처인 값들과 아닌 값들로 2개 그룹이 나눠져있다.

 

plot of chunk dbscan exercises

1.3. 여러가지 경우의 수

require(dbscan)
require(factoextra)
epsilon_values <- c(1.8, 0.5, 0.4)
kNNdistplot(df, k = 5)
for (e in epsilon_values) {
abline(h = e, col = "red")
}

여기서는 반원의 크기 (epsilon 값)을 0.8만 쓰는게 아니라 1.8, 0.5, 0.4 세 가지 종류로 바꿔보면서 어느 값이 가장 합리적일지 가늠해본다. 참고로 마지막 3 라인은 for-loop (특정 명령어가 반복되도록 하는 설정)이고, epsilon 값에 해당하는 빨간색 직선을 그으라는 명령어다.

 

plot of chunk dbscan exercises

for (e in epsilon_values)
{ db_clusters_iris <- dbscan(df, eps=e, minPts=4)
title <- paste("Plot for epsilon = ", e)
g <- fviz_cluster(db_clusters_iris, df, ellipse = TRUE, geom = "point", main = title)
print(g) }

이제 3가지 경우 각각에 해당하는 Plot을 fviz_cluster로 그려보자.

plot of chunk dbscan exercises

plot of chunk dbscan exercises

plot of chunk dbscan exercises

예상했던대로 epsilon의 숫자가 작아질수록 더 작은 원이 그려졌고, 그만큼 더 많은 숫자의 그룹들이 생겨난다. 또 그룹 안에 들어가지 못하는 점들의 숫자도 점차 증가한다.

 

 

2. UC Irvine의 머신러닝 Lap에서 받은 도매업 데이터

2.1. 데이터 정리

require(dbscan)
require(factoextra)
customers <- read.csv("Wholesale customers data.csv")
customers <- customers[, c("Fresh","Milk")]
customers <- scale(customers)
customers <- as.data.frame(customers)
kNNdistplot(customers, k = 5)
abline(h = 0.4, col = "red")

위에서 했던 작업을 Wholesale customers data.csv로 반복한다.

db_clusters_customers <- dbscan(customers, eps=0.4, minPts=5)
fviz_cluster(db_clusters_customers, customers, ellipse = FALSE, geom = "point")

epsilon의 크기를 살짝 줄인 것 말고는 같은 설정이다. 아래를 보면 원점 근처에 모인 데이터와 나머지 데이터로 그룹이 나뉘는 것을 볼 수 있다.

 

plot of chunk dbscan exercises

2.2. 데이터 재정렬

데이터 정리를 통해서 scale된 값이 2.5가 넘으면 그룹 밖으로 분류할 수 밖에 없는 Outlier 값으로 판단된다는 것을 알 수 있다.

# remove values beyond 2.5 standard deviations
customers_core <- customers[customers[['Fresh']] > -2.5 & customers[['Fresh']] < 2.5, ]
customers_core <- customers_core[customers_core[['Milk']] > -2.5 & customers_core[['Milk']] < 2.5, ]

Fresh와 Milk에서 표준편차가 2.5 이상인 값들을 제외했다.

km_clusters_customers <- kmeans(customers_core, centers = 4, nstart = 10)
fviz_cluster(km_clusters_customers, customers_core, ellipse = FALSE, geom = "point")

kmeans 함수로 그룹들 (Clusters)을 찾는다. 여기서 Centers는 중심의 갯수 (즉 4개), nstart는 iteration (같은 시도를 반복하는 작업)을 10번까지 해보라는 설정이다. 참고로, 처음에 어느 점에서 시작하느냐에 따라 그룹이 다르게 묶일 수 있으므로 iteration을 여러번 해서 모델이 찾아낸 값을 검증하는 작업은 필수다. (보통 robust check이라고 표현한다.)

아래는 그렇게 찾아낸 그룹을 표현한 그래프다.

plot of chunk dbscan exercises

자 이게 실루엣 정보 (Silhouette Information)을 찾아보자. 각 그룹에 들어간 점들이 얼마나 가까이 붙어 있는지를 보여주는 정보다. 다시말하면, k-Means에서 모델을 잘못 만들었으면, 혹은 Outlier를 제거하지 않았으면, 어느 한 그룹의 실루엣이 엄청 크게 나오는 경우가 나올 것이다. 위에서 Outlier도 다 제거했고 (표준편차 2.5 이상 out 시킴), epsilon 값을 0.4, 모델 내 점의 최소 숫자 5개 등으로 나름 합리적인 모델을 만들었다. 실제로 그 숫자들이 합리적인지 아닌지는 아래 실루엣 그래프에서 밝혀진다.

(참고로 아래 dist 함수는 데이터의 분포를 그려주는 함수다)

km_clusters_vector <- km_clusters_customers[['cluster']]
km_distances <- dist(customers_core)
km_silhouette <- silhouette(km_clusters_vector, km_distances)
fviz_silhouette(km_silhouette)

 plot of chunk dbscan exercises

평균 0.41. 이 정도면 필자의 눈에는 합리적인거 같은데, 다른 epsilon과 minPts로 테스트해보고 더 좋은 결과가 있으면 알려주시면 좋겠다.

소스 링크, 소스 코드

X