web-dev-qa-db-ja.com

(フラットバイナリファイルの代わりに)大規模アレイストレージにHDF5を使用することで、分析速度またはメモリ使用量の利点はありますか?

私は大規模な3Dアレイを処理しています。これは、さまざまなデータ分析を行うために、さまざまな方法でスライスする必要があります。典型的な「キューブ」は最大100GBです(将来的にはさらに大きくなる可能性があります)

pythonの大規模なデータセットに推奨される典型的なファイル形式は、HDF5(h5pyまたはpytables)を使用することです。これらのキューブを単純なフラットバイナリファイルに格納して分析しますか?HDF5は、作業中のような大きな配列とは対照的に、表形式データに適していますか?HDF5はニース圧縮を提供できることがわかりますが、処理速度に興味がありますそして、メモリオーバーフローに対処します。

キューブの1つの大きなサブセットのみを分析したいことがよくあります。 pytablesとh5pyの両方の欠点の1つは、配列のスライスを取得すると、常にメモリを消費してnumpy配列を取得することです。ただし、フラットバイナリファイルのnumpy memmapをスライスすると、データをディスクに保持するビューを取得できます。したがって、メモリを使い果たすことなく、データの特定のセクターをより簡単に分析できるようです。

私はpytablesとh5pyの両方を調査しましたが、これまでのところ、どちらの利点も私の目的には見ていません。

77
Caleb

HDF5の利点:組織、柔軟性、相互運用性

HDF5の主な利点のいくつかは、階層構造(フォルダー/ファイルに似ています)、各アイテムとともに保存されるオプションの任意のメタデータ、およびその柔軟性(圧縮など)です。この組織構造とメタデータストレージは簡単に思えるかもしれませんが、実際には非常に便利です。

HDFのもう1つの利点は、データセットを固定サイズまたは柔軟にサイズ変更できることです。したがって、新しいコピー全体を作成することなく、大きなデータセットにデータを簡単に追加できます。

さらに、HDF5はほぼすべての言語で使用可能なライブラリを備えた標準形式であるため、Matlab、Fortran、R、C、およびPythonとの間でディスク上のデータを共有することはHDFで非常に簡単です。 (公平を期すために、CとFの順序を認識し、格納された配列の形状、dtypeなどを知っている限り、大きなバイナリ配列でもそれほど難しくありません。)

大規模アレイに対するHDFの利点:任意のスライスの高速I/O

TL/DRと同様:〜8GB 3Dアレイの場合、任意の軸に沿った「フル」スライスの読み取りには、チャンクHDF5データセットで〜20秒かかりました、0.3秒(ベストケース)からまで3時間(最悪のケース)で、同じデータのマップされた配列の場合。

上記のものに加えて、HDF5などの「チャンク」*ディスク上のデータ形式には別の大きな利点があります。ディスク上のデータはより連続しているため、通常、任意のスライスの読み取り(任意の強調)平均。

*(HDF5はチャンクデータ形式である必要はありません。チャンクをサポートしますが、それを必要としません。実際、h5pyでデータセットを作成するデフォルトは、正しく思い出せばチャンクではありません。)

基本的に、データセットの特定のスライスに対する最適なディスク読み取り速度と最悪の場合のディスク読み取り速度は、チャンクHDFデータセットでかなり近くなります(妥当なチャンクサイズを選択するか、ライブラリに選択させると仮定します)。単純なバイナリ配列を使用すると、ベストケースは高速になりますが、ワーストケースはmuchより悪くなります。

1つの注意点として、SSDを使用している場合は、読み取り/書き込み速度の大きな違いに気付かないでしょう。ただし、通常のハードドライブでは、シーケンシャルリードはランダムリードよりもはるかに高速です。 (つまり、通常のハードドライブはseek時間が長い。)HDFはSSDでの優位性をまだ持っていますが、生の速度よりも他の機能(たとえば、メタデータ、組織など)によります。


まず、混乱を解消するために、h5pyデータセットにアクセスすると、numpy配列とほぼ同様に動作するオブジェクトが返されますが、スライスされるまでデータはメモリにロードされません。 (memmapに似ていますが、同一ではありません。)詳細については、 h5py Introduction をご覧ください。

データセットをスライスすると、データのサブセットがメモリに読み込まれますが、おそらくそれを使用して何かをしたいので、とにかくメモリ内で必要になります。

コア外の計算を行いたい場合は、pandasまたはpytablesを使用して表形式のデータを簡単に作成できます。 h5py(大きなN-Dアレイには適しています)を使用することもできますが、タッチの下位レベルにドロップダウンして、反復を自分で処理する必要があります。

ただし、numpyのようなコア外計算の未来はBlazeです。 見てみてください 本当にそのルートを取りたいのなら。


「変更されていない」ケース

最初に、ディスクに書き込まれた3D C配列の配列を考えます(arr.ravel()を呼び出して結果を出力し、より見やすくするためにシミュレートします)。

In [1]: import numpy as np

In [2]: arr = np.arange(4*6*6).reshape(4,6,6)

In [3]: arr
Out[3]:
array([[[  0,   1,   2,   3,   4,   5],
        [  6,   7,   8,   9,  10,  11],
        [ 12,  13,  14,  15,  16,  17],
        [ 18,  19,  20,  21,  22,  23],
        [ 24,  25,  26,  27,  28,  29],
        [ 30,  31,  32,  33,  34,  35]],

       [[ 36,  37,  38,  39,  40,  41],
        [ 42,  43,  44,  45,  46,  47],
        [ 48,  49,  50,  51,  52,  53],
        [ 54,  55,  56,  57,  58,  59],
        [ 60,  61,  62,  63,  64,  65],
        [ 66,  67,  68,  69,  70,  71]],

       [[ 72,  73,  74,  75,  76,  77],
        [ 78,  79,  80,  81,  82,  83],
        [ 84,  85,  86,  87,  88,  89],
        [ 90,  91,  92,  93,  94,  95],
        [ 96,  97,  98,  99, 100, 101],
        [102, 103, 104, 105, 106, 107]],

       [[108, 109, 110, 111, 112, 113],
        [114, 115, 116, 117, 118, 119],
        [120, 121, 122, 123, 124, 125],
        [126, 127, 128, 129, 130, 131],
        [132, 133, 134, 135, 136, 137],
        [138, 139, 140, 141, 142, 143]]])

以下の行4に示すように、値はディスク上に順番に保存されます。 (ここでは、ファイルシステムの詳細と断片化を無視しましょう。)

In [4]: arr.ravel(order='C')
Out[4]:
array([  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,
        13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,
        26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,
        39,  40,  41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,
        52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  62,  63,  64,
        65,  66,  67,  68,  69,  70,  71,  72,  73,  74,  75,  76,  77,
        78,  79,  80,  81,  82,  83,  84,  85,  86,  87,  88,  89,  90,
        91,  92,  93,  94,  95,  96,  97,  98,  99, 100, 101, 102, 103,
       104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
       117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
       130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])

最良のシナリオでは、最初の軸に沿ってスライスを取りましょう。これらは配列の最初の36個の値にすぎないことに注意してください。これはvery高速読み取りになります! (シーク1回、読み取り1回)

In [5]: arr[0,:,:]
Out[5]:
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23],
       [24, 25, 26, 27, 28, 29],
       [30, 31, 32, 33, 34, 35]])

同様に、最初の軸に沿った次のスライスは、次の36個の値になります。この軸に沿ってスライス全体を読み取るには、seek操作が1つだけ必要です。この軸に沿ったさまざまなスライスのみを読み取る場合、これは完全なファイル構造です。

ただし、最悪のシナリオを考えてみましょう。最後の軸に沿ったスライスです。

In [6]: arr[:,:,0]
Out[6]:
array([[  0,   6,  12,  18,  24,  30],
       [ 36,  42,  48,  54,  60,  66],
       [ 72,  78,  84,  90,  96, 102],
       [108, 114, 120, 126, 132, 138]])

このスライスを読み込むには、すべての値がディスク上で分離されているため、36シークと36リードが必要です。それらのどれも隣接していません!

これはかなりマイナーに思えるかもしれませんが、配列が大きくなるにつれて、seek操作の数とサイズが急速に増加します。この方法で保存され、memmapを介して読み込まれる大容量(〜10Gb)の3D配列の場合、「最悪」の軸に沿ったスライス全体の読み取りは、最新のハードウェアでも簡単に数十分かかります。同時に、最適な軸に沿ったスライスの所要時間は1秒未満です。簡単にするために、単一の軸に沿って「フル」スライスのみを表示していますが、データのサブセットの任意のスライスでもまったく同じことが起こります。

ちなみに、これを利用していくつかのファイル形式があり、基本的にディスク上のhuge3D配列の3つのコピーを保存します:1つはCオーダー、もう1つはFオーダー、 2つの中間。 (この例はGeoprobeのD3D形式ですが、どこに文書化されているかはわかりません。)最終的なファイルサイズが4TBであるかどうかは誰にとっても気になりません。それについてのクレイジーなことは、主なユースケースが各方向に単一のサブスライスを抽出しているため、あなたがしたい読み取りが非常に、非常に速いということです。とてもうまくいきます!


単純な「チャンク」ケース

3D配列の2x2x2の「チャンク」をディスク上の連続ブロックとして保存するとします。つまり、次のようなものです。

nx, ny, nz = arr.shape
slices = []
for i in range(0, nx, 2):
    for j in range(0, ny, 2):
        for k in range(0, nz, 2):
            slices.append((slice(i, i+2), slice(j, j+2), slice(k, k+2)))

chunked = np.hstack([arr[chunk].ravel() for chunk in slices])

したがって、ディスク上のデータはchunkedのようになります。

array([  0,   1,   6,   7,  36,  37,  42,  43,   2,   3,   8,   9,  38,
        39,  44,  45,   4,   5,  10,  11,  40,  41,  46,  47,  12,  13,
        18,  19,  48,  49,  54,  55,  14,  15,  20,  21,  50,  51,  56,
        57,  16,  17,  22,  23,  52,  53,  58,  59,  24,  25,  30,  31,
        60,  61,  66,  67,  26,  27,  32,  33,  62,  63,  68,  69,  28,
        29,  34,  35,  64,  65,  70,  71,  72,  73,  78,  79, 108, 109,
       114, 115,  74,  75,  80,  81, 110, 111, 116, 117,  76,  77,  82,
        83, 112, 113, 118, 119,  84,  85,  90,  91, 120, 121, 126, 127,
        86,  87,  92,  93, 122, 123, 128, 129,  88,  89,  94,  95, 124,
       125, 130, 131,  96,  97, 102, 103, 132, 133, 138, 139,  98,  99,
       104, 105, 134, 135, 140, 141, 100, 101, 106, 107, 136, 137, 142, 143])

そして、それらがarrの2x2x2ブロックであることを示すために、これらはchunkedの最初の8つの値であることに注意してください。

In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0,  1],
        [ 6,  7]],

       [[36, 37],
        [42, 43]]])

軸に沿ってスライスを読み込むには、6または9の連続したチャンク(必要なデータの2倍)を読み込み、必要な部分だけを保持します。これは、チャンクされていないバージョンの最大36シークに対して最大9シークのワーストケースです。 (ただし、最適なケースは、6回のシークとmemmapped配列の1です。)シーケンシャル読み取りはシークに比べて非常に高速であるため、任意のサブセットをメモリに読み込む時間を大幅に削減します。繰り返しますが、この効果は配列が大きいほど大きくなります。

HDF5は、これをさらに数歩進めます。チャンクは連続して保存する必要はなく、Bツリーによってインデックスが付けられます。さらに、それらはディスク上で同じサイズである必要はないため、各チャンクに圧縮を適用できます。


h5pyを使用したチャンク配列

デフォルトでは、h5pyはディスク上にチャンクHDFファイルを作成しません(対照的に、pytablesは作成すると思います)。ただし、データセットの作成時にchunks=Trueを指定すると、ディスク上にチャンク配列が作成されます。

簡単な最小限の例として:

import numpy as np
import h5py

data = np.random.random((100, 100, 100))

with h5py.File('test.hdf', 'w') as outfile:
    dset = outfile.create_dataset('a_descriptive_name', data=data, chunks=True)
    dset.attrs['some key'] = 'Did you want some metadata?'

chunks=Trueh5pyにチャンクサイズを自動的に選択するように指示することに注意してください。最も一般的なユースケースについて詳しく知っている場合は、シェイプタプルを指定することでチャンクサイズ/シェイプを最適化できます(上記の簡単な例では(2,2,2))。これにより、特定の軸に沿った読み取りをより効率的にしたり、特定のサイズの読み取り/書き込みを最適化したりできます。


I/Oパフォーマンスの比較

ポイントを強調するために、チャンク化されたHDF5データセットと同じデータを含む大きな(〜8GB)Fortran順序の3D配列からのスライスの読み取りを比較してみましょう。

すべてのOSキャッシュをクリアした を実行するたびに行ったので、「冷たい」パフォーマンスが見られます。

ファイルの種類ごとに、最初の軸に沿って「フル」xスライスで、最後の軸に沿って「フル」zスライスで読み取りをテストします。 Fortran順序のmemmapped配列の場合、「x」スライスが最悪のケースであり、「z」スライスが最良のケースです。

使用されるコードは 要点hdfファイルの作成を含む)です。ここで使用するデータを簡単に共有することはできませんが、同じ形状のゼロの配列(621, 4991, 2600)とタイプnp.uint8でシミュレートできます。

chunked_hdf.pyは次のようになります。

import sys
import h5py

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    Elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    f = h5py.File('/tmp/test.hdf5', 'r')
    return f['seismic_volume']

def z_slice(data):
    return data[:,:,0]

def x_slice(data):
    return data[0,:,:]

main()

memmapped_array.pyは似ていますが、スライスが実際にメモリにロードされることを保証するために少し複雑です(デフォルトでは、別のmemmapped配列が返されますが、これはリンゴ同士の比較ではありません)。

import numpy as np
import sys

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    Elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    big_binary_filename = '/data/nankai/data/Volumes/kumdep01_flipY.3dv.vol'
    shape = 621, 4991, 2600
    header_len = 3072

    data = np.memmap(filename=big_binary_filename, mode='r', offset=header_len,
                     order='F', shape=shape, dtype=np.uint8)
    return data

def z_slice(data):
    dat = np.empty(data.shape[:2], dtype=data.dtype)
    dat[:] = data[:,:,0]
    return dat

def x_slice(data):
    dat = np.empty(data.shape[1:], dtype=data.dtype)
    dat[:] = data[0,:,:]
    return dat

main()

最初にHDFのパフォーマンスを見てみましょう。

jofer at cornbread in ~ 
$ Sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py z
python chunked_hdf.py z  0.64s user 0.28s system 3% cpu 23.800 total

jofer at cornbread in ~ 
$ Sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py x
python chunked_hdf.py x  0.12s user 0.30s system 1% cpu 21.856 total

「フル」xスライスと「フル」zスライスは、ほぼ同じ時間(約20秒)かかります。これが8GBアレイであることを考慮すると、それほど悪くはありません。ほとんどの時間

そして、これをmemmapped配列時間と比較すると(Fortranで順序付けられています:「zスライス」が最良のケースであり、「xスライス」が最悪のケースです。):

jofer at cornbread in ~ 
$ Sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py z
python memmapped_array.py z  0.07s user 0.04s system 28% cpu 0.385 total

jofer at cornbread in ~ 
$ Sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py x
python memmapped_array.py x  2.46s user 37.24s system 0% cpu 3:35:26.85 total

はい、あなたはその権利を読みます。一方のスライス方向で0.3秒、もう一方の方向で〜3.5hours

「x」方向にスライスする時間は、8GBアレイ全体をメモリにロードして必要なスライスを選択するのにかかる時間よりもfar長くなります。 (繰り返しますが、これはFortran配列の配列です。逆のx/zスライスのタイミングは、C配列の配列の場合です。)

ただし、常にベストケースの方向に沿ってスライスを取得したい場合、ディスク上の大きなバイナリ配列は非常に優れています。 (〜0.3秒!)

マッピングされたアレイを使用すると、このI/Oの不一致に悩まされます(または異方性の方が適切な用語かもしれません)。ただし、チャンク化されたHDFデータセットでは、アクセスが等しいか、特定のユースケースに最適化されるようにチャンクサイズを選択できます。これにより、柔軟性が大幅に向上します。

要約すれば

それがあなたの質問の一部を、とにかく解決するのに役立つことを願っています。 HDF5には、「未加工」のmemmapよりも多くの利点がありますが、ここですべてを拡張する余地はありません。圧縮により、処理速度が向上します(使用するデータは圧縮の恩恵を受けないため、ほとんど使用しません)。また、OSレベルのキャッシュは、「生の」memmapよりもHDF5ファイルでより適切に再生されることがよくあります。それを超えて、HDF5は本当に素晴らしいコンテナ形式です。データを管理する上で大きな柔軟性が得られ、多かれ少なかれあらゆるプログラミング言語から使用できます。

全体として、試してみて、ユースケースに適しているかどうかを確認してください。驚くかもしれません。

133
Joe Kington