대용량 어레이 스토리지 (플랫 바이너리 파일 대신)에 HDF5를 사용하면 분석 속도 또는 메모리 사용 이점이 있습니까?
다양한 데이터 분석을 수행하기 위해 다양한 방법으로 슬라이스해야하는 경우가 많은 대형 3D 어레이를 처리하고 있습니다. 일반적인 "큐브"는 ~ 100GB 일 수 있습니다 (향후 더 커질 수 있음).
파이썬의 대규모 데이터 세트에 대한 일반적인 권장 파일 형식은 HDF5 (h5py 또는 pytables)를 사용하는 것 같습니다. 내 질문은 : HDF5를 사용하여 이러한 큐브를 단순한 플랫 바이너리 파일에 저장하는 것보다 저장 및 분석하는 데 속도 나 메모리 사용 이점이 있습니까? HDF5는 내가 작업하는 것과 같은 대형 어레이와 달리 테이블 형식 데이터에 더 적합합니까? HDF5가 좋은 압축을 제공 할 수 있지만 처리 속도와 메모리 오버플로 처리에 더 관심이 있습니다.
나는 종종 큐브의 큰 하위 집합을 하나만 분석하고 싶습니다. pytables와 h5py의 한 가지 단점은 배열의 한 조각을 가져 오면 메모리를 사용하여 항상 numpy 배열을 다시 얻는다는 것입니다. 그러나 플랫 바이너리 파일의 numpy memmap을 슬라이스하면 데이터를 디스크에 보관하는 뷰를 얻을 수 있습니다. 따라서 내 데이터의 특정 섹터를 메모리를 초과하지 않고 더 쉽게 분석 할 수있는 것 같습니다.
나는 pytables와 h5py를 모두 탐색했으며 지금까지 내 목적을 위해 어느 쪽의 이점도 보지 못했습니다.
HDF5 장점 : 조직, 유연성, 상호 운용성
HDF5의 주요 장점 중 일부는 계층 구조 (폴더 / 파일과 유사), 각 항목에 저장된 선택적 임의 메타 데이터 및 유연성 (예 : 압축)입니다. 이 조직 구조와 메타 데이터 저장은 사소하게 들릴 수 있지만 실제로는 매우 유용합니다.
HDF의 또 다른 장점은 데이터 세트가 고정 된 크기 이거나 유연한 크기 일 수 있다는 것 입니다. 따라서 전체 새 사본을 만들지 않고도 대규모 데이터 세트에 데이터를 쉽게 추가 할 수 있습니다.
또한 HDF5는 거의 모든 언어에 사용할 수있는 라이브러리가 포함 된 표준화 된 형식이므로 Matlab, Fortran, R, C 및 Python간에 디스크상의 데이터를 공유하는 것이 HDF를 사용하면 매우 쉽습니다. (공평하게 말하면, C 대 F 순서를 알고 있고 저장된 배열의 모양, dtype 등을 알고있는 한 큰 이진 배열도 그렇게 어렵지 않습니다.)
대형 어레이를위한 HDF 이점 : 임의 슬라이스의 더 빠른 I / O
TL / DR과 마찬가지로 : ~ 8GB 3D 어레이의 경우 모든 축을 따라 "전체"슬라이스를 읽는 데 청크 된 HDF5 데이터 세트로 약 20 초가 걸렸고, 0.3 초 (최상의 경우)에서 3 시간 이상 (최악의 경우)이 소요 되었습니다. 동일한 데이터의 memmapped 배열.
위에 나열된 것 외에도 HDF5와 같은 "청크 된"* 온 디스크 데이터 형식의 또 다른 큰 이점이 있습니다. 평균.
*
(HDF5는 청크 데이터 형식 일 필요는 없습니다. 청크를 지원하지만 필요하지 않습니다. 실제로 데이터 세트를 생성하는 기본값은 h5py
제가 올바르게 기억하면 청크가 아닙니다.)
기본적으로 데이터 세트의 특정 슬라이스에 대한 최상의 디스크 읽기 속도와 최악의 디스크 읽기 속도는 청크 된 HDF 데이터 세트와 상당히 비슷합니다 (적당한 청크 크기를 선택하거나 라이브러리에서 선택하도록 가정). 간단한 이진 배열을 사용하면 최상의 경우가 더 빠르지 만 최악의 경우는 훨씬 더 나쁩니다.
한 가지주의 할 점은 SSD가 있다면 읽기 / 쓰기 속도에 큰 차이를 느끼지 못할 것입니다. 그러나 일반 하드 드라이브에서는 순차적 읽기가 임의 읽기보다 훨씬 빠릅니다. (예 : 일반 하드 드라이브는 seek
시간 이 오래 걸립니다.) HDF는 여전히 SSD에서 장점이 있지만 원시 속도보다는 다른 기능 (예 : 메타 데이터, 구성 등) 때문입니다.
먼저 혼란을 없애기 위해 h5py
데이터 세트에 액세스하면 numpy 배열과 상당히 유사하게 작동하지만 슬라이스 될 때까지 데이터를 메모리로로드하지 않는 객체가 반환됩니다. (memmap과 유사하지만 동일하지는 않습니다.) 자세한 내용 은 h5py
소개 를 참조하십시오.
데이터 세트를 슬라이스하면 데이터의 하위 집합이 메모리에로드되지만, 아마도이를 사용하여 무언가를하고 싶을 것입니다. 그 시점에서 어쨌든 메모리에 필요합니다.
당신이 밖으로의 핵심 계산을 수행 할 경우, 당신은 비교적 쉽게와 테이블 형식의 데이터에 대한 수 pandas
또는 pytables
. 그것은 가능합니다 h5py
(큰 ND 배열에 대한 좋네요),하지만 당신은 자신을 터치 낮은 수준 아래로 삭제하고 반복을 처리해야합니다.
그러나 numpy와 같은 out-of-core 계산의 미래는 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 개 값에 불과합니다. 이것은 매우 빨리 읽을 것입니다! (하나의 검색, 하나의 읽기)
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
작업 만 필요 합니다. 우리가 읽을 모든 것이이 축을 따라 다양한 슬라이스라면, 이것이 완벽한 파일 구조입니다.
그러나 최악의 시나리오 인 마지막 축을 따른 슬라이스를 고려해 보겠습니다.
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 초도 걸리지 않습니다. 단순화를 위해 단일 축을 따라 "전체"조각 만 표시하지만 데이터 하위 집합의 임의 조각에서도 똑같은 일이 발생합니다.
우연히도 이것을 활용하고 기본적으로 디스크 에 거대한 3D 어레이 의 세 복사본을 저장하는 여러 파일 형식이 있습니다 . 하나는 C 순서, 하나는 F 순서, 하나는 둘 사이의 중간에 있습니다. (이것의 예는 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])
그리고 그것들이의 2x2x2 블록이라는 것을 보여주기 위해 arr
, 이것들이 다음의 처음 8 개 값임을 주목하십시오 chunked
:
In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0, 1],
[ 6, 7]],
[[36, 37],
[42, 43]]])
축을 따라 슬라이스를 읽으려면 6 개 또는 9 개의 연속 청크 (필요한 데이터의 두 배)를 읽은 다음 원하는 부분 만 유지합니다. 이는 청크되지 않은 버전의 경우 최대 36 개의 검색에 비해 최악의 경우 최대 9 개의 검색입니다. (그러나 가장 좋은 경우는 여전히 memmapped 배열의 경우 6 개의 검색 대 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?'
Note that chunks=True
tells h5py
to automatically pick a chunk size for us. If you know more about your most common use-case, you can optimize the chunk size/shape by specifying a shape tuple (e.g. (2,2,2)
in the simple example above). This allows you to make reads along a particular axis more efficient or optimize for reads/writes of a certain size.
I/O Performance comparison
Just to emphasize the point, let's compare reading in slices from a chunked HDF5 dataset and a large (~8GB), Fortran-ordered 3D array containing the same exact data.
I've cleared all OS caches between each run, so we're seeing the "cold" performance.
For each file type, we'll test reading in a "full" x-slice along the first axis and a "full" z-slize along the last axis. For the Fortran-ordered memmapped array, the "x" slice is the worst case, and the "z" slice is the best case.
The code used is in a gist (including creating the hdf
file). I can't easily share the data used here, but you could simulate it by an array of zeros of the same shape (621, 4991, 2600)
and type np.uint8
.
The chunked_hdf.py
looks like this:
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
is similar, but has a touch more complexity to ensure the slices are actually loaded into memory (by default, another memmapped
array would be returned, which wouldn't be an apples-to-apples comparison).
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()
Let's have a look at the HDF performance first:
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
A "full" x-slice and a "full" z-slice take about the same amount of time (~20sec). Considering this is an 8GB array, that's not too bad. Most of the time
And if we compare this to the memmapped array times (it's Fortran-ordered: A "z-slice" is the best case and an "x-slice" is the worst case.):
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
Yes, you read that right. 0.3 seconds for one slice direction and ~3.5 hours for the other.
The time to slice in the "x" direction is far longer than the amount of time it would take to load the entire 8GB array into memory and select the slice we wanted! (Again, this is a Fortran-ordered array. The opposite x/z slice timing would be the case for a C-ordered array.)
However, if we're always wanting to take a slice along the best-case direction, the big binary array on disk is very good. (~0.3 sec!)
With a memmapped array, you're stuck with this I/O discrepancy (or perhaps anisotropy is a better term). However, with a chunked HDF dataset, you can choose the chunksize such that access is either equal or is optimized for a particular use-case. It gives you a lot more flexibility.
In summary
Hopefully that helps clear up one part of your question, at any rate. HDF5 has many other advantages over "raw" memmaps, but I don't have room to expand on all of them here. Compression can speed some things up (the data I work with doesn't benefit much from compression, so I rarely use it), and OS-level caching often plays more nicely with HDF5 files than with "raw" memmaps. Beyond that, HDF5 is a really fantastic container format. It gives you a lot of flexibility in managing your data, and can be used from more or less any programming language.
Overall, try it and see if it works well for your use case. I think you might be surprised.
'code' 카테고리의 다른 글
Maven : 인수를 전달하는 명령 줄에서 .java 파일을 실행하는 방법 (0) | 2020.10.10 |
---|---|
제약 조건을 변경하는 방법 (0) | 2020.10.10 |
Django의 FileField를 기존 파일로 설정 (0) | 2020.10.10 |
코드가 꼬리 호출 최적화를 적극적으로 방지하려는 이유는 무엇입니까? (0) | 2020.10.10 |
Eclipse를 Indigo에서 Juno로 업그레이드하는 설정을 유지할 수 있습니까? (0) | 2020.10.10 |