이 장에서 배울 것
이번 장에서는 결정 트리와 앙상블 모델을 배운다. 결정 트리는 데이터를 여러 질문으로 나누며 예측하는 모델이다. 예를 들어 "나이가 30세 이하인가?", "월 사용 시간이 10시간 이상인가?" 같은 질문을 순서대로 던지고, 마지막에 도착한 지점에서 예측값을 낸다.
여기서 한 걸음 더 나아가 여러 개의 트리를 함께 사용하는 방법도 배운다. 대표적으로 랜덤 포레스트와 그래디언트 부스팅이 있다. 랜덤 포레스트는 여러 트리의 의견을 모아 더 안정적인 예측을 만들고, 그래디언트 부스팅은 이전 모델이 틀린 부분을 다음 모델이 보완하는 방식으로 성능을 높인다.
이 장의 목표는 단순히 모델 이름을 외우는 것이 아니다. 트리가 왜 이해하기 쉬운 모델인지, 왜 너무 깊은 트리는 위험한지, 여러 트리를 합치면 어떤 장점과 단점이 생기는지를 판단하는 것이다.

왜 이 주제가 중요한가
현업에서 처음 시도해 볼 만한 강력한 기본 모델을 꼽으라면 트리 계열 모델이 자주 등장한다. 이유는 분명하다. 숫자형 데이터와 범주형 데이터를 비교적 잘 다루고, 변수 간의 복잡한 관계도 어느 정도 잡아낼 수 있으며, 데이터 전처리에 지나치게 민감하지 않은 편이다.
앞 장에서 우리는 정확도, 정밀도, 재현율, F1 점수, 교차검증을 배웠다. 이제 이 평가 도구를 사용해 여러 모델을 공정하게 비교할 수 있다. 모델을 고르는 기준은 "유명한가"가 아니라 "지금 문제에서 검증 성능이 좋은가", "해석할 수 있는가", "운영하기 쉬운가"여야 한다.
결정 트리는 해석하기 쉽지만 과적합되기 쉽다. 랜덤 포레스트는 더 안정적이지만 단일 트리보다 설명하기 어렵다. 그래디언트 부스팅은 성능이 강력한 경우가 많지만 설정에 더 민감하다. 이 장에서는 이 균형을 보는 눈을 기른다.
핵심 개념 설명
결정 트리: 질문을 따라 내려가는 모델
결정 트리는 데이터를 나누는 질문을 반복해서 만든다. 맨 위의 시작점을 루트 노드라고 한다. 중간의 질문 지점을 내부 노드라고 하고, 마지막 예측 지점을 리프 노드라고 한다.
분류 문제에서 리프 노드는 보통 가장 많은 클래스의 비율을 기준으로 예측한다. 예를 들어 어떤 리프에 정상 데이터 90개, 이상 데이터 10개가 있다면 이 리프는 정상으로 예측할 가능성이 높다. 회귀 문제라면 리프에 속한 정답값의 평균을 예측값으로 사용할 수 있다.
중요한 것은 트리가 아무 질문이나 고르지 않는다는 점이다. 트리는 데이터를 더 잘 구분하는 질문을 찾는다. 분류에서는 한쪽 노드에 한 클래스가 많이 모이도록 나누고, 회귀에서는 나뉜 그룹 안의 값들이 비슷해지도록 나눈다. 이때 사용하는 기준을 분할 기준이라고 부른다.
트리 깊이와 과적합
트리는 계속 질문을 추가하면 훈련 데이터를 매우 세밀하게 나눌 수 있다. 문제는 너무 세밀하게 나누면 훈련 데이터의 우연한 패턴까지 외워 버린다는 것이다. 이것이 과적합이다.
예를 들어 고객 이탈을 예측하는 트리가 "최근 접속일", "가입 기간", "결제 횟수"를 넘어 "특정 날짜에 로그인했는가" 같은 지나치게 세부적인 조건까지 사용한다면 새 고객에게는 잘 맞지 않을 수 있다.
그래서 트리 모델에는 복잡도를 제한하는 설정이 있다.
max_depth: 트리의 최대 깊이min_samples_split: 노드를 더 나누기 위해 필요한 최소 샘플 수min_samples_leaf: 리프 노드에 남아야 하는 최소 샘플 수가지치기: 불필요하게 복잡한 가지를 줄이는 방법
초보자는 트리가 깊을수록 좋다고 생각하기 쉽다. 하지만 머신러닝에서 중요한 것은 훈련 데이터를 완벽히 맞히는 것이 아니라, 새 데이터에서 잘 맞히는 것이다.
랜덤 포레스트: 여러 트리의 의견을 모으기
랜덤 포레스트는 이름 그대로 여러 결정 트리를 모은 숲이다. 각각의 트리는 조금씩 다른 데이터를 보고, 조금씩 다른 특징 조합을 사용해 학습한다. 그런 다음 분류 문제에서는 다수결로, 회귀 문제에서는 평균으로 최종 예측을 만든다.
한 명의 사람이 판단하면 편향될 수 있다. 여러 사람이 독립적으로 판단하고 의견을 모으면 더 안정적인 결론에 가까워질 수 있다. 랜덤 포레스트도 비슷하다. 단일 트리 하나는 데이터의 작은 변화에 민감할 수 있지만, 여러 트리를 모으면 예측이 흔들리는 정도가 줄어든다.
다만 랜덤 포레스트는 단일 트리보다 해석이 어렵다. 하나의 트리는 질문 흐름을 따라가며 설명할 수 있지만, 수십 개 또는 수백 개의 트리가 함께 낸 결론은 한눈에 설명하기 어렵다.
그래디언트 부스팅: 실수를 이어서 고치기
그래디언트 부스팅 계열 모델은 여러 약한 모델을 순서대로 쌓는다. 핵심 아이디어는 간단하다. 첫 번째 모델이 예측한다. 그다음 모델은 첫 번째 모델이 부족했던 부분을 보완한다. 다음 모델은 다시 남은 실수를 줄인다. 이런 식으로 작은 개선을 반복한다.
랜덤 포레스트가 여러 트리를 비교적 독립적으로 만든 뒤 의견을 모으는 방식이라면, 부스팅은 앞 모델의 결과를 보고 다음 모델이 이어서 학습하는 방식이다.
이 장에서는 수식보다 흐름만 이해하면 충분하다. 부스팅은 성능이 좋은 경우가 많지만, 학습률, 트리 개수, 깊이 같은 설정에 민감하다. 잘 설정하면 강력하지만, 무작정 복잡하게 만들면 과적합될 수 있다.
feature importance: 어떤 특징이 많이 쓰였는가
트리 계열 모델은 feature importance, 즉 특징 중요도를 제공하는 경우가 많다. 특징 중요도는 모델이 예측할 때 어떤 입력 변수를 많이 활용했는지 보여 준다.
하지만 중요한 주의점이 있다. 중요도가 높다고 해서 그 특징이 결과의 원인이라는 뜻은 아니다. 예를 들어 우산 판매량이 감기 환자 수 예측에 중요하게 나왔다고 해서 우산이 감기를 일으킨다는 뜻은 아니다. 날씨라는 다른 요인이 둘 모두에 영향을 주었을 수 있다.
특징 중요도는 "모델이 무엇을 참고했는지"를 보는 도구이지, "세상이 왜 그렇게 작동하는지"를 증명하는 도구가 아니다.
쉬운 비유 또는 사례
결정 트리를 병원 접수 창구의 문진표로 생각해 보자.
먼저 "열이 있습니까?"라고 묻는다. 예라고 답하면 "기침이 있습니까?"로 이동한다. 아니라고 답하면 "복통이 있습니까?"로 이동한다. 이렇게 질문을 따라가다 보면 마지막에 "감기 가능성 높음", "장염 가능성 높음", "추가 검사 필요" 같은 판단에 도착한다.
단일 트리는 한 명의 의사가 만든 문진표와 비슷하다. 이해하기 쉽지만, 그 의사의 경험에 따라 편향될 수 있다. 랜덤 포레스트는 여러 의사가 각자 문진표를 만들고 의견을 모으는 것과 비슷하다. 그래디언트 부스팅은 첫 진단에서 놓친 부분을 다음 의사가 보완하고, 다시 다음 의사가 남은 오류를 줄이는 과정과 비슷하다.
이 비유에서 중요한 점은 하나다. 질문이 많다고 항상 좋은 진단은 아니다. 너무 세부적인 질문은 과거 환자에게만 맞는 규칙이 될 수 있다. 좋은 모델은 충분히 구체적이지만, 새 환자에게도 통하는 규칙을 찾아야 한다.
직접 해보는 실습
이번 실습에서는 scikit-learn의 유방암 진단 예제 데이터를 사용해 결정 트리, 랜덤 포레스트, 그래디언트 부스팅을 비교한다. 목표는 종양이 악성인지 아닌지 분류하는 것이다. 이 데이터는 학습용 예제이며, 실제 의료 판단에 사용해서는 안 된다.
scikit-learn의 원래 라벨은 0=malignant, 1=benign이다. 여기서는 앞 장과 마찬가지로 악성을 관심 클래스, 즉 positive class 1로 바꾸어 평가한다. 이렇게 해야 F1 점수와 재현율을 "악성을 얼마나 잘 찾아냈는가"라는 관점에서 해석하기 쉽다.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import classification_report, confusion_matrix
import pandas as pd
# 1. 데이터 불러오기
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
# 원래 라벨: 0=malignant, 1=benign
# 관심 클래스인 악성을 1로 변환한다.
y = (data.target == 0).astype(int)
target_names = ["not malignant", "malignant"]
# 2. 훈련 데이터와 테스트 데이터 나누기
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y
)
# 3. 모델 준비
models = {
"Decision Tree": DecisionTreeClassifier(max_depth=4, random_state=42),
"Random Forest": RandomForestClassifier(n_estimators=100, random_state=42),
"Gradient Boosting": GradientBoostingClassifier(random_state=42)
}
# 4. 교차검증으로 비교
for name, model in models.items():
scores = cross_val_score(model, X_train, y_train, cv=5, scoring="f1")
print(name, "평균 F1:", scores.mean().round(4))
# 5. 테스트 데이터에서 최종 평가
best_model = RandomForestClassifier(n_estimators=100, random_state=42)
best_model.fit(X_train, y_train)
y_pred = best_model.predict(X_test)
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, target_names=target_names))
# 6. 특징 중요도 확인
importances = pd.Series(best_model.feature_importances_, index=X.columns)
print(importances.sort_values(ascending=False).head(10))실행 후에는 다음을 확인하자.
첫째, 단일 결정 트리와 랜덤 포레스트의 교차검증 F1 점수를 비교한다. 단일 분할의 결과만 보지 말고, 5개 검증 결과의 평균을 본다. 앞 장에서 배운 것처럼 한 번의 train/test 분할은 우연에 영향을 받을 수 있다.
둘째, 혼동 행렬을 확인한다. confusion_matrix의 기본 출력은 [[TN, FP], [FN, TP]] 순서다. 여기서 FN은 실제 악성인데 모델이 악성이 아니라고 판단한 경우다. 의료 문제에서는 이런 실수가 특히 중요하므로 재현율도 함께 봐야 한다.
셋째, 특징 중요도 상위 10개를 본다. 어떤 특징이 모델 예측에 많이 기여했는지 확인하되, 그것을 곧바로 원인으로 해석하지 않는다.
추가 실험도 해 보자. DecisionTreeClassifier(max_depth=None)로 바꾸면 훈련 데이터 성능은 좋아질 수 있다. 하지만 교차검증 점수나 테스트 점수가 항상 좋아지는 것은 아니다. max_depth=2, max_depth=4, max_depth=8을 비교하면서 트리 깊이와 과적합의 관계를 직접 확인해 보자.
흔한 실수와 오류 분석
첫 번째 실수는 훈련 정확도만 보는 것이다. 결정 트리는 훈련 데이터를 거의 외울 수 있다. 훈련 점수가 높아도 테스트 점수가 낮다면 좋은 모델이 아니다.
두 번째 실수는 트리 그림을 해석할 수 있다는 이유만으로 단일 트리를 고집하는 것이다. 설명 가능성은 중요하지만, 예측 성능과 안정성도 중요하다. 단일 트리가 너무 불안정하다면 랜덤 포레스트를 사용하고, 대신 특징 중요도나 오류 분석으로 설명을 보완할 수 있다.
세 번째 실수는 feature importance를 원인으로 착각하는 것이다. 중요도는 모델 내부의 사용량에 가깝다. 데이터에 편향이 있으면 중요도도 편향될 수 있다. 예를 들어 특정 지역 정보가 대출 승인 예측에 높게 나타났다면, 그 지역이 정말 상환 능력의 원인인지, 아니면 과거 심사 관행이 반영된 것인지 따져봐야 한다.
네 번째 실수는 부스팅 모델을 무조건 최고라고 생각하는 것이다. 부스팅은 강력하지만 설정에 민감하고, 데이터가 작거나 노이즈가 많을 때 과적합될 수 있다. 처음에는 결정 트리, 랜덤 포레스트, 부스팅을 같은 평가 기준으로 비교하는 습관이 더 중요하다.
다섯 번째 실수는 라벨의 의미를 확인하지 않고 지표를 해석하는 것이다. 특히 유방암 예제처럼 원래 라벨에서 악성이 0, 양성 종양이 1인 데이터는 그대로 F1을 계산하면 benign 클래스를 기준으로 해석될 수 있다. 어떤 클래스를 관심 대상으로 볼지 정하고, 필요하면 라벨을 변환한 뒤 지표를 계산해야 한다.
오류 분석을 할 때는 틀린 예측만 따로 모아 보자. 어떤 클래스에서 자주 틀리는가? 특정 구간의 값에서 오류가 많은가? 중요도가 높은 특징이 실제로 오류 감소에 도움이 되는가? 이런 질문을 던지면 모델 성능 개선의 방향이 보인다.
요약
결정 트리는 질문을 따라 데이터를 나누며 예측하는 모델이다. 이해하기 쉽고 설명하기 좋지만, 너무 깊어지면 훈련 데이터를 외워 과적합될 수 있다. max_depth, min_samples_split, min_samples_leaf 같은 설정은 트리의 복잡도를 조절하는 장치다.
랜덤 포레스트는 여러 트리를 만들어 예측을 모은다. 단일 트리보다 안정적인 경우가 많지만 해석은 더 어려워진다. 그래디언트 부스팅은 이전 모델의 실수를 다음 모델이 보완하는 방식으로 성능을 높인다. 강력하지만 설정에 주의해야 한다.
특징 중요도는 모델 해석에 도움이 된다. 그러나 중요도는 인과관계를 뜻하지 않는다. 모델이 무엇을 사용했는지와 현실에서 무엇이 원인인지는 구분해야 한다.
트리 계열 모델을 비교할 때도 평가 지표의 기준 클래스와 라벨 의미를 반드시 확인해야 한다. 좋은 비교는 같은 데이터 분할, 같은 관심 클래스, 같은 평가 지표에서 시작된다.
퀴즈
결정 트리에서 리프 노드는 어떤 역할을 하는가?
트리 깊이가 너무 깊을 때 생길 수 있는 문제는 무엇인가?
랜덤 포레스트가 단일 결정 트리보다 안정적인 이유는 무엇인가?
그래디언트 부스팅의 핵심 아이디어를 한 문장으로 설명해 보자.
feature importance가 높다는 말이 왜 인과관계를 의미하지 않는가?
분류 지표를 해석하기 전에 라벨의 의미를 확인해야 하는 이유는 무엇인가?
체크리스트
결정 트리를 질문 흐름으로 설명할 수 있다.
루트 노드, 내부 노드, 리프 노드의 의미를 구분할 수 있다.
트리 깊이와 과적합의 관계를 설명할 수 있다.
랜덤 포레스트와 단일 트리의 차이를 말할 수 있다.
그래디언트 부스팅의 기본 흐름을 이해했다.
교차검증으로 여러 모델을 비교할 수 있다.
feature importance를 해석할 때 인과관계와 구분할 수 있다.
관심 클래스와 라벨 의미를 확인한 뒤 F1과 재현율을 해석할 수 있다.
다음 장 연결
이번 장에서는 트리 계열 모델을 통해 성능과 해석 가능성의 균형을 살펴보았다. 다음 장에서는 다른 방식으로 문제를 푸는 세 가지 모델을 배운다. SVM은 경계를 잘 긋는 모델이고, kNN은 가까운 이웃을 보고 판단하는 모델이며, 나이브 베이즈는 확률을 이용해 빠르게 분류하는 모델이다.
트리 모델이 모든 문제의 답은 아니다. 데이터의 크기, 특징의 수, 경계의 형태, 설명 요구 수준에 따라 더 적합한 모델이 달라진다. 다음 장에서는 모델 선택의 폭을 한 번 더 넓혀 보자.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.