Keunggulan HDF5: Organisasi, fleksibilitas, interoperabilitas
Beberapa keunggulan utama HDF5 adalah struktur hierarkinya (mirip dengan folder / file), metadata arbitrer opsional yang disimpan dengan setiap item, dan fleksibilitasnya (misalnya kompresi). Struktur organisasi dan penyimpanan metadata ini mungkin terdengar sepele, tetapi sangat berguna dalam praktiknya.
Keuntungan lain dari HDF adalah bahwa kumpulan data dapat berukuran tetap atau berukuran fleksibel. Oleh karena itu, mudah untuk menambahkan data ke kumpulan data besar tanpa harus membuat salinan baru seluruhnya.
Selain itu, HDF5 adalah format standar dengan pustaka yang tersedia untuk hampir semua bahasa, jadi berbagi data di disk Anda antara, katakanlah Matlab, Fortran, R, C, dan Python sangat mudah dengan HDF. (Agar adil, tidak terlalu sulit dengan array biner yang besar, juga, selama Anda mengetahui urutan C vs. F dan mengetahui bentuk, tipe d, dll dari array yang disimpan.)
Keunggulan HDF untuk array besar: I / O lebih cepat dari slice arbitrer
Sama seperti TL / DR: Untuk array 3D ~ 8 GB, membaca potongan "penuh" di sepanjang sumbu apa pun membutuhkan waktu ~ 20 detik dengan kumpulan data HDF5 yang terpotong, dan 0,3 detik (kasus terbaik) hingga lebih dari tiga jam (kasus terburuk) untuk array yang dipetakan dari data yang sama.
Di luar hal-hal yang tercantum di atas, ada keuntungan besar lain dari format data pada disk yang "terpotong" * seperti HDF5: Membaca potongan sembarang (penekanan pada sembarang) biasanya akan jauh lebih cepat, karena data pada disk lebih berdekatan rata-rata.
*
(HDF5 tidak harus dalam format data chunked. Ini mendukung chunking, tetapi tidak memerlukannya. Faktanya, default untuk membuat dataset di h5py
bukanlah chunk, jika saya ingat dengan benar.)
Pada dasarnya, kecepatan pembacaan disk kasus terbaik Anda dan kecepatan pembacaan disk kasus terburuk untuk bagian tertentu dari kumpulan data Anda akan cukup dekat dengan kumpulan data HDF yang dipotong (dengan asumsi Anda memilih ukuran potongan yang wajar atau membiarkan perpustakaan memilih satu untuk Anda). Dengan array biner sederhana, kasus terbaik lebih cepat, tetapi kasus terburuk jauh lebih buruk.
Satu peringatan, jika Anda memiliki SSD, Anda kemungkinan tidak akan melihat perbedaan besar dalam kecepatan baca / tulis. Dengan hard drive biasa, pembacaan berurutan jauh lebih cepat daripada pembacaan acak. (mis. hard drive biasa memiliki seek
waktu lama .) HDF masih memiliki keunggulan pada SSD, tetapi lebih karena fitur-fiturnya yang lain (misalnya metadata, organisasi, dll) daripada karena kecepatan mentah.
Pertama, untuk menghilangkan kebingungan, mengakses h5py
set data akan mengembalikan objek yang berperilaku cukup mirip dengan array numpy, tetapi tidak memuat data ke dalam memori hingga diiris. (Mirip dengan memmap, tetapi tidak identik.) Lihat h5py
pengantar untuk informasi lebih lanjut.
Mengiris dataset akan memuat subset data ke dalam memori, tetapi mungkin Anda ingin melakukan sesuatu dengannya, pada titik mana Anda tetap membutuhkannya di memori.
Jika Anda ingin melakukan penghitungan out-of-core, Anda dapat dengan mudah menggunakan data tabel dengan pandas
atau pytables
. Hal ini dimungkinkan dengan h5py
(lebih bagus untuk array ND besar), tetapi Anda perlu turun ke tingkat yang lebih rendah dan menangani iterasi sendiri.
Namun, masa depan komputasi out-of-core numpy-like adalah Blaze. Silakan lihat jika Anda benar-benar ingin mengambil rute itu.
Kasus "belum dipotong"
Pertama, pertimbangkan array berurutan C 3D yang ditulis ke disk (saya akan mensimulasikannya dengan memanggil arr.ravel()
dan mencetak hasilnya, untuk membuatnya lebih terlihat):
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]]])
Nilai akan disimpan di disk secara berurutan seperti yang ditunjukkan pada baris 4 di bawah ini. (Mari kita abaikan detail sistem file dan fragmentasi untuk saat ini.)
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])
Dalam skenario kasus terbaik, mari kita ambil potongan di sepanjang sumbu pertama. Perhatikan bahwa ini hanyalah 36 nilai pertama dari larik. Ini akan menjadi bacaan yang sangat cepat! (satu pencarian, satu bacaan)
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]])
Demikian pula, potongan berikutnya di sepanjang sumbu pertama hanya akan menjadi 36 nilai berikutnya. Untuk membaca potongan lengkap sepanjang sumbu ini, kita hanya membutuhkan satu seek
operasi. Jika semua yang akan kita baca adalah berbagai irisan di sepanjang sumbu ini, maka ini adalah struktur file yang sempurna.
Namun, mari pertimbangkan skenario terburuk: Sebuah potongan di sepanjang sumbu terakhir.
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]])
Untuk membaca bagian ini, kita membutuhkan 36 pencarian dan 36 pembacaan, karena semua nilai dipisahkan pada disk. Tidak ada satupun yang berdekatan!
Ini mungkin tampak sangat kecil, tetapi saat kita mendapatkan array yang semakin besar, jumlah dan ukuran seek
operasi tumbuh dengan cepat. Untuk larik 3D berukuran besar (~ 10 Gb) yang disimpan dengan cara ini dan dibaca melalui memmap
, membaca potongan penuh di sepanjang sumbu "terburuk" dapat memakan waktu puluhan menit dengan mudah, bahkan dengan perangkat keras modern. Pada saat yang sama, irisan di sepanjang sumbu terbaik dapat memakan waktu kurang dari satu detik. Untuk kesederhanaan, saya hanya menampilkan irisan "penuh" di sepanjang sumbu tunggal, tetapi hal yang sama persis terjadi dengan irisan sembarang subset data.
Kebetulan ada beberapa format file yang memanfaatkan ini dan pada dasarnya menyimpan tiga salinan array 3D besar pada disk: satu dalam urutan-C, satu dalam urutan-F, dan satu lagi di antara keduanya. (Contoh dari ini adalah format D3D Geoprobe, meskipun saya tidak yakin itu didokumentasikan di mana pun.) Siapa yang peduli jika ukuran file akhirnya adalah 4TB, penyimpanan itu murah! Hal gila tentang itu adalah karena kasus penggunaan utama mengekstrak satu sub-irisan di setiap arah, pembacaan yang ingin Anda buat sangat, sangat cepat. Ini bekerja dengan sangat baik!
Kasus sederhana yang "dipotong"
Misalkan kita menyimpan "potongan" 2x2x2 dari larik 3D sebagai blok yang berdekatan pada disk. Dengan kata lain, sesuatu seperti:
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])
Jadi data di disk akan terlihat seperti 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])
Dan hanya untuk menunjukkan bahwa itu adalah blok 2x2x2 arr
, perhatikan bahwa ini adalah 8 nilai pertama dari chunked
:
In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0, 1],
[ 6, 7]],
[[36, 37],
[42, 43]]])
Untuk membaca dalam potongan mana pun di sepanjang sumbu, kami akan membaca 6 atau 9 bagian yang berdekatan (dua kali lebih banyak data yang kami butuhkan) dan kemudian hanya menyimpan bagian yang kami inginkan. Itu kasus terburuk maksimum 9 pencarian vs maksimum 36 pencarian untuk versi non-chunked. (Tapi kasus terbaik masih 6 pencarian vs 1 untuk array yang dipetakan.) Karena pembacaan berurutan sangat cepat dibandingkan dengan pencarian, ini secara signifikan mengurangi jumlah waktu yang diperlukan untuk membaca subset arbitrer ke dalam memori. Sekali lagi, efek ini menjadi lebih besar dengan array yang lebih besar.
HDF5 mengambil langkah ini lebih jauh. Potongan tidak harus disimpan berdekatan, dan mereka diindeks oleh B-Tree. Selain itu, ukurannya tidak harus sama di disk, jadi kompresi dapat diterapkan ke setiap bagian.
Larik terpotong dengan h5py
Secara default, h5py
tidak membuat file HDF yang dipotong pada disk (menurut saya pytables
, sebaliknya). Namun, jika Anda menentukan chunks=True
saat membuat kumpulan data, Anda akan mendapatkan larik terpotong pada disk.
Sebagai contoh cepat dan minimal:
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?'
Perhatikan bahwa chunks=True
memberitahu h5py
untuk secara otomatis memilih ukuran potongan untuk kita. Jika Anda mengetahui lebih banyak tentang kasus penggunaan Anda yang paling umum, Anda dapat mengoptimalkan ukuran / bentuk potongan dengan menetapkan tupel bentuk (misalnya (2,2,2)
dalam contoh sederhana di atas). Hal ini memungkinkan Anda untuk membuat pembacaan sepanjang sumbu tertentu lebih efisien atau mengoptimalkan pembacaan / penulisan dengan ukuran tertentu.
Perbandingan kinerja I / O
Hanya untuk menekankan intinya, mari bandingkan membaca dalam potongan dari kumpulan data HDF5 yang terpotong dan array 3D pesanan Fortran yang besar (~ 8 GB) yang berisi data persis sama.
Saya telah membersihkan semua cache OS di antara setiap proses, jadi kami melihat performa "dingin".
Untuk setiap jenis file, kami akan menguji pembacaan dalam potongan x "penuh" di sepanjang sumbu pertama dan garis miring z "penuh" di sepanjang sumbu terakhir. Untuk larik memmapped berurutan Fortran, potongan "x" adalah kasus terburuk, dan potongan "z" adalah kasus terbaik.
Kode yang digunakan ada dalam sebuah intisari (termasuk membuat hdf
file). Saya tidak dapat dengan mudah membagikan data yang digunakan di sini, tetapi Anda dapat mensimulasikannya dengan array nol dengan bentuk ( 621, 4991, 2600)
dan tipe np.uint8
.
The chunked_hdf.py
terlihat seperti ini:
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
serupa, tetapi memiliki sentuhan yang lebih rumit untuk memastikan irisan benar-benar dimuat ke dalam memori (secara default, memmapped
larik lain akan dikembalikan, yang tidak akan menjadi perbandingan apel-ke-apel).
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()
Mari kita lihat kinerja HDF terlebih dahulu:
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
Irisan-x "penuh" dan irisan-z "penuh" membutuhkan waktu yang kurang lebih sama (~ 20 detik). Mengingat ini adalah array 8GB, itu tidak terlalu buruk. Sebagian besar waktu
Dan jika kita membandingkannya dengan waktu array yang dipetakan (urutannya sesuai Fortran: "z-slice" adalah kasus terbaik dan "x-slice" adalah kasus terburuk.):
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
Ya, Anda membacanya dengan benar. 0,3 detik untuk satu arah irisan dan ~ 3,5 jam untuk yang lainnya.
Waktu untuk mengiris ke arah "x" jauh lebih lama daripada jumlah waktu yang dibutuhkan untuk memuat seluruh larik 8GB ke dalam memori dan memilih potongan yang kita inginkan! (Sekali lagi, ini adalah larik berurutan Fortran. Waktu irisan x / z yang berlawanan akan terjadi pada larik berurutan C.)
Namun, jika kita selalu ingin mengambil bagian di sepanjang arah kasus terbaik, array biner besar pada disk sangat bagus. (~ 0,3 dtk!)
Dengan array yang dipetakan, Anda terjebak dengan perbedaan I / O ini (atau mungkin anisotropi adalah istilah yang lebih baik). Namun, dengan kumpulan data HDF yang dipotong, Anda dapat memilih ukuran potongan sedemikian rupa sehingga aksesnya sama atau dioptimalkan untuk kasus penggunaan tertentu. Ini memberi Anda lebih banyak fleksibilitas.
Singkatnya
Mudah-mudahan itu membantu menjernihkan satu bagian dari pertanyaan Anda, bagaimanapun juga. HDF5 memiliki banyak keunggulan dibandingkan memmap "mentah", tetapi saya tidak memiliki ruang untuk mengembangkan semuanya di sini. Kompresi dapat mempercepat beberapa hal (data yang saya gunakan tidak mendapatkan banyak manfaat dari kompresi, jadi saya jarang menggunakannya), dan cache tingkat OS sering kali berfungsi lebih baik dengan file HDF5 daripada dengan memmaps "mentah". Selain itu, HDF5 adalah format kontainer yang sangat fantastis. Ini memberi Anda banyak fleksibilitas dalam mengelola data Anda, dan dapat digunakan kurang lebih dari bahasa pemrograman apa pun.
Secara keseluruhan, cobalah dan lihat apakah itu berfungsi dengan baik untuk kasus penggunaan Anda. Saya pikir Anda mungkin akan terkejut.
h5py
lebih cocok untuk kumpulan data seperti milik Anda daripadapytables
. Juga,h5py
tidak tidak kembali di memori array yang numpy. Sebaliknya ia mengembalikan sesuatu yang berperilaku seperti itu, tetapi tidak dimuat ke dalam memori (mirip denganmemmapped
array). Saya sedang menulis jawaban yang lebih lengkap (mungkin tidak menyelesaikannya), tapi semoga komentar ini sedikit membantu sementara itu.