AIFFEL(220118)_나의 첫 번째 캐글 경진대회, 무작정 따라해보기

케글코리아(Kaggle Korea)가 2019년에 주최했던 캐글 경진대회인 2019 2nd ML month with KaKR를 처음부터 끝까지 경험해보자.


학습목표

  1. 데이터 사이언스 관련 최대 커뮤니티인 캐글의 경진대회에 직접 참여해서 문제를 해결해본다.
  2. 캐글에서 데이터를 내려받는 것으로부터 시작해서, 로컬 서버에서 자유롭게 다루어보며 문제 해결을 위한 고민을 해본다.
  3. 앙상블 기법의 개념과 강점을 이해하고, 여러 모델의 예측 결과를 Averaging 한 최종 결과로 캐글에 제출해본다.
  4. 하이퍼 파라미터 튜닝의 필요성과 의미를 이해하고, Grid Search, Random Search 등의 기법을 알아본다.
  5. Grid Search 기법을 활용해서 직접 하이퍼 파라미터 튜닝 실험을 해보고, 모델의 성능을 최대한 끌어올려본다.

📖 대회의 시작_(1) 참가 규칙, 평가 기준 살펴보기

Description, 대회 소개


캐글에는 아주 다양한 경진대회들이 있고, 각 경진대회들은 모두 대회 소개, 데이터셋 소개, 규칙 설명 등 대회에 참가하는 사람들을 위한 세부 내용들로 구성되어 있다.

대회별로 평가 방법과 규칙이 상이하니, 대회를 본격적으로 시작하기 전에 소개와 세부 내용을 잘 읽고 시작하는 것이 좋다.

Evaluation, 점수 평가 기준


대회에서 각 참여자들의 점수를 어떤 기준으로 평가하는지도 매우 중요한 부분이다.

왼쪽 탭에서 Evaluation을 누르면 평가 방식은 *RMSE라고 한다.
* RMSE(Root Mean Squared Error) : 실제정답과 예측한 값의 차이의 제곱을 평균한 값의 제곱근

1N(ytypr)2{\sqrt{ {1 \over N} \sum{(yt - y{pr})}^2}}

이번 대회에서 다루는 문제는 "집값"을 예측하는 문제이기 때문에 예측해야 하는 값과 실제 정답값이 모두 실숫값이다.

그 두 가지 값의 차이를 사용해 얼마나 떨어져 있는지 계산 할 목적으로 RMSE를 평가 척도로 사용한다.

"집값"을 예측하는 문제는 어떤 카테고리에 속하는지를 맞추는 문제가 아니라, 실수값을 맞추는 문제이므로 회귀 문제이다.

Prize, 상품


상위 리더보드 100명, 즉 100등까지 상품을 주는데, 조건이 특이하다. 대회가 마무리된 후 사용한 소스코드를 커널 항목에 공개해야 하는 의무가 있다. 지식의 "공유"와 "공개"를 지향하는 캐글의 정신을 본따 정해진 규칙이다.

Timeline, 대회 일정


실제 대회를 나간다면 대회 일정을 숙지하는 것도 중요하다. 상금을 노리고 있다면 대회의 공식 마감일 전에 여러 실험을 통해 성능을 끌어올려야한다.

Rules, 대회 규칙


많은 사람이 참가하고, 또 상품이 걸려있는 만큼 엄격한 규칙들이 있을 수 있다. 부정행위를 통해 얻은 점수는 무효가 될 뿐만 아니라 향후 캐글의 다른 대회에 참가하는 것에도 불이익이 있을 수 있다.

📖 대회의 시작_(2) 데이터 살펴보기

Data Description, 데이터 설명


다음은 데이터에 대한 설명이다. 잡한 데이터를 다루는 대회일수록, 데이터의 설명을 굉장히 꼼꼼하게 읽는 것도 중요하다. 데이터를 잘 이해할수록, 더 좋은 결과를 낼 수 있다.

다양한 컬럼들을 가지고 있는 데이터. 여기서 예측해야 하는 컬럼은 price, 즉 집의 가격이다.

Data Explorer, 데이터 파일


데이터셋 자체에 대한 설명 외에도, 우리가 다운받아야 할 데이터 파일에 대한 형태를 살펴봐야 한다.

이 대회에서는 train.csv라는 모델 학습용 파일과, test.csv라는 테스트용 파일, 그리고 sample_submission.csv라는 제출용 파일이 제공된다.

train.csv를 활용해서 데이터를 뜯어보고 모델을 학습시킨 후, test.csv 파일의 데이터에 대해 price를 예측해서 sample_submission.csv의 형식에 맞는 형태로 캐글에 제출을 해볼 것이다.

위 화면에서 Download 버튼을 누르면 데이터를 다운 받을 수 있지만 이번 대회에서는 데이터가 중간에 한 번 변경되었다. 따로 준비된 파일을 이용한다.

📖 일단 제출하고 시작해! Baseline 모델_(1) Baseline 셋팅하기

이번 대회에서는 주최자 차원에서 Baseline을 제공했다. Baseline이라 함은 기본적으로 문제 해결을 시작할 때 쉽게 사용해볼 수 있는 샘플을 이야기한다.

보통 대회에서 Baseline은 제공이 되는 경우도, 아닌 경우도 있는데 이번 대회는 특히 교육적인 성격도 있어서 제공이 되었던 것 같다.

2019 ML month 2nd baseline 커널

다른 사람의 커널을 ipynb 파일로 다운받아 사용하기


캐글의 커널(Kernel) 은 우리가 쓰는 주피터 노트북 형태의 파일이 캐글 서버에서 실행될 때 그 프로그램을 일컫는 개념이다.

캐글 자체의 서버에서 baseline 노트북 파일을 돌리고 모델 학습을 시킬 수도 있는데, 아래와 같이 보이는 화면에서 Copy and Edit 버튼을 클릭해본다.

Copy and Edit 버튼을 클릭했다면 다음과 같은 웹상에서 코드를 돌려볼 수 있는 커널 창이 뜬다.

이렇게 커널 창 위에서 그대로 진행해도 되지만, 노트북 파일을 다운로드하여 사용해본다. File > Download 를 통해 커널을 ipynb 파일로 다운받아준다.

Baseline 커널 파일 실행 준비


Baseline 노트북 파일을 다운받았으니 직접 돌려보고 점수까지 내본다. 다만 Baseline의 모든 코드를 에러없이 잘 돌리기 위해서는 몇 가지 준비가 필요하다.

✓ 데이터 파일을 현재 디렉토리로 옮기기
먼저, 당연히 데이터를 노트북 파일과 같은 폴더 내에 두어야한다. 혹시 다른 위치에 압축을 해제했다면, 모델 학습 및 예측에 필요한 파일이 들어있는 데이터 폴더를 아래와 같은 형태로 노트북과 같은 위치로 옮겨야한다.

✓ 필요한 라이브러리 설치하기
다음은 Baseline이 사용하는 몇 가지 라이브러리를 설치하는 법을 알아본다. 이번 대회에서는 회귀 모델을 구현하는 데에 사용하는 xgboostlightgbm 라이브러리와, 결측 데이터를 확인하는 missingno 라이브러리가 필요하다.

✓ Jupyter Notebook 파일 실행 후 matplotlib 시각화를 위해 다음 셀 실행하기
마지막으로, 위에서 다운받았던 2019-ml-month-2nd-baseline.ipynb 주피터 노트북 파일을 실행한다.
Baseline 커널에는 다양한 시각화 코드가 있기 때문에 노트북의 맨 위에 아래의 코드를 실행시켜서 시각화 그래프가 나타날 수 있도록한다.

import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

📖 일단 제출하고 시작해! Baseline 모델_(2) 라이브러리, 데이터 가져오기

이제부터는 실제 Baseline 커널에 있는 내용이다. 대다수의 코드는 Baseline 커널에 있는 그대로이고, 중간중간 필요한 부분에 한해 부가설명이 추가되어있다.

✓ 필요한 라이브러리 import 하기
전체 코드를 실행시키는 데에 필요한 모든 라이브러리는 한 번에 가져온다.

import warnings
warnings.filterwarnings("ignore")

import os
from os.path import join

import pandas as pd
import numpy as np

import missingno as msno

from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import KFold, cross_val_score
import xgboost as xgb
import lightgbm as lgb

import matplotlib.pyplot as plt
import seaborn as sns

print('얍💢')

✓ 데이터 경로 지정하기
그다음은 데이터의 경로를 지정한다. 여기서 주의해야 할 점은 우리의 파일 경로는 Baseline 커널과 다르다는 점이다.

파이썬 공식 문서 - os.path.join

Baseline 커널은 위에서 말했던 캐글의 서버에서 돌아가도록 코드가 설계되었기 때문에 데이터가 아래와 같이 ../input 이라는 디렉토리에 위치한다.
1) Baseline 커널의 기존 코드

train_data_path = join('../input', 'train.csv')
sub_data_path = join('../input', 'test.csv')

하지만 우리는 프로젝트 디렉토리(~/aiffel/kaggle_kakr_housing 등) 내 data 폴더에 있는 파일을 사용하기로 했으므로 다음과 같이 바꿔준다.

2) LMS에서 사용할 때 알맞은 파일 경로

data_dir = os.getenv('HOME')+'/aiffel/kaggle_kakr_housing/data'

train_data_path = join(data_dir, 'train.csv')
sub_data_path = join(data_dir, 'test.csv')      # 테스트, 즉 submission 시 사용할 데이터 경로

print(train_data_path)
print(sub_data_path)
# 실행결과
/aiffel/aiffel/kaggle_kakr_housing/data/train.csv
/aiffel/aiffel/kaggle_kakr_housing/data/test.csv

📖 일단 제출하고 시작해! Baseline 모델_(3) 데이터 이해하기

✓ 데이터 살펴보기
Baseline 노트북은 먼저 아래와 같이 데이터를 살펴보고 있다. 각 변수들이 나타내는 의미를 읽어 본다.

1. ID : 집을 구분하는 번호
2. date : 집을 구매한 날짜
3. price : 타겟 변수인 집의 가격
4. bedrooms : 침실의 수
5. bathrooms : 침실당 화장실 개수
6. sqft_living : 주거 공간의 평방 피트
7. sqft_lot : 부지의 평방 피트
8. floors : 집의 층수
9. waterfront : 집의 전방에 강이 흐르는지 유무 (a.k.a. 리버뷰)
10. view : 집이 얼마나 좋아 보이는지의 정도
11. condition : 집의 전반적인 상태
12. grade : King County grading 시스템 기준으로 매긴 집의 등급
13. sqft_above : 지하실을 제외한 평방 피트
14. sqft_basement : 지하실의 평방 피트
15. yr_built : 집을 지은 년도
16. yr_renovated : 집을 재건축한 년도
17. zipcode : 우편번호
18. lat : 위도
19. long : 경도
20. sqft_living15 : 2015년 기준 주거 공간의 평방 피트(집을 재건축했다면, 변화가 있을 수 있음)
21. sqft_lot15 : 2015년 기준 부지의 평방 피트(집을 재건축했다면, 변화가 있을 수 있음)

집에 대한 다양한 정보가 들어있는 것으로 보인다. 이들의 특징을 활용해서 집의 가격을 맞추어야 한다.

✓ 데이터 불러오기
데이터를 data, sub이라는 변수로 불러온다.

data = pd.read_csv(train_data_path)
sub = pd.read_csv(sub_data_path)
print('train data dim : {}'.format(data.shape))
print('sub data dim : {}'.format(sub.shape))
# 실행결과
train data dim : (15035, 21)
sub data dim : (6468, 20)

학습 데이터는 약 1만 5천 개, 테스트 데이터는 약 6천 개로 이루어져 있다. 테스트 데이터는 맞추어야 할 집의 가격, price가 없기 때문에 컬럼이 하나 적다.

✓ 학습 데이터에서 라벨 제거하기
price 컬럼은 따로 y라는 변수에 저장한 후 해당 컬럼은 지워준다.

y = data['price']
del data['price']

print(data.columns)
# 실행결과

Index(['id', 'date', 'bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot',
       'floors', 'waterfront', 'view', 'condition', 'grade', 'sqft_above',
       'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode', 'lat', 'long',
       'sqft_living15', 'sqft_lot15'],
      dtype='object')

✓ 학습 데이터와 테스트 데이터 합치기
모델을 학습시키기 전에, 전체 데이터에 대해 탐색해보기 위해 두 데이터를 pd.concat으로 합쳐본다.

물론, 모델 학습을 진행할 때에는 다시 분리해서 사용해야 하기 때문에 데이터를 합치기 전 train_lentraining data의 개수를 저장해서 추후에 학습데이터만 불러올 수 있는 인덱스로 사용하도록 한다.

train_len = len(data)
data = pd.concat((data, sub), axis=0)

print(len(data))
# 실행결과
21503
data.head()

✓ 간단한 전처리
빈 데이터와 전체 데이터의 분포를 확인하는 전처리 작업이다. 결측치, 즉 빈 데이터가 있는지는 위에서 설치했던 missingno 라이브러리를 사용해서 확인한다.
원본 노트북에서는 다음과 같이 설명한다.

각 변수들에 대해 결측 유무를 확인하고, 분포를 확인해보면서 간단하게 전처리를 하겠습니다.
먼저 데이터에 결측치가 있는지를 확인하겠습니다.
missingno 라이브러리의 matrix 함수를 사용하면, 데이터의 결측 상태를 시각화를 통해 살펴볼 수 있습니다.

msno.matrix(data)
# 실행결과
<AxesSubplot:>


위에 출력된 것은 data라는 DataFrame을 매트릭스 모양 그대로 시각화한 것이다. 만약 특정 row, col에 NaN이라는 결측치가 있었다면 해당 부분이 하얗게 나오며, 결측치가 없다면 매트릭스 전체가 까맣게 나온다.

아니면 아래와 같이 직접 결측치의 개수를 출력해서 확인할 수도 있다.
데이터프레임 고급 인덱싱

우선 id를 가지고 데이터프레임 인덱싱을 적용해 본다. 천천히 진행하면 3단계를 거치게 된다.

  • id컬럼이 결측치인지 확인합니다.
  • 결측치인 데이터만 뽑아냅니다.
  • 결측치인 데이터의 개수를 셉니다
# 1. id 컬럼이 결측치인지 확인합니다.
null_check = pd.isnull(data['id'])
print(null_check)
# 실행결과
0       False
1       False
2       False
3       False
4       False
        ...  
6463    False
6464    False
6465    False
6466    False
6467    False
Name: id, Length: 21503, dtype: bool

True와 False로 이루어진 데이터프레임이 출력된다. 이 다음에 이루어지는 것이 인덱싱이다.

# 2. 결측치인 데이터만 뽑아냅니다.
null_data = data.loc[null_check, 'id']
null_data.head()
# 실행결과
Series([], Name: id, dtype: int64)

결측치가 없기 때문에 빈 데이터프레임이 나온다.
인덱싱을 이용하면 데이터프레임을 그대로 사용할 수 있다는 것이 매우 큰 장점이다. 게다가 인덱싱 기능이 속도면에서도 월등히 빠르다.

이제 마지막으로 데이터 개수를 출력해주면 결측치 개수를 확인할 수 있다.

# 3. 결측치인 데이터의 개수를 셉니다.
print('{}: {}'.format('id', len(null_data.values)))
# 실행결과
id: 0

한 번에 작성하기

# 한 번에 뿅!
print('{} : {}'.format('id', len(data.loc[pd.isnull(data['id']), 'id'].values)))

id 컬럼 외에도 모든 컬럼에 적용해야 하니 for문을 사용하면 완성이다.

for c in data.columns:
    print('{} : {}'.format(c, len(data.loc[pd.isnull(data[c]), c].values)))
# 실행결과
id : 0
date : 0
bedrooms : 0
bathrooms : 0
sqft_living : 0
sqft_lot : 0
floors : 0
waterfront : 0
view : 0
condition : 0
grade : 0
sqft_above : 0
sqft_basement : 0
yr_built : 0
yr_renovated : 0
zipcode : 0
lat : 0
long : 0
sqft_living15 : 0
sqft_lot15 : 0

id, date 변수 정리
필요 없는 id 컬럼을 제거한다. 나중에 예측 결과를 제출할 때를 대비하여 sub_id 변수에 id 칼럼을 저장해두고 지운다.

sub_id = data['id'][train_len:]
del data['id']

print(data.columns)
# 실행결과
Index(['date', 'bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors',
       'waterfront', 'view', 'condition', 'grade', 'sqft_above',
       'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode', 'lat', 'long',
       'sqft_living15', 'sqft_lot15'],
      dtype='object')

date 컬럼은 apply 함수로 필요한 부분만 잘라준다.
Pandas Lambda, apply를 활용하여 복잡한 로직 적용하기

data['date'] = data['date'].apply(lambda x : str(x[:6]))

data.head()


여기에서 str(x[:6]) 으로 처리한 것은 20141013T000000 형식의 데이터를 연/월 데이터만 사용하기 위해 201410까지 자르기 위한 것이다.

✓ 각 변수들의 분포 확인
전체 데이터들의 분포를 확인한다. 특히 너무 치우친 분포를 가지는 컬럼의 경우 모델이 결과를 예측하는 데에 좋지 않은 영향을 미치므로 다듬는 작업을 한다.

아래 시각화 코드를 통해 id 컬럼을 제외한 19개 컬럼에 대해 한 번에 모든 그래프를 그려준다. 10행 2열의 subplot에 그래프를 그리기 위해 2중 for문을 사용하고 있다.

그래프의 종류는 sns.kdeplot을 사용한다. kdeplot은 이산(discrete) 데이터의 경우에도 부드러운 곡선으로 전체 분포를 확인할 수 있도록 하는 시각화 함수이다.

seaborn.kdeplot

아래 코드로 전체 그래프를 그려본다.

fig, ax = plt.subplots(9, 2, figsize=(12, 50))   # 가로스크롤 때문에 그래프 확인이 불편하다면 figsize의 x값 조절 가능

# id 변수(count==0인 경우)는 제외하고 분포 확인
count = 1
columns = data.columns
for row in range(9):
    for col in range(2):
        sns.kdeplot(data=data[columns[count]], ax=ax[row][col])
        ax[row][col].set_title(columns[count], fontsize=15)🔎
        count += 1
        if count == 19 :
            break

위 그래프 중에서는 bedrooms, sqft_living, sqft_lot, sqft_above, sqft_basement, sqft_living15, sqft_lot15 변수가 한쪽으로 치우친 경향을 보인다.

이렇게 한 쪽으로 치우친 분포의 경우에는 로그 변환(log-scaling)을 통해 데이터 분포를 정규분포에 가깝게 만들 수 있다.

아래와 같이 치우친 컬럼들을 skew_columns 리스트 안에 담고, 모두 np.log1p()를 활용해서 로그 변환을 해준다. numpy.log1p() 함수는 입력 배열의 각 요소에 대해 자연로그 log(1 + x)을 반환해 주는 함수이다.

skew_columns = ['bedrooms', 'sqft_living', 'sqft_lot', 'sqft_above', 'sqft_basement', 'sqft_lot15', 'sqft_living15']

for c in skew_columns:
    data[c] = np.log1p(data[c].values)

print('얍💢')
fig, ax = plt.subplots(4, 2, figsize=(12, 24))

count = 0
for row in range(4):
    for col in range(2):
        if count == 7:
            break
        sns.kdeplot(data=data[skew_columns[count]], ax=ax[row][col])
        ax[row][col].set_title(skew_columns[count], fontsize=15)
        count += 1


이전보다 훨씬 치우침이 줄어든 분포를 확인할 수 있다.

왜 로그 변환은 분포의 치우침을 줄어들게 만드는 걸까?
이는 로그 함수의 형태를 보면 알 수 있다. 아래의 일반적인 로그 함수를 살펴본다.

xx = np.linspace(0, 10, 500)
yy = np.log(xx)

plt.hlines(0, 0, 10)
plt.vlines(0, -5, 5)
plt.plot(xx, yy, c='r')
plt.show()

<로그함수의 특징>

  • 0<x<10<x<1
  • 따라서 0에 가깝게 모여있는 값들이 xx로 입력되면, 그 함수값인 yy 값들은 매우 큰 범위로 벌어지게 된다. 즉, 로그 함수는 0에 가까운 값들이 조밀하게 모여있는 입력값을, 넓은 범위로 펼칠 수 있는 특징을 가진다.
  • 반면, xx값이 점점 커짐에 따라 로그 함수의 기울기는 급격히 작아진다. 이는 곧 큰 xx값들에 대해서는 yy값이 크게 차이나지 않게 된다는 뜻이고, 따라서 넓은 범위를 가지는 xx를 비교적 작은 yy값의 구간 내에 모이게 하는 특징을 가진다.

위와 같은 특성 때문에 한 쪽으로 몰려있는 분포에 로그 변환을 취하게 되면 넓게 퍼질 수 있는 것이다.

그렇다면 맞추어야 할 타겟인 집의 가격, 즉 data[price]의 분포를 로그 변환했을 때 결과를 유추해보자.

원래 price의 분포

sns.kdeplot(y)
plt.show()

위 분포를 log 변환하게 되면 그 분포는 어떤 모양을 가지게 될까?

  • 위 분포는 0 쪽으로 매우 심하게 치우쳐져 있는 분포를 보인다.
  • 즉, 0과 1000000 사이에 대부분의 값들이 몰려있고, 아주 소수의 집들이 굉장히 높은 가격을 보인다.
  • 따라서 이 분포에 로그 변환을 취하면, 0에 가깝게 몰려있는 데이터들은 넓게 퍼질 것이고, 매우 크게 퍼져있는 소수의 데이터들은 작은 y값으로 모일 것이다.
  • 즉, 왼쪽으로 치우친 값들은 보다 넓은 범위로 고르게 퍼지고 오른쪽으로 얇고 넓게 퍼진 값들은 보다 작은 범위로 모이게 되므로 전체 분포는 정규분포의 형상을 띄는 방향으로 변환될 것이다.

확인해보자

y_log_transformation = np.log1p(y)

sns.kdeplot(y_log_transformation)
plt.show()

여기까지 로그 변환이 필요한 데이터에 대해 처리를 마무리하였으니, 아래와 같이 전체 데이터를 다시 나누어 준다.

앞에서 저장해두었던 train_len을 인덱스로 활용해서 :train_len까지는 학습 데이터, 즉 x에 저장하고, train_len: 부터는 실제로 추론을 해야 하는 테스트 데이터, 즉 sub 변수에 저장한다.

sub = data.iloc[train_len:, :]
x = data.iloc[:train_len, :]

print(x.shape)
print(sub.shape)
# 실행결과
(15035, 19)   # 'id'와 'price'가 사라져서 19
(6468, 19)

📖 일단 제출하고 시작해! Baseline 모델_(4) 모델 설계

✓ 모델링
이제 본격적으로 학습시킬 모델을 준비한다. Baseline 커널에서는 여러 가지 모델을 함께 사용해서 결과를 섞는, 블렌딩(blending) 이라는 기법을 활용한다.

블렌딩이란 하나의 개별 모델을 사용하는 것이 아니라 다양한 여러 모델을 종합하여 결과를 얻는 기법이다. 블렌딩은 앙상블 기법이라고 하기도 하는데, 아래 링크를 통해 자세히 알아보자.

Part 1. Introduction to Ensemble Learning

Q. 앙상블이란 무엇이고, 어떤 아이디어로부터 착안된 기법인가?
A. 앙상블(Ensemble) 학습은 여러 개의 학습 알고리즘을 사용하고, 그 예측을 결합함으로써 보다 정확한 최종 예측을 도출하는 기법이다.
하나의 강한 머신러닝 알고리즘보다 여러 개의 약한 머신러닝 알고리즘이 낫다는 아이디어로부터 시작되었다.

Q. 앙상블 기법 중 가장 기본적인 것은 보팅(Voting)과 에버리징(Averaging)이다. 각각은 무엇이고, 언제 쓰이는가?
A. 이 둘은 모두 서로 다른 알고리즘을 가진 분류기를 결합하는 방식이다.
Voting은 여러 모델이 분류해 낸 결과들로부터 말 그대로 다수결 투표를 통해 최종 결과를 선택하는 방법으로, 분류 문제에서 사용된다.
반면, Averaging은 각 모델이 계산해 낸 실수값들을 평균 혹은 가중평균하여 사용하는 방법으로, 회귀 문제에서 사용된다.

앙상블은 강력한 개별 모델 하나보다 약한 여러 개의 모델의 결과를 합치는 것이 낫다는 기본 전제로부터 시작되는데, 그 전제가 실제로 성립한다면, 그 이유는 무엇일까?

Kaggle Ensemble Guide

Q. 각각 성능이 70%(0.7)인 다섯 개의 분류기가 있다고 가정한다.
이 때 이 다섯 개의 모델이 예측한 결과에 다수결 투표를 진행하여 최종 결과를 결정한다면 예상 정확도는 얼마일까?
A 다섯 개의 분류기가 전부 다 틀릴 확률은 0.3^5 = 0.0024 이다.
하나의 분류기만 맞고, 네 개의 분류기가 틀릴 확률은 5C_1 × 0.3^4 × 0.7 = 0.0284 이다.
두 개의 분류기만 맞고, 세 개의 분류기가 틀릴 확률은 5C_2 × 0.3^3 × 0.7^2 = 0.1323 이다.
세 개의 분류기만 맞고, 두 개의 분류기가 틀릴 확률은 5C_3 × 0.3^2 × 0.7^3 = 0.3087 이다.
네 개의 분류기가 맞고, 한 개의 분류기가 틀릴 확률은 5C_4 × 0.3 × 0.7^4 = 0.3602 이다.
다섯 개의 분류기가 모두 맞을 확률은 0.7^5 = 0.1681 이다.
(확인 : 위의 여섯가지의 확률의 총합은 0.0024 + 0.0284 + 0.1323 + 0.3087 + 0.3602 + 0.1681 = 1 이 맞다)
위의 여섯 가지 중, 다수결을 통해 최종 예측을 결정을 할 때 옳은 예측을 하려면 최소 세 개 이상의 분류기가 맞아야 한다. 즉, 세 개 이상의 분류기가 맞을 확률의 총 합은 0.3087 + 0.3602 + 0.1681 =0.837, 즉 83.7%입니다. 이는 개별 분류기의 성능인 70%보다 훨씬 높은 성능을 보인다.

간단한 산술을 통해, 어느 정도의 성능을 보이는 개별 모델의 결과를 종합하면 보다 나은 결과를 만들어낼 수 있다는 것을 확인할 수 있다.

이번에 회귀 문제를 풀고 있으므로, 위 분류기의 앙상블처럼 투표로 정하는 대신 예측 결과를 평균 내어 활용할 예정이다.

✓ Average Blending
여러 가지 모델의 결과를 산술평균하여 블렌딩 모델을 만든다. 모델은 부스팅 계열인 gboost, xgboost, lightgbm 세 가지를 사용한다.

gboost = GradientBoostingRegressor(random_state=2019)
xgboost = xgb.XGBRegressor(random_state=2019)
lightgbm = lgb.LGBMRegressor(random_state=2019)

models = [{'model':gboost, 'name':'GradientBoosting'}, {'model':xgboost, 'name':'XGBoost'},
          {'model':lightgbm, 'name':'LightGBM'}]

print('얍💢')

✓ Cross Validation
교차 검증을 통해 모델의 성능을 간단히 평가해본다.

def get_cv_score(models):
    kfold = KFold(n_splits=5).get_n_splits(x.values)
    for m in models:
        CV_score = np.mean(cross_val_score(m['model'], X=x.values, y=y, cv=kfold))
        print(f"Model: {m['name']}, CV score:{CV_score:.4f}")
print('얍💢')
get_cv_score(models)
# 실행결과
Model: GradientBoosting, CV score:0.8598
Model: XGBoost, CV score:0.8860
Model: LightGBM, CV score:0.8819

✓ Make Submission File
cross_val_score() 함수는 회귀모델을 전달할 경우 R2R^2 점수를 반환한다. R2R^2 값은 1에 가까울수록 모델이 잘 학습되었다는 것을 나타낸다. 결정계수 R2R^2 값에 대한 간단한 설명은 아래 링크의 글을 참고한다.

결정계수 R squared

위의 결과를 보니 3개 트리 모델이 모두 훈련 데이터에 대해 괜찮은 성능을 보여주고 있다.

Baseline 모델에서는 다음과 같이 여러 모델을 입력하면 각 모델에 대한 예측 결과를 평균 내어 주는 AveragingBlending() 함수를 만들어 사용한다. AveragingBlending() 함수는 models 딕셔너리 안에 있는 모델을 모두 xy로 학습시킨 뒤 predictions에 그 예측 결괏값을 모아서 평균한 값을 반환한다.

def AveragingBlending(models, x, y, sub_x):
    for m in models : 
        m['model'].fit(x.values, y)
    
    predictions = np.column_stack([
        m['model'].predict(sub_x.values) for m in models
    ])
    return np.mean(predictions, axis=1)

print('얍💢')

함수를 활용해서 예측값을 생성해본다.

y_pred = AveragingBlending(models, x, y, sub)
print(len(y_pred))
y_pred
# 실행결과
6468
array([ 529966.66304912,  430726.21272617, 1361676.91242777, ...,
        452081.69137012,  341572.97685942,  421725.1231835 ])

적당한 실수값들로 예측을 해낸 것 같다. 그렇다면 이 결과를 캐글에 제출하기 위해 어떻게 해야 할까?

제출해야 하는 csv 파일의 샘플이 바로 data 폴더에 있는 sample_submission.csv이다. 다음 코드로 sample_submission.csv 파일을 확인해본다.

data_dir = os.getenv('HOME')+'/aiffel/kaggle_kakr_housing/data'

submission_path = join(data_dir, 'sample_submission.csv')
submission = pd.read_csv(submission_path)
submission.head()

idprice의 두 가지 열로 구성되어 있다. 이에 맞게 idprice로 구성된 데이터 프레임을 만들어 준다.

result = pd.DataFrame({
    'id' : sub_id, 
    'price' : y_pred
})

result.head()

이제 제출할 일만 남았다. 다음 코드로 submission.csv 파일을 저장한다.

my_submission_path = join(data_dir, 'submission.csv')
result.to_csv(my_submission_path, index=False)

print(my_submission_path)
# 실행결과
/aiffel/aiffel/kaggle_kakr_housing/data/submission.csv

📖 일단 제출하고 시작해! Baseline 모델_(5) 캐글에 첫 결과 제출하기

이번 대회는 이미 끝난 대회이기 때문에 Late Submission만 가능한 상태이다. 아래와 같이 탭에 있는 Late Submission 버튼을 클릭하면 다음과 같은 화면을 만날 수 있다.

위의 화면에서 Step 1에 점선으로 보이는 제출 박스를 클릭하거나, submission.csv 파일을 해당 박스 안에 드래그앤드롭 하면 파일이 업로드된다.

성공적으로 업로드했다면, Step 2에 원하는 메세지를 작성한 후 Make Submission을 클릭한다.

위와 같은 화면이 보인다면 성공이다.

📖 랭킹을 올리고 싶다면?_(1) 다시 한 번, 내 입맛대로 데이터 준비하기

최적의 모델을 찾아서, 하이퍼 파라미터 튜닝


이제 지난 결과를 개선해 본다.

더 좋은 결과를 얻으려면 어떻게 해야 할까? 아직 모델을 하나도 건드려보지 않았다. 이번 스텝에서는 직접 다양한 하이퍼 파라미터를 튜닝해보면서 모델의 성능을 끌어올려 본다.
Q. 머신러닝에서 '파라미터' (혹은 모델 파라미터) 와 '하이퍼 파라미터'는 각각 무엇이고, 어떻게 다를까?
A. 모델 파라미터는 모델이 학습을 하면서 점차 최적화되는, 그리고 최적화가 되어야 하는 파라미터이다. 예를 들어 선형회귀의 경우 y_pred = W*x+b라는 식으로 예측값을 만들어 낼 텐데, 여기에서 모델 파라미터는 W이다. 모델은 학습 과정을 거치면서 최적의 y_pred 값, 즉 y_true에 가장 가까운 값을 출력해낼 수 있는 최적의 W를 찾아나갈 것이다.
반면, 하이퍼파라미터는 모델이 학습을 하기 위해서 사전에 사람이 직접 입력해 주는 파라미터이다. 이는 모델이 학습하는 과정에서 변하지 않는다. 예를 들어 학습 횟수에 해당하는 epoch 수, 가중치를 업데이트할 학습률(learning rate), 또는 선형 규제를 담당하는 labmda 값 등이 이에 해당한다.

다시 한번, 내 입맛대로 데이터 준비하기


Baseline 커널을 열심히 따라 해보았으니, 이제는 주도적으로 다시 데이터를 다뤄본다.

data_dir = os.getenv('HOME')+'/aiffel/kaggle_kakr_housing/data'

train_data_path = join(data_dir, 'train.csv')
test_data_path = join(data_dir, 'test.csv') 

train = pd.read_csv(train_data_path)
test = pd.read_csv(test_data_path)

print('얍💢')

데이터 다시 한 번 살펴보기

train.head()


date 전처리해주기
Baseline 커널이 했던 것과 달리, int, 즉 정수형 데이터로 처리해본다. 이렇게 하면 모델이 date도 예측을 위한 특성으로 활용할 수 있다.

train['date'] = train['date'].apply(lambda i: i[:6]).astype(int)
train.head()


두 번째로 처리해야 할 것은 바로 타겟 데이터에 해당하는 price 컬럼이다. y 변수에 price를 넣어두고, train에서는 삭제한다.

y = train['price']
del train['price']

print(train.columns)
# 실행결과
Index(['id', 'date', 'bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot',
       'floors', 'waterfront', 'view', 'condition', 'grade', 'sqft_above',
       'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode', 'lat', 'long',
       'sqft_living15', 'sqft_lot15'],
      dtype='object')

마지막으로 id 컬럼을 삭제하는 것까지 하면 기본적인 전처리는 모두 마무리된다.

del train['id']

print(train.columns)
# 실행결과 
Index(['date', 'bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors',
       'waterfront', 'view', 'condition', 'grade', 'sqft_above',
       'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode', 'lat', 'long',
       'sqft_living15', 'sqft_lot15'],
      dtype='object')

test 데이터에 대해서도 같은 작업을 진행한다. 단, test에 우리가 맞추어야 할 타겟 데이터인 price는 없으니 훈련 데이터셋과는 다르게 price에 대한 처리는 해주지 않아도 된다.

test['date'] = test['date'].apply(lambda i: i[:6]).astype(int)

del test['id']

print(test.columns)
# 실행결과
Index(['date', 'bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors',
       'waterfront', 'view', 'condition', 'grade', 'sqft_above',
       'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode', 'lat', 'long',
       'sqft_living15', 'sqft_lot15'],
      dtype='object')

타겟 데이터인 y를 한번 확인해본다.

y
# 실행결과
0         221900.0
1         180000.0
2         510000.0
3         257500.0
4         291850.0
           ...    
15030     610685.0
15031    1007500.0
15032     360000.0
15033     400000.0
15034     325000.0
Name: price, Length: 15035, dtype: float64

아주 큰 값들로 이루어져 있다. 가격 데이터의 분포도 한번 확인해본다.

sns.kdeplot(y)
plt.show()


앞서 살펴봤듯이 price는 왼쪽으로 크게 치우쳐 있는 형태를 보인다.

따라서 ynp.log1p() 함수를 통해 로그 변환을 해주고, 나중에 모델이 값을 예측한 후에 다시 np.expm1()을 활용해서 되돌려준다. np.exp1m()np.log1p()과는 반대로 각 원소 x마다 exp(x)-1의 값을 반환해준다.

numpy.log1p

numpy.expm1

y = np.log1p(y)
y
# 실행결과
0        12.309987
1        12.100718
2        13.142168
3        12.458779
4        12.583999
           ...    
15030    13.322338
15031    13.822984
15032    12.793862
15033    12.899222
15034    12.691584
Name: price, Length: 15035, dtype: float64
sns.kdeplot(y)
plt.show()


비교적 완만한 정규분포의 형태로 잘 변환되었다.

info() 함수로 전체 데이터의 자료형을 한눈에 확인해본다.

train.info()
# 실행결과
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 15035 entries, 0 to 15034
Data columns (total 19 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   date           15035 non-null  int64  
 1   bedrooms       15035 non-null  int64  
 2   bathrooms      15035 non-null  float64
 3   sqft_living    15035 non-null  int64  
 4   sqft_lot       15035 non-null  int64  
 5   floors         15035 non-null  float64
 6   waterfront     15035 non-null  int64  
 7   view           15035 non-null  int64  
 8   condition      15035 non-null  int64  
 9   grade          15035 non-null  int64  
 10  sqft_above     15035 non-null  int64  
 11  sqft_basement  15035 non-null  int64  
 12  yr_built       15035 non-null  int64  
 13  yr_renovated   15035 non-null  int64  
 14  zipcode        15035 non-null  int64  
 15  lat            15035 non-null  float64
 16  long           15035 non-null  float64
 17  sqft_living15  15035 non-null  int64  
 18  sqft_lot15     15035 non-null  int64  
dtypes: float64(4), int64(15)
memory usage: 2.2 MB

모두 실수 또는 정수 자료형으로, 문제 없이 모델 학습에 활용할 수 있을 것 같다.

📖 랭킹을 올리고 싶다면?_(2) 다양한 실험을 위해 함수로 만들어 쓰자

본격적으로 모델 튜닝을 해본다.

머신러닝 모델을 학습시키고 튜닝을 하다 보면 몇 시간이 훌쩍 지났는지 모를 만큼 실험해볼 것들이 많다.
보다 다양하고 많은 실험을 하기 위해서는, 그만큼 실험을 위한 도구들이 잘 준비되어 있는 것이 유리하다.

따라서 여러 가지 반복되는 작업들은 함수로 먼저 만들어 놓고 많은 실험을 하는 것이 좋다.

RMSE 계산


먼저 필요한 라이브러리를 가져온다.
데이터셋을 훈련 데이터셋과 검증 데이터셋으로 나누기 위한 train_test_split 함수와, RMSE 점수를 계산하기 위한 mean_squared_error를 가져온다.

from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

평가 척도인 RMSE를 계산하기 위해 다음과 같은 함수를 만들어놓는다.
한 가지 주의해야 할 것은, y_testy_pred는 위에서 np.log1p()로 변환이 된 값이기 때문에 원래 데이터의 단위에 맞게 되돌리기 위해 np.expm1()을 추가해야 한다.
exp로 다시 변환해서 mean_squared_error를 계산한 값에 np.sqrt를 취하면 RMSE 값을 얻을 수 있다.

def rmse(y_test, y_pred):
    return np.sqrt(mean_squared_error(np.expm1(y_test), np.expm1(y_pred)))

다음으로 XGBRegressor, LGBMRegressor, GradientBoostingRegressor, RandomForestRegressor 네 가지 모델을 가져온다.

from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor

아래와 같이 모델 인스턴스를 생성한 후 models라는 리스트에 넣어준다.

# random_state는 모델초기화나 데이터셋 구성에 사용되는 랜덤 시드값 
#random_state=None    # 이게 초기값 아무것도 지정하지 않고 None을 넘겨주면 모델 내부에서 임의로 선택  
random_state=2020        # 하지만 지금은 이렇게 고정값 세팅

gboost = GradientBoostingRegressor(random_state=random_state)
xgboost = XGBRegressor(random_state=random_state)
lightgbm = LGBMRegressor(random_state=random_state)
rdforest = RandomForestRegressor(random_state=random_state)

models = [gboost, xgboost, lightgbm, rdforest]

각 모델의 이름은 다음과 같이 클래스의 __name__ 속성에 접근해서 얻을 수 있다.

gboost.__class__.__name__
# 실행결과
'GradientBoostingRegressor'

이렇게 이름을 접근할 수 있다면 다음과 같이 for문 안에서 각 모델 별로 학습 및 예측을 해볼 수 있다.

df = {}

for model in models:
    # 모델 이름 획득
    model_name = model.__class__.__name__

    # train, test 데이터셋 분리 - 여기에도 random_state를 고정합니다. 
    X_train, X_test, y_train, y_test = train_test_split(train, y, random_state=random_state, test_size=0.2)

    # 모델 학습
    model.fit(X_train, y_train)
    
    # 예측
    y_pred = model.predict(X_test)

    # 예측 결과의 rmse값 저장
    df[model_name] = rmse(y_test, y_pred)
    
    # data frame에 저장
    score_df = pd.DataFrame(df, index=['RMSE']).T.sort_values('RMSE', ascending=False)
    
df
# 실행결과
{'GradientBoostingRegressor': 128360.19649691365,
 'XGBRegressor': 110318.66956616656,
 'LGBMRegressor': 111920.36735892233,
 'RandomForestRegressor': 125487.07102453562}

위의 과정을 get_scores(models, train, y) 함수로 만들어 본다.

def get_scores(models, train, y):
    df = {}
    
    for model in models:
        model_name = model.__class__.__name__
        
        X_train, X_test, y_train, y_test = train_test_split(train, y, random_state=random_state, test_size=0.2)
        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        
        df[model_name] = rmse(y_test, y_pred)
        score_df = pd.DataFrame(df, index=['RMSE']).T.sort_values('RMSE', ascending=False)
            
    return score_df

get_scores(models, train, y)

📖 랭킹을 올리고 싶다면?_(3) 하이퍼 파라미터 튜닝의 최강자, 그리드 탐색

이제 모델과 데이터셋이 있다면 RMSE 결괏값을 나타내주는 함수가 준비되었으니, 다양한 하이퍼 파라미터로 실험하는 일만 남았다.

실험은 sklearn.model_selection 라이브러리 안에 있는 GridSearchCV 클래스를 활용한다. 다음 함수를 import한다.

from sklearn.model_selection import GridSearchCV

GridSearchCV란 무엇일까?

우선 그리드 탐색과 랜덤 탐색을 알아볼 필요가 있다. 두 가지 모두 하이퍼 파라미터를 조합해 보는 방법이다.

그리드 탐색은 사람이 먼저 탐색할 하이퍼 파라미터의 값들을 정해두고, 그 값들로 만들어질 수 있는 모든 조합을 탐색한다. 특정 값에 대한 하이퍼 파라미터 조합을 모두 탐색하고자 할 때 유리하다.

랜덤 탐색은 사람이 탐색할 하이퍼 파라미터의 공간만 정해두고, 그 안에서 랜덤으로 조합을 선택해서 탐색하는 방법이다.

그리드 탐색에서는 사람이 정해둔 값들로 이루어지는 조합만 탐색하기 때문에 최적의 조합을 놓칠 수 있는 반면, 랜덤 탐색에서는 말 그대로 랜덤으로 탐색하기 때문에 최적의 조합을 찾을 수 있는 가능성이 언제나 열려 있다. 하지만 그 가능성 또한 랜덤성에 의존하기 때문에 언제나 최적을 찾는다는 보장은 없다.

다음 이미지가 그리드 탐색(grid search)과 랜덤 탐색(random search) 두 가지를 나타내고 있다. 그리드 탐색은 정해진 하이퍼 파라미터의 조합을 격자와 같이 탐색하는 반면, 랜덤 탐색은 랜덤으로 점을 찍어서 탐색한다.

확실히 그리드 탐색의 탐색 공간은 매우 제한적인 반면 랜덤 탐색은 탐색하는 공간이 훨씬 넓다.

GridSearchCV에 입력되는 인자들은 다음과 같다.

  • param_grid : 탐색할 파라미터의 종류 (딕셔너리로 입력)
  • scoring : 모델의 성능을 평가할 지표
  • cv : cross validation을 수행하기 위해 train 데이터셋을 나누는 조각의 개수
  • verbose : 그리드 탐색을 진행하면서 진행 과정을 출력해서 보여줄 메세지의 양 (숫자가 클수록 더 많은 메세지를 출력합니다.)
  • n_jobs : 그리드 탐색을 진행하면서 사용할 CPU의 개수

param_grid에 탐색할 xgboost 관련 하이퍼 파라미터를 넣어서 준비한다.

param_grid = {
    'n_estimators': [50, 100],
    'max_depth': [1, 10],
}

그다음으로 모델을 준비한다. 모델은 LightGBM(lgbm)를 사용해본다.

model = LGBMRegressor(random_state=random_state)

그러면 model, param_grid와 함께 다른 여러 가지 인자를 넣어서 GridSearchCV를 수행할 수 있다.

grid_model = GridSearchCV(model, param_grid=param_grid, \
                        scoring='neg_mean_squared_error', \
                        cv=5, verbose=1, n_jobs=5)

grid_model.fit(train, y)
# 실행결과
Fitting 5 folds for each of 4 candidates, totalling 20 fits
GridSearchCV(cv=5, estimator=LGBMRegressor(random_state=2020), n_jobs=5,
             param_grid={'max_depth': [1, 10], 'n_estimators': [50, 100]},
             scoring='neg_mean_squared_error', verbose=1)

Q. 위에 출력된 메세지에 나타난 totalling 20 fits에서 20은 어떻게 계산된 값일까?
A. 위에서 입력한 param_grid에 n_estimators 두 가지, max_depth 두 가지를 넣었으므로 가능한 조합은 총 2X2=4가지이다.
또한, cross validation은 각 경우마다 5번을 진행하니 총 20 fits를 진행하게 된다.
여기에서 cross validation을 5번 진행하는 이유는, 각 조합에 대해 단 한 번만 실험을 하는 것보다 5번을 진행해서 평균을 취하는 것이 일반화 오차를 추정하는 데에 더 신뢰도가 높기 때문이다.

실험에 대한 결과는 다음과 같이 gridmodel.cv_results 안에 저장된다.

grid_model.cv_results_
# 실행결과
{'mean_fit_time': array([0.14933739, 0.17484746, 0.30724301, 0.59494796]),
 'std_fit_time': array([0.0466377 , 0.01159918, 0.00312133, 0.09078761]),
 'mean_score_time': array([0.02516804, 0.01520286, 0.02418046, 0.03481836]),
 'std_score_time': array([0.0159746 , 0.00944715, 0.00964454, 0.0109334 ]),
 'param_max_depth': masked_array(data=[1, 1, 10, 10],
              mask=[False, False, False, False],
        fill_value='?',
             dtype=object),
 'param_n_estimators': masked_array(data=[50, 100, 50, 100],
              mask=[False, False, False, False],
        fill_value='?',
             dtype=object),
 'params': [{'max_depth': 1, 'n_estimators': 50},
  {'max_depth': 1, 'n_estimators': 100},
  {'max_depth': 10, 'n_estimators': 50},
  {'max_depth': 10, 'n_estimators': 100}],
 'split0_test_score': array([-0.0756974 , -0.05555652, -0.02885847, -0.02665428]),
 'split1_test_score': array([-0.07666447, -0.057876  , -0.03041465, -0.02795896]),
 'split2_test_score': array([-0.07354904, -0.05546079, -0.03068533, -0.02834112]),
 'split3_test_score': array([-0.07510863, -0.05582109, -0.02987609, -0.02774809]),
 'split4_test_score': array([-0.06595281, -0.05038773, -0.02605217, -0.02443328]),
 'mean_test_score': array([-0.07339447, -0.05502043, -0.02917734, -0.02702714]),
 'std_test_score': array([0.00385583, 0.00247946, 0.00168295, 0.00141292]),
 'rank_test_score': array([4, 3, 2, 1], dtype=int32)}

정보가 너무 많아서 눈에 잘 들어오지 않는다. 원하는 값만 정제해서 확인하도록 한다.
관심 있는 정보는 어떤 파라미터 조합일 때 점수가 어떻게 나오게 되는지에 관한 것이다. 파라미터 조합은 위 딕셔너리 중 params에, 각각에 대한 테스트 점수는 mean_test_score에 저장되어 있다. 이 두 정보만 빼내본다.

params = grid_model.cv_results_['params']
params
# 실행결과
[{'max_depth': 1, 'n_estimators': 50},
{'max_depth': 1, 'n_estimators': 100},
{'max_depth': 10, 'n_estimators': 50},
{'max_depth': 10, 'n_estimators': 100}]
score = grid_model.cv_results_['mean_test_score']
score
# 실행결과
array([-0.07339447, -0.05502043, -0.02917734, -0.02702714])

params에는 각 파라미터의 조합이, score에는 각 조합에 대한 점수가 들어가 있다.
이제 이 둘만 가지고 데이터 프레임을 만들고 최적의 성능을 내는 하이퍼 파라미터의 조합을 찾아본다.

results = pd.DataFrame(params)
results['score'] = score

results


왜 점수가 음수일까? 그 이유는 바로 GridSearchCV을 초기화 한 코드에 힌트가 있다. GridSearchCV에서 모델을 초기화할 때 scoring 인자에 MSE에 음수를 취한 값인 neg_mean_squared_error를 입력했다.

GridSearchCV를 사용할 때에는 이 외에도 다양한 점수 체계(scoring)를 사용할 수 있다.
아래 페이지를 살펴보면 점수 체계(scoring)를 각각 Classification, Clustering, Regression. 세 가지로 분류해놓고 있다.
사이킷런 - The scoring parameter: defining model evaluation rules

여기서는 Regression 문제를 풀고 있기 때문에 그에 알맞은 성능 평가 지표를 사용했다. neg_mean_squared_error를 사용했기 때문에 점수가 음수로 표현된 것이다.

아래와 같은 간단한 변환 함수로 RMSE 점수를 볼 수 있도록 만든다. 음수로 된 MSE였으니, -1을 곱해주고 np.sqrt로 루트 연산을 해주면 된다.

results['RMSE'] = np.sqrt(-1 * results['score'])
results


하지만 아직도 위에서 보았던 10만 단위의 RMSE와는 값의 크기가 아주 다르다.

그 이유는 price에 있다. 위에서는 price의 분포가 한쪽으로 치우쳐져 있는 것을 보고 log 변환을 했었다.
그 후 RMSE 값을 계산하기 위한 함수에서는 np.expm1 함수를 활용해 다시 원래대로 복원한 후 RMSE 값을 계산했다.

하지만 그리드 탐색을 하면서는 np.expm1()으로 변환하는 과정이 없었기 때문에 log 변환되어 있는 price 데이터에서 손실함수값을 계산한 것이다.
따라서 사실, 위의 데이터 프레임에 나타난 값은 정확히 말하면 RMSE가 아니라 RMSLE, 즉 Root Mean Squared Log Error이다. log를 취한 값에서 RMSE를 구했다는 뜻이다.

이에 맞게 컬럼의 이름을 RMSLE로 변환해준다. 판다스에서 컬럼의 이름 변환은 rename으로 할 수 있다.

results = results.rename(columns={'RMSE': 'RMSLE'})
results


이제 마지막 할 일은 RMSLE가 낮은 순서대로 정렬하는 것이다. sort_values로 간단히 할 수 있다.

pandas.DataFrame.sort_values

result = results.sort_values('RMSLE')
result

지금까지의 과정을 하나의 함수로 만들어서 앞으로는 간결한 코드로 진행한다.

def my_GridSearch(model, train, y, param_grid, verbose=2, n_jobs=5):
    # GridSearchCV 모델로 초기화
    grid_model = GridSearchCV(model, param_grid=param_grid, scoring='neg_mean_squared_error', \
                              cv=5, verbose=verbose, n_jobs=n_jobs)
    
    # 모델 fitting
    grid_model.fit(train, y)

    # 결과값 저장
    params = grid_model.cv_results_['params']
    score = grid_model.cv_results_['mean_test_score']
    
    # 데이터 프레임 생성
    results = pd.DataFrame(params)
    results['score'] = score
    
    # RMSLE 값 계산 후 정렬
    results['RMSLE'] = np.sqrt(-1 * results['score'])
    results = results.sort_values('RMSLE')

    return results

📖 랭킹을 올리고 싶다면?_(4) 제출하는 것도, 빠르고 깔끔하게!

제출 과정 또한 하나의 함수로 깔끔하게 진행한다.
먼저 위에서 만들어놓은 my_GridSearch() 함수로 간단한 그리드 탐색을 해본다.

param_grid = {
    'n_estimators': [50, 100],
    'max_depth': [1, 10],
}

model = LGBMRegressor(random_state=random_state)
my_GridSearch(model, train, y, param_grid, verbose=2, n_jobs=5)
# 실행결과
Fitting 5 folds for each of 4 candidates, totalling 20 fits


가장 좋은 조합은 max_depth=10, n_estimators=100이다.
해당 모델로 학습을 해서 예측값인 submission.csv 파일을 만들어서 제출해본다.

먼저 해당 파라미터로 구성된 모델을 준비하고, 학습 후 예측 결과를 생성한다.

model = LGBMRegressor(max_depth=10, n_estimators=100, random_state=random_state)
model.fit(train, y)
prediction = model.predict(test)
prediction
# 실행결과
array([13.13580793, 13.08051399, 14.11202067, ..., 13.01592878,
       12.69894979, 12.96297768])

예측 결과에 np.expm1()을 씌워서 다시 원래 스케일로 되돌리는 것도 잊지 말아야 한다.

prediction = np.expm1(prediction)
prediction
# 실행결과
array([ 506766.66784595,  479506.10405112, 1345155.15609376, ...,
        449515.92243642,  327402.87855805,  426332.71354302])

이제 위에서 했던 대로 sample_submission.csv 파일을 가져와본다.

data_dir = os.getenv('HOME')+'/aiffel/kaggle_kakr_housing/data'

submission_path = join(data_dir, 'sample_submission.csv')
submission = pd.read_csv(submission_path)
submission.head()


위의 데이터프레임에 우리의 모델이 예측한 값을 덮어씌우면 제출할 데이터가 완성된다.

submission['price'] = prediction
submission.head()


위의 데이터를 csv 파일로 저장한다.

단, 앞으로는 많은 실험이 있을 예정이니 파일 이름에 모델의 종류와 위에서 확인했던 RMSLE 값을 넣어주면 제출 파일들이 깔끔하게 관리될 것이다.

submission_csv_path = '{}/submission_{}_RMSLE_{}.csv'.format(data_dir, 'lgbm', '0.164399')
submission.to_csv(submission_csv_path, index=False)
print(submission_csv_path)
# 실행결과
/aiffel/aiffel/kaggle_kakr_housing/data/submission_lgbm_RMSLE_0.164399.csv

위의 과정들도 하나의 함수로 정리해두면 사용하기 편리할 것이다.

def save_submission(model, train, y, test, model_name, rmsle=None):
    model.fit(train, y)
    prediction = model.predict(test)
    prediction = np.expm1(prediction)
    data_dir = os.getenv('HOME')+'/aiffel/kaggle_kakr_housing/data'
    submission_path = join(data_dir, 'sample_submission.csv')
    submission = pd.read_csv(submission_path)
    submission['price'] = prediction
    submission_csv_path = '{}/submission_{}_RMSLE_{}.csv'.format(data_dir, model_name, rmsle)
    submission.to_csv(submission_csv_path, index=False)
    print('{} saved!'.format(submission_csv_path))

이 함수를 사용한다면 다음 한 줄로 모델을 학습시킨 후 예측 결과를 저장할 수 있다.

save_submission(model, train, y, test, 'lgbm', rmsle='0.0168')
# 실행결과
/aiffel/aiffel/kaggle_kakr_housing/data/submission_lgbm_RMSLE_0.0168.csv saved!

좋은 웹페이지 즐겨찾기