위키북스의 파이썬 머신러닝 완벽 가이드 책을 토대로 공부한 내용입니다.
결정 트리는 ML 알고리즘 중 직관적으로 이애하기 쉬운 알고리즘이다. 데이터에 있는 규칙을 학습을 통해 자동으로 찾아내는 트리(Tree) 기반의 분류 규칙을 만드는 것이다. 일반적으로 규칙을 가장 쉽게 표현하는 방법은 if/else 기반으로 나타내는 것인데, 이 룰 기반의 프로그램에 적용되는 if/else를 자동으로 찾아내서 예측을 위한 규칙을 만드는 알고리즘으로 이해하면 쉽다. 따라서 데이터의 어떤 기준을 바탕으로 규칙을 만들어 내는지에 따라 알고리즘의 성능이 크게 좌우된다.
위 이미지는 결정 트리의 구조를 간략하게 나타낸 것이다. 규칙 노드(Decision Node)는 규칙 조건이 되고, 리프 노드(Leaf Node)는 결정 class 값이다. 그리고 새로운 구칙 조건마다 서브 트리(Sub Tree)가 생성된다. dataset에 feature가 있고, 이러한 feature가 결합하여 규칙 조건을 만들 때마다 규칙 노드가 만들어지는데 많은 규칙이 있다는 것은 그만큼 결정 트리가 복잡해진다는 것이기 때문에 과적합이 되기 쉽다. 즉, 트리의 깊이(depth)가 깊어질수록 결정 트리의 예측 성능이 저하될 수 있다. 따라서 가능한 적은 결정 노드로 높은 정확도를 가져야 하기 때문에 데이터를 분류할 때 최대한 많은 dataset이 해당 분류에 속할 수 있도록 결정 노드의 규칙이 정해져야 한다. 이를 위해서 트리(Tree)를 어떻게 분할(Split)할 지가 중요한데 최대한 균일한 dataset을 구성할 수 있도록 분할하는 것이 필요하다.
위의 이미지에서 C가 가장 균일도가 높고 그 다음이 B, A 순이다. dataset의 균일도는 데이터를 구분하는 데 필요한 정보의 양에 영향을 미친다. 무작위로 dataset C에서 데이터를 뽑는다면 데이터에 대한 정보가 없어도 검은 공일 것이라고 간단하게 예측이 가능하다. 하지만 A에서 뽑는다면 상대적으로 균일도가 낮아서 같은 조건으로 데이터를 판단할 경우 더 많은 정보가 필요하다.
결정 노드는 정보 균일도가 높은 dataset을 먼저 선택할 수 있도록 규칙 조건을 만든다. 즉, 정보 균일도가 dataset이 쪼개질 수 있도록 조건을 찾아 서브 dataset을 만들고, 다시 서브 dataset에서 균일도가 높은 자식 dataset으로 쪼개는 방식을 자식 트리로 내려가면서 반복하는 방식으로 데이터 값을 예측한다. 이러한 정보의 균일도를 측정하는 대표적인 방법은 엔트로피를 이용한 정보 이득(Information Gain) 지수와 지니 계수가 있다.
- 정보 이득은 엔트로피라는 개념을 기반으로 한다. 엔트로피는 주어진 데이터 집합의 혼잡도를 의미하는데, 서로 다른 값이 섞여 있으면 엔트로피가 높고, 같은 값이 섞여 있으면 엔트로피가 낮다. 정보 이득 지수는 1에서 엔트로피 지수를 뺀 값이다. 즉, 1 - 엔트로피 지수이다. 결정 트리는 이 정보 이득 지수로 분할 기준을 정한다. 즉, 정보 이득이 높은 속성을 기준으로 분할한다.
- 지니 계수는 원래 경제학에서 불평등 지수를 나타낼 때 사용하는 계수이다. 경제학자인 코라도 지니(Corrado Gini)의 이름에서 딴 계수로서 0이 가장 평등하고 1로 갈수록 불평등하다. 머신러닝에 적용될 때는 지니 계수가 낮을수록 데이터 균일도가 높은 것으로 해석해 지니 계수가 낮은 속성을 기준으로 분할한다.
사이킷런에서 구현한 결정 트리 알고리즘인 DecisionTreeClassifier는 기본으로 지니 계수를 이용해 dataset을 분할한다. 결정 트리의 일반적인 알고리즘은 dataset을 분할하는 데 가장 좋은 조건인 정보 이득이 높거나 지니 계수가 낮은 조건을 찾아서 자식 트리 노드에 걸쳐 반복적으로 분할한 뒤, 데이터가 모두 특정 분류에 속하게 되면 분할을 멈추고 분류를 결정한다.
결정 트리의 가장 큰 장점은 정보의 균일도를 기반으로 하고 있어 알고리즘이 쉽고 직관적이라는 것이다. 균일도라는 룰이 매우 명확하기 때문에 규칙 노드와 리프 노드가 어떻게 만들어지는지 알 수 있고, 시각화도 가능하며 다른 룰이 없기 때문에 각 feature의 스케일링이나 정규화 같은 전처리 작업이 필요없다. 하지만 가장 큰 단점으로는 과적합에 걸리기 쉽다는 것이다. 균일도에 따라 서브 트리를 계속 만들게 되면 feature가 많아지고 트리의 깊이가 커져서 복잡해진다. 실제로 모든 데이터의 상황을 만족하는 완벽한 규칙을 만들지 못하는 경우가 더 많음에도 결정트리 알고리즘은 학습 데이터에 대해 모델의 정확도를 높이기 위해 계속해서 조건을 추가하여 복잡한 모델이 된다. 복잡한 모델은 실제 상황에서 유연한 대처가 어렵기 때문에 예측 성능이 떨어질 수 밖에 없다. 따라서 완벽한 규칙을 만들 수 없다고 인정하여 트리의 크기를 사전에 제한하는 것이 성능 튜닝에 더 도움이 될 수 있다.
결정 트리 장점 결정 트리 단점 - 쉽고 직관적이다.
- feature의 스테일링이나 정규화 등의 사전
가공 영향도가 크지 않다. - 과적합으로 알고리즘 성능이 떨어진다. 이를 극복하기
위해 트리의 크기를 사전에 제한하는 튜닝 필요
Graphviz 패키지는 결정 트리 알고리즘이 어떠한 규칙을 가지고 트리를 생성하는지 시각적으로 보여준다. 사이킷런은 이 Graphviz 패키지와 쉽게 인터페이스할 수 있도록 export_graphviz() API를 제공한다. export_graphviz()는 함수 인자로 학습이 완료된 Extimator, feature의 이름 리스트, label 이름 리트스를 입력하면 학습된 결정 트리 규칙을 실제 트리 형태로 시각화해 보여준다.
이전의 붓꽃 dataset에 결정 트리를 적용했을 때 어떻게 서브 트리를 구성되었는지 시각화해보았다.
from sklearn.datasets import load_iris from sklearn.tree import export_graphviz from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split import warnings warnings.filterwarnings('ignore') # DecisionTree Classifier 생성 dt_clf = DecisionTreeClassifier(random_state=156) # 붓꽃 데이터를 로딩하고, 학습과 테스트 데이터 셋으로 분리 iris_data = load_iris() X_train , X_test , y_train , y_test = train_test_split(iris_data.data, iris_data.target, test_size=0.2, random_state=11) # DecisionTreeClassifer 학습. dt_clf.fit(X_train , y_train) # export_graphviz()의 호출 결과로 out_file로 지정된 tree.dot 파일을 생성함. export_graphviz(dt_clf, out_file="tree.dot", class_names=iris_data.target_names, feature_names=iris_data.feature_names, impurity=True, filled=True)
[output] export_graphviz()는 Graphviz가 읽어 들여서 그래프 형태로 시각화할 수 있는 출력 파일을 생성한다. 위의 코드에서는 "tree.dot" 파일을 생성하였다.
import graphviz # 위에서 생성된 tree.dot 파일을 Graphviz 읽어서 Jupyter Notebook상에서 시각화 with open("tree.dot") as f: dot_graph = f.read() graphviz.Source(dot_graph)
[output] 출력된 결과로 트리의 브랜치(branch) 노드와 말단 리프(leaf) 노드가 어떻게 구성되어 있는지 알 수 있다. 자식 노드가 있으면 브랜치 노드이고, 없으면 리프 노드이다. 브랜치 노드를 기준으로 노드 내의 지표들의 의미를 설명하겠다.
- petal length(cm) <= 2.45 와 같이 feature의 조건이 있는 것은 자식노드를 만들기 위한 규칙 조건이다. 리프 노드에는 없는 지표이다.
- gini는 다음의 value안의 데이터 분포에서의 지니 계수이다.
- samples는 현재 노드의 데이터 수이다.
- value는 class 값 기반의 데이터 수이다. 붓꽃 dataset은 class값으로 0, 1, 2 가지기 때문에 길이가 3이다.
그리고 트리 구조의 첫 줄 노드부터 깊이(depth)를 1로 새어 위의 이미지와 같은 트리 구조는 깊이가 6인 트리 구조이며, depth가 낮고 왼쪽에 있는 노드부터 번호를 매겨 순서를 정하여 루트 노드는 1번 노드로 불린다.
4번 노드를 보면 value가 [0, 37, 1]로 되어 이를 구분하기 위해 자식노드를 다시 생성한다. 이처럼 결정 트리는 완벽한 규칙을 찾기 위해 트리 노드를 계속 만들기 때문에 매우 복잡한 트리 구조를 만들기 쉬워져서 과적합이 상당히 많이 일어나는 ML 알고리즘이다. 그래서 결정 트이 알고리즘을 제어하는 대부분의 hyper parameter는 복잡한 tree가 생성되는 것을 막기 위한 용도이다.
- max_dapth
dt_clf = DecisionTreeClassifier(random_state=156, max_depth=3)
[output] 최대 depth를 3으로 고정시켜 굉장히 간단한 트리 구조로 바뀌었다.
- min_samples_split
dt_clf = DecisionTreeClassifier(random_state=156, min_samples_split=4)
[output] min_samples_split는 자식 노드를 만들기 위한 최소한의 samples의 수이다. 즉, 4로 설정하면 최소 4개의 sample이 있어야 자식 노드를 만들 수 있다.
- min_samples_leaf
dt_clf = DecisionTreeClassifier(random_state=156, min_samples_leaf=4)
[output] 리프 노드는 자식 노드가 없는 노드를 말하는데 min_samples_split는 리프 노드가 될 수 있는 최소한의 samples의 수를 말한다. 따라서 위와 같은 경우엔 분할하여 자식 노드로 만들어 질 경우 해당 자식 노드들 안의 samples의 수가 최소 4 이상이어야 한다.
결정 트리는 균일도에 기반하여 어떠한 속성을 규칙 조건으로 선택하느냐가 중요한 요건이다. 중요한 feature들이 명확한 규칙 트리를 만드는 데 크게 기여하며, 모델을 간결하게 만들어 준다. 사이킷런은 이러한 규칙을 정하는데 있어 각 feature의 중요도를 featureimportances를 통해 제공한다.
import seaborn as sns import numpy as np %matplotlib inline # feature importance 추출 print("Feature importances:\n{0}".format(np.round(dt_clf.feature_importances_, 3))) # feature별 importance 매핑 for name, value in zip(iris_data.feature_names , dt_clf.feature_importances_): print('{0} : {1:.3f}'.format(name, value)) # feature importance를 column 별로 시각화 하기 sns.barplot(x=dt_clf.feature_importances_ , y=iris_data.feature_names)
[output] featureimportances로 각 feature 별로 중요도 값을 구하고 막대그래프로 시각화하였다.
결정 트리가 어떻게 학습 데이터를 분할해 예측을 수행하고 이로 인한 과적합 문제를 시각화해보겠다. 먼저 분류를 위한 dataset을 만들어야 하는데 사이킷런은 분류를 위한 test용 data를 쉽게 만들 수 있도록 make_classification() 함수를 제공한다.
from sklearn.datasets import make_classification import matplotlib.pyplot as plt %matplotlib inline plt.title("3 Class values with 2 Features Sample data creation") # 2차원 시각화를 위해서 feature는 2개, 결정값 클래스는 3가지 유형의 classification 샘플 데이터 생성. X_features, y_labels = make_classification(n_features=2, n_redundant=0, n_informative=2, n_classes=3, n_clusters_per_class=1, random_state=0) # plot 형태로 2개의 feature로 2차원 좌표 시각화, 각 클래스값은 다른 색깔로 표시됨. plt.scatter(X_features[:, 0], X_features[:, 1], marker='o', c=y_labels, s=25, cmap='rainbow', edgecolor='k')
[output] 위 코드로 2개의 feature와 3개의 class로 이루어진 dataset을 만들고 그래프로 시각화하였다. 그 다음 결정 트리 모델을 생성해 학습을 시킨 뒤 class 값을 예측하는 결정 기준을 색상과 경계로 시각화하여 나타내었다.
import numpy as np from sklearn.tree import DecisionTreeClassifier # Classifier의 Decision Boundary를 시각화 하는 함수 def visualize_boundary(model, X, y): fig,ax = plt.subplots() # 학습 데이타 scatter plot으로 나타내기 ax.scatter(X[:, 0], X[:, 1], c=y, s=25, cmap='rainbow', edgecolor='k', clim=(y.min(), y.max()), zorder=3) ax.axis('tight') ax.axis('off') xlim_start , xlim_end = ax.get_xlim() ylim_start , ylim_end = ax.get_ylim() # 호출 파라미터로 들어온 training 데이타로 model 학습 . model.fit(X, y) # meshgrid 형태인 모든 좌표값으로 예측 수행. xx, yy = np.meshgrid(np.linspace(xlim_start,xlim_end, num=200),np.linspace(ylim_start,ylim_end, num=200)) Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape) # contourf() 를 이용하여 class boundary 를 visualization 수행. n_classes = len(np.unique(y)) contours = ax.contourf(xx, yy, Z, alpha=0.3, levels=np.arange(n_classes + 1) - 0.5, cmap='rainbow', clim=(y.min(), y.max()), zorder=1) # 특정한 트리 생성 제약없는 결정 트리의 Decsion Boundary 시각화. dt_clf = DecisionTreeClassifier().fit(X_features, y_labels) visualize_boundary(dt_clf, X_features, y_labels)
[output] 일부 이상치(Outlier) 데이터까지 분류하기 위해 분할이 많이 일어나 결정 기준 경계가 많아진 것을 볼 수 있다. 따라서 이번엔 min_samples_leaf를 6으로 설정하고 해보았다.
# min_samples_leaf=6 으로 트리 생성 조건을 제약한 Decision Boundary 시각화 dt_clf = DecisionTreeClassifier( min_samples_leaf=6).fit(X_features, y_labels) visualize_boundary(dt_clf, X_features, y_labels)
[output] 이상치에 반응하지 않아 좀 더 일반화된 분류 규칙에 따르게 된 것을 알 수 있다.
UCI Machine Learning Repository에서 제공하는 Human Activity Recognition dataset에 대하여 결정 트리 알고리즘을 이용해 예측 분류를 해보겠다. 해당 데이터는 30명에서 스마트폰 센서를 장착한 뒤 사람의 동작과 관련된 여러 가지 feature를 수집한 데이터이다. 따라서 결정 트리를 이용하여 어떤 동작인지를 예측하여 볼 것이다.
dataset을 내려 받으면 위 이미지와 같은 구성으로 되어 있다. README.txt와 feature_info.txt에는 dataset과 feature에 대한 간략한 설명이 되어 있고, feature.txt에는 feature의 이름이 적혀있으며 activity_labels.txt는 label 값에 대한 설명이 있다. feature는 모두 561개가 있고 공백으로 구분되어 있다. feature.txt는 feature index와 feature 이름을 가지고 있고 DataFrame으로 로딩하여 이름만 간략하게 확인해 보았다.import pandas as pd import matplotlib.pyplot as plt %matplotlib inline # features.txt 파일에는 피처 이름 index와 피처명이 공백으로 분리되어 있음. 이를 DataFrame으로 로드. feature_name_df = pd.read_csv('/content/drive/MyDrive/pymldg-rev/4장/human_activity/features.txt', sep='\s+', header=None, names=['column_index','column_name']) # 피처명 index를 제거하고, 피처명만 리스트 객체로 생성한 뒤 샘플로 10개만 추출 feature_name = feature_name_df.iloc[:, 1].values.tolist() print('전체 피처명에서 10개만 추출:') for i in range(10): print(feature_name[i])
[output] 이 DataFrame을 이용해 dataset을 불러오기 전에 중복된 feature 명에 유의해야 한다. 중복된 feature 명을 확인한 후 중복된 feature 명에는 원래 이름에 _1 또는 _2를 추가한 후에 DataFrame을 로드해야한다.
feature_dup_df = feature_name_df.groupby('column_name').count() print(feature_dup_df[feature_dup_df['column_index'] > 1].count()) feature_dup_df[feature_dup_df['column_index'] > 1].head()
[output] 42개의 중복된 feature 명이 있다.
import pandas as pd def get_new_feature_name_df(old_feature_name_df): feature_dup_df = pd.DataFrame(data=old_feature_name_df.groupby('column_name').cumcount(), columns=['dup_cnt']) feature_dup_df = feature_dup_df.reset_index() new_feature_name_df = pd.merge(old_feature_name_df.reset_index(), feature_dup_df, how='outer') new_feature_name_df['column_name'] = new_feature_name_df[['column_name', 'dup_cnt']].apply(lambda x : x[0]+'_'+str(x[1]) if x[1] > 0 else x[0], axis=1) new_feature_name_df = new_feature_name_df.drop(['index'], axis=1) return new_feature_name_df def get_human_dataset( ): # 각 데이터 파일들은 공백으로 분리되어 있으므로 read_csv에서 공백 문자를 sep으로 할당. feature_name_df = pd.read_csv('/content/drive/MyDrive/pymldg-rev/4장/human_activity/features.txt',sep='\s+', header=None,names=['column_index','column_name']) # 중복된 피처명을 수정하는 get_new_feature_name_df()를 이용, 신규 피처명 DataFrame생성. new_feature_name_df = get_new_feature_name_df(feature_name_df) # DataFrame에 피처명을 컬럼으로 부여하기 위해 리스트 객체로 다시 변환 feature_name = new_feature_name_df.iloc[:, 1].values.tolist() # 학습 피처 데이터 셋과 테스트 피처 데이터을 DataFrame으로 로딩. 컬럼명은 feature_name 적용 X_train = pd.read_csv('/content/drive/MyDrive/pymldg-rev/4장/human_activity/train/X_train.txt',sep='\s+', names=feature_name ) X_test = pd.read_csv('/content/drive/MyDrive/pymldg-rev/4장/human_activity/test/X_test.txt',sep='\s+', names=feature_name) # 학습 레이블과 테스트 레이블 데이터을 DataFrame으로 로딩하고 컬럼명은 action으로 부여 y_train = pd.read_csv('/content/drive/MyDrive/pymldg-rev/4장/human_activity/train/y_train.txt',sep='\s+',header=None,names=['action']) y_test = pd.read_csv('/content/drive/MyDrive/pymldg-rev/4장/human_activity/test/y_test.txt',sep='\s+',header=None,names=['action']) # 로드된 학습/테스트용 DataFrame을 모두 반환 return X_train, X_test, y_train, y_test X_train, X_test, y_train, y_test = get_human_dataset() print('## 학습 피처 데이터셋 info()') print(X_train.info())
[output] 중복 feature를 제외한 후 dataset을 불러왔다. 학습 dataset은 7352개로 561개의 feature를 가지고 있다. feature는 모두 float형으로 되어 있으므로 카테고리 인코딩을 하지 않아도 된다.
print(y_train['action'].value_counts())
[output] label은 총 6가지이고 고르게 분포되어 있다.
이제 결정 트리 모델을 이용하여 동작 예측 분류를 해보겠다. 먼저 hyper parameter 값을 모두 default로 설정한 뒤 학습을 하고, hyper parameter 값을 모두 추출해 보겠다.
from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score # 예제 반복 시 마다 동일한 예측 결과 도출을 위해 random_state 설정 dt_clf = DecisionTreeClassifier(random_state=156) dt_clf.fit(X_train , y_train) pred = dt_clf.predict(X_test) accuracy = accuracy_score(y_test , pred) print('결정 트리 예측 정확도: {0:.4f}\n'.format(accuracy)) # DecisionTreeClassifier의 하이퍼 파라미터 추출 print('DecisionTreeClassifier 기본 하이퍼 파라미터:') hyper_parameters = dt_clf.get_params() for hyper_parameter in hyper_parameters.items(): print(hyper_parameter)
[output] 정확도는 약 85.48%이다. 다음은 깊이(depth)가 예측 정확도에 주는 영향을 알기 위해 GridSearchCV를 이용하여 max_depth 값을 6 ~ 24로 늘리면서 예측 성능을 측정해보았다.
from sklearn.model_selection import GridSearchCV params = {'max_depth' : [6, 8 ,10, 12, 16 ,20, 24]} grid_cv = GridSearchCV(dt_clf, param_grid=params, scoring='accuracy', cv=5, verbose=1) grid_cv.fit(X_train , y_train) print('GridSearchCV 최고 평균 정확도 수치:{0:.4f}'.format(grid_cv.best_score_)) print('GridSearchCV 최적 하이퍼 파라미터:', grid_cv.best_params_) # GridSearchCV객체의 cv_results_ 속성을 DataFrame으로 생성. cv_results_df = pd.DataFrame(grid_cv.cv_results_) # max_depth 파라미터 값과 그때의 테스트(Evaluation)셋, 학습 데이터 셋의 정확도 수치 추출 cv_results_df[['param_max_depth', 'mean_test_score']]
[output] maxdepth가 16일때 85.13%로 가장 높은 정확도가 나왔다. 그리고 5개의 CV set에서 max_depth에 따라 성능이 어떻게 변하는 지 cv_results 속성을 통해 알아보았다. max_depth가 16일 때 정확도가 가장 높고 16을 넘어가면 정확도가 점점 떨어진다. 그 다음은 max_depth에 따라 test dataset에서의 정확도를 측정해보았다.
max_depths = [ 6, 8 ,10, 12, 16 ,20, 24] # max_depth 값을 변화 시키면서 그때마다 학습과 테스트 셋에서의 예측 성능 측정 for depth in max_depths: dt_clf = DecisionTreeClassifier(max_depth=depth, random_state=156) dt_clf.fit(X_train , y_train) pred = dt_clf.predict(X_test) accuracy = accuracy_score(y_test , pred) print('max_depth = {0} 정확도: {1:.4f}'.format(depth , accuracy))
[output] test dataset에서는 max_depth가 8일 때 87.07%로 가장 높은 정확도를 보였다. 그리고 max_depth가 8을 넘어가면 정확도가 점점 감소하는 것을 볼 수 있다. 이번엔 max_depth와 min_samples_split을 같이 변경하면서 정확도를 튜닝해보았다.
params = {'max_depth' : [8, 12, 16, 20], 'min_samples_split' : [16, 24]} grid_cv = GridSearchCV(dt_clf, param_grid=params, scoring='accuracy', cv=5, verbose=1 ) grid_cv.fit(X_train , y_train) print('GridSearchCV 최고 평균 정확도 수치: {0:.4f}'.format(grid_cv.best_score_)) print('GridSearchCV 최적 하이퍼 파라미터:', grid_cv.best_params_)
[output] max_depth가 8, min_samples_split이 16인 경우에 가장 높은 정확도를 보인다. 이 hyper parameter의 결정 트리 모델에 대해 test dataset에서 정확도 성능을 확인해보았다. 그리고 학습에 가장 중요한 영향을 미치는 feature 20개를 막대그래프로 시각화하였다.
best_df_clf = grid_cv.best_estimator_ pred1 = best_df_clf.predict(X_test) accuracy = accuracy_score(y_test , pred1) print('결정 트리 예측 정확도:{0:.4f}'.format(accuracy))
[output]
import seaborn as sns ftr_importances_values = best_df_clf.feature_importances_ # Top 중요도로 정렬을 쉽게 하고, 시본(Seaborn)의 막대그래프로 쉽게 표현하기 위해 Series변환 ftr_importances = pd.Series(ftr_importances_values, index=X_train.columns ) # 중요도값 순으로 Series를 정렬 ftr_top20 = ftr_importances.sort_values(ascending=False)[:20] plt.figure(figsize=(8,6)) plt.title('Feature importances Top 20') sns.barplot(x=ftr_top20 , y = ftr_top20.index) plt.show()
[output]