머신러닝&딥러닝

Machine Learning #3 Decision Tree & Ensemble : 신용카드 연체 예측

seungbeomdo 2023. 2. 8. 00:17

 

 

 

GitHub - SeungbeomDo/DataAnalysis: Practical Codes for Data Analysis using Machine Learning and Deep Learning

Practical Codes for Data Analysis using Machine Learning and Deep Learning - GitHub - SeungbeomDo/DataAnalysis: Practical Codes for Data Analysis using Machine Learning and Deep Learning

github.com


1. Decision Tree의 개요

  • 분류 문제에 자주 사용되는 머신러닝 방법론인 Decision Tree를 설명한다.

 

  • Decision Tree는 스무고개와 같은 방식으로 분류 문제를 해결한다. 주어진 샘플들을 구분하기 위한 질문들을 던지고 질문에 대한 답변에 따라 조금씩 샘플들을 분류해나가는 것이다. 가령 신용카드 연체를 예측하는 데이터에서
    • 소득이 200만원 이상인지? $\rightarrow$ N
    • 위 질문에 N으로 답변된 경우: 근로연수가 10년 이상인지? $\rightarrow$ N
    • 위 질문에 N으로 답변된 경우: 가족구성원이 2명 이상인지? $\rightarrow$ N
    • 위 질문에 N으로 답변된 경우: 신용카드 연체 그룹($Y$=1)으로 분류하는 방식이다.

2. 질문과 불순도(Impurity)

  • Decision Tree의 핵심은 데이터들을 제대로 분류하기 위한 질문들을 찾는 것이다. 신용카드 연체 여부를 예측하고자 하는데 이와는 전혀 무관한 질문을 던질 경우 질문을 아무리 많이 던져도 좋은 예측 결과를 기대할 수 없다.

 

  • 그렇다면 좋은 질문을 결정하는 기준은 무엇일까? Decision Tree는 질문을 통해 샘플의 불순도가 많이 감소하는 질문이 좋은 질문이라고 본다. 아래에서 불순도 개념을 설명한다.

 

2.1. 불순도(Impurity)

  • 불순도란 주어진 노드에 포함된 샘플들에 서로 다른 클래스들이 얼마나 섞여있는지를 나타낸다. 불순도를 최소화한다는 것은 샘플들이 최대한 하나의 클래스로만 구성될 수 있도록 한다는 것이다.

 

  • 자주 사용되는 불순도의 측정치는 지니 불순도(Gini Impurity)이다. 주어진 샘플 $S$의 지니 불순도는 아래와 같이 정의된다. 두 개의 클래스가 존재할 때, 클래스 1의 비율이 $p$라고 하면

$$Gini(S) = 1 - p^{2} - (1-p)^{2}$$

  • 가령 주어진 샘플에서 연체한 사람의 비율이 10%이면 $Gini(S) = 1 - 0.01 - 0.81 = 0.18$
  • 한편, 하나의 클래스로만 이루어진 샘플의 지니 불순도는 1이 될 것임을 알 수 있다.

 

2.2. 정보 이득(Information Gain)

  • 정보 이득이란 질문을 통해 불순도가 얼마나 감소했는가를 측정하는 지표이다. 주어진 샘플 $S$가 질문 $A$에 의해 $S_{1}$과 $S_{2}$로 분류되었을 때 정보 이득의 크기는 다음과 같다.

$$IG(S,A) = Gini(S) - \Sigma_{i=1,2}\frac{|S_{i}|}{|S|}Gini(S_{i})$$

  • 딱히 복잡할 것 없이, 정보 이득이란 부모 노드의 지니불순도에서 자식노드들의 지니불순도의 가중 평균을 뺀 값이다.

 

  • Decision Tree는 주어진 노드에서 정보 이득이 가장 큰 질문을 선택해서 다음 가지를 펼쳐나간다.

 

  • 그럼 구체적으로 어떻게 질문을 선택한다는 것일지 조금만 더 들여다보자. 우선 모든 독립변수들에 대해서 정보이득을 계산한다. 만약 독립변수가 이진 변수라면 그 독립변수를 선택하는 것이 곧 질문을 선택한 것과 다름 없다. yes or no이기 때문이다.
  • 만약 독립변수가 연속 변수라면 조금 복잡해진다. 다음과 같은 과정을 거치는데, 먼저 주어진 변수를 크기 순으로 정렬한 후에 이웃하는 두 변수값의 평균을 구한다. 만약 관측값이 10개라면 9개의 이웃-평균이 나올 것이다. 그리고 9개의 이웃-평균들을 기준으로 가지를 펼친다고 했을 때 정보이득을 계산한 후, 가장 정보이득이 큰 이웃-평균을 임계값으로 사용한다.

 

  • 이처럼 Decision tree는 주어진 노드에서 정보 이득이 가장 큰 질문을 선택해 가지를 펼치고, 다음 가지의 각 노드에서 마찬가지로 정보 이득이 가장 큰 질문을 선택해서 그 다음 가지를 펼치고, ... , 를 반복한다. 자식 노드가 적정한 지니 불순도에 도달할 때까지 이런 과정을 반복한다.

3. 가지치기

  • 여타의 모델에서 독립변수 벡터의 차원이 너무 커지면 여러 문제가 발생하듯이, Decision Tree 모델에서도 독립변수 벡터의 차원을 줄이는 것이 중요하다. Decision Tree의 맥락에서는 너무 많은 질문, 다시 말해 너무 많은 가지가 나타나지 않도록 하는 것이다. 이 과정을 가지치기라고 한다.

 

  • 가지치기에는 두 가지 종류가 있는데 사전 가지치기와 사후 가지치기이다. 사전 가지치기가 간단하니까 바로 설명하면 의사결정 나무 전체가 완성되기 이전에 미리 특정 조건을 만족하면 훈련이 중단되도록 설정해두는 것이다. 예컨대 나무의 depth의 한계를 둔다든지 뭐 그런 것들.

 

  • 사후 가지치기는 의사결정 나무가 완성된 이후 맨 마지막 노드부터 유의미 여부를 판단해서 잘라나가는 방식이다. 대상이 된 노드가 필요한 노드인지를 결정하기 위한 사용하는 지표는 비용-복잡도(Cost-Complexity)가 있다. 주어진 트리 $T$의 비용복잡도 $CC(T)$는 트리의 오분류율 $Err(T)$와 끝 노드의 개수 $|T|$에 대해 다음과 같이 정의한다.

$$CC(T) = Err(T) + \alpha|T|$$

  • $\alpha$는 나무 복잡도에 대한 민감도를 나타내는 계수이다. 이 값이 커질수록 나무가 너무 길어지지 않는 것이 중요해진다.

 

  • 완성된 트리의 비용-복잡도를 계산한 후 마지막 노드부터 쳐내고 비용-복잡도가 감소한다면 그 노드를 제거하는 방식이다. 비용-복잡도가 더이상 감소하지 않을 때까지 가지치기를 수행한다.

4. 실습

4.1. 데이터 전처리

  • 배운 내용을 실습해보자. 신용카드 연체 여부를 예측하는 의사결정 나무를 만들 것이다. 주어진 데이터를 독립변수와 종속변수로 나누어 저장한다. 주어진 데이터에서 신용카드 연체(Y=1) 비율은 약 10%이다.
raw_X = loan.dropna().drop(['Personal Loan'], axis=1) #종속변수 Personal Loan 열 빼기
raw_y = loan['Personal Loan']

raw_y.value_counts(normalize = True)

  • 독립변수 중 범주형인 변수들은 원-핫 인코딩으로 더미처리 해준다.
# 범주형인 'Education'열을 One-hot Encoding으로 전처리
raw_X_one_hot = pd.get_dummies(data=raw_X, columns=['Education'], prefix='Education')

 

4.2. 파라미터 튜닝

  • train set을 분리해준다. 이때 stratify 옵션을 사용하면, 종속변수의 분포가 원 데이터셋과 트레인셋, 테스트셋에서 모두 동일하도록 만들어줄 수 있다. 이런 문제는 특히 종속변수의 분포가 불균형할 때 더욱 치명적으로 된다.
from sklearn.model_selection import train_test_split

train_X, test_X, train_y, test_y = train_test_split(raw_X_one_hot, raw_y, train_size=0.7, test_size=0.3, random_state=1, stratify=raw_y)
print(train_X.shape, test_X.shape, train_y.shape, test_y.shape)

 

  • Decision tree의 최적 하이퍼파라미터를 찾아야 한다. 여기서는 나무의 깊이(층수)와 자식 노드 생성이 중지되는 노드의 샘플 개수를 정해준다. 후자의 경우, 만약 그 값이 5이면 노드에 포함된 샘플 개수가 5일 경우 자식 노드 생성이 중지된다. 즉 이것들은 사전 가지치기를 하는 데 필요한 하이퍼 파라미터이다.

 

  • 튜닝해야 할 파라미터가 둘 이상이므로 GridSearchCV를 사용한다. 이전 포스팅에서도 설명했듯 설정한 파라미터들의 후보에 대해 알아서 k-fold 검증을 실시해주는 편리한 라이브러리이다. 파라미터 튜닝의 결과 가장 좋은 경우는 나무의 깊이가 5이고, 최소 샘플 수는 2인 경우이다. 이때 5개 validation 샘플에 대한 평균 accuracy는 0.98 정도이다.
from sklearn.model_selection import GridSearchCV 

hyperparamters = {'max_depth': list(range(2, 8)),  'min_samples_split': list(range(2, 20))}

GridCV = GridSearchCV(estimator=loan_tree, param_grid=hyperparamters, cv=5)
GridCV.fit(train_X, train_y)

print(GridCV.best_params_)
print(GridCV.best_score_)

 

  • 결정된 하이퍼 파라미터를 가지고 의사결정 나무를 훈련한 결과이다.
loan_tree = DecisionTreeClassifier(max_depth=5, min_samples_split=9, random_state=0).fit(train_X, train_y)

from sklearn.metrics import accuracy_score, f1_score

print("Decision Tree Accuracy for test data : {:.3f}".format(accuracy_score(test_y, loan_tree.predict(test_X))))
print("Decision Tree F1 score for test data : {:.3f}".format(f1_score(test_y, loan_tree.predict(test_X))))

  • 완성된 트리를 시각화하는 것도 가능하다. 너무 그림이 커서 다 가져오진 않았다.
#export_graphviz 함수: graphviz 패키지를 불러와 시각화하기 위한 파일을 만든다
from sklearn.tree import export_graphviz

#export_graphviz()의 호출 결과로 out_file로 파일을 생성함
export_graphviz(loan_tree, #모델의 이름
                out_file = "loan_tree.dot",
                feature_names = train_X.columns,
                impurity = True, # impurity = True: 분순도를 표기 (True가 디폴트)
                filled = True) # filled = True: 색깔을 추가 (False가 디폴트) 
                
# 결정트리를 위한 시각화 패키지
import graphviz

#loan_tree 파일 열기 / 읽기
with open("loan_tree.dot") as f:
  dot_graph = f.read()

#시각화
graphviz.Source(dot_graph)


5. Ensemble

5.1. 앙상블 기법의 개요

  • 앙상블 기법이란 복수의 모델의 예측을 결합하여 최종 예측을 제시하는 방법을 말한다. 비단 분류에만 적용되는 기법은 아니고 예측값의 평균을 낸다든지 하여 회귀 문제에도 적용될 수 있다. 이때 각각의 모델들을 Weak Learner라고 한다.

 

5.2. Voting

  • 모든 앙상블 기법에 공통되는 내용으로 Voting이라는 방법이 있다. 이는 각각의 모델들의 결과를 결합하는 방법이다.

 

  • Voting의 두 가지 유형이 있는데 하나는 Hard Voting이다. 분류 문제에서, 각 weak learner들의 분류를 다수결 투표하여 결정하는 방법이다. 예를 들어 5개의 분류 모델을 훈련한 후 주어진 사람이 신용카드 연체를 할 사람인지 아닌지 투표한다. 모델 3개 이상이 연체를 했다고 결과를 낸다면, 그 사람은 연체를 할 사람이라고 예측하는 것이다.

 

  • 두번째 방식은 Soft Voting이다. 분류 문제나 회귀 문제에서 weak learner들의 예측값을 평균내는 것이다. 만약 신용카드 연체 분류 문제라면 클래스에 속할 확률을 평균내어, 그 확률의 평균이 임계치(가령 0.5)를 넘으면 연체를 한다고 예측하는 것이다.
  • 이때 각 weak learner들에 가중치를 매겨서 평균내는 방법도 사용할 수 있다.

 

5.3. Bagging

  • 앙상블을 하는 방법에도 다양한 형태가 있다. 첫번째는 배깅이다. 이는 주어진 데이터셋에서 랜덤하게 샘플들을 복원추출하고 각 샘플들에 대해 weak learner들을 훈련한 후 앙상블하는 것이다. 복원추출이므로 sub sample 간 중복이 허용된다.

 

  • 배깅 방법의 대표적인 예는 Random Forest이다. Bagging에 대한 설명과 딱히 다를 것이 없기 때문에 덧붙일 말이 없다. 다만 독특한 것은 샘플링을 할 때 어떤 독립변수를 선택할지도 임의 추출 방식으로 결정한다는 것이다. 가령 추출된 두 샘플 중 한 샘플은 임금과 학력, 나이만을 독립변수로 사용하고 다른 한 샘플은 임금과 성별, 가족구성원 수만을 독립변수로 사용한다.

 

  • 100개의 샘플로부터 총 100개의 트리들을 만드는 랜덤 포레스트를 전술한 신용카드 연체 예측 문제에 적용해보았다. 이때도 트리의 깊이를 파라미터 튜닝하여 결정하였다.
from sklearn.model_selection import GridSearchCV

params = {'max_depth': [5, 10, 15, 20]}

rf = RandomForestClassifier(n_estimators = 100)
GridCV = GridSearchCV(rf, param_grid = params, cv = 5, n_jobs = -1)
GridCV.fit(train_X, train_y)

rf = RandomForestClassifier(max_depth = 20, n_estimators = 100)
rf.fit(train_X, train_y)
pred = rf.predict(test_X)
print("Random Forest Classifier(New) Accuracy for test data : {:.4f}".format(accuracy_score(test_y, pred)))

 

5.4. Boosting

  • 부스팅 기법은 복수의 weak learner들이 동시적으로 결합하는 것이 아니라, 순차적으로 결합하는 형태를 갖는다. 1번째 모델을 학습한 후에 학습된 결과를 토대로 그 다음에 어떤 학습을 해야 할지 결정해 2번째 모델을 학습한다. 그 후에 다시 3번째 모델을 학습하고, ... 를 반복한다.
  • 이때 각 weak learner들은 이전 모델들이 잘못 예측한 부분을 중점적으로 학습한다는 특징이 있다.

 

  • 아래 그림이 좋은 예시인데, 1번째 분류기는 x>1 여부를 기준으로 도형을 분류하고, 그러고 나서 남은 도형들을 분류하기 위해 2번째 분류기는 y>4 여부를 기준으로 도형을 분류한다. 각각의 분류기준은 어떻게 해야 불순도를 최소화할 수 있을지 학습한 결과로 얻어진 것이다.

 

5.4.1. GBM(Gradient Boosting Machine)

  • 부스팅 모델의 대표적인 예는 GBM이다. GBM은 회귀문제에 주로 사용되는데, 잔차 예측을 핵심 알고리즘으로 한다. 이는 이전 모델이 예측하고 남은 잔차를 새로운 모델의 종속변수로 두고 예측을 수행하는 것을 말한다. 첫번째 모델이 예측하고 남은 잔차를 종속변수로 하는 모델을 훈련하고, 그러고도 남은 잔차를 다시 종속변수로 하는 모델을 훈련하고, ... 를 반복한다. 이때 베이스모델은 보통 의사결정트리이다. 의사결정트리를 회귀를 위해서도 사용할 수 있다.

 

  • 그런데 이때 Gradient라는 말은 왜 붙었을까? 회귀 트리가 연달아 훈련을 할 때, 이들의 최종 목표는 잔차제곱합을 최소화하는 것이다. 그런데 GBM이 잔차제곱합을 최소화하는 과정이 경사하강법(Gradient Descent)과 동일하다.

 

  • 우리가 최소화하려고 하는 것은 예측 잔차의 제곱합 $J_{t}$이다. t번째 예측값을 $F_{t}(X_{i})$라고 두면 손실함수는 아래와 같다. 계산을 쉽게 하기 위해 2로 나눈 값을 사용한다.

$$J = \Sigma_{i}\frac{(Y_{i} - F_{t}(X_{i}))^{2}}{2}$$

  • 손실함수를 예측값에 대하여 편미분하면

$$\frac{\partial J_{t}}{\partial F_{t}(X_{i})} = Y_{i} - F_{t}(X_{i})$$

  • 이 값은 예측값을 1만큼 변화시켰을 때 손실함수가 감소하는 정도를 나타낸다. 다른 말로 그래디언트이다.
  • 이를 반영한 t+1번째 예측값은 다음과 같다. $\eta$는 학습률이다. 손실함수가 감소하는 방향으로 예측값을 얼마나 빠르게 조정할지를 나타낸다.

$$F_{t+1}(X_{i}) = F_{t}(X_{i}) - \eta[Y_{i} - F_{t}(X_{i})]$$

  • 그런데 여기서 잔차 $Y_{i} - F_{t}(X_{i})$는 곧 t+1번째 모델이 예측할 종속변수이다. 다시 말해 GBM은 개선된 예측값을 제시하기 위해 필요한 그래디언트를 예측하는 모델인 것이다. 그리고 이 그래디언트들을 학습률을 곱해 이전 예측값들에서 지속적으로 빼나가면 잔차제곱합을 최소화하는 지점에 다다르게 된다.

 

  • 신용카드 연체 예측 문제에 적용한 결과는 아래와 같다. 파라미터 튜닝을 하지 않은 순정 모델이기 때문에 썩 좋은지 어떤지는 모르겠다.
from sklearn.ensemble import GradientBoostingClassifier

gb_clf = GradientBoostingClassifier()
gb_clf.fit(train_X, train_y)
gb_pred = gb_clf.predict(test_X)

print("Random Forest Classifier Accuracy for test data : {:.4f}".format(accuracy_score(test_y, gb_pred)))
print("Random Forest Classifier F1 score for test data : {:.4f}".format(f1_score(test_y, gb_pred)))

 

5.4.2. GBM의 변형

  • GBM을 응용한 알고리즘들이 몇 가지 있다. 자세하게 다룰 생각은 없지만,
  • XGBoost는 GBM에 규제항을 추가해 트리의 복잡성을 방지하고 과적합 문제를 줄이는 알고리즘이다.
  • LightGBM은 GBM을 모든 노드에서 동시적으로 진행하지 않고 가장 불순도가 감소하는 노드에 대해서만 적용하는 알고리즘이다.