티스토리 뷰

반응형

https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/overview

 

이 글을 쓰고 있는 2024년 2월 19일 현재, 캐글에서 진행하고 있는 컴피티션이다.

필자는 본 경진대회를 진행하면서, 실제로 배운 데이터 분석 요소들을 적용해보려고 한다.

 

글로 써질 것들은 모델을 학습하고, 만들기 까지의 과정이다.

 

오늘 시간에는 베이스라인 모델링을 진행한다.

 

Group K-Fold 적용하기

 

CatBoost를 학습하기 위해서 기본적인 CatBoost 환경을 사용한다.

이때 K_Fold를 같이 사용하기 위해서, TARGET으로 각 타겟을 라벨링 인코딩을 해줄 것이다.

 

또한 GPU로 지정해줌으로서, 더 빠른 학습 속도를 가지게 만든다.

import catboost as cat
from catboost import CatBoostClassifier, Pool
print('CatBoost version',cat.__version__)

 

CatBoost를 불러오는 코드.

 

from sklearn.model_selection import KFold, GroupKFold

all_oof = []
all_true = []
TARS = {'Seizure':0, 'LPD':1, 'GPD':2, 'LRDA':3, 'GRDA':4, 'Other':5} 

gkf = GroupKFold(n_splits=5)

 

이건 Kfold를 불러오는 코드이다.

여기서 다중분류를 위해 타겟을 명목별로 지정해주고, 교차검증을 위해 GroupKFold를 사용한다.

 

GroupKFold는 일반적인 Kfold가 그냥 나눠서 훈련하는 것이라면, GroupKFold는 그룹별로는 겹치지 않게 해준다.

현재 훈련 데이터가 그룹화되어 있으므로, GroupKFold를 사용하는 것.

 

import numpy as np
from sklearn.model_selection import KFold

X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 8.8, 9, 10]
kf = KFold(n_splits=3)
for train, test in kf.split(X):
    print("%s %s" % (train, test))
    
[4 5 6 7 8 9] [0 1 2 3]
[0 1 2 3 7 8 9] [4 5 6]
[0 1 2 3 4 5 6] [7 8 9]

일반 Kfold의 결과

from sklearn.model_selection import GroupKFold

X = [0.1, 0.2, 2.2, 2.4, 2.3, 4.55, 5.8, 8.8, 9, 10]
y = ["a", "b", "b", "b", "c", "c", "c", "d", "d", "d"]
groups = [1, 1, 1, 2, 2, 2, 3, 3, 3, 3]

gkf = GroupKFold(n_splits=3)
for train, test in gkf.split(X, y, groups=groups):
    print("%s %s" % (train, test))

[0 1 2 3 4 5] [6 7 8 9]
[0 1 2 6 7 8 9] [3 4 5]
[3 4 5 6 7 8 9] [0 1 2]

GroupKFold의 결과

 

잘 보면 알겠지만, 처음 abb를 그룹으로 잡아놓았고(0,1,2) 그 다음 b,c,c를 그룹으로, 다음 6789를 그룹으로 잡아놓았기 때문에 훈련 세트와 테스트 세트가 겹치는 것 없이 나뉘어졌다.

 

이렇게 일반적인 KFold와는 다르게 그룹을 지정해서 훈련 세트와 테스트 세트로 나눠주는 검증이 가능하다.

 

훈련 세트와 테스트 세트에 다른 그룹의 사진이 들어가도록 해야 더 검증이 정확할 것이므로 이렇게 나눠주는 것이다.

 

아무튼 그 셋팅에 이어서 이제 드디어 catboost를 지정해준다.

 

Catboost 모델링

for i, (train_index, valid_index) in enumerate(gkf.split(train, train.target, train.patient_id)):   
    
    print('#'*25)
    print(f'### Fold {i+1}')
    print(f'### train size {len(train_index)}, valid size {len(valid_index)}')
    print('#'*25)
    
    model = CatBoostClassifier(task_type='GPU',
                               loss_function='MultiClass')
    
    train_pool = Pool(
        data = train.loc[train_index,FEATURES],
        label = train.loc[train_index,'target'].map(TARS),
    )
    
    valid_pool = Pool(
        data = train.loc[valid_index,FEATURES],
        label = train.loc[valid_index,'target'].map(TARS),
    )
    
    model.fit(train_pool,
             verbose=100,
             eval_set=valid_pool,
             )
    model.save_model(f'CAT_v{VER}_f{i}.cat')
    
    oof = model.predict_proba(valid_pool)
    all_oof.append(oof)
    all_true.append(train.loc[valid_index, TARGETS].values)
    
    del train_pool, valid_pool, oof #model
    gc.collect()
    
    #break
    
all_oof = np.concatenate(all_oof)
all_true = np.concatenate(all_true)

 

코드가 길어서 각 부분을 나눠서 설명하면 다음과 같다.

for i, (train_index, valid_index) in enumerate(gkf.split(train, train.target, train.patient_id)):   
    
    print('#'*25)
    print(f'### Fold {i+1}')
    print(f'### train size {len(train_index)}, valid size {len(valid_index)}')
    print('#'*25)

 

앞에서 구한 gkf, 즉 groupKfold를 나누는데, 훈련은 train, 타겟 y는 train.target, 그리고 지정해주는 그룹은 train.patient_id를 중복되지 않도록 넣어준다는 의미이다.

 

원래는 split(X,y,groups)로 쓴다.

 

코드만 객관적으로 보면 와닿지 않으므로 실제 출력해보면 다음과 같다.

for i, (train_index, valid_index) in enumerate(gkf.split(train, train.target, train.patient_id)):   
    
    print(i)
    print(train_index)
    print(valid_index)

0
[    0     1     2 ... 17086 17087 17088]
[    7     9    15 ... 17068 17069 17083]
1
[    0     1     2 ... 17085 17087 17088]
[    5    17    18 ... 17082 17084 17086]
2
[    0     2     5 ... 17085 17086 17088]
[    1     3     4 ... 17066 17078 17087]
3
[    0     1     2 ... 17086 17087 17088]
[    6    14    16 ... 17074 17076 17085]
4
[    1     3     4 ... 17085 17086 17087]
[    0     2    12 ... 17079 17080 17088]

i는 gkf의 그룹 번호, 그리고 train_index는 train할 인덱스들의 번호, 그리고 valid index는 검증할 인덱스들의 번호다.

 

잘 생각해보면, 이 train_index와 valid index에는 같은 환자가 없을 것이라 확실하게 말할 수 있다. 왜? gkf로 환자 id 별로 그룹을 나누었으니까!

 

그렇기에 훨씬 더 검증이 잘 될 것이다. 훈련 데이터와 테스트 데이터에 같은 환자가 없기 때문이다.

 

아무튼 이어서 두 번쨰 코드를 말하자면 다음과 같다.

 model = CatBoostClassifier(task_type='GPU',
                               loss_function='MultiClass')

 

드디어 catboost가 나왔다.

catboost를 지정해주고, GPU를 사용한다. 그리고 여기서는 다중 분류이므로 multiclass로 손실 함수를 지정해준다.

multiclass의 설명은 다음과 같다.

Use object/group weights to calculate metrics if the specified value is true and set all weights to 1 regardless of the input data if the specified value is false.

 

객체/그룹 가중치를 사용하여 지정된 값이 참이면 메트릭을 계산하고 지정된 값이 거짓이면 입력 데이터에 관계없이 모든 가중치를 1로 설정합니다.

 

위의 함수는 얼핏 보면 크로스 엔트로피 함수와 닮은 부분이 있다. 다만 로그 안의 값을 확률적으로 구하기 위해 소프트맥스 함수에 대입하여 분류할 클래스들의 확률을 구해주는 것이다.

 

한마디로 위의 값에서 해당 분류가 참이라고 판단되면 위 함수를 가정하는 거고, 아니라면 모든 가중치를 1로 설정하여 완전히 손실이 나게 만든다.

 

다른 손실함수에는MultiClassOneVsAll이 있는데, 이쪽은 완전히 크로스 엔트로피 함수를 가져온 것.

 

누가 봐도 크로스 엔트로피와 같은 형태이다.

 

다중 분류 Optimization에는 위의 두개만 쓰이는 것 같으니, 이 두개만 숙지해둬도 CatBoost로 다중분류를 하는 데는 지장이 없을 것이다.

 

Train pool과 Vaild Pool

   train_pool = Pool(
        data = train.loc[train_index,FEATURES],
        label = train.loc[train_index,'target'].map(TARS),
    )
    
    valid_pool = Pool(
        data = train.loc[valid_index,FEATURES],
        label = train.loc[valid_index,'target'].map(TARS),
    )

 

코드를 이어서 설명하자면 다음과 같이 Pool 함수를 써서 묶어준다.

Pool 함수는 병렬처리를 진행하기 위한 클래스이다. multiprocessing 모듈에서, map을 통해 dat와 label을 지정해준다.

 

data는 당연히도 train_index와 FEATURE를 포함하는 행이다.

그리고 라벨(타깃값)은 train.loc에서 train_index로 행을 받고, 그 타겟을 지정한 시리즈에서 TARS를 통해 각각 값을 0~5로 지정해준다. 

 

마찬가지로 검증 데이터에 대해서도 진행해준다.

 

 model.fit(train_pool,
             verbose=100,
             eval_set=valid_pool,
             )
    model.save_model(f'CAT_v{VER}_f{i}.cat')
    
    oof = model.predict_proba(valid_pool)
    all_oof.append(oof)
    all_true.append(train.loc[valid_index, TARGETS].values)
    
    del train_pool, valid_pool, oof #model
    gc.collect()
    
    #break

그리고 나면 드디어 fit을 할 수 있다.

 

oof는 검증셋의 proba로 나오게 되고, 이렇게 만들어진 proba를 all_oof라고 하는 것에 차곡차곡 쌓아준다.

그리고 all_true에는 실제 검증셋과 타겟의 값을 쌓아준다.

all_oof = np.concatenate(all_oof)
all_true = np.concatenate(all_true)

마지막으로 리스트화 된 all_oof를 concatenate를 통해 합쳐주면, 최종적인 oof(out of fold)를 구할 수 있다.

이렇게 oof가 나오는 이유는 k-fold를 사용했기 떄문이고, 그래서 합쳐주는 것이다.

 

그렇게 나온 all_oof와 true를 출력해주면 다음과 같다.

all_oof[0:5]

array([[8.54164864e-02, 1.38399229e-01, 2.26718950e-01, 5.17475961e-02,
        1.50044004e-01, 3.47673734e-01],
       [2.67589262e-02, 8.78184895e-03, 5.07318140e-04, 9.37429230e-02,
        1.70750480e-01, 6.99458504e-01],
       [8.23822130e-01, 2.54214770e-02, 1.76694279e-02, 1.87613274e-03,
        7.96039570e-03, 1.23250436e-01],
       [5.20692158e-02, 7.67570880e-03, 9.76535628e-05, 3.91950503e-02,
        6.43161155e-03, 8.94530760e-01],
       [1.39063354e-01, 2.07491598e-01, 4.08163451e-03, 3.15441050e-01,
        2.87310774e-02, 3.05191286e-01]])
        
all_true[0:5]

array([[0., 0., 0., 0., 0., 1.],
       [0., 0., 0., 0., 0., 1.],
       [1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1.],
       [0., 0., 0., 1., 0., 0.]])

즉, all_oof는 실제적인 검증 확률이고, all_true는 어디가 true인지에 대한 원핫 인코딩과도 같다. 

 

이런 array가 17089개 있다고 보면 된다.

 

그 후로는 각 모델에서 가장 중요한 25개의 특징을 뽑아내는 코드를 작성한다.

여기서는 .feature_importances_ 메소드를 쓰면 된다.

TOP = 25

feature_importance = model.feature_importances_
sorted_idx = np.argsort(feature_importance)
fig = plt.figure(figsize=(10, 8))
plt.barh(np.arange(len(sorted_idx))[-TOP:], feature_importance[sorted_idx][-TOP:], align='center')
plt.yticks(np.arange(len(sorted_idx))[-TOP:], np.array(FEATURES)[sorted_idx][-TOP:])
plt.title(f'Feature Importance - Top {TOP}')
plt.show()

 

argsort는 어레이를 정렬하는 것이고, barh는 sorted_idx된 것에서 25개까지 가장 높은 것들에 대한 수평 막대 그래프를 그려준다.

np.arange(len(sorted_idx))[-TOP:]

>array([3623, 3624, 3625, 3626, 3627, 3628, 3629, 3630, 3631, 3632, 3633,
       3634, 3635, 3636, 3637, 3638, 3639, 3640, 3641, 3642, 3643, 3644,
       3645, 3646, 3647])
       
feature_importance[sorted_idx][-TOP:]      

>array([0.36424973, 0.37880933, 0.38001791, 0.39010772, 0.39643271,
       0.39730672, 0.40102671, 0.42070425, 0.43316715, 0.44875696,
       0.4924778 , 0.49596785, 0.50387737, 0.50502569, 0.51085957,
       0.53758586, 0.54737969, 0.5524732 , 0.57873736, 0.60106374,
       0.62980888, 0.66452143, 0.82455308, 0.90555407, 1.00502717])

 

arrange 함수는 인덱스를 표시하기 위해 sorted_idx의 길이로 설정하고, 해당 인덱스를 표시해준다. 

해당 값을 y로 준다.

 

yticks도 역시 마찬가지로 설정.

 

결과적으로 보면, 10초동안의 eeg_mean이 가장 높은 feature 결정력을 차지하고, 그 다음에 뒤따르는 게 특정 환자의 LP 10분 피처다.

 

여기서 LL LP RR RP라는 용어가 나오는데, 간단하게 어느 부위별 지점인지를 나타낸 것이다.

SPEC_COLS = pd.read_parquet(f'{PATH}1000086677.parquet').columns[1:]

해당 코드에서 FEATURE를 불러올 떄 열의 이름으로 지정해준 것.

 

전체적으로 봤을 때, 확실히 eeg와 spectrogam 파일이 골고루 분포되어 있는 걸 알 수 있다.

 

아무튼 해당 모델의 cv를 kl-div를 통해 구해보면 다음과 같다.

import sys
sys.path.append('/kaggle/input/kaggle-kl-div')
from kaggle_kl_div import score

oof = pd.DataFrame(all_oof.copy())
oof['id'] = np.arange(len(oof))

true = pd.DataFrame(all_true.copy())
true['id'] = np.arange(len(true))

cv = score(solution=true, submission=oof, row_id_column_name='id')
print('CV Score KL-Div for CatBoost =',cv)
CV Score KL-Div for CatBoost = 0.7551946998936143

 

0.755정도다.

 

1.6:1이라고 했으니, 리더보드 스코어는 대략 0.47정도 될 것이라고 예상해볼 수 있다.

 

정말인지 확인하기 위해서 이제 submmision을 만들어볼 차례.

 

그런데, 여기서는 submmision을 만드는 것조차 복잡하다.

 

Submmision을 위한 Test data 변환하기

각각의 LL,LP,RP,RR마다 해당하는 공식이 있다. 그렇기에 해당 모델의 정확한 예측도를 만들려면 LL,LP,RP,RR을 조합해서 새로운 확률을 만들어내야 한다.

 

이 과정을 test 데이터셋에 있는 단 한개의 parquet을 위해서 만들어준다.

pywt에서 wavedec을 통해 spectrogram을 denosie 하고, eeg의 형태로 만든다.

import pywt, librosa

USE_WAVELET = None 

NAMES = ['LL','LP','RP','RR']

FEATS = [['Fp1','F7','T3','T5','O1'],
         ['Fp1','F3','C3','P3','O1'],
         ['Fp2','F8','T4','T6','O2'],
         ['Fp2','F4','C4','P4','O2']]

# DENOISE FUNCTION
def maddest(d, axis=None):
    return np.mean(np.absolute(d - np.mean(d, axis)), axis)

def denoise(x, wavelet='haar', level=1):    
    coeff = pywt.wavedec(x, wavelet, mode="per")
    sigma = (1/0.6745) * maddest(coeff[-level])

    uthresh = sigma * np.sqrt(2*np.log(len(x)))
    coeff[1:] = (pywt.threshold(i, value=uthresh, mode='hard') for i in coeff[1:])

    ret=pywt.waverec(coeff, wavelet, mode='per')
    
    return ret

def spectrogram_from_eeg(parquet_path, display=False):
    
    # LOAD MIDDLE 50 SECONDS OF EEG SERIES
    eeg = pd.read_parquet(parquet_path)
    middle = (len(eeg)-10_000)//2
    eeg = eeg.iloc[middle:middle+10_000]
    
    # VARIABLE TO HOLD SPECTROGRAM
    img = np.zeros((128,256,4),dtype='float32')
    
    if display: plt.figure(figsize=(10,7))
    signals = []
    for k in range(4):
        COLS = FEATS[k]
        
        for kk in range(4):
        
            # COMPUTE PAIR DIFFERENCES
            x = eeg[COLS[kk]].values - eeg[COLS[kk+1]].values

            # FILL NANS
            m = np.nanmean(x)
            if np.isnan(x).mean()<1: x = np.nan_to_num(x,nan=m)
            else: x[:] = 0

            # DENOISE
            if USE_WAVELET:
                x = denoise(x, wavelet=USE_WAVELET)
            signals.append(x)

            # RAW SPECTROGRAM
            mel_spec = librosa.feature.melspectrogram(y=x, sr=200, hop_length=len(x)//256, 
                  n_fft=1024, n_mels=128, fmin=0, fmax=20, win_length=128)

            # LOG TRANSFORM
            width = (mel_spec.shape[1]//32)*32
            mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max).astype(np.float32)[:,:width]

            # STANDARDIZE TO -1 TO 1
            mel_spec_db = (mel_spec_db+40)/40 
            img[:,:,k] += mel_spec_db
                
        # AVERAGE THE 4 MONTAGE DIFFERENCES
        img[:,:,k] /= 4.0
        
        if display:
            plt.subplot(2,2,k+1)
            plt.imshow(img[:,:,k],aspect='auto',origin='lower')
            plt.title(f'EEG {eeg_id} - Spectrogram {NAMES[k]}')
            
    if display: 
        plt.show()
        plt.figure(figsize=(10,5))
        offset = 0
        for k in range(4):
            if k>0: offset -= signals[3-k].min()
            plt.plot(range(10_000),signals[k]+offset,label=NAMES[3-k])
            offset += signals[3-k].max()
        plt.legend()
        plt.title(f'EEG {eeg_id} Signals')
        plt.show()
        print(); print('#'*25); print()
        
    return img

코드가 상당히 길다.

그러나 plt를 안보아도 된다면 다음과 같은 코드로 줄어든다.

 

import pywt, librosa

USE_WAVELET = None 

NAMES = ['LL','LP','RP','RR']

FEATS = [['Fp1','F7','T3','T5','O1'],
         ['Fp1','F3','C3','P3','O1'],
         ['Fp2','F8','T4','T6','O2'],
         ['Fp2','F4','C4','P4','O2']]

# DENOISE FUNCTION
def maddest(d, axis=None):
    return np.mean(np.absolute(d - np.mean(d, axis)), axis)

def denoise(x, wavelet='haar', level=1):    
    coeff = pywt.wavedec(x, wavelet, mode="per")
    sigma = (1/0.6745) * maddest(coeff[-level])

    uthresh = sigma * np.sqrt(2*np.log(len(x)))
    coeff[1:] = (pywt.threshold(i, value=uthresh, mode='hard') for i in coeff[1:])

    ret=pywt.waverec(coeff, wavelet, mode='per')
    
    return ret

def spectrogram_from_eeg(parquet_path, display=False):
    
    # LOAD MIDDLE 50 SECONDS OF EEG SERIES
    eeg = pd.read_parquet(parquet_path)
    middle = (len(eeg)-10_000)//2
    eeg = eeg.iloc[middle:middle+10_000]
    
    # VARIABLE TO HOLD SPECTROGRAM
    img = np.zeros((128,256,4),dtype='float32')
    
    if display: plt.figure(figsize=(10,7))
    signals = []
    for k in range(4):
        COLS = FEATS[k]
        
        for kk in range(4):
        
            # COMPUTE PAIR DIFFERENCES
            x = eeg[COLS[kk]].values - eeg[COLS[kk+1]].values

            # FILL NANS
            m = np.nanmean(x)
            if np.isnan(x).mean()<1: x = np.nan_to_num(x,nan=m)
            else: x[:] = 0

            # DENOISE
            if USE_WAVELET:
                x = denoise(x, wavelet=USE_WAVELET)
            signals.append(x)

            # RAW SPECTROGRAM
            mel_spec = librosa.feature.melspectrogram(y=x, sr=200, hop_length=len(x)//256, 
                  n_fft=1024, n_mels=128, fmin=0, fmax=20, win_length=128)

            # LOG TRANSFORM
            width = (mel_spec.shape[1]//32)*32
            mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max).astype(np.float32)[:,:width]

            # STANDARDIZE TO -1 TO 1
            mel_spec_db = (mel_spec_db+40)/40 
            img[:,:,k] += mel_spec_db
                
        # AVERAGE THE 4 MONTAGE DIFFERENCES
        img[:,:,k] /= 4.0

maddset 함수는 denoise 함수를 사용할 때 쓰는 함수이다.

 

그 아래는 전부 파동을 벡터 값으로 바꾸기 위한 함수들이다. 그렇기에 깊게 들어가지는 않겠다. 아마 LLM에 대해 더 잘 배우게 되면 이해할 수 있을지도 모른다. 필자가 이해한 바에 따르면 그냥 각 feature(LL,LP,RP,RR)에 대해 denoising을 하고, 더해주는 것이라는 함수라는 것 같다.

 

나중에 다른 모델에서 테스팅을 할 떄도 이 함수를 그대로 가져다 써야할 듯 싶다. 이 분 자체도 다른 분에게서 아이디어를 얻어서 가져다 쓴 것이다.

 

https://www.kaggle.com/code/cdeotte/how-to-make-spectrogram-from-eeg

 

무튼 그렇게 만들고 나면, 앞서 해줬던 것처럼 eeg에 대해서도 array가 나온다.

# CREATE ALL EEG SPECTROGRAMS
PATH2 = '/kaggle/input/hms-harmful-brain-activity-classification/test_eegs/'
DISPLAY = 0
EEG_IDS2 = test.eeg_id.unique()
all_eegs2 = {}

print('Converting Test EEG to Spectrograms...'); print()
for i,eeg_id in enumerate(EEG_IDS2):
        
    # CREATE SPECTROGRAM FROM EEG PARQUET
    img = spectrogram_from_eeg(f'{PATH2}{eeg_id}.parquet', i<DISPLAY)
    all_eegs2[eeg_id] = img

 

위 코드는 eeg를 바꿔주는 행위이다.

각 테스트 셋에서의 eeg id는 당연히 하나.

그렇기에 i도 1개이고, eeg_id도 한개지만 여기서는 여러 개를 쓸 때를 대비해서 코드를 짜놓았다.

 

그렇게 하나의 이미지를 만들면 all_eegs2 파일에 이미지를 상장하는 array가 들어가게 된다.

all_eegs2
{3911565283: array([[[ 0.469213  ,  0.6447936 ,  0.569684  ,  0.5585266 ],
         [ 0.5671965 ,  0.7803434 ,  0.66798425,  0.657292  ],
         [ 0.3944862 ,  0.7709389 ,  0.5861016 ,  0.68678236],
         ...,
         [ 0.5196512 ,  0.44228584,  0.5878383 ,  0.66300213],
         [ 0.573151  ,  0.5442892 ,  0.5968475 ,  0.58067995],
         [ 0.6481345 ,  0.49603462,  0.5529235 ,  0.6289295 ]],
 
        [[ 0.4680193 ,  0.64338297,  0.5683993 ,  0.5573801 ],
         [ 0.5638069 ,  0.77682495,  0.665632  ,  0.65368396],
         [ 0.4191659 ,  0.7668035 ,  0.5831317 ,  0.68348694],
         ...,
         [ 0.5290423 ,  0.45693034,  0.5830183 ,  0.66085154],
         [ 0.57085395,  0.54082966,  0.59267026,  0.5773413 ],
         [ 0.6451447 ,  0.4963679 ,  0.56164235,  0.6363056 ]],
 
        [[ 0.46646073,  0.6415393 ,  0.56672096,  0.5558824 ],
         [ 0.5593617 ,  0.77221614,  0.66248506,  0.64897424],
         [ 0.4337548 ,  0.7613873 ,  0.58078194,  0.67918473],
         ...,
         [ 0.53742456,  0.4694663 ,  0.5766961 ,  0.65796894],
         [ 0.56772995,  0.536295  ,  0.5871953 ,  0.5728285 ],
         [ 0.64118445,  0.49600285,  0.5677443 ,  0.64160514]],
 
        ...,
 
        [[ 0.22570941,  0.09536854,  0.01234052, -0.02693339],
         [ 0.23955497, -0.08727053, -0.23022385, -0.13647838],
         [ 0.22259173, -0.04008012, -0.30399907, -0.16331546],
         ...,
         [-0.11602728, -0.10392866, -0.45809275, -0.38332593],
         [-0.15014753, -0.06604576, -0.28420356, -0.09510688],
         [ 0.07544328, -0.12024219, -0.20098105, -0.08058362]],
 
        [[ 0.18970947,  0.07125123, -0.01450923, -0.05007764],
         [ 0.1909284 , -0.06394968, -0.2371249 , -0.13732031],
         [ 0.18552051, -0.04843099, -0.3349878 , -0.1980283 ],
         ...,
         [-0.1378346 , -0.14186676, -0.43433276, -0.4660574 ],
         [-0.12000379, -0.09190319, -0.31996   , -0.12861462],
         [ 0.05425864, -0.16127138, -0.21784031, -0.10019433]],
 
        [[ 0.17970082,  0.06902526, -0.0178993 , -0.05184323],
         [ 0.17231429, -0.03752675, -0.2182887 , -0.12943125],
         [ 0.17367148, -0.04022679, -0.34263134, -0.20461884],
         ...,
         [-0.11317398, -0.13510951, -0.41513205, -0.38523573],
         [-0.10012555, -0.09715565, -0.33271798, -0.13856973],
         [ 0.05346172, -0.17864573, -0.2163721 , -0.0998791 ]]],
       dtype=float32)}

 

128개의 리스트이고, 각각 256개의 리스트에서 또 4개의 값(이게 각각의 부위별 값)을 가지고 있다.

즉, 4 * 256 * 128인 것.

 

마찬가지로 테스트 스펙트로그램에 대해서 포함하여, 피처 엔지니어링을 진행해준다.

# FEATURE ENGINEER TEST
PATH2 = '/kaggle/input/hms-harmful-brain-activity-classification/test_spectrograms/'
data = np.zeros((len(test),len(FEATURES)))
    
for k in range(len(test)):
    row = test.iloc[k]
    s = int( row.spectrogram_id )
    spec = pd.read_parquet(f'{PATH2}{s}.parquet')
    
    # 10 MINUTE WINDOW FEATURES
    x = np.nanmean( spec.iloc[:,1:].values, axis=0)
    data[k,:400] = x
    x = np.nanmin( spec.iloc[:,1:].values, axis=0)
    data[k,400:800] = x

    # 20 SECOND WINDOW FEATURES
    x = np.nanmean( spec.iloc[145:155,1:].values, axis=0)
    data[k,800:1200] = x
    x = np.nanmin( spec.iloc[145:155,1:].values, axis=0)
    data[k,1200:1600] = x
    
    # RESHAPE EEG SPECTROGRAMS 128x256x4 => 512x256
    eeg_spec = np.zeros((512,256),dtype='float32')
    xx = all_eegs2[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

test[FEATURES] = data
print('New test shape',test.shape)
New test shape (1, 3651)

 

중간에 xx가 all_eegs2로 바뀐 걸 알 수 있다.

 

eeg_spec 함수를 만들고, all_eeg2로 채워준 것이다.

 

이제 진짜 마지막 단계이다.

# INFER CATBOOST ON TEST
preds = []

for i in range(5):
    print(i,', ',end='')
    model = CatBoostClassifier(task_type='GPU')
    model.load_model(f'CAT_v{1}_f{i}.cat')
    
    test_pool = Pool(
        data = test[FEATURES]
    )
    
    pred = model.predict_proba(test_pool)
    preds.append(pred)
pred = np.mean(preds,axis=0)
print()
print('Test preds shape',pred.shape)

아까 각 모델을 5개의 Kfold로 만들어놓았다.

 

여기서 이제 test_pool을 위에서 구한 1,3651의 테스트 데이터로 잡아놓고, catboost를 통해 predict를 돌린다.

 

그리고 나온 pred를 axis=0에 대해 mean 하면 끝이다. 즉, 각 열의 평균을 구한다.

sub = pd.DataFrame({'eeg_id':test.eeg_id.values})
sub[TARGETS] = pred
sub.to_csv('submission.csv',index=False)
print('Submissionn shape',sub.shape)
sub.head()

submission의 csv 양식을 pd.dataframe으로 만들어주고, test.eeg_id.value를 넣어준다.

그리고 각 traget에 따라 pred를 넣어준다.

그럼 당연히도 다음과 같은 데이터프레임이 나오게 된다.

	eeg_id	seizure_vote	lpd_vote	gpd_vote	lrda_vote	grda_vote	other_vote
0	3911565283	0.282869	0.006726	0.000382	0.154421	0.106088	0.449513

 

이를 csv로 만들어주면 끝.

 

이제 결과를 한 번 봐보자.

 

 

결과는 0.6

 

cv가 0.755가 나왔는데, 결과는 0.6이 나왔다.

똑같이 따라해서 똑같이 나온 듯 싶다.

 

어찌되었든 평가 방법과 피처 엔지니어링 방법을 잘 알았으므로, 다른 모델에도 적용할 수 있을 것이다.

 

다음 시간에는 imagenet으로 한 번 성능 개선을 해볼 것이다. 더불어, 피처 엔지니어링에서도 더 성능 개선을 위해 k-mean와 같은 걸 적용하여 특징 추출을 해보자.

반응형