결정트리는 데이터에 있는 규칙을 학습을 통해 자동으로 찾아내 트리(Tree) 기반의 분류 규칙을 만든다. 일반적으로 쉽게 표현하는 방법은 if/else 로 스무고개 게임을 한다고 생각하면 된다.
아래 그림에서 다이아몬드 모양은 규칙 노드를, 타원형은 리프 노드를 뜻한다. 데이터 세트에 피처가 있고 이러한 피처가 결합해 규칙 조건을 만들 때마다 규칙 노드가 만들어지지만, 많은 규칙이 있으면 분류를 결정하는 방식이 복잡해지고 이는 과적합으로 이어지기 쉽다. 즉 트리의 깊이(depth)가 깊어질수록 결정 트리의 예측 성능이 저하될 가능성이 높다.
결정 노드는 정보 균일도가 높은 데이터 세트를 먼저 선택할 수 있도록 규칙 조건을 만든다. 정보의 균일도를 측정하는 대표적인 방법은 정보 이득 지수와 지니 계수가 있다.
사이킷런에서 제공하는 결정 트리 클래스는 DecisionTreeClassifier 와 DecisionTreeRegressor 가 있다. DecisionTreeClassifier 는 분류를 위한 클래스, DecisionTreeRegressor 는 회귀를 위한 클래스이다.
Homebrew가 설치된 경우
터미널을 켜고 아래 명령어 입력
brew install graphviz
pip install pygraphviz
Homebrew가 없는 경우
터미널에 붙여넣기
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
설치 후 1번을 실행하면 됩니다.
붓꽃 데이터 세트에 결정 트리를 적용해서 시각화해보자.
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings('ignore')
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)
dt_clf.fit(X_train, y_train)
DecisionTreeClassifier(random_state=156)
from sklearn.tree import export_graphviz
# 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)
import graphviz
with open("tree.dot") as f:
dot_graph = f.read()
graphviz.Source(dot_graph)
feature_importance_
를 사용해서 나타낼 수 있다.
import seaborn as sns
import numpy as np
%matplotlib inline
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)
Feature importances:
[0.025 0. 0.555 0.42 ]
sepal length (cm) : 0.025
sepal width (cm) : 0.000
petal length (cm) : 0.555
petal width (cm) : 0.420
<matplotlib.axes._subplots.AxesSubplot at 0x7fba8b8b8e50>
4개의 피처들 중 petal length 가 가장 피처 중요도가 높은 것을 알 수 있다.
결정 트리가 어떻게 학습 데이터를 분할해 예측을 수행하는지와 이로 인한 과적합 문제를 시각화해보자. 분류를 위한 데이터 세트를 임의로 만드는데 사이킷런은 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차원 시각화를 위해서 피처는 2개, 클래스는 3가지 유형의 분류 샘플 데이터 생성
X_feature, y_labels = make_classification(n_features=2, n_redundant=0, n_informative=2, n_classes=3, n_clusters_per_class=1, random_state=0)
# 그래프 형태로 2개의 피처로 2차원 좌표 시각화, 각 클래스 값은 다른 색깔로 표시
plt.scatter(X_feature[:, 0], X_feature[:, 1], marker='o', c=y_labels, s=25, edgecolors='k')
<matplotlib.collections.PathCollection at 0x7fba8ba69ac0>
유틸리티 함수 visualize_boundary()
를 만들어서 머신러닝 모델이 클래스 값을 예측하는 결정 기준을 색상과 경계로 나타내 모델이 어떻게 데이터 세트를 예측 분류하는지 이해하게 함.
import numpy as np
# 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)
dt_clf = DecisionTreeClassifier().fit(X_feature, y_labels)
visualize_boundary(dt_clf, X_feature, y_labels)
이상치 데이터까지 분류하기 위해 분할이 자주 일어나서 경계가 많아진 것을 알 수 있다. 이렇게되면 지나치게 학습데이터에만 최적화되기에 오히려 테스트 데이터 세트에서 정확도를 떨어뜨릴 수 있다. min_sample_leaf = 6 으로 설정해서 생성 규칙을 완화한 뒤 확인해보자.
dt_clf = DecisionTreeClassifier(min_samples_leaf=6).fit(X_feature, y_labels)
visualize_boundary(dt_clf, X_feature, y_labels)
결정 트리를 UCI 머신러닝 레포지토리에서 제공하는 사용자 행동 인식 데이터 세트에 대한 예측 분류를 수행해보자. 해당 데이터는 30명에게 스마트폰 센서를 장착한 뒤 동작에 관련된 여러 피처를 수집한 데이터이다. 아래 링크에서 데이터 셋을 다운받을 수 있다.
http://archive.ics.uci.edu/ml/datasets/Human+Activity+Recognition+Using+Smartphones
import pandas as pd
# features.txt 파일에는 피처 이름 index와 피처명이 공백으로 분리되어 있음
feature_name_df = pd.read_csv('./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(f'전체 피처명에서 10개만 추출: {feature_name[:10]}')
전체 피처명에서 10개만 추출: ['tBodyAcc-mean()-X', 'tBodyAcc-mean()-Y', 'tBodyAcc-mean()-Z', 'tBodyAcc-std()-X', 'tBodyAcc-std()-Y', 'tBodyAcc-std()-Z', 'tBodyAcc-mad()-X', 'tBodyAcc-mad()-Y', 'tBodyAcc-mad()-Z', 'tBodyAcc-max()-X']
인체의 움직임과 관련된 속성의 평균/표준편차가 X, Y, Z축 값으로 되어 있다. 현재 features_info.txt 파일에는 중복된 피처명이 있고 이를 그대로 사용하면 오류가 발생한다. 따라서 중복된 피처명이 얼마나 있는지 확인해보자.
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()
column_index 42
dtype: int64
column_index | |
---|---|
column_name | |
fBodyAcc-bandsEnergy()-1,16 | 3 |
fBodyAcc-bandsEnergy()-1,24 | 3 |
fBodyAcc-bandsEnergy()-1,8 | 3 |
fBodyAcc-bandsEnergy()-17,24 | 3 |
fBodyAcc-bandsEnergy()-17,32 | 3 |
42개의 피처명이 중복되므로, 이에 _1, _2 등을 추가하는 함수를 생성한다.
cumcount 는 누적합
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('./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('./human_activity/train/X_train.txt',sep='\s+', names=feature_name )
X_test = pd.read_csv('./human_activity/test/X_test.txt',sep='\s+', names=feature_name)
# 학습 레이블과 테스트 레이블 데이터을 DataFrame으로 로딩하고 컬럼명은 action으로 부여
y_train = pd.read_csv('./human_activity/train/y_train.txt',sep='\s+',header=None,names=['action'])
y_test = pd.read_csv('./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(X_train.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7352 entries, 0 to 7351
Columns: 561 entries, tBodyAcc-mean()-X to angle(Z,gravityMean)
dtypes: float64(561)
memory usage: 31.5 MB
None
학습 데이터 세트는 7352개의 레코드로 561개의 피처를 가지고 있고 전부 float 형이므로 카테고리 인코딩을 수행하지 않아도 된다.
from sklearn.metrics import accuracy_score
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(f'결정 트리 예측 정확도: {np.round(accuracy, 4)}')
print(f'DecisionTreeClassifier 기본 하이퍼 파라미터:\n{dt_clf.get_params()}')
결정 트리 예측 정확도: 0.8548
DecisionTreeClassifier 기본 하이퍼 파라미터:
{'ccp_alpha': 0.0, 'class_weight': None, 'criterion': 'gini', 'max_depth': None, 'max_features': None, 'max_leaf_nodes': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'presort': 'deprecated', 'random_state': 156, 'splitter': 'best'}
85.48%의 정확도를 나타내고 있다.
결정 트리의 트리 깊이가 예측 정확도에 주는 영향을 알아보기 위해 GridSearchCV 를 이용해 max_depth 값을 변화시키면서 확인해 보자.
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(f'GridSearchCV 최고 평균 정확도 수치: {np.round(grid_cv.best_score_, 4)}')
print(f'GridSearchCV 최적 하이퍼 파라미터: {grid_cv.best_params_}')
Fitting 5 folds for each of 7 candidates, totalling 35 fits
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done 35 out of 35 | elapsed: 1.3min finished
GridSearchCV 최고 평균 정확도 수치: 0.8513
GridSearchCV 최적 하이퍼 파라미터: {'max_depth': 16}
max_depth = 16 일때 85.13% 의 정확도를 보였다.
cv_results_df = pd.DataFrame(grid_cv.cv_results_)
cv_results_df[['param_max_depth', 'mean_test_score']]
param_max_depth | mean_test_score | |
---|---|---|
0 | 6 | 0.850791 |
1 | 8 | 0.851069 |
2 | 10 | 0.851209 |
3 | 12 | 0.844135 |
4 | 16 | 0.851344 |
5 | 20 | 0.850800 |
6 | 24 | 0.849440 |
이번에는 별도의 테스트 데이터 세트에서 결정 트리의 정확도를 측정해보자.
max_depths = [6, 8, 10, 12, 16, 20, 24]
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))
max_depth = 6 정확도: 0.8558
max_depth = 8 정확도: 0.8707
max_depth = 10 정확도: 0.8673
max_depth = 12 정확도: 0.8646
max_depth = 16 정확도: 0.8575
max_depth = 20 정확도: 0.8548
max_depth = 24 정확도: 0.8548
max_depth = 8 일때 87.07% 의 정확도를 보였고 max_depth 가 커지면서 정확도가 떨어진다.
이번에는 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(f'GridSearchCV 최고 평균 정확도 수치: {np.round(grid_cv.best_score_)}')
print(f'GridSearchCV 최적 하이퍼 파라미터: {grid_cv.best_params_}')
Fitting 5 folds for each of 8 candidates, totalling 40 fits
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done 40 out of 40 | elapsed: 1.6min finished
GridSearchCV 최고 평균 정확도 수치: 1.0
GridSearchCV 최적 하이퍼 파라미터: {'max_depth': 8, 'min_samples_split': 16}
책이랑 똑같이 썼는데 책은 max_depth: 8, min_samples_split: 16 에서 정확도 85.5% 가 나왔다... 아무리 봐도 내가 뽑아낸 평균 정확도 1.0은 과적합 같은데 일단은 넘어가겠다. (고수님들 알려주세요... 뭐가 문제였는지..) 아래 결정 트리 예측 정확도도 책과 일치하는 87.17% 이다.... 뭐가 잘못된걸까....
best_df_clf = grid_cv.best_estimator_
pred1 = best_df_clf.predict(X_test)
accuracy = accuracy_score(y_test, pred1)
print(f'결정 트리 예측 정확도: {np.round(accuracy, 4)}')
결정 트리 예측 정확도: 0.8717
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 Importance Top 20')
sns.barplot(x=ftr_top20, y=ftr_top20.index)
<matplotlib.axes._subplots.AxesSubplot at 0x7fba8be6a370>
Top 5인 tFravityAcc-min()-X, fBodyAccJerk-bandsEnergy()-1,16, angle(Y,gravityMean), fBodyAccMag-energy(), tGravityAcc-arCoeff()-Z,2 가 매우 중요하게 규칙 생성에 영향을 미치는 것을 알 수 있다.
Source: 파이썬 머신러닝 완벽 가이드 / 위키북스