머신러닝·딥러닝 문제해결 전략 책을 읽으면서
Kaggle 경진대회 코드와 문제해결 전략을 정리한 글
- 피처 맞춤 인코딩
- 피처 스케일링
- 하이퍼퍼라미터 최적화
# 훈련, 테스트, 제출 샘플 파일 불러오기
import pandas as pd
data_path = '/kaggle/input/cat-in-the-dat/'
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')
인코딩은 이진 피처, 순서형 피처, 명목형 피처, 날짜 피처 순으로 진행
all_data
는 훈련 데이터와 테스트 데이터를 합친 데이터로, 이진 피처, 순서형 피처, 명목형 피처, 날짜 피처가 모두 존재.
이진 피처와 순서형 피처는 적절히 인코딩해서all_data
에 바로 적용하고 명목형 피처와 날짜 피처는 원-핫 인코딩해 별도의 행렬로 저장.
이때all_data
에서 기존의 명목형 피처와 날짜 피처는 삭제함.
명목형 피처와 날짜 피처를all_data
에 바로 적용하지 못하는 이유는 원-핫 인코딩을 하면 열 개수가 많아지기 때문.
마지막으로all_data
와 원-핫 인코딩된 명목형 피처와 날짜 피처를 합쳐야 하며 이는 피처 스케일링까지 모두 끝낸 뒤에 이루어짐.
all_data = pd.concat([train, test]) # 훈련 데이터와 테스트 데이터 합치기
all_data = all_data.drop('target', axis=1) # 타깃값 제거
all_data
bin_0 | bin_1 | bin_2 | bin_3 | bin_4 | nom_0 | ... | nom_9 | ord_0 | ord_1 | ord_2 | ord_3 | ord_4 | ord_5 | day | month | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | ||||||||||||||||
0 | 0 | 0 | 0 | T | Y | Green | ... | 2f4cb3d51 | 2 | Grandmaster | Cold | h | D | kr | 2 | 2 |
1 | 0 | 1 | 0 | T | Y | Green | ... | f83c56c21 | 1 | Grandmaster | Hot | a | A | bF | 7 | 8 |
2 | 0 | 0 | 0 | F | Y | Blue | ... | ae6800dd0 | 1 | Expert | Lava Hot | h | R | Jc | 7 | 2 |
3 | 0 | 1 | 0 | F | Y | Red | ... | 8270f0d71 | 1 | Grandmaster | Boiling Hot | i | D | kW | 2 | 1 |
4 | 0 | 0 | 0 | F | N | Red | ... | b164b72a7 | 1 | Grandmaster | Freezing | a | R | qP | 7 | 8 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
499995 | 0 | 0 | 0 | F | N | Green | ... | acc31291f | 1 | Novice | Lava Hot | j | A | Gb | 1 | 3 |
499996 | 1 | 0 | 0 | F | Y | Green | ... | eae3446d0 | 1 | Contributor | Lava Hot | f | S | Ed | 2 | 2 |
499997 | 0 | 1 | 1 | T | Y | Green | ... | 33dd3cf4b | 1 | Novice | Boiling Hot | g | V | TR | 3 | 1 |
499998 | 1 | 0 | 0 | T | Y | Blue | ... | d4cf587dd | 2 | Grandmaster | Boiling Hot | g | X | Ye | 2 | 1 |
499999 | 0 | 0 | 0 | T | Y | Green | ... | 2d610f52c | 2 | Novice | Freezing | l | J | ex | 2 | 2 |
500000 rows × 23 columns
all_data['bin_3'] = all_data['bin_3'].map({'F':0, 'T':1})
all_data['bin_4'] = all_data['bin_4'].map({'N':0, 'Y':1})
DataFrame의 열(피처)을 호출하면 반환값이 Series 타입이며 Series 객체에 map() 함수를 호출하면 전달받은 딕셔너리나 함수를 Series의 모든 원소에 적용해 결과를 반환함.
# ord_1, ord_2 피처 인코딩 - map() 함수 사용
ord1dict = {'Novice':0, 'Contributor':1, 'Expert':2,
'Master':3, 'Grandmaster':4}
ord2dict = {'Freezing':0, 'Cold':1, 'Warm':2,
'Hot':3, 'Boiling Hot':4, 'Lava Hot':5}
all_data['ord_1'] = all_data['ord_1'].map(ord1dict)
all_data['ord_2'] = all_data['ord_2'].map(ord2dict)
# ord_3, ord_4, ord_5 피처 인코딩 - OrdinalEncoder 사용
from sklearn.preprocessing import OrdinalEncoder
ord_345 = ['ord_3', 'ord_4', 'ord_5']
ord_encoder = OrdinalEncoder() # OrdinalEncoder 객체 생성
# ordinal 인코딩 적용
all_data[ord_345] = ord_encoder.fit_transform(all_data[ord_345])
# 피처별 인코딩 순서 출력
for feature, categories in zip(ord_345, ord_encoder.categories_):
print(feature)
print(categories)
ord_3
['a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o']
ord_4
['A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R'
'S' 'T' 'U' 'V' 'W' 'X' 'Y' 'Z']
ord_5
['AP' 'Ai' 'Aj' 'BA' 'BE' 'Bb' 'Bd' 'Bn' 'CL' 'CM' 'CU' 'CZ' 'Cl' 'DH'
'DN' 'Dc' 'Dx' 'Ed' 'Eg' 'Er' 'FI' 'Fd' 'Fo' 'GD' 'GJ' 'Gb' 'Gx' 'Hj'
'IK' 'Id' 'JX' 'Jc' 'Jf' 'Jt' 'KR' 'KZ' 'Kf' 'Kq' 'LE' 'MC' 'MO' 'MV'
'Mf' 'Ml' 'Mx' 'NV' 'Nf' 'Nk' 'OR' 'Ob' 'Os' 'PA' 'PQ' 'PZ' 'Ps' 'QM'
'Qb' 'Qh' 'Qo' 'RG' 'RL' 'RP' 'Rm' 'Ry' 'SB' 'Sc' 'TR' 'TZ' 'To' 'UO'
'Uk' 'Uu' 'Vf' 'Vx' 'WE' 'Wc' 'Wv' 'XI' 'Xh' 'Xi' 'YC' 'Yb' 'Ye' 'ZR'
'ZS' 'Zc' 'Zq' 'aF' 'aM' 'aO' 'aP' 'ac' 'av' 'bF' 'bJ' 'be' 'cA' 'cG'
'cW' 'ck' 'cp' 'dB' 'dE' 'dN' 'dO' 'dP' 'dQ' 'dZ' 'dh' 'eG' 'eQ' 'eb'
'eg' 'ek' 'ex' 'fO' 'fh' 'gJ' 'gM' 'hL' 'hT' 'hh' 'hp' 'iT' 'ih' 'jS'
'jV' 'je' 'jp' 'kC' 'kE' 'kK' 'kL' 'kU' 'kW' 'ke' 'kr' 'kw' 'lF' 'lL'
'll' 'lx' 'mb' 'mc' 'mm' 'nX' 'nh' 'oC' 'oG' 'oH' 'oK' 'od' 'on' 'pa'
'ps' 'qA' 'qJ' 'qK' 'qP' 'qX' 'qo' 'qv' 'qw' 'rZ' 'ri' 'rp' 'sD' 'sV'
'sY' 'sn' 'su' 'tM' 'tP' 'tv' 'uJ' 'uS' 'ud' 'us' 'ut' 'ux' 'uy' 'vK'
'vq' 'vy' 'wu' 'wy' 'xP' 'xy' 'yN' 'yY' 'yc' 'zU']
ordencoder.categories는 어떤 순서로 ordinal 인코딩을 적용했는지 보여줌.
all_data[ord_345].head()
ord_3 | ord_4 | ord_5 | |
---|---|---|---|
id | |||
0 | 7.0 | 3.0 | 136.0 |
1 | 0.0 | 0.0 | 93.0 |
2 | 7.0 | 17.0 | 31.0 |
3 | 8.0 | 3.0 | 134.0 |
4 | 0.0 | 17.0 | 158.0 |
알파벳순으로 인코딩했기 때문에 a는 0.0, b는 1.0, c는 2.0, d는 3.0 이런식으로 바뀜
nom_features = ['nom_' + str(i) for i in range(10)] # 명목형 피처
이 명목형 피처를 원-핫 인코딩해 별도 행렬에 저장하고, 이어서 all_data에서 명목형 피처를 삭제. 원-핫 인코딩을 하면 열 개수가 늘어나서 all_data에서 곧바로 인코딩할 수 없기 때문.
from sklearn.preprocessing import OneHotEncoder
onehot_encoder = OneHotEncoder() # OneHotEncoder 객체 생성
# 원-핫 인코딩 적용
encoded_nom_matrix = onehot_encoder.fit_transform(all_data[nom_features])
encoded_nom_matrix
<500000x16276 sparse matrix of type '<class 'numpy.float64'>'
with 5000000 stored elements in Compressed Sparse Row format>
OneHotEncoder로 원-핫 인코딩을 적용하면 희소 행렬을 CSR 형식으로 반환
출력 결과를 보면 원-핫 인코딩된 명목형 피처의 행렬 크기가 (500000 x 16276) 인 것을 확인
희소 행렬과 COO, CSR 형식
대부분 값이 0으로 채워진 행렬을 희소 행렬(sparse matrix)이라고 함. 원-핫 인코딩을 적용하면 희소 행렬을 만들게 되어 메모리 낭비가 심해지고 행렬 크기가 늘어서 연산 시간도 오래 걸림.
이런 문제를 개선하도록 행렬 형식을 변환해 줘야 하는데 대표적으로 COO(coordinate list) 형식과 CSR(compressed sparse row) 형식이 있음. 희소 행렬을 COO 형식이나 CSR 형식으로 표현하면 메모리 낭비를 줄일 수 있으며
이중에서도 CSR 형식이 메모리를 더 적게 쓰면서 연산도 빠름.
# all_data에서 기존 명목형 피처 삭제
all_data = all_data.drop(nom_features, axis=1)
date_features = ['day', 'month']
# 원-핫 인코딩 적용
encoded_date_matrix = onehot_encoder.fit_transform(all_data[date_features])
# 기존 날짜 피처 삭제
all_data = all_data.drop(date_features, axis=1)
encoded_date_matrix
<500000x19 sparse matrix of type '<class 'numpy.float64'>'
with 1000000 stored elements in Compressed Sparse Row format>
원-핫 인코딩된 행렬 크기는 (500000 x 19)
day 피처 고윳값은 7개, month 피처 고윳값은 12개라서 인코딩 후 열이 총 19개가 되었음
from sklearn.preprocessing import MinMaxScaler
ord_features = ['ord_' + str(i) for i in range(6)] # 순서형 피처
# min-max 정규화
all_data[ord_features] = MinMaxScaler().fit_transform(all_data[ord_features])
# 합치기 전 all_data
all_data.head()
bin_0 | bin_1 | bin_2 | bin_3 | bin_4 | ord_0 | ord_1 | ord_2 | ord_3 | ord_4 | ord_5 | |
---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||
0 | 0 | 0 | 0 | 1 | 1 | 0.5 | 1.0 | 0.2 | 0.500000 | 0.12 | 0.712042 |
1 | 0 | 1 | 0 | 1 | 1 | 0.0 | 1.0 | 0.6 | 0.000000 | 0.00 | 0.486911 |
2 | 0 | 0 | 0 | 0 | 1 | 0.0 | 0.5 | 1.0 | 0.500000 | 0.68 | 0.162304 |
3 | 0 | 1 | 0 | 0 | 1 | 0.0 | 1.0 | 0.8 | 0.571429 | 0.12 | 0.701571 |
4 | 0 | 0 | 0 | 0 | 0 | 0.0 | 1.0 | 0.0 | 0.000000 | 0.68 | 0.827225 |
from scipy import sparse
# 인코딩 및 스케일링된 피처 합치기
all_data_sprs = sparse.hstack([sparse.csr_matrix(all_data),
encoded_nom_matrix,
encoded_date_matrix],
format='csr')
all_data_sprs
<500000x16306 sparse matrix of type '<class 'numpy.float64'>'
with 9163718 stored elements in Compressed Sparse Row format>
hstack()은 행렬을 수평 방향으로 합치며 format='csr'을 전달하여 합친 결과를 CSR 형식으로 반환
인코딩된 모든 피처를 합친 all_data_sprs를 출력해보면 500,000행 16,306열로 구성되어 있는 것을 확인
이정도 크기를 DataFrame으로 처리하면 메모리 낭비가 심하고 훈련 속도가 떨어지기 때문에
DataFrame으로 변환하지 않고 CSR 형식 그대로 사용하도록 함
num_train = len(train) # 훈련 데이터 개수
# 훈련 데이터와 테스트 데이터 나누기
X_train = all_data_sprs[:num_train] # 0~num_train - 1행
X_test = all_data_sprs[num_train:] # num_train~마지막 행
y = train['target']
from sklearn.model_selection import train_test_split
# 훈련 데이터, 검증 데이터 분리
X_train, X_valid, y_train, y_valid = train_test_split(X_train, y,
test_size=0.1,
stratify=y,
random_state=10)
%%time # 해당 셀 실행 후 소요 시간을 출력해 주는 기능
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression
# 로지스틱 회귀 모델 생성
logistic_model = LogisticRegression()
# 하이퍼파라미터 값 목록
lr_params = {'C': [0.1, 0.125, 0.2], 'max_iter': [800, 900, 1000],
'solver': ['liblinear'], 'random_state': [42]}
# 그리드서치 객체 생성
gridsearch_logistic_model = GridSearchCV(estimator=logistic_model,
param_grid=lr_params,
scoring='roc_auc', # 평가지표
cv=5)
# 그리드서치 수행
gridsearch_logistic_model.fit(X_train, y_train)
print('최적 하이퍼파라미터:', gridsearch_logistic_model.best_params_)
UsageError: Can't use statement directly after '%%time'!
최적 하이퍼파라미터는 C: 0.125, max_iter: 800
CPU 시간의 총합이 Wall time보다 큰 이유는 이 코드를 병렬로 실행했기 때문 (CPU 시간은 개별 코어의 수행 시간을 모두 합친 값)
# 검증 데이터로 타깃 예측값을 구함
y_valid_preds = gridsearch_logistic_model.predict_proba(X_valid)[:, 1]
from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수
# 검증 데이터 ROC AUC
roc_auc = roc_auc_score(y_valid, y_valid_preds)
print(f'검증 데이터 ROC AUC : {roc_auc:.4f}')
ROC AUC가 0.8045로 베이스라인 모델보다 0.008만큼 향상됨
# 타깃값 1일 확률 예측
y_preds = gridsearch_logistic_model.best_estimator_.predict_proba(X_test)[:, 1]
# 제출 파일 생성
submission['target'] = y_preds
submission.to_csv('submission.csv')
num_train = len(train) # 훈련 데이터 개수
# 훈련 데이터와 테스트 데이터 나누기
X_train = all_data_sprs[:num_train] # 0~num_train - 1행
X_test = all_data_sprs[num_train:] # num_train~마지막 행
y = train['target']
# 그리드서치 수행
gridsearch_logistic_model.fit(X_train, y)
print('최적 하이퍼파라미터:', gridsearch_logistic_model.best_params_)
# 타깃값 1일 확률 예측
y_preds = gridsearch_logistic_model.best_estimator_.predict_proba(X_test)[:, 1]
# 제출 파일 생성
submission['target'] = y_preds
submission.to_csv('submission.csv')
90%의 훈련 데이터만으로 모델링했을 때보다 0.0062만큼 향상되었음
더 일반적인 흐름
이번 장에서는 단 하나의 머신러닝 모델을 계속 사용했지만, 일반적으로는 여러 가지 방법으로 모델링을 해서 그 중 검증 데이터 성능이 가장 높은 모델을 제출용으로 사용함. 이렇게 제출용 모델을 선정했다면, 선정된 모델을 '검증 데이터까지 포함한 전체 훈련 데이터로 다시 훈련'하여 그 결과를 최종 제출 데이터로 쓴다는 것이 이번 절의 요지.