공부일기
ML/ 트리 알고리즘, 교차 검증, 하이퍼파라미터 튜닝, 랜덤 서치 본문
결정 트리는 말 그대로 트리 알고리즘을 사용해서 정답을 찾아 학습하는 알고리즘이다. 화이트 와인과 레드와인을 구별하는 분류 문제를 해결해보겠다.
결정 트리
사이킷런의 DecisionTreeClassifier 클래스를 사용한다.
결정 트리에서는 특성값의 스케일이 아무런 영향을 미치지 않는다. 따라서 표준화 전처리를 할 필요가 없다!!!!!
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
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)
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.8584615384615385
test_size=0.2로 설정한 것은 20%만 테스트세트로 나누겠다는 뜻.
현재 모델은 과대적합인 모델이다. 매우. 이 트리를 그려보겠다.
사이킷런에서는 plot_tree() 메서드를 제공한다. 맷플로립의 figure() 함수는 사이즈를 조절해준다.
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
plt.figure(figsize=(10,7))
plot_tree(dt)
plt.show()

이런 미친!!! 이런 쌉!!! 엄청난 트리가 생겨버렸다.
결정 트리를 가지치기 하지 않는다면 훈련 세트에는 아주 잘 맞겠지만 테스트 세트에서 점수는 그에 못미칠 것이다. 이를 두고 일반화가 잘 안될 것 같다고 하는데, 가지치기를 할 수 있는 가장 간단한 방법은 트리의 최대 깊이를 제한하는 것이다.
그 전에, 결정트리의 불순도에 대해서 정의하고 넘어가겠다.
plt.figure(figsize=(10,7))
plot_tree(dt, max_depth=1, filled=True, feature_names=['alcohol','sugar','pH'])
plt.show()

최대 깊이를 1로 설정하고 그림을 그려봤다. filled 속성을 True로 주면 알아서 색을 칠해준다. 타겟 클래스가 한쪽으로 치우쳐지면 색깔이 진해진다. 결정 트리에서 예측하는 방법은 리프 노드(맨 바닥 노드)에서 가장 많은 클래스가 예측 클래스가 된다. 현재 상태에서 트리를 멈춘다면 왼쪽, 오른쪽 노드 둘 다 양성 클래스(화이트 와인)로 예측된다. 또한 노드를 보면 gini라는 값이 등장한다. gini가 바로 이제부터 설명할 불순도이다.
불순도
위에 있는 그림의 트리에서 루트 노드는 어떻게 sugar가 4.325보다 작은 기준으로 데이터를 분할했을까?
바로 criterion 매개변수에 지정한 지니 불순도 때문이다.
지니 불순도는 각각의 타겟 클래스의 비율을 제곱해서 더한 뒤 1에서 빼면 된다. 루트 노드의 지니값을 계산 해보면,
1 - ((1258/5197)^2 + (3939/5197)^2) = 0.367
이 나오게 된다. 만약 양쪽의 클래스가 같은 비율로 있다고 하면,
1 - ((1/2)^2 + (1/2)^2) = 0.5
가 되는데, 데이터가 매우 고르게 퍼져있다는 뜻으로 불순도가 최악이라는 뜻!
만약 노드에 하나의 클래스만 남게 되었다면(깨끗), 1-1=0이 되어서 불순도가 가장 작게 된다. 이러한 노드를 순수 노드라고 한다.
결정 트리 모델은 부모 노드와 자식 노드의 불순도 차이가 가능한 크도록 트리를 성장시킨다. 이걸 정보 이득이라고 하는데,
불순도 차이가 많이 난다? -> 이전 단계보다 훨씬 더 잘 분류했다!! 그러면 정보 이득은 어떻게 계산할까?
부모의 불순도 -
((왼쪽 자식 샘플 수/부모 샘플 수) X 왼쪽 자식 불순도 +
(오른쪽 자식 샘플 수/부모 샘플 수) X 오른쪽 자식 불순도)
로 구할 수 있다.
현재 모델에 적용시키면,
0.367 - (2922/5197)*0.481 - (2275/5197)*0.069 = 0.066
이 된다.
결정 트리 알고리즘은 정보 이득이 최대가 되도록 데이터를 나눈다! -> 불순도가 낮아야 좋음.
그럼 이제 본격적으로 가지치기를 해보자!
가지치기
앞서 말했듯이 최대 깊이를 3으로 제한시켜서 성장시켜보겠다.
from sklearn.tree import DecisionTreeClassifier
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
과대적합이 해결된 것 같지만, 여전히 테스트 세트의 점수가 너무 낮다. 그래프를 그려본다.
plt.figure(figsize=(20,15))
plot_tree(dt,filled=True, feature_names=['alcohol','sugar','pH'])
plt.show()

보면 왼쪽에서 세번째 리프노드만 주황색이다. 이 노드에 도착해야지만 레드와인(음성 클래스)으로 예측하는 것이다.
그럼 이제 모델의 교차 검증에 대해서 알아보자.
교차 검증
교차 검증은 훈련 데이터를 훈련 데이터와 검증 데이터로 나눠서 나눠진 훈련 데이터로 모델을 훈련 시키고 검증 데이터로 모델의 성능을 평가한다. 그 다음 이때의 매개변수를 활용해 다시 나눠진 훈련 데이터와 검증 데이터를 합쳐서 전체 훈련데이터로 모델을 다시 훈련하고 마지막으로 테스트 세트에서 최종 점수를 평가한다. 조금이라도 더 일반화 시키기 위해 필수이다.
5-폴드 교차 검증은 전체 훈련 데이터를 5분할 해서 검증은 5번 실행하는거다. 그리고 각 폴드에서 계산한 검증 점수를 평균한다.
교차 검증은 사이킷런에 있는 cross_validate() 함수를 사용하면 된다.
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
{'fit_time': array([0.03779936, 0.01859117, 0.01465774, 0.01798987, 0.0150249 ]),
'score_time': array([0.01201916, 0.0051825 , 0.00416923, 0.00415897, 0.00396371]),
'test_score': array([0.84230769, 0.83365385, 0.84504331, 0.8373436 , 0.8479307 ])}
이 함수의 반환값에 있는 처음 두개의 키는 모델을 훈련하는 시간과 검증하는 시간이다.
교차 검증의 최종 점수는 test_score 키에 담긴 점수들의 평균이다.
import numpy as np
print(np.mean(scores['test_score']))
0.8412558303102096
cross_validate() 메서드에서 훈련 세트를 섞으려면 cv 매개변수에 회귀모델일 경우 KFold(), 분류모델일 경우 StratifiedKFold를 사용한다.
from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, train_input, train_target, cv=StratifiedKFold())
print(np.mean(scores['test_score']))
0.8412558303102096
우리는 이미 0.2의 사이즈로 섞은다음에 했으므로 원래와 똑같이 나오는게 정상이다.
만약 5-폴드가 아니라 10-폴드를 하고싶으면 StratifiedKFold() 선언 시 n_splits 매개변수를 설정해주면 된다.
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv=splitter)
그럼 이제 결정 트리의 매개변수를 바꿔가면서 가장 성능이 잘 나오는 모델을 찾아보자!
하이퍼파라미터 튜닝
하이퍼파라미터 튜닝은 모델을 최적화하기 위해서 하이퍼파라미터를 조절하는 과정이다. 우리는 사이킷런에서 제공하는 그리드 서치를 사용해보겠다.
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))
0.9615162593804117
GridSearchCV 클래스는 하이퍼파라미터 탐색과 교차 검증을 한 번에 수행한다. params에 노드를 분할했을때 해당 값보다 불순도 감소가 크지 않으면 분할을 금지시키는 매개변수 min_impurity_decrease 값들을 저장해놓고 GridSearchCV 매개변수에 결정트리와 함께 전달한다. n_jobs=-1 은 병렬 실행에 있어서 시스템에 있는 모든 코어를 사용하라는 뜻이다.
그리드 서치의 cv(cross_validate 즉, 교차 검증 횟수) 매개변수의 기본값은 5로 기본적으로 5-폴드 교차 검증을 수행한다.
따라서 위의 코드는 min_inpurity_decrease 값 5개, 각각 5-폴드 교차 해서 총 25개의 모델을 훈련한다!!
그리드 서치 모델을 훈련하면 25개의 모델 중에서 검증 점수가 가장 높은 모델의 매개변수 조합으로 자동으로 모델을 훈련해서 best_estimator_ 속성에 저장한다!!! 저장한 최고의 모델의 점수를 한 결과다.
min_impurity_decrease의 각 매개변수에서 수행한 교차 검증들의 평균 점수는 cv_results_ 속성의 'mean_test_score' 키에 저장되어있다. 당연하게도 제일 큰 값의 인덱스 위치에 있는 min_impurity_decrease의 값이 최고의 하이퍼파라미터 값이 된다.
이제 조금 더 다양한 값들을 조정해보겠다.
랜덤 서치
하이퍼파라미터 튜닝을 하다보면 매개변수의 값의 범위나 간격을 정하기가 어렵다. 랜덤하게 값을 전달해준다면 조금이라도 더 일반화가 잘될 듯 싶다. RandomizedSearchCV를 사용해보겠다.
싸이파이의 uniform, randint 클래스를 사용하면 쉽게 랜덤한 값을 전달할 수 있다.
from scipy.stats import uniform, randint
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)}
uniform은 (시작점, 범위) 의 랜덤한 실수값을 전달해준다. randint는 (시작점, 끝점-1)의 랜덤한 정수 값을 전달한다.
min_samples_split은 노드를 분할하기 위한 최소의 샘플 수 이다.
min_samples_leaf는 리프노드에 존재할 최소의 샘플 수 이다.
이 params를 이제 RandomizedSearchCV에 결정트리와 함께 전달해주자. 이 params를 몇 개?? 샘플링?? 할 지는 n_iter 매개변수에 저장한다. 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_)
{'max_depth': 39, 'min_impurity_decrease': np.float64(0.00034102546602601173), 'min_samples_leaf': 7, 'min_samples_split': 13}
이런식으로 최적의 조합을 찾아준다. 그렇다면 최고의 교차 검증 점수는??
print(np.max(gs.cv_results_['mean_test_score']))
0.8695428296438884
조금씩 높아지는 것을 알 수 있죠
훈련세트와 테스트 세트 출력!~
dt = gs.best_estimator_
print(dt.score(train_input, train_target))
print(dt.score(test_input, test_target))
0.8928227823744468
0.86
다양한 랜더마이즈도 서치를 사용한 매개변수로 하이퍼파라미터 튜닝까지 진행해봤다.
'ML' 카테고리의 다른 글
| ML/ 앙상블 학습, 랜덤 포레스트, 엑스트라 트리, 그라디언트 부스팅 (1) | 2026.01.20 |
|---|---|
| ML/ 확률적 경사 하강법을 활용한 분류 (0) | 2026.01.15 |
| ML/ 로지스틱 회귀, 이진 분류 vs 다중 분류 (1) | 2026.01.15 |
| ML/ 선형 회귀, 다항 회귀, 다중 회귀, 릿지, 라쏘 (0) | 2026.01.14 |
| ML/ k-최근접 이웃 회귀로 무게 예측하기 (0) | 2026.01.14 |
