Post

chapter.5 트리 알고리즘

chapter.5 트리 알고리즘

chapter.5 트리 알고리즘

5-1 결정 트리

와인 캔에 인쇄된 알코올 도수, 당도, PH 값으로 와인 종류를 구분할 수 있을까?
→ 로지스틱 회귀 모델 적용

1
2
3
4
5
6
7
8
data = wine[['alcohol', 'sugar', 'PH']].to_numpy()
target = wine['class'].to_numpy()

from sklearn.model_selection import train_test_split

train_input, test_input, train_target, test_target = train_test_split(
    data, target, test_size=0.2, random_state=42)

→ 판다스 데이터프레임을 넘파이 배열로 바꾸고 훈련세트와 테스트 세트로 나누어주기

이때 샘플 개수가 충분히 많기 때문에 20%정도만 테스트 세트로 나누어준다.

1
2
print(train_input.shape, test_input.shape)
# output: (5197,3) (1300,3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 훈련세트 전처리
from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

# 로지스틱 회귀 모델 훈련
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))
#output: 0.7808350971714451
#        0.7776923076923077

→ 점수가 그렇게 높게 나오지 않은걸 보아 모델이 과소적합 된 것을 알 수 있다.

1
2
print(lr.coef_, lr.intercept_)
# output: [[0.51270274 1.6733911 -0.68767781]] [1.81777902]

→ 알코올 도수와 당도가 높을수록 화이트 와인일 가능성이 높고, PH가 높을수록 레드 와인일 가능성이 높다고 예상할 수 있음. 하지만 정확하게 이러한 계수 값이 어떤 의미인지 설명하기는 어렵다.

※ 결정 트리
결정 트리를 활용하면 모델의 학습 이유를 설명하기 쉽다. (트리의 깊이가 길어질수록 해석이 어려워지긴 함)

1
2
3
4
5
6
7
8
from sklearn.tree import DecisionTreeClassifier

dt = DecisionTreeClassifier(random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))  # 훈련 세트
print(dt.score(test_scaled, test_target))    # 테스트 세트
# output: 0.996921300750433
#         0.8592307692307692

→ 훈련 세트 성능은 올라갔지만 테스트 세트의 성능은 조금 낮은 것으로 보아 모델이 과대적합 된걸 알 수 있음

1
2
3
4
5
6
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

plt.figure(figsize=(10,7))
plot_tree(dt)
plt.show()

image1.png

  • 맨 위의 노드를 루트 노드, 맨 아래 끝에 달린 노드를 리프 노드 라고 부른다.
1
2
3
plt.figure(figsize=(10,7))
plot_tree(dt, max_depth=1, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

image1.png

  • 당도(sugar)가 -0.239 이하면 왼쪽 가지로 이동, 그렇지 않으면 오른쪽 가지로 이동. 이때 노드의 총 샘플 수는 5197개이며 이 중 음성 클래스(레드와인)은 1258개, 양성 클래스(화이트 와인)은 3939개이다.

  • 왼쪽 노드에서는 당도가 더 낮은지 확인. 당도가 -0.802 이하면 왼쪽 가지, 아니면 오른쪽 가지로 이동. 이때 노드의 샘플 개수는 각각 1177, 1745개이다.

  • 오른쪽 노드에서는 대부분의 화이트 와인 샘플이 해당 노드로 이동한 것을 볼 수 있음.

  • plot_tree()함수에서 filled=True로 지정하면 클래스마다 색깔 부여 가능, 어떤 클래스의 비율이 높아지면 점점 진한 색으로 표시

※ 불순도
gini는 지니 불순도를 의미한다.
$ 지니 \, 불순도 = 1 - (음성\, 클래스 \,비율^2 + 양성 \,클래스 \,비율^2)$

결정 트리 모델은 부모 노드와 자식 노드의 불순도 차이가 가능한 크도록 트리를 성장시킨다.
→ 부모의 불순도 - (왼쪽 노드 샘플 수 / 부모의 샘플 수) * 왼쪽 노드 불순도 - (오른쪽 노드 샘플 수 / 부모의 샘플 수) * 오른쪽 노드 불순도

이런 부모와 자식 노드 사이의 불순도 차이를 정보 이득이라고 부른다.

이외에도 엔트로피 불순도를 사용할 수 있음
DecisionTreeClassifier클래스에서 criterion=’entropy’를 지정할 수 있음.
엔트로피 불순도 = -음성 클래스 비율$log_2$(음성 클래스 비율) - 양성클래스 비율 $log_2$(양성 클래스 비율)

※ 가지치기
모델이 훈련 세트에 과대적합 되지 않도록 해주는 것
→ 트리의 최대 깊이 지정. max_depth매개변수를 활용하여 지정할 수 있다.

1
2
3
4
5
6
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))
# output: 0.8454877814123533
#         0.8415384615384616
  • 트리 모델의 장점 중 하나는 데이터 전처리 과정이 필요없다는 것. 특성값의 스케일이 트리 알고리즘에 아무런 영향을 미치지 않는다.
    1
    2
    3
    4
    5
    6
    7
    
    # 전처리 하지 않은 데이터로 트리 모델 훈련
    dt = DecisionTreeClassifier(max_depth=3, random_state=42)
    dt.fit(train_input, train_target)
    print(dt.score(train_input, train_target))
    print(dt.score(test_input, test_target))
    # output: 0.8454877814123533
    #         0.8415384615384616
    

    전처리를 진행하지 않은 데이터로 모델을 훈련함으로써 결과 해석이 훨씬 직관적이다.

1
2
3
# 특성 중요도 출력
print(dt.feature_importances_)
# output: [0.12345626 0.86862934 0.0079144]

→ 두번째 특성인 당도가 가장 높은 것을 확인할 수 있음. 특성 중요도를 활용하면 결정트리 모델을 특성 선택에 활용할 수 있다.

5-2 교차 검증과 그리드 서치

테스트 세트를 계속 사용하여 모델을 테스트하면 일반화 성능이 떨어지게 될 수 있다
→ 검증 세트 활용
image1.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd

wine = pd.read_csv('https://bit.ly/wine-date')

data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

# 훈련 세트와 테스트 세트 배분
from sklearn.model_selection import train_test_split

train_input, test_input, train_target, test_target = train_test_split(
    data, target, test_size=0.2, random_state=42
)

# train_input, train_target에서 검증 세트 추출
sub_input, val_input, sub_target, val_target = train_test_split(
    train_input, train_target, test_size=0.2, random_state=42
)

print(sub_input.shape, val_input.shape)
# output: (4157,3) (1040,3)

→ 원래 있던 5197개의 훈련 세트가 4157개로 줄고, 검증 세트가 1040개가 되었음.

1
2
3
4
5
6
7
8
9
from sklearn.tree import DecisionTreeClassifier

dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)

print(dt.score(sub_input, sub_target))
print(dt.score(val_input, val_target))
# output: 0.9971133028626413
#         0.864423076923077

※ 교차 검증(cross validation)
검증 세트를 만드는 과정에서 훈련 세트가 줄어들어 검증 점수의 간격이 크고 불안정해지는 것을 막아줌

교차 검증은 검증 세트를 떼어내어 평가하는 과정을 여러 번 반복한다.
ex. 3-폴드 교차 검증image1.png

1
2
3
4
5
6
7
from sklearn.model_selection import cross_validate

scores = cross_validate(dt, train_input, train_target)
print(scores)
# output: {'fit_time': array([0.01334453, 0.01186419, 0.00783849, 0. 0077858, 0.00726461]),
# 'score_time': array([0.00085783, 0.00062561, 0.00061512, 0.00063181, 0.00067616]),
# 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
1
2
3
import numpy as np
print(np.,mean(scores['test_score']))
# output: 0.855300214703487

교차 검증을 할 때 훈련 세트를 섞으려면 분할기를 지정해야 함

1
2
3
4
5
6
from sklearn.model_selection import StratifiedKFold

scores = cross_validate(dt, train_input, train_target, cv=StratifiedKFold())
print(np.mean(scores['test_score']))

# Output: 0.855300214703487
1
2
3
4
5
6
# 10-폴드 교차 검증
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))

# Output: 0.8574181117533719

※ 하이퍼파라미터 튜닝
모델이 학습할 수 없어서 사용자가 지정해야만 하는 파라미터를 하이퍼파라미터라고 부른다.

  • 먼저 라이브러리가 제공하는 기본값을 그대로 사용하여 모델을 훈련한 후 검증 세트의 점수나 교차 검증을 통해서 매개변수를 조금씩 바꾼다. 모델마다 적게는 1~2개, 많게는 5~6개의 매개변수 제공. 이 매개변수를 바꿔가면서 모델을 훈련하고 교차 검증을 수행해야 한다.

  • max_depth의 최적 값을 구하여 고정한 상태로 min_samples_split을 바꾸는 것은 불가능함. min_samples_split 값이 바뀌면 max_depth또한 바뀌기에 두 매개변수를 동시에 바꿔나가야 한다.

매개변수가 많아질 경우 사이킷런의 그리드 서치 활용
→ 하이퍼파라미터 탐색과 교차 검증을 한 번의 수행한다.

1
2
3
4
5
6
7
8
9
10
11
12
from sklearn.model_selection import GridSearchCV

params = {'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)

gs.fit(train_input, train_target)

dt = gs.best_estimator_
print(dt.score(train_input, train_target))

# Output: 0.9615162593804117
1
2
3
# 그리드 서치로 찾은 최적의 매개변수
print(gs.best_params_)
# output: {'min_impurity_decrease': 0.0001}
1
2
3
# 각 매개변수에서 수행한 교차 검증의 평균 점수
print(gs.cv_results_['mean_test_score'])
# output: [0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]

→ 이렇게 모든 매개변수에 대한 점수를 출력해서 결정해도 되고 argmax()함수로 가장 큰 값의 인덱스를 추출 할 수도 있음.

1
2
3
4
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])

# Output: {'min_impurity_decrease': 0.0001}

※ 과정 정리

  1. 먼저 탐색할 매개변수 지정
  2. 훈련 세트에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다. 이 조합은 그리드 서치 객체에 저장된다.
  3. 그리드 서치는 최상의 매개변수에서 전체 훈련 세트를 사용해 최종 모델을 훈련한다. 이 모델도 그리드 서치 객체에 저장

조금 더 복잡한 매개변수 조합 탐색
image1.png
함수 1에서는 첫 번째 매개변수 값에서 시작하여 두 번째 매개변수에 도달할 때까지 세 번째 매개변수를 계속 더한 배열을 만든다.

함수 2에서는 정수만 사용 가능하게 설정되어 max_depth를 5에서 20까지 1씩 증가하면서 15개의 값을 만든다. min_samples_split은 2~100까지 10씩 증가

1
2
3
4
5
6
7
8
9
10
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)

print(gs.best_params_)

# Output: {'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}

print(np.max(gs.cv_results_['mean_test_score']))

# Output: 0.868386577302731

※ 랜덤 서치
매개변수의 값이 수치일 때 값의 범위나 간격을 정하기 어려울 수 있음. 또한 너무 많은 매개변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있는데 이때 사용하는 것이 랜덤서치이다.

랜덤서치는 매개변수 값의 목록을 전달하는 것이 아니라 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다.

1
2
3
4
5
6
# randint는 정숫값을 뽑고 uniform은 실숫값을 뽑음
from scipy.stats import uniform, randint

rgen = randint(0,10)
rgen.rvs(10)
# output: array([6,4,2,2,7,7,0,0,5,4])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
params = {
    'min_impurity_decrease': uniform(0.0001, 0.001),
    'max_depth': randint(20, 50),
    'min_samples_split': randint(2, 25),
    'min_samples_leaf': randint(1, 25),
}

# params에 정의된 매개변수 범위에서 총 100번 샘플링
from sklearn.model_selection import RandomizedSearchCV

gs = RandomizedSearchCV(DecisionTreeClassifier(random_state=42), params, 
                        n_iter=100, n_jobs=-1, random_state=42)

gs.fit(train_input, train_target)

print(gs.best_params_)
# output: {'max_depth': 39, 'min_impurity_decrease': 0.0003410254602601173,
# 'min_samples_leaf': 7, 'min_samples_split': 13}
1
2
3
4
5
6
7
8
print(np.max(gs.cv_results_['mean_test_score']))

# Output: 0.8695428296438884

dt = gs.best_estimator_
print(dt.score(test_input, test_target))

# Output: 0.86

→ 일반적으로 테스트 세트 점수는 검증 세트에 대한 점수보다 조금 작다.

5-3 트리의 앙상블

정형데이터: 구조화된 형식으로, 행과 열로 구성된 테이블 형태의 데이터(예: 데이터베이스의 스프레드시트).
비정형데이터: 특정한 구조가 없는 자유로운 형식의 데이터(예: 텍스트, 이미지, 오디오 파일).

정형 데이터를 다루는 데 가장 뛰어난 성과를 내는 알고리즘이 앙상블 학습이다. 이 알고리즘은 대부분 결정 트리를 기반으로 만들어져 있다.

비정형 데이터를 사용하는 알고리즘은 신경망 알고리즘.

※ 랜덤 포레스트
결정 트리를 랜덤하게 만들어 결정 트리의 숲을 만드는 것.
image1.png
랜덤 포레스트는 각 트리를 훈련하기 위해 훈련 데이터에서 랜덤하게 샘플을 추출하여 훈련 데이터를 만든다. 이때 한 샘플이 중복되어 추출될 수도 있다.
→ 이렇게 만들어진 샘플을 부트스트랩 샘플 이라고 부른다.

image1.png

각 노드를 분할할 때는 전체 특성 중에서 일부 특성을 무작위로 고른 다음 이 중에서 최선의 분할을 찾는다.
RandomForestClassifier: 전체 특성 개수의 제곱근만큼의 특성 선택
RandomForestRegressor: 전체 특성 선택

랜덤 포레스트는 랜덤하게 선택한 샘플과 특성을 사용하기 때문에 훈련 세트에 과대적합되는 것을 방지하고 검증 세트와 테스트 세트에서 안정적인 성능을 얻을 수 있음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

wine = pd.read_csv('https://bit.ly/wine-date')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

train_input, test_input, train_target, test_target = train_test_split(
    data, target, test_size=0.2, random_state=42
)

from sklearn.model_selection import cross_validate
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_jobs=-1, random_state=42)

scores = cross_validate(rf, train_input, train_target, 
                        return_train_score=True, n_jobs=-1)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))
# output: 0.9973541965122431 0.8905151032797809
1
2
3
4
rf.fit(train_input, train_target)
print(rf.feature_importances_)

# Output: [0.23167441 0.50039841 0.26792718]

→ 랜덤 포레스트가 특성의 일부를 랜덤하게 선택하여 결정트리를 훈련하였기 때문에 더 많은 특성이 훈련에 기여할 기회를 얻음. 따라서 과대적합을 줄이고 일반화 성능을 높이는 데 도움을 줌.

부트스트랩 샘플에 포함되지 않고 남는 샘플을 OOB라고 한다. 이 남는 샘플을 사용하여 부트스트랩 샘플로 훈련한 결정 트리를 평가할 수 있다.

1
2
3
4
5
6
rf = RandomForestClassifier(oob_score=True, n_jobs=-1, random_state=42)

rf.fit(train_input, train_target)
print(rf.oob_score_)

# Output: 0.8934000384837406

※ 엑스트라 트리
기본적으로 100개의 결정 트리를 훈련하며 전체 특성 중에 일부 특성을 랜덤하게 선택하여 노드를 분할하는데 사용된다.

랜덤 포레스트 VS 엑스트라 트리
랜덤 포레스트와의 차이점은 부트스트랩 샘플을 사용하지 않는다는 점. 엑스트라 트리는 각 결정 트리를 만들 때 전체 훈련 세트를 사용한다.

랜덤 포레스트보다 무작위성이 좀 더 크기 때문에 더 많은 결정 트리를 훈련해야 하지만 랜덤하게 노드를 분할하기 때문에 빠른 계산 속도가 장점

1
2
3
4
5
6
7
8
9
from sklearn.ensemble import ExtraTreesClassifier

et = ExtraTreesClassifier(n_jobs=-1, random_state=42)
scores = cross_validate(et, train_input, train_target,
                        return_train_score=True, n_jobs=-1)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))

# Output: 0.9974503966084433 0.8887848893166506

→ 예제 특성이 많지 않아 두 모델의 차이가 크진 않음.

1
2
3
4
et.fit(train_input, train_target)
print(et.feature_importances_)

# Output: [0.20183568 0.52242907 0.27573525]

※ 그라디언트 부스팅
깊이가 얕은 결정 트리를 사용하여 이전 트리의 오차를 보완하는 방식으로 앙상블하는 방법.
깊이가 얕은 결정 트리를 사용하기 때문에 과대적합에 강하고 일반적으로 높은 일반화 성능을 기대할 수 있다.

경사 하강법을 사용하여 트리를 앙상블에 추가. 분류에서는 로지스틱 손실 함수를 사용하고 회귀에서는 평균 제곱 오차 함수를 사용

1
2
3
4
5
6
7
8
9
from sklearn.ensemble import GradientBoostingClassifier

gb = GradientBoostingClassifier(random_state=42)
scores = cross_validate(gb, train_input, train_target,
                        return_train_score=True, n_jobs=-1)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))

# Output: 0.8881086892152563 0.8720430147331015

→ 그라디언트 부스팅은 결정 트리의 개수를 늘려도 과대적합에 매우 강하다.

1
2
3
4
5
6
7
gb = GradientBoostingClassifier(n_estimators=500, learning_rate=0.2, random_state=42)
scores = cross_validate(gb, train_input, train_target,
                        return_train_score=True, n_jobs=-1)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))

# Output: 0.9464595437171814 0.8780082549788999

→ 결정 트리 개수를 500개로 늘렸음에도 과대적합을 잘 억제하고 있음.

1
2
3
4
gb.fit(train_input, train_target)
print(gb.feature_importances_)

# Output: [0.15872278 0.68010884 0.16116839]

subsample매개변수의 기본값은 1.0으로 전체 훈련 세트를 사용한다. 이때 subsample이 1보다 작으면 훈련 세트의 일부를 사용하는데 이는 마치 경사 하강법 단계마다 일부 샘플을 랜덤하게 선택하여 진행하는 확률적 경사 하강법이나 미니배치 경사 하강법과 비슷하다.

일반적으로 그라디언트 부스팅이 랜덤 포레스트보다 더 높은 성능을 얻을 수 있지만 순서대로 트리를 추가하기 때문에 훈련 속도가 느리다는 단점이 있음.
→ 이러한 단점을 개선한 것이 히스토그램 기반 그라디언트 부스팅

※ 히스토그램 기반 그라디언트 부스팅
정형 데이터를 다루는 머신러닝 알고리즘 중에 가장 인기가 높은 알고리즘으로 입력 특성을 256개의 구간으로 나누고 이 중 하나를 떼어놓아 누락된 값을 위해 사용한다.

사이킷런의 히스토그램 기반 그라디언트 부스팅 클래스는 HistGradientBoostingClassifier으로 기본 매개변수에서 안정적인 성능을 얻을 수 있음

HistGradientBoostingClassifier에는 트리의 개수를 지정하는데 n_estimators 대신에 부스팅 반복 횟수를 지정하는 max_iter 사용 → 성능을 높이려면 max_iter 매개변수를 테스트하면됨.

1
2
3
4
5
6
7
8
9
10
from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier

hgb = HistGradientBoostingClassifier(random_state=42)
scores = cross_validate(hgb, train_input, train_target,
                        return_train_score=True)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))

# Output: 0.9321723946453317 0.8801241948619236

→ 과대적합을 잘 억제하면서 그라디언트 부스팅보다 조금 더 높은 성능 제고

1
2
3
4
5
6
7
8
hgb.fit(train_input, train_target)
print(hgb.feature_importances_)

# Output: [0.23167441 0.50039841 0.26792718]

hgb.score(test_input, test_target)

# Output: 0.8723076923076923

사이킷런 이외에도 대표적인 히스토그램 기반 그라디언트 부스팅 알고리즘을 구현한 라이브러리로 XGBoost, LightGMB이 있음.

1
2
3
4
5
6
7
8
9
from xgboost import XGBClassifier

xgb = XGBClassifier(tree_method='hist', random_state=42)
scores = cross_validate(xgb, train_input, train_target, 
                        return_train_score=True)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))

# Output: 0.8827690284750664 0.8708899089361072
1
2
3
4
5
6
7
8
9
from lightgbm import LGBMClassifier

lgb = LGBMClassifier(random_state=42)
scores = cross_validate(lgb, train_input, train_target, 
                        return_train_score=True, n_jobs=-1)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))

# Output: 0.9338079582727165 0.8789710890649293
This post is licensed under CC BY 4.0 by the author.

Trending Tags