Hey Tech

[Python] Binary Classfication 모델링 코드 공유(feat. Wine Quality Dataset) 본문

AI & 빅데이터/머신러닝·딥러닝

[Python] Binary Classfication 모델링 코드 공유(feat. Wine Quality Dataset)

Tony Park (토니) 2021. 10. 3. 11:44
728x90
반응형

 

안녕하세요, 오늘은 머신러닝 알고리즘 Random Forest로 Binary Classification 모델링하는 절차와 방법을 공유하고자 합니다. 실습 데이터는 오픈소스인 Wine Quality Data Set입니다. 

1.  실습코드 및 데이터셋

데이터셋과 전체 파이썬 코드는 이곳에서 받으실 수 있습니다.

 

GitHub - park-gb/wine-data-classification: The wine data classification using machine learning algorithms

The wine data classification using machine learning algorithms - GitHub - park-gb/wine-data-classification: The wine data classification using machine learning algorithms

github.com

코드에 주석을 달아뒀습니다. 분석 절차나 방법론에 대한 상세한 설명이 필요 없고 바쁘신 분들은 코드만 확인하셔도 무리 없으실 겁니다 :)

2.  코드 설명

이제 주피터 노트북 "Cell by Cell"로 분석 절차나 방법론에 대해 설명하겠습니다. 이곳에서 데이터셋과 완성된 코드를 다운로드 받으셔도 좋고, 한 cell씩 따라 치셔도 됩니다. 파일 다운로드 방법은 2가지입니다.

 

1) 알집으로 직접 다운로드 받기

아래 그림 1과 같이 초록색 버튼 'code'를 클릭하시고 알집을 다운로드하여 알집을 풀어 주세요.

그림 1. 소스코드 및 데이터셋 전체 파일 ZIP으로 다운로드 받기

 

2) Git으로 다운로드하기

Git이 설치되어 있으신 분들은 clone 하셔도 됩니다.

git clone https://github.com/park-gb/wine-classification-rfc.git

소스코드는 src 폴더에, 데이터셋은 dataset 폴더에 있습니다.

라이브러리 Import

주피터 노트북 파일을 켜셔서 필요한 라이브러리를 import 합니다.

import numpy as np # 벡터, 행렬 등 수치 연산을 수행하는 선형대수 라이브러리
import pandas as pd # 시리즈, 데이터프레임 포맷 데이터 처리를 위한 라이브러리
import warnings; warnings.filterwarnings(action='ignore') # 경고 메시지 무시
import matplotlib.pyplot as plt # 데이터 시각화 라이브러리
import pickle # 객체 입출력을 위한 라이브러리
from sklearn.model_selection import train_test_split # 훈련 데이터, 테스트 데이터 분리
from sklearn.preprocessing import StandardScaler # 정규화
from sklearn.ensemble import RandomForestClassifier as RFC # 랜덤포레스트 분류 알고리즘
from sklearn.tree import DecisionTreeClassifier as DTC # 의사결정나무 분류 알고리즘
from sklearn.ensemble import GradientBoostingClassifier as GBC # 그래디언트 부스팅 분류 알고리즘
# 모델 평가를 위한 metrics
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, plot_confusion_matrix

만약 모듈이 설치되어 있지 않다는 에러 메시지가 출력된다면 해당 모듈을 아래와 같이 설치해 주시기 바랍니다.

pip install 모듈명

데이터 불러오기

온라인에서 레드 와인과 화이트 와인 데이터셋을 불러옵니다.

# 레드 와인데이터셋 불러오기
red = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv', sep=';')
# 화이트 와인데이터셋 불러오기
white = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv', sep=';')

데이터 로드가 잘 되었는지 확인해 봅니다(그림 2).

red.head()

그림 2. 레드 와인 데이터 로드 확인

white.head()

그림 3. 화이트 와인 데이터 로드 확인

데이터 전처리

불필요한 변수 제거

레드 와인이냐, 화이트 와인이냐를 분류하는 데 있어 불필요한 설명변수가 있을 수 있습니다. 저는 데이터셋 내 설명변수 중에서도 'quality(품질)'이 그러한 변수라고 생각했습니다. 따라서 해당 변수 정보를 전처리 단계에서부터 제거하였습니다. 데이터프레임 내 특정 칼럼을 제외하기 위해 판다스에 drop 모듈을 활용하였습니다.

# 일반적으로 와인 퀄리티가 와인의 종류를 구분하지 않기 때문에 quality 변수 제거
red = red.drop(['quality'], axis = 1)
white = white.drop(['quality'], axis = 1)

결측치 처리

판다스에 isnull().sum() 모듈을 활용하면 데이터프레임 칼럼별로 결측치(NaN)가 몇 개씩 있는지 쉽게 확인할 수 있습니다.

red.isnull().sum() # 결측치 미존재

아래와 같이 모든 칼럼에 결측치 개수가 0으로 전혀 없는 것을 알 수 있습니다.

fixed acidity           0
volatile acidity        0
citric acid             0
residual sugar          0
chlorides               0
free sulfur dioxide     0
total sulfur dioxide    0
density                 0
pH                      0
sulphates               0
alcohol                 0
dtype: int64

화이트 와인의 경우에도 모든 변수에 결측치가 전혀 없는 것을 알 수 있습니다.

red.isnull().sum() # 결측치 미존재
fixed acidity           0
volatile acidity        0
citric acid             0
residual sugar          0
chlorides               0
free sulfur dioxide     0
total sulfur dioxide    0
density                 0
pH                      0
sulphates               0
alcohol                 0
dtype: int64

이상치 확인 및 제거

이제 이상치(outlier)를 처리해 보겠습니다. 와인 종류에 따라 설명변수 데이터의 분포가 다를 수 있기 때문에 이상치는 클래스별로 따로 처리하는 것이 좋습니다.

이상치 시각화

설명변수별로 이상치가 얼마나 있는지 Boxplot(한국어: 상자 수염 그림)을 시각화하는 함수를 작성해 보겠습니다. matplotlib에서 subplot 모듈을 활용하면 여러 개의 그래프를 하나의 figure에 나타낼 수 있습니다. 이를 활용하여 11개의 설명변수를 6행 2열(가로 2줄, 세로 6줄)에 시각화하였습니다. 하이퍼파라미터인 flierprops 조정을 통해 이상치에 해당하는 데이터는 빨간색 다이아몬드 모양으로 시각화하였습니다.

def boxplot_vis(data, target_name):
    plt.figure(figsize=(30, 30))
    for col_idx in range(len(data.columns)):
        # 6행 2열 서브플롯에 각 feature 박스플롯 시각화
        plt.subplot(6, 2, col_idx+1)
        # flierprops: 빨간색 다이아몬드 모양으로 아웃라이어 시각화
        plt.boxplot(data[data.columns[col_idx]], flierprops = dict(markerfacecolor = 'r', marker = 'D'))
        # 그래프 타이틀: feature name
        plt.title("Feature" + "(" + target_name + "):" + data.columns[col_idx], fontsize = 20)
    plt.savefig('../figure/boxplot_' + target_name + '.png')
    plt.show()

먼저, Boxplot 그래프를 해석하는 방법에 대해 간단히 설명해 드리겠습니다. 아래 그림 4와 같이 세로축은 특정 값의 범위를 나타내고, 이 범위 내에서 데이터는 주로 파란색 박스 안에 분포합니다. 파란색 박스 가운데 노란색 직선으로 표시한 부분이 데이터의 중앙값(Median)이 됩니다.

 

박스 최상단은 제3 사분위수(Q3, 75th percentile), 최하단은 제1 사분위수(Q1, 25th percentile)입니다. 사분위수(Quantile)란 전체 데이터를 오름차순 정렬한 다음 25%씩 동일한 비율로 데이터를 나눈 것입니다. 즉, 제1 사분위수(Q1)는 가장 작은 데이터부터 전체 중 25% 비율만큼의 데이터를(25%) 의미하고, 제3 사분위수(Q3)는 중앙값(50%)에서부터 25% 비율만큼의 데이터를(75%) 의미합니다. 이 Q3과 Q1 사이 범위인 (Q3-Q1)을 사분범위(Interquartile range, IQR)이라고 합니다.

 

Boxplot의 수염에 해당하는 박스 바깥의 위, 아래에 가로로 긴 직선을 각각 Maximum, Minimum이라고 부릅니다. Maximum은 IQR 값에 1.5를 곱한 값에 Q3을 더한 값이고, Minimum은 Q1에서 IQR 값에 1.5를 곱한 값을 뺀 값입니다. 이상치는 이 Minimum과 Maximum 보다 각각 크거나 작은 값으로 극단치라고 부릅니다.  즉, 그림 4에서는 빨간색 다이아몬드가 모두 이상치에 해당하는 것입니다.

그림 4. Boxplot 설명

이제 다시 본론으로 돌아와, 레드 와인 데이터부터 이상치를 시각화해 보겠습니다. 앞선 설명과 마찬가지로 그림 5에서도 붉은색 다이아몬드 마커로 이상치를 표현하였습니다. alcohol, citric acid 변수 외 대부분의 변수에는 이상치가 많은 것을 알 수 있습니다.

그림 5. Boxplot을 활용한 레드 와인 데이터 설명변수별 이상치 분포 시각화

다음으로 화이트 와인 데이터에 대해 이상치를 시각화해 보겠습니다. residual sugar, density, alcohol 변수 외 다른 변수에는 이상치가 많을 것을 알 수 있습니다. 이처럼 와인 종류별로 이상치 개수가 상이합니다. 만일 두 데이터를 결합한 후 이상치를 검증했다면, 소중한 데이터가 이상치로 처리되어 활용하지 못하는 경우가 생길 수 있습니다.

그림 6. Boxplot을 활용한 화이트 와인 데이터 설명변수별 이상치 분포 시각화

이상치 제거

앞서 이상치의 기준에 대해 설명드린 것처럼, minimum과 maximum 사이에 분포하는 데이터만 분석에 선정하겠습니다.

사분위수 개념을 활용해 이상치 제거  함수를 작성하였고 이상치를 제거한 데이터를 반환합니다.

def remove_outlier(input_data):
    q1 = input_data.quantile(0.25) # 제 1사분위수
    q3 = input_data.quantile(0.75) # 제 3사분위수
    iqr = q3 - q1 # IQR(Interquartile range) 계산
    minimum = q1 - (iqr * 1.5) # IQR 최솟값
    maximum = q3 + (iqr * 1.5) # IQR 최댓값
    # IQR 범위 내에 있는 데이터만 산출(IQR 범위 밖의 데이터는 이상치)
    df_removed_outlier = input_data[(minimum < input_data) & (input_data < maximum)]
    return df_removed_outlier

레드와인 데이터 이상치 제거

레드와인 데이터부터 이상치를 제거해 보겠습니다.

# 이상치 제거한 데이터셋
red_prep = remove_outlier(red)

이상치를 처리한 데이터에 목표변수를 추가합니다. 레드와인은 target 값을 0으로 설정하겠습니다.

# 목표변수 할당
red_prep['target'] = 0

이상치를 처리한 데이터프레임에 이상치를 처리하지 않은 목표변수 칼럼('target')을 추가하였기 때문에, 이상치로 처리된 행은 모두 결측치(NaN)로 채워집니다. 따라서 결측치를 카운팅하면 변수별 이상치 개수를 확인할 수 있습니다.

# 결측치(이상치 처리된 데이터) 확인
red_prep.isnull().sum()

아래와 같이 Boxplot에서 확인했던 것처럼, alcohol, citric acid 변수는 이상치가 15개 이하이지만, 100개 이상의 이상치가 포함된 변수도 있는 것을 알 수 있습니다.

fixed acidity            49
volatile acidity         19
citric acid               1
residual sugar          155
chlorides               112
free sulfur dioxide      33
total sulfur dioxide     58
density                  45
pH                       35
sulphates                59
alcohol                  14
target                    0
dtype: int64

이제 결측치가 포함된 모든 행을 삭제하기 위해 판다스에 dropna 모듈을 활용하였습니다. 행 삭제를 위해 axis 값은 0으로, 행 내 결측치가 하나라도 포함하면 삭제하기 위해 how 변수에는 'any'를 입력해 주었습니다. inplace 값에 True를 입력하면 결측치를 제거한 객체를 따로 저장할 필요 없이 바로 해당 객체가 업데이트됩니다.

# 이상치 포함 데이터(이상치 처리 후 NaN) 삭제
red_prep.dropna(axis = 0, how = 'any', inplace = True)
print(f"이상치 포함된 데이터 비율: {round((len(red) - len(red_prep))*100/len(red), 2)}%")

이상치가 1개라도 포함된 데이터의 비율은 25.58%입니다.

이상치 포함된 데이터 비율: 25.58%

화이트 와인 데이터 이상치 제거

화이트 와인 데이터도 앞서 정의한 함수를 통해 이상치를 제거합니다.

# 이상치 제거한 데이터셋
white_prep = remove_outlier(white)

화이트 와인의 target 값은 1로 설정하겠습니다.

# 목표변수 할당
white_prep['target'] = 1

레드 와인 데이터의 이상치 개수를 확인할 때와 마찬가지로, 화이트 와인 데이터의 이상치 개수를 결측치 개수를 카운팅 하여 계산합니다.

# 결측치(이상치 처리된 데이터) 확인
white_prep.isnull().sum()

앞서 Boxplot에서 확인했듯이, residual sugar, density, alcohol 변수에는 이상치가 상대적으로 적은 것을 알 수 있습니다.

fixed acidity           146
volatile acidity        186
citric acid             270
residual sugar            7
chlorides               208
free sulfur dioxide      50
total sulfur dioxide     19
density                   5
pH                       75
sulphates               124
alcohol                   0
target                    0
dtype: int64

이제 결측치가 포함된, 즉 이상치가 포함된 행을 데이터프레임에서 모두 제거합니다.

# 이상치 포함 데이터(이상치 처리 후 NaN) 삭제
white_prep.dropna(axis = 0, how = 'any', inplace = True)
print(f"이상치 포함된 데이터 비율: {round((len(white) - len(white_prep))*100/len(white), 2)}%")

레드 와인 데이터에서 이상치 비율이 25.58% 나온 것에 비해, 화이트 와인 데이터에서는 이상치 비율이 18.37%를 차지하였습니다.

이상치 포함된 데이터 비율: 18.37%

데이터 저장

전처리를 마친 두 데이터셋을 저장합니다.

# 데이터셋 저장
red_prep.to_csv('../dataset/red_prep.csv')
white_prep.to_csv('../dataset/white_prep.csv')

데이터 병합

이제 레드 와인과 화이트 와인 데이터를 병합합니다. 판다스에서 concat 모듈을 활용하였습니다.

# 레드 와인, 화이트 와인 데이터셋 병합
df = pd.concat([red_prep, white_prep], axis = 0)
df.head()

병합한 데이터셋은 따로 저장해 두겠습니다.

df.to_csv('../dataset/wine_combined.csv')

와인 종류별로 데이터셋에서 비율은 어떻게 될까요? 타겟 칼럼에서 value_counts 모듈을 활용해 알아봤습니다. 이때 하이퍼파라미터인 normalize 값을 True로 설정하면 비율을 구할 수 있습니다. 참고로 기본값은 False이고 데이터 개수를 출력해 줍니다.

# 화이트 와인이 레드와인보다 약 3배 더 많음
df.target.value_counts(normalize=True)

아래와 같이 타겟 칼럼의 값이 1인 화이트 와인 데이터의 개수가 레드 와인보다 약 3.3배 더 많은 것을 알 수 있습니다.

1    0.770625
0    0.229375
Name: target, dtype: float64

설명변수별 목표변수 간의 관계 시각화

이제 어떤 설명변수가 와인의 종류를 결정짓는 주요 영향인자인지 그래프를 시각화하여 살펴보겠습니다. 설명변수 1개씩 반복문을 통해 선정한 후 레드 와인과 화이트 와인 각각에 해당하는 데이터를 histogram으로 나타냅니다. subplot 모듈을 활용해 한 figure 안에 그래프를 모두 시각화하고 이를 저장하는 코드입니다.

# 설명변수만 분리
x = df[df.columns.difference(['target'])]
# 설명변수명 리스트
feature_name = x.columns
plt.figure(figsize=(30, 30))
for col_idx in range(len(feature_name)):
    # 6행 2열 서브플롯에 각 feature 박스플롯 시각화
    plt.subplot(6, 2, col_idx+1)
    # 레드 와인에 해당하는 데이터 histogram 시각화
    plt.hist(df[df["target"] == 0][feature_name[col_idx]], label = "Red wine", alpha = 0.5)
    # 화이트 와인에 해당하는 데이터 histogram 시각화
    plt.hist(df[df["target"] == 1][feature_name[col_idx]], label = "White wine", alpha = 0.5)
    plt.legend()
    # 그래프 타이틀: feature name
    plt.title("Feature: "+ feature_name[col_idx], fontsize = 20)
plt.savefig('../figure/relationship.png')
plt.show()

시각화한 결과는 아래 그림 7과 같습니다. 그래프 해석은 간단합니다. 주황색이 화이트 와인 데이터이고 파란색이 레드와인 데이터입니다. 두 그래프가 겹치는 구간이 적게 데이터가 분포한다는 것은 해당 설명변수의 값에 따라 와인의 종류를 분류할 가능성이 높다는 뜻입니다. 이러한 관점에서, alcohol, PH 변수는 와인의 종류를 구분 짓는 데 어려움이 있을 것으로 보입니다. 이제 더욱 정량적이고 과학적인 방법으로 목표변수(와인의 종류)를 결정하는 설명변수를 찾아보겠습니다.

그림 7. 와인 종류에 따라 설명변수별 데이터 분포의 차이 시각화

데이터 스케일링

설명변수별로 데이터 범위가 천차만별이기 때문에, 이들 간의 비교를 쉽게 하기 위해 데이터 스케일링을 수행하겠습니다. 본 포스팅에서는 표준 스케일러인 StandardScaler를 사용합니다. 표준 스케일러는 평균을 0으로, 분산을 1로 스케일링합니다.목표변수(\(y\))는 따로 스케일링할 필요가 없기 때문에, 설명변수(\(x\))만 스케일링하겠습니다.

# 표준 스케일러(평균 0, 분산 1)
scaler = StandardScaler()
# 설명변수 및 목표변수 분리
x = df[df.columns.difference(['target'])]
y = df['target']
# 설명변수 데이터 스케일링
x_scaled = scaler.fit_transform(x)

학습/테스트 데이터 분리

이제 분류 모델을 학습하고 평가하기 위해 학습 데이터셋과 테스트 데이터셋을 분리합니다. 본 포스팅에서는 학습 데이터와 테스트 데이터의 비율을 7:3으로 분리하였습니다.

# 학습, 테스트 데이터셋 7:3 비율로 분리
x_train, x_test, y_train, y_test = train_test_split(x_scaled, y, 
                                                    test_size = 0.3, random_state = 123)

학습 데이터에서의 와인 종류별 비율은 다음과 같습니다.

# 훈련 데이터 내 와인별 비율
y_train.value_counts(normalize=True)

클래스별 데이터 비율이 데이터 분리 이전과 유사한 것을 알 수 있습니다.

1    0.767006
0    0.232994
Name: target, dtype: float64

테스트 데이터에서의 와인 종류별 비율 역시 다음과 같습니다.

# 테스트 데이터 내 와인별 비율
y_test.value_counts(normalize=True)

클래스별 데이터 비율이 데이터 분리 이전과 유사한 것을 알 수 있습니다.

1    0.779062
0    0.220938
Name: target, dtype: float64

Classifier 모델링 함수

이제 모델 학습 및 최적화에 필요한 함수를 작성해 보겠습니다.

기본 모델링 함수

먼저 하이퍼파라미터 튜닝 없이 모델을 학습하고 성능을 평가하는 함수입니다. 함수는 파라미터로 알고리즘 종류, 설명변수와 목표변수의 훈련 데이터, 테스트 데이터를 각각 전달받습니다.

def modeling_uncustomized (algorithm, x_train, y_train, x_test, y_test):
    # 하이퍼파라미터 조정 없이 모델 학습
    uncustomized = algorithm(random_state=1234)
    uncustomized.fit(x_train, y_train)
    # Train Data 설명력
    train_score_before = uncustomized.score(x_train, y_train).round(3)
    print(f"학습 데이터셋 정확도: {train_score_before}")
    # Test Data 설명력
    test_score_before = uncustomized.score(x_test, y_test).round(3)
    print(f"테스트 데이터셋 정확도: {test_score_before}")
    return train_score_before, test_score_before

하이퍼파라미터별 모델 성능 시각화 함수

다음은 하이퍼파라미터 튜닝에 따라 모델 성능이 어떻게 달라지는지 추이를 시각화하는 함수입니다. 하이퍼파라미터 값을 \(x\)축으로, \(y\)축은 학습 및 테스트 데이터 기반의 모델 정확도를 나타냅니다.

def optimi_visualization(algorithm_name, x_values, train_score, test_score, xlabel, filename):
    # 하이퍼파라미터 조정에 따른 학습 데이터셋 기반 모델 성능 추이 시각화
    plt.plot(x_values, train_score, linestyle = '-', label = 'train score')
    # 하이퍼파라미터 조정에 따른 테스트 데이터셋 기반 모델 성능 추이 시각화
    plt.plot(x_values, test_score, linestyle = '--', label = 'test score')
    plt.ylabel('Accuracy(%)') # y축 라벨
    plt.xlabel(xlabel) # x축 라벨
    plt.legend() # 범례표시
    plt.savefig('../figure/' + algorithm_name + '_' + filename + '.png') # 시각화한 그래프는 로컬에 저장

모델 최적화 함수: 학습할 트리 모델 개수 선정

이제 모델 최적화 함수로서 모델이 학습할 최적의 트리 개수를 결정하기 위한 함수입니다. 파라미터로 입력받은 트리 개수의 최솟값부터 트리 개수를 5개씩 최대 트리 개수까지 늘려가며 모델의 성능을 평가합니다. 모델 성능은 앞서 작성한 시각화 함수에 전달하여 하이퍼파라미터에 따른 성능 변화 추이를 시각화합니다.

def optimi_estimator(algorithm, algorithm_name, x_train, y_train, x_test, y_test, n_estimator_min, n_estimator_max):
    train_score = []; test_score =[]
    para_n_tree = [n_tree*5 for n_tree in range(n_estimator_min, n_estimator_max)]

    for v_n_estimators in para_n_tree:
        model = algorithm(n_estimators = v_n_estimators, random_state=1234)
        model.fit(x_train, y_train)
        train_score.append(model.score(x_train, y_train))
        test_score.append(model.score(x_test, y_test))

    # 트리 개수에 따른 모델 성능 저장
    df_score_n = pd.DataFrame({'n_estimators': para_n_tree, 'TrainScore': train_score, 'TestScore': test_score})
    # 트리 개수에 따른 모델 성능 추이 시각화 함수 호출
    optimi_visualization(algorithm_name, para_n_tree, train_score, test_score, "The number of estimator", "n_estimator")
    print(round(df_score_n, 4))

모델 최적화 함수: 최대 깊이 선정

다음은 모델이 학습할 트리별 최대 깊이를 결정하기 위한 함수입니다. 전달받은 최대 깊이의 최솟값부터 깊이를 1씩 최대 깊이의 최댓값까지 늘려가며 모델의 성능을 평가합니다. 모델 성능은 앞서 작성한 시각화 함수에 전달하여 하이퍼파라미터에 따른 성능 변화 추이를 시각화합니다.

def optimi_maxdepth (algorithm, algorithm_name, x_train, y_train, x_test, y_test, depth_min, depth_max, n_estimator):
    train_score = []; test_score = []
    para_depth = [depth for depth in range(depth_min, depth_max)]

    for v_max_depth in para_depth:
        # 의사결정나무 모델의 경우 트리 개수를 따로 설정하지 않기 때문에 RFC, GBC와 분리하여 모델링
        if algorithm == DTC:
            model = algorithm(max_depth = v_max_depth,
                              random_state=1234)
        else:
            model = algorithm(max_depth = v_max_depth,
                              n_estimators = n_estimator,
                              random_state=1234)
        
        model.fit(x_train, y_train)
        train_score.append(model.score(x_train, y_train))
        test_score.append(model.score(x_test, y_test))

    # 최대 깊이에 따른 모델 성능 저장
    df_score_n = pd.DataFrame({'depth': para_depth, 'TrainScore': train_score, 'TestScore': test_score})
    # 최대 깊이에 따른 모델 성능 추이 시각화 함수 호출
    optimi_visualization(algorithm_name, para_depth, train_score, test_score, "The number of depth", "n_depth")
    print(round(df_score_n, 4))

모델 최적화 함수: 분리 노드의 최소 자료 수 선정

다음은 노드를 분리하기 위한 최소 자료 수를 결정하기 위한 함수입니다. 전달받은 분리 노드의 최소 자료 수의 최솟값부터 깊이를 2씩 분리 노드의 최소 자료 수의 최댓값까지 늘려가며 모델의 성능을 평가합니다. 모델 성능은 앞서 작성한 시각화 함수에 전달하여 하이퍼파라미터에 따른 성능 변화 추이를 시각화합니다.

def optimi_minsplit (algorithm, algorithm_name, x_train, y_train, x_test, y_test, n_split_min, n_split_max, n_estimator, n_depth):
    train_score = []; test_score = []
    para_split = [n_split*2 for n_split in range(n_split_min, n_split_max)]
    for v_min_samples_split in para_split:
        # 의사결정나무 모델의 경우 트리 개수를 따로 설정하지 않기 때문에 RFC, GBC와 분리하여 모델링
        if algorithm == DTC:
            model = algorithm(min_samples_split = v_min_samples_split,
                              max_depth = n_depth,
                              random_state = 1234)
        else:
            model = algorithm(min_samples_split = v_min_samples_split,
                              n_estimators = n_estimator,
                              max_depth = n_depth,
                              random_state = 1234)
        model.fit(x_train, y_train)
        train_score.append(model.score(x_train, y_train))
        test_score.append(model.score(x_test, y_test))

    # 분리 노드의 최소 자료 수에 따른 모델 성능 저장
    df_score_n = pd.DataFrame({'min_samples_split': para_split, 'TrainScore': train_score, 'TestScore': test_score})
    # 분리 노드의 최소 자료 수에 따른 모델 성능 추이 시각화 함수 호출
    optimi_visualization(algorithm_name, para_split, train_score, test_score, "The minimum number of samples required to split an internal node", "min_samples_split")
    print(round(df_score_n, 4))

모델 최적화 함수: 잎사귀 노드의 최소 자료 수 선정

다음은 잎사귀 노드 내 최소 자료 수를 결정하기 위한 함수입니다. 전달받은 잎사귀 노드의 최소 자료 수의 최솟값부터 깊이를 2씩 잎사귀 노드의 최소 자료 수의 최댓값까지 늘려가며 모델의 성능을 평가합니다. 모델 성능은 앞서 작성한 시각화 함수에 전달하여 하이퍼파라미터에 따른 성능 변화 추이를 시각화합니다.

def optimi_minleaf(algorithm, algorithm_name, x_train, y_train, x_test, y_test, n_leaf_min, n_leaf_max, n_estimator, n_depth, n_split):
    train_score = []; test_score = []
    para_leaf = [n_leaf*2 for n_leaf in range(n_leaf_min, n_leaf_max)]

    for v_min_samples_leaf in para_leaf:
        # 의사결정나무 모델의 경우 트리 개수를 따로 설정하지 않기 때문에 RFC, GBC와 분리하여 모델링
        if algorithm == DTC:
            model = algorithm(min_samples_leaf = v_min_samples_leaf,
                                        max_depth = n_depth,
                                        min_samples_split = n_split,
                                        random_state=1234)
        else:
            model = algorithm(min_samples_leaf = v_min_samples_leaf,
                                n_estimators = n_estimator,
                                max_depth = n_depth,
                                min_samples_split = n_split,
                                random_state=1234)
        model.fit(x_train, y_train)
        train_score.append(model.score(x_train, y_train))
        test_score.append(model.score(x_test, y_test))

    # 잎사귀 노드의 최소 자료 수에 따른 모델 성능 저장
    df_score_n = pd.DataFrame({'min_samples_leaf': para_leaf, 'TrainScore': train_score, 'TestScore': test_score})
    # 잎사귀 노드의 최소 자료 수에 따른 모델 성능 추이 시각화 함수 호출
    optimi_visualization(algorithm_name, para_leaf, train_score, test_score, "The minimum number of samples required to be at a leaf node", "min_samples_leaf")
    print(round(df_score_n, 4))

최종 모델 학습

앞서 구한 최적의 하이퍼파라미터를 기반으로 최종 모델을 학습합니다. 학습한 모델을 pickle 모듈을 통해 저장합니다. 모델 성능 평가를 위해 평가지표로서 Accuracy, Precision, Recall, F1 스코어, Confusion Matrix를 활용합니다. 마지막으로 변수별 중요도를 산출하고 시각화합니다.

def model_final(algorithm, algorithm_name, feature_name, x_train, y_train, x_test, y_test, n_estimator, n_depth, n_split, n_leaf):
    # 의사결정나무 모델의 경우 트리 개수를 따로 설정하지 않기 때문에 RFC, GBC와 분리하여 모델링
    if algorithm == DTC:
        model = algorithm(random_state=1234, 
                          min_samples_leaf = n_leaf,
                          min_samples_split = n_split, 
                          max_depth = n_depth)
    else:
        model = algorithm(random_state = 1234, 
                          n_estimators = n_estimator, 
                          min_samples_leaf = n_leaf,
                          min_samples_split = n_split, 
                          max_depth = n_depth)
    # 모델 학습
    model.fit(x_train, y_train)
    # 모델 저장
    model_path = '../model/'
    model_filename = 'wine_classification_' + algorithm_name + '.pkl'
    with open(model_path + model_filename, 'wb') as f:
        pickle.dump(model, f)
    print(f"최종 모델 저장 완료! 파일 경로: {model_path + model_filename}\n")
    
    # 최종 모델의 성능 평가
    train_acc = model.score(x_train, y_train)
    test_acc = model.score(x_test, y_test)
    y_pred = model.predict(x_test)
    print(f"Accuracy: {accuracy_score(y_test, y_pred):.3f}") # 정확도
    print(f"Precision: {precision_score(y_test, y_pred):.3f}") # 정밀도
    print(f"Recall: {recall_score(y_test, y_pred):.3f}") # 재현율
    print(f"F1-score: {f1_score(y_test, y_pred):.3f}") # F1 스코어
    
    # 혼동행렬 시각화
    plt.figure(figsize =(30, 30))
    plot_confusion_matrix(model, 
                         x_test, y_test,
                         include_values = True,
                         display_labels = ['Red', 'White'], # 목표변수 이름
                         cmap = 'Pastel1') # 컬러맵
    plt.savefig('../figure/' + algorithm_name + '_confusion_matrix.png') # 혼동행렬 자료 저장
    plt.show()
    
    # 변수 중요도 산출
    dt_importance = pd.DataFrame()
    dt_importance['Feature'] = feature_name # 설명변수 이름
    dt_importance['Importance'] = model.feature_importances_ # 설명변수 중요도 산출

    # 변수 중요도 내림차순 정렬
    dt_importance.sort_values("Importance", ascending = False, inplace = True)
    print(dt_importance.round(3))
    # 변수 중요도 오름차순 정렬
    dt_importance.sort_values("Importance", ascending = True, inplace = True)
    # 변수 중요도 시각화
    coordinates = range(len(dt_importance)) # 설명변수 개수만큼 bar 시각화
    plt.barh(y = coordinates, width = dt_importance["Importance"])
    plt.yticks(coordinates, dt_importance["Feature"]) # y축 눈금별 설명변수 이름 기입
    plt.xlabel("Feature Importance") # x축 이름
    plt.ylabel("Features") # y축 이름
    plt.savefig('../figure/' + algorithm_name + '_feature_importance.png') # 변수 중요도 그래프 저장

Random Forest Classifier 기반 분류 모델 성능 평가

라이브러리를 import 할 때 RandomForestClassifier를 RFC로 치환하였습니다. 본 포스팅에서는 위와 같은 플로우에서 학습할 알고리즘 종류만 바꿔 추후에 활용하기 위해 algorithm이라는 객체에 알고리즘을 할당하는 방식을 사용하였습니다. 즉, 그래디언트 부스팅을 사용한다면, algorithm 객체에 GBC를 할당하면 되는 것이죠.

# 랜덤포레스트 분류 알고리즘
algorithm = RFC
algorithm_name = 'rfc'

기본 모델 학습

하이퍼파라미터 튜닝 없이 기본 모델을 학습시켜 봅니다.

train_acc_before, test_acc_before = modeling_uncustomized(algorithm, 
                                                          x_train,
                                                          y_train,
                                                          x_test,
                                                          y_test)

학습 데이터 기반 분류 모델의 정확도가 100%인 것으로 미루어 보아 과대적합이 발생한 것으로 보입니다. 따라서 하이퍼파라미터 튜닝을 통해 과대적합을 방지하기 위한 조치가 필요합니다.

학습 데이터셋 정확도: 1.0
테스트 데이터셋 정확도: 0.999

모델 최적화: (1) 학습할 트리 개수 선정

Random Forest 모델이 학습할 최적의 트리 개수를 선정해 보겠습니다.

n_estimator_min = 1
n_estimator_max = 31
optimi_estimator(algorithm, algorithm_name, 
                 x_train, y_train, x_test, y_test, 
                 n_estimator_min, n_estimator_max)

트리 개수에 따른 학습 데이터 및 테스트 데이터에서의 모델 성능은 아래 그림 8과 같습니다. 학습 데이터 기반 모델 정확도가 여전히 100%인 것으로 보아 과적합이 발생한 것으로 보입니다.

그림 8. 트리 개수에 따른 모델 성능 추이 시각화

트리 개수는 많을수록 과적합 방지에 유리합니다. 따라서 트리 개수가 많고 학습 데이터 기반 모델 정확도와 테스트 데이터 기반 모델 정확도의 차이가 적은 값으로 선정하는 것이 좋습니다. 그래프보다 더욱 정밀하게 하이퍼파라미터별 모델 성능을 알아보기 위해 데이터프레임 형태로도 추이를 출력해 봤습니다(그림 9).

그림 9. 트리 개수별 모델 성능 추이

트리 개수가 30개 일 때 테스트 데이터 기반 모델 정확도가 가장 높고, 트리 개수가 더 많아져도 성능에 차이가 없다는 점에,최적의 트리 개수는 30으로 설정하겠습니다.

n_estimator = 30

모델 최적화: (2) 최대 깊이

Random Forest 모델의 최대 깊이를 선정해 보겠습니다.

depth_min = 1
depth_max = 21
optimi_maxdepth(algorithm, algorithm_name, 
                x_train, y_train, x_test, y_test, 
                depth_min, depth_max, n_estimator)

최대 깊이에 따른 학습 데이터 및 테스트 데이터에서의 모델 성능은 아래 그림 10과 같습니다.

그림 10. 최대 깊이에 따른 모델 성능 추이 시각화

최대 깊이는 적을수록 과적합 방지에 유리합니다. 따라서 최대 깊이는 적고 학습 데이터 기반 모델 정확도와 테스트 데이터 기반 모델 정확도의 차이가 적은 값으로 선정하는 것이 좋습니다.  아래 그림 11과 같이 최대 깊이 값에 따른 모델 성능 추이를 데이터프레임 형태로 출력해 봤습니다.

그림 11. 최대 깊이별 모델 성능 추이

테스트 데이터 기반 모델 정확도가 점차 증가하다가 감소하기 시작하는 구간으로, 최대 깊이 6을 최적의 값으로 선정하였습니다. 

n_depth = 6

모델 최적화: (3) 분리 노드의 최소 자료 수

다음으로 노드를 분리할 때 최소 자료 수의 최적의 값을 선정해 보겠습니다.

n_split_min = 1
n_split_max = 101
# 데이터프레임 행 최대 100개까지 반드시 출력
pd.set_option('display.max_row', 100)
optimi_minsplit (algorithm, algorithm_name,
                 x_train, y_train, x_test, y_test,
                 n_split_min, n_split_max, n_estimator, n_depth)

분리 노드의 최소 자료 수에 따른 학습 데이터 및 테스트 데이터에서의 모델 성능은 아래 그림 11과 같습니다.

그림 11. 분리 노드의 최소 자료 수에 따른 모델 성능 추이 시각화

아래 그림 13과 같이 분리 노드의 최소 자료 수에 따른 모델 성능 추이를 데이터프레임 형태로 출력해 봤습니다.

그림 13. 분리 노드의 최소 자료 수에 따른 모델 성능 추이(일부)

분리 노드의 최소 자료 수는 많을수록 과적합 방지에 유리합니다. 따라서 분리 노드의 최소 자료 수는 많되 학습 데이터 기반 모델 정확도와 테스트 데이터 기반 모델 정확도의 차이가 적은 값으로 선정하는 것이 좋습니다. 이러한 관점에서, 분리 노드의 최소 자료 수는 66으로 선정하였습니다.

n_split = 66

모델 최적화: (4) 잎사귀 노드의 최소 자료 수

다음으로 최적의 잎사귀 노드의 최소 자료 수를 선정해 보겠습니다.

n_leaf_min = 1
n_leaf_max = 51
optimi_minleaf(algorithm, algorithm_name, 
               x_train, y_train, x_test, y_test, 
               n_leaf_min, n_leaf_max, n_estimator, n_depth, n_split)

잎사귀 노드의 최소 자료 수에 따른 학습 데이터 및 테스트 데이터에서의 모델 성능은 아래 그림 14과 같습니다.

그림 11. 분리 노드의 최소 자료 수에 따른 모델 성능 추이 시각화

아래 그림 15와 같이 잎사귀 노드의 최소 자료 수에 따른 모델 성능 추이를 데이터프레임 형태로 출력해 봤습니다.

그림 15. 잎사귀 노드의 최소 자료 수에 따른 모델 성능 추이(일부)

잎사귀 노드의 최소 자료 수는 많을수록 과적합 방지에 유리합니다. 따라서 잎사귀 노드의 최소 자료 수는 많되 학습 데이터 기반 모델 정확도와 테스트 데이터 기반 모델 정확도의 차이가 적은 값으로 선정하는 것이 좋습니다. 이러한 관점에서, 잎사귀 노드의 최소 자료 수는 20으로 선정하였습니다.

n_leaf = 20

최종 모델 학습

이제 앞서 선정한 하이퍼파라미터로 최종 모델을 학습해 보겠습니다.

model_final(algorithm, algorithm_name, feature_name,
            x_train, y_train, x_test, y_test,
            n_estimator, n_depth, n_split, n_leaf)

모델 성능은 아래와 같습니다. 모델의 정확도가 99.8%, F1 점수 역시 0.999인 만큼 분류 성능이 우수하다는 것을 알 수 있습니다.

Accuracy: 0.998
Precision: 0.999
Recall: 0.998
F1-score: 0.999

Confusion Matrix를 시각화하면 아래 그림 16과 같습니다.

그림 16. Confusion Matrix 시각화

변수별 중요도를 산출하면 그림 17 그림 18과 같습니다. chlorides(염화물), total sulfur dioxide(총 이산화황), volatile acidity(휘발성산), density(밀도)가 와인 분류 시 가장 중요한 Top 4임을 확인하실 수 있습니다.

그림 17. 변수별 중요도 산출
그림 18. 변수별 중요도 산출

📚 참고할 만한 포스팅

1.  의사결정나무(Decision Tree)의 사용이유, 장단점, 모델평가방법, 변수 중요도 산출방법에 대해 알아보자!
2. 랜덤포레스트(Random Forest)의 정의, 장단점, 모델 최적화 방법(+파이썬 실습)

오늘은 Wine Quality Dataset을 활용하여 이진 분류 문제를 해결하는 절차와 방법을 소개해 드렸습니다.

포스팅 내용에 오류가 있을 경우 아래에 👇👇👇  댓글 남겨주시면 감사드리겠습니다.

그럼 오늘도 즐거운 하루 보내시길 바랍니다 :)

고맙습니다.

728x90
반응형