EEGデータセットをまとめるPythonライブラリを作った

BCIの判別器に深層学習を利用した際、いくつかの公開されているデータセットと手持ちのデータセットを組み合わせて学習させることがありました。
その際に形式の異なる複数のデータセットをまとめてkerasなどで扱いやすい1つのデータセット(HDF)に統一、集約するライブラリを作りました。もちろんデータセットが1種類でも活用できます。

#### できること
+ 簡単にデータセットを試行ごとに分割して格納できる。
    + 全試行が連番で保存されるので学習時に取り出しやすい。
    + 連番データには参加者番号などを関連付けることができる。
+ 複数のデータセットを集約できる。
    + まとめて前処理を適用できる。
+ データセットごとに異なるチャンネルの順序を揃えることができる(使用しないチャンネルは外せる)。
+ EEGLABから読み取ることができる。

一応想定しているのは主要BCIパラダイム(P300、SSVEP、MI)の判別ですが、基本的な分類問題なら対応しています。

https://github.com/s-n-1-0/bci-dataset

# データセット集約の流れ
1. EEGデータセットの多くは被験者・セッションごとに.m(MATLAB)形式のファイルで配布されていることが多いので、EEGLABへ通して.set形式でファイル保存するか、scipyを使ってnumpy形式(ndarray)で取得します。.npy形式ならnumpy.loadでそのまま読み込みます。
※EEGLABの操作方法については割愛します。使い方は、[この本](https://www.amazon.co.jp/dp/4130121189)が参考になるかと思います。
1. 後述するbci-datasetライブラリを使用して1つのHDFファイルに全被験者・セッションのデータを格納します。
1. 全てのデータセットに対して流れ1と2をそれぞれ行います。
1. データセットごとに作成したHDFファイルをbci-datasetによって1つのHDFファイルに集約し、集約用HDFファイルとして保存します。
1. 全てのデータセットに共通の前処理を適用します。前処理後のデータを格納したグループが集約用HDFに生成されます。
※生波形以外をモデルの入力とする場合は、この時に特徴量を抽出します。
1. 前処理後のデータをkerasやPyTorchで読み取って学習に使用します。


# bci-dataset使用方法
## インストール
bci-datasetは、pipでインストールできます。

`pip install bci-dataset`

## 手順2 : EEGデータをHDFに格納する。
EEGLAB形式(.set)とnumpyのndarrayの形式に対応しています。

### 共通事項
bci-datasetのDatasetUpdaterインスタンスを作成します。指定するパスはこれから生成する/既に生成されているHDFファイルのパスです。
```python
from bci_dataset import DatasetUpdater

fpath = "./dataset.hdf"
fs = 500 # sampling rate
updater = DatasetUpdater(fpath,fs=fs)
updater.remove_hdf() # 既にfpathにファイルが存在するなら削除する。
```

### EEGLAB形式の場合
#### 前提
+ EEGLAB上でエポッキングを行っている必要があります。
エポック削除等は任意です。
+ EEGLAB上でサンプリングレートを揃えている必要があります。
#### データを追加
eeglab_listは追加したいsetファイルのパスのリストです。
lablesは分類するラベルです。EEGLABと揃える必要があります。
```python
labels = ["left","right"]
eeglab_list = ["./sample.set"] # path list of eeglab files

# add eeglab(.set) files
for fp in eeglab_list:
    updater.add_eeglab(fp,labels)
```
### numpy形式の場合
#### 前提
+ 追加する前にデータのサンプリングレートを他のデータセットと揃えている必要があります。

#### データを追加
ダミーデータで説明します。
+ dummy_dataはチャンネル×サンプル数の信号データです。
+ dummy_indexesは試行を開始した時のインデックスリストです。
+ dummy_lablesは設定するラベルです。
    + dummy_indexesとdummy_labelsの要素数は等しい必要があります。
+ dummy_size = 1試行のサンプル数です。

add_numpyメソッドを呼ぶことで追加できます。
```python
#dummy
dummy_data = np.ones((12,6000)) # channel × signal
dummy_indexes = [0,1000,2000,3000,4000,5000] #Index of trial start
dummy_labels = ["left","right"]*3 #Label of trials
dummy_size = 500 #Size of 1 trial

updater.add_numpy(dummy_data,dummy_indexes,dummy_labels,dummy_size)
```

## HDFの中身
生成されるデータセットの構成です。
**注意 : 図のdatasetはHDF Datasetクラスのことです。**

HDFにはoriginグループと後述する前処理によって生成されるpreproグループがあります。add_eeglab/add_numpyメソッドによって追加されるデータは試行ごとにoriginグループ下に配置されます。
例えば先ほどのダミーデータだと、6試行なのでoriginグループ下に「1」から「6」のHDF Datasetが追加されます(値は連番)。
```
hdf file
├ origin : group / raw data
│ ├ 1 : dataset
│ ├ 2 : dataset
│ ├ 3 : dataset
│ ├ 4 : dataset
│ ├ 5 : dataset
│ └ …
└ prepro : group / data after preprocessing
　 ├ custom : group / "custom" is any group name
　 │ ├ 1 : dataset
　 │ ├ 2 : dataset
　 │ ├ 3 : dataset
　 │ ├ 4 : dataset
　 │ ├ 5 : dataset
　 │ └ …
　 └ custom2 : group
　 　 └ ...omit (1,2,3,4,…)
```

#### HDFファイルの読み取り方
読み取り方について詳細は割愛しますがh5pyライブラリを使えば読み取ることができます。
```python
import h5py
with h5py.File(fpath) as h5:
    fs = h5["prepro/custom"].attrs["fs"]
    dataset_size = h5["prepro/custom"].attrs["count"]
    dataset79 = h5["prepro/custom/79"][()] #ch × samples
    dataset79_label = h5["prepro/custom/79"].attrs["label"]
```

### 手順4 : データセットの結合
データセットAとデータセットBを合わせたデータセットCを作ることもできます。
データセットごとにチャンネルの順序が異なる場合が多いのでここで順序を揃えたり、使うチャンネルのみ指定することができます(ch_indexes)。


結合する場合は、結合元のDatasetUpdaterにdataset_nameを設定する必要があります。
```python
target = DatasetUpdater("new_dataset_c.h5",fs=fs)
target.remove_hdf() # reset hdf
s1 = DatasetUpdater("source_a.h5",fs=fs,dataset_name="source_a")
s2 = DatasetUpdater("source_b.h5",fs=fs,dataset_name="source_b")
s1_ch_indexes = [1,60,10,5]# channel indexes to use
target.merge_hdf(s1,ch_indexes=s1_ch_indexes)
target.merge_hdf(s2)
```

### 手順5 : 前処理
追加したデータに対して、前処理を適用することができます。
前処理後のデータを含むグループは、`prepro/<指定したグループ名>`に生成されます。
再度同じグループ名を指定してpreprocessメソッドを実行した場合、既に生成されている指定グループを一度削除してから前処理が行われます。

```python
"""
preprocessing example
bx : ch × samples
"""
def prepro_func(bx:np.ndarray): 
    x = bx[12:15,:]
    return StandardScaler().fit_transform(x.T).T
updater.preprocess("custom",prepro_func)
```

---
以上の操作によってHDF形式のデータセットを構築することができます。
構築後の各試行データは連番になっているのでgeneratorなどを使って簡単に読み込むことができます。

次は作成したHDFファイルをkerasで読み込む方法とEEGLABの処理をGUIではなくコードで実行する方法の記事を投稿したいと思います。