Vantaggi di HDF5: organizzazione, flessibilità, interoperabilità
Alcuni dei principali vantaggi di HDF5 sono la sua struttura gerarchica (simile a cartelle / file), metadati arbitrari opzionali memorizzati con ogni elemento e la sua flessibilità (ad esempio compressione). Questa struttura organizzativa e l'archiviazione dei metadati possono sembrare banali, ma sono molto utili nella pratica.
Un altro vantaggio di HDF è che i set di dati possono essere di dimensioni fisse o flessibili. Pertanto, è facile aggiungere dati a un set di dati di grandi dimensioni senza dover creare un'intera nuova copia.
Inoltre, HDF5 è un formato standardizzato con librerie disponibili per quasi tutte le lingue, quindi condividere i dati su disco tra, ad esempio Matlab, Fortran, R, C e Python è molto semplice con HDF. (Per essere onesti, non è troppo difficile anche con un grande array binario, purché tu sia a conoscenza dell'ordinamento C contro F e conosci la forma, il dtype, ecc. Dell'array memorizzato.)
Vantaggi HDF per un array di grandi dimensioni: I / O più veloce di uno slice arbitrario
Proprio come TL / DR: per un array 3D da ~ 8 GB, la lettura di una sezione "completa" lungo qualsiasi asse richiedeva circa 20 secondi con un set di dati HDF5 in blocchi e da 0,3 secondi (nel migliore dei casi) a oltre tre ore (nel peggiore dei casi) un array memmap degli stessi dati.
Oltre alle cose elencate sopra, c'è un altro grande vantaggio in un formato di dati su disco "chunked" * come HDF5: la lettura di una slice arbitraria (enfasi sull'arbitrario) sarà tipicamente molto più veloce, poiché i dati su disco sono più contigui su media.
*
(HDF5 non deve essere un formato di dati in blocchi. Supporta il raggruppamento in blocchi, ma non lo richiede. In effetti, l'impostazione predefinita per la creazione di un set di dati in h5py
non è il blocco, se ricordo correttamente.)
Fondamentalmente, la velocità di lettura del disco nel caso migliore e la velocità di lettura del disco nel caso peggiore per una data porzione del tuo set di dati saranno abbastanza simili a un set di dati HDF in blocchi (supponendo che tu abbia scelto una dimensione di blocco ragionevole o che una libreria ne scelga uno per te). Con un semplice array binario, il caso migliore è più veloce, ma il caso peggiore è molto peggio.
Un avvertimento, se hai un SSD, probabilmente non noterai un'enorme differenza nella velocità di lettura / scrittura. Con un normale disco rigido, tuttavia, le letture sequenziali sono molto, molto più veloci delle letture casuali. (Ad esempio, un normale disco rigido ha molto seek
tempo.) HDF ha ancora un vantaggio su un SSD, ma è più dovuto alle sue altre caratteristiche (ad esempio metadati, organizzazione, ecc.) che alla velocità pura.
Prima di tutto, per chiarire la confusione, l'accesso a un h5py
set di dati restituisce un oggetto che si comporta in modo abbastanza simile a un array numpy, ma non carica i dati in memoria finché non vengono tagliati. (Simile a memmap, ma non identico.) Dai un'occhiata h5py
all'introduzione per maggiori informazioni.
Il taglio del set di dati caricherà un sottoinsieme di dati in memoria, ma presumibilmente vuoi farci qualcosa, a quel punto ne avrai comunque bisogno in memoria.
Se vuoi eseguire calcoli out-of-core, puoi abbastanza facilmente per i dati tabulari con pandas
o pytables
. È possibile con h5py
(più bello per i grandi array ND), ma è necessario scendere a un livello inferiore di tocco e gestire l'iterazione da soli.
Tuttavia, il futuro dei calcoli out-of-core di tipo insensibile è Blaze. Dai un'occhiata se vuoi davvero prendere quella strada.
Il caso "unchunked"
Prima di tutto, considera un array 3D C-ordinato scritto su disco (lo simulerò chiamando arr.ravel()
e stampando il risultato, per rendere le cose più visibili):
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]]])
I valori verranno memorizzati su disco in sequenza come mostrato nella riga 4 di seguito. (Ignoriamo i dettagli e la frammentazione del filesystem per il momento.)
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])
Nella migliore delle ipotesi, prendiamo una fetta lungo il primo asse. Si noti che questi sono solo i primi 36 valori dell'array. Questa sarà una lettura molto veloce! (una ricerca, una lettura)
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]])
Allo stesso modo, la fetta successiva lungo il primo asse sarà solo i successivi 36 valori. Per leggere una fetta completa lungo questo asse, abbiamo solo bisogno di seek
un'operazione. Se tutto ciò che leggeremo sono varie sezioni lungo questo asse, allora questa è la struttura del file perfetta.
Tuttavia, consideriamo lo scenario peggiore: una fetta lungo l'ultimo asse.
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]])
Per leggere questa slice, abbiamo bisogno di 36 ricerche e 36 letture, poiché tutti i valori sono separati su disco. Nessuno di loro è adiacente!
Questo può sembrare piuttosto secondario, ma man mano che arriviamo a array sempre più grandi, il numero e la dimensione delle seek
operazioni crescono rapidamente. Per un array 3D di grandi dimensioni (~ 10Gb) memorizzato in questo modo e letto tramite memmap
, la lettura di uno slice completo lungo l'asse "peggiore" può richiedere facilmente decine di minuti, anche con hardware moderno. Allo stesso tempo, una fetta lungo l'asse migliore può richiedere meno di un secondo. Per semplicità, sto solo mostrando le sezioni "complete" lungo un singolo asse, ma la stessa identica cosa accade con le sezioni arbitrarie di qualsiasi sottoinsieme di dati.
Per inciso, ci sono diversi formati di file che sfruttano questo vantaggio e fondamentalmente memorizzano tre copie di enormi array 3D su disco: uno in ordine C, uno in ordine F e uno intermedio tra i due. (Un esempio è il formato D3D di Geoprobe, anche se non sono sicuro che sia documentato da nessuna parte.) Chi se ne frega se la dimensione finale del file è di 4 TB, lo spazio di archiviazione è economico! La cosa pazzesca è che poiché il caso d'uso principale è l'estrazione di una singola sottosezione in ciascuna direzione, le letture che si desidera eseguire sono molto, molto veloci. Funziona molto bene!
Il semplice caso "chunked"
Supponiamo di memorizzare 2x2x2 "blocchi" dell'array 3D come blocchi contigui su disco. In altre parole, qualcosa come:
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])
Quindi i dati su disco sarebbero 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])
E solo per mostrare che sono blocchi 2x2x2 di arr
, nota che questi sono i primi 8 valori di chunked
:
In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0, 1],
[ 6, 7]],
[[36, 37],
[42, 43]]])
Per leggere in qualsiasi fetta lungo un asse, leggiamo in 6 o 9 blocchi contigui (il doppio dei dati di cui abbiamo bisogno) e quindi manterremo solo la parte che volevamo. Si tratta di un massimo di 9 ricerche nel caso peggiore contro un massimo di 36 ricerche per la versione non suddivisa in blocchi. (Ma il caso migliore è ancora 6 seek vs 1 per l'array memmap). Poiché le letture sequenziali sono molto veloci rispetto alle seek, questo riduce significativamente la quantità di tempo necessaria per leggere un sottoinsieme arbitrario in memoria. Ancora una volta, questo effetto diventa maggiore con array più grandi.
HDF5 fa qualche passo in più. I blocchi non devono essere archiviati in modo contiguo e sono indicizzati da un albero B. Inoltre, non devono avere le stesse dimensioni su disco, quindi la compressione può essere applicata a ciascun blocco.
Array in blocchi con h5py
Per impostazione predefinita, h5py
non crea file HDF in blocchi su disco (penso che lo pytables
faccia, al contrario). Se si specifica chunks=True
durante la creazione del set di dati, tuttavia, si otterrà un array in blocchi su disco.
Come esempio rapido e minimo:
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?'
Nota che chunks=True
dice h5py
di scegliere automaticamente una dimensione di blocco per noi. Se sai di più sul tuo caso d'uso più comune, puoi ottimizzare la dimensione / forma del blocco specificando una tupla di forma (ad esempio (2,2,2)
nel semplice esempio sopra). Ciò consente di rendere più efficienti le letture lungo un particolare asse o di ottimizzarle per letture / scritture di una certa dimensione.
Confronto delle prestazioni di I / O
Solo per enfatizzare il punto, confrontiamo la lettura in sezioni da un set di dati HDF5 in blocchi e un grande array 3D (~ 8 GB) ordinato da Fortran contenente gli stessi dati esatti.
Ho cancellato tutte le cache del sistema operativo tra ogni esecuzione, quindi stiamo assistendo a prestazioni "fredde".
Per ogni tipo di file, testeremo la lettura in una sezione x "completa" lungo il primo asse e in una dimensione z "completa" lungo l'ultimo asse. Per l'array memmap ordinato da Fortran, la sezione "x" è il caso peggiore e la sezione "z" è il caso migliore.
Il codice utilizzato è in un gist (inclusa la creazione del hdf
file). Non riesco a condividere facilmente i dati usati qui, ma potresti simularli con una matrice di zeri della stessa forma ( 621, 4991, 2600)
e tipo np.uint8
.
Si chunked_hdf.py
presenta così:
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
è simile, ma ha un tocco in più di complessità per garantire che le fette siano effettivamente caricate in memoria (per impostazione predefinita, memmapped
verrebbe restituito un altro array, che non sarebbe un confronto tra mele e mele).
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()
Diamo prima un'occhiata alle prestazioni dell'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
Una x-slice "completa" e una z-slice "completa" impiegano circa la stessa quantità di tempo (~ 20sec). Considerando che si tratta di un array da 8 GB, non è poi così male. La maggior parte delle volte
E se confrontiamo questo con i tempi dell'array memmap (è ordinato da Fortran: uno "z-slice" è il caso migliore e un "x-slice" è il caso peggiore.):
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
Sì, avete letto bene. 0,3 secondi per una direzione di fetta e ~ 3,5 ore per l'altra.
Il tempo necessario per eseguire lo slice nella direzione "x" è molto più lungo del tempo necessario per caricare l'intero array da 8 GB in memoria e selezionare lo slice che volevamo! (Di nuovo, questo è un array ordinato da Fortran. Il tempo di slice x / z opposto sarebbe il caso di un array ordinato da C.)
Tuttavia, se vogliamo sempre prendere una fetta nella direzione migliore, il grande array binario su disco è molto buono. (~ 0,3 sec!)
Con un array memmap, sei bloccato con questa discrepanza I / O (o forse anisotropia è un termine migliore). Tuttavia, con un set di dati HDF in blocchi, è possibile scegliere la dimensione in blocchi in modo che l'accesso sia uguale o ottimizzato per un particolare caso d'uso. Ti dà molta più flessibilità.
In sintesi
Si spera che questo aiuti a chiarire almeno una parte della tua domanda. HDF5 ha molti altri vantaggi rispetto alle memmap "non elaborate", ma non ho spazio per espanderle tutte qui. La compressione può velocizzare alcune cose (i dati con cui lavoro non beneficiano molto della compressione, quindi la uso raramente) e il caching a livello di sistema operativo spesso viene riprodotto più bene con i file HDF5 che con le memmap "non elaborate". Oltre a ciò, HDF5 è un formato contenitore davvero fantastico. Offre molta flessibilità nella gestione dei dati e può essere utilizzato da più o meno qualsiasi linguaggio di programmazione.
Nel complesso, provalo e vedi se funziona bene per il tuo caso d'uso. Penso che potresti essere sorpreso.
h5py
è più adatto a set di dati come il tuo dipytables
. Inoltre,h5py
non non restituire una matrice NumPy in memoria. Invece restituisce qualcosa che si comporta come tale, ma non viene caricato in memoria (simile a unmemmapped
array). Sto scrivendo una risposta più completa (potrei non finirla), ma spero che questo commento nel frattempo aiuti un po '.