ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 혼공머신 CH 05 트리 알고리즘
    Study/머신러닝 딥러닝 2025. 9. 25. 17:07

    목차

    1. 들어가며
    2. 결정 트리 (Decision Tree)
      • 데이터 준비 및 로지스틱 회귀
      • 결정 트리 모델과 과대적합
      • 결정 트리의 작동 방식 및 가지치기
      • 특성 중요도
    3. 교차 검증과 하이퍼파라미터 튜닝
      • 검증 세트와 교차 검증의 이해
      • 그리드 서치 (Grid Search)
      • 랜덤 서치 (Random Search)
    4. 트리의 앙상블 (Ensemble)
      • 랜덤 포레스트 (Random Forest)
      • 엑스트라 트리 (Extra Trees)
      • 그레이디언트 부스팅 (Gradient Boosting)
      • 히스토그램 기반 부스팅
      • XGBoost
      • LightGBM
    5. 끝내며

     


     

    1. 들어가며

    오늘은 머신러닝 모델 중 가장 직관적으로 이해하기 쉬운 '결정 트리' 모델부터 시작하여,

    모델의 성능을 극대화하기 위한 '교차 검증''하이퍼파라미터 튜닝' 방법을 알아보고자 합니다.

     

    또한 단일 트리의 한계를 넘어 여러 트리를 함께 사용하는 '앙상블 학습' 기법인 랜덤 포레스트, 그레이디언트 부스팅까지 차근차근 살펴보겠습니다. 와인 데이터를 활용하여 레드 와인과 화이트 와인을 분류하는 문제를 함께 해결해 보시죠.

     

     


     

    2. 결정 트리 (Decision Tree)

    2-1. 데이터 준비 및 로지스틱 회귀

    먼저 실습에 사용할 와인 데이터를 불러와 훈련 세트와 테스트 세트로 나눕니다.

    이 데이터는 알코올 도수, 당도, pH 값을 바탕으로 와인 종류(레드/화이트)를 예측하는 과제를 다룹니다.

    import pandas as pd
    
    wine = pd.read_csv('https://bit.ly/wine_csv_data')
    
    data = wine[['alcohol', 'sugar', 'pH']]
    target = wine['class']
    
    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)
    

     

    와인 데이터셋은 총 6,497개의 샘플로 구성되어 있으며,

    'alcohol', 'sugar', 'pH', 'class' 네 가지 특성 모두 누락된 값 없이 실수(float64) 형태임을 확인할 수 있습니다.

     describe() 메서드를 통해 각 특성의 간단한 통계치를 보면, 평균, 표준 편차, 최소/최대값 등이 특성마다 크게 다름을 알 수 있습니다.

     

     

    결정 트리를 살펴보기 전,

    기준 모델로서 로지스틱 회귀를 사용해 와인을 분류해 보겠습니다.

    데이터의 스케일을 표준화한 후 모델을 훈련시키고 성능을 평가합니다.

    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))
    # 출력:
    # 0.7808350971714451
    # 0.7776923076923077
    

     

    로지스틱 회귀 모델은 훈련 세트와 테스트 세트에서 약 78%의 정확도를 보입니다.

    로지스틱 회귀는 입력 특성(알코올, 당도, pH)에 가중치(계수)를 곱하여 결과를 예측하는데,

    이 계수들이 왜 이런 값을 가지는지 직관적으로 이해하기 어렵다는 단점이 있습니다. 이러한 특성 중요도에 대한 설명이 부족한 모델을 흔히 '블랙박스 모델'이라고 부릅니다.

     

    2-2. 결정 트리 모델과 과대적합

    결정 트리 모델은 별도의 전처리 없이 훈련 데이터에 바로 적용할 수 있습니다.

    from sklearn.tree import DecisionTreeClassifier
    
    dt = DecisionTreeClassifier(random_state=42)
    dt.fit(train_input, train_target)
    
    print(dt.score(train_input, train_target))
    print(dt.score(test_input, test_target))
    # 출력:
    # 0.996921300750433
    # 0.8592307692307692
    

    훈련 세트에서는 99.7%에 가까운 매우 높은 정확도를 보이지만,

    테스트 세트에서는 약 86%로 성능이 떨어집니다. 이는 모델이 훈련 세트에 과대적합(Overfitting)되었음을 의미합니다.

     

    2-3. 결정 트리의 작동 방식 및 가지치기

     

    결정 트리는 질문을 통해 데이터를 구분하고 정답을 찾아가는 플로우차트(Flowchart) 형태의 모델입니다.

    트리의 맨 위 노드를 루트 노드(Root Node), 맨 끝 노드를 리프 노드(Leaf Node)라고 부릅니다. 사이킷런의 DecisionTreeClassifier는 데이터를 두 개의 가지로 분할하는 이진 트리를 제공합니다. 예를 들어, '당도가 2보다 작은가요?'와 같은 질문을 통해 데이터를 좌우 노드로 분리하며, 이 과정을 반복하여 최종 예측을 수행합니다.

     

     

    노드를 분할하는 기준은 지니 불순도(Gini Impurity)를 사용하여 결정됩니다.

    지니 불순도는 1에서 음성 클래스 비율의 제곱과 양성 클래스 비율의 제곱을 뺀 값으로 계산됩니다. 지니 불순도가 0에 가까울수록 해당 노드는 순수(Pure)하다고 판단되며, 이는 특정 클래스의 샘플만 존재함을 의미합니다. 결정 트리는 부모 노드와 자식 노드의 지니 불순도 차이가 크게 나도록 노드를 분할하는 방식을 사용합니다. 트리는 기본적으로 리프 노드가 순수 노드가 될 때까지 계속 성장하므로, 깊이가 매우 깊어지고 과대적합이 발생하기 쉽습니다.

     

     

    과대적합을 해결하고 트리의 성장을 멈추게 하는 방법을 가지치기(Pruning)라고 합니다. max_depth와 같은 매개변수를 사용하여 트리의 최대 깊이를 제한할 수 있습니다.


     
    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))
    # 출력:
    # 0.8454877814123533
    # 0.8415384615384616
    

     

    트리의 깊이를 3으로 제한하자 훈련 세트의 성능은 낮아졌지만 테스트 세트의 성능은 이전보다 개선되어 과대적합이 완화되었습니다.

     

    결정 트리는 각 노드에서 하나의 특성만 사용하여 데이터를 분할하므로,

    특성 간의 스케일을 맞출 필요가 없어 특성 전처리(Feature Scaling)가 필요 없다는 장점이 있습니다. 또한, 결정 트리는 feature_importances_ 속성을 통해 각 특성의 중요도(Feature Importance)를 제공하여 어떤 특성이 모델의 예측에 더 큰 영향을 미 미쳤는지 쉽게 파악할 수 있습니다.

     

    2-4. 특성 중요도

    dt.fit(train_input, train_target)
    print(dt.feature_importances_)
    # 출력: [0.12345626 0.86862934 0.0079144 ]
    

     

    위 결과는 당도(sugar) 특성이 와인 분류에 가장 중요한 역할을 했음을 보여줍니다.

     


    3. 교차 검증과 하이퍼파라미터 튜닝

    3-1. 검증 세트와 교차 검증의 이해

    모델의 성능을 올리기 위해서는 다양한 하이퍼파라미터(Hyperparameter)를 조정해야 합니다. 하지만 테스트 세트를 사용하여 하이퍼파라미터를 튜닝하면, 결국 모델이 테스트 세트에 맞춰져 실전 성능이 떨어질 수 있습니다. 이를 방지하기 위해 훈련 세트에서 검증 세트(Validation Set)를 따로 분리하여 사용합니다.

    sub_input, val_input, sub_target, val_target = train_test_split(
        train_input, train_target, test_size=0.2, random_state=42)
    

     

    하지만 검증 세트를 분리하면 훈련 데이터의 양이 줄어든다는 단점이 있습니다.

    이를 보완하는 방법이 교차 검증(Cross-Validation)입니다. 교차 검증은 훈련 세트를 여러 개의 폴드(fold)로 나누어, 각 폴드를 한 번씩 검증 세트로 사용하고 나머지 폴드들을 훈련에 사용하는 방식입니다.

    from sklearn.model_selection import cross_validate
    import numpy as np
    
    dt = DecisionTreeClassifier(random_state=42)
    scores = cross_validate(dt, train_input, train_target)
    print(np.mean(scores['test_score']))
    # 출력: 0.855300214703487
    

    3-2. 그리드 서치 (Grid Search)

    최적의 하이퍼파라미터 조합을 찾기 위해 그리드 서치를 사용합니다. 

    GridSearchCV는 지정된 하이퍼파라미터 값들의 모든 조합에 대해 교차 검증을 수행하여 가장 좋은 성능을 내는 조합을 찾아줍니다.

    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)
    
    print(gs.best_params_)
    # 출력: {'min_impurity_decrease': 0.0001}
    

    3-3. 랜덤 서치 (Random Search)

    탐색할 하이퍼파라미터의 범위가 넓을 경우, 모든 조합을 시도하는 그리드 서치는 매우 오랜 시간이 걸릴 수 있습니다. 랜덤 서치는 지정된 범위 내에서 임의의 값을 선택하여 지정된 횟수만큼만 탐색을 수행하므로 훨씬 효율적입니다.

    from scipy.stats import uniform, randint
    from sklearn.model_selection import RandomizedSearchCV
    
    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),
              }
    
    rs = RandomizedSearchCV(DecisionTreeClassifier(random_state=42), params,
                            n_iter=100, n_jobs=-1, random_state=42)
    rs.fit(train_input, train_target)
    
    print(rs.best_params_)
    # 출력: {'max_depth': 39, 'min_impurity_decrease': 0.000341..., 'min_samples_leaf': 7, 'min_samples_split': 13}
    

    4. 트리의 앙상블 (Ensemble)

    앙상블 학습은 여러 개의 모델을 훈련시켜 그 예측을 종합함으로써 단일 모델보다 더 나은 성능을 내는 기법입니다.

    4-1. 랜덤 포레스트 (Random Forest)

    랜덤 포레스트는 대표적인 배깅(Bagging) 기반의 앙상블 모델입니다. 훈련 세트에서 중복을 허용하여 샘플을 뽑는 부트스트랩 샘플링을 통해 여러 개의 결정 트리를 훈련시키고, 각 트리의 예측을 투표(다수결)를 통해 종합합니다.

    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']))
    # 출력: 0.9973541965122431 0.8905151032797809
    

     

    랜덤 포레스트는 OOB(Out Of Bag) 점수를 제공하여 교차 검증을 수행하지 않고도 모델의 일반화 성능을 가늠할 수 있습니다.

    rf = RandomForestClassifier(oob_score=True, n_jobs=-1, random_state=42)
    rf.fit(train_input, train_target)
    print(rf.oob_score_)
    # 출력: 0.8934000384837406
    

    4-2. 엑스트라 트리 (Extra Trees)

    엑스트라 트리는 랜덤 포레스트와 유사하지만, 노드를 분할할 때 최적의 분할을 찾는 대신 무작위로 분할한다는 차이점이 있습니다. 이로 인해 더 많은 트리를 필요로 하지만 계산 속도가 빠르다는 장점이 있습니다.

    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']))
    # 출력: 0.9974503966084433 0.8887848893166506
    

     

    엑스트라 트리의 특성 중요도 역시 확인할 수 있습니다. 

    et.fit(train_input, train_target)
    print(et.feature_importances_)
    # 출력: [0.20183568 0.52242907 0.27573525]
    

     

    4-3. 그레이디언트 부스팅 (Gradient Boosting)

    그레이디언트 부스팅은 부스팅(Boosting) 기반의 앙상블 모델로, 깊이가 얕은 결정 트리를 순차적으로 훈련시키며 이전 트리의 오차를 보완해 나가는 방식으로 작동합니다.

     
    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']))
    # 출력: 0.8881086892152563 0.8720430147331015
    

     

    기본 매개변수 외에 n_estimators(트리 개수)와 learning_rate(학습률)를 조정하여 성능을 높일 수 있습니다.

    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']))
    # 출력: 0.9464595437171814 0.8780082549788999
    

     

    그레이디언트 부스팅의 특성 중요도도 확인해 봅니다.

    gb.fit(train_input, train_target)
    print(gb.feature_importances_)
    # 출력: [0.15887763 0.6799705  0.16115187]
    

     

    4-4. 히스토그램 기반 부스팅

     

    히스토그램 기반 그레이디언트 부스팅은 기존 그레이디언트 부스팅의 훈련 속도를 개선한 모델입니다.

    입력 특성을 256개의 구간으로 나누어 최적의 분할을 빠르게 찾습니다. 사이킷런 외에도 XGBoost, LightGBM과 같은 라이브러리가 널리 사용됩니다.

    from sklearn.ensemble import HistGradientBoostingClassifier
    
    hgb = HistGradientBoostingClassifier(random_state=42)
    scores = cross_validate(hgb, train_input, train_target,
                            return_train_score=True, n_jobs=-1)
    
    print(np.mean(scores['train_score']), np.mean(scores['test_score']))
    # 출력: 0.9321723946453317 0.8801241948619236
    

     

    퍼뮤테이션 중요도(Permutation Importance)를 통해 특성 중요도를 평가할 수 있습니다.

    from sklearn.inspection import permutation_importance
    
    hgb.fit(train_input, train_target)
    result = permutation_importance(hgb, train_input, train_target, n_repeats=10,
                                    random_state=42, n_jobs=-1)
    print(result.importances_mean)
    # 출력: [0.08876275 0.23438522 0.08027708]
    
    result = permutation_importance(hgb, test_input, test_target, n_repeats=10,
                                    random_state=42, n_jobs=-1)
    print(result.importances_mean)
    # 출력: [0.05969231 0.20238462 0.049     ]
    

     

    테스트 세트에서의 최종 점수는 다음과 같습니다.

    hgb.score(test_input, test_target)
    # 출력: 0.8723076923076923
    

     

    XGBoost

    XGBoost는 히스토그램 기반 그레이디언트 부스팅을 사용하는 대표적인 라이브러리 중 하나입니다.

    from xgboost import XGBClassifier
    
    xgb = XGBClassifier(tree_method='hist', random_state=42)
    xgb._estimator_type = "classifier" # 사이킷런 cross_validate 사용을 위해 필요
    scores = cross_validate(xgb, train_input, train_target,
                            return_train_score=True, n_jobs=-1)
    
    print(np.mean(scores['train_score']), np.mean(scores['test_score']))
    # 출력: 0.9567059184812372 0.8783915747390243
    

    LightGBM

    LightGBM 역시 히스토그램 기반 그레이디언트 부스팅을 구현한 인기 있는 라이브러리입니다.

    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']))
    # 출력: 0.935828414851749 0.8801251203079884
    

    끝내며

     

    지금까지 와인 분류 문제를 통해서,

    결정 트리부터 랜덤 포레스트, 그레이디언트 부스팅과 같은 다양한 트리 기반 앙상블 모델까지 알아보았습니다.

     

    단일 결정 트리의 과대적합 문제를 해결하기 위해 가지치기를 사용하고,

    검증 세트와 교차 검증을 통해 최적의 하이퍼파라미터를 찾는 과정을 살펴보았습니다.

     

    마지막으로, 여러 트리를 결합하여 더 강력한 성능을 내는 앙상블 기법들의 원리와 장점을 확인했습니다.

     

    이처럼 모델의 특성을 이해하고 적절한 튜닝과 기법을 적용하는 것이 머신러닝 모델의 성능을 끌어올리는 핵심이라 할 수 있습니다.

     

     


     

     

    코드 출처:

     

    https://github.com/rickiepark/hg-mldl2/blob/main/05-1.ipynb

     

    hg-mldl2/05-1.ipynb at main · rickiepark/hg-mldl2

    <혼자 공부하는 머신러닝+딥러닝(개정판)>(한빛미디어, 2025)의 코드 저장소입니다. Contribute to rickiepark/hg-mldl2 development by creating an account on GitHub.

    github.com

     

     

    https://github.com/rickiepark/hg-mldl2/blob/main/05-2.ipynb

     

    hg-mldl2/05-2.ipynb at main · rickiepark/hg-mldl2

    <혼자 공부하는 머신러닝+딥러닝(개정판)>(한빛미디어, 2025)의 코드 저장소입니다. Contribute to rickiepark/hg-mldl2 development by creating an account on GitHub.

    github.com

     

     

    https://github.com/rickiepark/hg-mldl2/blob/main/05-3.ipynb

     

    hg-mldl2/05-3.ipynb at main · rickiepark/hg-mldl2

    <혼자 공부하는 머신러닝+딥러닝(개정판)>(한빛미디어, 2025)의 코드 저장소입니다. Contribute to rickiepark/hg-mldl2 development by creating an account on GitHub.

    github.com

     

    내용 출처:

     

    https://www.youtube.com/watch?v=XLBUmmFZ58c&list=PLVsNizTWUw7E2RxZ4aspcR9vNamXccmFE&index=12

     

    https://www.youtube.com/watch?v=LThlLTlvIzg&list=PLVsNizTWUw7E2RxZ4aspcR9vNamXccmFE&index=13

     

    https://www.youtube.com/watch?v=68AxaeEgfa4&list=PLVsNizTWUw7E2RxZ4aspcR9vNamXccmFE&index=14

     

     

Designed by Tistory.