본문 바로가기
AI SCHOOL/TIL

[DAY 59] 메르세데스 벤츠 테스팅 시간 예측 - XGBoost, LightGBM

2023. 3. 21.

Kaggle Competition : Mercedes-Benz Greener Manufacturing을 통해 부스팅 모델을 사용하여 회귀 예측을 했다.

Tree 계열 알고리즘 중 사이킷런이 아닌 라이브러리의 알고리즘을 사용했으며 수 백개 이상의 변수, 익명화된 데이터셋을 경험했다.

데이터 로드

colab 환경에서 진행했는데, competition page에서 다운로드한 zip파일을 그대로 올리고 unzip 명령어를 통해 압축을 풀어 사용했다.

!unzip mercedes-benz-greener-manufacturing.zip

!unzip 파일경로 명령어를 실행해서 압축을 해제했다.

압축 해제 결과인 총 3개의 파일 train.csv.zip, test.csv.zip, sample_submission.csv.zip을 pandas의 read_csv를 사용하여 로드했다.

train = pd.read_csv('train.csv.zip', index_col="ID")
test = pd.read_csv('test.csv.zip', index_col="ID")
submission = pd.read_csv('sample_submission.csv.zip', index_col="ID")

train.shape, test.shape, submission.shape

# 실행 결과
((4209, 377), (4209, 376), (4209, 1))

zip 파일을 그대로 지정해도 데이터셋이 잘 로드되는 것을 확인할 수 있었다. 특이 사항으로는 train과 test 데이터의 행 수가 같다.

train.head()

train 데이터의 일부를 확인하여 형태를 보면

train

컬럼명이 직관적이지 않은, 익명화된 데이터셋임을 알 수 있다. 각 변수는 메르세데스 차량의 커스텀 기능을 나타낸다고 한다. 예를 들어 4WD, 에어 서스펜션 추가, 헤드업 디스플레이 등을 의미한다. 실제 정보를 그대로 노출시키지 않기 위해 이렇게 익명화를 한 것이다.
또한 각 수치형 변수들은 0과 1의 바이너리 값을 가진다.

test.head()

test 데이터 또한 확인해 보면

test

label인 y만 제외하고 train 데이터와 같은 변수를 가지는 것이 확인된다.

EDA

데이터프레임의 info()와 nunique()를 통해 정보를 확인

toomany

컬럼 수가 300개가 넘다 보니 info와 nunique 값을 직관적으로 확인할 수 없었다.

train.isna().sum().sum()  # 0
test.isna().sum().sum()  # 0

train과 test의 결측치가 없음을 확인했고

len(set(train.index) & set(test.index))  # 0
len(set(train.index) | set(test.index))  # 8418

train과 test의 인덱스가 서로 겹치지 않는 것을 확인했다.

train.describe(include='object')

object 데이터 중에서 binary 형태가 있는지 확인

object

binary 형태는 없었다. 하지만 X4 컬럼의 경우 대부분의 값이 d로 되어 있는 것을 확인했다.
이런 경우 해당 변수를 사용할 것인지 여부는 도메인 지식을 바탕으로 결정해야 한다.
그러나 모두 익명화되어 있어 어떤 변수인지 모르기 때문에 제거하지 않기로 결정하였다.

# y 컬럼을 제외한 데이터프레임의 기술통계값의 기술통계값
train.drop(columns='y').describe().T.describe()

train에서 target인 y를 제외한 데이터프레임의 기술통계값을 구하고 다시 기술통계값을 확인해 보면

disdiscribe

다음의 해석을 할 수 있다.
1. 최솟값은 0, 최댓값은 1이다.
2. 모두 0으로 이루어진 컬럼이 있다.
3. 모두 1로 이루어진 컬럼은 없다.
4. 한쪽으로 치우친 데이터를 가진 컬럼이 존재한다

전체 368개 수치 변수에 대한 왜도와 첨도를 확인해 보자.

train.drop(columns="y").select_dtypes(exclude="O").agg(["skew", "kurt"]).T.describe()

select_dtypes(exclude='O')를 통해 수치형 변수만 선택하여 왜도와 첨도에 대한 기술통계를 출력했다.

skew

극단적으로 치우친 값이 있는 것을 알 수 있다.

train_skew = abs(train.drop(columns="y").select_dtypes(exclude="O").skew()) 
train[train_skew[train_skew > 50].index].hist();

히스토그램을 그려 시각적으로 확인해 보면

histogram


총 18개 컬럼이 극단적으로 편향된 데이터를 가짐을 볼 수 있다.
해당 컬럼들을 skewed_col 변수에 저장하였다.

이번엔 모든 값이 같은 값을 가지는 컬럼에 대해 살펴보자.

train.loc[:, train.nunique()==1]

모든 값이 같은 값을 가진다는 것은 유일값의 개수가 1개라는 것이다.

nunique1

총 12개의 컬럼이 모든 값이 0으로 채워진 것을 확인할 수 있다.
해당 컬럼들을 unique_one_col 변수에 저장하였다.

극단적으로 편향된 컬럼과 모든 값이 같은 컬럼을 합쳐 remove_col 변수에 저장했으며, 해당 컬럼들은 머신러닝의 성능에 오히려 안 좋은 영향을 줄 수 있으므로 제거한다.

# 총 30개 컬럼 제거
train_remove_col = train.drop(columns=remove_col).copy()
test_remove_col = test.drop(columns=remove_col).copy()

train_remove_col.shape, test_remove_col.shape

# 실행 결과
((4209, 347), (4209, 346))

30개 컬럼이 제거되었다.

Feature Engineering

범주형 변수를 category 타입으로 변경

# 범주형 변수 찾기
cat_col = train_remove_col.select_dtypes(include='object').columns.to_list()
# category 타입으로 변환
train_remove_col[cat_col] = train_remove_col[cat_col].astype('category')
test_remove_col[cat_col] = test_remove_col[cat_col].astype('category')

object형 변수를 astype을 통해 category형으로 변환해 주었다.

One Hot Encoding

from sklearn.preprocessing import OneHotEncoder

sklearn의 함수를 사용하여 원핫인코딩한다.

# handle_unknown='ignore' : train에 없는 test 값이 들어오면 무시한다는 의미
# 미래에 어떤 데이터가 들어올지 모르기 때문
ohe = OneHotEncoder(handle_unknown='ignore')
train_ohe = ohe.fit_transform(train_remove_col[cat_col])
test_ohe = ohe.transform(test_remove_col[cat_col])

train_ohe.shape, test_ohe.shape

# 실행 결과
((4209, 195), (4209, 195))

원핫인코딩을 할 때 test 데이터에 대해서는 fit을 하지 않는다. Data Leakage를 방지하기 위해서다.
category형 8개의 변수에 대해 원핫인코딩 결과 195개 변수가 되었다.

df_train_ohe = pd.DataFrame(train_ohe.toarray(), columns=ohe.get_feature_names_out())
df_train_ohe.index = train.index

df_test_ohe = pd.DataFrame(test_ohe.toarray(), columns=ohe.get_feature_names_out())
df_test_ohe.index = test.index

train, test의 카테고리형 변수에 대한 원핫인코딩 결과를 판다스 데이터프레임으로 만들어 주었다. 원핫인코딩이 필요 없는 데이터와 합쳐줄 것이다.

df_train_num = train_remove_col.drop(columns=cat_col).drop(columns='y')
df_train_num.index = train.index

df_test_num = test_remove_col.drop(columns=cat_col)
df_test_num.index = test.index

원핫인코딩이 필요 없는(원래 수치형) train, test 데이터 프레임

# 인코딩한 train 데이터
df_train_enc = pd.concat([df_train_num, df_train_ohe], axis=1)
print(df_train_enc.shape)
df_train_enc

concat 후 인코딩이 완료된 train 데이터의 shape, 형태 확인

encodedtrain

train 인코딩 결과 : 4209 rows * 533 cols

# 인코딩한 test 데이터
df_test_enc = pd.concat([df_test_num, df_test_ohe], axis=1)
print(df_test_enc.shape)
df_test_enc

concat 후 인코딩이 완료된 test 데이터의 shape, 형태 확인

encodedtest

test 인코딩 결과 : 4209 rows * 533 cols

인코딩까지 끝났다. 학습과 검증에 사용할 X, y를 준비한다.

# X, y 준비
X = df_train_enc.copy()
y = train['y'].copy()
X_test = df_test_enc.copy()

 

학습, 검증 데이터셋으로 분할

from sklearn.model_selection import train_test_split

train_test_split 함수를 활용할 것이다.

X_train, X_valid, y_train, y_valid = train_test_split(X, y, 
                                                      test_size=0.1, 
                                                      random_state=42)
X_train.shape, X_valid.shape, y_train.shape, y_valid.shape

# 실행 결과
((3788, 533), (421, 533), (3788,), (421,))

Hold-out validation을 위해 train 90%, valid 10%로 분할했다.

모델 사용 학습, 예측

scikit-learn이 아닌 부스팅 알고리즘을 사용한다.
XGBoost
모델
- GBT(Gradient Boosting Tree)에서 병렬 학습을 지원하여 학습 속도 개선
- 기본 GBT에 비해 더 효율적이고 다양한 종류의 데이터에 대응 가능, 이식성 높음

xgboost1
디시전트리 기반 부스팅 모델

 

boost2

XGBoost 이미지 출처

import xgboost as xgb

model_xgb = xgb.XGBRegressor(random_state=42)
model_xgb.fit(X_train, y_train)
y_valid_pred = model_xgb.predict(X_valid)

XGBRegressor 모델을 생성하고 학습한 후, X_valid에 대한 예측값을 얻었다.

from sklearn.metrics import r2_score
r2_score(y_valid, y_valid_pred)

# 실행 결과
0.5448494457039215

검증 결과 결정계수 약 0.545가 나왔다.

해당 모델을 사용하여 X_test의 예측값을 얻어 kaggle에 제출한 결과 Private Score 0.49468을 받았다.


LightGBM 모델
- XGBoost에 비해 비슷한 성능이지만 학습 시간 단축
- XGBoost에 비해 시간과 메모리 절약 효과

lightgbm
다른 부스팅 알고리즘과 LightGBM의 동작 방식

LightGBM 이미지 출처
=> 일반적인 대부분의 부스팅 알고리즘과 다르게 LightGBM은 리프 중심 트리 분할(leaf wise) 방식을 사용하여 균형을 맞추지 않아 학습 시간이 단축되고, 리프 노드를 지속적 분할하여 트리가 깊어지고 예측 오류를 최소화한다.

import lightgbm

model_lgbm = lightgbm.LGBMRegressor(boosting_type='goss',
                                    n_estimators=500,
                                    max_depth=3,
                                    learning_rate=0.01, n_jobs=-1,
                                    random_state=42)
model_lgbm.fit(X_train, y_train)

LGBMRegressor 모델에 다양한 하이퍼파라미터를 지정하여 성능 향상을 도모했다.

score_lgbm = model_lgbm.score(X_valid, y_valid)
score_lgbm

# 실행 결과
0.6289292649067887

모델의 score를 확인한 결과 약 0.629를 얻었다.

해당 모델을 사용하여 X_test의 예측값을 얻고 kaggle에 제출한 결과 Private Score 0.54494를 받았다.


scikit-learn 라이브러리에서 제공하는 모델이 아닌 부스팅 모델 XGBoost, LightGBM을 사용하여 회귀 예측 결과를 얻고 캐글에 제출하여 점수를 확인해 봤다.
데이터 전처리 과정과 하이퍼파라미터 튜닝에 더 많은 시간을 투자한다면 분명 더 좋은 성능을 내는 결과를 확인할 수 있을 것 같다.

반응형

댓글