为什么使用zarr ?
在处理太大无法一次性加载到内存的NumPy数组时,可以使用分块处理,可以透明地处理,也可以仅从磁盘逐个加载一个块。无论哪种方式,您都需要以某种方式将数组存储在磁盘上。
mmap(),通过numpy.memmap() API,让您透明地将磁盘上的文件视为全部在内存中。 Zarr和HDF5,一对相似的存储格式,让您按需加载和存储数组的压缩块。
缓存机制的优点
提高读取速度: 当数据首次从磁盘读取到内存时,会存储一份副本到操作系统的缓存中。如果稍后再次读取相同的数据,操作系统会直接从缓存中获取,避免了再次从磁盘读取的时间消耗,因此读取速度大大加快。
减少磁盘访问: 由于数据在缓存中已经存在,所以避免了频繁访问磁盘的需要。这有助于减少对磁盘的读写操作,降低了磁盘的负载和磁盘的磨损,延长了硬件的使用寿命。
提高系统响应性: 缓存可以加速数据访问,使得系统更加响应快速。对于需要频繁访问磁盘的应用程序,通过减少磁盘I/O操作,可以使系统更加流畅和高效。
自动管理内存: 缓存系统会自动管理缓存中的数据,当系统需要释放内存以供其他用途时,会根据一定的策略清理不再使用的数据,以确保系统的内存使用效率和性能。
方法1:mmap
mmap()
是一种将磁盘上的文件映射到内存中的方法,让你可以将磁盘上的文件视为内存中的数组。操作系统会根据数据是否在缓存中来透明地读取/写入数据,这样你就可以直接访问数据而无需考虑它是在内存中还是在磁盘上:
如果数据在缓存中,那么你可以直接访问它。 如果数据只在磁盘上,访问速度会较慢,但你无需考虑它,因为数据会被透明地加载。
NumPy内置了对mmap()
的支持,你可以使用np.memmap()
函数来创建一个内存映射的数组:
import numpy as np
array = np.memmap("mydata/myarray.arr", mode="r",
dtype=np.int16, shape=(1024, 1024))
运行这段代码,你将得到一个数组,它会透明地从缓存中返回内存,或者从磁盘上读取数据。
虽然 mmap() 在某些情况下效果很好,但它也存在一些局限性:
数据必须位于文件系统中。无法从像AWS S3这样的 Blob 存储中加载数据。 如果要加载足够多的数据,从磁盘读取或写入可能会成为瓶颈。请记住,磁盘比 RAM 慢得多。仅因为磁盘读取和写入是透明的并不意味着它们更快。 如果你有一个 N 维数组,想要沿不同轴切片,只有与默认结构对齐的切片才会很快。其余的将需要大量从磁盘读取的操作。
方法2:zarr
Zarr 更加现代化和灵活,但在 Python 之外的支持较少;Z5 是 C++ 的一个实现。我认为在大多数情况下,除非你需要 HDF5 的多语言支持,否则 Zarr 是一个更好的选择,例如 Zarr 有更好的线程支持。
Zarr 允许你存储数据块并将它们作为数组加载到内存中,并将数据写回这些数据块。
以下是使用 Zarr 加载数组的方法:
import zarr
import numpy as np
z = zarr.open('example.zarr', mode='a',
shape=(1024, 1024),
chunks=(512, 512), dtype=np.int16)
type(z)
# <class 'zarr.core.Array'>
type(z[100:200])
# <class 'numpy.ndarray'>
Zarr 解决了我们之前讨论的 mmap() 的限制:
你可以将数据块存储在磁盘上、AWS S3 上,或者任何提供键/值查找的存储系统中。 数据块的大小和形状由你决定,因此你可以将它们结构化,以允许跨多个轴进行有效读取。这也适用于 HDF5。 数据块可以被压缩;这也适用于 HDF5。
zarr介绍
Zarr是一种用于存储分块、压缩、N维数组的格式,灵感来自HDF5、h5py和bcolz。
https://zarr.readthedocs.io/en/stable/index.html
使用任何NumPy数据创建N维数组。 可以沿任何维度对数组进行分块。 将数组存储在内存中、磁盘上、Zip文件内、S3上等位置。 可以从多个线程或进程并发地读取数组。 可以从多个线程或进程并发地向数组写入数据。
zarr使用
https://zarr.readthedocs.io/en/stable/tutorial.html
创建数组
Zarr提供了多个用于创建数组的函数。
import zarr
z = zarr.zeros((10000, 10000), chunks=(1000, 1000), dtype='i4')
print(z)
上面的代码创建了一个二维数组,其中包含10000行和10000列的32位整数,被分成块,每个块有1000行和1000列(因此总共会有100个块)。
要获取完整的数组创建例程列表,请参阅zarr.creation
模块的文档。
读取和写入数据
Zarr数组支持与NumPy数组类似的接口用于读取和写入数据。例如,可以使用标量值填充整个数组:
z[:] = 42
也可以写入数组的特定区域,例如:
import numpy as np
z[0, :] = np.arange(10000)
z[:, 0] = np.arange(10000)
可以通过切片来检索数组的内容,这将把请求的区域加载到内存中作为一个NumPy数组,例如:
print(z[0, 0])
print(z[-1, -1])
print(z[0, :])
print(z[:, 0])
print(z[:])
持久化数组
在上面的示例中,数组的每个块的压缩数据都存储在主内存中。Zarr数组也可以存储在文件系统中,从而实现数据在会话之间的持久化。例如:
z1 = zarr.open('data/example.zarr', mode='w', shape=(10000, 10000),
chunks=(1000, 1000), dtype='i4')
上面的数组将其配置元数据和所有压缩的块数据存储在一个名为data/example.zarr
的目录中。虽然函数被称为open
,但不需要关闭数组:数据会自动刷新到磁盘,并且每当修改数组时,文件都会自动关闭。
持久化数组支持相同的接口来读取和写入数据,例如:
z1[:] = 42
z1[0, :] = np.arange(10000)
z1[:, 0] = np.arange(10000)
检查数据是否已经被写入并且可以再次读取:
z2 = zarr.open('data/example.zarr', mode='r')
print(np.all(z1[:] == z2[:]))
调整大小和追加
Zarr数组可以调整大小,这意味着可以增加或减少其任何维度的长度。例如:
z = zarr.zeros(shape=(10000, 10000), chunks=(1000, 1000))
z[:] = 42
z.resize(20000, 10000)
print(z.shape)
当数组调整大小时,底层数据不会以任何方式重新排列。如果一个或多个维度被缩小,那么任何超出新数组形状的块将从底层存储中被删除。
为了方便起见,Zarr数组还提供了一个append()
方法,用于将数据追加到任何轴上。例如:
a = np.arange(10000000, dtype='i4').reshape(10000, 1000)
z = zarr.array(a, chunks=(1000, 100))
print(z.shape)
z.append(a)
print(z.shape)
z.append(np.vstack([a, a]), axis=1)
print(z.shape)
压缩器
Zarr可以使用多种不同的压缩器,可以通过所有数组创建函数接受的compressor
关键字参数来提供不同的压缩器。例如:
from numcodecs import Blosc
compressor = Blosc(cname='zstd', clevel=3, shuffle=Blosc.BITSHUFFLE)
data = np.arange(100000000, dtype='i4').reshape(10000, 10000)
z = zarr.array(data, chunks=(1000, 1000), compressor=compressor)
print(z.compressor)
上面的数组将使用Blosc作为主要压缩器,在Blosc内部使用Zstandard算法(压缩级别为3),并且应用了比特洗牌过滤器。
当使用压缩器时,获取一些压缩比的诊断信息可能很有用。Zarr数组提供了一个info
属性,用于打印一些诊断信息,例如:
print(z.info)
如果不指定压缩器,默认情况下Zarr使用Blosc压缩器。Blosc通常非常快速,并且可以以多种方式配置,以提高不同类型数据的压缩比。事实上,Blosc是一种"元压缩器",这意味着它可以在内部使用多种不同的压缩算法来压缩数据。Blosc还提供了高度优化的字节和位洗牌过滤器的实现,这可以提高某些数据的压缩比。可以通过以下方式获取Blosc内部可用的压缩库列表:
from numcodecs import blosc
print(blosc.list_compressors())
除了Blosc之外,还可以使用其他压缩库。例如,下面是一个使用Zstandard压缩,级别为1的数组:
from numcodecs import Zstd
z = zarr.array(np.arange(100000000, dtype='i4').reshape(10000, 10000),
chunks=(1000, 1000), compressor=Zstd(level=1))
print(z.compressor)
这里是一个使用LZMA的示例,其中包含LZMA内置的delta过滤器的自定义过滤器管道:
import lzma
lzma_filters = [dict(id=lzma.FILTER_DELTA, dist=4),
dict(id=lzma.FILTER_LZMA2, preset=1)]
from numcodecs import LZMA
compressor = LZMA(filters=lzma_filters)
z = zarr.array(np.arange(100000000, dtype='i4').reshape(10000, 10000),
chunks=(1000, 1000), compressor=compressor)
print(z.compressor)
可以通过设置zarr.storage.default_compressor
变量来更改默认压缩器的值,例如:
import zarr.storage
from numcodecs import Zstd, Blosc
# 切换到使用Zstandard
zarr.storage.default_compressor = Zstd(level=1)
z = zarr.zeros(100000000, chunks=1000000)
print(z.compressor)
# 切换回Blosc默认设置
zarr.storage.default_compressor = Blosc()
要禁用压缩,请在创建数组时将compressor=None
设置,例如:
z = zarr.zeros(100000000, chunks=1000000, compressor=None)
print(z.compressor is None)
过滤器
在某些情况下,通过对数据进行某种方式的转换,可以改善压缩效果。例如,如果附近的值倾向于相关联,那么在每个数字值内部对字节进行洗牌,或者存储相邻值之间的差异,可能会增加压缩比。
以下是一个使用Blosc压缩器的delta过滤器的示例:
from numcodecs import Blosc, Delta
filters = [Delta(dtype='i4')]
compressor = Blosc(cname='zstd', clevel=1, shuffle=Blosc.SHUFFLE)
data = np.arange(100000000, dtype='i4').reshape(10000, 10000)
z = zarr.array(data, chunks=(1000, 1000), filters=filters, compressor=compressor)
print(z.info)
上面的示例中,数组将使用Blosc作为主要压缩器,并使用delta过滤器。过滤器会在压缩之前对数据进行转换。通过调用z.info
属性,可以获取有关数组的一些诊断信息,包括使用的过滤器和压缩器以及存储的字节数等信息。
分组
组
Zarr支持通过组对数组进行分层组织。与数组一样,组可以存储在内存中、磁盘上,或通过其他支持类似接口的存储系统中。
要创建一个组,可以使用:func:zarr.group
函数::
root = zarr.group()
print(root)
组与h5py <https://www.h5py.org/>
_中的Group类具有类似的API。例如,组可以包含其他组::
foo = root.create_group('foo')
bar = foo.create_group('bar')
组也可以包含数组,例如::
z1 = bar.zeros('baz', shape=(10000, 10000), chunks=(1000, 1000), dtype='i4')
print(z1)
在HDF5术语中,数组称为“数据集”(dataset)。为了与h5py兼容,Zarr组还实现了create_dataset()
和require_dataset()
方法,例如::
z = bar.create_dataset('quux', shape=(10000, 10000), chunks=(1000, 1000), dtype='i4')
print(z)
可以使用后缀表示法访问组的成员,例如::
print(root['foo'])
'/'字符可以用于一次访问层次结构的多个级别,例如::
print(root['foo/bar'])
print(root['foo/bar/baz'])
:func:zarr.hierarchy.Group.tree
方法可用于打印层次结构的树表示,例如::
root.tree()
:func:zarr.convenience.open
函数提供了一个方便的方式来在文件系统上的目录中创建或重新打开一个组,子组存储在子目录中,例如::
root = zarr.open('data/group.zarr', mode='w')
print(root)
z = root.zeros('foo/bar/baz', shape=(10000, 10000), chunks=(1000, 1000), dtype='i4')
print(z)
组可以作为上下文管理器使用(在with
语句中)。如果底层存储有close
方法,在退出时将调用它。
块优化
一般来说,至少为1兆字节(1M)未压缩大小的块似乎提供了更好的性能,至少在使用Blosc压缩库时是这样。
最佳的块形状取决于您如何访问数据。例如,对于二维数组,如果您只是沿着第一个维度获取切片,那么就在第二个维度上进行分块。如果您知道要在整个维度上进行分块,则可以在chunks
参数中使用None
或-1
,例如::
>>> z1 = zarr.zeros((10000, 10000), chunks=(100, None), dtype='i4')
>>> z1.chunks
(100, 10000)
或者,如果您只是沿着第二个维度获取切片,则在第一个维度上进行分块,例如:
>>> z2 = zarr.zeros((10000, 10000), chunks=(None, 100), dtype='i4')
>>> z2.chunks
(10000, 100)
并行计算
Zarr数组已经被设计用于在并行计算中作为数据源或数据汇的用途。所谓的数据源意味着可以进行多个并发读取操作。所谓的数据汇意味着可以进行多个并发写入操作,其中每个写入者更新数组的不同区域。
多线程和多进程并行性都是可能的。对于大多数存储和检索操作来说,瓶颈通常是压缩/解压缩,而且在这些操作期间,Python全局解释器锁(GIL)会尽可能地释放,因此Zarr通常不会阻止其他Python线程运行。
# 竞赛交流群 邀请函 #

每天大模型、算法竞赛、干货资讯





