데이콘 대회 참여 - 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를 같이 사용하도록 해본다.
    • 데이터 전처리를 하지 않고 진행할 에정이다.