AIFFEL(220118)_나의 첫 번째 캐글 경진대회, 무작정 따라해보기
케글코리아(Kaggle Korea)가 2019년에 주최했던 캐글 경진대회인 2019 2nd ML month with KaKR를 처음부터 끝까지 경험해보자.
학습목표
- 데이터 사이언스 관련 최대 커뮤니티인 캐글의 경진대회에 직접 참여해서 문제를 해결해본다.
- 캐글에서 데이터를 내려받는 것으로부터 시작해서, 로컬 서버에서 자유롭게 다루어보며 문제 해결을 위한 고민을 해본다.
- 앙상블 기법의 개념과 강점을 이해하고, 여러 모델의 예측 결과를 Averaging 한 최종 결과로 캐글에 제출해본다.
- 하이퍼 파라미터 튜닝의 필요성과 의미를 이해하고, Grid Search, Random Search 등의 기법을 알아본다.
- Grid Search 기법을 활용해서 직접 하이퍼 파라미터 튜닝 실험을 해보고, 모델의 성능을 최대한 끌어올려본다.
📖 대회의 시작_(1) 참가 규칙, 평가 기준 살펴보기
Description, 대회 소개
캐글에는 아주 다양한 경진대회들이 있고, 각 경진대회들은 모두 대회 소개, 데이터셋 소개, 규칙 설명 등 대회에 참가하는 사람들을 위한 세부 내용들로 구성되어 있다.
대회별로 평가 방법과 규칙이 상이하니, 대회를 본격적으로 시작하기 전에 소개와 세부 내용을 잘 읽고 시작하는 것이 좋다.
Evaluation, 점수 평가 기준
대회에서 각 참여자들의 점수를 어떤 기준으로 평가하는지도 매우 중요한 부분이다.
왼쪽 탭에서 Evaluation
을 누르면 평가 방식은 *RMSE라고 한다.
* RMSE(Root Mean Squared Error) : 실제정답과 예측한 값의 차이의 제곱을 평균한 값의 제곱근
이번 대회에서 다루는 문제는 "집값"을 예측하는 문제이기 때문에 예측해야 하는 값과 실제 정답값이 모두 실숫값이다.
그 두 가지 값의 차이를 사용해 얼마나 떨어져 있는지 계산 할 목적으로 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은 제공이 되는 경우도, 아닌 경우도 있는데 이번 대회는 특히 교육적인 성격도 있어서 제공이 되었던 것 같다.
다른 사람의 커널을 ipynb 파일로 다운받아 사용하기
캐글의 커널(Kernel) 은 우리가 쓰는 주피터 노트북 형태의 파일이 캐글 서버에서 실행될 때 그 프로그램을 일컫는 개념이다.
캐글 자체의 서버에서 baseline 노트북 파일을 돌리고 모델 학습을 시킬 수도 있는데, 아래와 같이 보이는 화면에서 Copy and Edit
버튼을 클릭해본다.
Copy and Edit
버튼을 클릭했다면 다음과 같은 웹상에서 코드를 돌려볼 수 있는 커널 창이 뜬다.
이렇게 커널 창 위에서 그대로 진행해도 되지만, 노트북 파일을 다운로드하여 사용해본다. File > Download
를 통해 커널을 ipynb 파일로 다운받아준다.
Baseline 커널 파일 실행 준비
Baseline 노트북 파일을 다운받았으니 직접 돌려보고 점수까지 내본다. 다만 Baseline의 모든 코드를 에러없이 잘 돌리기 위해서는 몇 가지 준비가 필요하다.
✓ 데이터 파일을 현재 디렉토리로 옮기기
먼저, 당연히 데이터를 노트북 파일과 같은 폴더 내에 두어야한다. 혹시 다른 위치에 압축을 해제했다면, 모델 학습 및 예측에 필요한 파일이 들어있는 데이터 폴더를 아래와 같은 형태로 노트북과 같은 위치로 옮겨야한다.
✓ 필요한 라이브러리 설치하기
다음은 Baseline이 사용하는 몇 가지 라이브러리를 설치하는 법을 알아본다. 이번 대회에서는 회귀 모델을 구현하는 데에 사용하는 xgboost
와 lightgbm
라이브러리와, 결측 데이터를 확인하는 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 커널과 다르다는 점이다.
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라는 변수에 저장한 후 해당 컬럼은 지워준다.
- w3schools - python del keyword
- 참고로 데이터 분석 과정에서 칼럼을 없애고 싶다면 pandas.DataFrame.drop도 사용할 수 있다.
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_len
에 training 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) 데이터의 경우에도 부드러운 곡선으로 전체 분포를 확인할 수 있도록 하는 시각화 함수이다.
아래 코드로 전체 그래프를 그려본다.
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에 가깝게 모여있는 값들이 로 입력되면, 그 함수값인 값들은 매우 큰 범위로 벌어지게 된다. 즉, 로그 함수는 0에 가까운 값들이 조밀하게 모여있는 입력값을, 넓은 범위로 펼칠 수 있는 특징을 가진다.
- 반면, 값이 점점 커짐에 따라 로그 함수의 기울기는 급격히 작아진다. 이는 곧 큰 값들에 대해서는 값이 크게 차이나지 않게 된다는 뜻이고, 따라서 넓은 범위를 가지는 를 비교적 작은 값의 구간 내에 모이게 하는 특징을 가진다.
위와 같은 특성 때문에 한 쪽으로 몰려있는 분포에 로그 변환을 취하게 되면 넓게 퍼질 수 있는 것이다.
그렇다면 맞추어야 할 타겟인 집의 가격, 즉 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은 각 모델이 계산해 낸 실수값들을 평균 혹은 가중평균하여 사용하는 방법으로, 회귀 문제에서 사용된다.
앙상블은 강력한 개별 모델 하나보다 약한 여러 개의 모델의 결과를 합치는 것이 낫다는 기본 전제로부터 시작되는데, 그 전제가 실제로 성립한다면, 그 이유는 무엇일까?
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()
함수는 회귀모델을 전달할 경우 점수를 반환한다. 값은 1에 가까울수록 모델이 잘 학습되었다는 것을 나타낸다. 결정계수 값에 대한 간단한 설명은 아래 링크의 글을 참고한다.
위의 결과를 보니 3개 트리 모델이 모두 훈련 데이터에 대해 괜찮은 성능을 보여주고 있다.
Baseline 모델에서는 다음과 같이 여러 모델을 입력하면 각 모델에 대한 예측 결과를 평균 내어 주는 AveragingBlending()
함수를 만들어 사용한다. AveragingBlending()
함수는 models
딕셔너리 안에 있는 모델을 모두 x
와 y
로 학습시킨 뒤 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()
id
와 price
의 두 가지 열로 구성되어 있다. 이에 맞게 id
와 price
로 구성된 데이터 프레임을 만들어 준다.
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
는 왼쪽으로 크게 치우쳐 있는 형태를 보인다.
따라서 y
는 np.log1p()
함수를 통해 로그 변환을 해주고, 나중에 모델이 값을 예측한 후에 다시 np.expm1()
을 활용해서 되돌려준다. np.exp1m()
은 np.log1p()
과는 반대로 각 원소 x
마다 exp(x)-1
의 값을 반환해준다.
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_test
나y_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로 간단히 할 수 있다.
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!
Author And Source
이 문제에 관하여(AIFFEL(220118)_나의 첫 번째 캐글 경진대회, 무작정 따라해보기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@mjk3136/AIFFEL210118나의-첫-번째-캐글-경진대회-무작정-따라해보기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)