วิธีที่ดีที่สุดในการรักษาอาร์เรย์จำนวนนับบนดิสก์


124

ฉันกำลังมองหาวิธีที่รวดเร็วในการรักษาอาร์เรย์จำนวนมาก ฉันต้องการบันทึกลงในดิสก์ในรูปแบบไบนารีจากนั้นอ่านกลับเข้าไปในหน่วยความจำค่อนข้างเร็ว cPickle ไม่เร็วพอน่าเสียดาย

ผมพบว่าnumpy.savezและnumpy.load แต่สิ่งที่แปลกคือ numpy.load โหลดไฟล์ npy ลงใน "memory-map" นั่นหมายความว่าการจัดการอาร์เรย์ปกติจะช้ามาก ตัวอย่างเช่นสิ่งนี้จะช้ามาก:

#!/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;

อย่างแม่นยำยิ่งขึ้นบรรทัดแรกจะเร็วมาก แต่บรรทัดที่เหลือที่กำหนดอาร์เรย์ให้objช้าอย่างน่าขัน:

loading time =  0.000220775604248
assining time =  2.72940087318

มีวิธีใดที่ดีกว่าในการรักษาอาร์เรย์ numpy หรือไม่? ตามหลักการแล้วฉันต้องการจัดเก็บอาร์เรย์หลายรายการในไฟล์เดียว


3
โดยค่าเริ่มต้นไม่np.loadควรmmap ไฟล์
Fred Foo

6
แล้วpytablesล่ะ?
dsign

@larsmans ขอบคุณสำหรับการตอบกลับ แต่ทำไมเวลาในการค้นหา (z ['a'] ในตัวอย่างโค้ดของฉัน) ช้าจัง
Vendetta

1
คงจะดีไม่น้อยหากเรามีข้อมูลเพิ่มเติมเล็กน้อยในคำถามของคุณเช่นประเภทของอาร์เรย์ที่เก็บไว้ใน ifile และขนาดของมันหรือถ้าเป็นอาร์เรย์หลายอาร์เรย์ในไฟล์ที่ต่างกันหรือคุณจะบันทึกอย่างไร จากคำถามของคุณฉันรู้สึกว่าบรรทัดแรกไม่ได้ทำอะไรเลยและการโหลดจริงจะเกิดขึ้นหลังจากนั้น แต่เป็นเพียงการคาดเดาเท่านั้น
dsign

19
@larsmans - สำหรับสิ่งที่คุ้มค่าสำหรับไฟล์ "npz" (เช่นหลายอาร์เรย์ที่บันทึกด้วยnumpy.savez) ค่าเริ่มต้นคือ "โหลดอย่างเกียจคร้าน" อาร์เรย์ ไม่ใช่การจับคู่ แต่จะไม่โหลดจนกว่าNpzFileวัตถุจะถูกจัดทำดัชนี (ดังนั้นความล่าช้าที่ OP อ้างถึง) เอกสารสำหรับloadข้ามสิ่งนี้จึงเป็นการสัมผัสที่ทำให้เข้าใจผิด ...
Joe Kington

คำตอบ:


63

ฉันเป็นแฟนตัวยงของ hdf5 สำหรับการจัดเก็บอาร์เรย์จำนวนมาก มีสองตัวเลือกในการจัดการกับ hdf5 ใน python:

http://www.pytables.org/

http://www.h5py.org/

ทั้งสองได้รับการออกแบบมาเพื่อทำงานกับอาร์เรย์ numpy อย่างมีประสิทธิภาพ


35
คุณยินดีที่จะให้โค้ดตัวอย่างโดยใช้แพ็คเกจเหล่านี้เพื่อบันทึกอาร์เรย์หรือไม่
dbliss


1
จากประสบการณ์ของฉัน hdf5 แสดงการอ่านและเขียนช้ามากโดยเปิดใช้งานการจัดเก็บข้อมูลและการบีบอัดแบบก้อน ตัวอย่างเช่นฉันมีอาร์เรย์ 2 มิติสองตัวที่มีรูปร่าง (2500,000 * 2000) ที่มีขนาดก้อน (10,000 * 2000) การดำเนินการเขียนเพียงครั้งเดียวของอาร์เรย์ที่มีรูปร่าง (2000 * 2000) จะใช้เวลาประมาณ 1 ~ 2 วินาทีจึงจะเสร็จสมบูรณ์ คุณมีข้อเสนอแนะเกี่ยวกับการปรับปรุงประสิทธิภาพหรือไม่? ขอบคุณ.
ไซม่อน หลี่

206

ฉันได้เปรียบเทียบประสิทธิภาพ (พื้นที่และเวลา) สำหรับหลายวิธีในการจัดเก็บอาร์เรย์จำนวนนับ มีเพียงไม่กี่ตัวที่รองรับหลายอาร์เรย์ต่อไฟล์ แต่บางทีมันก็มีประโยชน์อยู่ดี

เกณฑ์มาตรฐานสำหรับการจัดเก็บอาร์เรย์จำนวนนับ

ไฟล์ Npy และ binary นั้นทั้งเร็วและเล็กมากสำหรับข้อมูลที่หนาแน่น หากข้อมูลกระจัดกระจายหรือมีโครงสร้างมากคุณอาจต้องการใช้ npz กับการบีบอัดซึ่งจะช่วยประหยัดพื้นที่ได้มาก แต่ต้องเสียเวลาในการโหลด

หากการพกพาเป็นปัญหาไบนารีจะดีกว่า npy หากความสามารถในการอ่านของมนุษย์มีความสำคัญคุณจะต้องเสียสละประสิทธิภาพจำนวนมาก แต่ก็สามารถทำได้ดีพอสมควรโดยใช้ csv (ซึ่งแน่นอนว่าพกพาได้ง่ายมาก)

รายละเอียดเพิ่มเติมและรหัสที่มีอยู่ที่repo GitHub


2
คุณช่วยอธิบายได้ไหมว่าทำไมถึงbinaryดีกว่าnpyการพกพา? สิ่งนี้ใช้สำหรับnpz?
daniel451

1
@ daniel451 เนื่องจากภาษาใด ๆ สามารถอ่านไฟล์ไบนารีได้หากพวกเขาเพียงแค่รู้รูปร่างชนิดข้อมูลและไม่ว่าจะเป็นแถวหรือคอลัมน์ หากคุณเพิ่งใช้ Python ดังนั้น npy ก็ใช้ได้อาจจะง่ายกว่าไบนารีเล็กน้อย
Mark

1
ขอบคุณ! อีกคำถามหนึ่ง: ฉันมองข้ามบางสิ่งไปหรือคุณปล่อย HDF5 ออกไป? เนื่องจากนี่เป็นเรื่องธรรมดาฉันจึงสนใจว่าจะเปรียบเทียบกับวิธีอื่นอย่างไร
daniel451

1
ฉันพยายามใช้ png และ npy เพื่อบันทึกภาพเดียวกัน png ใช้พื้นที่เพียง 2K ในขณะที่ npy ใช้เวลา 307K ผลลัพธ์นี้แตกต่างจากงานของคุณจริงๆ ฉันทำอะไรผิดหรือเปล่า? ภาพนี้เป็นภาพสีเทาและมีเพียง 0 และ 255 เท่านั้นที่อยู่ภายใน ฉันคิดว่านี่เป็นข้อมูลที่กระจัดกระจายถูกต้องหรือไม่? จากนั้นฉันก็ใช้ npz ด้วย แต่ขนาดเท่ากันหมด
York Yang

3
ทำไม h5py ถึงหายไป? หรือฉันขาดอะไรไป?
daniel451

49

ตอนนี้มีการpickleเรียกโคลนจาก HDF5 แล้ว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 )

แก้ไข:

นอกจากนี้ยังมีความเป็นไปได้ที่จะ "ดอง" ลงในไฟล์บีบอัดโดยตรงโดยทำ:

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

การอัด


ภาคผนวก

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

คำเตือนอย่างหนึ่งที่บางคนอาจสนใจคือ pickle สามารถรันโค้ดโดยพลการซึ่งทำให้ปลอดภัยน้อยกว่าโปรโตคอลอื่นในการบันทึกข้อมูล
Charlie Parker

นี่มันเยี่ยมมาก! คุณสามารถระบุรหัสสำหรับอ่านไฟล์ที่ดองลงในการบีบอัดโดยตรงโดยใช้ lzma หรือ bz2 ได้หรือไม่?
Ernest S Kirubakaran

14

savez () บันทึกข้อมูลในไฟล์ zip อาจใช้เวลาสักครู่ในการ zip & คลายซิปไฟล์ คุณสามารถใช้ฟังก์ชัน 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()

หากต้องการบันทึกหลายอาร์เรย์ในไฟล์เดียวคุณต้องเปิดไฟล์ก่อนจากนั้นบันทึกหรือโหลดอาร์เรย์ตามลำดับ


7

ความเป็นไปได้อีกอย่างในการจัดเก็บอาร์เรย์จำนวนมากอย่างมีประสิทธิภาพคือ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)

และเอาต์พุตสำหรับแล็ปท็อปของฉัน (MacBook Air ที่ค่อนข้างเก่าที่มีโปรเซสเซอร์ Core2):

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

นั่นหมายความว่ามันสามารถจัดเก็บได้เร็วมากกล่าวคือคอขวดมักจะเป็นดิสก์ อย่างไรก็ตามเนื่องจากอัตราส่วนการบีบอัดค่อนข้างดีที่นี่ความเร็วที่ได้ผลจะคูณด้วยอัตราส่วนการบีบอัด นี่คือขนาดของอาร์เรย์ 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

โปรดทราบว่าการใช้คอมเพรสเซอร์Bloscเป็นพื้นฐานในการบรรลุเป้าหมายนี้ สคริปต์เดียวกัน แต่ใช้ 'clevel' = 0 (เช่นปิดใช้งานการบีบอัด):

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

เป็นปัญหาคอขวดอย่างชัดเจนจากประสิทธิภาพของดิสก์


2
ผู้ที่อาจกังวล: แม้ว่า Bloscpack และ PyTables จะเป็นโปรเจ็กต์ที่แตกต่างกัน แต่ก่อนหน้านี้มุ่งเน้นเฉพาะการถ่ายโอนข้อมูลในดิสก์และไม่ได้จัดเก็บการแบ่งส่วนอาร์เรย์ แต่ฉันได้ทดสอบทั้งสองอย่างและสำหรับ "โครงการถ่ายโอนไฟล์" แบบบริสุทธิ์ Bloscpack นั้นเร็วกว่า PyTables เกือบ 6 เท่า
Marcelo Sardelich

4

เวลาในการค้นหาช้าเนื่องจากเมื่อคุณใช้mmapเพื่อไม่โหลดเนื้อหาของอาร์เรย์ไปยังหน่วยความจำเมื่อคุณเรียกใช้loadเมธอด ขี้เกียจโหลดข้อมูลเมื่อจำเป็นต้องใช้ข้อมูลเฉพาะ และสิ่งนี้เกิดขึ้นในการค้นหาในกรณีของคุณ แต่การค้นหาครั้งที่สองจะไม่ช้านัก

นี่เป็นคุณสมบัติที่ดีmmapเมื่อคุณมีอาร์เรย์ขนาดใหญ่คุณไม่ต้องโหลดข้อมูลทั้งหมดลงในหน่วยความจำ

ในการแก้ปัญหา can use joblib ของคุณคุณสามารถถ่ายโอนออบเจ็กต์ใดก็ได้ที่คุณต้องการโดยใช้joblib.dumpสองตัวขึ้นnumpy arraysไปดูตัวอย่าง

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

ห้องสมุดไม่มีให้บริการแล้ว
Andrea Moro
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.