티스토리 뷰
[경진대회] 실전 - 해로운 뇌 활동 감지 및 분류 (5) : 피처 엔지니어링 2 (for Catboost)
sikaro 2024. 2. 18. 21:20https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/overview
이 글을 쓰고 있는 2024년 2월 18일 현재, 캐글에서 진행하고 있는 컴피티션이다.
필자는 본 경진대회를 진행하면서, 실제로 배운 데이터 분석 요소들을 적용해보려고 한다.
글로 써질 것들은 모델을 학습하고, 만들기 까지의 과정이다.
오늘 시간에는 베이스라인 모델링을 위한 추가적인 피처 엔지니어링을 진행한다.
베이스라인 모델링 개요
베이스라인 모델링은 본격적으로 성능 개선을 하기 전에, 어떤 모델을 싱글 모델로 가장 좋은 모델을 활용할 지, 그리고 그 단계에서 어떻게 모델링을 해 나갈지 고민하는 과정이다.
앙상블 기법은 당연히 해야 하는 과정이지만, 그 전에 어떤 모델들이 해당 컴피티션에 좋고 나쁨의 요소를 파악하는 부분이 필요하기에 그 과정에서는 많은 지식의 탐구와 Discussion이 필요하다.
일단 필자가 본 글을 쓰고 있는 지금은 어느 정도 컴피티션이 진행된 상태이다. 그렇기에 kaggle에서 starter pack이라고 하는, 일종의 게임에서 고인물들이 뉴비들을 위해 선물해주는 것 같이 데이터 분석된 모델의 노트북을 배포해준다.
일단 피처 엔지니어링이 먼저 진행이 되었으므로 해당 모델들 중에서 가장 fit한 모델을 찾는 과정을 진행할 것이나, 일단 지금 필자에게 중요한 건 가장 좋은 모델을 따라하는 것이 아닌, 다양한 모델의 종류를 어느 때, 어느 지점에 써야 하는지를 찾는 것이다.
그래야만 해당 컴피티션이 끝나도 그 경험을 활용해서 다른 컴피티션에 적용할 수 있다.
그렇기에 본 글에서는 일단 본 대회에서 가장 첫 결과가 나왔던 Catboost모델을 활용해서 베이스라인 모델을 만들어볼 것이다.
CatBoost?
Catboost 모델은 결정 트리 타입의 모델로, XGBoost와 LightGBM과 같은 궤를 가지고 있다.
그러나 조금 더 늦게 출시되었고, Catboost의 장점은 말 그대로 Categorical Boost 이기 때문에, 범주형 데이터를 더 잘 분리한다는 특징이 있다.
Catboost는 범주형 데이터 면에서 중복을 자체적으로 처리하고, 인코딩 작업을 하지 않아도 데이터를 처리한다는 아주 좋은 장점이 있다. 정확히는 Ordered target Encoding을 진행하는데, 과거의 데이터를 이용해 현재의 데이터를 인코딩한다. 그리고 가장 첫 줄의 데이터에 대해서는 Laplace Smoothing이라는 방법을 이용해 처리한다.
Ordered target Encoding에서는 과적합 방지를 위해 Random Permutation을 이용해 데이터를 일단 섞어주고, 그를 이용해 예측값을 하나씩 계산해나가면서 계산된 예측값을 뒤쪽 데이터들에 반영하여 연이어 계산한다.
이렇게 하면, 타겟 인코딩에서의 예시같이 각 특성에 같은 값이 부여되어 있기 떄문에 생기는 데이터 누수 문제를 해결할 수 있다. 예를 들어, 16,17,18이 있다면 타겟 인코딩은 그냥 평균으로 전부 17,17,17이지만, 만약 Ordered target Encoding을 적용하면 두번째는 (16+17)/2 = 16.5이고, 세번째에서야 (16+17+18)/3=17이 되어 16,16.5,17의 순서대로 Order로 정렬된 인코딩을 가지게 된다.
이렇게 되면 모델링을 할 때 같은 범주 안에서도 순서에 따라 영향력의 구분이 된다. 따라서 해당 모델에서 어떤 범주형 값이 많은 영향을 끼치는 지 잘 알 수 있게 되는 것이다.
참고한 모델
https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/discussion/467576
해당 모델에서는 명확하게 CatBoost를 사용할 떄 어떤 피처 엔지니어링을 했는지 알려주고 있다.
필자도 이를 참고해서 피처 엔지니어링을 진행했고, 확실히 eeg_id 별로 묶은 17089개의 데이터만으로 K-fold를 5로 하여 진행한다고 한다.
여기서 더 중요한 건, 이 모델에서는 parquet 파일도 활용했다는 것이다.
parquet 파일은 하둡에서 쓰이는 컬럼 저장방식인데, 필자는 어떻게 쓰이는지 모른다. 그래서 이번에 한 번 공부해보기로 했다.
parquet 파일 불러오기
처음에 경진대회에 대해서 소개할 때, 필자가 parquet 파일들에 대해 해석한 설명을 읽어오면 다음과 같다.
- train_eegs/ 및 test_eegs/: 이 폴더는 실제 EEG 데이터 파일을 포함합니다.
- train_eegs/는 훈련 세트의 겹치는 샘플 데이터를 포함합니다. train.csv를 사용하여 특정 주석이 있는 하위 집합을 선택합니다. 열 이름은 개별 전극 위치를 나타냅니다.
- test_eegs/는 각 테스트 샘플에 대한 50초 길이의 EEG 데이터 파일을 포함합니다.
- train_spectrograms/ 및 test_spectrograms/: 이 폴더는 EEG 데이터로부터 조립된 스펙트로그램을 포함합니다.
- train_spectrograms/는 주석이 있는 훈련 세트의 스펙트로그램을 포함합니다. train.csv를 사용하여 특정 하위 집합을 선택합니다. 열 이름은 주파수와 기록 영역(LL, RL, LP, RP)을 나타냅니다.
- test_spectrograms/는 각 테스트 샘플에 대한 10분 길이의 EEG 데이터로부터 조립된 스펙트로그램을 포함합니다.
그리고 해당 데이터 셋들의 크기는 기본제공되는 overview에서 확인할 수 있다.
train_eegs - This preview shows 30 out of 17300 items.
train_spectrograms - This preview shows 30 out of 11138 items.
테스트에 대해서는 데이터 누수가 일어나므로 당연히 고려하지 않았다.
결국, 이 모델에 대해서 이해하려면 이 eeg 파일과 spectrogram 파일에 대해 이해해야 한다.
처음에 이 글을 쓴 사람은 eeg를 활용하지 않았다. 테스트 데이터셋이 eeg가 겹치지 않는다고 결론이 이미 났기 때문이다. 따라서 모델링에 더 활용한다면 Spectrogram이기에 처음에는 Spectrogram을 선택한 것 같다.
그러다가 eeg 파일도 포함해서 더 성능을 향상시켰다. 그 후에 리더보드에서 성능이 더 올라간 걸 보니, 확실히 eeg 셋 또한 영향을 주기는 한 모양.
이 사실은 중요한 것 같다.
그런데 문제는, 이 Parquet 파일을 python에서 불러오는 게 상당히 느리다.그래서 이 스타터팩을 만든 사람은 정말 똑똑하게도 모든 spectrogram을 npy 파일로 만들어서 빠르게 불러올 수 있게 만들어 놓았다. 11분이 걸리던 걸 1분 20초만에 로딩할 수 있게 만들어놓았으니 감사를 표하지 않을 수 없다.
구체적으로 코드를 비교하자면 다음과 같다.
%%time
# READ ALL SPECTROGRAMS
PATH = '/kaggle/input/hms-harmful-brain-activity-classification/train_spectrograms/'
files = os.listdir(PATH)
print(f'There are {len(files)} spectrogram parquets')
if READ_SPEC_FILES:
spectrograms = {}
for i,f in enumerate(files):
if i%100==0: print(i,', ',end='')
tmp = pd.read_parquet(f'{PATH}{f}')
name = int(f.split('.')[0])
spectrograms[name] = tmp.iloc[:,1:].values
else:
spectrograms = np.load('/kaggle/input/brain-spectrograms/specs.npy',allow_pickle=True).item()
원래는 READ_SPEC_FILES의 IF문을 이용해 전부 읽어들여야 하나,specs.npy로 인해서 그냥 npy를 읽어들이면 된다.
그렇게 읽은 파일은 다음과 같이 딕셔너리 형태로 출력된다.
spectrograms
{319287046: array([[16.72, 24.51, 36.19, ..., 0.1 , 0.34, 0.35],
[16.65, 19.84, 18.75, ..., 0.13, 0.46, 0.42],
[ 8.47, 8.57, 11.68, ..., 0.12, 0.4 , 0.38],
...,
[43.94, 34.39, 36.66, ..., 0.31, 0.63, 0.52],
[53.59, 73.56, 64.55, ..., 0.23, 0.47, 0.57],
[54.76, 80.39, 76. , ..., 0.49, 0.89, 0.81]], dtype=float32),
440834944: array([[0.26, 0.55, 1.74, ..., 0.05, 0.4 , 0.38],
[0.2 , 0.51, 1.34, ..., 0.06, 0.2 , 0.29],
[0.4 , 0.47, 0.53, ..., 0.2 , 0.27, 0.31],
...,
[0.33, 0.42, 1.32, ..., 0.03, 0.07, 0.1 ],
[0.38, 0.49, 0.98, ..., 0.03, 0.04, 0.09],
[0.69, 1.14, 2.21, ..., 0.03, 0.05, 0.05]], dtype=float32),
1009815187: array([[18.05, 23.65, 20.4 , ..., 0.19, 0.21, 0.2 ],
[22.27, 42.02, 44.48, ..., 0.26, 0.24, 0.12],
[19.6 , 30.57, 34.24, ..., 0.2 , 0.25, 0.24],
...,
[15.5 , 15.79, 15.37, ..., 0.1 , 0.07, 0.06],
[27.01, 27.86, 28.36, ..., 0.09, 0.1 , 0.08],
[17.36, 20.93, 18.36, ..., 0.07, 0.08, 0.07]], dtype=float32),
1649311149: array([[2.3087e+02, 2.4559e+02, 2.6506e+02, ..., 3.7000e-01, 4.2000e-01,
3.4000e-01],
[1.8569e+02, 1.7014e+02, 1.9030e+02, ..., 2.4000e-01, 2.1000e-01,
1.4000e-01],
[3.2320e+02, 3.5297e+02, 2.7522e+02, ..., 5.0000e-01, 3.1000e-01,
1.9000e-01],
...,
[5.4100e+00, 5.5500e+00, 6.2900e+00, ..., 5.0000e-02, 5.0000e-02,
1.2000e-01],
[1.4820e+01, 1.4390e+01, 1.0710e+01, ..., 9.0000e-02, 7.0000e-02,
7.0000e-02],
[1.6600e+01, 2.0110e+01, 2.2070e+01, ..., 8.0000e-02, 6.0000e-02,
6.0000e-02]], dtype=float32),
딕셔너리 형태로 있는 리스트에 각 환자들의 spectorgrams 마다 값이 분류되어 있다.
len(spectrograms[319287046][0]) #첫번째 spectrogram의 첫번째 리스트의 길이
>400
len(spectrograms[319287046]) #첫번째 spectrogram의 리스트 개수
>752
여기서 문제는, 400개의 값이 나오는데 이 400개의 값들이 뭔지를 모르겠다는 것.
이를 이해하지 못하면, 데이터를 쓰지 못한다. 더불어서 왜 써야하는지도 이해할 수 없을 것이다.
그래서 설명을 보기로 했다.
Spectrogram은 파형의 중첩
https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/discussion/467400
Spectrogram이 뭔지는 어렴풋이 알고 있었다. Spectrogram은 파형의 중첩으로서, 여러 개의 사인 파를 중첩하면 다른 파형이 탄생한다는 퓨리에 변환의 공식을 적용해서 탄생한 일종의 도구이다. 이 자체의 개념은 전자공학에서도 쓰이므로 전자공학에서 배운 지식으로 어렴풋이 알고 있었다.
이 모델에서 쓰는 spectrogram에 대해 제공자는 이렇게 설명하고 있다.
Next we need to engineer features for our CatBoost model. In version 1 notebook, we just take the mean (over time) of each of the 400 spectrogram frequencies (using middle 10 minutes). This produces 400 features (per each unique eeg id). We can improve CV and LB score by engineering new features (and/or tuning CatBoost).
한마디로, 400 spectorgram 주파수를 그냥 mean을 취한다는 걸 보면 이 값들은 전부 주파수이다. 즉, 환자에게서 10분간 취한 400개의 주파수인 것 같다.
결국 데이터를 더 찾아보고, Spectrogram에 대해 요약하자면 다음과 같다.
- Spectrogram은 소리나 파동을 시각화하여 파악하기 위한 도구이다. 파형과 스펙트럼의 특징이 조합되어 있다.
- x축은 시간 축, y축은 주파수를 의미한다. 시간축과 주파수 축의 변화에 따라 진폭의 차이를 나타낸다.
한마디로 저 400개의 값은 주파수를 의미한다. 즉, middle 10 minutes라는 걸 보면 10분 동안의 값이 맞다.
그렇기에 피처 엔지니어링을 할 때, 다음과 같은 코드를 써서 각 주파수들의 mean을 뽑아낼 수 있다.
FEATURES = [f'{c}_mean_10m' for c in SPEC_COLS]
이는 다른 요소들에도 마찬가지.
일단 처음에 feature을 만들어야 하므로, 첫 parquet에 대해서는 그대로 가져와준다.
%time
# ENGINEER FEATURES
import warnings
warnings.filterwarnings('ignore')
# FEATURE NAMES
SPEC_COLS = pd.read_parquet(f'{PATH}1000086677.parquet').columns[1:]
FEATURES = [f'{c}_mean_10m' for c in SPEC_COLS]
FEATURES += [f'{c}_min_10m' for c in SPEC_COLS]
FEATURES += [f'{c}_mean_20s' for c in SPEC_COLS]
FEATURES += [f'{c}_min_20s' for c in SPEC_COLS]
FEATURES += [f'eeg_mean_f{x}_10s' for x in range(512)]
FEATURES += [f'eeg_min_f{x}_10s' for x in range(512)]
FEATURES += [f'eeg_max_f{x}_10s' for x in range(512)]
FEATURES += [f'eeg_std_f{x}_10s' for x in range(512)]
print(f'We are creating {len(FEATURES)} features for {len(train)} rows... ',end='')
참고로 eeg에 대한 코드도 같이 포함되어 있다.
이쪽은 무조건 512로 고정되어 있으므로 512로 해준 것.
그 후에는, spectrograms에 대해 다음과 같은 코드를 수행한다.
data = np.zeros((len(train),len(FEATURES)))
for k in range(len(train)):
if k%100==0: print(k,', ',end='')
row = train.iloc[k]
r = int( (row['min'] + row['max'])//4 )
일단 np.zreos로 행을 만들어주는데, train의 길이인 17089와, 위에서 만든 FEATURE의 길이인 3648을 만들어준다.
이렇게 만들어주는 이유는 각 train에 대해서 FEATURE를 탐색한 데이터를 만들 것이기 때문이다.
그런 뒤에 row=train.iloc[k]를 불러오는데, 이는 train에서 다음과 같은 예시의 모든 행을 불러온다.
eeg_id 568657
spec_id 789577333
min 0.0
max 16.0
patient_id 20654
seizure_vote 0.0
lpd_vote 0.0
gpd_vote 0.25
lrda_vote 0.0
grda_vote 0.166667
other_vote 0.583333
target Other
Name: 0, dtype: object
그리고 row 행에서 min과 max를 더해 4로 나눈 몫을 구한다.
4로 나누는 이유는, 해당 스펙트로그램에서 min과 max는 초를 의미하는데, 그 사이의 값에 대한 데이터를 구해야 하기 떄문이다.
Our features are simple. A 10 minute spectrogram has 300 readings (taking every 2 seconds). Readings are taken for 100 frequencies from 4 quadrants of the brain. We take the average over time of each of these 400 time series. This produces 400 features to be used with each eeg_id.
10minute spectrogram은 300개의 reading(2초마다 반복되는) data를 가지고 있고, 이는 100 주파수를 4개의 부위에서 가져온 것이다. 400개의 타임 시리즈의 mean을 구해야 하므로 4로 나눠야 하나의 부위에 대한 계산을 할 수 있는 것 같다.
이것이 eeg_id마다 400 feature를 만들 수 있게 하는 것. 이는 만들어진 데이터의 특징이므로 숙지할 필요가 있는 것 같다.
즉, 결국에는 최종적으로 다음과 같은 코드를 수행한다.
# 10 MINUTE WINDOW FEATURES (MEANS and MINS)
x = np.nanmean(spectrograms[row.spec_id][r:r+300,:],axis=0)
data[k,:400] = x
x = np.nanmin(spectrograms[row.spec_id][r:r+300,:],axis=0)
data[k,400:800] = x
# 20 SECOND WINDOW FEATURES (MEANS and MINS)
x = np.nanmean(spectrograms[row.spec_id][r+145:r+155,:],axis=0)
data[k,800:1200] = x
x = np.nanmin(spectrograms[row.spec_id][r+145:r+155,:],axis=0)
data[k,1200:1600] = x
1번의 train eeg_id에, 400까지는 10분 단위의 mean을, 400~800까지는 min을 형성한다.
그리고 800~1200까지는 20초의 mean을, 그리고 1200~1600까지는 20초의 min을 형성한다.
개인적으로 이는 max까지도 확장할 수 있는 부분이기에, 추가적인 피처 엔지니어링을 할 수도 있을 것 같다.
마찬가지로 eeg spectrograms에 대해서도 512마다 하나의 부위를 차지하므로, 4번씩 수행해준다.
# RESHAPE EEG SPECTROGRAMS 128x256x4 => 512x256
eeg_spec = np.zeros((512,256),dtype='float32')
xx = all_eegs[row.eeg_id]
for j in range(4): eeg_spec[128*j:128*(j+1),] = xx[:,:,j]
# 10 SECOND WINDOW FROM EEG SPECTROGRAMS
x = np.nanmean(eeg_spec.T[100:-100,:],axis=0)
data[k,1600:2112] = x
x = np.nanmin(eeg_spec.T[100:-100,:],axis=0)
data[k,2112:2624] = x
x = np.nanmax(eeg_spec.T[100:-100,:],axis=0)
data[k,2624:3136] = x
x = np.nanstd(eeg_spec.T[100:-100,:],axis=0)
data[k,3136:3648] = x
train[FEATURES] = data
print(); print('New train shape:',train.shape)
New train shape: (17089, 3660)
그렇게 완성된 train shape은 17089,3660.
그래서 결론적으론 이 엔지니어링 된 데이터로 베이스라인 모델을 형성한다.
다음 이 시간에는 본격적으로 모델링을 해보도록 하자.