포스트

머신러닝 기초 (10) - 결정트리

결정트리가 특성 기준으로 데이터를 반복 분할하며 무엇을 학습하는지, 지니와 엔트로피로 분할 기준을 잡는 직관, 깊이가 깊어질 때의 과적합과 max_depth, min_samples_leaf 제어, 그리고 트리의 장단점을 load_breast_cancer, load_diabetes로 확인하는 10편입니다.

머신러닝 기초 (10) - 결정트리

머신러닝 기초 시리즈의 10편입니다. 9편의 “선형 모델: 선형 회귀와 로지스틱 회귀”에 이어집니다.

선형 모델이 그을 수 없는 경계

9편의 선형 모델은 특성들의 가중합 하나로 예측을 만들었습니다. 강력하고 해석도 쉬운 골격이지만, 그 형태 자체에서 오는 한계가 있습니다.

  • 휘어 있는 관계: 특성과 타깃의 관계가 곡선이면 직선(초평면)으로는 잡히지 않습니다.
  • 특성 간 상호작용: “혈압이 높으면서 동시에 BMI가 클 때만 위험이 급증”처럼 특성이 함께 작용하는 구조를, 가중합의 기본형은 표현하지 못합니다.
  • 스케일 민감성: 계수의 크기가 특성의 단위에 좌우되므로, 규제를 쓰려면 표준화가 먼저입니다(6편, 12편).

결정트리(decision tree)는 이 셋을 전혀 다른 방식으로 우회합니다. 데이터를 특성 기준으로 반복해서 잘라 나가는, 완전히 다른 골격의 모델입니다.

결정트리는 질문을 반복한다

결정트리의 예측은 질문의 연쇄입니다. 하나의 특성과 임계값으로 데이터를 둘로 가르는 질문(예를 들어 “worst radius가 16.8 이하인가?”)을 던지고, 답에 따라 갈라진 쪽에서 다음 질문을 던집니다. 이 과정을 반복하다 더 나누지 않는 지점(잎, leaf)에 도달하면, 그 잎에 모인 훈련 샘플의 다수 클래스(분류)나 평균값(회귀)을 예측으로 내놓습니다.

즉 학습된 트리는 중첩된 if/else 규칙의 묶음입니다. 유방암 데이터에 깊이 2짜리 트리를 학습시켜 규칙을 직접 꺼내 보면 구조가 그대로 보입니다.

1
2
3
4
5
6
7
8
9
10
11
12
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

data = load_breast_cancer()
X, y = data.data, data.target          # 타깃: 0=악성, 1=양성
X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

clf = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)
print(export_text(clf, feature_names=list(data.feature_names)))
1
2
3
4
5
6
7
8
9
10
|--- worst radius <= 16.80
|   |--- worst concave points <= 0.14
|   |   |--- class: 1          ← 양성
|   |--- worst concave points >  0.14
|   |   |--- class: 0          ← 악성
|--- worst radius >  16.80
|   |--- texture error <= 0.47
|   |   |--- class: 1
|   |--- texture error >  0.47
|   |   |--- class: 0

뿌리(root)에서 “worst radius ≤ 16.8”을 묻고, 그 답에 따라 다음 질문이 달라집니다. 여기에 이미 두 가지 성질이 드러납니다. 첫째, 트리는 예측을 조건들의 연쇄로 만듭니다(“반지름이 작고 & 오목한 정도가 낮으면 양성”). 둘째, 각 질문은 특성 하나의 임계값만 봅니다. 이 단순함이 뒤에서 스케일 불필요라는 장점으로 이어집니다.

무엇을 기준으로 나누는가: 불순도

왜 하필 “worst radius ≤ 16.8”이 첫 질문으로 뽑혔을까요. 좋은 분할이란, 나눈 뒤 각 쪽이 한 클래스로 쏠릴수록 좋은 분할입니다. 한쪽은 거의 다 양성, 다른 쪽은 거의 다 악성으로 갈리면 그 질문이 정답에 대한 정보를 많이 준 것입니다. 이 “쏠린 정도”의 반대말, 즉 여러 클래스가 섞인 정도를 불순도(impurity)라고 부릅니다.

불순도를 재는 방법이 둘입니다. 클래스별 비율을 p라 할 때,

1
2
3
4
5
6
7
8
# 지니 불순도  gini = 1 - sum(p_i ** 2)
# 엔트로피     entropy = -sum(p_i * log2(p_i))

def gini(p):    return 1 - sum(pi**2 for pi in p)

gini([1.0, 0.0])    # 0.0  한 클래스뿐이라 완전히 순수
gini([0.5, 0.5])    # 0.5  반반으로 섞여 이진에서 가장 불순
gini([0.7, 0.3])    # 0.42 한쪽으로 쏠린 만큼 낮아짐

트리는 모든 특성과 가능한 임계값을 훑어보며, 나눈 뒤 두 자식의 불순도 가중평균이 부모보다 가장 많이 줄어드는 분할을 고릅니다(이 감소량을 정보이득이라 합니다). 이걸 뿌리부터 잎까지 탐욕적으로(greedy) 반복하는 것이 트리 학습의 전부입니다. 회귀트리도 원리는 같고, 불순도 대신 자식 안의 분산(MSE) 이 가장 많이 줄도록 나눕니다.

지니와 엔트로피는 모양이 비슷해 결과 트리도 대체로 비슷합니다. 어느 쪽을 쓸지는 성능에 큰 영향이 없어, scikit-learn 기본값인 지니를 그냥 써도 무방합니다. 분할 기준의 선택보다 트리 깊이를 통제하는 것이 훨씬 중요합니다.

깊이가 깊어지면 외운다

제약을 걸지 않으면 트리는 잎이 순수해질 때까지, 즉 각 잎에 한 클래스(또는 한 샘플)만 남을 때까지 계속 나눕니다. 그 끝에서 트리는 훈련 데이터를 통째로 외웁니다. 4편에서 본 과대적합이 트리에서는 이렇게 나타납니다. 깊이 제한 없이 회귀트리를 키우며 깊이별 train/val 성능을 비교하면 분명하게 보입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn.datasets import load_diabetes
from sklearn.tree import DecisionTreeRegressor

X, y = load_diabetes(return_X_y=True)
X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, random_state=42
)

for d in [1, 2, 3, 5, None]:          # None = 제한 없이 끝까지
    reg = DecisionTreeRegressor(max_depth=d, random_state=42).fit(X_train, y_train)
    print(d, round(reg.score(X_train, y_train), 3), round(reg.score(X_val, y_val), 3))
# 1     0.304 0.131   ← 너무 얕아 train도 낮음(과소적합)
# 2     0.447 0.295
# 3     0.517 0.329   ← val이 정점 부근
# 5     0.669 0.334
# None  1.0   0.061   ← train은 완벽, val은 붕괴(과적합)

깊이가 깊어질수록 train R²는 1.0까지 단조롭게 올라갑니다. 트리가 데이터를 점점 더 세밀하게 외우기 때문입니다. 반면 val R²는 깊이 3~5 부근에서 정점을 찍고 이후 떨어집니다. 제한 없이 키운 트리(깊이 19까지 자랍니다)는 train을 완벽히 맞히면서 val은 0.06으로 무너집니다. 평균으로 찍는 것과 다를 바 없는 수준입니다. 훈련 점수가 1.0이라는 사실 자체가 과적합의 경고입니다.

분류에서도 방향은 같습니다.

1
2
3
4
5
6
7
for d in [1, 2, 3, 5, None]:
    clf = DecisionTreeClassifier(max_depth=d, random_state=42).fit(X_train, y_train)
    print(d, round(clf.score(X_train, y_train), 3), round(clf.score(X_val, y_val), 3))
# (breast_cancer 기준)
# 1     0.923 0.921
# 3     0.976 0.939   ← val 최고 부근
# None  1.0   0.912   ← train만 완벽

과적합을 제어하는 설정

해법은 트리를 끝까지 자라기 전에 미리 멈추는 것입니다(사전 가지치기, pre-pruning). 자주 쓰는 설정 둘입니다.

  • max_depth: 트리의 최대 깊이. 질문을 몇 단계까지 던질지 직접 제한합니다.
  • min_samples_leaf: 잎 하나에 최소 몇 개의 샘플이 남아야 하는지. 이 값을 키우면 샘플 한두 개짜리 잎이 생기지 못해, 트리가 잡음까지 외우는 것을 막습니다.

제한 없는 트리와 min_samples_leaf를 건 트리를 비교하면 효과가 드러납니다.

1
2
3
4
5
for m in [1, 10]:
    clf = DecisionTreeClassifier(min_samples_leaf=m, random_state=42).fit(X_train, y_train)
    print(m, round(clf.score(X_train, y_train), 3), round(clf.score(X_val, y_val), 3))
# 1    1.0   0.912   ← 잎을 끝까지 나눔: train 완벽, val 낮음
# 10   0.963 0.947   ← 잎당 최소 10개 강제: train 낮아지고 val 올라감

잎마다 최소 10개를 강제하자 train은 1.0에서 0.963으로 내려오지만 val은 0.912에서 0.947로 올라갑니다. 훈련 성능을 일부 포기하고 일반화를 얻는, 전형적인 편향-분산 절충입니다(4편). 이 값들을 눈대중으로 정하지 않고 교차검증으로 고르는 방법은 5편에서 다뤘습니다.

결정트리는 기본값(max_depth=None)으로 두면 거의 항상 훈련 데이터를 과적합합니다. model.score(X_train, y_train)이 1.0에 가깝다고 좋아할 일이 아니라, 깊이나 잎 크기를 통제했는지부터 확인해야 합니다.

트리의 장점과 단점

트리는 성질이 뚜렷한 모델입니다. 강점과 약점이 모두 “특성 기준으로 반복 분할한다”는 한 구조에서 나옵니다.

장점

  • 비선형과 상호작용을 잡는다: 분할의 연쇄가 곧 “A가 크고 그중 B가 작으면…” 같은 조건부 규칙이라, 곡선 관계와 특성 간 상호작용을 자연스럽게 표현합니다. 선형 모델이 놓치던 부분입니다.
  • 스케일이 필요 없다: 각 질문은 특성 하나의 임계값만 봅니다. 어떤 특성에 100을 곱하거나 로그를 취해도(순서를 뒤집지 않는 한) 분할 지점만 그만큼 옮겨질 뿐 트리 구조는 같습니다. 표준화와 정규화가 결과를 바꾸지 않습니다.
  • 해석이 쉽다: 얕은 트리는 위에서 본 규칙표처럼 사람이 그대로 읽을 수 있습니다. 어떤 특성이 예측에 크게 기여했는지도 feature_importances_로 확인됩니다(불순도를 많이 줄인 특성일수록 값이 큽니다).
1
2
3
4
5
6
7
8
import numpy as np
clf = DecisionTreeClassifier(max_depth=3, random_state=42).fit(X_train, y_train)
top = np.argsort(clf.feature_importances_)[::-1][:3]
for i in top:
    print(data.feature_names[i], round(clf.feature_importances_[i], 3))
# worst radius         0.764   ← 이 트리가 가장 많이 의존한 특성
# worst concave points 0.127
# texture error        0.048

단점

  • 불안정하다: 훈련 데이터가 조금만 바뀌어도 뿌리의 분할이 달라지고, 그 아래 전체가 딸려 바뀝니다. 탐욕적으로 한 번에 하나씩 최선의 분할을 고르는 방식이라, 작은 변화에 트리 모양이 크게 흔들립니다(분산이 크다).
  • 과적합 경향: 앞에서 본 대로, 통제하지 않으면 데이터를 외웁니다.
  • 축에 정렬된 경계: 각 분할이 한 특성의 임계값이라 경계가 항상 축에 수직입니다. 비스듬한 경계는 계단 모양으로 근사할 수밖에 없습니다.

약점을 앙상블이 메운다

트리의 가장 아픈 약점은 불안정성입니다. 한 그루의 트리는 분산이 커서, 같은 문제에 대해서도 데이터가 조금 바뀌면 예측이 출렁입니다. 그런데 이 약점에는 정공법이 있습니다. 서로 조금씩 다른 트리를 여러 그루 학습시켜 예측을 평균(회귀)하거나 투표(분류)로 모으면, 각자의 흔들림이 상쇄되어 분산이 줄어듭니다. 여러 개를 모아 하나보다 나은 모델을 만드는 이 방식이 앙상블(ensemble)입니다.

방향은 크게 둘입니다. 데이터를 조금씩 다르게 뽑아 독립적인 트리를 많이 키운 뒤 모으는 배깅 계열(랜덤포레스트), 그리고 약한 트리를 순서대로 세우며 앞선 트리의 오차를 뒤 트리가 교정해 가는 부스팅 계열입니다. 결정트리가 실전에서 자주 이기는 모델의 부품이 되는 이유가 여기 있습니다. 이 두 방향을 11편에서 다룹니다.

정리

개념한 줄 요약
결정트리특성 기준의 질문(임계값 분할)을 반복해 잎에 도달하는 if/else 규칙의 묶음
예측잎에 모인 훈련 샘플의 다수 클래스(분류)나 평균(회귀)
분할 기준나눈 뒤 불순도(지니와 엔트로피)가 가장 많이 줄어드는 특성과 임계값을 탐욕적으로 선택. 회귀는 분산 감소
과적합제한이 없으면 잎이 순수해질 때까지 나눠 데이터를 외움(train 1.0, val 붕괴)
제어max_depth로 깊이를, min_samples_leaf로 잎 크기를 제한. 값은 교차검증으로
장점비선형과 상호작용 포착, 스케일 불필요, 규칙과 특성 중요도로 해석 가능
단점데이터 변화에 불안정(분산 큼), 통제 없으면 과적합, 축에 정렬된 경계
앙상블 예고서로 다른 트리 여러 그루를 모으면 분산이 줄어든다. 배깅과 부스팅(11편)

다음 편에서는 이 트리를 부품으로 삼아, 한 그루의 불안정을 여러 그루로 메우는 앙상블(랜덤포레스트와 부스팅)을 다룹니다.

다음 글: 머신러닝 기초 (11) - 앙상블: 랜덤포레스트와 부스팅

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.