데이콘 대회 참여 - 06 교차검증과 파라미터 튜닝

Page content

공지

제 수업을 듣는 사람들이 계속적으로 실습할 수 있도록 강의 파일을 만들었습니다. 늘 도움이 되기를 바라며. 참고했던 교재 및 Reference는 꼭 확인하셔서 교재 구매 또는 관련 Reference를 확인하시기를 바랍니다.

사전작업

!pip install https://github.com/pandas-profiling/pandas-profiling/archive/master.zip
Collecting https://github.com/pandas-profiling/pandas-profiling/archive/master.zip
  Using cached https://github.com/pandas-profiling/pandas-profiling/archive/master.zip
.
.
.
Successfully built pandas-profiling

I. 빅쿼리 연동

  • 지난 시간에 데이콘에서 내려받은 데이터를 빅쿼리에 넣는 작업을 진행하였다.
  • 빅쿼리에 저장된 데이터를 구글 코랩으로 불러오려면 다음과 같이 진행한다.

(1) 사용자 계정 인증

구글 코랩을 사용해서 인증 절차를 밟도록 한다. 아래 소스코드는 변경시키지 않는다. 아래 절차대로 진행하면 된다. Gmail 인증 절차와 비슷하다.

from google.colab import auth
auth.authenticate_user()
print('Authenticated')
Authenticated

(2) 데이터 불러오기

  • 이번에는 빅쿼리에서 Random Sampling을 활용하여 데이터를 가져온다.
  • 층화추출도 할 수 있는데, 쿼리상 조금 복잡할 수 있어서, 이부분은 추후 포스팅 하는 것으로 남겨둔다.
  • 이번에는 데이터 갯수를 기존 10000개 $\rightarrow$ 100000개로 확장해서 추출한다.
from google.cloud import bigquery
from tabulate import tabulate
import pandas as pd

project_id = 'your_project_id'
client = bigquery.Client(project=project_id)

train = client.query('''
  SELECT 
      * 
  FROM `your_project_id.jeju_data_ver1.201901_202003_train` 
  WHERE RAND() < 100000 / (SELECT COUNT(*) FROM `your_project_id.jeju_data_ver1.201901_202003_train`)
  ''').to_dataframe()

(3) 데이터 시각화 하기

from pandas_profiling import ProfileReport
profile = ProfileReport(train, title='Pandas Profiling Report', explorative=True)
profile.to_notebook_iframe()
  • 데이터 시각화를 통해서 전처리를 진행하는 것이 순리이다. 그러나, 본 과정에서는 GBM의 개념과 원리에 집중해야하기 때문에, Raw-Data로 그대로 적용하도록 한다.

II. GBM의 개요 및 실습

  • 부스팅 알고리즘은 여러 개의 약한 학습기(Weak Learner)를 순차적으로 학습-예측하면서 잘못 예측한 데이터에 가중치 부여를 통해 오류 개선하며 학습하는 방식.

  • 위 그림에서 첫번째 그림과 마지막 그림의 차이점에서 결국 성능입니다.
  • 첫번째에서 오분류는 3개가 발생되었지만, 마지막 그림에서는 오분류가 발생하지 않음을 확인할 수 있다.
  • 산술적으로 가중치는 다음과 같이 부여할 수 있습니다.
    • 첫번째 학습기에 0.3, 두번째 학습기에 0.5, 세번째 학습기에 0.8을 부여합니다.
  • 가중치를 업데이트 방법은 경사하강법을 이용합니다.
  • 오류값은 실제값 - 예측값이다.
  • 경사하강법의 기본 원리는 반복 수행을 통해 오류를 최소화할 수 있도록 가중치의 업데이트 값을 도출하는 기법 정도로 이해하면 좋습니다.

(1) 머신러닝 & 시각화 & 통계 패키지 Loading

  • 종속변수와 독립변수로 구분할 필요가 있다.
import pandas as pd
import numpy as np
import sklearn
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.preprocessing import LabelEncoder

# 03 Chapter에서 추가
# 평가 메트릭
from sklearn.metrics import mean_squared_error, mean_absolute_error

# 시각화
import seaborn as sns
color = sns.color_palette()
sns.set_style('darkgrid')
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt  # Matlab-style plotting

# 통계
from scipy import stats
from scipy.stats import norm, skew #for some statistics

# 06 Chapter 추가
from sklearn.base import BaseEstimator, TransformerMixin, RegressorMixin, clone
from sklearn.model_selection import KFold, cross_val_score, train_test_split, GridSearchCV
from sklearn.metrics import mean_squared_error
/usr/local/lib/python3.6/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
  import pandas.util.testing as tm
print('Pandas : %s'%(pd.__version__))
print('Numpy : %s'%(np.__version__))
print('Scikit-Learn : %s'%(sklearn.__version__))
print('seaborn : %s'%(sns.__version__))
print('matplotlib : %s'%(matplotlib.__version__))
!python --version
Pandas : 1.0.5
Numpy : 1.18.5
Scikit-Learn : 0.22.2.post1
seaborn : 0.10.1
matplotlib : 3.2.2
Python 3.6.9

(2) 날짜 데이터 전처리

  • 데이터 전처리 기본함수 작성
  • 우선 날짜 처리 진행
    • year & month로 구분한다.
def grap_year(data):
    data = str(data)
    return int(data[:4])

def grap_month(data):
    data = str(data)
    return int(data[4:])
# 날짜 처리
data = train.copy()
data = data.fillna('')
print(data['REG_YYMM'].head())
0    201906
1    201902
2    201911
3    202002
4    202001
Name: REG_YYMM, dtype: int64
data['year'] = data['REG_YYMM'].apply(lambda x: grap_year(x))
data['month'] = data['REG_YYMM'].apply(lambda x: grap_month(x))
data = data.drop(['REG_YYMM'], axis=1)
data.head()

(3) 시군구 컬럼 제거

  • submission 제출 파일 목록에서 시/도는 해당되나, 시군구 항목은 해당되지 않는다.
  • 따라서, 해당 컬럼은 삭제한다.
# 데이터 정제
df = data.drop(['CARD_CCG_NM', 'HOM_CCG_NM'], axis=1)
columns = ['CARD_SIDO_NM', 'STD_CLSS_NM', 'HOM_SIDO_NM', 'AGE', 'SEX_CTGO_CD', 'FLC', 'year', 'month']
df = df.groupby(columns).sum().reset_index(drop=False)

(4) 라벨 인코딩

  • 사이킷런의 ML알고리즘은 결측치가 허용되지 않는다.
  • 사이킷런의 머신러닝 알고리즘은 문자열 값을 입력 값으로 허용하지 않는다.
    • 따라서, 이를 숫자형으로 변환해야 한다.
    • 이를 데이터 인코딩이라 부른다.
  • 데이터 인코딩에는 크게 두가지 있다.
    • 레이블 인코딩 VS. 원-핫 인코딩
  • 레이블 인코딩은 카테고리 피처를 코드형 숫자 값으로 변환한다.
    • 이 때, 일괄적으로 숫자로 변환이 되면 선형회귀와 같은 ML 알고리즘에는 적용하지 않는다. 이유는 숫자 값의 경우 크고 작음에 대한 특성이 작용한다.
    • 그러나, 본 예제에서는 주로 트리 계열을 알고리즘을 사용할 것이기 때문에 크게 상관은 없다.
  • 원핫 인코딩은 피처 값의 유형에 따라 새로운 피처를 추가해 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시한다.
  • 본 실습에서는 주로 라벨 인코딩만 사용하고 진행해본다.
# 인코딩
dtypes = df.dtypes
encoders = {}
for column in df.columns:
    if str(dtypes[column]) == 'object':
        encoder = LabelEncoder()
        encoder.fit(df[column])
        encoders[column] = encoder
        
df_num = df.copy()        
for column in encoders.keys():
    encoder = encoders[column]
    df_num[column] = encoder.transform(df[column])

(5) 데이터셋 분리

  • Train 데이터를 분리해서 Validate 데이터셋을 생성한다.
from sklearn.model_selection import train_test_split

X_data, y_data = df_num.loc[:, df_num.columns != 'AMT'], df.loc[:, df_num.columns == 'AMT']
y_target = y_data['AMT']
X_data = X_data.drop(['CSTMR_CNT', 'CNT'], axis=1)
X_train, X_test, y_train, y_test = train_test_split(X_data, y_target, test_size=0.3, random_state=126, shuffle=True)
  • test_size: 훈련데이터와 테스트 데이터로 나누는 비율이다. 디폴트는 0.25이며, 이는 25%에 해당한다.
  • random_state: 동일한 학습/테스트용 데이터 세트를 생성하기 위해 주어지는 난수 값. (실험의 재현성)
  • shuffle: 데이터 분리 전 데이터를 섞을지 결정함. 디폴트는 True이며, 데이터를 분산시켜서 좀 더 효율적인 학습 및 테스트 데이터 세트를 만드는 데 사용함.

(6) 종속 변수 확인

  • 라벨 인코딩을 진행하기 전에 타겟변수(=종속변수)의 모양을 확인한다.
  • Target값의 분포가 왜곡되면, 예측 성능이 저하되는 경우가 발생한다.
  • 빠르게 시각화를 진행해서 확인해보자.
sns.distplot(y_train , fit=norm);
(mu, sigma) = norm.fit(y_train)
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],
            loc='best')
plt.ylabel('Frequency')
plt.title('AMT distribution')

fig = plt.figure()
res = stats.probplot(y_train, plot=plt)
plt.show()
 mu = 3415650.33 and sigma = 21861749.83

png

png

  • 위 결과가 말해주듯이 타겟변수를 정규화 하는 작업이 필요합니다.
  • 정규화 작업은 1줄이면 가능합니다.
y_train = np.log1p(y_train)

sns.distplot(y_train , fit=norm);
(mu, sigma) = norm.fit(y_train)
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],
            loc='best')
plt.ylabel('Frequency')
plt.title('AMT distribution')

fig = plt.figure()
res = stats.probplot(y_train, plot=plt)
plt.show()
 mu = 12.83 and sigma = 1.82

png

png

  • 로그를 Target값을 변환한 후에 정규분포를 이루는 것을 확인하였습니다.

III. 교차검증 및 하이퍼 파라미터 튜닝

  • 본 장에서는 교차검증을 실시하는 목적과 파라미터 튜닝에 대해 배울 것이다.

(1) 교차검증의 목적

  • 가장 중요한 목적은 과적합(Overfitting)을 방지하기 위해서다.
  • 과적합은 무엇인가?
    • 가령, 교과서 범위 내의 있는 문제는 잘 풀지만, 모의고사 또는 수능처럼 약간 응용된 문제가 나오면 잘 풀지 못하는 것과 유사하다.
    • 즉, 고정된 학습데이터와 테스트 데이터로 평가 하다 보니, 테스트 데이터에만 최적의 성능을 발휘할 수 있도록 편향되게 모델이 학습하게 되는 현상을 말한다.
    • 이러한 문제점을 개선하기 위해 교차 검증을 이용해 더 다양한 학습과 평가를 한다.
    • 즉, 본 게임에 앞서서 여러번의 사전 모의평가를 진행하는 것과 유사하다.

(2) K폴드 교차 검증

  • K개의 데이터 세트를 만들고, 검증 평가를 반복적으로 수행하는 방법이다.
  • 이 때, 5개의 평가를 평균한 결과를 가지고 예측 성능을 평가한다.
  • 아래 그림을 보자.

  • 기존 처럼, 훈련/테스트 데이터로 분리 한다.
  • 그리고, 해당 모델을 처음에 분할하였던 Test Set을 활용하여 평가 한다.
  • 층화추출 K폴드도 있다. 데이터의 분포가 불균형을 이룰 때 적용하면 모형의 예측 성능보다는 보다 안정성 있게 모델이 만들어질 수 있다.

(3) RMSLE 성능 평가 방법

  • 회귀 모형의 대표적인 평가 지표이다.
    • RMSLE: Root Mean Square Log Error는 기존 RMSE에 로그를 적용해준 지표다.
  • 아쉽게도 sklearn에는 해당 옵션이 제공되지 않기 때문에 RMSLE를 수행하는 성능 평가 함수를 직접 만들어 본다.
  • log값 변환 시 NaN등의 이슈로 log가 아닌 log1p()를 이용해 계산한다.
  • 이 때, log1p()로 변환된 값은 np.expm1() 함수로 쉽게 원래의 스케일로 복원될 수 있다.
def rmsle(y, pred): 
  log_y = np.log1p(y)
  log_pred = np.log1p(pred)
  squared_error = (log_y - log_pred)**2
  rmsle = np.sqrt(np.mean(squared_error))
  return print('RMSLE: {0:.3f}'.format(rmsle))

(4) 하이퍼 파라미터 및 튜닝

  • 1차적으로 모형은 GBM만 사용한다.

  • 이 때, 보통 모형 알고리즘에 대한 하이퍼 파라미터 튜닝도 같이 진행하게 된다.

    • n_estimators: weak learner가 순차적으로 오류를 보정함. 개수가 많아지면 성능이 좋아지지만, 수행시간이 오래 발생될 수 있음.
    • learning_rate: 오차를 얼마나 강하게 보정할 것인지 제어
    • max_depth: 복잡도를 너무 높이지 말고 트리의 깊이가 정해진 숫자보다 깊어지지 않게 함.
    • min_samples_leaf: Leaf Node가 되기 위한 최소 샘플 개수
    • loss: huber, 이상치에 민감하지 않도록 보정해주는 함수.
    • max_features: 다차원 독립 변수 중 선택할 차원의 수를 의미함
  • 이 때, 보통 최적의 하이퍼 파라미터 튜닝을 위해 값을 조정해서 알고리즘의 예측 성능을 개선한다고 했다.

  • GridSearchCVRandomizedSearchCV로 구분되는데, 이 부분에 대한 설명은 다음 Chapter에서 진행하도록 한다.

    • 간단하게 설명하면, 파라미터를 순차적으로 변경하면서 최고의 성능을 가지는 파라미터 조합을 찾는 과정이다.

(5) 교차검증 + 하이퍼 파라미터

  • 지금까지 설명한 내용을 코드로 작성한다.
  • 이 때, 최적의 파라미터를 찾기 위한 과정도 추가했다.
# 모델 선언 
gbm = GradientBoostingRegressor(random_state=0)

# 파라미터 지정 
gbm_parameters = {'max_depth':[2, 3, 5, 10], 
                  'min_samples_split':[2, 3, 5], 
                  'min_samples_leaf':[1,5,8]}

# 교차검증 수행
grid_gbm = GridSearchCV(gbm, param_grid=gbm_parameters, scoring="neg_mean_squared_error", cv = 5)
grid_gbm.fit(X_train, y_train)
GridSearchCV(cv=5, error_score=nan,
             estimator=GradientBoostingRegressor(alpha=0.9, ccp_alpha=0.0,
                                                 criterion='friedman_mse',
                                                 init=None, learning_rate=0.1,
                                                 loss='ls', max_depth=3,
                                                 max_features=None,
                                                 max_leaf_nodes=None,
                                                 min_impurity_decrease=0.0,
                                                 min_impurity_split=None,
                                                 min_samples_leaf=1,
                                                 min_samples_split=2,
                                                 min_weight_fraction_leaf=0.0,
                                                 n_estimators=100,
                                                 n_iter_no_change=None,
                                                 presort='deprecated',
                                                 random_state=0, subsample=1.0,
                                                 tol=0.0001,
                                                 validation_fraction=0.1,
                                                 verbose=0, warm_start=False),
             iid='deprecated', n_jobs=None,
             param_grid={'max_depth': [2, 3, 5, 10],
                         'min_samples_leaf': [1, 5, 8],
                         'min_samples_split': [2, 3, 5]},
             pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
             scoring='neg_mean_squared_error', verbose=0)
print("GridSearchCV 최적 하이퍼 파라미터 :", grid_gbm.best_params_)

rmse = np.sqrt(-1*grid_gbm.best_score_)
print("GridSearchCV 최적 평균 RMSE값 :", np.round(rmse, 4))
GridSearchCV 최적 하이퍼 파라미터 : {'max_depth': 10, 'min_samples_leaf': 8, 'min_samples_split': 2}
GridSearchCV 최적 평균 RMSE값 : 1.4327
gbm_best = grid_gbm.best_estimator_
eval_pred = gbm_best.predict(X_test)
eval_pred = np.expm1(eval_pred)
rmsle(y_test, eval_pred)
RMSLE: 1.421
  • 위 모형을 평가한 결과 1.421이 나왔다.
  • 그러나, 위 평가모형 결과가 실제와 똑같을 거라 생각하면 안된다.
  • 다만, 최종 제출폼을 작성하기에 앞서서, 중간에 평가 측정표를 검증하여 마지막 모형을 선정하는 중간 지표로 삼기에는 적절하다.

(6) 예측 템플릿 작성

  • 예측 템플릿을 작성한다.
from itertools import product

# 예측 템플릿 만들기
CARD_SIDO_NMs = df_num['CARD_SIDO_NM'].unique()
STD_CLSS_NMs  = df_num['STD_CLSS_NM'].unique()
HOM_SIDO_NMs  = df_num['HOM_SIDO_NM'].unique()
AGEs          = df_num['AGE'].unique()
SEX_CTGO_CDs  = df_num['SEX_CTGO_CD'].unique()
FLCs          = df_num['FLC'].unique()
years         = [2020]
months        = [4, 7]

comb_list = [CARD_SIDO_NMs, STD_CLSS_NMs,HOM_SIDO_NMs, AGEs, SEX_CTGO_CDs, FLCs, years, months]
temp = np.array(list(product(*comb_list)))

train_features = df_num.drop(['CSTMR_CNT', 'AMT', 'CNT'], axis=1)
temp = pd.DataFrame(data=temp, columns=train_features.columns)
  • 예측된 결과를 데이터프레임에 제출한다.
# 예측
pred = gbm_best.predict(temp)
pred = np.expm1(pred)
temp['AMT'] = np.round(pred, 0)
temp['REG_YYMM'] = temp['year']*100 + temp['month']
temp = temp[['REG_YYMM', 'CARD_SIDO_NM', 'STD_CLSS_NM', 'AMT']]
temp = temp.groupby(['REG_YYMM', 'CARD_SIDO_NM', 'STD_CLSS_NM']).sum().reset_index(drop=False)
  • 라벨 인코딩 했던 부분을 제출을 위해 다시 디코딩하는 작업을 진행한다.
# 디코딩 
temp['CARD_SIDO_NM'] = encoders['CARD_SIDO_NM'].inverse_transform(temp['CARD_SIDO_NM'])
temp['STD_CLSS_NM'] = encoders['STD_CLSS_NM'].inverse_transform(temp['STD_CLSS_NM'])
print(temp.head())
   REG_YYMM CARD_SIDO_NM           STD_CLSS_NM          AMT
0    202004           강원            건강보조식품 소매업  290440460.0
1    202004           강원               골프장 운영업  460111841.0
2    202004           강원           과실 및 채소 소매업  137966925.0
3    202004           강원     관광 민예품 및 선물용품 소매업   72254352.0
4    202004           강원  그외 기타 분류안된 오락관련 서비스업   74292633.0

(7) Submission 파일 작업 및 내보내기

submission = client.query('''
  SELECT 
    * 
  FROM `your_project_id.jeju_data_ver1.submission` 
  ''').to_dataframe()
submission = submission.drop(['AMT'], axis=1)
submission = submission.merge(temp, left_on=['REG_YYMM', 'CARD_SIDO_NM', 'STD_CLSS_NM'], right_on=['REG_YYMM', 'CARD_SIDO_NM', 'STD_CLSS_NM'], how='left')
submission['AMT'] = submission['AMT'].fillna(0)
print(submission.head())
   id  REG_YYMM CARD_SIDO_NM           STD_CLSS_NM          AMT
0   0    202004           강원            건강보조식품 소매업  290440460.0
1   1    202004           강원               골프장 운영업  460111841.0
2   2    202004           강원           과실 및 채소 소매업  137966925.0
3   3    202004           강원     관광 민예품 및 선물용품 소매업   72254352.0
4   4    202004           강원  그외 기타 분류안된 오락관련 서비스업   74292633.0
submission.to_csv('submission.csv', encoding='utf-8-sig', index=False)

III. 평가지표

Chapter No. 제출일시 RMSLE Score in Test Final Score
02 2020-06-28 23:28:27 - 15.6796527331
03 2020-06-29 17:48:23 - 6.xxx
04 2020-06-30 15:11:28 1.494 7.xxx
05 2020-07-01 00:00:16 1.398 6.xxx
06 2020-07-01 01:50:57 1.421 7.xxx
  • 중간 결과표를 보면 알 수 있듯이, 2.2GB Raw 데이터에서 추출을 이미 Random Sampling을 통해서 가져와서 진행하였기 때문에 훈련 데이터와 테스트 데이터를 나누는 것 자체가 성능 향상에는 큰 향상이 없음을 알 수 있다.
  • 이 때, 중요한 것은 종속변수에 log_transformation을 통해 데이터 정규화를 진행하는 것이 오히려 성능 향상에 더 좋은 결과를 가져 왔음을 알 수 있다.
  • 교차검증과 하이퍼 파라미터를 수행한다고 해도 결과가 크게 달라지지 않음을 볼 수 있다.
  • 그럼 이번에는 최신기법인 xgboost으로 모형을 바꿔서 제출하면 결과가 달라질까?
    • 정말, 이러한 기법들을 사용하면 평가지표는 달라질까?
    • Kaggle 노트북을 보더라도 다양한 테크닉과 솔루션, 그리고 알고리즘을 소개하는 여러 기법들이 있다.

IV. What’s Next

  • 교차검증과, 최적의 하이퍼 파라미터를 조정하는 방법에 대해 배웠다.
  • 긴 시간 동안 학습을 한다고 할지라도 모형에는 큰 변화가 없음을 확인했다.
  • 이 쯤 되면 감이 빨리 오신 분이 있으면 좋겠다. (데이터 전처리의 중요성!)
  • 그러나, 우선 데이터 전처리를 하기에 앞서서 다른 알고리즘을 써보자.
    • xgboost를 같이 사용하도록 해본다.
    • 데이터 전처리를 하지 않고 진행할 에정이다.