Saya sedang memproses array 3D besar, yang sering kali perlu saya potong dengan berbagai cara untuk melakukan berbagai analisis data. "Kubus" biasa bisa berukuran ~ 100 GB (dan kemungkinan akan bertambah besar di masa mendatang)
Tampaknya format file tipikal yang direkomendasikan untuk kumpulan data besar dengan python adalah menggunakan HDF5 (baik h5py atau pytables). Pertanyaan saya adalah: apakah ada manfaat kecepatan atau penggunaan memori untuk menggunakan HDF5 untuk menyimpan dan menganalisis kubus ini daripada menyimpannya dalam file biner datar sederhana? Apakah HDF5 lebih sesuai untuk data tabular, dibandingkan dengan array besar seperti yang saya kerjakan? Saya melihat bahwa HDF5 dapat memberikan kompresi yang bagus, tetapi saya lebih tertarik pada kecepatan pemrosesan dan menangani overflow memori.
Saya sering ingin menganalisis hanya satu bagian besar dari kubus. Salah satu kelemahan dari pytables dan h5py adalah bahwa ketika saya mengambil sepotong array, saya selalu mendapatkan kembali array yang numpy, menggunakan memori. Namun, jika saya mengiris memmap dari file biner datar, saya bisa mendapatkan tampilan, yang menyimpan data di disk. Jadi, tampaknya saya dapat lebih mudah menganalisis sektor tertentu dari data saya tanpa membebani memori saya.
Saya telah menjelajahi pytables dan h5py, dan sejauh ini belum melihat manfaat keduanya untuk tujuan saya.
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.type(cube)
memberih5py._hl.dataset.Dataset
. Saattype(cube[0:1,:,:])
memberinumpy.ndarray
.Jawaban:
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 dih5py
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.) Lihath5py
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
ataupytables
. Hal ini dimungkinkan denganh5py
(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 melaluimemmap
, 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 darichunked
: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 sayapytables
, sebaliknya). Namun, jika Anda menentukanchunks=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
memberitahuh5py
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 tipenp.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.
sumber