[혼공머신]_4주차 트리 알고리즘
1. 로지스틱 회귀로 와인 분류하기
먼저, 와인 데이터를 불러오자
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 |
만약, 데이터를 잘 불러왔다면, 처음 열 3개에는 각각 알콜의 도수, 당도, ph 값을 나타낸다.
네번째 열은 타깃값으로 0이면 레드와인, 1이면 화이트 와인이다. 레드와 화이트 와인 중 어떤 것인지 구분하는 이진 분류 문제이며, 화이트 와인이 양성 클래스이다.
즉, 전체 와인에서 화이트 와인이 어떤 것인지 골라내는 문제이다.
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
위 처럼 info 라는 매서드를 사용하면, 쉽게 누락된 값이 있는 지 확인할 수 있다.
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 |
또한, describe() 메서드를 이용하면, 열에 대한 간략한 통계를 출력할 수 있다.
그리고 이 결과를 통해 알코올 도수와 당도, PH 값의 스케일이 다르다는 것을 알 수 있다.
따라서, 먼저 데이터 프레임을 넘파이 배열로 바꾸고 훈련 세트와 테스트 세트로 나눈 후에 StandardScaler 클래스를 사용해 특성을 표준화하자
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)
#rain_test_split() 함수는 설정값을 지정하지 않으면 25%를 테스트 세트로 지정한다.
#샘플개수가 충분히 많으므로 20% 정도만 테스트 세트로 나누었다.
#코드의 test_size=0.2가 이런 의미이다.
print(train_input.shape, test_input.shape)
# 결과는 : (5197, 3) (1300, 3)
이제 아까 살펴보았듯이 스케일이 달랐기 때문에, 이제 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
2. 설명하기 쉬운 모델과 어려운 모델
로지스틱 회귀가 학습한 계수와 절편을 출력해 보자.
print(lr.coef_, lr.intercept_)
#결과 : [[ 0.51270274 1.6733911 -0.68767781]] [1.81777902]
결과를 살펴보면, 사실 이 모델이 왜 저런 계수 값을 학습했는지 정확히 이해하기 어렵다. 아마도 알코올 도수와 당도가 높을수록 화이트 와인일 가능성이 높고, PH가 높을수록 레드 와인일 가능성이 높은 것 같다.
하지만 정확히 이 숫자가 어떤 의미인지 설명하긴 어렵다. 더군다나 다항 특성을 추가한다면 설명하기가 더 어려울 것이다. 대부분 머신러닝 모델은 이렇게 학습의 결과를 설명하기 어렵다. 쉬운 방법으로 설명할 수 있는 모델을 알아보자.
결정 트리
결정 트리(Decision Tree, 의사결정트리 )는 분류(Classification)와 회귀(Regression) 모두 가능한 지도 학습 모델 중 하나이다. 결정 트리는 스무고개와 같이 질문을 하나씩 던져 정답을 맞춰가며 학습하는 알고리즘이다.
따라서, 결정 트리 모델이 이유를 설명하기가 쉽다. 데이터를 잘 나눌 수 있는 질문을 찾는다면 계속 질문을 추가해서 분류 정확도를 높일 수 있다. 사이킷런의 DecisionTreeClassifier 클래스를 사용해 결정 트리 모델을 훈련해 보자. fit() 메서드를 호출해서 모델을 훈련한 다음 score() 메서드로 정확도를 평가해 보자.
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() 함수를 사용해 결정 트리를 이해하기 쉬운 트리 그림으로 출력해 보자.
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
plt.figure(figsize=(10,7))
plot_tree(dt)
plt.show()
이렇게 특정 기준(질문)에 따라 데이터를 구분하는 모델을 결정 트리 모델이라고 부르고, 한번의 분기 때마다 변수 영역을 두 개로 구분한다. 결정 트리에서 질문이나 정답을 담은 네모 상자를 노드(Node)라고 부르고, 맨 처음 분류 기준 (즉, 첫 질문)을 Root Node라고 하고, 맨 마지막 노드를 Terminal Node 혹은 Leaf Node라고 한다.
너무 복잡하니 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 이하인지 질문을 한다. 만약 어떤 샘플의 당도가 -0.239와 같거나 작으면 왼쪽 가지로 이동한다. 그렇지 않으면 오른쪽으로 이동한다. 왼쪽이 yes 오른쪽이 no이다. 루트 노드의 총 샘플수는 5197개이다. 이 중에서 음성 클래스(레드와인)이 1258개이고, 양성 클래스(화이트 와인)는 3939개 이다. 이 값이 value에 나타나 있다.
이어서 왼쪽 노드를 살펴보자. 이 노드는 sugar가 더 낮은지 물어본다. -0.802 보다 같거나 낮으면 다시 왼쪽 가지로, 그렇지 않으면 오른쪽 가지로 이동한다. 이 노드에서 음성 클래스와 양성 클래스의 샘플 개수는 각각 1177개와 1745개이다. 루트 노드보다 양성 클래스, 즉 화이트 와인의 비율이 크게 줄었다. 그 이유는 오른쪽 노드를 보면 알 수 있다.
오른쪽 노드는 음성 클래스가 81개, 양성 클래스가 2194개로 대부분의 화이트 와인 샘플이 이 노드로 이동했다. 결정 트리에서 예측하는 방법은 간단하다. 리프 노드에서 가장 많은 클래스가 예측 클래스가 된다. 앞에서 보았던 k-최근접 이웃과 매우 유사하다. 만약 이 결정트리의 성장을 여기서 멈춘다면 왼쪽 노드에 도달한 샘플과 오른쪽 노드에 도달한 샘플은 모두 양성 클래스로 예측된다. 두 노드 모두 양성 클래스의 개수가 많기 때문이다.
이번엔 노드 상자 안에 있는 gini에 대해 알아보자.
지니 불순도
바로 위에 있는 루트 노드의 지니값이 어떻게 나오게 된 것인지 함께 살펴보자면, 루트 노드에 총 5,197개의 샘플이 있고, 그 중에 1,258개가 음성 클래스 3,939개가 양성 클래스이다. 따라서 아래와 같이 계산해보면 지니 불순도를 알 수 있다.
여기서, 만약 100개의 샘플이 있는 어떤 노드의 두 클래스 비율이 정확히 1/2씩이면, 지니불순도는 0.5가 되아 최악이 되고, 반대로 노드에 하나의 클래스만 있다면 지니불순도는 0이 되어 가장 작아진다.
위에 그림을 참고하면 쉽게 이해할 수 있다.
결정트리 모델에서는 부모 노드와 자식 노드의 불순도가 가능한 크도록 트리를 성장시킨다.
계산하는 방법을 함께 알아보자면 아래와 같다.
이렇게 부모와 자식 노드 사이의 불순도 차이를 정보 이득 이라고 한다.
사이킷런에서는 또 다른 불순도 기준이 있는데 DecisionTreeClassifier 클래스에서 criterion = ‘entropy’를 지정하여 엔트로피 불순도를 사용할 수 있다.
엔트로피 불순도는 아래와 같이 계산할 수 있다.
예를 들면,
따라서 보통 기본값인 지니 불순도와 엔트로피 불순도가 만든 결과의 차이는 크지 않다.
그렇기 때문에 그냥 기본값인 지니 불순도를 계속 사용해서 설명하도록 하겠다.
가지치기
예를 들어 열매를 잘 맺기 위해 과수원에서 가지치기를 하는 것처럼 결정 트리도 가지치기를 해야 한다. 그렇지 않으면 무작정 끝까지 자라나는 트리가 만들어진다. 훈련 세트에는 아주 잘 맞겠지만 테스트 세트에서 점수는 그에 못 미칠 것이다. 이것을 일반화가 잘 되지 않았다고 한다.
이제 가지치기를 해보자. 결정 트리에서 가지치기를 하는 가장 간단한 방법은 자라날 수 있는 트리의 최대 깊이를 지정하는 것이다. DecisionTreeClassifier 클래스의 max_depth 매개변수를 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()
그리게 되면 위에 처럼 나온다.
앞서 불순도를 기준으로 샘플을 나눈다고 했다. 불순도는 클래스별 비율을 가지고 계산하였다. 샘플을 어떤 클래스 비율로 나누는지 계산할 때 특성값의 스케일이 계산에 영향을 미칠까?
아니다. 특성값의 스케일은 결정 트리 알고리즘에 아무런 영향을 미치지 않는다. 따라서 표준화 전처리를 할 필요가 없다. 이것이 결정 트리 알고리즘의 또 다른 장점 중 하나이다.
그럼 앞서 전처리하기 전의 훈련 세트와 테스트 세트로 결정 트리모델을 다시 훈련해 보자.
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를 사용했기 때문에 아마도 sugar가 가장 유용한 특성중 하나일 것 같다. 특성 중요도는 결정 트리 모델의 featureimportances 속성에 저장되어 있다. 이 값을 출력해 보자.
print(dt.feature_importances_)
#결과 :[0.12345626 .86862934 0.0079144 ]
역시 두 번째 특성인 sugar가 0.87 정도로 특성 중요도가 가장 높다. 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산한다. 특성 중요도를 활용하면 결정 트리 모델을 특성 선택에 활용할 수 있다. 이것이 결정 트리 알고리즘의 또 다른 장점중 하나이다.
교차 검증과 그리드 서치 (Ch.5 - 2)
검증세트
테스트 세트를 사용하지 않으면 모델이 과대적합인지 과소적합인지 판단하기 어렵기 때문에, 테스트 세트를 사용하지 않고 이를 측정하는 간단한 방법은 훈련 세트를 또 나누는 것이다. 이 데이터를 검증 세트라고 부른다.
앞서 전체 와인 데이터중 20%를 테스트 세트로 만들고 80%를 훈련 세트로 만들었다. 이 훈련 세트중에서 다시 20%를 떼어 내어 검증 세트로 만든다.
훈련 세트에서 모델을 훈련하고 검증 세트로 모델을 평가한다. 이런 식으로 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 고른다. 그다음 이 매개변수를 사용해 훈련 세트와 검증 세트를 합쳐 전체 훈련 데이터에서 모델을 다시 훈련한다. 그리고 마지막에 테스트 세트에서 최종 점수를 평가한다.
아까 사용했던 데이터를 다시 불러오자.
import pandas as pd
wine = pd.read_csv('/home/jaeyoon89/hg-mldl/wine.csv')
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)
이 과정 다음에 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)
크기를 확인해 보자.
print(sub_input.shape, val_input.shape)
#결과 : (4157, 3) (1040, 3)
원래 5197개 였던 훈련 세트가 4157개로 줄고 검증 세트는 1040개가 되었다. 이제 모델을 만들고 평가해 보자.
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_validate() 라는 교차 검증 함수가 있다. 먼저 평가할 모델 객체를 첫 번째 매개변수로 전달한다. 그다음 앞에서처럼 직접 검증 세트를 떼어내지 않고 훈련 세트 전체를 cross_validate() 함수에 전달한다.
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
# 결과 : {'fit_time': array([0.00835681, 0.00643492, 0.00545597, 0.00457048, 0.00498867])
#, 'score_time': array([0.00071645, 0.00080824, 0.0004096 , 0.00037885, 0.00034475])
#, 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
cross_validate() 함수는 fit_time,score_time,test_score 키를 가진 딕셔너리를 반환한다. 처음 2개의 키는 각각 모델을 훈련하는 시간과 검증하는 시간을 의미한다. 각 키마다 5개의 숫자가 담겨있다. cross_validate() 함수는 기본적으로 5-폴드 교차 검증을 수행하지만, cv 매개변수에서 폴드 수를 바꿀 수도 있다.
교차 검증의 최종 점수는 test_score 키에 담긴 5개의 점수를 평균하여 얻을 수 있다.
import numpy as np
print(np.mean(scores['test_score']))
#결과 : 0.855300214703487
교차 검증을 수행하면 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해 볼 수 있다. 한 가지 주의할 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않는다.
앞서 우리는 train_test_split() 함수로 전체 데이터를 섞은 후 훈련 세트를 준비했기 때문에 따로 섞을 필요가 없다. 하지만 만약 교차 검증을 할 때 훈련 세트를 섞으려면 분할기를 지정해야 한다.
사이킷런의 분할기는 교차 검증에서 폴드를 어떻게 나눌지 결정해 준다. 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
하이퍼파라미터 튜닝
[하이퍼파라미터를 튜닝하는 과정]
- 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련
- 검증세트의 점수나 교차 검증을 통해서 매개변수를 조금씩 바꿔본다.
- 하지만 이럴 경우, 매개변수가 많아지면 각 값을 하나씩 변경하여 무한히 많이 반복을 돌려야 하므로 일일이 구하기 어려울 수 있다.
- AutoML : 사람의 개입 없이 하이퍼파라미터튜닝을 자동으로 수행하는 기술
- 이를 해결하는 도구 중 하나가 **그리드서치(GridSearch)**이다.
< 그리드 서치 >
#GridSearchCV를 임포트
from sklearn.model_selection import GridSearchCV
#찾고자 하는 매개변수의 최적값 리스트를 딕셔너리로 변경
params = {'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}
#GridSearchCV클래스에 탐색 대상 모델과 params 변수를 전달하여 그리드 서치 객체를 만든다.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)
GridSearchCV의 cv 매개변수 기본값은 5이다, 따라서 찾고자 하는 매개변수의 값마다 5-폴드 교차검증을 수행한다.
- 즉, 총 25개의 모델을 훈련하는 것이다.
- 많은 모델을 훈련하기 때문에 n_jobs매개변수를 사용해 병렬실행에 사용할 CPU코어수를 정할 수 있다. 이떄 n_jobs를 '-1'로 지정하면 모든 코어를 사용하게 된다.
dt = gs.best_estimator_
print(dt.score(train_input, train_target))
print(gs.best_params_)
- 그리드 서치는 훈련이 끝나면 검증 점수가 가장 높은 모델의 매개변수 조합으로 지정해 전체 훈련세트에서 자동으로 다시 훈련한다. 이때 사용되는 매개변수는 best_estimator_에 저장되어 있다.
- 그리드 서치로 찾은 최적의 매개변수는 best_params_에 저장되어 있다.
dt = gs.best_estimator_
print(dt.score(train_input, train_target))
매개변수는 하나가 아니라 여러개를 사용해서 사용이 가능하다.
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)
print(gs.best_params_)
[그리드 서치 과정 정리]
- 탐색할 매개변수 목록 지정
- 훈련 세트에서 그리드서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾음 => 이 매개변수 조합은 그리드 서치 객체에 저장됨.
- 저장된 최상의 매개변수로 교차검증에 사용한 훈련세트가 아닌 전체 훈련 세트를 사용해 최종모델 훈련 => 이 역시 그리드 서치 객체에 저장됨.
랜덤 서치
매개변수의 값이 수치일 때 값의 범위나 간격을 미리 정하기 어려울 수 있다. 또 너무 많은 매개변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있다. 이럴 때 랜덤 서치를 사용하면 좋다.
랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아니라 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다. 먼저 싸이파이에서 2개의 확률 분포 클래스를 임포트하자.
from scipy.stats import uniform, randint
uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑는다. 이를 균등 분포에서 샘플링한다 라고 한다. randint는 정숫값을 뽑고 uniform은 실수값을 뽑는다.
rgen = randint(0,10)
rgen.rvs(10)
#결과 : array([9, 0, 1, 3, 4, 8, 8, 2, 1, 3])
이번엔 1000개를 샘플링해서 각 숫자의 개수를 세어보자.
np.unique(rgen.rvs(1000), return_counts=True)
#결과 : (array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
# array([ 95, 98, 108, 92, 104, 102, 88, 120, 106, 87]))
개수가 늘어나니 0에서 9까지의 숫자가 어느 정도 고르게 추출된 것을 볼 수 있다. uniform 클래스의 사용법도 동일하다. 0 ~ 1 사이에서 10개의 실수를 추출해 보자.
ugen = uniform(0,1)
ugen.rvs(10)
#결과 : array([0.00585314, 0.20742187, 0.75448161, 0.67021122, 0.00958848,
# 0.13415657, 0.32193048, 0.08565726, 0.18960181, 0.07269254])
이제 탐색할 매개변수의 딕셔너리를 만들어 보자. 여기에서는 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),
}
샘플링 횟수는 사이킷런의 랜덤 서치 클래스인 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 0x7fc7fe22c828>,
# 'min_impurity_decrease': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fc7fe22c400>,
# 'min_samples_leaf': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fc7fe22c7b8>,
# 'min_samples_split': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fc7fe22c160>},
# random_state=42)
위 params에 정의된 매개변수 범위에서 총 100번을 샘플링하여 교차 검증을 수행하고 최적의 매개변수 조합을 찾는다. 앞서 그리드 서치보다 훨씬 교차 검증 수를 줄이면서 넓은 영역을 효과적으로 탐색할 수 있다. 먼저 최적의 매개변수 조합을 출력하자.
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
최적의 모델은 이미 전체 훈련 세트로 훈련되어 best_estimator_속성에 저장되어 있다. 이 모델을 최종으로 테스트 세트의 성능을 확인해 보자.
dt = gs.best_estimator_
print(dt.score(test_input, test_target))
#결과 : 0.86
트리의 앙상블(Ch.5 - 3)
정형 데이터와 비정형 데이터
정형데이터는 쉽게 말해 어떤 구조로 되어있는 이런 데이터는 CSV나 데이터베이스, 혹은 엑셀에 저장하기 쉽다. 이와 반대되는 데이터를 비정형 데이터라고 부른다. 비정형 데이터는 데이터베이스나 엑셀로 표현하기 어려운 것들이다. 예를 들면 텍스트 데이터, 카메라로 찍은 사진, 핸드폰으로 듣는 디지털 음악 등이 있다.
지금까지 배운 머신러닝 알고리즘은 정형 데이터에 잘 맞는다. 그중에 정형 데이터를 다루는데 가장 뛰어난 성과를 내는 알고리즘이 앙상블 학습이다. 이 알고리즘은 대부분 결정 트리를 기반으로 만들어져 있다.
랜덤 포레스트
랜덤 포레스트는 앙상블 학습의 대표 주자 중 하나로 안정적인 성능 덕분에 널리 사용되고 있다. 이름 자체로 유추할 수 있듯 랜덤 포레스트는 결정 트리를 랜덤하게 만들어 결정 트리로 이루어진 숲을 만든다. 그리고 각 결정 트리의 예측을 이용해 최종 예측을 만든다.
랜덤 포레스트는 각 트리를 훈련하기 위한 데이터를 랜덤하게 만드는데 이때 부트스트랩 샘플을 사용한다.
부트스트랩 샘플
- 전체 세트에서 뽑았던 샘플을 다시 넣어서 복원 추출(중복 허용) 을 하게 하는 것. 이 경우 중복이 적용된다.
- 보통 부트 스트랩 방식이라고 한다.
- 기본적으로 부트스트랩 샘플은 훈련 세트의 크기와 같게 만든다. 1000개 가방에서 중복하여 1000개의 샘플을 뽑기 때문에 부트스트랩 샘플은 훈련 세트와 크기가 같다.
또한 각 노드를 분할할때 전체 특성 중 일부 특성을 무작위로 고른 다음 이 중에서 최선의 분할을 찾는다.
- 분류 모델인 RandomForestClassifier는 기본적으로 전체 특성 개수의 제곱근 만큼 특성을 선택한다.
- 다만 회귀 모델인 RandomForestRegressor는 전체 특성을 사용한다.
랜덤 포레스트는 랜덤하게 선택한 샘플과 특성을 사용하기 때문에 훈련 세트에 과대적합되는 것을 막아주고 검증 세트와 테스트 세트에서 안정적인 성능을 얻을 수 있다. 기본 매개변수만으로도 아주 좋은 결과를 낸다.
실습코드
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']))
코드의 사용방법은 다른 학습모델과 큰 차이가 없다.
- 랜덤 포레스트는 결정 트리의 앙상블이기 때문에 특성 중요도를 계산해서 출력할 수 있다.
# 특성 중요도 계산
rf.fit(train_input, train_target)
print(rf.feature_importances_)
- RandomForestClassifier에는 자체적으로 모델을 평가하는 점수를 얻을 수 있다.
- 부트스트래핑을 할때,부트스트랩 샘플에 포함되지 않고 남은 샘플(=OOB샘플)로 훈련한 결정트리를 평가할 수 있다.(이때 OOB샘플은 검증 세트의 역할을 하는 셈이다) 이 점수를 얻으려면 매개변수 oob_score를 True로 지정해야 한다.
rf = RandomForestClassifier(oob_score=True, n_jobs=-1, random_state=42)
rf.fit(train_input, train_target)
print(rf.oob_score_)
엑스트라 트리
랜덤 포레스트와 비슷하게 동작하지만, 엑스트라 트리는 부트스트랩 샘플을 사용하지 않고 결정트리를 만들때 전체 훈련 세트를 사용한다.
- 가장 좋은 분할을 찾지 않는 대신 노드를 분할할때 무작위로 분할하게 된다(spliter=random)
- 성능은 낮아질 수 있지만 많은 트리를 앙상블 하기 때문에 과대적합을 막고 검증세트의 점수를 높이는 효과가 있다.
- 사이킷런에서 제공하는 엑스트라 트리는 ExtraTreesClassifier이다.
- 그 외 매개변수는 랜덤포레스트와 거의 동일하다.
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']))
그레이디언트 부스팅
깊이가 얕은 결정 트리를 사용하여 이진트리의 오차를 보완하는 방식으로 앙상블 하는 방법
랜덤포레스트나 엑스트라 트리와 달리 결정 트리를 연속적으로 추가하여 손실함수를 최소화 하는 앙상블 방법
- 경사하강법을 사용하여 트리를 앙상블에 추가하며, 분류에서는 로지스틱 손실 함수를, 회귀에서는 평균 제곱 오차 함수를 사용한다.
- 일반적으로 그레이디언트 부스팅이 랜덤 포레스트보다 조금 더 높은 성능을 얻을 수 있다. 하지만 순서대로 트리를 추가하기 때문에 훈련 속도가 느리다.(그래서 GradientBoostingClassifier에는 n_jobs가 없다.)
- 사이킷런에서 제공하는 그레이디언트 부스팅 클래스는 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']))
- 결정 트리에는 아래와 같은 매개변수들이 있다.
- n_estimators : 사용할 결정 트리의 개수, 기본값은 100이다.
- learning_rate: 학습률, 기본값은 0.1이다.
히스토그램 기반 그레이디언트 부스팅
- 입력 특성을 256개의 구간으로 나누고, 그 구간중에서 하나를 떼어놓고 누락된 값을 위해서 사용한다.
- 그레이디언트 부스팅의 속도를 개선한 것이 히스토그램 기반 그레이디언트 부스팅, 따라서 대개 안정적인결과와 좋은 성능을 보여줌
- 사이킷런에서 제공하는 히스토그램 기반 그레이디언트 부스팅 클래스는 HistGradientBoostingClassifier이다.
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']))
- 히스토그램 기반 그레이디언트 부스팅의 회귀버전은 HistGradientBoostingRegressor에 구현되어 있다.
- 사이킷런 말고도 히스토그램 기반 그레이디언트 부스팅 알고리즘을 구현한 라이브러리로 XGBoost, LightGBM등이 있다.
XGBOOST
여러개의 Decision Tree를 조합해서 사용하는 앙상블 알고리즘
- XGBoost에서 tree_method를 hist로 지정하면 히스토그램 기반 그레이디언트 부스팅 알고리즘을 쓸 수 있다.
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']))
Light GBM
LightGBM의 경우에는 최대 손실값을 가지는 노드를 중심으로 계속해서 분할하는 '리프 중심 트리 분할(leaf-wise)' 방식을 사용한다.
- 따라서 트리가 깊어지기위해 소요되는 시간과 메모리를 많이 절약할 수 있다.
- 다만 적은 데이터에 대한 과적합(overfitting)이 발생하기 쉽다
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']))
기본 미션
선택 미션 :