QoreSDKの紹介とQoreで不整脈検出

これは[深層学習以外の機械学習と応用技術 by QuantumCore Advent Calendar 2019](https://qiita.com/advent-calendar/2019/quantumcore_reservoir)の第2日目の記事です。

# はじめに　

[QuantumCore](https://www.qcore.co.jp/)のリードエンジニアの@sk1010kです。
@ryoppippiに続いて、QoreとQoreSDKについて紹介していきます。

弊社QuantumCoreは時系列解析のソリューションを提供している会社で、Reservoir Computingを応用したQoreシリーズというものを展開しています。
その中の、コア技術である「Qore」と、前処理・特徴抽出・APIクライアントを含んだ「QoreSDK」を、今回のアドベントカレンダーでは無料で公開しています。
Reservoir ComputingやQoreについては、第1日目に@ryoppippiが紹介しているので、この記事ではQoreSDKについて紹介していきます。
合わせて、Qore+QoreSDKを使って、心電図から不整脈を検出する事例を紹介します。
不整脈検出で使うコードや環境構築の方法は[Githubの公式リポジトリ](https://github.com/qcore-info/advent-calendar-2019)に載せていますので、ぜひ試してみてください。

本企画では、iPhone11やGoogle Pixel4、NVIDIA Jetson Nano開発キットといった豪華プレゼントを用意していますので、ぜひ皆さんの投稿もお待ちしております！
QoreかQoreSDKの一方のみを使ってもいいですし、Reservoir Computingに関することやその他の機械学習技術についてでも歓迎です！
ちなみに、社内の人はプレゼント対象外ですので、ご安心を。。。

# QoreSDKについて

QoreSDKは、Qoreの前段で使われる、前処理・特徴抽出・APIクライアントなどをひとまとめにしたライブラリです。
生データを直接Qoreに入れて学習・推論することもできますが、**QoreSDKで前処理・特徴抽出することで、より少ないデータで、かつ、Qore内部のReservoirを小さく保ちつつ（省メモリで）、高精度を維持することができます。**
QoreSDKの特徴抽出器（qore_sdk.featurizer.Featurizer）は、時系列データに対するフィルタ処理を基本としたものになっています。
詳細は非公開としていますが、一言でいうと周波数分解です。

Qore及びQoreSDKの利用には現在アカウント申請が必要です。
[こちら](https://docs.google.com/forms/d/e/1FAIpQLSffXV4AQQFsj_SsGujBvYgCt3qhEb4Id-Gv3QvWOV5iVp1meA/viewform)のフォームから「利用タイプ」で「Qiitaアドベントカレンダ執筆」を選択し、Qiitaアカウント名、メールアドレスを入力し送信していただくと、返信メールからQoreSDKをダウンロードできます。
今回QoreSDKはPython版のバイナリパッケージのみ公開しています。
自分のOSやPythonのバージョンにあったバイナリをダウンロードして、インストールしてください。
具体的な手順は[Githubの公式リポジトリ](https://github.com/qcore-info/advent-calendar-2019)に記載されています。

## コード例

簡単に使い方を説明します。

QoreSDKはNumPyなどと同じく、Pythonのライブラリとなっているので、pipでインストール可能で、（Ubuntu18.04, Python3.6の例）

```shell
$ pip install qore_sdk-0.1.0-cp36-cp36m-linux_x86_64.whl
```

importも同様です。

```python
>>> import qore_sdk.featurizer
```

QoreSDKの特徴抽出器(qore_sdk.featurizer.Featurizer)にかけるデータを用意します。
shapeが (サンプル数, 時系列長)の2次元のNumPy配列です。つまり、単変量の時系列データです。

```python
>>> import numpy as np
>>> X = np.arange(10*100).reshape(10,100)
>>> print(X.shape)
(10,100)
```

それではこのデータにFeaturizerを使います。
Featurizerをインスタンス化するときに、コンストラクタにいくつのフィルタを使うか（n_filters）を指定します。
n_filtersは大体20~40くらいでうまくいく場合が多いです。axisでどの軸を処理するか指定します。

```python
>>> n_filters = 40
>>> featurizer = qore_sdk.featurizer.Featurizer(n_filters)
>>> features = featurizer.featurize(X, axis=1)
>>> print(features.shape)
(10, 40)
```

100だった第2次元の長さが40に変わっています。
これは、長さが100の単変量時系列データから40個のフィルタを使って、40個の特徴を抽出したことを表しています。

※追記
その他のクラスやメソッドの使い方は、help関数などを使って、docstringを参照してください。

# 不整脈検出

上記で扱ったのは、説明のためだけの無意味な配列でした。
以降は、QoreとQoreSDKを使って、もっと実践的なデータを扱ってみます。
題材は心電図のデータから不整脈を検出するというタスクです。（正常拍動と不整脈の2値分類）
日本メディカルAI学会のオンライン講義資料にある[8. 実践編: ディープラーニングを使ったモニタリングデータの時系列解析](https://japan-medical-ai.github.io/medical-ai-course-materials/notebooks/08_Sequential_Data_Analysis_with_Deep_Learning.html)を参考にしています。
[Githubの公式リポジトリ](https://github.com/qcore-info/advent-calendar-2019)に環境構築方法がありますので、参考にしてください。
以下のコードは、[こちらのノートブック](https://github.com/qcore-info/advent-calendar-2019/blob/master/demo.ipynb)にまとめてあります。

## データセット

[MIT-BIH Arrhythmia Database (mitdb)](https://www.physionet.org/physiobank/database/mitdb/)を使います。以下のようなデータです。（上述の講義資料から引用）

正常拍動
![normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541082/d3182d3b-60f1-da3d-d0a8-acc86755a040.png)
不整脈
![veb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541082/d70c68c7-9f72-23e3-8a50-51e83a2b1159.png)

## コード解説

### データのダウンロード

上述の講義資料を参考に、mitdbから心電図データをダウンロードし、前処理を施すモジュール(dataloader.py)を用意しました。
モジュール実行ディレクトリ直下の`dataset/download/`にダウンロードされます。
と同時に、`dataset/preprocessed-denoise/`に前処理を施したデータが出力されます。
初回実行時は、ダウンロードに結構時間がかるので、辛抱強くお持ちください。。。

```python
from dataloader import load_dataset

X_train, X_test, y_train, y_test = load_dataset()
print('X_train.shape:', X_train.shape)
print('X_test.shape:', X_test.shape)
```
```
X_train.shape: (47738, 720)
X_test.shape: (45349, 720)
```

### 前処理

WebQoreへ投げるにはデータが多いので、減らします。(Undersampling)
というよりも、むしろ**データが少なくても精度を出せるのがQore+QoreSDKの特徴です。**

このデータセットは正常拍動の割合が多いimbalancedなデータなので、若干扱いに注意が必要です。
まず学習データは、正常拍動と不整脈クラスを、数が少ない不整脈クラスのデータ数の1/10に合わせます。
検証データは、正常拍動と不整脈クラスの割合を保ったまま、それぞれ1/100にします。

```python

import numpy as np
from imblearn.datasets import make_imbalance

# 学習データ
_, counts = np.unique(y_train, return_counts=True)
n_samples_per_class = counts.min() // 10
X_train, y_train = make_imbalance(X_train, y_train, {0:n_samples_per_class, 1:n_samples_per_class})
print('X_train.shape:', X_train.shape)
# 検証データ
_, counts = np.unique(y_test, return_counts=True)
X_test, y_test = make_imbalance(X_test, y_test, {0:counts[0]//100, 1:counts[1]//100})
print('X_test.shape:', X_test.shape)
```
```
X_train.shape: (748, 720)
X_test.shape: (453, 720)
```

Qoreへの入力はshapeが(サンプル数, 時系列長, 変量数)の配列なので、それに合わせるために時系列を小時系列に分割します。
つまり、(サンプル数, 時系列長, 小時系列長)の配列をまず作って、QoreSDKのFeaturizerで第3次元の時系列から特徴抽出をし、(サンプル数, 時系列長, 変量数)の配列に変換します。
そのための小時系列への分割です。

```python
from qore_sdk.utils import sliding_window

width = 144
stepsize = 36
X_train = sliding_window(X_train, width, stepsize)
X_test = sliding_window(X_test, width, stepsize)
print('X_train.shape:', X_train.shape)
print('X_test.shape:', X_test.shape)
```
```
X_train.shape: (748, 17, 144)
X_test.shape: (453, 17, 144)
```

### 特徴抽出

QoreSDKのFeaturizerを使って特徴抽出をし、(サンプル数, 時系列長, 変量数)の配列に変換します。

```python
from qore_sdk.featurizer import Featurizer

n_filters = 40
featurizer = Featurizer(n_filters)
X_train = featurizer.featurize(X_train, axis=2)
X_test = featurizer.featurize(X_test, axis=2)
print('X_train.shape:', X_train.shape)
print('X_test.shape:', X_test.shape)
```
```
X_train.shape: (748, 17, 40)
X_test.shape: (453, 17, 40)
```

### 学習

発行したアカウント情報を代入して、学習してください。
**学習が数秒、遅くとも10秒以内で終わるはずです。**

```python
from qore_sdk.client import WebQoreClient

username = ''
password = ''
endpoint = ''

client = WebQoreClient(username, password, endpoint=endpoint)
client.classifier_train(X_train, y_train)
```

### 推論

推論結果は以下のようになりました。
**データが少ないにもかかわらず高い精度**を出せていると思います！

```python
from sklearn.metrics import classification_report

res = client.classifier_predict(X_test)
report = classification_report(y_test, res['Y'])
print(report)
```
```
              precision    recall  f1-score   support

           0       0.99      0.94      0.96       421
           1       0.53      0.94      0.67        32

    accuracy                           0.94       453
   macro avg       0.76      0.94      0.82       453
weighted avg       0.96      0.94      0.94       453
```

参考までに、先程の講義資料の中でDeep Learningを使った場合の結果を載せておきます。
Deep Learningのほうでは、データをフルに使って、学習により長い時間をかけていることを考えると、Qoreの**少量データで高速学習**という特徴の有用性がわかるのではないでしょうか。

```
              precision    recall  f1-score   support

      Normal       0.99      0.97      0.98     42149
         VEB       0.67      0.93      0.78      3200

   micro avg       0.96      0.96      0.96     45349
   macro avg       0.83      0.95      0.88     45349
weighted avg       0.97      0.96      0.97     45349
```

ちなみに、Qoreで全データを使うと、上記の**Deep Learningの結果を超えます！**

```
              precision    recall  f1-score   support

           0       1.00      0.97      0.98     42149
           1       0.73      0.94      0.82      3200

   micro avg       0.97      0.97      0.97     45349
   macro avg       0.86      0.96      0.90     45349
weighted avg       0.98      0.97      0.97     45349
```