【PyTorch】メモリに乗り切らない大規模データのためのHDF5操作

よくわからなかったので忘れないように...


# HDF5はなに？
* HDF = Hierarchical Data Format 階層構造によってデータを管理できるファイルフォーマット
* たくさんのいろんな種類のデータをひとつのファイルで管理できて便利
* データ格納時に圧縮できるオプションもあるので直接ディレクトリでデータを扱うより小さくなることも
* またPythonでもNumPyやPandasみたいに扱える
* HDF開発団体からビュワーアプリも公開されていて扱いやすい
* **なによりメモリに乗り切らないほどの大量のデータも超高速に読み込むことができます**

# HDF5とPyTorch？
ディレクトリとそのデータとして扱えるので機械学習用のデータを管理するのにとっても都合がいいだけでなく、HDF5形式で扱うとデータが大規模過ぎてメモリにのらないよ～、データの読み書きに時間がかかりすぎてキレそうだよ～といったときにも、一つのデータごとにメモリに高速で載せてくれたり便利。

# 使ってみる
## 既存データセットをHDF5に変換する
とりあえず、私の手元にあるデータセットをHDF形式に変換してみます。
次のようなデータ構造で保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543519/cf7cec88-73a4-486b-5b92-5610444fb73c.png)
よくある感じのやつです。
各フォルダの中にはデータ形32000のwavファイルが入っています。
名前は`1.wav, 2.wav ....`といった感じに入っていて、データとラベルには同じ名前で格納されています。
これをそのままHDF5に変換してみます。

```hdf5.py
import os
import numpy as np
import h5py
from librosa import load

# データパス
dir_train=r"C:\dataset\data_train"
dir_test=r"C:\dataset\data_test"
hdf5_path="dataset.hdf5"

# hdf5を作って開きます
with h5py.File(hdf5_path,'w') as h5:
    # グループ（ディレクトリ）を作ります
    group_train_data = h5.create_group("train_data")
    group_train_label = h5.create_group("train_label")
    group_test_data = h5.create_group("test_data")
    group_test_label = h5.create_group("test_label")

    # 作ったディレクトリにデータを放り込んでいきます
    for i in range(10000):
        # wavファイルを読み込んで...
        d_label,_=load(os.path.join(dir_train,"label",str(i)+".wav",sr=32000,mono=True)
        d_data,_=load(os.path.join(dir_train,"data",str(i)+".wav",sr=32000,mono=True)
        # ディレクトリに追加
        ds_train_data = group_train_data.create_dataset(str(i),data=d_data,shape=(64000,),compression='gzip',chunks=True)
        ds_train_label= group_train_label.create_dataset(str(i),data=d_label,shape=(64000,),compression='gzip',chunks=True)
    # Add Test Dataset
    for i in range(5000):
        d_data,_=load(os.path.join(dir_test,"data",str(i)+".wav",sr=32000,mono=True)
        d_label,_=load(os.path.join(dir_test,"label",str(i)+".wav",sr=32000,mono=True)
        ds_test_data = group_test_data.create_dataset(str(i),data=d_data,shape=(64000,),compression='gzip',chunks=True)
        ds_test_label= group_test_label.create_dataset(str(i),data=d_label,shape=(64000,),compression='gzip',chunks=True)
```
ここでディレクトリを作っています
```py
group_train_data = h5.create_group("train_data")
```
さらにサブディレクトリを作りたくなったときは
```py
subgroup = group_train_data.create_group("sub_train_data")
```
みたいにすると階層を増やすことができます。


ここで、入れるデータの形を教えてあげます

```create_dataset.py
group_test_data.create_dataset(名前,
　　　　　　　　　　　　　　　　　　　data=データ(オプション),
                                shape=データの形(オプション),
                                compression='圧縮方法:gzip推奨'(オプション),
                                compression_opts='圧縮レベル(int:0-9)デフォルト4',
                                chunks=True(オプション))
```
基本的にNumPyがサポートするほとんどのデータ形式に対応しているようです。（参考[FAQ](https://docs.h5py.org/en/stable/faq.html#faq)）
未確認ですがたぶんTensor形式には対応していない感じがするので、Tensor to NumPyしてあげたほうがいいかも。
dtypeやビット数も指定できます。詳しくはドキュメントを確認してみてください。
指定しなかったら自動で入れてくれます。

また、こういう書き方でもデータを追加することもできます
```py
h5["train_data/data"] = arr
h5["train_data/label"] = arr

# サブディレクトリに入れるとき
h5["train_data/sub_train_data/data"] = arr
```

## 作ったHDF5ファイルを見てみよう
HDF Viewを使えば、ファイルをブラウザできます。便利ですね。
https://www.hdfgroup.org/downloads/hdfview/

HDF5ファイルを開いてみると、きちんとできてそうですね。
また、データの形や形式、圧縮方式も一目でわかります。超便利。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543519/d07da182-9d78-4527-6ce1-30cb31deb376.png)

## PyThonで読み込んでみる

読み込みも簡単です

```read.py
import h5py
h5 =  h5py.File("dataset.hdf5", 'r')
```
各ディレクトリにアクセスしたいときはこんな感じ

```group.py
data_h5 = h5['train_data']
label_h5 = h5['test_data']
```
このディレクトリの名前のことを`Key`と呼ぶらしい。
もしどんなKeyを充てたか忘れても次のようにすると確認できる。

```key.py
h5.keys()

>> KeysViewHDF5 ['test_data', 'test_label', 'train_data', 'train_label']
```
基本的に`len()`とかスライスとか大抵のPandasやNumPyの操作には対応しているので直感的に触れます。

# PyTorchのデータセットに使う
Datasetで使いたいですよね。
カスタムデータセットは次のように作りました。
もっと賢い方法があったら教えてください。

```dataset.py
import h5py
from torch.utils.data import Dataset

class HDF5dataset(Dataset):
    def __init__(self,hdf5_path,mode="train"):
        """
        hdf5_path : "\dataset.hdf5"
        mode : "train" or "test"
        """
        self.mode=mode
        self.hdf5_path = hdf5_path
        self.dataset = h5py.File(self.hdf5_path, 'r')
        if mode=="train":
            self.data_h5 = self.dataset['train_data']
            self.label_h5 = self.dataset['train_label']
        else:
            self.data_h5 = self.dataset['test_data']
            self.label_h5 = self.dataset['test_label']

    def __len__(self):
        return len(self.data_h5)

    def __getitem__(self,idx):
        data = self.data_h5[str(idx)][0]
        label = self.label_h5[str(idx)][0]
        d=torch.from_numpy(data.astype(np.float32)).clone()
        l=torch.from_numpy(label.astype(np.float32)).clone()
        sample = {"data":d,"label":l}
        return sample

hdf5_path="dataset.hdf5"
training_data = HDF5dataset(hdf5_path,mode="train")
test_data = HDF5dataset(hdf5_path,mode="test")
```

これをデータローダーで使って...

```dataloader.py
from torch.utils.data import DataLoader

train_dataloader = DataLoader(training_data ,batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data ,batch_size=64, shuffle=True)
```
これでOKのはず

# おわりに
TensorFlowにおけるTFRecordのようなものとしてPytorchでHDF5を使用していますが、他にいいスマートな方法はないだろうか....
もっと賢いやり方があったら教えてください。頼む。