머신러닝·딥러닝 문제해결 전략 | Ch7 Categorical Feature Encoding Challenge - Modeling

리혜·2022년 6월 29일
0
post-thumbnail

머신러닝·딥러닝 문제해결 전략 책을 읽으면서
Kaggle 경진대회 코드와 문제해결 전략을 정리한 글


7장 범주형 데이터 이진분류 경진대회 - 성능 개선

성능 개선Ⅰ

  • 6장에서는 베이스라인 모델과 다른 모델을 사용해 더 높은 성능을 얻었지만 이번 7장에서는 베이스라인 모델 자체의 성능을 높임
  1. 피처 맞춤 인코딩
  2. 피처 스케일링
  3. 하이퍼퍼라미터 최적화
# 훈련, 테스트, 제출 샘플 파일 불러오기
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

이진 피처 인코딩

  • bin_0, bin_1, bin_2 피처는 이미 0과 1로만 잘 구성되어 있어 따로 인코딩하지 않아도 됨
  • 반면 bin_3, bin_4 피처는 각각 T와 F, Y와 N이라는 문자로 구성되어 있어 각각 1과 0으로 변경.
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_0 피처는 이미 숫자로 구성되어 있어 인코딩하지 않아도 됨
  • ord_1과 ord_2 피처는 순서를 정해서 인코딩하며 ord_3, ord_4, ord_5는 알파벳 순서대로 인코딩
# 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_0부터 nom_9까지 총 10개
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)

날짜 피처 인코딩

  • data와 month는 날짜 피처이며 원-핫 인코딩을 적용
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개가 되었음

피처 엔지니어링Ⅱ : 피처 스케일링

  • 피처 스케일링(feature scaling)이란 서로 다른 피처들의 값의 범위가 일치하도록 조정하는 작업
  • 피처 스케일링이 필요한 이유는 수치형 피처들의 유효 값 범위가 서로 다르면 훈련이 제대로 안될 수도 있기 때문
  • 순서형 피처는 여러 가지 값을 가지고 있으므로 값 범위를 0~1 사이가 되도록 스케일링

순서형 피처 스케일링

  • 다른 피처들과 범위를 맞추기 위해 순서형 피처에 min-max 정규화 적용
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에는 이진 피처와 순서형 피처가 인코딩되어 있으며
  • 명목형 피처와 날짜 피처는 원-핫 인코딩되어 각각 encoded_nom_matrix와 encoded_date_maxtrix에 저장되어 있으므로 세 데이터를 합쳐야 함
  • 그런데 all_data는 DataFrame이고 encoded_nom_matrix와 encoded_date_maxtrix는 CSR 형식의 행렬이므로 all_data를 CSR 형식으로 만들어 합침
# 합치기 전 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)

하이퍼파라미터 최적화

  • 그리드서치를 활용해 로지스틱 회귀 모델의 하이퍼파라미터 최적화
  • 탐색할 파라미터는 Cmax_iter
    • C : 규제 강도를 조절하는 파라미터, 값이 작을수록 규제 강도가 세짐
%%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')

성능 개선 Ⅱ

  • 앞서 모델 훈련 시 전체 훈련 데이터를 9:1 비율로 훈련 데이터와 검증 데이터로 나눔
  • 훈련 데이터는 훈련용으로만 사용하고 검증 데이터는 모델 성능 검증용으로만 사용했지만 이렇게 많은 데이터를 검증용으로만 사용하기엔 아까움
  • 그래서 지금까지 다룬 모델링 절차를 그대로 유지한 채 훈련 데이터 전체를 사용해 모델 훈련
  • train_test_split()으로 훈련 데이터(90%)와 검증 데이터(10%)로 나누는 부분과 연관된 코드를 제외하고 나머지 모든 절차는 동일하게 수행
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만큼 향상되었음

더 일반적인 흐름
이번 장에서는 단 하나의 머신러닝 모델을 계속 사용했지만, 일반적으로는 여러 가지 방법으로 모델링을 해서 그 중 검증 데이터 성능이 가장 높은 모델을 제출용으로 사용함. 이렇게 제출용 모델을 선정했다면, 선정된 모델을 '검증 데이터까지 포함한 전체 훈련 데이터로 다시 훈련'하여 그 결과를 최종 제출 데이터로 쓴다는 것이 이번 절의 요지.

핵심 요약

  1. 피처 요약표는 피처별 데이터 타입, 결측값 개수, 고윳값 개수, 실제 입력값 등을 정리한 표
  2. 타깃값 분포를 알면 데이터가 얼마나 불균형한지 파악하여 부족한 타깃값에 더 집중해 모델링을 수행할 수 있음
  3. 피처별로 데이터 특성에 맞게 인코딩해줘야 모델 성능을 효과적으로 끌어올릴 수 있음
    • 이진 피처 : 값이 0과 1이 아닌 경우 0과 1로 인코딩함
    • 명목형 피처 : 고윳값 개수가 너무 많지 않다면 머신러닝 모델이 이해할 수 있도록 원-핫 인코딩을 적용함
    • 순서형 피처 : 고윳값들의 순서에 맞게 인코딩함
    • 날짜 피처 : 순환형 데이터는 삼각함수를 이용해 인코딩하거나 원-핫 인코딩을 적용
  4. 로지스틱 회귀는 선형 회귀 방식을 응용해 분류 문제에 적용한 모델. 비록 분류 문제에 사용되는 모델이지만 훈련 원리는 선형 회귀 모델과 유사함
  5. 피처 스케일링이란 피처 간 값의 범위를 일치시키는 작업. 피처마다 값의 범위가 다르면 훈련이 제대로 안 될 수 있으므로 범위 차이가 심한 피처들은 스케일링하여 비슷하게 맞춰줘야 함.

0개의 댓글