강의자료 · 머신러닝의 이해

[10주차] 비선형 문제 해결사: Decision Tree와 Random Forest 🌳🌲

안녕하세요! 10주차 수업에 오신 것을 환영합니다.
지난 시간까지 우리는 Linear Regression과 Logistic Regression, 즉 선형 모델을 배웠습니다.
이 모델들은 데이터의 관계를 하나의 ‘직선’ 또는 ’평면’으로 설명하려 했습니다.

하지만 현실 세계의 데이터는 직선만으로 설명하기 어려운,
더 복잡한 비선형(Non-linear) 관계를 가지는 경우가 많습니다.

오늘은 이러한 비선형 문제를 효과적으로 해결할 수 있는 강력한 모델인
Decision Tree 와, 여기서 더 발전된 Random Forest 에 대해
학습하고 scikit-learn으로 직접 구현해 보겠습니다.

🌳 모듈 1: Decision Tree - 스무고개처럼 질문하며 나누기

Decision Tree란?

데이터 예시: 놀이공원에 갈지 말지 결정하는 데이터

날씨 주말여부 간다
맑음 주말 Yes
맑음 평일 Yes
흐림 주말 Yes
비 주말 No
비 평일 No

목표: 데이터를 가장 잘 나누는 최적의 질문을 찾는 것!
➡️ 그룹이 최대한 ‘순수한(pure)’ 상태가 되도록 (한 그룹에는 한 종류만)

1단계: 현재 상태(루트 노드)의 불순도 계산

2단계: 가능한 첫 번째 질문들의 효과 평가

질문 후보 1: “날씨는?”

질문 후보 2: “주말여부는?”

3단계: 최적의 질문 선택

4단계: 트리 완성 및 구조 시각화

아래 코드는 이 최종 트리 구조를 시각화하고, 주요 용어를 설명합니다.

# graphviz 라이브러리를 이용한 트리 시각화
import graphviz

# 트리 구조 정의 (DOT language)
dot_source = """
digraph WeatherTree {
    node [shape=box, style="filled, rounded", fillcolor="#e8f4f8"];
    edge [fontname="Helvetica"];

    // 루트 노드
    0 [label="날씨는?", fillcolor="#a1d6e2"];

    // 리프 노드
    1 [label="간다 (Yes)\n(맑음)", fillcolor="#7dd3a2"];
    2 [label="간다 (Yes)\n(흐림)", fillcolor="#7dd3a2"];
    3 [label="안간다 (No)\n(비)", fillcolor="#f8a7a7"];

    // 가지 (Branch)
    0 -> 1 [label="맑음"];
    0 -> 2 [label="흐림"];
    0 -> 3 [label=" 비 "];
}
"""
graph = graphviz.Source(dot_source)
graph # Colab에서 바로 시각화 결과를 보여줍니다.
<graphviz.sources.Source at 0x7e59e45aaed0>

Decision Tree 주요 용어 (위 그림 참고):

이처럼 Decision Tree는 각 단계에서 정보 획득을 최대화하는 질문을 찾아
데이터를 반복적으로 분할하여 최종 예측 규칙을 만들어 나갑니다.

🌸 모듈 2: 단계별 Decision Tree 모델링 (붓꽃 데이터)

이제 scikit-learn을 이용하여 붓꽃(Iris) 데이터셋으로 Decision Tree 모델을 만들어 봅시다.
이번에는 꽃잎 길이(petal length) 와 꽃잎 너비(petal width) 두 특징을 사용하여
세 가지 품종(setosa, versicolor, virginica)을 모두 분류하는
다중 분류(Multi-class Classification) 문제입니다.

다시 한번 Machine Learning 워크플로우 5단계를 따라갑니다.

# 필요한 라이브러리 임포트
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
# DecisionTreeClassifier 를 불러옵니다.
from sklearn.tree import DecisionTreeClassifier
# 평가 지표 함수들을 불러옵니다.
from sklearn.metrics import (accuracy_score,
                             confusion_matrix,
                             classification_report)

# 1. 데이터 로드
iris = load_iris()

# 특징(X): 꽃잎 길이(2번), 꽃잎 너비(3번) -> 2개의 특징 사용!
X = iris.data[:, 2:4]
# 정답(y): 붓꽃 품종 (0, 1, 2)
y = iris.target
feature_names = iris.feature_names[2:]
        # 'petal length (cm)', 'petal width (cm)'
target_names = iris.target_names
        # 'setosa', 'versicolor', 'virginica'

# 2. 데이터 탐색 (시각화)
plt.figure(figsize=(8, 6))
# 품종(y)별로 다른 색상/마커로 산점도 표시
for i, target_name in enumerate(target_names):
    plt.scatter(X[y == i, 0], X[y == i, 1], label=target_name)

plt.title("Iris data (feature)")
plt.xlabel(feature_names[0])
plt.ylabel(feature_names[1])
plt.legend()
plt.grid(True)
plt.show()

print("결과 해석:")
print("- Setosa(파랑색)는 명확히 구분됩니다.")
print("- Versicolor(주황색)와 Virginica(녹색)는 일부 겹쳐 보입니다.")
print("- 직선 하나로는 세 품종을 완벽히 나누기 어려워 보입니다.")
<Figure size 800x600 with 1 Axes>
실행 결과
결과 해석:
- Setosa(파랑색)는 명확히 구분됩니다.
- Versicolor(주황색)와 Virginica(녹색)는 일부 겹쳐 보입니다.
- 직선 하나로는 세 품종을 완벽히 나누기 어려워 보입니다.

다음으로, 데이터를 연습 문제와 실전 시험으로 분리합니다.
다중 분류 문제이므로 stratify 옵션을 사용합니다.

# 데이터를 '연습 문제'와 '실전 시험'으로 7:3 분리
# 다중 분류 문제이므로 stratify=y 옵션 사용
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Train 데이터 크기: {X_train.shape}")
print(f"Test 데이터 크기: {X_test.shape}")
Train 데이터 크기: (105, 2)
Test 데이터 크기: (45, 2)

이제 Decision Tree 모델을 생성하고 학습시킵니다.

DecisionTreeClassifier의 주요 하이퍼파라미터:

# 1. 결정 트리 모델 객체 생성
# random_state: 동일한 결과를 얻기 위해 난수 발생 시드 고정
# max_depth: 트리의 최대 깊이를 제한 (None이면 무제한 성장)
# -> 과적합 방지를 위한 중요한 하이퍼파라미터 (나중에 설명)
model_dt = DecisionTreeClassifier(random_state=42)

# 2. '연습 문제'로 모델 학습 (fit)
# 내부적으로 최적의 질문(특징/기준값)을 찾아가며
# 데이터를 분할하는 트리 구조를 만듭니다.
model_dt.fit(X_train, y_train)

print("결정 트리 모델 학습 완료!")
결정 트리 모델 학습 완료!

학습된 Decision Tree의 내부 구조를 시각화하여
모델이 어떤 질문들을 통해 데이터를 분류하는지 직접 확인할 수 있습니다.
(graphviz 라이브러리 필요)

import graphviz
from sklearn.tree import export_graphviz
# 학습된 트리 구조 시각화
dot_data = export_graphviz(model_dt, out_file=None,
                           feature_names=feature_names,
                           class_names=target_names,
                           filled=True, rounded=True,
                           special_characters=True)
graph = graphviz.Source(dot_data)
graph # Colab에서 바로 시각화 결과를 보여줍니다.
<graphviz.sources.Source at 0x7e59b4f63020>

학습된 트리가 데이터 공간을 어떻게 나누는지(결정 경계) 시각화해 봅시다.
이를 통해 Overfitting 의 징후를 확인할 수 있습니다.

Overfitting이란, 모델이 연습 문제에 너무 특화되어
실전 시험에서는 성능이 떨어지는 현상을 말합니다.
Decision Tree는 제한 없이 성장하면 연습 데이터의 노이즈까지 학습하여
매우 복잡한 결정 경계를 만들고 Overfitting되기 쉽습니다.

# 결정 경계 시각화 함수 정의 (이전 강의 내용 재사용)
def plot_decision_boundary(model, X, y, target_names, feature_names, title):
    plt.figure()
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                         np.arange(y_min, y_max, 0.02))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    plt.contourf(xx, yy, Z, cmap='coolwarm', alpha=0.8)
    for i, target_name in enumerate(target_names):
        plt.scatter(X[y == i, 0], X[y == i, 1], label=target_name,
                    edgecolors='k')
    plt.title(title)
    plt.xlabel(feature_names[0])
    plt.ylabel(feature_names[1])
    plt.legend()
    plt.show()

# 학습된 결정 트리 모델의 결정 경계 확인 (Train 데이터 기준)
plot_decision_boundary(model_dt, X_train, y_train, target_names,
                       feature_names, "Decision Boundary (Train Data)")

print("결과 해석:")
print("- 연습 문제(Train Data)를 잘 분류하기 위해")
print("  절묘한 결정 경계가 만들어졌습니다.")
print("- 과적합(Overfitting)의 가능성이 있습니다.")
<Figure size 640x480 with 1 Axes>
실행 결과
결과 해석:
- 연습 문제(Train Data)를 잘 분류하기 위해
  절묘한 결정 경계가 만들어졌습니다.
- 과적합(Overfitting)의 가능성이 있습니다.
# 결정 트리 모델 성능 평가 (Test 데이터 사용)
y_pred_dt = model_dt.predict(X_test)

accuracy_dt = accuracy_score(y_test, y_pred_dt)
report_dt = classification_report(y_test, y_pred_dt, target_names=target_names)

print("--- 결정 트리 성능 평가 (Test Data) ---")
print(f"정확도: {accuracy_dt:.4f}")
print("\nClassification Report:")
print(report_dt)

# 결정 경계 시각화 (Test 데이터 기준)
plot_decision_boundary(model_dt, X_test, y_test, target_names,
                       feature_names, "Decision Boundary (Test Data)")
--- 결정 트리 성능 평가 (Test Data) ---
정확도: 0.9333

Classification Report:
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       0.88      0.93      0.90        15
   virginica       0.93      0.87      0.90        15

    accuracy                           0.93        45
   macro avg       0.93      0.93      0.93        45
weighted avg       0.93      0.93      0.93        45

<Figure size 640x480 with 1 Axes>
실행 결과

🌲 모듈 3: Overfitting과 Random Forest

Decision Tree의 문제점: Overfitting (Overfitting)

Overfitting 완화 방법:

Random Forest (Random Forest): 앙상블의 힘

Decision Tree가 혼자서 모든 환자를 진단하는 만능 의사라고 해봅시다.
이 의사는 자신이 본 환자들(학습 데이터)에게는 완벽한 진단(100점)을 내리지만,
그 경험에 너무 특화된 나머지, 약간 다른 증상을 보이는 새로운 환자(테스트 데이터)에게는
오히려 오진(Overfitting)을 내릴 위험이 있습니다.

Random Forest는 이런 문제를 해결하기 위해,
한 명의 만능 의사 대신 여러 명의 ‘조금씩 다른 전문의’ 들을 모아
의사 위원회(Committee) 를 구성하는 것과 같습니다.
➡️ 이 의사의 수가 바로 RandomForestClassifier의 n_estimators 파라미터입니다.

어떻게 ‘조금씩 다른’ 의사들을 만들까요? (Randomness!)

  1. 서로 다른 환자 경험 (데이터 샘플링 - Bagging):
    • 전체 환자 기록(원본 데이터)에서 무작위로 일부 환자 기록을 복원 추출하여
      각 의사에게 전달합니다.
    • 어떤 의사는 특정 환자를 여러 번 볼 수도 있고, 어떤 환자는 아예 못 볼 수도 있습니다.
    • 이렇게 하면 각 의사는 조금씩 다른 환자 그룹을 경험하게 됩니다.
    • ➡️ bootstrap=True (기본값) 파라미터가 이 방식을 사용함을 의미합니다.
  2. 집중하는 증상 제한 (특징 무작위 선택):
    • 각 의사가 진단(질문)을 내릴 때마다,
      고려할 수 있는 증상(특징)의 종류를 무작위로 제한합니다.
    • 예를 들어, 어떤 의사는 ’체온’과 ’기침’만 보고 판단하고,
      다른 의사는 ’두통’과 ’피로감’만 보고 판단하도록 강제합니다.
    • 이렇게 하면 각 의사는 특정 증상 조합에 더 전문성을 갖게 됩니다.
    • ➡️ max_features 파라미터가 이 단계를 제어합니다.
      (기본값 n_features\sqrt{n\_features})

진단 과정 (예측):

왜 더 효과적일까요?

이제 Random Forest 모델을 생성하고 학습시켜
Decision Tree 모델과 성능을 비교해 봅시다.

# 랜덤 포레스트 모델 임포트
from sklearn.ensemble import RandomForestClassifier

# 1. 랜덤 포레스트 모델 생성 및 학습
# n_estimators: 만들 트리의 개수 (의사의 수). 많을수록 좋지만 계산 시간 증가.
# random_state: 결과 재현을 위한 시드값.
model_rf = RandomForestClassifier(n_estimators=15, random_state=42)
model_rf.fit(X_train, y_train)
plot_decision_boundary(model_rf, X_train, y_train, target_names,
                       feature_names, "Random Forest Decision Boundary (Train Data)")

# 2. 랜덤 포레스트 모델 성능 평가 (Test 데이터 사용)
y_pred_rf = model_rf.predict(X_test)
accuracy_rf = accuracy_score(y_test, y_pred_rf)
report_rf = classification_report(y_test, y_pred_rf, target_names=target_names)

print("--- 랜덤 포레스트 성능 평가 (Test Data) ---")
print(f"정확도: {accuracy_rf:.4f}")
print("\nClassification Report:")
print(report_rf)

# 결정 경계 시각화 (Test 데이터 기준)
plot_decision_boundary(model_rf, X_test, y_test, target_names,
                       feature_names, "Random Forest Decision Boundary (Test Data)")

print("\n비교:")
print(f"- 결정 트리 정확도: {accuracy_dt:.4f}")
print(f"- 랜덤 포레스트 정확도: {accuracy_rf:.4f}")
print("\n=> 과적합의 느낌이 없지는 않지만, 정확도가 약간 향상되었습니다.")
<Figure size 640x480 with 1 Axes>
실행 결과
--- 랜덤 포레스트 성능 평가 (Test Data) ---
정확도: 0.9556

Classification Report:
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       0.93      0.93      0.93        15
   virginica       0.93      0.93      0.93        15

    accuracy                           0.96        45
   macro avg       0.96      0.96      0.96        45
weighted avg       0.96      0.96      0.96        45

<Figure size 640x480 with 1 Axes>
실행 결과
비교:
- 결정 트리 정확도: 0.9333
- 랜덤 포레스트 정확도: 0.9556

=> 과적합의 느낌이 없지는 않지만, 정확도가 약간 향상되었습니다.

Random Forest는 각 특징이 모델 예측에 얼마나 중요한 역할을 했는지 알려주는
특징 중요도(Feature Importance) 정보를 제공합니다.

# (참고) 특징 중요도 확인
# model_rf 객체의 feature_importances_ 속성에 저장되어 있음
importances = model_rf.feature_importances_
print("\n--- 특징 중요도 (Feature Importances) ---")
for name, importance in zip(feature_names, importances):
    print(f"{name}: {importance:.4f}")
print("\n=> 이 문제에서는 꽃잎 너비(petal width)가 품종 분류에")
print("   꽃잎 길이(petal length)보다 약간 더 중요한 특징임을 알 수 있습니다.")
--- 특징 중요도 (Feature Importances) ---
petal length (cm): 0.5217
petal width (cm): 0.4783

=> 이 문제에서는 꽃잎 너비(petal width)가 품종 분류에
   꽃잎 길이(petal length)보다 약간 더 중요한 특징임을 알 수 있습니다.

✨ 마무리

오늘은 비선형 문제를 해결하는 강력한 도구인
Decision Tree와 Random Forest를 배웠습니다.

다음 시간에는 정답 없이 데이터의 숨겨진 구조를 찾는
Unsupervised Learning의 대표 주자, K-Means 군집화(Clustering) 에 대해 알아보겠습니다.