🍎

kaggle HMS-HBAC EfficientNetB0 Starter - [LB 0.43] 解説

2024/04/10に公開

概要

https://www.kaggle.com/code/cdeotte/efficientnetb0-starter-lb-0-43
EfficientNetB0を使用した脳波分類タスクのソリューション
リンク先でUpvote(高評価)を忘れずに!

コンペ説明

患者の脳波が示す活動の種類を分類する。専門家の評価を正解として予測を行うコンペ

このコンペでは、生の脳波波形(eeg)とスペクトログラム(spectrogram)の両方が提供されており、スペクトログラムは10分、脳波波形は50秒です、またこれらのデータの中央の50秒は同じデータであり、この期間は同じデータを二つの表現方法で提示している。

eeg: 一般に想像する波形データ(株価など)
spectrogram: 信号を[横軸:時間,縦軸:周波数,強さ:色]で表すデータ形式

EfficientNetとは

EfficientNetは2019年にgoogleの研究チームから発表された画像分類のためのモデル。
主な目的はモデルの精度を最大化しながら、計算コストとモデルサイズを最小限に抑えること。

  1. ベース
    EfficientNetのベースはCNN
  2. コンパウンドスケーリング
    EfficientNet独自の特徴として、深さ(層の数)、幅(層ごとのチャネル数)、解像度(入力画像サイズ)の3つの次元を一定の比率で同時にスケーリング(B0~B7)し、これによって高速かつ高精度の学習を、効率的に得られることを観測的事実として明らかにした。
  3. EfficientNetモデルには種類(B0~B7)があり、数字が大きくなる程、計算リソースと性能が上昇します。これらは同じ基本的なアーキテクチャを持っているが、ネットワークの深さ、幅、および入力解像度が異なる。
  • まとめ
    EfficientNetはCNNベースであり、要件に応じてB0~B7までversionが選択可能な画像分類モデル。
    注意として、EfficientNetはスケーリングが可変な1つのモデルではなく、同じ基本構造を持ちながら、深さ、幅(チャネル数)、および解像度のスケーリングが予め異なる設定になっている複数のモデル群である

コード解説

1. インポート

import os, gc
os.environ["CUDA_VISIBLE_DEVICES"]="0,1"
import tensorflow as tf
import pandas as pd, numpy as np
import matplotlib.pyplot as plt
print('TensorFlow version =',tf.__version__)

各種ライブライのインストールを行います。
tensorflow, pandas, matplotのライブラリを使用する。
os.environ[環境変数名]で環境変数の値を取得できる

2. GPU設定

gpus = tf.config.list_physical_devices('GPU')
if len(gpus)<=1: 
    strategy = tf.distribute.OneDeviceStrategy(device="/gpu:0")
    print(f'Using {len(gpus)} GPU')
else: 
    strategy = tf.distribute.MirroredStrategy()
    print(f'Using {len(gpus)} GPUs')

GPU使用環境の設定を行います。
tf.config.list_physical_devices
ホストから認識できる物理デバイスのリストを返す。
引数(device_type, notes)

tf.distribute.OneDeviceStrategy
分散計算を一つのデバイスで行う
引数(device) 例:"/cpu:0", "/gpu:0", "/device:CPU:0", "/device:GPU:0"

tf.distribute.MirroredStrategy
複数のGPUデバイスで、同期学習を行う

3. 使用データの設定

VER = 5

# IF THIS EQUALS NONE, THEN WE TRAIN NEW MODELS
# IF THIS EQUALS DISK PATH, THEN WE LOAD PREVIOUSLY TRAINED MODELS
LOAD_MODELS_FROM = '/kaggle/input/brain-efficientnet-models-v3-v4-v5/'

USE_KAGGLE_SPECTROGRAMS = True
USE_EEG_SPECTROGRAMS = True

使用データのpath,有効無効などを設定します

4. 学習効率化設定

# USE MIXED PRECISION
MIX = True
if MIX:
    tf.config.optimizer.set_experimental_options({"auto_mixed_precision": True})
    print('Mixed precision enabled')
else:
    print('Using full precision')

学習の浮動小数点に関する設定を行います
tf.config.optimizer.set_experimental_options
特定の計算をfloat32からfloat16にする。

5. データのロード

df = pd.read_csv('/kaggle/input/hms-harmful-brain-activity-classification/train.csv')
TARGETS = df.columns[-6:]
print('Train shape:', df.shape )
print('Targets', list(TARGETS))
df.head()

pandasでデータを読み込みます

6. オーバーラップ(重複)の無いデータの作成

overlap: 複数のデータ間でデータの重複があること。例:同じidや名前のデータが複数のデータにある等

train = df.groupby('eeg_id')[['spectrogram_id','spectrogram_label_offset_seconds']].agg(
    {'spectrogram_id':'first','spectrogram_label_offset_seconds':'min'})
train.columns = ['spec_id','min']

tmp = df.groupby('eeg_id')[['spectrogram_id','spectrogram_label_offset_seconds']].agg(
    {'spectrogram_label_offset_seconds':'max'})
train['max'] = tmp

tmp = df.groupby('eeg_id')[['patient_id']].agg('first')
train['patient_id'] = tmp

tmp = df.groupby('eeg_id')[TARGETS].agg('sum')
for t in TARGETS:
    train[t] = tmp[t].values
    
y_data = train[TARGETS].values
y_data = y_data / y_data.sum(axis=1,keepdims=True)
train[TARGETS] = y_data

tmp = df.groupby('eeg_id')[['expert_consensus']].agg('first')
train['target'] = tmp

train = train.reset_index()
print('Train non-overlapp eeg_id shape:', train.shape )
train.head()

データを加工します。
eeg_idについての情報を整理。
それぞれのeeg_idについて、spectrogam_idと、オフセット時間の最小値と最大値(波形がどのくらいの時間存在するか)、patient_id、正規化したvoteをまとめてDataFrame(train)にする。

7. スペクトログラムのロード

%%time
READ_SPEC_FILES = False

# 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()

スペクトログラムデータ(csvでない)を読み込みます。
著者の方が高速でロードできるデータセットを作成してくれている。READ_SPEC_FILES = Falseでロードできる

8. kerasのデータローダー定義

# データ拡張用のライブラリ
import albumentations as albu
# ターゲットの辞書
TARS = {'Seizure':0, 'LPD':1, 'GPD':2, 'LRDA':3, 'GRDA':4, 'Other':5}
# 反転辞書
TARS2 = {x:y for y,x in TARS.items()}

class DataGenerator(tf.keras.utils.Sequence):
    'Generates data for Keras'
    def __init__(self, data, batch_size=32, shuffle=False, augment=False, mode='train',
                 specs = spectrograms, eeg_specs = all_eegs): 

        self.data = data
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.augment = augment
        self.mode = mode
        self.specs = specs
        self.eeg_specs = eeg_specs
        self.on_epoch_end()
        
    def __len__(self):
        'Denotes the number of batches per epoch'
        ct = int( np.ceil( len(self.data) / self.batch_size ) )
        return ct

    def __getitem__(self, index):
        'Generate one batch of data'
        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
        X, y = self.__data_generation(indexes)
        if self.augment: X = self.__augment_batch(X) 
        return X, y

    def on_epoch_end(self):
        'Updates indexes after each epoch'
        self.indexes = np.arange( len(self.data) )
        if self.shuffle: np.random.shuffle(self.indexes)
                        
    def __data_generation(self, indexes):
        'Generates data containing batch_size samples' 
        
        X = np.zeros((len(indexes),128,256,8),dtype='float32')
        y = np.zeros((len(indexes),6),dtype='float32')
        img = np.ones((128,256),dtype='float32')
        
        for j,i in enumerate(indexes):
            row = self.data.iloc[i]
            if self.mode=='test': 
                r = 0
            else: 
                r = int( (row['min'] + row['max'])//4 )

            for k in range(4):
                # EXTRACT 300 ROWS OF SPECTROGRAM
                img = self.specs[row.spec_id][r:r+300,k*100:(k+1)*100].T
                
                # LOG TRANSFORM SPECTROGRAM
                img = np.clip(img,np.exp(-4),np.exp(8))
                img = np.log(img)
                
                # STANDARDIZE PER IMAGE
                ep = 1e-6
                m = np.nanmean(img.flatten())
                s = np.nanstd(img.flatten())
                img = (img-m)/(s+ep)
                img = np.nan_to_num(img, nan=0.0)
                
                # CROP TO 256 TIME STEPS
                X[j,14:-14,:,k] = img[:,22:-22] / 2.0
        
            # EEG SPECTROGRAMS
            img = self.eeg_specs[row.eeg_id]
            X[j,:,:,4:] = img
                
            if self.mode!='test':
                y[j,] = row[TARGETS]
            
        return X,y
    
    def __random_transform(self, img):
        composition = albu.Compose([
            albu.HorizontalFlip(p=0.5),
            #albu.CoarseDropout(max_holes=8,max_height=32,max_width=32,fill_value=0,p=0.5),
        ])
        return composition(image=img)['image']
            
    def __augment_batch(self, img_batch):
        for i in range(img_batch.shape[0]):
            img_batch[i, ] = self.__random_transform(img_batch[i, ])
        return img_batch

データーローダーを定義します。

  • tf.keras.utils.Sequence
    データをバッチでモデルに提供する機能(データローダー)のために利用される。
    Sequenceクラスはスレッドセーフ(複数スレッドからの同時コードアクセスを許容)であり、大規模なデータでは必要なバッチのみをオンデマンドで読み込むことができる。また前処理やデータ拡張を幅広くサポートしている。

__getitem__と__len__メソッドをオーバーライドする必要がある。

  • __getitem__(self, idx)
    指定されたidxに対応するバッチを返す。バッチが100個、バッチサイズが10、データ総数が1000のデータローダーがあるなら、idx=56の場合は56個目のバッチ、つまり560~569番目のデータを内包するバッチ(560~569番目の入力データのリスト等, 560~569番目の正解データのリスト等)を返す。
  • __len__(self)
    全データをカバーできるシーケンスの長さ(バッチの数)を返す。全データポイント数/バッチサイズで定義されることが多い。上記の例では100を返す。
  • on_epoch_end() ※任意
    各エポック(前データが期待的に1度処理される)の終わりに呼び出される

__data_generation(self, indexes)
getitemにデータを提供する自作関数。indexesは必要なデータのindex全てのリストを受け取る

  • r: 評価時で無い場合、データの中心点を抽出の開始点に設定(オフセットの最小値+最大値 /4でおおよそデータ中心が取れる)
  • img = self.specs[row.spec_id][r:r+300,k*100:(k+1)*100].T: 4回に分けてスペクトラムデータを取得
  • np.clip: 最小値と最大値を設定、超過時は設定値に丸める
  • np.log: 対数変換
  • 正規化、欠損値を0埋め
  • X[j,14:-14,:,k] = img[:,22:-22] / 2.0: imgに格納されているspectrogramデータを時間方向に22から-22まで切り取り、中央の256行のみが残る。/2は正規化?
    Xのバッチ内の j 番目のサンプルの k 番目のチャンネルにクロップ(切り取り)されたスペクトログラムを割り当てる。
    また周波数方向に14から-14まで切り取る。
  • img = self.eeg_specs[row.eeg_id] \ X[j,:,:,4:] = img: 辞書eeg_specsからid指定でspectrogramを取得、Xの第4チャンネル以降にspectrogramデータを追加。第4チャンネルまでは上記の処理で埋まっている
  • if self.mode!='test': \ y[j,] = row[TARGETS]: 評価時で無い場合、対応ラベルをyに割り当て

9. データ拡張

    def __random_transform(self, img):
        composition = albu.Compose([
            albu.HorizontalFlip(p=0.5),
            #albu.CoarseDropout(max_holes=8,max_height=32,max_width=32,fill_value=0,p=0.5),
        ])
        return composition(image=img)['image']
            
    def __augment_batch(self, img_batch):
        for i in range(img_batch.shape[0]):
            img_batch[i, ] = self.__random_transform(img_batch[i, ])
        return img_batch

データ拡張を行います。

  • __random_transform
    __augment_batchのための関数。0.5×100%(50%)で入力のimgをデータ拡張(左右反転)する

  • __augment_batch
    画像のバッチ(入力シーケンスデータ、正解ラベルシーケンスデータ)を入力すると、データ拡張を行った後のバッチを返す

10. 学習率の時間的変更

LR_START = 1e-4
LR_MAX = 1e-3
LR_RAMPUP_EPOCHS = 0
LR_SUSTAIN_EPOCHS = 1
LR_STEP_DECAY = 0.1
EVERY = 1
EPOCHS = 4

def lrfn(epoch):
    if epoch < LR_RAMPUP_EPOCHS:
        lr = (LR_MAX - LR_START) / LR_RAMPUP_EPOCHS * epoch + LR_START
    elif epoch < LR_RAMPUP_EPOCHS + LR_SUSTAIN_EPOCHS:
        lr = LR_MAX
    else:
        lr = LR_MAX * LR_STEP_DECAY**((epoch - LR_RAMPUP_EPOCHS - LR_SUSTAIN_EPOCHS)//EVERY)
    return lr

rng = [i for i in range(EPOCHS)]
y = [lrfn(x) for x in rng]
plt.figure(figsize=(10, 4))
plt.plot(rng, y, 'o-'); 
plt.xlabel('epoch',size=14); plt.ylabel('learning rate',size=14)
plt.title('Step Training Schedule',size=16); plt.show()

LR = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose = True)

学習率を学習の経過とともに減少させます。

11. モデルの構築

EfficientNetB0を使用。kaggle及びeegの8つのspectrogramを1つの大きな平面画像に再形成し、EfficientNetに送る

!pip install --no-index --find-links=/kaggle/input/tf-efficientnet-whl-files /kaggle/input/tf-efficientnet-whl-files/efficientnet-1.1.1-py3-none-any.whl

kaggleデータセットからEfficientNetの学習済みの重みをロード

import efficientnet.tfkeras as efn

def build_model():
    
    inp = tf.keras.Input(shape=(128,256,8))
    base_model = efn.EfficientNetB0(include_top=False, weights=None, input_shape=None)
    base_model.load_weights('/kaggle/input/tf-efficientnet-imagenet-weights/efficientnet-b0_weights_tf_dim_ordering_tf_kernels_autoaugment_notop.h5')
    
    # RESHAPE INPUT 128x256x8 => 512x512x3 MONOTONE IMAGE
    # KAGGLE SPECTROGRAMS
    x1 = [inp[:,:,:,i:i+1] for i in range(4)]
    x1 = tf.keras.layers.Concatenate(axis=1)(x1)
    # EEG SPECTROGRAMS
    x2 = [inp[:,:,:,i+4:i+5] for i in range(4)]
    x2 = tf.keras.layers.Concatenate(axis=1)(x2)
    # MAKE 512X512X3
    if USE_KAGGLE_SPECTROGRAMS & USE_EEG_SPECTROGRAMS:
        x = tf.keras.layers.Concatenate(axis=2)([x1,x2])
    elif USE_EEG_SPECTROGRAMS: x = x2
    else: x = x1
    x = tf.keras.layers.Concatenate(axis=3)([x,x,x])
    
    # OUTPUT
    x = base_model(x)
    x = tf.keras.layers.GlobalAveragePooling2D()(x)
    x = tf.keras.layers.Dense(6,activation='softmax', dtype='float32')(x)
        
    # COMPILE MODEL
    model = tf.keras.Model(inputs=inp, outputs=x)
    opt = tf.keras.optimizers.Adam(learning_rate = 1e-3)
    loss = tf.keras.losses.KLDivergence()

    model.compile(loss=loss, optimizer = opt) 
        
    return model

ここでは、主にデータが流れる「枠組み」や「パイプライン」を作成します

  • import efficientnet.tfkeras as efn
    有志によるEfficientNetの再実装
  • tf.keras.Input(shape=(128,256,8))
    形状の定義。高さが128、幅が256、チャンネル数が8のデータをモデルが受け入れる。実際の形状は(バッチサイズ, 128, 256, 8)
  • base_model = efn.EfficientNetB0(include_top=False, weights=None, input_shape=None) \ base_model.load_weights('/kaggle/input/tf-efficientnet-imagenet-weights/efficientnet-b0_weights_tf_dim_ordering_tf_kernels_autoaugment_notop.h5')
    ベースモデルの選択、重みのロード。(ここでは入力を明示的に設定しない、出力の分類部分を含まない)

インプット形状を 128x256x8 => 512x512x3 のモノトーンに変更。EfficientNetはrgbの3チャンネルを期待しますが、ここではモノトーンで無理やり3チャンネルを確保します。B0は224×224が最適ですが、ここではデータ形状から512×512を使用します。

  • x1(kaggle spectrogram)
    チャンネル0~3を分割して新しい(バッチサイズ, 128, 256, 1)のテンソルをリスト化、axis=1で結合。 512×256×1

  • x2(eeg spectrogram)
    チャンネル4~7を分割して新しい(バッチサイズ, 128, 256, 1)のテンソルをリスト化、結合。 512×256×1

  • if ~
    使用するspectrogramを全て(kaggle specとeeg spec)axis=2で結合
    512×512×1

  • x = tf.keras.layers.Concatenate(axis=3)([x,x,x])
    3チャンネルを確保するためにaxis=3で同じデータを3つ結合 512×512×3

  • OUTPUT

出力層の定義。ベースモデルに入力を与えて得られた出力にaverage poolingと全結合(出力数6で活性化はsoftmax。分類用のDenseレイヤ)を通して最終出力を得る

  • COMPILE MODEL

モデルの学習前設定。入力すると出力してくれて、fit()で学習可能なモデルを返す。
model = tf.keras.Model(inputs=inp, outputs=x): 入力から出力までの流れを与える
opt = tf.keras.optimizers.Adam(learning_rate = 1e-3): 重みの最適化(更新)手法を決める
loss = tf.keras.losses.KLDivergence(): 最適化する損失関数を決める
model.compile(loss=loss, optimizer = opt): 設定をモデルに読み込む

12. モデルの学習

from sklearn.model_selection import KFold, GroupKFold
import tensorflow.keras.backend as K, gc

all_oof = []
all_true = []

gkf = GroupKFold(n_splits=5)
for i, (train_index, valid_index) in enumerate(gkf.split(train, train.target, train.patient_id)):  
    
    print('#'*25)
    print(f'### Fold {i+1}')
    
    train_gen = DataGenerator(train.iloc[train_index], shuffle=True, batch_size=32, augment=False)
    valid_gen = DataGenerator(train.iloc[valid_index], shuffle=False, batch_size=64, mode='valid')
    
    print(f'### train size {len(train_index)}, valid size {len(valid_index)}')
    print('#'*25)
    
    K.clear_session()
    with strategy.scope():
        model = build_model()
    if LOAD_MODELS_FROM is None:
        model.fit(train_gen, verbose=1,
              validation_data = valid_gen,
              epochs=EPOCHS, callbacks = [LR])
        model.save_weights(f'EffNet_v{VER}_f{i}.h5')
    else:
        model.load_weights(f'{LOAD_MODELS_FROM}EffNet_v{VER}_f{i}.h5')
        
    oof = model.predict(valid_gen, verbose=1)
    all_oof.append(oof)
    all_true.append(train.iloc[valid_index][TARGETS].values)
    
    del model, oof
    gc.collect()
    
all_oof = np.concatenate(all_oof)
all_true = np.concatenate(all_true)

k分割交差検証を使用して、データセットを分割して学習を行います。

  • gkf = GroupKFold(n_splits=5)
    k分割交差検証のオブジェクトを作成
  • for i, (train_index, valid_index) in enumerate(gkf.split(train, train.target, train.patient_id))
    特徴量とターゲットをinputして、patient_idによってグループ化。(patient_idが同じデータは同じグループに振り分けられる)
  • train_gen = DataGenerator(train.iloc[train_index], shuffle=True, batch_size=32, augment=False)
    valid_gen = DataGenerator(train.iloc[valid_index], shuffle=False, batch_size=64, mode='valid')
    kerasのDataLoaderとして定義したクラスから、インデックス指定でデータをバッチで取得。tarin_gen: 学習用、valid_gen: 評価用
  • K.clear_session()
    前のfoldのデータを消去
  • strategy.scope()
    このスコープ内でモデルを構築することにより、strategyに設定されている分散戦略に応じて、各GPUにモデルのコピーが作成され、バッチ内の異なるサンプルが異なるGPUで並行して処理される。
  • model.fit(train_gen, verbose=1,validation_data = valid_gen,epochs=EPOCHS, callbacks = [LR])
    訓練用データ、評価用データ、学習エポック数、学習率を指定して現在のfoldでモデルの重みを学習。
  • model.save_weights(f'EffNet_v{VER}_f{i}.h5')
    重みの保存
  • model.load_weights(f'{LOAD_MODELS_FROM}EffNet_v{VER}_f{i}.h5')
    重みのロード(学習済みモデルを使用する場合)
  • oof = model.predict(valid_gen, verbose=1)
    oof(out of fold(foldの出力結果))に予測値を格納。verbose=1で出力を適宜表示
  • all_true.append(train.iloc[valid_index][TARGETS].values)
    評価用データのindexのtarget(正解ラベル)をall_trueに全て格納。後に正解率の評価などに使用する
  • del model, oof \ gc.collect()
    変数の消去、データ解放
  • all_oof = np.concatenate(all_oof)
    配列データを全て結合して1次元配列に変更

13. 評価

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 EfficientNetB2 =',cv)

kaggleのklダイバージェンスライブラリを使用してcvスコアを計算します。

14. test.csvとspectrogramの読み込み

del all_eegs, spectrograms; gc.collect()
test = pd.read_csv('/kaggle/input/hms-harmful-brain-activity-classification/test.csv')
print('Test shape',test.shape)
test.head()

提出用inputデータ(test.csv)の読み込み

# READ ALL SPECTROGRAMS
PATH2 = '/kaggle/input/hms-harmful-brain-activity-classification/test_spectrograms/'
files2 = os.listdir(PATH2)
print(f'There are {len(files2)} test spectrogram parquets')
    
spectrograms2 = {}
for i,f in enumerate(files2):
    if i%100==0: print(i,', ',end='')
    tmp = pd.read_parquet(f'{PATH2}{f}')
    name = int(f.split('.')[0])
    spectrograms2[name] = tmp.iloc[:,1:].values
    
# RENAME FOR DATALOADER
test = test.rename({'spectrogram_id':'spec_id'},axis=1)

spectrogram(parquet)の読み込み

  • spectrograms2[name] = tmp.iloc[:,1:].values
    parquetファイルの名前をkeyとして、スペクトログラムをvalueとする辞書[spectrogram2]を作成。一列目はインデックスや識別子など不要なデータが多いため除外(通常のデータであったとしても、一列目は除外しても連続波形データにはほとんど影響がない)。
  • ロードしたtest.csvの列名を変更

15. spectrogramから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

test_eegsからparquetファイルを取得して、スペクトログラムに変更する。モデルはコンペ主催から与えられるスペクトログラムと、eegデータから生成したスペクトログラムの両方を利用するので、、テストeegデータからスペクトログラムを生成する必要がある。
また、オプションでeegスペクトログラム波形と元のeeg波形を描画する

# READ ALL EEG SPECTROGRAMS
PATH2 = '/kaggle/input/hms-harmful-brain-activity-classification/test_eegs/'
DISPLAY = 1
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波形を描画

16. 予測

# INFER EFFICIENTNET ON TEST
preds = []
model = build_model()
test_gen = DataGenerator(test, shuffle=False, batch_size=64, mode='test',
                         specs = spectrograms2, eeg_specs = all_eegs2)

for i in range(5):
    print(f'Fold {i+1}')
    if LOAD_MODELS_FROM:
        model.load_weights(f'{LOAD_MODELS_FROM}EffNet_v{VER}_f{i}.h5')
    else:
        model.load_weights(f'EffNet_v{VER}_f{i}.h5')
    pred = model.predict(test_gen, verbose=1)
    preds.append(pred)
pred = np.mean(preds,axis=0)
print()
print('Test preds shape',pred.shape)
  • spectrograms2 主催からデータとして与えられるスペクトログラム
  • eeg_specs test_eegデータから生成したスペクトログラム
  1. submissionファイルの作成
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()

# 予測値の和が1になっていることを確認
# SANITY CHECK TO CONFIRM PREDICTIONS SUM TO ONE
sub.iloc[:,-6:].sum(axis=1)

説明は以上になります。
以下に質問を抜粋します。

質問

  1. eegデータからスペクトログラムを生成しても、それは結局kaggleから与えられたスペクトログラムの中心の50秒と同一のデータではないのか?
    著者からの回答(翻訳):
    ありがとう。素晴らしい質問です。kaggleのスペクトログラムと、eegから生成したスペクトログラムの両方を使用してモデルをトレーニングすると、(kaggleのみの場合と比較して)CVおよびLBスコアが向上することが確認できました。したがってeegスペクトログラムには明らかに新しい情報が含まれています。
    1つの違いは、kaggleスペクトログラムの長さは10分であるのに対し、eegから生成されたスペクトログラムは僅か50秒であるということです。kaggleスペクトログラムの中心の50秒とeegスペクトログラムを比較して、どのように異なるのかを確認するのも興味深いでしょう。
    それらは次の理由から異なるのではないかと思います。
    ・Kaggleは、私達が利用できない信号からスペクトログラムを作成する場合があります。
    ・私の公式は、LL = ( (Fp1 - F7) + (F7 - T3) + (T3 - T5) + (T5 - O1) )/4です。これは、一つのLL時系列を作成するのに正しい方法ではない可能性があります。
    ・私のスペクトログラムのハイパーパラメータ設定はKaggleのものと異なります
    ・Kaggleはスペクトログラムを作成する前にノイズ除去を使用した可能性があります

Discussion