la mejor manera de preservar matrices numpy en disco

124

Estoy buscando una forma rápida de preservar grandes matrices numpy. Quiero guardarlos en el disco en formato binario y luego volver a leerlos en la memoria con relativa rapidez. cPickle no es lo suficientemente rápido, desafortunadamente.

Encontré numpy.savez y numpy.load . Pero lo extraño es que numpy.load carga un archivo npy en "memory-map". Eso significa que la manipulación regular de matrices es muy lenta. Por ejemplo, algo como esto sería muy lento:

#!/usr/bin/python
import numpy as np;
import time; 
from tempfile import TemporaryFile

n = 10000000;

a = np.arange(n)
b = np.arange(n) * 10
c = np.arange(n) * -0.5

file = TemporaryFile()
np.savez(file,a = a, b = b, c = c);

file.seek(0)
t = time.time()
z = np.load(file)
print "loading time = ", time.time() - t

t = time.time()
aa = z['a']
bb = z['b']
cc = z['c']
print "assigning time = ", time.time() - t;

más precisamente, la primera línea será realmente rápida, pero las líneas restantes que asignan las matrices objson ridículamente lentas:

loading time =  0.000220775604248
assining time =  2.72940087318

¿Hay alguna forma mejor de preservar matrices numpy? Idealmente, quiero poder almacenar múltiples matrices en un archivo.

Vendetta
fuente
3
Por defecto, nonp.load debería mmap del archivo.
Fred Foo
6
¿Qué pasa con las pytables ?
Diseño
@larsmans, gracias por la respuesta. pero ¿por qué el tiempo de búsqueda (z ['a'] en mi ejemplo de código) es tan lento?
Vendetta
1
Sería bueno si hubiera un poco más de información en su pregunta, como el tipo de matriz que se almacena en ifile y su tamaño, o si hay varias matrices en diferentes archivos, o cómo se guardan exactamente. Por su pregunta, tengo la impresión de que la primera línea no hace nada y que la carga real ocurre después, pero esas son solo conjeturas.
Diseño
19
@larsmans - Por lo que vale, para un archivo "npz" (es decir, múltiples arreglos guardados con numpy.savez), el valor predeterminado es "cargar lentamente" los arreglos. No los está mapeando, pero no los carga hasta que el NpzFileobjeto está indexado. (Por lo tanto, el retraso al que se refiere el OP.) La documentación para loadomite esto, y por lo tanto es un poco engañoso ...
Joe Kington

Respuestas:

63

Soy un gran fan de hdf5 para almacenar grandes matrices numpy. Hay dos opciones para tratar con hdf5 en python:

http://www.pytables.org/

http://www.h5py.org/

Ambos están diseñados para funcionar con matrices numpy de manera eficiente.

JoshAdel
fuente
35
¿Estaría dispuesto a proporcionar algún código de ejemplo usando estos paquetes para guardar una matriz?
dbliss
12
ejemplo de h5py y ejemplo de pytables
Kamil Slowikowski
1
Según mi experiencia, hdf5 tiene un rendimiento de lectura y escritura muy lento con almacenamiento de fragmentos y compresión habilitados. Por ejemplo, tengo dos matrices 2-D con forma (2500,000 * 2000) con tamaño de fragmento (10,000 * 2000). Una sola operación de escritura de una matriz con forma (2000 * 2000) tardará entre 1 y 2 segundos en completarse. ¿Tiene alguna sugerencia para mejorar el rendimiento? Gracias.
Simón. Li
206

He comparado el rendimiento (espacio y tiempo) de varias formas de almacenar matrices numpy. Pocos admiten múltiples matrices por archivo, pero quizás sea útil de todos modos.

punto de referencia para el almacenamiento de matrices numpy

Los archivos Npy y binarios son realmente rápidos y pequeños para datos densos. Si los datos son escasos o muy estructurados, es posible que desee usar npz con compresión, lo que ahorrará mucho espacio pero costará algo de tiempo de carga.

Si la portabilidad es un problema, binary es mejor que npy. Si la legibilidad humana es importante, entonces tendrá que sacrificar mucho rendimiento, pero se puede lograr bastante bien usando csv (que también es muy portátil, por supuesto).

Más detalles y el código están disponibles en el repositorio de github .

marca
fuente
2
¿Podría explicar por qué binaryes mejor que npypor portabilidad? ¿Esto también se aplica npz?
daniel451
1
@ daniel451 Porque cualquier idioma puede leer archivos binarios si solo conocen la forma, el tipo de datos y si se basan en filas o columnas. Si solo está usando Python, entonces npy está bien, probablemente un poco más fácil que el binario.
Mark
1
¡Gracias! Una pregunta más: ¿me olvido de algo o dejaste de lado HDF5? Dado que esto es bastante común, me interesaría cómo se compara con los otros métodos.
daniel451
1
Intenté usar png y npy para guardar una misma imagen. png solo ocupa 2K de espacio mientras que npy ocupa 307K. Este resultado es realmente diferente a tu trabajo. ¿Estoy haciendo algo mal? Esta imagen es una imagen en escala de grises y solo 0 y 255 están adentro. Creo que esto es un dato escaso, ¿correcto? Luego también usé npz pero el tamaño es totalmente el mismo.
York Yang
3
¿Por qué falta h5py? ¿O me estoy perdiendo algo?
daniel451
49

Ahora hay un clon basado en HDF5 de picklecalled hickle!

https://github.com/telegraphic/hickle

import hickle as hkl 

data = { 'name' : 'test', 'data_arr' : [1, 2, 3, 4] }

# Dump data to file
hkl.dump( data, 'new_data_file.hkl' )

# Load data from file
data2 = hkl.load( 'new_data_file.hkl' )

print( data == data2 )

EDITAR:

También existe la posibilidad de "encurtir" directamente en un archivo comprimido haciendo:

import pickle, gzip, lzma, bz2

pickle.dump( data, gzip.open( 'data.pkl.gz',   'wb' ) )
pickle.dump( data, lzma.open( 'data.pkl.lzma', 'wb' ) )
pickle.dump( data,  bz2.open( 'data.pkl.bz2',  'wb' ) )

compresión


Apéndice

import numpy as np
import matplotlib.pyplot as plt
import pickle, os, time
import gzip, lzma, bz2, h5py

compressions = [ 'pickle', 'h5py', 'gzip', 'lzma', 'bz2' ]
labels = [ 'pickle', 'h5py', 'pickle+gzip', 'pickle+lzma', 'pickle+bz2' ]
size = 1000

data = {}

# Random data
data['random'] = np.random.random((size, size))

# Not that random data
data['semi-random'] = np.zeros((size, size))
for i in range(size):
    for j in range(size):
        data['semi-random'][i,j] = np.sum(data['random'][i,:]) + np.sum(data['random'][:,j])

# Not random data
data['not-random'] = np.arange( size*size, dtype=np.float64 ).reshape( (size, size) )

sizes = {}

for key in data:

    sizes[key] = {}

    for compression in compressions:

        if compression == 'pickle':
            time_start = time.time()
            pickle.dump( data[key], open( 'data.pkl', 'wb' ) )
            time_tot = time.time() - time_start
            sizes[key]['pickle'] = ( os.path.getsize( 'data.pkl' ) * 10**(-6), time_tot )
            os.remove( 'data.pkl' )

        elif compression == 'h5py':
            time_start = time.time()
            with h5py.File( 'data.pkl.{}'.format(compression), 'w' ) as h5f:
                h5f.create_dataset('data', data=data[key])
            time_tot = time.time() - time_start
            sizes[key][compression] = ( os.path.getsize( 'data.pkl.{}'.format(compression) ) * 10**(-6), time_tot)
            os.remove( 'data.pkl.{}'.format(compression) )

        else:
            time_start = time.time()
            pickle.dump( data[key], eval(compression).open( 'data.pkl.{}'.format(compression), 'wb' ) )
            time_tot = time.time() - time_start
            sizes[key][ labels[ compressions.index(compression) ] ] = ( os.path.getsize( 'data.pkl.{}'.format(compression) ) * 10**(-6), time_tot )
            os.remove( 'data.pkl.{}'.format(compression) )


f, ax_size = plt.subplots()
ax_time = ax_size.twinx()

x_ticks = labels
x = np.arange( len(x_ticks) )

y_size = {}
y_time = {}
for key in data:
    y_size[key] = [ sizes[key][ x_ticks[i] ][0] for i in x ]
    y_time[key] = [ sizes[key][ x_ticks[i] ][1] for i in x ]

width = .2
viridis = plt.cm.viridis

p1 = ax_size.bar( x-width, y_size['random']       , width, color = viridis(0)  )
p2 = ax_size.bar( x      , y_size['semi-random']  , width, color = viridis(.45))
p3 = ax_size.bar( x+width, y_size['not-random']   , width, color = viridis(.9) )

p4 = ax_time.bar( x-width, y_time['random']  , .02, color = 'red')
ax_time.bar( x      , y_time['semi-random']  , .02, color = 'red')
ax_time.bar( x+width, y_time['not-random']   , .02, color = 'red')

ax_size.legend( (p1, p2, p3, p4), ('random', 'semi-random', 'not-random', 'saving time'), loc='upper center',bbox_to_anchor=(.5, -.1), ncol=4 )
ax_size.set_xticks( x )
ax_size.set_xticklabels( x_ticks )

f.suptitle( 'Pickle Compression Comparison' )
ax_size.set_ylabel( 'Size [MB]' )
ax_time.set_ylabel( 'Time [s]' )

f.savefig( 'sizes.pdf', bbox_inches='tight' )
Suuuehgi
fuente
Una advertencia que podría importarle a algunas personas es que pickle puede ejecutar código arbitrario, lo que lo hace menos seguro que otros protocolos para guardar datos.
Charlie Parker
¡Esto es genial! ¿También puede proporcionar el código para leer los archivos encurtidos directamente en compresión usando lzma o bz2?
Ernest S Kirubakaran
14

savez () guarda los datos en un archivo zip. Puede llevar algún tiempo comprimir y descomprimir el archivo. Puede usar la función save () & load ():

f = file("tmp.bin","wb")
np.save(f,a)
np.save(f,b)
np.save(f,c)
f.close()

f = file("tmp.bin","rb")
aa = np.load(f)
bb = np.load(f)
cc = np.load(f)
f.close()

Para guardar varias matrices en un archivo, solo necesita abrir el archivo primero y luego guardar o cargar las matrices en secuencia.

HYRY
fuente
7

Otra posibilidad para almacenar matrices numpy de manera eficiente es Bloscpack :

#!/usr/bin/python
import numpy as np
import bloscpack as bp
import time

n = 10000000

a = np.arange(n)
b = np.arange(n) * 10
c = np.arange(n) * -0.5
tsizeMB = sum(i.size*i.itemsize for i in (a,b,c)) / 2**20.

blosc_args = bp.DEFAULT_BLOSC_ARGS
blosc_args['clevel'] = 6
t = time.time()
bp.pack_ndarray_file(a, 'a.blp', blosc_args=blosc_args)
bp.pack_ndarray_file(b, 'b.blp', blosc_args=blosc_args)
bp.pack_ndarray_file(c, 'c.blp', blosc_args=blosc_args)
t1 = time.time() - t
print "store time = %.2f (%.2f MB/s)" % (t1, tsizeMB / t1)

t = time.time()
a1 = bp.unpack_ndarray_file('a.blp')
b1 = bp.unpack_ndarray_file('b.blp')
c1 = bp.unpack_ndarray_file('c.blp')
t1 = time.time() - t
print "loading time = %.2f (%.2f MB/s)" % (t1, tsizeMB / t1)

y la salida de mi computadora portátil (una MacBook Air relativamente antigua con un procesador Core2):

$ python store-blpk.py
store time = 0.19 (1216.45 MB/s)
loading time = 0.25 (898.08 MB/s)

eso significa que se puede almacenar muy rápido, es decir, el cuello de botella suele ser el disco. Sin embargo, como las relaciones de compresión son bastante buenas aquí, la velocidad efectiva se multiplica por las relaciones de compresión. Estos son los tamaños de estas matrices de 76 MB:

$ ll -h *.blp
-rw-r--r--  1 faltet  staff   921K Mar  6 13:50 a.blp
-rw-r--r--  1 faltet  staff   2.2M Mar  6 13:50 b.blp
-rw-r--r--  1 faltet  staff   1.4M Mar  6 13:50 c.blp

Tenga en cuenta que el uso del compresor Blosc es fundamental para lograrlo. El mismo script pero usando 'clevel' = 0 (es decir, desactivando la compresión):

$ python bench/store-blpk.py
store time = 3.36 (68.04 MB/s)
loading time = 2.61 (87.80 MB/s)

está claramente atascado por el rendimiento del disco.

Francesc
fuente
2
A quién le puede interesar: Aunque Bloscpack y PyTables son proyectos diferentes, el primero se centra solo en el volcado de disco y no en el corte de matrices almacenadas, probé ambos y, para "proyectos de volcado de archivos" puros, Bloscpack es casi 6 veces más rápido que PyTables.
Marcelo Sardelich
4

El tiempo de búsqueda es lento porque cuando lo usa mmapno carga el contenido de la matriz en la memoria cuando invoca el loadmétodo. Los datos se cargan de forma diferida cuando se necesitan datos particulares. Y esto sucede en la búsqueda en su caso. Pero la segunda búsqueda no será tan lenta.

Esta es una buena característica de mmapcuando tiene una gran matriz, no tiene que cargar datos completos en la memoria.

Para resolver su puede usar joblib , puede volcar cualquier objeto que desee usando joblib.dumpincluso dos o más numpy arrays, vea el ejemplo

firstArray = np.arange(100)
secondArray = np.arange(50)
# I will put two arrays in dictionary and save to one file
my_dict = {'first' : firstArray, 'second' : secondArray}
joblib.dump(my_dict, 'file_name.dat')
Michal
fuente
La biblioteca ya no está disponible.
Andrea Moro