melhor maneira de preservar matrizes numpy no disco

124

Estou procurando uma maneira rápida de preservar grandes matrizes numpy. Quero salvá-los no disco em um formato binário e depois lê-los de volta na memória de forma relativamente rápida. Infelizmente, o cPickle não é rápido o suficiente.

Encontrei numpy.savez e numpy.load . Mas o mais estranho é que o numpy.load carrega um arquivo npy no "mapa da memória". Isso significa que a manipulação regular de matrizes é realmente lenta. Por exemplo, algo assim seria muito 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;

mais precisamente, a primeira linha será realmente rápida, mas as linhas restantes que atribuem as matrizes objsão ridiculamente lentas:

loading time =  0.000220775604248
assining time =  2.72940087318

Existe alguma maneira melhor de preservar matrizes numpy? Idealmente, desejo poder armazenar várias matrizes em um arquivo.

vendeta
fonte
3
Por padrão, nãonp.load deve mapear o arquivo.
Fred Foo
6
E quanto às tabelas ?
Dsign
@ Larsmans, obrigado pela resposta. mas por que o tempo de pesquisa (z ['a'] no meu exemplo de código) é tão lento?
Vendetta
1
Seria bom se tivéssemos um pouco mais de informação em sua pergunta, como o tipo de matriz armazenada em ifile e seu tamanho, ou se são várias matrizes em arquivos diferentes, ou como exatamente você as salva. Com sua pergunta, tenho a impressão de que a primeira linha não faz nada e que o carregamento real ocorre depois, mas essas são apenas suposições.
Dsign
19
@larsmans - Para o que vale a pena, para um arquivo "npz" (ou seja, várias matrizes salvas com numpy.savez), o padrão é "carregar preguiçosamente" as matrizes. Ele não está mapeando-os, mas não os carrega até que o NpzFileobjeto seja indexado. (Assim, o atraso do OP está se referindo.) A documentação para loadsaltos isso, e é, portanto, um toque enganosa ...
Joe Kington

Respostas:

63

Sou um grande fã do hdf5 por armazenar grandes matrizes numpy. Existem duas opções para lidar com o hdf5 em python:

http://www.pytables.org/

http://www.h5py.org/

Ambos são projetados para trabalhar com matrizes numpy de maneira eficiente.

JoshAdel
fonte
35
você gostaria de fornecer algum código de exemplo usando esses pacotes para salvar uma matriz?
dbliss
12
exemplo h5py e pytables exemplo
Kamil Slowikowski
1
Pelas minhas experiências, o hdf5 apresenta uma leitura e gravação muito lentas, com o armazenamento e a compactação de blocos habilitados. Por exemplo, eu tenho duas matrizes 2D com forma (2500.000 * 2000) com tamanho de bloco (10.000 * 2000). Uma única operação de gravação de uma matriz com forma (2000 * 2000) levará cerca de 1 ~ 2s para ser concluída. Você tem alguma sugestão para melhorar o desempenho? THX.
Simon. Li
205

Comparei o desempenho (espaço e tempo) de várias maneiras de armazenar matrizes numpy. Poucos deles suportam várias matrizes por arquivo, mas talvez seja útil de qualquer maneira.

referência para armazenamento numpy numérico

Arquivos Npy e binários são realmente rápidos e pequenos para dados densos. Se os dados forem escassos ou muito estruturados, convém usar o npz com compactação, o que economizará muito espaço, mas custará algum tempo de carregamento.

Se a portabilidade é um problema, o binário é melhor que o npy. Se a legibilidade humana for importante, você terá que sacrificar muito desempenho, mas isso pode ser alcançado bastante bem usando o csv (que também é muito portátil, é claro).

Mais detalhes e o código estão disponíveis no repositório do github .

Marca
fonte
2
Você poderia explicar por que binaryé melhor do que npya portabilidade? Isso também se aplica npz?
Daniel451
1
@ daniel451 Porque qualquer idioma pode ler arquivos binários se souber apenas a forma, o tipo de dados e se é baseado em linha ou coluna. Se você está usando Python, então o npy é bom, provavelmente um pouco mais fácil que o binário.
Mark
1
Obrigado! Mais uma pergunta: eu negligencio alguma coisa ou você deixou de fora o HDF5? Como isso é bastante comum, eu estaria interessado em como ele se compara aos outros métodos.
precisa saber é o seguinte
1
Tentei usar png e npy para salvar a mesma imagem. png ocupa apenas 2K de espaço, enquanto o npy ocupa 307K. Este resultado é realmente diferente do seu trabalho. Estou fazendo algo errado? Esta imagem é uma imagem em escala de cinza e apenas 0 e 255 estão dentro. Eu acho que este é um dado esparso correto? Então eu também usei npz, mas o tamanho é totalmente o mesmo.
York Yang
3
Por que o h5py está ausente? Ou eu estou esquecendo de alguma coisa?
Daniel451
49

Agora existe um clone baseado em HDF5 picklechamado 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:

Também existe a possibilidade de "selecionar" diretamente em um arquivo compactado, fazendo:

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' ) )

compressão


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
fonte
Um aviso de que algumas pessoas podem se preocupar é que o pickle pode executar código arbitrário, o que o torna menos seguro do que outros protocolos para salvar dados.
Charlie Parker
Isso é ótimo! Você também pode fornecer o código para ler os arquivos armazenados diretamente na compactação usando lzma ou bz2?
Ernest S Kirubakaran
14

savez () salva dados em um arquivo zip. Pode levar algum tempo para compactar e descompactar o arquivo. Você pode usar a função 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 salvar várias matrizes em um arquivo, basta abrir o arquivo primeiro e depois salvar ou carregar as matrizes em sequência.

HYRY
fonte
7

Outra possibilidade de armazenar matrizes numpy com eficiência é o 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)

e a saída do meu laptop (um MacBook Air relativamente antigo com um processador Core2):

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

isso significa que ele pode armazenar muito rápido, ou seja, o gargalo é tipicamente o disco. No entanto, como as taxas de compressão são muito boas aqui, a velocidade efetiva é multiplicada pelas taxas de compressão. Aqui estão os tamanhos para essas matrizes 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

Observe que o uso do compressor Blosc é fundamental para conseguir isso. O mesmo script, mas usando 'clevel' = 0 (ou seja, desativando a compactação):

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

está claramente afunilado pelo desempenho do disco.

Francesc
fonte
2
Para quem possa interessar: Embora o Bloscpack e o PyTables sejam projetos diferentes, o primeiro focando apenas no despejo de disco e não no fatiamento de matrizes armazenadas, testei tanto o Bloscpack quanto para "projetos de despejo de arquivos" puros. O Bloscpack é quase 6x mais rápido que o PyTables.
Marcelo Sardelich 23/03/2015
4

O tempo de pesquisa é lento porque, quando você usa mmap, não carrega o conteúdo da matriz na memória quando invoca o loadmétodo. Os dados são carregados com preguiça quando dados específicos são necessários. E isso acontece na pesquisa no seu caso. Mas a segunda pesquisa não será tão lenta.

Esse é um recurso interessante mmapquando você tem uma grande matriz e não precisa carregar dados inteiros na memória.

Para resolver o seu pode usar joblib, você pode despejar qualquer objeto que desejar usando joblib.dumpdois ou mais numpy arrays, veja o exemplo

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
fonte
A biblioteca não está mais disponível.
Andrea Moro