기계 학습의 A to Z
scikit-learn을 활용한 기계 학습 (차원 축소)
기념일에 좋은 레스토랑에 가서 메뉴판을 본다. 음식이름, 종류, 가격, 원료, 원산지 등 다섯 가지 설명이 보인다. 기념일이라 가격에 크게 신경 쓰지 않는 대신, 원료와 원산지를 고려하며 취향껏 고른다. 30가지가 넘는 음식들에서 원료와 원산지 위주로 보니 수월하게 음식을 결정할 수 있다.
이와 같이 관심 있는 부분으로 조건을 좁히면 취향에 맞는 음식을 빠르게 선택할 수 있다. 하지만 모든 음식에 대해 다섯 가지 조건을 다 고려한다면 시간이 더 걸린다. 그리고 다른 조건을 고려하면 자신의 취향이 아닌 음식을 먹을 수도 있다.
차원의 저주(Curse of dimensionality)
레스토랑 예제는 일상적인 이야기이지만 차원 축소와 연관이 깊다. 기계 학습이나 통계학에서는 ‘차원의 저주’를 해결하고자 차원을 줄인다. 차원의 저주란 차원이 낮을 때 일어나지 않던 문제들이 높은 차원에서 데이터를 분석하거나 구조화할 때 나타나는 현상을 의미한다. 얼핏 생각하면 더 많은 속성들이 더 정확한 결과를 낼 수 있을 것이라고 생각할 수 있지만 꼭 그렇지만은 않다.
일반적으로 차원을 줄이면 몇 가지를 이점을 얻을 수 있다. 데이터가 클 경우 기법에서 사용하는 모델이 모든 데이터를 고려해야 하기 때문에 시간과 컴퓨팅 파워가 많이 필요하다. 마치 메뉴판 음식에 대한 모든 정보를 고려해야 하기 때문에 늦어지는 것과 같다. 불필요한 요소인 노이즈를 제거하게 되어 최종적으로 더 나은 결과를 얻는다. 다른 이점은 시각화이다.
4차원 이상에서는 데이터를 시각화 할 수 없기 때문에 2, 3차원으로 데이터를 시각화한다면 좀 더 직관적으로 데이터를 분석할 수 있다. 음식 종류가 30개이고 음식에 대한 정보는 다섯 개이기 때문에 30X5 행렬이 되지만 내가 관심 있는 원료와 원산지만을 고려한다면 30X2 행렬이 돼 처리 용량과 속도가 개선된다. 그리고 불필요한 요소도 제거할 수 있다. 추가적인 이점으로 속도가 개선되면 훈련이 빨라지고 다른 기법을 적용할 여지가 생겨 같은 시간에서 더 많은 실험을 할 수 있다.
차원 축소(Dimension Reduction)
차원 축소는 속성 선택(feature selection)과 속성 추출(feature extraction)로 나뉜다. 속성 선택은 목적 속성에 가장 영향을 미치는 일부 속성을 선택해 차원을 줄이는 방법이다. 속성 추출은 속성의 특성을 모두 고려해 원하는 수만큼 줄인다. 각각에 대해 살펴보기 전 이전부터 사용해 왔던 붓꽃(iris) 데이터를 좀 더 살펴본다.
Pandas, 파이썬으로 구현한 데이터 분석 라이브러리
데이터를 분석할 때 가장 기본적으로 할 수 있는 작업은 각 속성에 대한 평균, 분산, 최대값, 최소값을 구하는 일이다. 이 작업과 더불어 데이터를 이렇게 저렇게 변경해야 할 일이 종종 생긴다. 이러한 분석을 편하게 해 주는 라이브러리가 pandas(http://pandas.pydata.org/)이다. pandas는 R의 DataFrame과 유사하여 R을 사용해 본 경험이 있는 사용자는 쉽게 사용할 수 있으며 경험이 없다하더라도 약간의 함수나 메소드를 익히면 쉽게 사용할 수 있다. pandas는 데이터를 쉽게 조작하게 할 수 있는 기능을 제공할 뿐만 아니라 간단하게 데이터를 시각화 할 수 있도록 해준다. https://pypi.python.org/pypi/pandas/0.14.0/#downloads에서 윈도우용 설치자를 내려 받아 설치한다. 우선 사용할 붓꽃 데이터를 로드하고 pandas의 DataFrame에 데이터를 넣은 다음, 붓꽃 데이터의 기본 정보를 얻는다.
<리스트 1> pandas로 붓꽃 데이터 분석 import matplotlib.pyplot as plt import pandas as pd from sklearn.datasets import load_iris iris = load_iris() X, y = iris.data, iris.target iris_df = pd.DataFrame(X, columns=list(iris.feature_names)) print(iris_df.describe()) iris_df.boxplot() iris_df.plot() plt.show()
<리스트 1>의 코드로 <리스트 2>의 4개의 속성에 대한 평균(mean), 표준편차(std), 최소값(min), 최대값(min)을 얻는다. 첫 번째 속성인 꽃받침 길이(sepal length)의 총 개수는 150개이며 평균은 5.84, 표준편차는 0.82다.
<리스트 2> 붓꽃 데이터의 기술 통계 sepal length (cm) sepal width (cm) petal length (cm) \ count 150.000000 150.000000 150.000000 mean 5.843333 3.054000 3.758667 std 0.828066 0.433594 1.764420 min 4.300000 2.000000 1.000000 25% 5.100000 2.800000 1.600000 50% 5.800000 3.000000 4.350000 75% 6.400000 3.300000 5.100000 max 7.900000 4.400000 6.900000 petal width (cm) count 150.000000 mean 1.198667 std 0.763161 min 0.100000 25% 0.300000 50% 1.300000 75% 1.800000 max 2.500000
다음으로, iris_df.boxplot()를 사용해 데이터를 시각화한다.

<그림 1>은 상자 수염(boxplot whisker)으로 상자 아래의 선은 최소값, 상자의 밑변은 25%에 해당하는 수치, 상자의 중앙선은 중앙값(50%), 상자의 윗변은 75%에 해당하는 수치, 마지막으로 상자 위의 선은 최대값이 된다. 따라서, 꽃받침 너비(sepal width)의 경우, 데이터가 아주 밀집되어 있고, 꽃잎 길이(petal length)는 데이터가 넓게 퍼져 있음을 알 수 있다. iris_df.plot()사용한 <그림 2>를 통해서는 각 속성에 따른 목적 속성의 변화를 볼 수 있다.

이 도표에서 재미있는 점은 도표의 x축의 데이터의 번호에?? 너비(petal width)는 데이터 번호가 따라 어느 정도 구별할 수 있다. 즉, 0에서 50까지 한 분류, 51에서 100까지 한 분류, 101에서 150까지 한 분류로 분포되어 있어 이를 이용해서 구별할 수 있다. 시각화와 통계를 통해 데이터를 어느 정도 파악할 수 있다. <그림 2>로 꽃잎 길이와 꽃잎 너비가 목적 속성인 꽃의 분류와 어느 정도 선형적인 관계가 있음을 알 수 있다. 이러한 관계는 상관 관계 분석(http://ko.wikipedia.org/wiki/상관분석)을 통해 알 수 있다. 상관 관계 분석이란 두 변수 간에 선형적 관계를 분석하는 방법으로 상관 관계의 정도를 상관 계수로 나타낸다. 꽃잎 길이와 목적 속성인 꽃의 분류가 선형적인 관계를 보이기 때문에 상관 계수는 최대값인 1에 가깝지만 꽃받침 너비(sepal width)는 꽃의 분류와 선형적인 관계가 비교적 적기 때문에 0에 가깝다. 그러면 각 속성과 목적 속성간의 상관 계수를 구한다.
<리스트 3> 붓꽃 데이터 속성의 상관 계수 y1 = np.vstack(y) iris_df = pd.DataFrame(np.hstack((X, y1)), columns=iris.feature_names+['target']) iris_df.corr()
꽃의 분류인 목적 속성과 iris의 속성을 합해 새로운 DataFrame을 만들고 iris_df.corr()를 호출한다. <표 1>과 같이 꽃잎 길이와 너비는 목적 속성과 선형적인 관계가 있다. 통계적으로 꽃잎 길이와 꽃잎 너비를 고려하여 꽃의 종류를 판단한다면 다른 속성을 고려하는 것보다 더 나은 결과를 얻을 수 있다.
속성 선택(Feature selection)
속성 선택은 속성 중 일부 속성을 그대로 선택하는 것을 말한다. 우리가 다루는 붓꽃 데이터는 4개의 속성과 데이터 수가 150개 밖에 되지 않는 비교적 작은 데이터이지만 만약 속성 수가 많다면 속성의 관계를 분석하여 선택한다. 네 가지 속성 중에서 가장 목적 속성에 선형적 관계를 가지고 있는 꽃잎 길이와 꽃잎 너비만을 선택하여 이전에 사용했던 kNN(k 최근접 이웃 알고리즘, k- Nearest Neighbors algorithm)에 넣어 본다.
<리스트 4> 상관 계수에 따른 속성 선택 적용 후 kNN 실행 from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import Pipeline from sklearn.cross_validation import KFold from sklearn.grid_search import GridSearchCV iris = datasets.load_iris() X, y = iris.data, iris.target # 0.953 # X = iris_df.iloc[:, [0,1]] # 0.733 # X = iris_df.iloc[:, [2,3]] # 0.947 kf = KFold(len(y), n_folds=10) ## KNN estimators = [('knn', KNeighborsClassifier())] parameters = {'knn__n_neighbors':(7,), 'knn__weights':('distance',)} pipeline = Pipeline(estimators) grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, cv=kf) grid_search.fit(X, y) print("Best score: %0.3f" % grid_search.best_score_)
붓꽃 데이터를 그대로 사용해 kNN에 넣어 실행하면 약 95.3%정도 정확하게 예측한다. iris_df.iloc[:, [2,3]]로 꽃잎 길이와 꽃잎 너비만을 선택해 실행하면 약 94.7%의 정확성을 갖는 반면, 꽃받침 길이와 꽃받침 너비로 선택하여 실행하면 약 73%정도 정확하게 예측한다. 두 개로 속성을 줄여 실행 시간을 줄였으나 네 개 속성을 전부 사용한 결과보다 약 0.6%가 적게 나왔다.
왜 이러한 결과가 나왔을까? 분명, 좀 더 분별력 있는 속성으로 줄여 실행했는데 결과는 더 나쁘게 나왔다. 그 이유는 다른 나머지 속성에도 꽃의 종류를 분별할 수 있는 정보가 들어 있기 때문이다. 이를 테면, 암을 일으키는 원인을 찾는다고 가정한다. 여러 가지 속성 중에 흡연, 음주, 불면, 스트레스 등은 발암의 중요한 원인이다. 각각의 요소들이 발암의 주요한 요소이지만 각 요소들이 복합적으로 일어나면 발암율은 매우 높게 나타난다. 따라서 하나 하나의 속성이 목적 속성에 영향을 미치지만 함께 일어날 경우, 좀 더 강하게 영향을 미치게 된다.
속성 추출(Feature extraction)
속성 추출은 특별히 속성 선택을 할 수 없거나 속성을 선택했더라도 속성이 너무 많을 경우 사용한다. 속성 추출은 수학적으로 행렬 분해(matrix decomposition, http://ko.wikipedia.org/wiki/행렬분해)과 뗄 수 없는 관계이기 때문에 선형 대수(linear algebra, http://ko.wikipedia.org/wiki/선형대수학)을 보면 좀 더 쉽게 이해 할 수 있다. 기본적인 PCA, LDA, FA를 살펴보도록 하겠다.
● PCA(주성분 분석, (principal component analysis)
PCA는 주성분 분석은 데이터를 분산이 가장 커지는 새로운 첫 번째 축으로 사상시키고 두 번째로 커지는 축으로 사상시키는 등 원하는 차수만큼의 새로운 축으로 만들어 새로운 좌표계로 데이터를 선형 변환한다(http://ko.wikipedia.org/wiki/주성분_분석). 비록, 비선형적인 데이터에는 적용할 수 없지만 kernel PCA를 사용하면 비선형 데이터에도 적용할 수 있다. <리스트 5>를 통해 붓꽃 데이터를 PCA에 적용하고 시각화한다.
<리스트 5> 붓꽃 데이터에 PCA 적용 from sklearn.decomposition import PCA pca = PCA(n_components=2) X_r = pca.fit_transform(X) for c, i, target_name in zip("rgb", [0, 1, 2], target_names): plt.scatter(X_r[y == i, 0], X_r[y == i, 1], c=c, label=target_name) plt.legend(loc="best") plt.title('PCA of IRIS dataset')
위의 코드에서 차수를 2로 PCA(n_components=2)를 생성하고 iris 데이터를 넣어 새로운 2차원의 X_r들 만든다.

4차원 데이터를 2차원으로 변환하여 시각화 할 수 있으며, 비교적 쉽게 3개의 꽃의 종류를 구별할 수 있다.
● LDA(선형 판별 분석, Linear Discriminant Analysis)
LDA은 분류돼 있는 데이터를 가장 잘 분별할 수 있는 새로운 축에 사상하는 방법이다. PCA와 다른 점은 데이터가 분류되어 있어야 한다는 점이다. LDA도 선형적인 데이터를 변환할 수 있다. scikit learn 라이브러리에서 LDA는 차원 축소 역할과 더불어 하나의 분류 기법으로 사용할 수 있다.
<리스트 6> 붓꽃 데이터에 LDA 적용 from sklearn.lda import LDA lda = LDA(n_components=2) X_r2 = lda.fit(X, y).transform(X) for c, i, target_name in zip("rgb", [0, 1, 2], target_names): plt.scatter(X_r2[y == i, 0], X_r2[y == i, 1], c=c, label=target_name) plt.legend(loc="best") plt.title('LDA of IRIS dataset')
<리스트 6>의 코드는 차수를 2로 LDA(n_components=2)를 생성하고 iris 데이터를 넣어 새로운 2차원의 X_r2를 만든다. 
<그림 4>를 보면 PCA보다 좀 더 분별력이 있게 나타나는 것을 알 수 있다. 붓꽃 데이터가 꽃의 종류별로 구분되어 있기 때문이다.
● FA (요인 분석, Factor Analysis)
요인 분석은 속성 중에서 잠재된 몇 개의 속성(요인)을 찾아내는 방법이다. 다시 말해, 속성간의 상관 관계가 있는 속성을 찾아 잠재된 속성으로 나타낸다.
<리스트 7> 붓꽃 데이터에 FA 적용 from sklearn.decomposition import FA fa = FactorAnalysis(n_components=2) X_r3 = fa.fit_transform(X) for c, i, target_name in zip("rgb", [0, 1, 2], target_names): plt.scatter(X_r3[y == i, 0], X_r[y == i, 1], c=c, label=target_name) plt.legend(loc="best") plt.title('FA of IRIS dataset')
다른 클래스와 같은 방법으로 2차원 데이터로 변환한다.

지금까지 3가지 속성 추출 기법을 살펴봤다. 모든 선형 데이터를 축소하는 방법이다. MSD와 kernel PCA는 비선형 데이터를 축소한다. 그러면 이렇게 축소한 데이터를 kNN에 적용하여 얼마나 정확도를 높일 수 있는지 살펴보자. 다음 코드는 붓꽃 데이터를 PCA, LDA, FA에 적용하여 2차원으로 줄이고 kNN를 사용하여 예측력을 측정했다.(<리스트 8> 참조)
<리스트 8> PCA, LDA, FA에 적용후 kNN 실행 from datetime import datetime from sklearn.neighbors import KNeighborsClassifier from sklearn.decomposition import PCA, FactorAnalysis from sklearn.lda import LDA from sklearn.pipeline import Pipeline from sklearn.svm import SVC from sklearn.linear_model import LogisticRegression from sklearn.cross_validation import KFold from sklearn.grid_search import GridSearchCV iris = datasets.load_iris() X, y = iris.data, iris.target kf = KFold(len(y), n_folds=10) def grid_search_cv(estimators, parameters, title): print("===========================================================") print(title) pipeline = Pipeline(estimators) grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, cv=kf) tstart = datetime.now() grid_search.fit(X, y) t = datetime.now() -datetime.now() print("time : %f" % t.microseconds) print("Best score: %0.3f" % grid_search.best_score_) ## KNN knn_estimators = [('knn', KNeighborsClassifier())] knn_parameters = {'knn__n_neighbors':(7,), 'knn__weights':('distance',)} grid_search_cv(knn_estimators, knn_parameters, 'Only KNN') # KNN with PCA knn_pca_estimators = [('pca', PCA()), ('knn', KNeighborsClassifier())] knn_pca_parameters = {'knn__n_neighbors':(7,), 'knn__weights':('distance',)} # knn_pca_parameters = {'pca__n_components':(2, 3), 'knn__n_neighbors': (6,7,8,9,10,11,12,13), 'knn__weights':('uniform', 'distance')} grid_search_cv(knn_pca_estimators, knn_pca_parameters, 'KNN with PCA') # KNN with FA knn_fa_estimators = [('fa', FactorAnalysis()), ('knn', KNeighborsClassifier())] knn_fa_parameters = {'knn__n_neighbors':(7,), 'knn__weights':('distance',)} # knn_fa_parameters = {'fa__n_components':(2, 3), 'knn__n_neighbors': (6,7,8,9,10,11,12,13), 'knn__weights':('uniform', 'distance')} grid_search_cv(knn_fa_estimators, knn_fa_parameters, 'KNN with FA') # LDA knn_lna_estimators = [('lda', LDA()), ('knn', KNeighborsClassifier())] knn_fa_parameters = {'knn__n_neighbors':(7,), 'knn__weights':('distance',)} # knn_lna_parameters = {'lda__n_components':(2, 3), 'knn__n_neighbors': (6,7,8,9,10,11,12,13), 'knn__weights':('uniform', 'distance')} grid_search_cv(knn_lna_estimators, knn_fa_parameters, 'KNN with LDA') # SVM svm_estimators = [('svm', SVC())] svm_parameters = {'svm__C':(0.4, 1, 1.5, 2, 3), 'svm__kernel':('linear', 'rbf', 'poly')} grid_search_cv(svm_estimators, svm_parameters, 'only SVM') # SVM svm_estimators = [('pca', PCA()), ('svm', SVC())] svm_parameters = {'pca__n_components':(2, 3), 'svm__C':(0.4, 1, 1.5, 2, 3), 'svm__kernel':('linear', 'rbf', 'poly')} grid_search_cv(svm_estimators, svm_parameters, 'SVM with PCA') ## LogisticRegression lr_estimators = [('pca', PCA()), ('lr', LogisticRegression())] lr_parameters = {'pca__n_components':(2, 3), 'lr__penalty':( 'l1', 'l2')} grid_search_cv(lr_estimators, lr_parameters, 'LR with PCA')
<리스트 9>는 결과다. 단지 kNN을 사용할 때는 약 95% 정확도이지만 LDA를 적용하고 kNN을 실행하면 약 97.3%로 약 2% 더 정확하게 예측한다. 더불어 다른 기법인 SVM만 사용할 경우도 96.7%이였지만 PCA를 적용하여 차수를 줄이고 적용하면 약 98%로 성능 개선이 된다. 실행시간도 차원 축소와 기법을 같이 실행하였지만 kNN만 실행할 때와 거의 같다.
<리스트 9> 실행 결과 ===================================================================== Only KNN Fitting 10 folds for each of 1 candidates, totalling 10 fits time : 999966.000000 Best score: 0.953 ===================================================================== KNN with PCA Fitting 10 folds for each of 1 candidates, totalling 10 fits time : 999969.000000 Best score: 0.953 ===================================================================== KNN with FA Fitting 10 folds for each of 1 candidates, totalling 10 fits time : 999961.000000 Best score: 0.927 ===================================================================== KNN with LDA Fitting 10 folds for each of 4 candidates, totalling 40 fits time : 999969.000000 Best score: 0.973 ===================================================================== only SVM Fitting 10 folds for each of 15 candidates, totalling 150 fits time : 999978.000000 Best score: 0.967 ===================================================================== SVM with PCA Fitting 10 folds for each of 30 candidates, totalling 300 fits time : 999978.000000 Best score: 0.980 ===================================================================== LR with PCA Fitting 10 folds for each of 4 candidates, totalling 40 fits time : 999969.000000 Best score: 0.887
결론
데이터에 따라 속성의 개수는 다양하다. 속성이 많을수록 예측력이 증가할 것으로 생각하기 쉬우나 꼭 그렇지만은 않다. 더욱이, 차원의 저주로, 사용할 기법에 실행 속도와 다양한 문제가 발생 할 수도 있다. 이러한 문제를 해결하기 위해 차원 축소를 할 수 있으며, 속성 선택과 속성 추출을 사용해 보았다. 붓꽃 데이터를 2차원으로 줄이기 위해 PCA, LDA, FA를 사용하였으며 이렇게 축소된 데이터를 kNN에 적용하여 정확도가 높아짐을 보았다. 실행 코드는 https://github.com/brenden17/iris에서 볼 수 있다.
출처 : 마이크로소프트웨어 9월호
제공 : 데이터전문가 지식포털 DBguide.net