캐글의 안전 운전자 예측 경진대회 'Porto Seguro's Safe Driver Prediction' compeition에 참가해 다양한 모델링 기법을 연습해보았다.
Porto Seguro라는 브라질의 보험사에서 제공한 고객 데이터를 활용해 운전자가 보험을 청구할 확률을 예측하는 대회이다. 데이터에 결측값이 많기에 이를 잘 해결하는 것이 핵심이다. 타깃값은 0과 1로 구분되는데, 0이면 보험금을 청구하지 않는다는 것, 1이면 보험금을 청구한다는 것이다. 타깃값이 2개이므로 이진분류 문제에 속한다.
데이터를 불러와 training data와 test data를 확인해본다.
train.info()로 training data를 자세히 살펴본다.
feature명에서 데이터 타입을 알 수 있는데, bin이면 이진 feature, cat이면 명목 feature임을 알 수 있다.
또한 non-null로 결측값이 없다고 표기되는데, 이는 결측값 자리에 -1로 값이 모두 입력되어 있어서 그런것으로, -1을 np.NaN으로 변환한 다음 결측값의 개수를 세면 된다.
missingno 패키지를 통해 결측값을 시각화해서 볼 수 있다. 원본을 바로 바꾸면 안되니 copy()로 복사본을 만들었고, feature가 57개로 많은 편이기에 먼저 절반만 보면 다음과 같다.
import numpy as np
import missingno as msno
# 훈련 데이터 복사본에서 -1을 np.NaN로 변환
train_copy = train.copy().replace(-1, np.NaN)
# 결측값 시각화(처음 28개만)
msno.bar(df=train_copy.iloc[:, 1:29], figsize=(13, 6));
나머지 feature들의 결측값도 살펴본다.
msno.bar(df=train_copy.iloc[:, 29:], figsize=(13, 6));
bar() 대신 matrix()를 사용하면 matrix 형태로 시각화할 수 있다. 오른쪽 막대가 결측값의 상대적인 분포를 보여주는데, 튀어나온 부분이 결측값이 몰려있는 행이다. 22는 결측값이 없는 열 개수, 28은 전체 열 개수이다.
msno.matrix(df=train_copy.iloc[:, 1:29], figsize=(13, 6));
def resumetable(df):
print(f'데이터 세트 형상: {df.shape}')
summary = pd.DataFrame(df.dtypes, columns=['데이터 타입'])
summary['결측값 개수'] = (df == -1).sum().values # 피처별 -1 개수
summary['고윳값 개수'] = df.nunique().values
summary['데이터 종류'] = None
for col in df.columns:
if 'bin' in col or col == 'target':
summary.loc[col, '데이터 종류'] = '이진형'
elif 'cat' in col:
summary.loc[col, '데이터 종류'] = '명목형'
elif df[col].dtype == float:
summary.loc[col, '데이터 종류'] = '연속형'
elif df[col].dtype == int:
summary.loc[col, '데이터 종류'] = '순서형'
return summary
결측값이 -1이므로 -1의 개수를 구해 결측값의 개수를 알아본다. 또한 for문을 순회하며 데이터 종류를 추가해주었다.
training data의 feature 요약표는 다음과 같다.
summary = resumetable(train)
summary
feature 요약표를 통해 feature 별 데이터 종류, 결측값 개수, 고윳값 개수 등을 한눈에 볼 수 있는데, 이 중 원하는 feature를 추출할 수 도 있다.
summary[summary['데이터 종류'] == '명목형'].index
summary[summary['데이터 타입'] == 'float64'].index
고윳값별 타깃값 비율을 보고 모델링 시 어떤 feature가 필요한지 구분할 수 있다.
타깃값 분포
def write_percent(ax, total_size):
'''도형 객체를 순회하며 막대 그래프 상단에 타깃값 비율 표시'''
for patch in ax.patches:
height = patch.get_height() # 도형 높이(데이터 개수)
width = patch.get_width() # 도형 너비
left_coord = patch.get_x() # 도형 왼쪽 테두리의 x축 위치
percent = height/total_size*100 # 타깃값 비율
# (x, y) 좌표에 텍스트 입력
ax.text(left_coord + width/2.0, # x축 위치
height + total_size*0.001, # y축 위치
'{:1.1f}%'.format(percent), # 입력 텍스트
ha='center') # 가운데 정렬
mpl.rc('font', size=15)
plt.figure(figsize=(7, 6))
ax = sns.countplot(x='target', data=train)
write_percent(ax, len(train)) # 비율 표시
ax.set_title('Target Distribution');
이진 feature
import matplotlib.gridspec as gridspec
def plot_target_ratio_by_features(df, features, num_rows, num_cols,
size=(12, 18)):
mpl.rc('font', size=9)
plt.figure(figsize=size) # 전체 Figure 크기 설정
grid = gridspec.GridSpec(num_rows, num_cols) # 서브플롯 배치
plt.subplots_adjust(wspace=0.3, hspace=0.3) # 서브플롯 좌우/상하 여백 설정
for idx, feature in enumerate(features):
ax = plt.subplot(grid[idx])
# ax축에 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
sns.barplot(x=feature, y='target', data=df, palette='Set2', ax=ax)
bin_features = summary[summary['데이터 종류'] == '이진형'].index # 이진 피처
# 이진 피처 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
plot_target_ratio_by_features(train, bin_features, 6, 3) # 6행 3열 배치
명목형 feature
명목형 feature도 시각화해준다.
nom_features = summary[summary['데이터 종류'] == '명목형'].index # 명목형 피처
plot_target_ratio_by_features(train, nom_features, 7, 2) # 7행 2열
순서형 feature
순서형 feature도 시각화해준다.
ord_features = summary[summary['데이터 종류'] == '순서형'].index # 순서형 피처
plot_target_ratio_by_features(train, ord_features, 8, 2, (12, 20)) # 8행 2열
연속형 feature
연속형 feature는 연속된 값이므로 고윳값이 매우 많기에 몇 개의 구간으로 나누어 구간별 타깃값 1의 비율을 알아보도록한다.
cut()함수로 여러 개의 값을 구간으로 나눈다. 값들의 리스트를 첫 인수로 전달하고, 구간의 개수를 두번째 인수로 전달한다. 연속형 데이터가 범주형 데이터로 바뀐다.
cont_features = summary[summary['데이터 종류'] == '연속형'].index # 연속형 피처
plt.figure(figsize=(12, 16)) # Figure 크기 설정
grid = gridspec.GridSpec(5, 2) # GridSpec 객체 생성
plt.subplots_adjust(wspace=0.2, hspace=0.4) # 서브플롯 간 여백 설정
for idx, cont_feature in enumerate(cont_features):
# 값을 5개 구간으로 나누기
train[cont_feature] = pd.cut(train[cont_feature], 5)
ax = plt.subplot(grid[idx]) # 분포도를 그릴 서브플롯 설정
sns.barplot(x=cont_feature, y='target', data=train, palette='Set2', ax=ax)
ax.tick_params(axis='x', labelrotation=10) # x축 라벨 회전
연속형 feature 2 - 상관관계
상관관계가 강한 feature들은 예측력이 비슷하므로 하나만 남겨두는 것이 좋다. 상황에 따라 다르므로 종합적으로 보고 제거할지 판단한다.
상관계수의 강도는 보통 피어슨 상관계수 기준 0 ~ 0.2는 매우 약함, 0.2 ~ 0.4는 약함, 0.4 ~ 0.6은 보통, 0.6 ~ 0.8은 강함, 0.8 ~ 1.0은 매우 강함으로 평가한다. 0.8 이상의 매우 강한 상관관계를 보이는 feature가 있다면 제거하는 것도 하나의 방법이다.
결측값이 있으면 상관계수를 구하기 힘드므로 제거하고 히트맵을 그려본다.
train_copy = train_copy.dropna() # np.NaN 값 삭제
plt.figure(figsize=(10, 8))
cont_corr = train_copy[cont_features].corr() # 연속형 피처 간 상관관계
sns.heatmap(cont_corr, annot=True, cmap='OrRd'); # 히트맵 그리기
all_data = pd.concat([train, test], ignore_index=True)
all_data = all_data.drop('target', axis=1) # 타깃값 제거
all_features = all_data.columns # 전체 피처
all_features
고윳값별 순서가 없는 명목형 데이터에 원-핫 인코딩을 적용해준다. 이름에 cat이 포함된 feature가 명목형 feature에 해당한다.
from sklearn.preprocessing import OneHotEncoder
# 명목형 피처 추출
cat_features = [feature for feature in all_features if 'cat' in feature]
onehot_encoder = OneHotEncoder() # 원-핫 인코더 객체 생성
# 인코딩
encoded_cat_matrix = onehot_encoder.fit_transform(all_data[cat_features])
encoded_cat_matrix
EDA에서 결정한 제거할 feature(calc 분류의 feature들 및 그 외 6개 feature)를 제거해주어야 한다. 원-핫 인코딩에 사용한 명목형 feature, calc feature, 제거할 6개 feature를 제외하고 나머지 feature들을 모두 remaining_features에 따로 저장해준다.
# 추가로 제거할 피처
drop_features = ['ps_ind_14', 'ps_ind_10_bin', 'ps_ind_11_bin',
'ps_ind_12_bin', 'ps_ind_13_bin', 'ps_car_14']
# '1) 명목형 피처, 2) calc 분류의 피처, 3) 추가 제거할 피처'를 제외한 피처
remaining_features = [feature for feature in all_features
if ('cat' not in feature and
'calc' not in feature and
feature not in drop_features)]
reminaing_features와 원-핫 인코딩한 명목형 feature들이 담긴 encoded_cat_matrix를 합쳐주어 인코딩과 feature 제거를 완료한다.
from scipy import sparse
all_data_sprs = sparse.hstack([sparse.csr_matrix(all_data[remaining_features]),
encoded_cat_matrix],
format='csr')
합쳐놓은 전체 데이터를 training data, test data로 다시 나눈다. 타깃값도 y에 할당해준다.
num_train = len(train) # 훈련 데이터 개수
# 훈련 데이터와 테스트 데이터 나누기
X = all_data_sprs[:num_train]
X_test = all_data_sprs[num_train:]
y = train['target'].values
평가지표로는 정규화된 지니계수를 사용한다. 정규화 지니계수는 '예측 값에 대한 지니계수 / 예측이 완벽할 때의 지니계수'로, 0에 가까울수록 성능이 나쁘고 1에 가까울수록 좋은 것이다. 다른 사람이 만들어놓은 평가지표함수 코드를 가져다 쓰는 경우가 많다.
import numpy as np
def eval_gini(y_true, y_pred):
# 실제값과 예측값의 크기가 같은지 확인 (값이 다르면 오류 발생)
assert y_true.shape == y_pred.shape
n_samples = y_true.shape[0] # 데이터 개수
L_mid = np.linspace(1 / n_samples, 1, n_samples) # 대각선 값
# 1) 예측값에 대한 지니계수
pred_order = y_true[y_pred.argsort()] # y_pred 크기순으로 y_true 값 정렬
L_pred = np.cumsum(pred_order) / np.sum(pred_order) # 로렌츠 곡선
G_pred = np.sum(L_mid - L_pred) # 예측 값에 대한 지니계수
# 2) 예측이 완벽할 때 지니계수
true_order = y_true[y_true.argsort()] # y_true 크기순으로 y_true 값 정렬
L_true = np.cumsum(true_order) / np.sum(true_order) # 로렌츠 곡선
G_true = np.sum(L_mid - L_true) # 예측이 완벽할 때 지니계수
# 정규화된 지니계수
return G_pred / G_true
만든 eval_gini()를 활용해 모델 훈련 시 검증 파라미터에 전달하기 위한 함수를 만든다.
def gini(preds, dtrain):
labels = dtrain.get_label()
return 'gini', eval_gini(labels, preds), True # 반환값
OOF 예측 방식으로 모델을 훈련하고 예측한다. OOF 예측(Out Of Fold Prediction) 방식은 K-Fold Cross Validation을 하면서 Fold마다 training data로 훈련하고, valid data로 모델 성능을 측정하며 test data로 최종 타깃 확률도 예측하는 것으로 각 Fold별 모델로 여러 번 예측해 평균을 내는 방식이다.
타깃값이 불균형하기에 타깃값이 균등하게 배치되게 Fold를 나눠주는 StratifiedKFold()를 사용한다.
from sklearn.model_selection import StratifiedKFold
# 층화 K 폴드 교차 검증기
folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1991)
이어서 LightGBM의 하이퍼파라미터를 설정해주는데, 이진분류 문제이므로 objetive 파라미터를 binary로 설정해준다.
params = {'objective': 'binary',
'learning_rate': 0.01,
'force_row_wise': True,
'random_state': 0}
이어서 훈련된 모델로 타깃값을 예측할 확률을 담을 1차원 배열을 두 개 만들어 준다.
# OOF 방식으로 훈련된 모델로 검증 데이터 타깃값을 예측한 확률을 담을 1차원 배열
oof_val_preds = np.zeros(X.shape[0])
# OOF 방식으로 훈련된 모델로 테스트 데이터 타깃값을 예측한 확률을 담을 1차원 배열
oof_test_preds = np.zeros(X_test.shape[0])
LightGBM 모델을 훈련하며 OOF 예측도 수행해준다.
import lightgbm as lgb
# OOF 방식으로 모델 훈련, 검증, 예측
for idx, (train_idx, valid_idx) in enumerate(folds.split(X, y)):
# 각 폴드를 구분하는 문구 출력
print('#'*40, f'폴드 {idx+1} / 폴드 {folds.n_splits}', '#'*40)
# 훈련용 데이터, 검증용 데이터 설정
X_train, y_train = X[train_idx], y[train_idx] # 훈련용 데이터
X_valid, y_valid = X[valid_idx], y[valid_idx] # 검증용 데이터
# LightGBM 전용 데이터셋 생성
dtrain = lgb.Dataset(X_train, y_train) # LightGBM 전용 훈련 데이터셋
dvalid = lgb.Dataset(X_valid, y_valid) # LightGBM 전용 검증 데이터셋
# LightGBM 모델 훈련
lgb_model = lgb.train(params=params, # 훈련용 하이퍼파라미터
train_set=dtrain, # 훈련 데이터셋
num_boost_round=1000, # 부스팅 반복 횟수
valid_sets=dvalid, # 성능 평가용 검증 데이터셋
feval=gini, # 검증용 평가지표
early_stopping_rounds=100, # 조기종료 조건
verbose_eval=100) # 100번째마다 점수 출력
# 테스트 데이터를 활용해 OOF 예측
oof_test_preds += lgb_model.predict(X_test)/folds.n_splits
# 모델 성능 평가를 위한 검증 데이터 타깃값 예측
oof_val_preds[valid_idx] += lgb_model.predict(X_valid)
# 검증 데이터 예측 확률에 대한 정규화 지니계수
gini_score = eval_gini(y_valid, oof_val_preds[valid_idx])
print(f'폴드 {idx+1} 지니계수 : {gini_score}\n')
folds.split(X,y)로 데이터를 K개로 나눠주고 enumarte()함수를 적용해 인덱스, K Fold로 나뉜 training data 인덱스, valid data 인덱스를 사용할 수 있게 해주었다.
전체 training data를 훈련용과 검증용으로 나눈 후, lightGBM 전용 데이터셋을 만들어 훈련해준다. 훈련된 모델에 test data로 타깃 확률값을 OOF 예측해주고, 모델 성능 평가를 위해 valid data로도 예측하여 정규화 지니계수를 계산해준다. Fold마다 계산되는 중간 점검용 점수를 알 수 있다.
모델 훈련 시 verbose_eval=100으로 입력했기에 100번째 iteration마다 성능 평가 점수를 출력하는데, logloss는 lightGBM 모델의 기본 평가지표이고 gini는 feval 파라미터에 추가로 전달했던 평가지표이다. iteration이 반복되며 지니계수가 커지다 early stopping 조건이 100으로 되어있기에 100번 연속으로 지니계수가 최댓값을 갱신하지 못해 훈련이 종료된다. 부스팅 최대 반복 횟수는 1000번인데 이를 기록한 경우는 없다. feval 파라미터에 전달한 gini를 기준으로 early stopping을 판단한다.
valid data로 예측한 확률을 실제 타깃값과 비교해 지니계수를 출력하면 다음과 같다.
print('OOF 검증 데이터 지니계수:', eval_gini(y, oof_val_preds))
submission['target'] = oof_test_preds
submission.to_csv('submission.csv')
training data와 test data를 합치고, 타깃값은 제거하며 전체 feature는 따로 저장해준다.
all_data = pd.concat([train, test], ignore_index=True)
all_data = all_data.drop('target', axis=1) # 타깃값 제거
all_features = all_data.columns # 전체 피처
from sklearn.preprocessing import OneHotEncoder
# 명목형 피처
cat_features = [feature for feature in all_features if 'cat' in feature]
# 원-핫 인코딩 적용
onehot_encoder = OneHotEncoder()
encoded_cat_matrix = onehot_encoder.fit_transform(all_data[cat_features])
# '데이터 하나당 결측값 개수'를 파생 피처로 추가
all_data['num_missing'] = (all_data==-1).sum(axis=1)
# 명목형 피처, calc 분류의 피처를 제외한 피처
remaining_features = [feature for feature in all_features
if ('cat' not in feature and 'calc' not in feature)]
# num_missing을 remaining_features에 추가
remaining_features.append('num_missing')
ind 분류의 모든 feature 값들을 연결해 새로운 feature mix_ind를 만들어본다.
# 분류가 ind인 피처
ind_features = [feature for feature in all_features if 'ind' in feature]
is_first_feature = True
for ind_feature in ind_features:
if is_first_feature:
all_data['mix_ind'] = all_data[ind_feature].astype(str) + '_'
is_first_feature = False
else:
all_data['mix_ind'] += all_data[ind_feature].astype(str) + '_'
all_data['mix_ind']
명목형 feature의 고윳값별 개수를 새로운 feature로 추가한다. value_counts()함수로 개수를 구하는데, 이는 Series 타입이기에 to_dict()함수를 이용해 딕셔너리 타입으로 바꿔준다. 방금 추가한 mix_ind feature도 명목형 feature 이므로 cat 분류에 속하는 feature들과 mix_ind feature의 고윳값별 개수를 파생 feature로 만든다.
cat_count_features = []
for feature in cat_features+['mix_ind']:
val_counts_dict = all_data[feature].value_counts().to_dict()
all_data[f'{feature}_count'] = all_data[feature].apply(lambda x:
val_counts_dict[x])
cat_count_features.append(f'{feature}_count')
cat_count_features = []
for feature in cat_features+['mix_ind']:
val_counts_dict = all_data[feature].value_counts().to_dict()
all_data[f'{feature}_count'] = all_data[feature].apply(lambda x:
val_counts_dict[x])
cat_count_features.append(f'{feature}_count')
EDA에서 필요없다고 판단한 feature들을 제거하고, 새로 만들어준 encoded_cat matrix, cat_count_features를 추가해준다.
from scipy import sparse
# 필요 없는 피처들
drop_features = ['ps_ind_14', 'ps_ind_10_bin', 'ps_ind_11_bin',
'ps_ind_12_bin', 'ps_ind_13_bin', 'ps_car_14']
# remaining_features, cat_count_features에서 drop_features를 제거한 데이터
all_data_remaining = all_data[remaining_features+cat_count_features].drop(drop_features, axis=1)
# 데이터 합치기
all_data_sprs = sparse.hstack([sparse.csr_matrix(all_data_remaining),
encoded_cat_matrix],
format='csr')
1) 명목형 feature에 원-핫 인코딩을 적용했다.
2) 데이터 하나 당 가지고 있는 결측값 개수를 새로운 feature로 만들었다.
3) 모든 ind feature 값을 연결해서 새로운 명목형 feature를 만들어 4에서 활용했다.
4) 명목형 feature의 고윳값별 개수를 새로운 feature로 만들었다.
5) 필요 없는 featue를 제거했다.
Feature engineering을 마무리했으니 다시 training data와 test data로 나눈다.
num_train = len(train) # 훈련 데이터 개수
# 훈련 데이터와 테스트 데이터 나누기
X = all_data_sprs[:num_train]
X_test = all_data_sprs[num_train:]
y = train['target'].values
베이지안 최적화를 위한 데이터셋을 준비한다.
import lightgbm as lgb
from sklearn.model_selection import train_test_split
# 8:2 비율로 훈련 데이터, 검증 데이터 분리 (베이지안 최적화 수행용)
X_train, X_valid, y_train, y_valid = train_test_split(X, y,
test_size=0.2,
random_state=0)
# 베이지안 최적화용 데이터셋
bayes_dtrain = lgb.Dataset(X_train, y_train)
bayes_dvalid = lgb.Dataset(X_valid, y_valid)
하이퍼파라미터를 설정할 때는 범위를 점점 좁히거나 다른 상위권 코드를 참고해서 설정한다. 범위를 좁힐 때는 0~1 범위의 파라미터의 경우 0~1 전체로 최적화를 수행하고, 0.5가 최적 하이퍼파라미터라면 다시 0.5 주변으로 0.4~0.6 정도로 좁혀 최적화를 수행하는 식으로 진행한다.
탐색할 하이퍼파라미터 범위를 param_bounds에 지정하고, 값을 고정할 하이퍼파라미터는 fixed_params에 저장해둔다.
# 베이지안 최적화를 위한 하이퍼파라미터 범위
param_bounds = {'num_leaves': (30, 40),
'lambda_l1': (0.7, 0.9),
'lambda_l2': (0.9, 1),
'feature_fraction': (0.6, 0.7),
'bagging_fraction': (0.6, 0.9),
'min_child_samples': (6, 10),
'min_child_weight': (10, 40)}
# 값이 고정된 하이퍼파라미터
fixed_params = {'objective': 'binary',
'learning_rate': 0.005,
'bagging_freq': 1,
'force_row_wise': True,
'random_state': 1991}
베이지안 최적화를 수행하기 위한 평가지표 지니계수 계산 함수를 만들어 지니계수를 계산해 최적 하이퍼파라미터를 찾도록한다. 최적화하려는 LightGBM 모델의 하이퍼파라미터 7개를 인수로 전달받아 지니계수를 반환한다.
def eval_function(num_leaves, lambda_l1, lambda_l2, feature_fraction,
bagging_fraction, min_child_samples, min_child_weight):
'''최적화하려는 평가지표(지니계수) 계산 함수'''
# 베이지안 최적화를 수행할 하이퍼파라미터
params = {'num_leaves': int(round(num_leaves)),
'lambda_l1': lambda_l1,
'lambda_l2': lambda_l2,
'feature_fraction': feature_fraction,
'bagging_fraction': bagging_fraction,
'min_child_samples': int(round(min_child_samples)),
'min_child_weight': min_child_weight,
'feature_pre_filter': False}
# 고정된 하이퍼파라미터도 추가
params.update(fixed_params)
print('하이퍼파라미터:', params)
# LightGBM 모델 훈련
lgb_model = lgb.train(params=params,
train_set=bayes_dtrain,
num_boost_round=2500,
valid_sets=bayes_dvalid,
feval=gini,
early_stopping_rounds=300,
verbose_eval=False)
# 검증 데이터로 예측 수행
preds = lgb_model.predict(X_valid)
# 지니계수 계산
gini_score = eval_gini(y_valid, preds)
print(f'지니계수 : {gini_score}\n')
return gini_score
베이지안 최적화 객체를 생성하고, maximize()메서드로 베이지안 회적화를 수행한다.
from bayes_opt import BayesianOptimization
# 베이지안 최적화 객체 생성
optimizer = BayesianOptimization(f=eval_function, # 평가지표 계산 함수
pbounds=param_bounds, # 하이퍼파라미터 범위
random_state=0)
# 베이지안 최적화 수행
optimizer.maximize(init_points=3, n_iter=6)
# 평가함수 점수가 최대일 때 하이퍼파라미터
max_params = optimizer.max['params']
max_params
# 정수형 하이퍼파라미터 변환
max_params['num_leaves'] = int(round(max_params['num_leaves']))
max_params['min_child_samples'] = int(round(max_params['min_child_samples']))
# 값이 고정된 하이퍼파라미터 추가
max_params.update(fixed_params)
max_params
베이지안 최적화는 그리드서치와 달리 최적 예측기를 제공하지 않는다. 베이지안 최적화로 찾은 하이퍼파라미터를 활용해 LightGBM 모델을 다시 훈련한다. 베이스라인 모델 훈련 코드와 비슷한데, 파라미터만 max_parmas로 바꿔주었다.
from sklearn.model_selection import StratifiedKFold
# 층화 K 폴드 교차 검증기 생성
folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1991)
# OOF 방식으로 훈련된 모델로 검증 데이터 타깃값을 예측한 확률을 담을 1차원 배열
oof_val_preds = np.zeros(X.shape[0])
# OOF 방식으로 훈련된 모델로 테스트 데이터 타깃값을 예측한 확률을 담을 1차원 배열
oof_test_preds = np.zeros(X_test.shape[0])
# OOF 방식으로 모델 훈련, 검증, 예측
for idx, (train_idx, valid_idx) in enumerate(folds.split(X, y)):
# 각 폴드를 구분하는 문구 출력
print('#'*40, f'폴드 {idx+1} / 폴드 {folds.n_splits}', '#'*40)
# 훈련용 데이터, 검증용 데이터 설정
X_train, y_train = X[train_idx], y[train_idx] # 훈련용 데이터
X_valid, y_valid = X[valid_idx], y[valid_idx] # 검증용 데이터
# LightGBM 전용 데이터셋 생성
dtrain = lgb.Dataset(X_train, y_train) # LightGBM 전용 훈련 데이터셋
dvalid = lgb.Dataset(X_valid, y_valid) # LightGBM 전용 검증 데이터셋
# LightGBM 모델 훈련
lgb_model = lgb.train(params=max_params, # 최적 하이퍼파라미터
train_set=dtrain, # 훈련 데이터셋
num_boost_round=2500, # 부스팅 반복 횟수
valid_sets=dvalid, # 성능 평가용 검증 데이터셋
feval=gini, # 검증용 평가지표
early_stopping_rounds=300, # 조기종료 조건
verbose_eval=100) # 100번째마다 점수 출력
# 테스트 데이터를 활용해 OOF 예측
oof_test_preds += lgb_model.predict(X_test)/folds.n_splits
# 모델 성능 평가를 위한 검증 데이터 타깃값 예측
oof_val_preds[valid_idx] += lgb_model.predict(X_valid)
# 검증 데이터 예측확률에 대한 정규화 지니계수
gini_score = eval_gini(y_valid, oof_val_preds[valid_idx])
print(f'폴드 {idx+1} 지니계수 : {gini_score}\n')
검증 데이터로 예측한 확률과 실제 타깃값의 지니계수를 보면 베이스라인 모델보다 0.0085 정도 높아졌다.
print('OOF 검증 데이터 지니계수 :', eval_gini(y, oof_val_preds))
# XGBoost용 gini() 함수
def gini(preds, dtrain):
labels = dtrain.get_label()
return 'gini', eval_gini(labels, preds)
import pandas as pd
# 데이터 경로
data_path = '/kaggle/input/porto-seguro-safe-driver-prediction/'
train = pd.read_csv(data_path + 'train.csv', index_col='id')
test = pd.read_csv(data_path + 'test.csv', index_col='id')
submission = pd.read_csv(data_path + 'sample_submission.csv', index_col='id')
# 베이지안 최적화를 위한 하이퍼파라미터 범위
param_bounds = {'max_depth': (4, 8),
'subsample': (0.6, 0.9),
'colsample_bytree': (0.7, 1.0),
'min_child_weight': (5, 7),
'gamma': (8, 11),
'reg_alpha': (7, 9),
'reg_lambda': (1.1, 1.5),
'scale_pos_weight': (1.4, 1.6)}
# 값이 고정된 하이퍼파라미터
fixed_params = {'objective': 'binary:logistic',
'learning_rate': 0.02,
'random_state': 1991}
LightGBM의 eval_function()과 전체적으로 유사하지만 4가지 다른 부분이 있다.
1) 하이퍼파라미터명
2) train() 메서드 내 검증 데이터 전달 방식
3) train() 메서드 내 maximize 파라미터
4) predict() 메서드에 Dmatrix 타입을 전달하는 점
def eval_function(max_depth, subsample, colsample_bytree, min_child_weight,
reg_alpha, gamma, reg_lambda, scale_pos_weight):
'''최적화하려는 평가지표(지니계수) 계산 함수'''
# 베이지안 최적화를 수행할 하이퍼파라미터
params = {'max_depth': int(round(max_depth)),
'subsample': subsample,
'colsample_bytree': colsample_bytree,
'min_child_weight': min_child_weight,
'gamma': gamma,
'reg_alpha':reg_alpha,
'reg_lambda': reg_lambda,
'scale_pos_weight': scale_pos_weight}
# 값이 고정된 하이퍼파라미터도 추가
params.update(fixed_params)
print('하이퍼파라미터 :', params)
# XGBoost 모델 훈련
xgb_model = xgb.train(params=params,
dtrain=bayes_dtrain,
num_boost_round=2000,
evals=[(bayes_dvalid, 'bayes_dvalid')],
maximize=True,
feval=gini,
early_stopping_rounds=200,
verbose_eval=False)
best_iter = xgb_model.best_iteration # 최적 반복 횟수
# 검증 데이터로 예측 수행
preds = xgb_model.predict(bayes_dvalid,
iteration_range=(0, best_iter))
# 지니계수 계산
gini_score = eval_gini(y_valid, preds)
print(f'지니계수 : {gini_score}\n')
return gini_score
베이지안 최적화를 수행하고 최적화된 파라미터를 출력해본다.
from bayes_opt import BayesianOptimization
# 베이지안 최적화 객체 생성
optimizer = BayesianOptimization(f=eval_function,
pbounds=param_bounds,
random_state=0)
# 베이지안 최적화 수행
optimizer.maximize(init_points=3, n_iter=6)
max_depth는 트리 깊이를 의미하므로 정수형이어야하니 정수형으로 바꾸고, 고정 하이퍼파라미터도 추가한다.
# 정수형 하이퍼파라미터 변환
max_params['max_depth'] = int(round(max_params['max_depth']))
# 값이 고정된 하이퍼파라미터 추가
max_params.update(fixed_params)
max_params
OOF 방식을 이용해 XGBoost 모델을 훈련한다.
from sklearn.model_selection import StratifiedKFold
# 층화 K 폴드 교차 검증기 생성
folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1991)
# OOF 방식으로 훈련된 모델로 검증 데이터 타깃값을 예측한 확률을 담을 1차원 배열
oof_val_preds = np.zeros(X.shape[0])
# OOF 방식으로 훈련된 모델로 테스트 데이터 타깃값을 예측한 확률을 담을 1차원 배열
oof_test_preds = np.zeros(X_test.shape[0])
# OOF 방식으로 모델 훈련, 검증, 예측
for idx, (train_idx, valid_idx) in enumerate(folds.split(X, y)):
# 각 폴드를 구분하는 문구 출력
print('#'*40, f'폴드 {idx+1} / 폴드 {folds.n_splits}', '#'*40)
# 훈련용 데이터, 검증용 데이터 설정
X_train, y_train = X[train_idx], y[train_idx]
X_valid, y_valid = X[valid_idx], y[valid_idx]
# XGBoost 전용 데이터셋 생성
dtrain = xgb.DMatrix(X_train, y_train)
dvalid = xgb.DMatrix(X_valid, y_valid)
dtest = xgb.DMatrix(X_test)
# XGBoost 모델 훈련
xgb_model = xgb.train(params=max_params,
dtrain=dtrain,
num_boost_round=2000,
evals=[(dvalid, 'valid')],
maximize=True,
feval=gini,
early_stopping_rounds=200,
verbose_eval=100)
# 모델 성능이 가장 좋을 때의 부스팅 반복 횟수 저장
best_iter = xgb_model.best_iteration
# 테스트 데이터를 활용해 OOF 예측
oof_test_preds += xgb_model.predict(dtest,
iteration_range=(0, best_iter))/folds.n_splits
# 모델 성능 평가를 위한 검증 데이터 타깃값 예측
oof_val_preds[valid_idx] += xgb_model.predict(dvalid,
iteration_range=(0, best_iter))
# 검증 데이터 예측 확률에 대한 정규화 지니계수
gini_score = eval_gini(y_valid, oof_val_preds[valid_idx])
print(f'폴드 {idx+1} 지니계수 : {gini_score}\n')
OOF 검증 데이터 지니계수도 출력해보면 LightGBM의 계수보다 높다.
print('OOF 검증 데이터 지니계수 :', eval_gini(y, oof_val_preds))
이를 바탕으로 최종 예측 확률을 제출하면 LightGBM의 점수보다 0.0002정도 높고, 상위 17등 정도된다.
oof_test_preds = oof_test_preds_lgb * 0.5 + oof_test_preds_xgb * 0.5
참고: 머신러닝·딥러닝 문제해결 전략 (캐글 수상작 리팩터링으로 배우는 문제해결 프로세스와 전략)
참고: https://www.kaggle.com/code/bertcarremans/data-preparation-exploration/notebook,
https://www.kaggle.com/code/xiaozhouwang/2nd-place-lightgbm-solution/script