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
작업 의 수와 크기가 빠르게 증가합니다. 이런 방식으로 저장되고 다음을 통해 읽는 대규모 (~ 10Gb) 3D 어레이의 경우memmap
읽는 경우 "최악의"축을 따라 전체 슬라이스를 읽는 데는 최신 하드웨어를 사용하더라도 수십 분이 걸릴 수 있습니다. 동시에 최상의 축을 따라 슬라이스하는 데 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?'
참고 chunks=True
알려줍니다 h5py
자동으로 우리를 위해 청크 크기를 선택 할 수 있습니다. 가장 일반적인 사용 사례에 대해 더 많이 알고 있다면 모양 튜플을 지정하여 청크 크기 / 모양을 최적화 할 수 있습니다 (예 : (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-slice"가 가장 좋은 경우이고 "x-slice"가 가장 나쁜 경우입니다.)
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.5 시간 .
"x"방향으로 슬라이스하는 시간 은 전체 8GB 어레이를 메모리에로드하고 원하는 슬라이스를 선택하는 데 걸리는 시간보다 훨씬 깁니다! (다시 말하지만, 이것은 Fortran 순서 배열입니다. 반대 x / z 슬라이스 타이밍은 C 순서 배열의 경우입니다.)
그러나 항상 최선의 방향으로 슬라이스를 취하고 싶다면 디스크의 큰 바이너리 배열이 매우 좋습니다. (~ 0.3 초!)
memmapped 어레이를 사용하면이 I / O 불일치가 발생합니다 (또는 이방성이 더 나은 용어 일 수 있음). 그러나 청크 된 HDF 데이터 세트를 사용하면 액세스가 동일하거나 특정 사용 사례에 최적화되도록 청크 크기를 선택할 수 있습니다. 훨씬 더 많은 유연성을 제공합니다.
요약해서 말하자면
어쨌든 이것이 질문의 한 부분을 해결하는 데 도움이되기를 바랍니다. HDF5는 "원시"memmap에 비해 다른 많은 장점이 있지만 여기에서는 모두 확장 할 여지가 없습니다. 압축은 일부 작업의 속도를 높일 수 있으며 (내가 작업하는 데이터는 압축의 이점이별로 없기 때문에 거의 사용하지 않습니다.) OS 수준 캐싱은 "원시"memmap보다 HDF5 파일에서 더 잘 재생되는 경우가 많습니다. 그 외에도 HDF5는 정말 환상적인 컨테이너 형식입니다. 데이터 관리에 많은 유연성을 제공하며 어느 프로그래밍 언어에서나 사용할 수 있습니다.
전반적으로 시도해보고 사용 사례에 잘 맞는지 확인하십시오. 깜짝 놀랄 것 같아요.
h5py
보다 귀하의 데이터 세트에 더 적합합니다pytables
. 또한 메모리 내 numpy 배열을 반환h5py
하지 않습니다 . 대신 하나처럼 동작하지만 메모리에로드되지 않는 무언가를 반환합니다 (memmapped
배열 과 유사 ). 나는 더 완전한 답변을 작성하고 있지만 (완료하지 않을 수도 있음) 그 동안이 의견이 약간 도움이되기를 바랍니다.