Chapter05. 트리 알고리즘 (화이트 와인을 찾아라!)¶
- 학습목표
- 성능이 좋고 이해하기 쉬운 트리 알고리즘에 대해 배운다.
- 알고리즘의 성능을 최대화하기 위한 하이퍼파라미터 튜닝을 실습한다.
- 여러 트리를 합쳐 일반화 성능을 높일 수 있는 앙상블 모델을 배운다.
05-1 결정트리¶
핵심키워드
- 결정 트리
- 불순도
- 정보 이득
- 가지치기
- 특성 중요도
결정 트리 알고리즘을 사용해 새로운 분류 문제를 다루어 봅니다. 결정 트리가 머신러닝 문제를 어떻게 해결하는지 이해한다.
문제 (화이트 와인을 찾아라!)¶
- 캔에 인쇄된 알코올 도수,당도,PH 값으로 와인 종류를 구별할 수 있는 방법이 있을까?
- 알코올 도수, 당도, PH 값에 로지스틱 회귀 모델을 적용할 계획을 세운다.
로지스틱 회귀로 와인 분류하기¶
6,497개의 와인 샘플 데이터를 받았다. 이 데이터셋을 불러와 보자. 4장에서 처럼 판다스를 사용해 인터넷에서 직접 불러오자.
- https://bit.ly/wine_csv_data (와인 데이터셋의 출처는 캐글의 Red wine Quality)
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
와인 데이터셋을 판다스 데이터프레임으로 제대로 읽어 들였는지 head() 메서드로 처음 5개의 샘플을 확인해 보자.
wine.head()
alcohol | sugar | pH | class | |
---|---|---|---|---|
0 | 9.4 | 1.9 | 3.51 | 0.0 |
1 | 9.8 | 2.6 | 3.20 | 0.0 |
2 | 9.8 | 2.3 | 3.26 | 0.0 |
3 | 9.8 | 1.9 | 3.16 | 0.0 |
4 | 9.4 | 1.9 | 3.51 | 0.0 |
처음 3개의 열 (alcohol, sugar, PH)은 각각 올코올 도수, 당도, PH 값을나타낸다. 네 번째 열(class)은 타깃값으로 0이면 레드 와인, 1이면 화이트 화인이라고 한다.레드 와인과 화이트 와인을 구분하는 이진 분류 문제이고, 화이트 와인이 양성 클래스이다. 즉 전체 화인 데이터에서 화이트 와인을 골라내는 문제이다.
로지스틱 회귀 모델을 바로 훈련하기 전에 판다스 데이터프레임의 유용한 메서드 2개를 먼저 알아보자.
- 먼저 info() 메서드이다. 이 메서드는 데이터프레임의 각 열의 데이터 타입과 누락된 데이터가 있는지 확인하는데 유용하다.
wine.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 6497 entries, 0 to 6496 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 alcohol 6497 non-null float64 1 sugar 6497 non-null float64 2 pH 6497 non-null float64 3 class 6497 non-null float64 dtypes: float64(4) memory usage: 203.2 KB
출력 결과를 보면 총 6,497개의 샘플이 있고, 4개의 열은 모두 실숫값이다. Non-Null Count가 모두 6497이므로 누락된 값은 없는 것 같다.
누락된 값이 있으면 어떻게 하나요?
- 누락된 값이 있다면 그 데이터를 버리거나 평균값으로 채운 후 사용할 수 있다. 어떤 방식이 최선인지는 미리 알기 어렵다. 두 가지 모두 시도해 보자. 여기에서도 항상 훈련 세트의 통계값으로 테스트 세트를 변환한다는 것을 잊지 말자. 즉 훈련 세트의 평균값으로 테스트 세트의 누락된 값을 채워야 한다.
다음에 알아볼 메서드는 describe()이다. 이 메서드는 열에 대한 간략한 통계를 출력해 준다. 최소, 최대, 평균값 등을 볼 수 있다. 이 메서드를 호출해 보겠다.
wine.describe()
alcohol | sugar | pH | class | |
---|---|---|---|---|
count | 6497.000000 | 6497.000000 | 6497.000000 | 6497.000000 |
mean | 10.491801 | 5.443235 | 3.218501 | 0.753886 |
std | 1.192712 | 4.757804 | 0.160787 | 0.430779 |
min | 8.000000 | 0.600000 | 2.720000 | 0.000000 |
25% | 9.500000 | 1.800000 | 3.110000 | 1.000000 |
50% | 10.300000 | 3.000000 | 3.210000 | 1.000000 |
75% | 11.300000 | 8.100000 | 3.320000 | 1.000000 |
max | 14.900000 | 65.800000 | 4.010000 | 1.000000 |
- 사분위수는 데이터를 순서대로 4등분 한 값이다. 예를 들어 2사분위수(중간값)는 데이터를 일렬로 늘어놓았을 때 정중앙의 값이다. 만약 데이터 개수가 짝수개라 중앙값을 선택할 수 없다면 가운데 2개 값의 평균을 사용한다.
여기서 알 수 있는 것은 알코올 도수와 당도, PH 값의 스케일이 다르다는 것이다. 이전에 했던 것처럼 사이킷런의 StandardScaler 클래스를 사용해 특성을 표준화해야겠다. 그 전에 먼저 판다스 데이터프레임을 넘파이 배열로 바꾸고 훈련 세트와 테스트 세트로 나누자.
data = wine[['alcohol', 'sugar', 'pH' ]].to_numpy()
target = wine['class'].to_numpy()
wine 데이터프레임에서 처음 3개의 열을 넘파이 배열로 바꿔서 data 배열에 저장하고 마지막 class열을 넘파이 배열로 바꿔서 target 배열에 저장했다. 이제 훈련 세트와 테스트 세트로 나누어 보자.
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)
- 실습과 결괏값이 같도록 random_state=42 로 설정 했다.
train_test_split() 함수는 설정값을 지정하지 않으면 25%를 테스트 세트로 지정한다. 샘플 개수가 충분히 많으므로 20% 정도만 테스트 세트로 나눴습니다. 코드의 test_size=0.2가 이런 의미이다. 만들어진 훈련 세트와 테스트 세트의 크기를 확인해 보자.
print(train_input.shape, test_input.shape)
(5197, 3) (1300, 3)
훈련세트는 5,197개이고 테스트 세트는 1,300개이다. 좋다. 이제 StandardScaler 클래스를 사용해 훈련 세트를 전처리해 보자. 그다음 같은 객체를 그대로 사용해 테스트 세트를 변환하겠다.
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
모든 준비가 끝났다. 이제 표준점수로 변환된 train_scaled 와 test_scaled를 사용해 로지스틱 회귀 모델을 훈련하자.
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
음, 점수가 높지않다. 생각보다 화이트 와인을 골라내는게 어렵나 보다. 훈련 세트와 테스트 세트의 점수가 모두 낮으니 모델이 다소 과소적합된것 같다. 이 문제를 해결하기 위해 규제 매개변수 C의 값을 바꿔 볼까? 아니면 solver 매개변수에 다른 알고리즘을 선택할 수도 있다. 또는 다항 특성을 만들어 추가 할 수도 있다.
설명하기 쉬운 모델과 어려운 모델
제출한 보고서를 만들려고한다. 이 모델을 설명하기 위해 로지스틱 회귀가 학습한 계수와 절편을 출력해보자.
print(lr.coef_, lr.intercept_)
[[ 0.51270274 1.6733911 -0.68767781]] [1.81777902]
결정 트리
결정 트리 Decision Tree 모델이 "이유를 설명하기 쉽다"라고 알려주었다. 생각해 보니 언뜻 책에서 본 것도 같다. 결정 트리 모델은 스무고개와 같다. 질문을 하나씩 던져서 정답과 맞춰가는 것이다.
데이터를 잘 나눌 수 있는 질문을 찾는다면 계속 질문을 추가해서 분류 정확도를 높일 수 있다. 이미 예상했겠지만 사이킷런이 결정 트리 알고리즘을 제공한다. 사이킷런의 DecisionTreeClassfier 클래스를 사용해 결정 트리 모델을 훈련해 보자. 새로운 클래스이지만 사용법은 이전과 동일하다. fit() 메서드를 호출해서 모델을 훈련한 다음 score() 메서드로 정확도르 평가해 보자.
- 결정 트리 모델을 만들 때 왜 random_state 를 지정 하나요?
- 사이킷런의 결정 트리 알고리즘은 노드에서 최적의 분할으 찾기 전에 특성의 순서를 섞는다. 따라서 약간의 무작위성이 주입되는데 실행할 때마다 점수가 조금씩 달라질 수 있기 때문이다. 여기에서는 독자들이 실습한 결과와 책의 내용이 같도록 유지하기 위해 random_state를 지정하지만, 실전에서는 필요하지 않다.
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target)) # 훈련 세트
print(dt.score(test_scaled, test_target)) # 테스트 훈련
0.996921300750433 0.8592307692307692
와우! 훈련 세트에 대한 점수가 엄청 높다. 거의 모두 맞춘 것 같다. 테스트 세트의 성능은 그에 비해 조금 낮다. 과대적합된 모델이라고 볼 수 있겠다. 그런데 이 모델을 그림으로 어떻게 표현할 수 있을까? 친절하게도 사이킷런은 plot_tree() 함수를 사용해 결정 트리를 이해하기 쉬운 트리 그림으로 출력해 준다. 위에서 만든 결정 트리 모델 객체를 plot_tree() 함수에 전달해서 어떤 트리가 만들어졌는지 그려보자.
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
plt.figure(figsize=(10,7))
plot_tree(dt)
plt.show()
엄청난 트리가 만들어졌다. 수양버들 나뭇잎처럼 늘어졌다. 진짜 나무는 밑에서부터 하늘 위로 자라지만, 결정 트리는 위에서부터 아래로 거꾸로 자라난다. 맨 위의 노드node를 루트 노드 root node라 부르고 맨 아래 끝에 달린 노드를 리프 노드 leaf node 라고 한다.
- 노드가 무엇인가?
- 노드는 결정 트리를 구성하는 핵심 요소이다. 노드는 훈련 데이터의 특성에 대한 테스트를 표현한다. 예를 들어 현재 샘플의 당도가 -0.239보다 작거나 같은지 테스트 한다. 가지(branch)는 테스트의 결과(True, False)를 나타내며 일반적으로 하나의 노드는 2개의 가지를 가진다.
너무 복잡하니 plot_tree() 함수에서 트리의 깊이를 제한해서 출력해 보자. max_depth 매개변수를 1로 주면 루트 노드를 제외하고 하나의 노드를 더 확장하여 그린다. 또 filled 매개변수에서 클래스에 맞게 노드의 색을 칠할 수 있다. feature_names 매개변수에는 특성의 이름을 전달 할 수 있다. 이렇게 하면 노드가 어떤 특성으로 나뉘는지 좀더 잘 이해할 수 있다. 한번 이렇게 그려보자.
plt.figure(figsize=(10,7))
plot_tree(dt, max_depth=1, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
오, 훨씬 낫다! 이 그림을 읽는 방법을 알아보겠다. 기본적으로 그림이 담고 있는 정보는 다음과 같다. 루트 노드는 당도(sugar)가 -0.239 이하인지 질문을 한다. 만약 어떤 샘플의 당도가 -.0239와 같거나 작으면 왼쪽 가지로 간다. 그렇지 않으면 오른쪽 가지로 이동한다. 즉 왼쪽이 Yes, 오른쪽이 No이다. 루트 노드의 총 샘플 수 (samples)는 5,197개 이다. 이 중에서 음성 클래스(레드와인)는 1,258개이고, 양성 클래스(화이트 와인)는 3,939개 이다. 이 값이 value에 나타나 있다.
이어서 왼쪽 노드를 살펴보겠다. 이 노드는 당도가 더 낮은지를 물어본다. 당도가 -0.802와 같거나 낮다면 다시 왼쪽 가지로, 그렇지 않으면 오른쪽 가지로 이동한다. 이 노드에서 음성 클래스와 양성 클래스의 샘플 개수는 각각 1,177개와 1,745개 이다. 루트 노드보다 양성 클래스, 즉 화이트 와인의 비율이 크게 줄었다. 그 이유는 오른쪽 노드를 보면 알 수 있다.
오른쪽 노드는 음성 클래스가 81개, 양성 클래스가 2,194개로 대부분의 화이트 와인 샘플이 이 노드로 이동했다. 노드의 바탕 색깔을 유심히 보자. 루트 노드보다 이 노드가 더 진하고, 왼쪽 노드는 더 연해지지 않았나? plot_tree() 함수에서 filled=True로 지정하면 클래스 마다 색깔을 부여하고, 어떤 클래스의 비율이 높아지면 점점 진한 색으로 표시한다. 아주 직관적이다.
결정 트리에서 예측하는 방법은 간단하다. 리프 노드에서 가장 많은 클래스가 예측 클래스가 된다. 앞에서 보았던 k-최근접 이웃과 매우 비슷해보인다. 만약 이 결정 트리의 성장을 여기서 멈춘다면 왼쪽 노드에 도달한 샘플과 오른쪽 노드에 도달한 샘플은 모두 양성 클래스로 예측된다. 두 노드 모두 양성 클래스의 개수가 많기 때문이다.
- 만약 결정 트리를 회귀 문제에 적용하려면 리프 노드에 도달한 샘플의 타깃을 평균하여 예측값으로 사용한다. 사이킷런의 결정 트리 회귀 모델은 DecisionTreeRegressor 이다.
그런데 노드 상자 안에 gini라는 것이 있다. 이것이 무엇인지 좀 더 자세히 알아보자.
불순도
gini는 지니 불순도 Gini impurity를 의미한다. DecisionTreeClassifier 클래스의 criterion 매개변수의 기본값이 'gini'이다. criterion 매개변수의 용도는 노드에서 데이터를 분할할 기준을 정하는 것이다. 앞의 그린 트리에서 루트 노드는 어떻게 당도 -0.239를 기준으로 왼쪽과 오른쪽 노드로 나우었을까? 바로 criterion 매개변수에 지정한 지니 불순도를 사용한다. 그럼 지니 불순도를 어떻게 계산하는지 알아보자.
지니 불순도는 클래스의 비율을 제곱해서 더한 다음 1에서 빼면 된다.
- 지니 불순도 = 1 - (음성 클래스 비율²+ 양성 클래스 비율²)
이게 끝이다. 다중 클래스 문제라면 클래스가 더 많겠지만 계산하는 방법은 동일하다. 그럼 이전 트리 그림에 있던 루트 노드의 지니 불순도를 계산해 보자. 루트 노드는 총 5,197개의 샘플이 있고 그 중에 1,258개가 음성 클래스, 3,939개가 양성 클래스이다. 따라서 다음과 같이 지니 불순도를 계산할 수 있다.
- 1 - ((1258 / 5197)²+ (3939/5197)²) = 0.367
왼쪽과 오른쪽 노드의 지니 불순도도 한번 계산해 보자. 만약 100개의 샘플이 있는 어떤 노드의 두 클래스의 비율이 정확히 1/2씩 이라면 지니 불순도는 0.5가 되어 최악이 된다.
- 1 - ((50/100)²+(50/100)²) = 0.5
노드에 하나의 클래스만 있다면 지니 불순도는 0이 되어 가장 작다. 이런 노드를 순수 노드라고도 부른다.
- 1 - ((0/100)²+(100/100)²) = 0
결정 트리 모델은 부모 노드parent node와 자식 노드 child node 의 불순도 차이가 가능한 크도록 트리를 성장시킨다. 부모 노드와 자식 노드의 불순도 차이를 계산하는 방법을 알아보자. 먼저 자식 노드의 분순도를 샘플 개수에 비례하여 모두 더한다. 그다음 부모 노드의 불순도에서 빼면 된다.
예를 들어 앞의 트리 그림에서 루트 노드를 부모 노드라 하면 왼쪽 노드와 오른쪽 노드가 자식 노드가 된다. 왼쪽 노드로 2,922개의 샘플이 이동했고, 오른쪽 노드로는 2,275개의 샘플이 이동했다. 그럼 불순도의 차이는 다음과 같이 계산한다.
- 부모의 불순도 - (왼쪽 노드 샘플 수 / 부모의 샘플 수 ) x 왼쪽 노드 불순도 - (오른쪽 노드 샘플 수 / 부모의 샘플 수) x 오른쪽 노드 불순도 = 0.367 - (2922/5197) x 0.481 - (2275/5197) x 0.069 = 0.066
이런 부모와 자식 노드 사이의 불순도 차이를 정보 이득 information gain 이라고 부른다. 좋다. 이제 결정 트리의 노드를 어떻게 나누는지 이해했다. 이 알고리즘은 정보 이득이 최대가 되도록 데이터를 나눈다. 이때 지니 불순도를 기준으로 사용한다. 그런데 사이킷런에는 또 다른 불순도 기준이 있다.
DecisionTreeClassifier 클래스에서 criterion='entropy'를 지정하여 엔트로피 불순도를 사용할 수 있다. 엔트로피 불순도도 노드의 클래스 비율을 사용하지만 지니 불순도처럼 제곱이 아니라 밑이 2인 로그를 사용하여 곱한다. 예를 들어 루트 노드의 엔트로피 불순도는 다음과 같이 계산할 수 있다.
- -음성 클래스 비율 x log₂(음성 클래스 비율) - 양성 클래스 비율 x log₂(양성 클래스 비율) = -(1258/5197) x log₂(3939/5197) = 0.798
보통 기본값인 지니 불순도와 엔트로피 분순도가 만든 결과의 차이는 크지 않다. 여기서는 기본 값인 지니 불순도를 계속 사용하겠다.
이제 결정 트리 알고리즘을 확실히 이해했다. 불순도 기준을 사용해 정보 이득이 최대가 되도록 노드를 분할한다. 노드를 순수하게 나눌수록 정보 이득이 커진다. 새로운 샘플에 대해 예측할 때 에는 노드의 질문에 따라 트리를 이동한다. 그리고 마지막에 도달한 노드의 클래스 비율을 보고 예측을 만든다.
그런데 앞의 트리는 제한 없이 자라났기 때문에 훈련 세트보다 테스트 세트에서 점수가 크게 낮았다. 이 문제를 다루어 보자.
가지치기
열매를 잘 맺기 위해 과수원에서 가지치기를 하는 것처럼 결정 트리도 가지치기를 해야한다. 그렇지 않으면 무작정 끝까지 자라나는 트리가 만들어진다. 훈련 세트에는 아주 잘맞겠지만 테스트 세트에서 점수는 그에 못 미칠 것이다. 이를 두고 일반화가 잘 안 될것 같다고 말한다. 그럼 가지치기를 해보자. 결정 트리에서 가지치기를 하는 가장 간단한 방법은 자라날 수 있는 트리의 최대 깊이를 지정하는 것이다. DecisionTreeClassifier 클래스의 max_depth 매개변수를 3으로 지정하여 모델을 만들어 보겠다. 이렇게 하면 루트 노드 아래로 최대 3개의 노드까지만 성장 할 수 있다.
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))
0.8454877814123533 0.8415384615384616
훈련 세트의 성능은 낮아졌지만 테스트 세트의 성능은 거의 그대로이다. 이런 모델을 트리 그래프로 그린다면 훨씬 이해하기 쉬울 것 같다. plot_tree() 함수로 그려보자.
plt.figure(figsize=(20,15))
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
훨씬 보기 좋다. 그래프를 따라가면서 샘플이 어떻게 나뉘는지 확인할 수 있다. 루트 노드 다음에 있는 깊이 1의 노드는 모두 당도(sugar)를 기준으로 훈련 세트를 나눈다. 하지만 깊이 2의 노드는 맨 왼쪽의 노드만 당도를 기준으로 나누고 왼쪽에서 두 번째 노드는 알코올 도수(alcohol)를 기준으로 나눈다. 오른쪽의 두 노드는 PH를 사용한다.
깊이 3에 있는 노드가 최종 노드인 리프 노드이다. 왼쪽에서 세 번째에 있는 노드만 음성 클래스가 더 많다. 이 노드에 도착해야만 레드 와인으로 예측한다. 그럼 루트 노드부터 이 노드까지 도달하려면 당도는 -0.239보다 작고 또 -0.802보다 커야 한다. 그리고 알코올 도수는 0.454보다 작아야 한다. 그럼 세 번째 리프 노드에 도달한다. 즉 당도가 -0.802보다 크고 -0.239보다 작은 와인 중에 알코올 도수가 0.454와 같거나 작은 것이 레드 와인이다.
- 실습한 내용은 트리의 깊이가 비교적 얼마 되지 않아서 해석이 쉽다. 하지만 실전에서 결정 트리를 사용할 때는 많은 특성을 사용하고 트리의 깊이도 깊어진다. 이때는 생각만큼 해석이 쉽지 않을 수 있다.
그런데 -0.802라는 음수로 된 당도를 이사님께 어떻게 설명해야 할까? 잠깐, 뭔가 이상하다. 앞서 불순도를 기준으로 샘플을 나눈다고 했다. 분순도는 클래스별 비율을 가지고 계산했다. 샘플을 어떤 클래스 비율로 나누는지 계산할 때 특성값의 스케일이 계산에 영향을 미칠까? 아니요. 특성값의 스케일은 결정 트리 알고리즘에 아무런 영향을 미치지 않는다. 따라서 표준화 전처리를 할 필요가 없다. 이것이 결정 트리 알고리즘의 또 다른 장점 중 하나이다.
그럼 앞서 전처리하기 전의 훈련 세트(train_input)와 테스트 세트(test_input)로 결정 트리 모델을 다시 훈련해 보자.
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()
결과를 보면 같은 트리지만, 특성값을 표준점수로 바꾸지 않은 터라 이해하기가 훨씬 쉽다. 당도가 1.625보다 크고 4.325보다 작은 와인 중에 알코올 도수가 11.025와 같거나 작은 것이 레드와인이다. 그 외에는 모두 화이트 와인으로 예측했다.
마지막으로 결정 트리는 어떤 특성이 가장 유용한지 나타내는 특성 중요도를 계산해 준다. 이 트리의 루트 노드와 깊이 1에서 당도를 사용했기 때문에 아마도 당도(sugar)가 가장 유용한 특성 중 하나일 것 같다. 특성 중요도는 결정 트리 모델의 feature_importtances_속성에 저장되어 있다. 이값을 출력해 확인해 보자.
print(dt.feature_importances_)
[0.12345626 0.86862934 0.0079144 ]
네, 역시 두 번째 특성인 당도가 0.87 정도로 특성 중요다가 가장 높다. 그 다음 알코올 도수, PH 순이다. 이 값을 모두 더하면 1이 된다. 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산한다. 특성 중요도를 활용하면 결정 트리 모델을 특성 선택에 활용할 수 있다. 이것이 결정 트리 알고리즘의 또 다른 장점 중 하나이다.
좋다. 이 모델은 비록 테스트 세트의 성능이 아주 높지 않아 많은 화이트 와인을 완벽하게 골라내지는 못하지만, 이사님에게 보고하기에는 아주 좋은 모델이다. 조금 부정확한 면이 걱정되면 와인을 위한 럭키백을 기획해 보자.
이해하기 쉬운 결정 트리 모델 - 문제해결 과정¶
알코올 도수, 당도, PH 데이터를 기준으로 화이트 와인을 골라내는 이진 분류 로지스틱 회귀 모델을 훈련했다. 하지만 보고서를 작성할때 도통 이해할수 없다고 했다.
그다음 결정 트리를 사용해 레드와인과 화이트 와인을 분류하는 문제를 풀었다. 특성을 더 추가하지 않고도 결정 트리의 성능이 로지스틱 회귀 모델보다 더 좋았다. 게다가 결정 트리는 깊이가 너무 깊지 않다면 비교적 설명하기 쉽다. 또 결정 트리가 어떻게 데이터를 분할하는지 이해하기 위해 분순도 개념과 정보 이득에 대해 알아보았다.
머신러닝 모델을 종종 블랙박스와 같다고 말한다. 실제로 모델의 계수나 절편이 왜 그렇게 학습되었는지 설명하기가 어렵다. 이에 비해 결저 트리는 비교적 비전문가에게도 설명하기 쉬운 모델을 만든다. 하지만 결정 트리는 여기에서 끝이 아니다. 결정 트리는 많은 앙상블 학습 알고리즘의 기반이 된다. 앙상블 학습은 신경망과 함께 가장 높은 성능을 내기 때문에 인기가 높은 알고리즘이다.
다음 절에서 결정 트리의 다양한 매개변수, 즉 하이퍼파라미터를 자동으로 찾기 위한 방법을 알아보고 그다음 앙상블 학습을 다루어 본다.
전체 소스 코드
결정 트리
로지스틱 회귀로 와인 분류하기
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
wine.head()
alcohol | sugar | pH | class | |
---|---|---|---|---|
0 | 9.4 | 1.9 | 3.51 | 0.0 |
1 | 9.8 | 2.6 | 3.20 | 0.0 |
2 | 9.8 | 2.3 | 3.26 | 0.0 |
3 | 9.8 | 1.9 | 3.16 | 0.0 |
4 | 9.4 | 1.9 | 3.51 | 0.0 |
wine.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 6497 entries, 0 to 6496 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 alcohol 6497 non-null float64 1 sugar 6497 non-null float64 2 pH 6497 non-null float64 3 class 6497 non-null float64 dtypes: float64(4) memory usage: 203.2 KB
wine.describe()
alcohol | sugar | pH | class | |
---|---|---|---|---|
count | 6497.000000 | 6497.000000 | 6497.000000 | 6497.000000 |
mean | 10.491801 | 5.443235 | 3.218501 | 0.753886 |
std | 1.192712 | 4.757804 | 0.160787 | 0.430779 |
min | 8.000000 | 0.600000 | 2.720000 | 0.000000 |
25% | 9.500000 | 1.800000 | 3.110000 | 1.000000 |
50% | 10.300000 | 3.000000 | 3.210000 | 1.000000 |
75% | 11.300000 | 8.100000 | 3.320000 | 1.000000 |
max | 14.900000 | 65.800000 | 4.010000 | 1.000000 |
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)
print(train_input.shape, test_input.shape)
(5197, 3) (1300, 3)
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
설명하기 쉬운 모델과 어려운 모델
print(lr.coef_, lr.intercept_)
[[ 0.51270274 1.6733911 -0.68767781]] [1.81777902]
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))
0.996921300750433 0.8592307692307692
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()
가지치기
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))
0.8454877814123533 0.8415384615384616
plt.figure(figsize=(20,15))
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
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()
print(dt.feature_importances_)
[0.12345626 0.86862934 0.0079144 ]
05-2 교차 검증과 그리드 서치¶
- 핵심 키워드
- 검증 세트
- 교차 검증
- 그리드 서치
- 랜덤 서치
- 검증 세트가 필요한 이유를 이해하고 교차 검증에 대해 배운다. 그리드 서치와 랜덤 서치를 이용해 최적의 성능을 내는 하이퍼파라미터를 찾는다.
- 문제 상황 :
" max_depth를 3말고 다른 값으로 하면 성능이 달라지나요?"
"네, 아마 그럴것 같습니다. 모든 값을 다 시도할 수는 없지만 시간이 허락하는 대로 테스트 하려합니다."
"이런저런 값으로 모델을 많이 만들어서 테스트 세트로 평가하면 결국 테스트 세트에 잘 맞는 모델이 만들어지는 것 아닌가요?"
지금까지 우리는 훈련 세트에서 모델을 훈련하고 테스트 세트에서 모델을 평가했다. 테스트 세트에서 얻은 점수를 보고 "실전에 투입하면 이 정도 성능을 기대할 수 있겠군"이라고 생각을 한다. 즉 일반화 성능을 가늠해 볼 수 있는 것이다. 그런데 테스트 세트를 사용해 자꾸 성능을 확인하다 보면 점점 테스트 세트에 맞추게 되는 셈이다.
이전까지는 문제를 간단히 하려고 테스트 세트를 사용했다. 하지만 테스트 세트로 일반화 성능을 올바르게 예측하려면 가능한 한 테스트 세트를 사용하지 말아야한다. 모델을 만들고 나서 마지막에 딱 한 번만 사용하는 것이 좋다. 그렇다면 max_depth 매개변수를 사용한 하이퍼파라미터 튜닝을 어떻게 할 수 있을까? 게다가 결정 트리는 테스트해 볼 매개변수가 많다.
검증 세트¶
테스트 세트를 사용하지 않으면 모델이 과대적합인지 과소적합인지 판단하기 어렵다. 테스트 세트를 사용하지 않고 이를 측정하는 간단한 방법은 훈련 세트를 또 나누는 것이다. 이 데이터를 검증 세트validation set이라고 부른다.
이 방법이 너무 단순해서 이상하게 들릴 수도 있겠지만, 실제로 많이 사용하는 방법이다. 1절에서 전체 데이터 중 20%를 테스트 세트로 만들고 나머지 80%를 훈련 세트로 만들었다. 이 훈련 세트 중에서 다시 20%를 떼어 내어 검증 세트로 만든다.
- 테스트 세트와 검증 세트에 얼마나 많은 샘플을 덜어 놔야 하나요?
- 보통 20~30%를 테스트 세트와 검증 세트로 떼어 놓는다. 하지만 문제에 따라 다르다. 훈련 데이터가 아주 많다면 단 몇 %만 떼어 놓아도 전체 데이터를 대표하는 데 문제가 없다.
훈련 세트에서 모델을 훈련하고 검증 세트로 모델을 평가한다. 이런 식으로 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 고른다. 그다음 이 매개변수를 사용해 훈련세트와 검증 세트를 합쳐 전체 훈련 데이터에서 모델을 다시 훈련한다. 그리고 마지막에 테스트 세트에서 최종 점수를 평가한다. 아마도 실전에 투입했을 때 테스트 세트의 점수와 비슷한 성능을 기대할 수 있을 것이다.
그럼 이전 절에 사용했던 데이터를 다시 불러와서 검증 세트를 만들어 보자. 먼저 판다스로 csv 데이터를 읽자.
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
그 다음 class 열을 타깃으로 사용하고 나머지 열은 특성 배열에 저장한다.
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
이제 훈련 세트와 테스트 세트를 나눌 차례이다. 방식은 이전과 동일하다. 훈련 세트의 입력 데이터와 타깃 데이터를 train_input과 train_target배열에 저장한다.
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)
print(train_input.shape, test_input.shape)
(5197, 3) (1300, 3)
그 다음 train_input과 train_target을 다시 train_test_split() 함수에 넣어 훈련 세트 sub_input, sub_target과 검증 세트 val_input, val_target을 만든다. 여기에서도 test_size 매개변수를 0.2로 지정하여 train_input의 약 20%를 val_input으로 만든다.
sub_input, val_input, sub_target, val_target = train_test_split(train_input, train_target, test_size=0.2, random_state=42)
어렵지 않군요. 단순히 train_test_split() 함수를 2번 적용해서 훈련 세트와 검증 세트로 나눠준 것뿐이다. 훈련 세트와 검증 세트의 크기를 확인해 보자.
print(sub_input.shape, val_input.shape)
(4157, 3) (1040, 3)
네, 원래 5,197개 였던 훈련 세트가 4,157개로 줄고, 검증 세트는 1,040개가 되었다. 이제 sub_input, sub_target과 val_input, val_target을 사용해 모델을 만들고 평가해 보겠다.
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)
print(dt.score(sub_input, sub_target))
print(dt.score(val_input, val_target))
0.9971133028626413 0.864423076923077
네, 좋습니다. 이렇게 val_input, val_target을 사용해서 모델을 평가하면 됩니다. 이 모델은 확실히 훈련 세트에 과대적합되어 있다. 매개변수를 바꿔서 더 좋은 모델을 찾아야 한다. 그전에 검증 세트에 관해 좀더 알아야 할것이 있다.
교차 검증¶
검증 세트를 만드느라 훈련 세트가 줄었다. 보통 많은 데이터를 훈련에 사용할수록 좋은 모델이 만들어진다. 그렇다고 검증 세트를 너무 조금 떼어 놓으면 검증 점수가 들쭉날쭉하고 불안정할 것이다. 이럴 때 교차 검증 cross validation을 이용하면 안정적인 검증 점수를 얻고 훈련에 더 많은 데이터를 사용할 수 있다.
교차 검증은 검증 세트를 떼어 내어 평가하는 과정을 여러 번 반복한다. 그 다음 이 점수를 평균하여 최종 검증 점수를 얻는다. 이 과정을 그림으로 보면 이해가 쉽다. 다음은 3-폴드 교차 검증 그림이다.
- 3-폴드 교차 검증이 뭔가요?
- 훈련 세트를 세 부분으로 나눠서 교차 검증을 수행하는 것을 3-폴드 교차 검증이라고 한다. 통칭 k-폴드 교차 검증(k-fold cross validation)이라고 하며, 훈련 세트를 몇 부분으로 나누냐에 따라 다르게 부른다. k-겹 교차 검증이라고도 부른다.
이해를 돕기 위해 3-폴드 교차 검증을 예시로 들었지만, 보통 5-폴드 교차 검증이나 10-폴드 교차 검증을 많이 사용한다. 이렇게 하면 데이터의 80~90%까지 훈련에 사용할 수 있다. 검증 세트가 줄어들지만 각 폴드에서 계산한 검증 점수를 평균하기 때문에 안정된 점수로 생각 할 수 있다.
사이킷런에는 cross_validate()라는 교차 검증 함수가 있다. 사용법은 간단한데, 먼저 평가할 모델 객체를 첫 번째 매개변수로 전달한다. 그 다음 앞에서 처럼 직접 검증 세트를 떼어 내지 않고 훈련 세트 전체를 cross_validate() 함수에 전달한다.
- 사이킷런에는 cross_validate() 함수의 전신인 cross_val_score() 함수도 있다. 이 함수는 cross_validate() 함수의 결과 중에서 test_score 값만 반환하게 된다.
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
{'fit_time': array([0.00679302, 0.00663161, 0.00682569, 0.00658035, 0.00649905]), 'score_time': array([0.00062847, 0.00059557, 0.00059533, 0.00058913, 0.00057817]), 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
이 함수는 fit_time, score_time, test_score 키를 가진 딕셔너리를 반환한다. 처음 2개의 키는 각각 모델을 훈련하는 시간과 검증하는 시간을 의미한다. 각 키마다 5개의 숫자가 담겨 있다. cross_validate()함수는 기본적으로 5-폴드 교차 검증을 수행한다. cv 매개변수에서 폴드 수를 바꿀 수도 있다.
- 훈련과 검증 시간은 코랩에서 리소스를 사용하는 상황에 따라 달라질 수 있으므로 fit_time과 score_time 세트는 출력 결과가 책과 다를 수 있다.
교차 검증의 최종 점수는 test_score 키에 담긴 5개의 점수를 평균하여 얻을 수 있다. 이름은 test_score지만 검증 폴드의 점수이다. 혼동하지말자.
import numpy as np
print(np.mean(scores['test_score']))
0.855300214703487
교차 검증을 수행하면 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해 볼 수 있다.
한 가지 주의할 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않는다. 앞서 우리는 train_test_split() 함수로 전체 데이터를 섞은 후 훈련 세트를 준비했기 때문에 따로 섞을 필요가 없다. 하지만 만약 교차 검증을 할 때 훈련 세트를 섞으려면 분할기 splitter를 지정해야한다.
사이킷런의 분할기는 교차 검증에서 폴드를 어떻게 나눌지 결정해 준다. cross_validate() 함수는 기본적으로 회귀 모델일 경우 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.855300214703487
만약 훈련 세트를 섞은 후 10-폴드 교차 검증을 수행하려면 다음과 같이작성한다.
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))
0.8574181117533719
KFold 클래스도 동일한 방식으로 사용할 수 있다. 네, 좋다. 이제 교차 검증에 대해 이해했다. 이어서 결정 트리의 매개변수 값을 바꿔가며 가장 좋은 성능이 나오는 모델을 찾아 보겠다. 이때 테스트 세트를 사용하지 않고 교차 검증을 통해서 좋은 모델을 고르면 된다. 그럼 시작해 보자.
하이퍼파라미터 튜닝¶
머신러닝 모델이 학습하는 파라미터를 모델 파라미터라고 부른다고 했던 것을 기억하나? 반면에 모델이 학습할 수 없어서 사용자가 지정해야만 하는 파라미터를 하이퍼파라미터라고 합니다. 사이킷런과 같은 머신러니 라이브러리를 사용할 때 이런 하이퍼파라미터는 모두 클래스나 메서드의 매개변수로 표현 된다.
- 하이퍼파라미터는 사용자가 지정 파라미터 이다.
그럼 이런 하이퍼파라미터를 튜닝하는 작업은 어떻게 진행할까? 먼저 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련한다. 그 다음 검증 세트의 점수나 교차 검증을 통해서 매개변수를 조금씩 바꿔 본다. 모델마다 적게는 1 ~ 2개에서, 많게는 5 ~ 6개의 매개변수를 제공한다. 이 매개변수를 바꿔가면서 모델을 훈련하고 교차 검즘을 수행해야 한다.
- 사람의 개입 없이 하이퍼파라미터 튜닝을 자동으로 수행하는 기술을 'AutoML'이라고 부른다.
그런데 아주 중요한 점이 있다. 가령 결정 트리 모델에서 최적의 max_depth 값을 찾았다고 가정해보자. 그다음 max_depth를 최적의 값으로 고정하고 min_samples_split을 바꿔가며 최적의 값을 찾는다. 이렇게 한 매개변수의 최적값을 찾고 다른 매개변수의 최적값을 찾아도 될까요? 아니요, 틀렸다. 불행하게도 max_depth의 최적값은 min_samples_split 매개변수의 값이 바뀌면 함께 달라진다. 즉 이 두 매개변수를 동시에 바꿔가면 최적의 값을 찾아야 하는 것이다.
게다가 매개변수가 많아지면 문제는 더 복잡해 진다. 파이썬의 for반복문으로 이런 과정을 직접 구현할 수도 있지만, 이미 만들어진 도구를 사용하는게 편리하겠다. 사이킷런에서 제공하는 그리드 서치 Grid Search를 사용하자.
사이킷런의 GridSearchCV 클래스는 친절하게도 하이퍼파라미터 탐색과 교차 검증을 한 번에 수행한다. 별도로 cross_validat()함수를 호출할 필요가 없다. 그럼 어떻게 사용하는지 간단한 예를 만들어보자. 기본 매개변수를 사용할 결정 트리 모델에서 min_impurity_decrease 매개변수의 최적값을 찾아본다. 먼저 GridSearchCV 클래스를 임포트하고 탐색할 매개변수와 탐색할 값의 리스트를 딕셔너리로 만든다.
from sklearn.model_selection import GridSearchCV
params = {'min_impurity_decrease' : [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}
여기서는 0.0001부터 0.0005까지 0.0001씩 증가하는 5개의 값을 시도하겠다. GridSearchCV 클래스에 탐색 대상 모델과 params 변수를 전달하여 그리드 서치 객체를 만든다.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params,n_jobs=-1)
결정 트리 클래스의 객체를 생성하자마자 바로 전달했다. 어렵지 않다. 그다음 일반 모델을 훈련하는 것처럼 gs 객체에 fit()메서드를 호출한다. 이 메서드를 호출하면 그리드 서치 객체는 결정 트리 모델 min_impuruty_decrease 값을 바꿔가며 총 5번 실행한다.
GridSearchCV의 cv 매개변수 기본값은 5이다. 따라서 min_imputiry_decrease 값마다 5-폴드 교차 검증을 수행한다. 결국 5 x 5 = 25 개의 모델을 훈련한다. 많은 모델을 훈련하기 때문에 GridSearchCV 클래스의 n_jobs 매개변수에서 병렬 실행에 사용할 CPU 코어 수를 지정하는 것이 좋다. 이 매개변수의 기본값은 1이다. -1로 지정하면 시스템에 있는 모든 코어를 사용한다. 그럼 그리드 서치를 수행해보자. 실행 결과는 크게 중요하지 않아 건너 뛴다.
gs.fit(train_input, train_target)
GridSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1, param_grid={'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]})
교차 검증에서 최적의 하이퍼파라미터를 찾으면 전체 훈련 세트로 모델을 다시 만들어야 한다고 했던 것을 기억하나?
아주 편리하게도 사이킷런의 그리드 서치는 훈련이 끝나면 25개의 모델 중에서 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련한다. 이 모델은 gs 객체의 bestestimator 속성에 저장되어 있다. 이 모델을 일반 결정 트리 처럼 똑같이 사용 할 수 있다.
dt = gs.best_estimator_
print(dt.score(train_input, train_target))
0.9615162593804117
그리고 서치로 찾은 최적의 매개변수는 bestparams 속성에 저장되어 있다.
print(gs.best_params_)
{'min_impurity_decrease': 0.0001}
여기서는 0.0001이 가장 좋은 값으로 선택되었다. 각 매개변수에서 수행한 교차 검증의 평균 점수는 cvresults 속성의 'mean_test_score'키에 저장되어 있다. 5번의 교차 검증으로 얻은 점수를 출력해 보자.
print(gs.cv_results_['mean_test_score'])
[0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]
첫 번째 값이 가장 큰 것 같다. 수동으로 고르는 것보다 넘파이 argmax() 함수를 사용하면 가장 큰 값의 인덱스를 추출할 수 있다. 그 다음 이 인덱스를 사용해 params키에 저장된 매개변수를 출력할 수 있다. 이 값이 최상의 검증 점수를 만든 매개변수 조합이다. 앞에서 출력한 gs.best_params_와 동일한지 확인해 보자.
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])
{'min_impurity_decrease': 0.0001}
좋다. 이 과정을 정리해 보자.
- 먼저 탐색할 매개변수를 지정한다.
- 그다음 훈련세트에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다. 이 조합은 그리드 서치 객체에 저장된다.
- 그리드 서치는 최상의 매개변수에서 (교차 검증에 사용한 훈련 세트가 아니라) 전체 훈련 세트를 사용해 최종 모델을 훈련한다. 이 모델도 그리드 서치 객체에 저장된다.
그럼 조금 더 복잡한 매개변수 조합을 탐색해보자. 결정 트리에서 min_impurity_decrease 는 노드를 분할하기 위한 불순도 감소 최소량을 지정한다. 여기에다가 max_depth로 트리의 깊이를 제한하고 min_samples_split 으로 노드를 나누기 위한 최소 샘플 수도 골라보겠다.
params = {'min_impurity_decrease': np.arange(0.0001, 0.001, 0.0001),
'max_depth': range(5, 20, 1),
'min_samples_split': range(2, 100, 10)}
넘파이 arange() 함수는 첫 번째 매개변수 값에서 시작하여 두 번째 매개변수에 도달할 때까지 세 번째 매개변수를 계속 더한 배열을 만든다. 코드에서는 0.0001 에서 시작하여 0.001이 될 때까지 0.0001을 계속 더한 배열이다. 두 번째 매개변수는 포함되지 않으므로 배열의 원소는 총 9개이다.
파이썬 range() 함수도 비슷하다. 하지만 이 함수는 정수만 사용할 수 있다. 이 경우 max_depth를 5에서 20까지 1씩 증가하면서 15개의 값을 만든다. min_samples_split은 2에서 100가지 10씩 증가하면서 10개의 값을 만든다.
따라서 이 매개변수로 수행할 교차 검증 횟수는 9 x 15 x 10 = 1,350개 이다. 기본 5-폴드 교차 검증을 수행하므로 만들어지는 모델의 수는 6,750개나 된다. n_jobs 매개변수를 -1로 설정하고 그리드 서치를 실행해 보자.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)
GridSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1, param_grid={'max_depth': range(5, 20), 'min_impurity_decrease': array([0.0001, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0008, 0.0009]), 'min_samples_split': range(2, 100, 10)})
최상의 매개변수 조합을 확인해 보겠다.
print(gs.best_params_)
{'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}
최상의 교차 검증 점수도 확인해 보겠다.
print(np.max(gs.cv_results_['mean_test_score']))
0.8683865773302731
훌륭하다. GridSearchCV 클래스를 사용하니 매개변수를 일일이 바꿔가며 교차 검증을 수행하지 않고 원하는 매개변수 값을 나열하면 자동으로 교차 검증을 수행해서 최상의 매개변수를 찾을 수 있다.
그런데 아직 조금 아쉬운 점이 있다. 앞에서 탐색할 매개변수의 간격을 0.0001 혹은 1로 설정했는데, 이렇게 간격을 둔 것에 특별한 근거가 없다. 이보다 더 좁거나 넓은 간격으로 시도해 볼 수 있지않을까?
랜덤 서치
매개변수의 값이 수치일 때 값의 범위나 간격을 미리 정하기 어려울 수 있다. 또 너무 많은 매개변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있다. 이럴때 랜덤서치 Random Search를 사용하면 좋다.
랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아니라 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다. 확률 분포라 하니 조금 어렵게 들릴 수 있지만 간단하고 쉽다. 먼저 싸이파이에서 2개의 확률 분포 클래스를 임포트 해보자.
- 싸이파이(scipy)는 어떤 라이브러라 인가?
- 싸이파이는 파이썬의 핵심 과학 라이브러리 중 하나이다. 적분, 보간, 선형대수, 확률 등을 포함한 수치 계산 전용 라이브러리이다. 사이킷런은 넘파이와 싸이파이 기능을 많이 사용한다.
from scipy.stats import uniform, randint
싸이파이의 stats 서브 패키지에 있는 uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑는다. 이를 '균등 분포에서 샘플링한다'라고 말한다. randint는 정숫값을 뽑고, uniform은 실숫값을 뽑는다. 사용하는 방법은 같다. 0에서 10 사이의 범위를 갖는 randint 객체를 만들고 10개의 숫자를 샘플링해 보자.
rgen = randint(0,10)
rgen.rvs(10)
array([4, 7, 3, 5, 2, 4, 4, 0, 5, 8])
- randint와 uniform은 임의로 샘플링하므로 실행 결과가 책과 다를 수 있다. 이어지는 실행 결과도 마찬가지이다.
10개밖에 되지 않기 때문에 고르게 샘플링되는 것 같지 않지만 샘플링 숫자를 늘리면 쉽게 확인할 수 있다 1,000개를 샘플링해서 각 숫자의 개수를 세어보겠다.
np.unique(rgen.rvs(1000), return_counts=True)
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([ 96, 110, 90, 104, 97, 77, 127, 95, 101, 103]))
개수가 늘어나니 0에서 9까지의 숫자가 어느 정도 고르게 추출된 것을 볼 수 있다. uniform 클래스의 사용법도 동일하다. 0 ~ 1 사이에서 10개의 실수를 추출해 보자.
ugen = uniform(0,1)
ugen.rvs(10)
array([0.60963866, 0.53600458, 0.34079883, 0.91652462, 0.79751471, 0.52242664, 0.49761594, 0.85851783, 0.51689427, 0.15915304])
좋다. 난수 발생기랑 유사하게 생각하면 된다. 랜덤 서치에 randint과 uniform 클래스 객체를 넘겨주고 총 몇 번을 샘플링해서 최적의 매개변수를 찾으라고 명령할 수 있다. 샘플링 횟수는 시스템 자원이 허락하는 범위 내에서 최대한 크게 하는 것이 좋다.
그럼 탐색할 매개변수의 딕셔너리를 만들어 보자. 여기에서는 min_samples_leaf 매개변수를 탐색 대상에 추가하겠다. 이 매개변수는 리프 노드가 되기 위한 최소 샘플의 개수이다. 어떤 노드가 분할하여 만들어질 자식 노드의 샘플 수가 이 값보다 작을 경우 분할하지 않는다.
탐색할 매개변수 범위는 다음과 같다.
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),
}
min_imputiry_decrease 는 0.0001에서 0.001 사이의 실숫값을 샘플링 한다. max_depth는 20에서 50 사이의 정수, min_samples_split은 2에서 25 사이의 정수, min_samples_leaf는 1에서 25 사이으이 정수를 샘플링 한다. 샘플링 횟수는 사이킷런의 랜덤 서치 클래스인 RandomizedSearchCV의 n_iter 매개변수에 지정한다.
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)
RandomizedSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_iter=100, n_jobs=-1, param_distributions={'max_depth': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c752fa58>, 'min_impurity_decrease': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c752f908>, 'min_samples_leaf': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c752f0f0>, 'min_samples_split': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c752f780>}, random_state=42)
위 params에 정의된 매개변수 범위에서 총 100번(n_iter 매개변수)을 샘플링하여 교차 검증을 수행하고 최적의 매개변수 조합을 찾는다. 앞서 그리드 서치보다 훨씬 교차 검증 수를 줄이면서 넓은 영역을 효과적으로 탐색할 수 있다. 결과를 확인해 보자. 먼저 최적의 매개변수 조합을 출력하겠다.
print(gs.best_params_)
{'max_depth': 39, 'min_impurity_decrease': 0.00034102546602601173, 'min_samples_leaf': 7, 'min_samples_split': 13}
최고의 교차 검증 점수도 확인해 보겠다.
print(np.max(gs.cv_results_['mean_test_score']))
0.8695428296438884
최적의 모델은 이미 전체 훈련 세트(train_input, train_target)로 훈련되어 bestestimator 속성에 저장되어있다. 이 모델을 최종 모델로 결정하고 테스트 세트의 성능을 확인해 보자.
dt = gs.best_estimator_
print(dt.score(test_input, test_target))
0.86
테스트 세트 점수는 검증 세트에 대한 점수보다 조금 작은 것이 일반적이다. 테스트 세트 점수가 아주 만족 스럽지는 않지만 다양한 매개변수를 테스트해서 얻은 결과임을 자랑스럽게 말할 수 있을 것 같다.
앞으로 수동으로 매개변수를 바꾸는 대신에, 그리도 서치나 랜덤 서치를 사용해야 겠다.
최적의 모델을 위한 하이퍼파라미터 탐색 - 문제해결 과정¶
레드 와인과 화이트 와인을 선별하는 작업의 성능을 끌어올리기 위해 결정 트리의 다양한 하이퍼파라미터를 시도해 봐야한다. 이런 과정에서 테스트 세트를 사용하면 결국 테스트 세트에 맞춰 모델을 훈련하는 효과를 만든다.
테스트 세트는 최종 모델을 선택할 때까지 사용하지 말아야 한다. 테스트 세트를 사용하지 않고 모델을 평가하려면 또 다른 세트가 필요하다. 이를 검증세트라고 부른다. 혹은 개발 세트 dev set 라고도 부른다. 검증 세트는 훈련세트 중 일부를 다시 덜어 내어 만든다.
검증 세트가 크지 않다면 어떻게 데이터를 나누었는지에 따라 검증 점수가 들쭉날쭉 할 것이다. 훈련한 모델의 성능을 안정적으로 평가하기 위해 검증 세트를 한 번 나누어 모델을 평가하는 것에 그치지 않고 여러 번 반복 할 수 있다. 이를 교차 검증 이라고 한다.
보통 훈련 세트를 5등분 혹은 10등분 한다. 나누어진 한 덩어리를 폴드라고 부르며 한 폴드씩 돌아가면서 검증 세트의 역할을 한다. 따라서 전체적으로 5개 혹은 10개의 모델을 만든다. 최종 검증 점수는 모든 폴드의 검증 점수를 평균하여 계산한다.
교차 검증을 사용해 다양한 하이퍼파라미터를 탐색한다. 머신러닝 라이브러리에서는 클래스와 메서드의 매개변수를 바꾸어 모델을 훈련하고 평가해 보는 작업이다. 이런 과정은 때론 지루하고 반복적이다. 테스트하고 싶은 매개변수 리스트를 만들어 이 과정을 자동화하는 그리드 서치를 사용하면 편리하다.
매개변수 값이 수치형이고 특히 연속적인 실숫값이라면 싸이파이의 확률 분포 객체를 전달하여 특정 범위 내에서 지정된 횟수만큼 매개변수 후보값을 샘플링하여 교차 검증을 시도할 수 있다. 이는 한정된 자원을 최대한 활용하여 효율적으로 하이퍼파라미터 공간을 탐색할 수 있는 아주 좋은 도구이다.
다음 절에서는 결정 트리를 확장하여 머신러닝 계를 제패한 앙상블 모델에 대해 알아보자.
전체 소스 코드
- https://bit.ly/hg-05-2 에 접속하면 코랩에서 이 절의 코드를 바로 열어 볼 수 있다.
교차 검증과 그리드 서치
검증 세트
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)
sub_input, val_input, sub_target, val_target = train_test_split(
train_input, train_target, test_size=0.2, random_state=42)
print(sub_input.shape, val_input.shape)
(4157, 3) (1040, 3)
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)
print(dt.score(sub_input, sub_target))
print(dt.score(val_input, val_target))
0.9971133028626413 0.864423076923077
교차 검증
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
{'fit_time': array([0.00663018, 0.00650835, 0.00684357, 0.00673199, 0.00646353]), 'score_time': array([0.00062466, 0.00060415, 0.00059652, 0.00059557, 0.00058413]), 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
import numpy as np
print(np.mean(scores['test_score']))
0.855300214703487
from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, train_input, train_target, cv=StratifiedKFold())
print(np.mean(scores['test_score']))
0.855300214703487
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))
0.8574181117533719
하이퍼파라미터 튜닝
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)
GridSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1, param_grid={'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]})
dt = gs.best_estimator_
print(dt.score(train_input, train_target))
0.9615162593804117
print(gs.best_params_)
{'min_impurity_decrease': 0.0001}
print(gs.cv_results_['mean_test_score'])
[0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])
{'min_impurity_decrease': 0.0001}
params = {'min_impurity_decrease': np.arange(0.0001, 0.001, 0.0001),
'max_depth': range(5, 20, 1),
'min_samples_split': range(2, 100, 10)
}
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)
GridSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1, param_grid={'max_depth': range(5, 20), 'min_impurity_decrease': array([0.0001, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0008, 0.0009]), 'min_samples_split': range(2, 100, 10)})
print(gs.best_params_)
{'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}
print(np.max(gs.cv_results_['mean_test_score']))
0.8683865773302731
랜덤 서치
from scipy.stats import uniform, randint
rgen = randint(0, 10)
rgen.rvs(10)
array([8, 1, 6, 6, 5, 7, 1, 2, 7, 3])
np.unique(rgen.rvs(1000), return_counts=True)
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([ 93, 94, 94, 116, 119, 92, 85, 89, 109, 109]))
ugen = uniform(0, 1)
ugen.rvs(10)
array([0.958198 , 0.35331899, 0.91008557, 0.3816731 , 0.60784189, 0.06520607, 0.23138592, 0.80252175, 0.9784105 , 0.13472516])
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),
}
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)
RandomizedSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_iter=100, n_jobs=-1, param_distributions={'max_depth': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c6e895c0>, 'min_impurity_decrease': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c6e899e8>, 'min_samples_leaf': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c6e89668>, 'min_samples_split': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fb6c6e89e48>}, random_state=42)
print(gs.best_params_)
{'max_depth': 39, 'min_impurity_decrease': 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(test_input, test_target))
0.86
05-3 트리의 앙상블¶
- 핵심키워드
- 앙상블 학습
- 랜덤 포레스트
- 엑스트라 트리
- 그레이디언트 부스팅
- 앙상블 학습이 무엇인지 이해하고 다양한 앙상블 학습 알고리즘을 실습을 통해 배운다.
문제 상황¶
"베스트 머신러닝 알고리즘을 찾아 보고하라시네" "그건 문제마다 그때그때 다를텐데여.." "그렇기는 한데, 그래도 대체로 성능이 좋은 알고리즘이 있지 않을까? 지난 번 결정 트리는 어때?" "글쎄요. 논의좀 해 볼게요"
- 가장 좋은 알고리즘이 있다고 해서 다른 알고리즘을 배울 필요가 없는 것은 아니다. 보편적으로 성능이 좋아 널리 사용되는 알고리즘이 있지만 문제마다 다를 수 있으며 어떤 알고리즘이 더 뛰어나다고 미리 판단해서는 안된다.
정형 데이터와 비정형 데이터¶
랜덤 포레스트에 대해 배우기 전에 잠시 우리가 다루었던 데이터를 되돌아보겠다. 4장까지는 생선의 길이,높이,무게 등을 데이터로 사용했다.이 데이터는 CSV 파일에 가지런히 정리되어 있었다. 또 이번 장에서 사용한 와인 데이터도 CSV파일이 있었다.
이런 형태의 데이터를 정형 데이터 structure data라고 부른다. 쉽게 말해 어떤 구조로 되어 있다는 뜻이다. 이런 데이터는 CSV나 데이터베이스 Database, 혹은 엑셀 Excel에 저장하기 쉽다.
온라인 쇼핑몰에 진열된 상품과 우리가 구매한 쇼핑 정보는 모두 데이터베이스에 저장되는 정형 데이터에 속한다. 사실 프로그래머가 다루는 대부분의 데이터가 정형 데이터이다. 정형 데이터의 반대도 있겠네요? 네, 이와 반대되는 데이터를 비정형 데이터 unstructured data라고 부른다.
비정형 데이터는 데이터베이스나 엑셀로 표현하기 어려운 것들이다. 우리 주위에서 찾아보면 이책의 글과 같은 텍스트 데이터, 디지털 카메라로 찍은 사진, 핸드폰으로 듣느 디지털 음악 등이 있다.
- 텍스트나 사진을 데이터베이스에 저장할 수는 없나요?
- 아니요. 저장할 수도 있다. 다만 여기에서는 보편적인 사례를 설명한 것이다. 데이터베이스 중에는 구조적이지 않은 데이터를 저장하는 데 편리하도록 발전한 것이 많다. 대표적으로 NoSQL 데이터베이스는 엑셀이나 CSV에 담기 어려운 텍스트나 JSON 데이터를 저장하는데 용이하다.
지금까지 배운 머신러닝 알고리즘은 정형 데이터에 잘맞는다. 그중에 정형 데이터를 다루는 데 가장 뛰어난 성과를 내는 알고리즘이 앙상블 학습 ensemble learning 이다. 이 알고리즘은 대부분 결정트리를 기반으로 만들어져 있다. 바로 이 절에서 배울 알고리즘들이 앙상블 학습에 속한다.
그럼 비정형 데이터에는 어떤 알고리즘을 사용해야 할까? 바로 7장에서 배울 신경망 알고리즘이다. 비정형 데이터는 규칙성을 찾기 어려워 전통적인 머신러닝 방법으로는 모델을 만들기 까다롭다. 하지만 신경망 알고리즘의 놀라운 발전 덕분에 사진을 인식하고 텍스트를 이해하는 모델을 만들 수 있다.
이제 사이킷런에서 제공하는 정형 데이터의 끝판왕인 앙상블 학습 알고리즘을 알아보겠다.
랜덤 포레스트¶
랜덤 포레스트 Random Forest는 앙상블 학습의 대표 주자 중 하나로 안정적인 성능 덕분에 널리 사용되고 있다. 앙상블 학습을 적용할 때 가장 먼저 랜덤 포레스트를 시도해 보길 권한다.
이름 자체로 유추할 수 있듯이 랜덤 포레스트는 결정 트리를 랜덤하게 만들어 결정 트리(나무)의 숲을 만든다.그리고 각 결정 트리의 에측을 사용해 최종 예측을 만든다. 그럼 랜덤 포레스트가 어떻게 숲을 구성하는지 관찰해 보자.
- 이 절은 사이킷런에 구현된 앙상블 학습알고리즘을 기준으로 설명한다. 머신러닝 라이브러리마다 구현 방식에 조그씩 차익 있을 수 있다.
먼저 랜덤 포레스트는 각 트리를 훈련하기 위한 데이터를 랜덤하게 만드는데, 이 데이터를 만드는 방법이 독특하다. 우리가 입력한 훈련 데이터에서 랜덤하게 샘플을 추출하여 훈련 데이터를 만든다. 이때 한 샘플이 중복되어 추출될 수도 있다.
예를 들어 1,000개 가방에서 100개씩 샘플을 뽑는다면 먼저 1개를 뽑고, 뽑았던 1개를 다시 가방에 넣는다. 이런 식으로 계속해서 100개를 가방에서 뽑으면 중복된 샘플을 뽑을 수 있다. 이렇게 만들어진 샘플을 부트스트랩 샘플 bootstrap sample라고 부른다. 기본적으로 부트스트랩 샘플은 훈련 세트의 크기와 같게 만든다. 1,000개 가방에서 중복하여 1,000개의 샘플을 뽑기 때문에 부트스트랩 샘플은 훈련 세트와 크기가 같다.
- 부트스트랩이 뭔가요?
- 보통 부트스트랩 방식이라고 하는데, 데이터 세트에서 중복을 허용하여 데이터를 샘플링하는 방식을 의미한다. 본문에서 설명한 것처럼 1,000개의 샘플이 있을 때 먼저 1개를 뽑고, 다시 가방에 넣어 그다음 샘플을 뽑는 방식을 뜻하는 거다. 부트스트랩 샘플이란 결국 부트스트랩 방식으로 샘플링하여 분류한 데이터라는 의미이다.
또한 각 노드를 분할할 때 전체 특성 중에서 일부 특성을 무작위로 고른 다음 이 중에서 최선의 분할을 찾는다. 분류 모델인 RandomForestClassifier는 기본적으로 전체 특성 개수의 제곱근만큼의 특성을 선택한다. 즉 4개의 특성이 있다면 노드마다 2개를 랜덤하게 선택하여 사용한다. 다만 회귀 모델인 RandomForestRegressor는 전체 특성을 사용한다.
사이킷런의 랜덤 포레스트는 기본적으로 100개의 결정 트리를 이런 방식으로 훈련한다. 그다음 분류일 때는 각 트리의 클래스별 확률을 평균하여 가장 높은 확률을 가진 클래스를 예측으로 삼는다. 회귀일 때는 단순히 각 트리의 예측을 평균한다.
- 분류와 회귀를 다시 살펴보자
- 지도 학습 알고리즘을 다루면서 분류와 회귀를 설명했다. 분류는 샘플을 몇개의 클래스 중 하나로 분류하는 문제고, 회귀는 임의의 어떤 숫자를 예측하는 문제였다.
랜덤 포레스트는 랜덤하게 선택한 샘플과 특성을 사용하기 때문에 훈련 세트에 과대적합되는 것을 막아주고 검증 세트와 테스트 세트에서 안정적인 성능을 얻을 수 있다. 종종 기본 매개변수 설정만으로도 아주 좋은 결과를 낸다.
그럼 사이킷런의 RandomForestClassifier 클래스를 화이트 와인을 분류하는 문제에 적용해 보자. 먼저 이전 절에서 했던 것처럼 와인 데이터셋을 판다스로 불러오고 훈련 세트와 테스트 세트로 나눈다.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
wine = pd.read_csv('https://bit.ly/wine_csv_data')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
train_input, test_input, train_target, test_target = train_test_split( data, target, test_size = 0.2, random_state=42)
cross_validate() 함수를 사용해 교차 검증을 수행해 보겠다. RandomForestClassifier 는 기본적으로 100개의 결정 트리를 사용하므로 n_jobs 매개변수를 -1로 지정하여 모든 CPU 코어를 사용하는 것이 좋다. cross_validate() 함수의 n_jobs 매개변수를 True로 지정하면 검증 점수뿐만 아니라 훈련 세트에 대한 점수도 같이 반환한다. 훈련 세트와 검증 세트의 점수를 비교하면 과대적합을 파악하는데 용이하다. (return_train_score 매개변수의 기본값은 False이다.)
from sklearn.model_selection import cross_validate
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
출력된 결과를 보면 훈련 세트에 다소 과대적합된 것 같다. 여기에서는 알고리즘을 조사하는 것이 목적이므로 매개변수를 더 조정하지 않도록 하겠다.
- 사실 이 예제는 매우 간단하고 특성이 많지 않아 그리드 서치를 사용하더라도 하이퍼파라미터 튜닝의 결과가 크게 나아지지 않는다.
랜덤 포레스트 결정 트리의 앙상블이기 때문에 DecisionTreeClassifier가 제공하는 중요한 매개 변수를 모두 제공한다. criterion, max_depth, max_feature, min_samples_split, min_impurity_decrease, min_samples_leaf 등이다. 또한 결정 트리의 큰 장점 중 하나인 특성 중 요도를 계산한다. 랜덤 포레스트의 특성 중요도는 각 결정 트리의 특성 중요도를 취합한 것이다. 앞의 랜덤 포레스트 모델을 훈련 세트에 훈련한 후 특성 중요도를 출력해 보자.
rf.fit(train_input, train_target)
print(rf.feature_importances_)
[0.23167441 0.50039841 0.26792718]
이 결과를 앞의 1절 '결정 트리'에서 만든 특성 중요도와 비교해보자. 결정트리에서 특성 중요도는 다음과 같았다.
0.12345626 , 0.86862934 , 0.0079144
각각 [알코올 도수, 당도, PH]였는데, 두 번째 특성인 당도의 중요도가 감소하고 알코올 도수와 PH특성의 중요도가 조금 상승했다. 이런 이유는 랜덤 포레스트가 특성의 일부를 랜덤하게 선택하여 결정 트리를 훈련하기 때문이다. 그 결과 하나의 특성에 과도하게 집중하지 않고 좀 더 많은 특성이 훈련에 기여할 기회를 얻는다. 이는 과대적합을 줄이고 일반화 성능을 높이는데 도움이 된다.
RandomForestClassifier에는 재미있는 기능이 하나 더 있는데, 자체적으로 모델을 평가하는 점수를 얻을 수 있다. 랜덤 포레스트는 훈련 세트에서 중복을 허용하여 부트스트랩 샘플을 만들어 결정 트리를 훈련한다고 했다. 이때 부트스트랩 샘플에 포함되지 않고 남는 샘플이 있다. 이런 샘프을 OOB (out of bag) 샘플이라고 한다. 이 남는 샘플을 사용하여 부트스트랩 샘플로 훈련한 결정 트리를 평가할 수 있다. 마치 검증 세트의 역할을 하는 것이다.
이 점수를 얻으려면 RandomForestClassifier 클래스의 oob_score 매개변수를 True로 지정해야 한다.(이 매개변수의 기본값은 False이다.) 이렇게 하면 랜덤 포레스트는 각 결정 트리의 OOB 점수를 평균하여 출력한다. oob_score = True로 지정하고 모델을 훈련하여 OOB 점수를 출력해 보겠다.
rf = RandomForestClassifier(oob_score=True, n_jobs= -1, random_state=42)
rf.fit(train_input, train_target)
print(rf.oob_score_)
0.8934000384837406
교차 검증에서 얻은 점수와 매우 비슷한 결과를 얻었다. OOB 점수를 사용하면 교차 검증을 대신할 수 있어서 결과적으로 훈련 세트에 더 많은 샘플을 사용할 수 있다.
다음에 알아볼 앙상블 학습은 랜덤 포레스트와 아주 비슷한 엑스트라 트리 이다.
엑스트라 트리¶
엑스트라 트리 Extra Tree 는 랜덤 포레스트와 매우 비슷하게 동작한다. 기본적으로 100개의 결정 트리를 훈련한다. 랜덤 포레스트와 동일하게 결정 트리가 제공하는 대부분의 매개변수를 지원한다. 또한 전체 특성 중에 일부 특성을 랜덤하게 선택하여 노드를 분할하는 데 사용 한다.
랜덤 포레스트와 엑스트라 트리의 차이점은 부트스트랩 샘플을 사용하지 않는다는 점이다. 즉각 결정 트리를 만들 때 전체 훈련 세트를 사용한다. 대신 노드를 분할할 때 가장 좋은 분할을 찾는 것이 아니라 무작위로 분할 하자! 실은 빼먹지 않고 책의 구석구석을 따라 읽고 실습했다면 이미 여러분은 엑스트라 트리를 조금 맛보았다. 2절의 확인 문제에서 DecisionTreeClassifier의 splitter 매개변수를 'random'으로 지정했다. 엑스트라 트리가 사용하는 결정 트리가 바로 splitter='random'인 결정 트리이다.
하나의 결정 트리에서 특성을 무작위로 분할 한다면 성능이 낮아지겠지만 많은 트리를 앙상블 하기때문에 과대적합을 막고 검증 세트의 점수를 높이는 효과가 있다. 사이킷런에서 제공하는 엑스트라 트리는 ExtraTreeClassifier 이다. 이 모델의 교차 검증 점수를 확인해 보자.
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
랜덤 포레스트와 비슷한 결과를 얻었다. 이 예제는 특성이 많지 않아 두 모델의 차이가 크지 않다. 보통 엑스트라 트리가 무작위성이 좀 더 크기 때문에 랜덤 포레스트보다 더 많은 결정 트리를 훈련해야 한다. 하지만 랜덤하게 노드를 분할하기 때문에 빠른 계산 속도가 엑스트라 트리의 장점이다.
- 결정 트리는 최적의 분할을 찾는 데 시간을 많이 소모한다. 특히 고려해야 할 특성의 개수가 많을 때 더 그렇다. 만약 무작위로 나눈다면 훨씬 빨리 트리를 구성할 수 있다.
엑스트라 트리도 랜덤 포레스트와 마찬가지로 특성 중요도를 제공한다. 순서는 [알코올 도수, 당도, PH]인데, 결과를 보면 엑스트라 트리도 결정 트리보다 당도에 대한 의존성이 작다.
et.fit(train_input, train_target)
print(et.feature_importances_)
[0.20183568 0.52242907 0.27573525]
엑스트라 트리의 회귀 버전은 ExtraTreeRegressor 클래스이다.
네, 좋다. 지금까지 비슷하지만 조금 다른 2개의 앙상블 학습을 알아보았다. 다음에는 이 둘과 다른방식을 사용하는 앙상블 학습을 알아보겠다. 먼저 그레이디언트 부스팅이다.
그레이디언트 부스팅¶
그레이디언트 부스팅 gradient boosting은 깊이가 얕은 결정 트리를 사용하여 이전 트리의 오차를 보완하는 방식으로 앙상블 하는 방법이다. 사이킷런의 GradientBoostingClassifier 는 기본적으로 깊이가 3인 결정트리를 100개 사용한다. 깊이가 얕은 결정 트리를 사용하기 때문에 과대적합에 강하고 일반적으로 높은 일반화 성능을 기대할 수 있다.
그레이디언트란 이름에서 눈치챘을지 모르지만 4장에서 배웠던 경사 하강법을 사용하여 트리를 앙상블레 추가한다. 분류에서는 로지스틱 손실 함수를 사용하고 회귀에서는 평균 제곱 오차 함수를 사용한다.
4장에서 경사 하강법은 손실 함수를 산으로 정의하고 가장 낮은 곳을 찾아 내려오는 과정으로 설명했다. 이때 가장 낮은 곳을 찾아 내려오는 방법은 모델의 가중치와 절편을 조금씩 바꾸는 것이다. 그레이디언트 부스팅은 결정 트리를 계속 추가하면서 가장 낮은 곳을 찾아 이동한다. 혹시 4장에서 손실 함수의 낮은 곳으로 천천히 조금씩 이동해야 한다고 말한 것을 기억하나요? 그레이디언트 부스팅도 마찬가지이다. 그래서 싶이가 얕은 트리를 사용하는 거다. 또 학습률 매개변수로 속도를 조절한다.
그레이디언트 부스팅의 개념에 대해 살펴 보았으니 이제 사이킷런에서 제공하는 GradientBoostingClassifier를 사용해 와인 데이터셋의 교차 검증 점수를 확인해 보자.
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
와우! 거의 과대적합이 되지 않았다. 그레이디언트 부스팅은 결정 트리의 개수를 늘려도 과대적합에 매우 강하다. 학습률을 증가시키고 트리의 개수를 늘리면 조금 더 성능이 향상될 수 있다.
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
결정 트리 개수를 500개로 5배나 늘렸지만 과대적합을 잘 억제하고 있다. 학습률 learning_rate 의 기본값은 0.1이다. 그레이디언트 부스팅도 특성 중요도를 제공한다. 결과에서 볼 수 있듯이 그레이디언트 부스팅이 랜덤 포레스트보다 일부 특성(당도)에 더 집중 한다.
gb.fit(train_input, train_target)
print(gb.feature_importances_)
[0.15872278 0.68010884 0.16116839]
재미있는 매개변수가 하나 있다. 트리 훈련에 사용할 훈련 세트의 비율을 정하는 subsample이다. 이 매개변수의 기본값은 1.0으로 전체 훈련 세트를 사용한다. 하지만 subsampledl 1보다 작으면 훈련 세트의 일부를 사용한다. 이는 마치 경사 하강법 단계마다 일부 샘플을 랜덤하게 선택하여 진행하는 확률적 경사 하강법이나 미니배치 경사 하강법과 비슷하다.
일반적으로 그레이디언트 부스팅 랜덤 포레스트보다 조금 더 높은 성능을 얻을 수 있다. 하지만 순서대로 트리를 추가하기 때문에 훈련 속도가 느리다. 즉 GradientBoostingClassifier에는 n_jobs 매개변수가 없다. 그레이디언트 부스팅의 회귀 버전은 GradientBoostingRegressor이다. 그레이디언트 부스팅의 속도와 성능을 더욱 개선한 것이 다음에 살펴볼 히스토그램 기반 그레이디언트 부스팅이다.
히스토그램 기반 그레이디언트 부스팅¶
히스토그램 기반 그레이디언트 부스팅 Histogram based Gradient Boosting 은 정형 데이터를 다루는 머신러닝 알고리즘 중에 가장 인기가 높은 알고리즘이다. 히스토그램 기반 그레이디언트 부스팅은 먼저 입력 특성을 256개의 구간으로 나눈다. 따라서 노드를 분할할 때 최적의 분할을 매우 빠르게 찾을 수 있다.
히스토그램 기반 그레이디언트 부스팅은 256개의 구간 중에서 하나를 떼어 놓고 누락된 값을 위해서 사용한다. 따라서 입력에 누락된 특성이 있더라도 이를 따로 전처리할 필요가 없다.
사이킷런의 히스토그램 기반 그레이디언트 부스팅 클래스는 HistGradientBoostingClassifier 이다. 일반적으로 HistGradientBoostingClassifier는 기본 매개변수에서 안정적인 성능을 얻을 수 있다. HistGradientBoostingClassifier에는 트리의 개수를 지정하는데 n_estimators 대신에 부스팅 반복 횟수를 지정하는 max_iter를 사용한다. 성능을 높이려면 max_iter 매개변수를 테스트해 보자.
그럼 와인 데이터세에 HistGradientBoostingClassifier 클래스를 적용해 보자. 사이킷런의 히스토그램 기반 그레이디언트 부스팅은 아직 테스트 과정에 있다. 이 클래스를 사용하려면 sklearn.experimental 패키지 아래에 있는 enable_hist_gradient_boosting 모듈을 임포트해야 한다.
from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier
hgb = HistGradientBoostingClassifier(random_state=42)
scores = cross_validate(hgb, train_input, train_target,
return_train_score=True)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))
0.9321723946453317 0.8801241948619236
과대적합을 잘 억제하면서 그레이디언트 부스팅보다 조금 더 높은 성능을 제공한다. 특성 중요를 확인해 보자.
히스토그램 기반 그레이디언트 부스팅의 특성 중요도를 계산하기 위해 permutation_importance() 함수를 사용하겠다. 이 함수는 특성을 하나씩 랜덤하게 섞어서 모델의 성능이 변화하는지를 관찰하여 어떤 특성이 중요한지를 계산한다. 훈련 세트뿐만 아니라 테스트 세트에도 적용할 수 있고 사이킷런에서 제공하는 추정기 모델에 모두 사용할 수 있다.
먼저 히스토그램 기반 그레이디언트 부스팅 모델을 훈련하고 훈련 세트에서 특성 중요도를 계산해보자. n_repeats 매개변수는 랜덤하게 섞을 횟수를 지정한다. 여기서는 10으로 지정하겠다. 기본값은 5이다.
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]
permutation_importance() 함수가 반화하는 객체는 반복하여 얻은 특성 중요도(importances), 평균(importances_mean),표준편차(importances_std)를 담고있다. 평균을 출력해 보면 랜덤 포레스트와 비슷한 비율임을 알 수 있다. 이번에는 테스트 세트에서 특성 중요도를 계산해 보겠다.
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 ]
테스트 세트의 결과를 보면 그레이디언트 부스팅과 비슷하게 조금 더 당도에 집중하고 있다는 것을 알 수 있다. 이런 분석을 통해 모델을 실전에 투입했을 때 어떤 특성에 관심을 둘지 예상할 수 있다.
그럼 HistGradientBoostingClassifier를 사용해 테스트 세트에서의 성능을 최종적으로 확인해보자.
hgb.score(test_input, test_target)
0.8723076923076923
테스트 세트에서는 약 87% 정확도를 얻었다. 실전에 투입하면 성능은 이보다는 조금 더 낮을 것이다. 앙상블 모델은 확실히 단일 결정 트리보다 좋은 결과를 얻을 수 있다.(기억이 나지 않을 수 있는데 2절의 랜덤 서치에서 테스트 정확도는 86%였다.)
히스토그램 기반 그레이디언트 부스팅의 회귀 버전은 HistGradientBoostingRegressor 클래스에 구현되어 있다. 사이킷런에서 제공하는 히스토그램 기반 그레이디언트 부스팅이 비교적 새로운 기능이다. 하지만 사이킷런 말고도 히스토그램 기반 그레이디언트 부스팅 알고리즘을 구현한 라이브러리가 여럿 있다.
가장 대표적인 라이브러리는 XGBoost 이다. 놀랍게도 이 라이브러리도 코랩에서 사용할 수 있을 뿐만 아니라 사이킷런의 cross_validate()함수와 함께 사용할 수도 있다. XGBoost는 다양한 부스팅 알고리즘을 지원한다. tree_method 매개변수를 'hist'로 지정하면 히스토그램 기반 그레이디언트 부스팅을 사용할 수 있다. 그럼 XGBoost를 사용해 와인 데이터의 교차 검증 점수를 확인해 보자.
from xgboost import XGBClassifier
xgb = XGBClassifier(tree_method='hist', random_state=42)
scores = cross_validate(xgb, train_input, train_target,
return_train_score=True)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))
/home/ubuntu/.local/lib/python3.6/site-packages/xgboost/sklearn.py:1146: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1]. warnings.warn(label_encoder_deprecation_msg, UserWarning) /home/ubuntu/.local/lib/python3.6/site-packages/xgboost/sklearn.py:1146: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1]. warnings.warn(label_encoder_deprecation_msg, UserWarning) /home/ubuntu/.local/lib/python3.6/site-packages/xgboost/sklearn.py:1146: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1]. warnings.warn(label_encoder_deprecation_msg, UserWarning)
[18:31:16] WARNING: /tmp/pip-build-2kqwo3v0/xgboost/build/temp.linux-x86_64-3.6/xgboost/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior. [18:31:16] WARNING: /tmp/pip-build-2kqwo3v0/xgboost/build/temp.linux-x86_64-3.6/xgboost/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior. [18:31:16] WARNING: /tmp/pip-build-2kqwo3v0/xgboost/build/temp.linux-x86_64-3.6/xgboost/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior. [18:31:16] WARNING: /tmp/pip-build-2kqwo3v0/xgboost/build/temp.linux-x86_64-3.6/xgboost/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior. [18:31:17] WARNING: /tmp/pip-build-2kqwo3v0/xgboost/build/temp.linux-x86_64-3.6/xgboost/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior. 0.9555033709953124 0.8799326275264677
/home/ubuntu/.local/lib/python3.6/site-packages/xgboost/sklearn.py:1146: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1]. warnings.warn(label_encoder_deprecation_msg, UserWarning) /home/ubuntu/.local/lib/python3.6/site-packages/xgboost/sklearn.py:1146: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1]. warnings.warn(label_encoder_deprecation_msg, UserWarning)
from xgboost import XGBClassifier
xgb = XGBClassifier(tree_method='hist', random_state=42)
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.9555033709953124 0.8799326275264677
널리 사용하는 또 다른 히스토그램 기반 그레이디언트 부스팅 라이브러리는 마이크로소프트에서 만든 LightGBM이다.LightGBM은 빠르고 최신 기술을 많이 적용하고 있어 인기가 점점 높아지고 있다. LightGBM도 코랩에 이미 설치되어 있어 바로 테스트해 볼 수 있다.
#!pip3 install 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
사실 이 사이킷런의 히스토그램 기반 그레이디언트 부스팅이 LightGBM에서 영향을 많이 받았다. 이제 히스토그램 기반 그레이디언트 부스팅까지 4개의 앙상블을 모두 다루어 보았다.
좀 더 열정적인 독자에게
- LightGBM: https://lightgbm.readthedocs.io/en/latest
앙상블 학습을 통한 성능 향상 - 문제해결 방식¶
앙상블 학습은 정형 데이터에서 가장 뛰어난 성능을 내는 머신러닝 알고리즘 중 하나이다. 대표적인 앙상블 학습은 다음과 같다.
사이킷런
- 랜덤포레스트: 부트스트랩 샘플사용. 대표 앙상블 학습 알고리즘임
- 엑스트라 트리: 결정 트리의 노드를 랜덤하게 분할함
- 그레이디언트 부스팅: 이진 트리의 손실을 보완하는 식으로 얕은 결정 트리를 연속하여 추가함
- 히스토그램 기반 그레이디언트 부스팅: 훈련 데이터를 256개 정수 구간으로 나누어 빠르고 높은 성능을 냄
그외 라이브러리
- XGBoost
- LightGBM
이번 절에서는 앙상블 학습을 배웠다. 결정 트리 기반의 앙상블 학습은 강력하고 뛰어난 성능을 제공하기 때문에 인기가 아주 높다. 사이킷런에서 제공하는 앙상블 학습 알고리즘 중 랜덤 포레스트, 엑스트라 트리, 그레이디언트 부스팅, 히스토그램 기반 그레이디언트 부스팅을 다루었다.
랜덤포레스트는 가장 대표적인 앙상블 학습 알고리즘이다. 성능이 좋고 안정적이기 때문에 첫 번째로 시도해 볼 수 있는 앙상블 학습 중 하나이다. 랜덤포레스트는 결정 트리를 훈련하기 위해 부트스트랩 샘플을 만들고 전체 특성 중 일부를 랜덤하게 선택하여 결정 트리를 만든다.
엑스트라 트리는 랜덤 포레스트와 매우 비슷하지만 부트스트랩 샘플을 사용하지 않고 노드를 분할할때 최선이 아니라 랜덤하게 분할 한다. 이런 특징 때문에 랜덤 포레스트보다 훈련 속도가 빠르지만 보통 더 많은 트리가 필요하다.
그레이디언트 부스팅은 깊이가 얕은 트리를 연속적으로 추가하여 손실 함수를 최소화하는 앙상블 방법이다. 성능이 뛰어나지만 병렬로 훈련할 수 없기 때문에 랜덤 포레스트나 엑스트라 트리보다 훈련 속도가 조금 느리다. 그레이디언트 부스팅에서 학습률 매개변수 조정하여 모델의 복잡도를 제어할 수 있다. 학습률 매개변수가 크면 복잡하고 훈련세트에 과대적합된 모델을 얻을 수 있다.
끝으로 가장 뛰어난 앙상블 학습으로 평가받는 히스토그램 기반 그레이디언트 부스팅 알고리즘을 살펴보았다. 히스토그램 기반 그레이디언트 부스팅은 훈련 데이터를 256개의 구간으로 변환하여 사용하기 때문에 노드 분할 속도가 매우 빠르다. 코랩에는 사이킷런뿐만 아니라 히스토그램 기반 그레이디언트 부스팅 라이브러리인 XGBoost와 LightGBM이 이미 설치되어 있어 바로 시험해 볼 수있다.
이 절에서 다양한 앙상블 학습 방법을 배워 보았다. 앙상블 학습과 그리드 서치, 랜덤 서치를 사용한 하이퍼파라미터 튜닝을 사용하면 최고 수준의 성능을 내는 머신러닝 모델을 얻을 수 있다.
지금까지는 입력과 타깃이 준비된 문제를 풀었다. 이런 머신러닝 분야를 지도학습 supervised learning 이라고 부른다. 타깃이 없다면 어떨까? 이때에도 유용한 무언가를 학습할 수 있을까? 다음 장에서 이에 대해 배워보겠다.
전체 소스 코드
- https://bit.ly/hg-05-3 에 접속하면 코랩에서 이 절의 코드를 바로 열어 볼 수 있다.
트리의 앙상블
랜덤포레스트
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
wine = pd.read_csv('https://bit.ly/wine_csv_data')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size=0.2, random_state=42)
from sklearn.model_selection import cross_validate
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
rf.fit(train_input, train_target)
print(rf.feature_importances_)
[0.23167441 0.50039841 0.26792718]
rf = RandomForestClassifier(oob_score=True, n_jobs=-1, random_state=42)
rf.fit(train_input, train_target)
print(rf.oob_score_)
0.8934000384837406
엑스트라트리
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]
그레이디언트 부스팅
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
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.15872278 0.68010884 0.16116839]
히스토그램 기반 부스팅
from sklearn.experimental import enable_hist_gradient_boosting
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
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
from xgboost import XGBClassifier
xgb = XGBClassifier(tree_method='hist', random_state=42)
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.9555033709953124 0.8799326275264677
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
- 출처: 혼자 공부하는 머신러닝 + 딥러닝
'혼자공부하는 머신러닝 + 딥러닝' 카테고리의 다른 글
Chapter 06. 비지도 학습(주성분 분석) (0) | 2021.05.29 |
---|---|
Chapter06. 비지도 학습 (군집알고리즘, k-평균) (0) | 2021.05.29 |
Chapter04. 다양한 분류 알고리즘 (0) | 2021.05.27 |
Chapter03.회귀 알고리즘과 모델 규제 (0) | 2021.05.25 |
Chapter 02 데이터 다루기 (훈련세트와 테스트 세트 / 데이터 전처리) (0) | 2021.05.17 |