대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 기계 학습의 A to Z : scikit-learn을 활용한 기계 학습 (군집화, Clustering)
등록일 조회수 6872
첨부파일  

기계 학습의 A to Z



scikit-learn을 활용한 기계 학습 (군집화, Clustering)

속담 한 줄이 복잡한 상황을 알기 쉽게 설명하는 경우가 종종 있다. 사람의 심리뿐만 아니라 과학적인 사실에도 속담이 적용되는 경우도 있으니 대단한 일이 아닌가하는 생각이 든다. 이러한 측면에서 ‘끼리끼리 모인다’ ‘유유상종’은 다른 어떤 설명보다 군집화(Clustering)를 가장 잘 설명하는 속담이 아닐까 싶다. 군집화는 비슷한 속성을 가진 객체끼리 모이는 경향을 이용하여, 같은 주제의 웹 문서를 모으거나 기호가 같은 소비자끼리 그룹화 하는 등 다양한 영역에서 사용할 수 있다.



군집화는 비지도(자율) 학습의 한 가지 기법이다. 지도 학습은 목적 변수(범주)을 알고 있지만, 비지도 학습은 목적 변수를 알지 못한다. 새로운 인스턴스의 목적 변수를 예측하거나 데이터에서 지식을 얻는데 목적 변수는 매우 중요하다. 하지만 목적 변수가 없다면 사람이 직접 목적 변수를 만들어야 하며 많은 시간과 노력을 들어야 한다는 단점이 있다. 군집화는 비지도 학습으로 목적 변수가 없이도 사용할 수 있다. 비지도 학습은 현실에서 목적 변수가 없는 많은 정보를 사용하여 새로운 지식을 얻을 수 있는 방법이다. 한 쇼핑몰 운영자가 있다고 하자. 운영자는 데이터를 잘 살펴 본 후 새로운 특징을 찾았다. 일부 고객들이 비슷한 물품을 구매하는 것이다. 이 고객들을 한 그룹으로 묶어 새로운 물품이 들어올 때 광고 메일을 보내면 고객들에게는 좋은 정보가 되고 자연히 구매로 이어져 매출은 늘어날 수 있다. 하지만 이 데이터에는 특별한 속성(지시 값)이 없다. 구매한 물품을 활용하여 사용자를 군집화할 수 있다.

군집화란 한 군집 안의 원소들은 유사하나 다른 군집의 요소들과는 차이가 있는 군집을 찾는 작업이다. 앞의 예제라면 10대 구매자 그룹이나 바지류를 좋아하는 그룹을 찾는 일이 해당된다. 이는 예측보다는 지식을 발견하는데 사용된다. 이제부터 파이썬 데이터 분석 라이브러리인 pandas와 기계 학습 라이브러리인 scikit learn을 사용하여 대표적인 군집화 기법인 k평균(k-means)과 DBSCAN을 적용해 보자.



k평균(k-means) 기법

k평균은 단순하며 여러 영역에서 뛰어난 성능을 보인다. 특별한 통계학 지식이 필요없기 때문에 이해하기가 쉽다. 하지만 무작위로 잡는 초기 중앙점에 영향이 크며 군집 개수인 k을 값을 알아야 하는 단점이 있다. 군집화를 하기 위해 가장 먼저 군집할 개수인 k값을 정해야 한다. 군집화하기 전에 k값을 알 수 있으나 대부분 알지 못하는 경우가 많다. k값을 알지 못한다면, 다양한 방법으로 k값을 유추할 수 있다. 이를 테면, 영화를 군집화 할 경우 영화 잡지에 있는 장르 개수를 사용하며 쇼핑몰의 경우, 연령대나 상품 카테고리 개수등으로 유추할 수 있다. 이런 기본적인 유추 꺼리도 없는 최악의 경우, 그냥 무작위 수를 넣을 수도 있지만 인스턴스 총수가 n이라면 n/2의 제곱근을 사용할 수도 있다. 하지만 k값에 따른 성능 평가를 통해 k값을 정할 수도 있다. 차후 이에 대해 알아보도록 하겠다.

k값을 정했다면 속성에 유사성을 계산할 거리 함수(distance function)을 선택해야 한다. 기본적으로 사용하는 함수는 유클리드 거리(Euclidean distance, http://en.wikipedia.org/wiki/Euclidean_distance)이지만 맨하튼(Manhattan) 거리, 마할라노비스(Mahalanobis) 거리 함수 등을 사용한다. 여기서는 가장 짧은 거리를 계산하는 유클리드 거리 함수를 사용하겠다.




<그림 1> 유클리드 거리 함수

계속해서 k평균 알고리즘의 주요 단계를 간단하게 설명하면 다음과 같다. 알고리즘은 크게 두 단계를 반복한다.

0) 초기화 - k개 최초 군집의 중앙점
1) 각 인스턴스는 가장 가까운 군집의 중앙점이 속한 군집이 된다.
2) 각 군집은 군집 내 인스턴스에 대해 평균을 구하고 그 평균값으로 새로운 중앙점을 이동한다.
- 변화가 없거나, 정해진 횟수만큼 1), 2)를 반복한다.

다음 그림은 알고리즘을 단계별로 설명한다. 최초로 무작위 군집의 중앙점이 지정된 모양이다.

- 군집의 중앙점을 무작위로 정한다
- 알고리즘 1번으로 각 인스턴스를 가장 가까운 중앙점의 군집으로 정한다.
- 알고리즘 2번으로 군집 내 모든 인스턴스의 평균을 구해 군집의 중앙점을 이동시킨다.
- 알고리즘 1번으로 인스턴스 C가 새로운 군집으로 속하게 된다.
- 알고리즘 2번으로 군집 내 모든 인스턴스의 평균을 구해 군집의 중앙점을 이동시킨다.
- 변화가 없기 때문에 알고리즘이 끝나고 인스턴스 A, B, C가 하나의 군집으로, 인스턴스 D, E가 다른 한 군집이 된다.


<그림 2> 신장 데이터의 상자 수염

알고리즘은 단순한 반면, 유연해 많은 영역에서 사용된다. 하지만 초기 무작위 중앙점에 영향을 받아 실행할 때마다 다른 군집을 형성할 수 있다. 최악의 경우, 항상 같은 결과를 보장하지 못할 수도 있다. 이 문제를 해결하기 위해 초기 중앙점을 지정하는 다양한 방법이 제시되어 있다. pandas와 scikit learn 라이브러리를 사용하여 k평균 알고리즘을 데이터에 적용해 보자.



k평균 기법을 활용한 버스 정거장 군집화

최근 서울시에 버스 정류장에 사건 사고가 많아 임시 파출소를 설치하고자 한다. 임시 파출소는 어디에 설치해야 최대 효과를 낼 수 있을까? 최소의 비용으로 최대 효과를 내고자 한다. 이를 분석하기 위해서는 당연히 서울시 있는 버스 정류장 정보가 필요하다. 서울시 지선 버스의 모든 정거장을 웹 스크래핑하면 된다.

파이썬 라이브러리 scrapy(http://scrapy.org/)를 사용하면 웹에 있는 정보를 쉽게 가져올 수 있으나 그러한 평면적인 웹 페이지를 찾지 못했다. 대신, 버스 정거장에 대한 정보를 json 형태로 제공하는 웹 페이지 주소가 있어 버스 번호와 함께 호출하면 버스의 정거장 정보를 구할 수 있다. 버스 번호들을 http://m.bus.go.kr/mBus/에 보내면 정거장 정보를 구할 수 있다. 서울시 지선 버스 번호는 총 226개이며 다음은 일부 버스 번호이다.



<리스트 1> 일부 지선 버스 번호 '0017', '0018', '1014', '1017', '1020', '1111', .... '7719', '7720', '7722', '7723', '7726', '7727', '7728', '7730', '7733', '7737', '7738'

이 버스 번호의 정거장을 모두 구해 파싱하여 파일로 만든다.



<리스트 2> json 형식의 버스 노선 가져오는 일부 코드 import urllib2, urllib import json parameters = {} parameters['busRouteId'] = routeid target = 'http://m.bus.go.kr/mBus/bus/getRouteAndPos.bms' parameters = urllib.urlencode(parameters) handler = urllib2.urlopen(target, parameters) f = handler.read() j = json.loads(f.decode('cp949')) routes = j["resultList"] for route in routes: busRouteNm = route['busRouteNm'] busRouteId = route['busRouteId'] stationNm = route['stationNm'] stationNo = route['stationNo'] x = route['gpsX'] y = route['gpsY'] l = ' '.join([busRouteNm, busRouteId, stationNo, stationNm, x, y]) print(l.encode('utf-8'))

버스 정류장 id, 정류장 이름, GPS 좌표를 구했고 이를 ‘bus.tsv' 파일로 저장한다. 파일의 일부는 <리스트 3>과 같다.



<리스트 3> 0017 버스의 일부 정류장 정보 busno busid stationid stationnm x y 0017 4001700 03689 청암자이아파트 126.9465552752314 37.534546996142446 0017 4001700 03298 청암동강변삼성아파트 126.94931774716754 37.53396790725961 0017 4001700 03321 청심경로당 126.95056033715161 37.53370621462281 0017 4001700 03304 원효2동주민센터 126.95095037982259 37.5342145961956 0017 4001700 03306 산천동 126.95400934954684 37.53533949535592

pandas를 읽어 필요한 정보를 가공한다.



<리스트 4> pandas로 사용해 버스 일부 정보 가공 import pandas as pd bus_df = pd.read_csv('bus.tsv', sep=' ') mbus_df = bus_df[bus_df['stationid']!='0'] mbus_df = mbus_df[mbus_df['stationid']!='미정차']

정거장 id가 0인 것과 ‘미정차’인 것을 제거한다. 이로써 필요한 정거장의 정보를 모두 가져 왔다. 총 정거장 수는 13,684이다. 다행스러운 일은 GPS 정보가 있어, 지도로 시각화할 수 있다. 구글 맵(https://developers.google.com/maps/?hl=ko)을 사용한다. 구글 맵 마커의 GPS 좌표를 넣으면 지도에 표시할 수 있다. GPS 좌표를 구글 맵의 마커에 넣기 위해 pandas의 DataFrame에 있는 좌표를 자바스크립트의 구글 맵 마커로 변환한다.(<리스트 5> 참조)



<리스트 5> pandas의 DataFrame에 있는 좌표를 마커로 변환하는 코드 일부 def generate_busstops(df): coos = ', '.join(['new google.maps.LatLng(%s, %s)' %(r['y'] ,r['x']) for i, r in df.iterrows()]) return 'var busstops = [{0}];'.format(coos)

1만3684개 정거장을 모두 표시 할 수 없으니 200개만 샘플링 해 이 함수로 구글 맵을 출력하면 다음과 같은 시각화 정보를 얻을 수 있다.




<그림 3> 200개 정거장 샘플링하여 구글 맵으로 시각화

지금까지 수행한 일은 버스 정류장 정보를 웹에서 가져왔고 이를 구글 맵으로 시각화하였다. 우리의 목적은 사건 사고를 방지하는 파출소를 설치할 최적의 위치를 찾는 일이다. 많은 버스들이 다니는 정거장은 유동 인구가 많고 사건 사고도 많다. 그러면 버스가 많이 정거하는 버스 정거장을 찾는다. DataFrame의 value_counts() 메소드는 각 열의 빈도수를 구할 수 있고 이를 이용하여 버스 정거장 id의 빈도수를 구해 일정 빈도수 이상의 버스 정류장만 필터링할 수도 있다.



<리스트 6> 버스들이 공통으로 8번 이상 정거하는 정거장 필터링 N_BUSSTOPS = 8 stops = mbus_df['stationid'].value_counts() d = stops[stops > N_BUSSTOPS].index.values.tolist() mbus_df = mbus_df[mbus_df['stationid'].isin(d)] busstop_df = mbus_df.drop_duplicates(cols='stationid', take_last=True)

<리스트 6>에서는 버스가 공통으로 8번 이상 정류하는 정류장을 찾고 중복된 id를 제거한다.


<그림 4> 8회 이상 공통으로 버스가 정류하는 정류장

참고로, 버스 정류자에 어떤 버스가 지나가는지도 시각화 할 수 있다. 다음은 정류장 id가 24138인 잠실역을 지나는 버스를 시각화한 지도다. 만약 이러한 시각하지 못하고 문자나 숫치만으로 생각한다면 결코 쉽지 않은 작업이 될 것이다. 시각화는 매우 중요한 작업이며 직관적인 이해를 높이는데 큰 역할을 한다.


<그림 5> 잠실역을 지나는 버스 시각화

버스가 많이 다니는 정류장을 k평균 기법으로 군집화를 한다. scikit learn의 cluster모듈에는 다양한 군집화 기법이 있다. 그 중 kmean을 사용하자. KMeans 클래스의 대표적인 매개 변수는 n_clusters, init, n_init이다. n_clusters은 k 값이며, init은 초기 중앙점을 정하는 방법이며, n_init은 초기 중앙점이 무작위로 선택되기 때문에 최상의 결과를 얻기 위해 몇 번 초기값을 변경하여 알고리즘을 실시할지를 정하는 횟수이다. 그 이외에도 다른 매개 변수를 변경하여 알고리즘을 실행할 수 있다. 위치 정보로 군집화하기 때문에 군집화 할 대상은 GPS 좌표이다. 우선 k값을 10으로 지정한다.



<리스트 7> k값을 10으로 설정하여 k평균을 적용 from sklearn.cluster import KMeans from sklearn import metrics X = busstop_df[['x', 'y']] y = busstop_df['busno'] km = KMeans(init='k-means++', n_clusters=10, n_init=10) km.fit(X) print(km.cluster_centers_)


<그림 6> 8회 이상 공통으로 정류하는 버스 정류장의 10개 군집의 중앙점

앞의 코드는 k값을 10으로 정하여 kmean기법을 실시했다. km.cluster_centers_는 각 군집의 최종 중앙점을 나타낸다. 최종 중앙점은 8회 이상 공통으로 정류하는 버스 정류장 군집의 중간 위치가 된다. 10개로 군집화하여 군집들의 중앙점인 위치에 파출소를 세우면 각 정류장에 사건 사고가 발생할 때 신속하게 처리할 수 있다. 하지만 문제점이 있다. 바로 k값이다. 우리는 임의로 군집 개수를 10로 나누었지만 10로 나눌 아무 근거가 없었다. 최적의 k값은 무엇일까?



k평균 성능 평가하기

k값을 구하는 것은 k평균의 성능과 상관있다. 성능을 평가하는 방법은 몇 가지가 있는데 간단히 설명하도록 하겠다. 우선, 크게 두 가지로 나누면 목적 변수가 있을 경우와 목적 변수가 없을 경우이다. 목적 변수가 있다는 것은 군집의 실제값이 있는 것이기 때문에 좀 더 정확하게 평가할 수 있다. Homogeneity, Completeness, V-measure등이 대표적인 방법이다. Homogeneity은 0.0에서 1.0까지(큰 값이 좋다)이며 1.0은 각 군집들이 각 범주의 데이터 점들만을 포함함을 나타낸다. 반면, Completeness는 주어진 범주의 모든 데이터 점이 같은 군집 내에 있는 것을 나타낸다. V-measure는 Homogeneity와 Completeness의 조합 평균이다. 목적 변수가 없다면 Sihouette coefficient를 사용한다. Sihouette coefficient는 a, b를 사용하는데 의미는 다음과 같다.

a : 같은 군집내의 인스턴스와 다른 모든 인스턴스간 거리의 평균
b : 군집내의 인스턴스와 가장 인접한 다른 군집내의 모든 인스턴스간 거리의 평균
Sihouette coefficient = b - a / max(a, b) 이다.

사실, 시각화를 통해 어느 정도 군집을 잡아야 할지 알 수 있으나 시각화를 하지 못할 경우 성능 평가를 통해 최적의 k값을 정할 수 있다. scikit learn의 metrics 모듈에는 homogeneity_score, completeness_score, v_measure_score, adjusted_rand_score, silhouette_score가 있다.



<리스트 8> 다양한 k값으로 k평균 적용한 후 성능 평가 cluster_range = range(2, 15) vmeasures = [] for n_cluster in cluster_range: km = KMeans(init='k-means++', n_clusters=n_cluster, n_init=10) km.fit(X) # print '----------------------------------------------------' # print n_cluster vmeasures.append(metrics.v_measure_score(y, km.labels_)) # vmeasures.append(metrics.silhouette_score(X, km.labels_, metric='euclidean'))

버스 정거장에 대한 정확한 목적 변수는 없지만 버스의 출발 지역과 종착 지역을 바탕으로 버스 번호를 만들었기 때문에 목적 변수로 완벽하지 않지만 사용할 수 있다. 이 경우 목적 변수를 버스 번호라고 하면 v_measure_score 함수를 사용할 수 있다. v_measure_score 함수에는 버스 번호인 목적 변수 y와 k평균으로 군집한 km.labels_를 넣는다. k값에 따른 V-measure을 시각화 해 보자.



<리스트 9> 다양한 k값에 따른 V-measure값 import matplotlib.pyplot as plt plt.plot(cluster_range, vmeasures) plt.xlabel('# cluster') plt.ylabel('v measure') plt.autoscale(tight=True) plt.grid() plt.show()

다음과 같은 시각화를 얻을 수 있다.


<그림 7> 성능 측정 그래프

그래프를 보면 군집 개수인 k가 7이 될 때까지 급격하게 능가하다가 이후 별 차이가 없어진다. 이와 같은 점을 엘보우 점(elbow point)라 하며 이 값으로 k값을 사용할 수 있다. 그럼 다시 k값을 7로 하여 군집화를 하고 시각화를 해 보도록 하자.


<그림 8> 8회 이상 공통으로 정류하는 버스 정류장의 7개 군집의 중앙점

<그림 6>과 비교해 홀로 떨어진 중앙점이 사라졌다. 일정 개수 이상 정류하는 정류장이나 군집의 개수는 분석자가 조절할 수 있다. 여러 가지를 해 보고 최적의 해를 찾으면 된다.

지금까지 k평균을 사용해서 이동 인구가 높은 정류장을 찾았고 이 정류장을 군집화하여 중앙 위치에 파출소를 설치하는 시나리오를 해 진행해 보았다. 군집화는 다양한 영역에 사용되고 있다. 다음은 군집화를 사용하여 이상치(outlier)를 찾아보도록 하겠다.



DBSCAN을 활용한 이상치 검출

DBSCAN(Density-based spatial clustering of applications with noise, http://en.wikipedia.org/wiki/DBSCAN)는 밀도 기반 공간 군집 알고리즘이다. 이 알고리즘은 노이즈를 포함한 공간 데이터를 다루는데 적합하다. 알고리즘을 소개하자면, 알고리즘의 입력 값은 ε(eps)과 minPts이다. ε는 두 인스턴스 최대 허용 거리이며 이 거리 이내에 있는 인스턴스는 이웃이라 한다. minPts는 군집이 형성하기 위해 필요한 최소의 인스턴스 개수이다. 군집을 형성하기 위해서는 minPts 개수 보다 커야 한다. ε-neighborhood은 한 인스턴스에서 거리 ε 이내에 있는 이웃 인스턴스로서, ε-neighborhood의 개수가 minPts 보다 크면 군집으로 형성되며, 반대로 작으면 노이즈를 취급한다.


<그림 9> DBSCAN 기법으로 군집화(출처: http://en.wikipedia.org/wiki/DBSCAN)

<그림 9>에서 minPts는 4이다. A는 core point로 거리 ε안에서 지정한 minPts보다 많은 수를 만족하는 점(인스턴스)이다. A의 ε-neighborhood은 자신의 거리 ε안에서 지정한 minPts보다 많은 수를 만족하는지를 검사하고 군집의 인스턴스를 늘려 나간다. 인스턴스 B, C는 주변의 core point가 있어 군집에 속하며 군집은 끝이 난다. 인스턴스 N의 경우 거리 ε안에서 지정한 minPts보다 인스턴스 개수가 작기 때문에 노이즈로 취급한다. DBSCAN의 장점은 k평균과 다르게 초기 k값이 필요하지 않고 일부 인스턴스를 노이즈로 취급한다는 점이며 더 중요한 점은 비선형으로 떨어진 군집을 찾을 수 있다는 것이다. 단점으로는 ε, minPts를 찾는 것은 쉬운 일이 아니다.



DBSCAN 기법을 활용한 버스 정거장 이상치 찾기

버스 정거장 중 일부가 멀리 떨어져 있다면 k평균을 사용하여 군집화 할 경우 군집의 중앙점에 영향을 미친다. 즉, 일부 정거장이 군집내의 정거장과 거리가 멀다면 이로 인해 군집내의 평균인 중앙점이 떨어진 버스 정?? 찾는다. scikit learn의 cluster 모듈에는 DBSCAN 클래스가 있다. 앞에서 말한 듯이, DBSCAN 클래스를 eps(ε)와 minPts로 초기화한다. 그전에 정확도와 계산을 편의성을 위해 입력값을 표준화(Standardization)하도록 한다.



<리스트 10> DBSCAN에 다양한 매개 변수를 적용하여 이상치 검출 from sklearn.preprocessing import StandardScaler X['sx'] = X.x X['sy'] = X.y ss = StandardScaler() X['sx'] = ss.fit_transform(X.sx) X['sy'] = ss.fit_transform(X.sy) Param = namedtuple('Param', ['eps', 'min_samples']) params = [Param(0.45, 2), Param(0.30, 2), Param(0.35, 2), Param(0.40, 2), Param(0.45, 4), Param(0.30, 4), Param(0.35, 4), Param(0.40, 4), Param(0.45, 3), Param(0.30, 3), Param(0.35, 3), Param(0.40, 3)] for param in params: dbscan = DBSCAN(eps=param.eps, min_samples=param.min_samples).fit(X[['sx', 'sy']].values) labels = dbscan.labels_ outliers = X[labels == -1] print(param) print(outliers) print("V-measure: %0.3f" % metrics.v_measure_score(y, labels)) # print(metrics.silhouette_score(X, labels, metric='euclidean'))

k평균과 마찬가지로 매개 변수를 변경하여 최적의 eps, min_samples를 구한다. eps=0.3, min_samples=3일 때 최적이다. 이를 기반으로 이상치를 찾아 시각화하면 다음과 같다.


<그림 10> DBSCAN을 적용하여 이상치 검출 후 시각화

세 개의 정거장을 찾았다. <그림 4>와 비교해 보면 다른 정거장에 비교하여 군집화되어 않고 못하고 따로 떨어진 정거장이다. 이 이상치를 제거하여 정거장에 대한 k평균을 구하면 좀 더 성능이 개선된다.



결론

지금까지 비지도 학습인 군집화를 알아보았다. k평균 기법을 적용하여 서울시에서 유동 인구가 높은 정거장을 군집화하였고 DBSCAN 기법을 적용하여 이상치인 동 떨어진 정거장을 검출하였다. 간단한 예제에서 보았듯이 군집화는 다양한 영역에서 사용할 수 있고 의미있는 지식을 찾아 낼 수 있다. 예제에 사용한 코드는 https://github.com/brenden17/bus/blob/master/busstop_analysis.py에서 볼 수 있다.



출처 : 마이크로소프트웨어 11월호

제공 : 데이터전문가 지식포털 DBguide.net