如何在Python中使用HDF5文件

大家好!

“ Python Web开发人员”课程即将开始,我们仍在共享有趣的文章,并邀请我们参加公开课程,在这里您可以观看有趣的材料,结识老师并向他们提问。

走吧

HDF5可以有效存储大量数据

当处理大量数据(无论是实验数据还是模拟数据)时,将其存储在多个文本文件中的效率不是很高。 有时您需要访问特定的数据子集,并且想要快速进行。 在这种情况下,HDF5格式通过高度优化的内置库解决了这两个问题。 HDF5广泛用于科学环境中,并具有出色的Python实现,旨在与NumPy配合使用。

HDF5格式支持任何大小的文件,每个文件都有一个内部结构,可让您搜索特定的数据集。 可以将其视为具有自己的层次结构的单独文件,以及一组文件夹和子文件夹。 默认情况下,数据以二进制格式存储,并且库与不同类型的数据兼容。 HDF5格式最重要的选项之一是,它允许您将元数据附加到结构的每个元素,使其成为创建脱机文件的理想选择。


在Python中,可以使用h5py包构建具有HDF5格式的接口。 该软件包最有趣的功能之一是仅在必要时才从文件中读取数据。 假设您有一个非常大的阵列,无法容纳在可用的RAM中。 例如,您可以在具有不同规格的计算机上生成阵列,这与用于数据分析的阵列不同。 HDF5格式允许您选择与NumPy等效的语法来读取数组的哪些元素。 然后,您可以使用存储在硬盘驱动器上而不是RAM中的数据,而无需对现有代码进行重大更改。

在本文中,我们将研究如何使用h5py来存储和检索硬盘驱动器中的数据。 我们将讨论存储数据的不同方式以及如何优化读取过程。 本文中出现的所有示例也都可以在我们的Github存储库中找到

安装方式

HDF5格式受HDF Group支持,并且基于开放源代码标准,这意味着即使该组消失了,您的数据也将始终可用。 通过h5py软件包提供了对Python的支持,该软件包可以通过pip安装。 请记住,您必须使用虚拟环境进行测试:

pip install h5py 

如果不在您的环境中,此命令还将安装NumPy。

如果您正在寻找一种图形工具来检查HDF5文件的内容,则可以安装HDF5 Viewer 。 它是用Java编写的,因此它几乎可以在任何计算机上运行。

基本数据存储和读取

让我们继续使用HDF5库。 我们将创建一个新文件,并将随机NumPy数组保存到其中。

 import h5py import numpy as np arr = np.random.randn(1000) with h5py.File('random.hdf5', 'w') as f: dset = f.create_dataset("default", data=arr) 

前几行非常简单:我们导入h5py和NumPy包并创建一个具有随机值的数组。 我们以写许可权w打开random.hdf5文件,这意味着如果同名文件已经存在,它将被覆盖。 如果要保存文件并且仍然能够对其进行写入,则可以使用a属性而不是w将其打开。 我们创建一个名为default的数据集,并将数据设置为之前创建的随机数组。 数据集是我们数据的保管人,主要是HDF5格式的构建基块。

笔记

如果您不熟悉with语句,请注意,这是打开和关闭文件的便捷方法。 即使出现错误,文件也会被关闭。 如果由于某种原因而不使用with ,请不要忘记将f.close()命令添加到末尾。 with语句可用于任何文件,而不仅限于HDF文件。

我们可以像读取NumPy文件一样读取数据:

 with h5py.File('random.hdf5', 'r') as f: data = f['default'] print(min(data)) print(max(data)) print(data[:15]) 

我们使用读取属性r打开文件,并通过直接访问称为default的数据集来还原数据。 如果您打开文件却不知道可用的数据集,则可以获取它们:

 for key in f.keys(): print(key) 

读取所需的数据集后,就可以像使用任何NumPy数组一样使用它。 例如,您可以找到最大值和最小值或选择数组的前15个值。 但是,这些简单的示例掩盖了许多幕后发生的事情,需要对它们进行讨论以了解HDF5的全部潜力。

在上面的示例中,您可以将数据用作数组。 例如,您可以通过输入数据[2]来引用第三个元素,或者可以获得一系列数据[1:3]值。 请注意:数据不是数组,而是数据集。 您可以通过输入print(type(data))来查看它。 数据集的工作方式与数组完全不同,这是因为数据集的信息存储在硬盘驱动器上,如果不使用它们,它们也不会将其加载到RAM中。 例如,以下代码将不起作用:

 f = h5py.File('random.hdf5', 'r') data = f['default'] f.close() print(data[1]) 

出现的错误有点麻烦,但是最后一行非常有用:

 ValueError: Not a dataset (not a dataset) 

该错误表示我们正在尝试访问不再有权访问的数据集。 这有点令人困惑,但这是因为我们关闭了文件,因此不再允许我们访问数据中的第二个值。 当我们将f ['default']分配给变量数据时,实际上并不从文件中读取数据,而是生成一个指向数据在硬盘驱动器上的位置的指针。 另一方面,此代码将起作用:

 f = h5py.File('random.hdf5', 'r') data = f['default'][:] f.close() print(data[10]) 

请注意,唯一的区别是我们在读取数据集后添加了[:]。 许多其他手册都集中在此类示例上,甚至没有展示使用h5py软件包的HDF5格式的全部潜力。 由于到目前为止我们已经研究了示例,您可能想知道:如果保存NumPy文件具有相同的功能,为什么要使用HDF5? 让我们深入了解HDF5格式的功能。

从HDF5文件中选择性读取

到目前为止,我们已经看到,当我们读取数据集时,我们还没有从磁盘读取数据,而是创建了指向硬盘上特定位置的链接。 例如,如果我们显式读取数据集的前10个元素,我们可以看到会发生什么:

 with h5py.File('random.hdf5', 'r') as f: data_set = f['default'] data = data_set[:10] print(data[1]) print(data_set[1]) 

我们将代码分成不同的行以使其更加明确,但是您可以在项目中更加综合。 在上面的几行中,我们首先读取文件,然后读取默认数据集。 我们将数据集的前10个元素分配给data变量。 关闭文件后(结束时),我们可以访问存储在数据中的值,但是data_set将引发错误。 请注意,只有在我们明确访问数据集的前10个元素时,才从磁盘读取数据。 如果查看data和data_set类型,您会发现它们确实有所不同。 第一个是NumPy数组,第二个是h5py DataSet。

在更复杂的情况下,相同的行为也很重要。 让我们创建一个新文件,这次包含两个数据集,然后基于另一个数据集选择其中一个的元素。 让我们开始创建一个新文件并存储数据。 这部分是最简单的:

 import h5py import numpy as np arr1 = np.random.randn(10000) arr2 = np.random.randn(10000) with h5py.File('complex_read.hdf5', 'w') as f: f.create_dataset('array_1', data=arr1) f.create_dataset('array_2', data=arr2) 

我们有两个名为array_1和array_2的数据集,每个数据集包含一个随机的NumPy数组。 我们要读取与array_1值为正的元素相对应的array_2值。 我们可以尝试做这样的事情:

 with h5py.File('complex_read.hdf5', 'r') as f: d1 = f['array_1'] d2 = f['array_2'] data = d2[d1>0] 

但这是行不通的。 d1是一个数据集,不能与整数进行比较。 唯一的方法是实际从磁盘读取数据,然后进行比较。 因此,我们得到如下信息:

 with h5py.File('complex_read.hdf5', 'r') as f: d1 = f['array_1'] d2 = f['array_2'] data = d2[d1[:]>0] 

当我们执行d1 [:]时,第一个数据集d1已完全加载到内存中,但是从第二个数据集d2中,我们仅获取了一些元素。 如果d1数据集太大而无法完全加载到内存中,则可以在循环中进行工作。

 with h5py.File('complex_read.hdf5', 'r') as f: d1 = f['array_1'] d2 = f['array_2'] data = [] for i in range(len(d1)): if d1[i] > 0: data.append(d2[i]) print('The length of data with a for loop: {}'.format(len(data))) 

当然,逐项读取并将项添加到列表的效率存在问题,但这是使用HDF5优于文本或NumPy文件的最大优点之一的一个很好的例子。 在循环内部,我们仅将一个元素加载到内存中。 在我们的示例中,每个元素只是一个数字,但可以是任何东西:从文本到图像或视频。

与往常一样,根据您的应用程序,您必须决定是否要将整个阵列读取到内存中。 有时,您在具有大量内存的特定计算机上运行模拟,但笔记本电脑的特性却不同,因此您不得不读取部分数据。 请记住,从硬盘驱动器读取相对较慢,尤其是如果您使用HDD而不是SDD磁盘,或者从网络驱动器读取甚至更长的时间。

选择性地写入HDF5文件

在以上示例中,我们在创建数据后就将其添加到数据集中。 但是,对于许多应用程序,您需要在生成过程中保存数据。 HDF5允许您以与读取数据几乎相同的方式保存数据。 让我们看看如何创建一个空数据集并向其中添加一些数据。

 arr = np.random.randn(100) with h5py.File('random.hdf5', 'w') as f: dset = f.create_dataset("default", (1000,)) dset[10:20] = arr[50:60] 

前两行与以前相同,除了create_dataset 。 创建数据时我们不会添加数据,我们只会创建一个空数据集,最多可容纳1000个元素。 按照与以前相同的逻辑,当我们从数据集中读取某些元素时,只有在将值分配给dset变量的某些元素时,我们才实际写入磁盘。 在上面的示例中,我们仅将值分配给数组的子集,索引从10到19。

警告

将值分配给数据集时,写入磁盘的内容并不完全正确。 确切的时间取决于几个因素,包括操作系统的状态。 如果程序关闭太快,则可能会记录所有内容。 始终使用close()方法非常重要,如果分阶段编写,还可以使用flush()强制输入。 使用with可以防止很多写问题。

如果您读取文件并打印数据集的前20个值,则将看到它们都是零,除了索引10到19。存在一个常见错误,这可能导致明显的头痛。 以下代码不会将任何内容保存到磁盘:

 arr = np.random.randn(1000) with h5py.File('random.hdf5', 'w') as f: dset = f.create_dataset("default", (1000,)) dset = arr 

此错误总是会引起很多问题,因为在尝试读取结果之前,您不会理解自己没有写任何东西。 这里的问题是,您没有指定要存储数据的位置,而只是用NumPy数组覆盖了dset变量。 由于数据集和数组的长度相同,因此应使用dset [:] = arr。 该错误发生的次数比您想像的要多,并且由于从技术上讲这不是错误,因此您不会在终端上看到任何错误,并且数据将为零。

到现在为止,我们一直都在使用一维数组,但我们不仅限于它们。 例如,假设我们要使用2D数组,我们可以简单地执行以下操作:

 dset = f.create_dataset('default', (500, 1024)) 

这样我们就可以将数据存储在500x1024数组中。 要使用数据集,我们可以使用与以前相同的语法,但要考虑到第二维:

 dset[1,2] = 1 dset[200:500, 500:1024] = 123 

指定数据类型以优化空间

到目前为止,我们仅考察了HDF5所提供功能的冰山一角。 除了要保留的数据长度之外,您还可以指定数据类型以优化空间。 h5py文档包含所有受支持类型的列表,此处仅显示其中几种。 同时,我们将在一个文件中处理多个数据集。

 with h5py.File('several_datasets.hdf5', 'w') as f: dset_int_1 = f.create_dataset('integers', (10, ), dtype='i1') dset_int_8 = f.create_dataset('integers8', (10, ), dtype='i8') dset_complex = f.create_dataset('complex', (10, ), dtype='c16') dset_int_1[0] = 1200 dset_int_8[0] = 1200.1 dset_complex[0] = 3 + 4j 

在上面的示例中,我们创建了三个不同的数据集,每个数据集都有不同的类型。 1个字节的整数,8个字节的整数和16个字节的复数。 即使我们的数据集最多可以包含10个元素,我们也只能存储一个数字。 您可以读取值并查看实际保存的内容。 在此应注意,1字节的整数应四舍五入为127(而不是1200),而8字节的整数应四舍五入为1200(而不是1200.1)。

如果您曾经使用C或Fortran等语言进行编程,则可能知道不同类型的数据的含义。 但是,如果您一直使用Python,那么在未明确声明要使用的数据类型的情况下,您可能不会遇到任何问题。 重要的是要记住,字节数告诉您可以保存多少个不同的数字。 如果使用1个字节,则有8位,因此可以存储2 ^ 8个不同的数字。 在上面的示例中,整数都是正数,负数和0。使用1个字节的整数时,可以存储-128到127之间的值,它们总共是2 ^ 8个可能的数字。 这等效于使用8个字节,但具有广泛的数字范围。

选择的数据类型将影响其大小。 首先,让我们看一个简单的例子。 让我们创建三个文件,每个文件具有一个包含100,000个元素的数据集,但是具有不同的数据类型。 我们将在其中保存相同的数据,然后比较它们的大小。 我们创建一个随机数组,分配给每个数据集以填充内存。 请记住,数据将转换为数据集中指定的格式。

 arr = np.random.randn(100000) f = h5py.File('integer_1.hdf5', 'w') d = f.create_dataset('dataset', (100000,), dtype='i1') d[:] = arr f.close() f = h5py.File('integer_8.hdf5', 'w') d = f.create_dataset('dataset', (100000,), dtype='i8') d[:] = arr f.close() f = h5py.File('float.hdf5', 'w') d = f.create_dataset('dataset', (100000,), dtype='f16') d[:] = arr f.close() 

当您检查每个文件的大小时,您将获得类似以下内容的信息:

档案文件尺寸(b)
整数_1102144
integer_9802144
飘浮1602144

大小和数据类型之间的关系很清楚。 当您从1字节到8字节的整数时,文件大小增加8倍,类似地,当您移至16字节时,它占用的空间大约增加16倍。 但是空间并不是要考虑的唯一重要因素;还必须考虑将数据写入磁盘所需的时间。 您必须写的越多,花费的时间就越长。 根据您的应用程序,优化数据的读取和写入可能至关重要。

请注意:如果使用错误的数据类型,则可能还会丢失信息。 例如,如果您有8个字节的整数,并将它们存储为1个字节的整数,则它们的值将被截断。 在实验室工作时,通常会使用创建不同类型数据的设备。 有些DAQ卡具有16位,有些相机可以8位,但是有些可以24位。重要的是要注意数据类型,但这也是Python开发人员可能不会考虑的问题,因为您不需要声明类型。

还记得有趣的是,默认的NumPy数组将以每个元素8个字节(64位)的形式浮动。 例如,如果用零初始化一个数组以存储数据,则该问题应该只有2个字节,这可能是一个问题。 数组本身的类型不会改变,如果在创建数据集时保存数据(添加数据= my_array),则默认格式为“ f8”,它是一个数组,但不是实际数据,

如果您在简单的应用程序中使用Python,那么思考数据类型就不会经常发生。 但是,您应该知道数据类型存在,以及它们可能对结果产生什么影响。 您可能有大型硬盘,并且实际上并不关心文件存储,但是当您关心保存速度时,除了优化代码的各个方面(包括数据类型)之外,别无其他方法。

资料压缩

保存数据时,可以选择使用不同算法的压缩。 h5py软件包支持多个压缩过滤器,例如GZIP,LZF和SZIP。 使用压缩过滤器之一时,数据将在到达磁盘的过程中被处理,并且在读取时将被解压缩。 因此,代码中没有特殊更改。 我们可以重复相同的实验,保存不同类型的数据,但是要使用压缩过滤器。 我们的代码如下所示:

 import h5py import numpy as np arr = np.random.randn(100000) with h5py.File('integer_1_compr.hdf5', 'w') as f: d = f.create_dataset('dataset', (100000,), dtype='i1', compression="gzip", compression_opts=9) d[:] = arr with h5py.File('integer_8_compr.hdf5', 'w') as f: d = f.create_dataset('dataset', (100000,), dtype='i8', compression="gzip", compression_opts=9) d[:] = arr with h5py.File('float_compr.hdf5', 'w') as f: d = f.create_dataset('dataset', (100000,), dtype='f16', compression="gzip", compression_opts=9) d[:] = arr 

我们选择gzip,因为它在所有平台上都受支持。 compression_opts选项指定压缩级别。 级别越高,数据占用的空间越少,但是处理器应该工作的时间越长。 默认压缩级别为4。我们可以根据压缩级别看到文件中的差异:

型式无压缩压缩9压缩4
整数_11021442801630463
integer_88021444332957971
飘浮160214414695801469868

压缩对整个数据数组的影响比对浮点数据集的影响要明显得多。 我留给您指出为什么压缩在前两种情况下效果很好,而在后一种情况下效果不佳。 提示:您应检查实际存储的数据。

读取压缩数据不会更改上述任何代码。 HDF5核心库将负责使用适当的算法从压缩数据集中提取数据。 因此,如果实现压缩以进行保存,则无需更改用于读取的代码。

数据压缩是您应与数据处理的所有其他方面一起考虑的附加工具。 您必须考虑额外的处理器时间和有效的压缩率,以便评估自己应用程序中数据压缩的好处。 它对下游代码是透明的,这使得测试和找到最佳解决方案变得异常容易。

调整数据集大小

在进行实验时,有时无法找出您的数据量。 想象一下,您正在录制电影,也许您会在一秒钟后或一个小时后停止播放。 幸运的是,HDF5允许您动态地调整数据集的大小,而计算量却很少。 数据集的长度可以超过最大大小。 使用maxshape关键字创建数据集时,将指定此最大大小:

 import h5py import numpy as np with h5py.File('resize_dataset.hdf5', 'w') as f: d = f.create_dataset('dataset', (100, ), maxshape=(500, )) d[:100] = np.random.randn(100) d.resize((200,)) d[100:200] = np.random.randn(100) with h5py.File('resize_dataset.hdf5', 'r') as f: dset = f['dataset'] print(dset[99]) print(dset[199]) 

首先,创建一个数据集以存储100个值,并将最大大小设置为500个值。 保存第一批值后,可以扩展数据集以保存下一个100。可以重复此过程,直到获得具有500个值的数据集。 , N- . , , .

, , . , - ( , , ):

 with h5py.File('resize_dataset.hdf5', 'a') as f: dset = f['dataset'] dset.resize((300,)) dset[:200] = 0 dset[200:300] = np.random.randn(100) with h5py.File('resize_dataset.hdf5', 'r') as f: dset = f['dataset'] print(dset[99]) print(dset[199]) print(dset[299]) 

, , 200 200 299. , , .

, , , . 2D-, , — , 2D-. 3- HDF-, . , :

 with h5py.File('movie_dataset.hdf5', 'w') as f: d = f.create_dataset('dataset', (1024, 1024, 1), maxshape=(1024, 1024, None )) d[:,:,0] = first_frame d.resize((1024,1024,2)) d[:,:,1] = second_frame 

1024x1024 , . , , . maxshape None.

(Chunks)

, . (chunk) , .. . , , . , :

 dset = f.create_dataset("chunked", (1000, 1000), chunks=(100, 100)) 

, dset [0: 100,0: 100] . dset [200: 300, 200: 300], dset [100: 200, 400: 500] . . h5py, :

(Chunking) . 10 KiB 1 MiB, . , , .

(auto-chunking), . , maxshape. :

 dset = f.create_dataset("autochunk", (1000, 1000), chunks=True) 

(Groups)

. HDF5, , . (groups), , . , :

 import numpy as np import h5py arr = np.random.randn(1000) with h5py.File('groups.hdf5', 'w') as f: g = f.create_group('Base_Group') gg = g.create_group('Sub_Group') d = g.create_dataset('default', data=arr) dd = gg.create_dataset('default', data=arr) 

Base_Group , Sub_Group. default . , , :

 with h5py.File('groups.hdf5', 'r') as f: d = f['Base_Group/default'] dd = f['Base_Group/Sub_Group/default'] print(d[1]) print(dd[1]) 

, : Base_Group/default Base_Group/Sub_Group/default. , , , , . — keys():

 with h5py.File('groups.hdf5', 'r') as f: for k in f.keys(): print(k) 

, , for-. , . visit(), :

 def get_all(name): print(name) with h5py.File('groups.hdf5', 'r') as f: f.visit(get_all) 

, get_all , , name. visit, get_all. visit , , None, . , , Sub_Group, get_all :

 def get_all(name): if 'Sub_Group' in name: return name with h5py.File('groups.hdf5', 'r') as f: g = f.visit(get_all) print(g) 

visit , , None, , get_all. Sub_Group, get_all , Sub_Group . , g , , :

 with h5py.File('groups.hdf5', 'r') as f: g_name = f.visit(get_all) group = f[g_name] 

. — , visititems, : name object. :

 def get_objects(name, obj): if 'Sub_Group' in name: return obj with h5py.File('groups.hdf5', 'r') as f: group = f.visititems(get_objects) data = group['default'] print('First data element: {}'.format(data[0])) 

visititems , , , . , , . . , , .

HDF5

, HDF5, , . , , , , , , .. . , , 200x300x250. , , , , — , .

HDF5 -. .

 import time import numpy as np import h5py import os arr = np.random.randn(1000) with h5py.File('groups.hdf5', 'w') as f: g = f.create_group('Base_Group') d = g.create_dataset('default', data=arr) g.attrs['Date'] = time.time() g.attrs['User'] = 'Me' d.attrs['OS'] = os.name for k in g.attrs.keys(): print('{} => {}'.format(k, g.attrs[k])) for j in d.attrs.keys(): print('{} => {}'.format(j, d.attrs[j])) 

, attrs . , , . , . , , , update:

 with h5py.File('groups.hdf5', 'w') as f: g = f.create_group('Base_Group') d = g.create_dataset('default', data=arr) metadata = {'Date': time.time(), 'User': 'Me', 'OS': os.name,} f.attrs.update(metadata) for m in f.attrs.keys(): print('{} => {}'.format(m, f.attrs[m])) 

, , hdf5, . , . hdf5, . Python -. JSON, , , , pickle.

 import json with h5py.File('groups_dict.hdf5', 'w') as f: g = f.create_group('Base_Group') d = g.create_dataset('default', data=arr) metadata = {'Date': time.time(), 'User': 'Me', 'OS': os.name,} m = g.create_dataset('metadata', data=json.dumps(metadata)) 

, . , . , json.dumps, . , HDF5. , json.loads:

巨蟒
 with h5py.File('groups_dict.hdf5', 'r') as f: metadata = json.loads(f['Base_Group/metadata'][()]) for k in metadata: print('{} => {}'.format(k, metadata[k])) 

json , . YAML, XML .. , , , attr , , .

HDF5

, . , , , . HDF , , , , , . , HDF .

HDF5 . , , . , . . SQL, HDFql , SQL HDF5.

. , , - , , . , . , , .

HDF5 — , . , , , , . HDF5 — , , .

结束

, .

Source: https://habr.com/ru/post/zh-CN416309/


All Articles