【HDF5】Virtual Datasets(VDS)ファイルの読み込み時間

## はじめに
この記事では、HDF5の機能の1つであるVirtual Datasets（VDS）を使って複数のファイルを1つのHDF5ファイルにまとめてデータを読み込む手順を紹介し、サンプルデータに対して読み込み時間を計測した結果について述べる。

結論からいうと、**VDSを使わない方が10倍以上速かった**。なので、複数ファイルを読み込むときにはこの機能を使わない方がいいんじゃないかと思っている。

環境は以下の通り。

+ macOS Mojave 10.14.6
+ python 3.7.3
+ hdf5 1.10.4
+ h5py 2.9.0
+ jupyter 4.4.0

## HDF5って？
バイナリデータのファイルフォーマットの1つで、名前（Hierarchical Data Format）の通り、階層構造を持たせることで必要なデータのみにアクセスすることができ、大規模データでも高速に読み書きができるそうだ（データ圧縮や並列処理にも対応）。

HDF5の基本的な内容については以下の記事などが大変参考になる。

+ [Pythonを使いHDFファイルの階層構造を把握してデータを読み込む](https://qiita.com/skotaro/items/873507dc8f8f967bbc03)
+ [意外と奥が深い、HDFの世界（Python・h5py入門）](https://qiita.com/simonritchie/items/23db8b4cb5c590924d95)

pythonでは[h5py](http://docs.h5py.org/en/stable/)というライブラリで簡単にHDF5ファイルを扱うことができる。
pandasやdask, vaexなどでもサポートしており、結構標準的なフォーマットっぽい（起源を遡ると30年前くらいからある模様。ついこの間まで知らなかった俺はモグリだぁ...）。

## VDSって？
[Virtual Datasets（VDS）](http://docs.h5py.org/en/stable/vds.html)は巨大なHDF5ファイルや複数に分割されたHDF5ファイルなどから必要な部分だけを抜き出した仮想的なデータセットを作ることができる機能である。[公式のサンプルコード](https://github.com/h5py/h5py/blob/master/examples/vds_simple.py)を見てもらえばどういうことができるかはすぐにわかると思う。

使い方も簡単で、設定の流れは以下の通り（これもサンプルコードを見てもらった方が早い）。

+ 読み込みたいデータに合わせた容れ物となる配列（virtual layout）を準備
+ 読み込みたいHDF5ファイルのデータをvirtual sourceとして指定
+ layoutにvirtual sourceを格納し、VDSファイルを作成（create_virtual_dataset）

後は通常のHDF5ファイルのようにVDSファイルからデータを読み込むだけ。データの実体はオリジナルの方にあるので、VDSファイル自体の容量は元ファイルに比べてかなり小さいが、HDFViewで中身を見るときちんとデータを確認できる。

背景としては、数値計算の結果を行列にまとめて色々分析したいニーズがあり、使っているソルバーがHDF5での出力に対応していたこともり、試してみた次第である。特に結果が各時刻ごとに分割されたファイルとして出力されるため、VDSでまとめると扱いやすいんじゃないかと思った。
最初は[外部リンク](http://docs.h5py.org/en/stable/high/group.html)を使おうとしてたのだが、なぜだかうまく行かなかった（内部的に似たような機能を使っているとは思う）。

## サンプルコード
テストデータとして要素数$n = 10^{6}$の乱数ベクトルのみが保存された10個のファイル（DATAというディレクトリにまとめて入れてある）を作り、VDSファイルとしてまとめてみる。コードは以下の通り。

```python
import numpy as np
import h5py

# テストデータの作成
n = 10**6
m = 11
files = ['./DATA/data{:02}.h5'.format(i) for i in range(1,m)]
for fname in files :
    with h5py.File(fname, mode='w') as f :
        X = np.random.randn(n)
        f.create_dataset('data', data=X)

# VDSファイルの作成
layout = h5py.VirtualLayout(shape=(n,len(files)), dtype=np.float)
with h5py.File('vds.h5', mode='w') as f:
    for i, fname in enumerate(files):
        layout[:,i] = h5py.VirtualSource(fname,'data', shape=(n,))
    f.create_virtual_dataset('vds_data', layout, fillvalue=0)
```

実際のデータ量やファイル数はもっと大きいが、基本的な設定は共通しているので、結果の傾向は同じになると思う。

## 実行結果
作成したVDSファイルから再度numpy配列にデータを読み込み、かかった時間を計測した（jupyterの`%%time`または`%%timeit`が便利）。

```python
%%timeit
# VDSファイルの読み込み
with h5py.File('vds.h5', mode='r') as f:
    for i,fname in enumerate(files) :
        X = np.array(f['vds_data'][:,i])
```

結果は以下の通り。

```python
# 実行結果
878 ms ± 1.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

続いて、比較のために連番ファイルから直接データを読み込んでみる。

```python
%%timeit
# 連番ファイルの読み込み
for i,fname in enumerate(files) :
    with h5py.File(fname, mode='r') as f :
        X = np.array(f['data'][:])
```

結果は以下の通り。

```python
# 実行結果
30.9 ms ± 118 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```
...というわけで、**やってみると圧倒的にVDSの方が遅かった**（このケースでは30倍の速度差）。データ容量が大きくなるとさらに差がつきそうだったので、少なくとも、連番ファイルの読み込みという処理に関してはVDSを使う理由はなさそうだ。

当初、自分のイメージではVDSで速度が遅くなるとは思っていなかったので、最初は読み込みが変に遅くて混乱したりしていた。遠回りしたといえばそうなのだが、テストコードで検証することの大切さが学べたのでよしとしよう。

もしかしたらVDSの方が速くなる条件もあるのかもしれないが、一番やりたいことでスピードが出なかったので、おとなしく連番ファイルを読み込む方法を使うことにする。自分が知らないだけでVDSの便利な使い方もあるのかもしれない。VDS自体が現バージョン（h5py 2.9.0, HDF5 1.10）の新機能っぽいので、今後パフォーマンスの改善がなされる可能性もある。期待せずに待っていようと思う。

## まとめ

分割された時系列データの連番ファイルを処理するときには素直に番号順に読み込みましょう。
同じようなことをやろうとしている人がいたら気をつけてくだされ、ってことで。
HDF5自体はとても便利なので引き続き利用してみるつもり。