데이터 & AI/혼자공부하는 머시런닝 딥러닝

혼공 머신러닝 딥러닝 CH)05-1

뭉실뭉실뜬구름 2023. 5. 30. 17:37
728x90
 

결정 트리

결정 트리 알고리즘을 사용해 분류 문제를 다루어 본다. 결정 트리가 머신러닝 문제를 어떻게 해결하는지 이해한다.

  • problem : 캔에 인쇄된 알코올 도수, 당도, ph 값으로 와인 종류를 구별해야 한다.

로지스틱 회귀로 와인 분류하기

  • 6,497개의 와인 샘플 분류하기
import pandas as pd 
wine = pd.read_csv('https://bit.ly/wine_csv_data') 
wine.head()
 
alcohol                                    sugar                                   pH                                                  class
9.4 1.9 3.51 0.0
9.8 2.6 3.20 0.0
9.8 2.3 3.26 0.0
9.8 1.9 3.16 0.0
9.4 1.9 3.51 0.0
  • 4번 째 열의 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
  • 만약 누락된 값이 있다면 그 데이터를 버리거나 평균값으로 채운 후 사용할 수 있다.
  • 어떤 방식이 최선인지는 미리 알기 어렵다. 두 가지 모두 시도해 봐야 한다.
# describe()메서드 : 이 메서드는 열에 대한 간략한 통계를 출력해 준다. 최소,최대,평균값 등을 볼 수 있다. 이 메서드를 호출해 보겠다.
wine.describe()
 
 
  • 통계값을 보면 각 특성별로 스케일이 다르다. 여기서도 사이킷런의 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)
  • train_test_split() 함수는 기본값으로 25%를 테스트 세트로 지정한다.
  • 샘플 개수가 충분히 많아 20% 정도만 테스트 세트로 나누겠다. test_size가 그걸 의미한다
 
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
  • 점수가 높지 않다. 모델이 과소적합된 것 같다.
  • 이 문제를 해결하기 위해 규제 매개변수 C의 값을 바꿔 볼까요 아니면
  • solver 매개변수에서 다른 알고리즘을 선택할 수도 있다. 또는
  • 다항 특성을 만들어 추가할 수도 있다.

설명하기 쉬운 모델과 어려운 모델

지금 훈련한 모델을 보고서로 작성한다고 생각해보자 이 모델은 알코올 도수 값에 0.51270274를 곱하고, 당도에 1.6733911을 곱하고 pH 값에 -0.68767781을 곱한 다음 모두 더하고 마지막으로 1.81777902를 더한다. 이값이 0보다 크면 화이트 와인 작으면 레드 와인이다. 현재 이 모델을 테스트 결과 약 77% 정도를 정확히 화이트 와인으로 분류했다.

근데 이걸 잘 설명할 수 있나? 도수와 당도가 높을수록 화이트 와인일 가능성이 높다정도이다...숫자가 의미하는걸 정확하게 설명하기 어렵다. 만약 여기에 다항 특성을 추가한다면 더 설명하기 어려울 것이다.

 
print(lr.coef_,lr.intercept_)
>>[[ 0.51270274  1.6733911  -0.68767781]] [1.81777902]

결정 트리

결정트리 모델이 이유를 설명하기 쉽다. 데이터를 잘 나눌 수 있는 질문을 찾는다면 곗고 질문을 추가해서 분류 정확도를 높일 수 있다. 사이킷런의 DecisionTreeClassifier클래스를 사용해 결정 트리 모델을 훈련할 수 있다.

  • 사이킷런의 결정 트리 알고리즘은 노드에서 최적의 분할을 찾기 전에 특성의 순서를 섞는다. 따라서 약간의 무작위성이 주입되는데 실행할 때마다 점수가 조금씩 달라질 수 있다. 그래서 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()함수를 사용해 결정 트리를 이해하기 쉬운 트리 그림으로 출력해 준다.
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
plt.figure(figsize=(10,7)) 
plot_tree(dt)
plt.show()
  • 너어무 복잡한 트리가 만들어졌다
  • 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()

만약 어떤 샘플의 당도가 -0.239보다 이하이면 왼쪽 가지로 아니면 오른쪽 가지로 이동한다. 왼쪽은 레드 와인 오른쪽 양성클래스는 화이트 와인이다.

총 5197 샘플 중 음성 클래스는 1258개이고 양성 클래스는 3939개이다.

왼쪽 노드를 살펴보면 당도 -0.802를 기준으로 다시 구분한다. 여기서 음성은 1177개 나머지는 양성이다.루트노드보다 양서 클래스 즉 화이트 화인 비율이 크게 줄었다.

 

오른쪽 노드는 음성 81개 나머지는 양성으로 대부분의 화이트 와인 샘플이 이 노드로 이동했다.

노드의 바탕색깔을 잘보면 오른쪽 노드가 루트 노드보다도 더 진하다. filled=True를 지정했기 때문에 클래스마다 색깔을 부여하고 어떤 클래스의 비율이 높아지면 점점 진한 색으로 표시한다.

 

결정트리에서 예측하는 방법은 아주 간단하다. 리프 노드에서 가장 많은 클래스가 예측 클래스가 된다. 앞에서 보았던 k-최근접 이웃과 매우 비슷하다.

 

만약 이 결정 트리가 여기서 멈추면 두 노드 모두 양성 클래스의 개수가 더 많아서 모두 양성 클래스로 예측한다.

불순도

gini는 지니 불순도를 의미한다. DecisionTreeClassifier 클래스의 criterion 매개변수의 기본값이 gini이다.

criterion 매개변수의 용도는 노드에서 데이터를 분할할 기준을 정하는 것이다.

앞의 그린 트리에서 루트 노드가 당도 -0.239기준으로 왼쪽과 오른쪽으로 나눴는데 그 기준을 지니 불순도를 사용한 것이다.

지니 불순도는 어떻게 계산하냐

  • 지니 불순도 = 1 - (음성 클래스 비율^2 + 양성 클래스 비율^2)

다중 클래스라면 클래스가 더 많겠지만 계산하는 방법은 동일하다.

결정 트리 모델은 부모 노드와 자식 노드의 불순도 차이가 가능한 크도록 트리를 성장시킨다.

  • 불순도의 차이 : 부모의 불순도 - (왼쪽 노드 샘플 수 / 부모의 샘플 수) X 왼쪽 노드 불순도 -(오른쪽 노드 샘플 수/부모의 샘플 수) X 오른쪽 노드 불순도

이런 부모와 자식 노드 사이의 불순도 차이를 *정보이득*이라고 한다.


DecisionTreeClassifier 클래스에서 criterion = 'entropy'를 지정하여 엔트로피 불순도를 사용할 수 있다. 엔트로피도 노드의 클래스 비율을 사용하지만 지니 불순도처럼 제곱이 아니라 밑이 2인 로그를 사용하여 곱한다.

  • -음성 클래스 비율 X log2(음성 클래스 비율) - 양성 클래스 비율 X log2(양성 클래스 비율)

이제 결정 트리 알고리즘을 확실히 이해했다. 불순도 기준을 사용해 정보 이득이 최대가 되도록 노드를 분할한다. 노드를 순수하게 나눌수록 정보 이득이 커진다. 새로운 샘플에 대해 예측할 때에는 노드의 질문에 따라 트리를 이동한다. 그리고 마지막에 도달한 노드의 클래스 비율을 보고 예측을 만든다.

  • 그런데 앞으 트리는 제한 없이 자라났기 때문에 훈련 세트보다 테스트 세트에서 점수가 크게 낮았다. 이 문제를 이제 다뤄보겠다.

가지치기

과수원에서 열매를 잘 맺기 위해 가지치기를 하는 것처럼 결정트리도 가지치기를 해야 한다.

결정 트리에서 가지치기를 하는 가장 간단한 방법은 자라날 수 있는 트리의 최대 깊이를 지정하는 것이다.

  • 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
plt.figure(figsize=(20,15)) 
plot_tree(dt,filled=True,feature_names = ['alcohol','sugar','pH'])
plt.show()
 
 
  • 깊이 3에 있는 노드가 최종 노드인 리프 노드이다. 왼쪽에서 세 번째에 있는 노드만 음성 클래스가 더 많다. 이 노드에 도착해야만 레드 와인으로 예측한다.
  • 결국 당도는 -0.239보다 작고 -0.802보다 크고 알코올 도수는 0.454보다 작으면 레드 와인이다.

그런데 음수로 된 당도를 어떻게 설명하지?? 잘 생각해보면 불순도를 기준으로 샘플을 나눈다. 그리고 불순도는 클래스별 비율을 가지고 계산했다. 샘플을 어떤 클래스 비율로 나누는지 계산할 때 특성값의 스케일이 계산에 영향을 미칠까???

아니다! 특성값의 스케일은 결정 트리 알고리즘에 아무런 영향을 미치지 않는다. 따라서 표준화 전처리를 할 필요가 없다.

  • 앞서 전처리하기 전의 훈련 세트와 테스트 세트로 결정 트리 모델을 다시 훈련해 보겠다.
 
df = DecisionTreeClassifier(max_depth=3,random_state=42) 
df.fit(train_input,train_target) 
print(df.score(train_input,train_target)) 
print(df.score(test_input,test_target))
>>0.8454877814123533
0.8415384615384616
plt.figure(figsize=(20,15))
plot_tree(df,filled=True,feature_names = ['alcohol','sugar','pH']) 
plt.show()
  • 결과를 보면 같은 트리지만, 당도가 1.625보다 크고 4.325보다 작은 와인 중에 알코올 도수가 11.025와 같거나 작은 것이 레드 와인이다. 그 외에는 모두 화이트 와인이다.
  • 마지막으로 어떤 특성이 가장 유용한지 나타내는 특성 중요도를 계산해주면 된다. 노드와 깊이 1에서 당도를 사용했기 때문에 아마도 당도가 가장 유용한 특성 중 하나일 것 같다.
  • feature_importances_ 속성에 저장되어 있다. 밑에서 확인 해보면 2번 째 특성인 당도가 0.87 정도로 특성 중요도가 가장 높다.
 
print(df.feature_importances_)
>>[0.12345626 0.86862934 0.0079144 ]
 
 

 

728x90