NumPy 1.26 中文官方指南(三)(3)https://developer.aliyun.com/article/1510618
数组接口协议
数组接口协议定义了类似数组的对象重复使用对方的数据缓冲区的方式。其实现依赖于以下属性或方法的存在:
__array_interface__
:一个包含数组-like 对象的形状,元素类型,和可选的数据缓冲区地址和步幅的 Python 字典;__array__()
:返回类似数组的对象的 NumPy ndarray 视图的方法;
可以直接检查__array_interface__
属性:
>>> import numpy as np >>> x = np.array([1, 2, 5.0, 8]) >>> x.__array_interface__ {'data': (94708397920832, False), 'strides': None, 'descr': [('', '<f8')], 'typestr': '<f8', 'shape': (4,), 'version': 3}
__array_interface__
属性还可以用于原地操作对象数据:
>>> class wrapper(): ... pass ... >>> arr = np.array([1, 2, 3, 4]) >>> buf = arr.__array_interface__ >>> buf {'data': (140497590272032, False), 'strides': None, 'descr': [('', '<i8')], 'typestr': '<i8', 'shape': (4,), 'version': 3} >>> buf['shape'] = (2, 2) >>> w = wrapper() >>> w.__array_interface__ = buf >>> new_arr = np.array(w, copy=False) >>> new_arr array([[1, 2], [3, 4]])
我们可以检查arr
和new_arr
是否共享相同的数据缓冲区:
>>> new_arr[0, 0] = 1000 >>> new_arr array([[1000, 2], [ 3, 4]]) >>> arr array([1000, 2, 3, 4])
__array__()
方法
__array__()
方法确保任何类似 NumPy 的对象(数组,任何暴露数组接口的对象,其__array__()
方法返回数组或任何嵌套序列的对象)都可以用作 NumPy 数组。如果可能的话,这意味着使用__array__()
来创建类似数组对象的 NumPy ndarray 视图。否则,这将复制数据到一个新的 ndarray 对象中。这并不是最佳情况,因为将数组强制转换为 ndarrays 可能会导致性能问题或创建需要复制和丢失元数据的情况,因为原始对象及其可能具有的任何属性/行为都会丢失。
要查看包括使用__array__()
的自定义数组实现的示例,请参见编写自定义数组容器。
DLPack 协议
DLPack协议定义了步进式 n 维数组对象的内存布局。它为数据交换提供了以下语法:
numpy.from_dlpack
函数,接受具有__dlpack__
方法的(数组)对象,并使用该方法来构建包含x
数据的新数组。- 数组对象上的
__dlpack__(self, stream=None)
和__dlpack_device__
方法将从from_dlpack
中调用,以查询数组所在的设备(可能需要传递正确的流,例如在多个 GPU 的情况下),并访问数据。
与缓冲协议不同,DLPack 允许交换包含设备上的数据(如 Vulkan 或 GPU)的数组。由于 NumPy 仅支持 CPU,因此只能转换数据存在于 CPU 上的对象。但其他库,如 PyTorch 和 CuPy,可以使用该协议在 GPU 上交换数据。
2. 在不转换的情况下操作外部对象
NumPy API 定义的第二组方法允许我们将一个 NumPy 函数的执行延迟到另一个数组库。
考虑以下函数。
>>> import numpy as np >>> def f(x): ... return np.mean(np.exp(x))
请注意,np.exp
是一个 ufunc,这意味着它按元素方式在 ndarrays 上操作。另一方面,np.mean
沿数组的一个轴操作。
我们可以直接将 f
应用于 NumPy ndarray 对象:
>>> x = np.array([1, 2, 3, 4]) >>> f(x) 21.1977562209304
我们希望这个函数在任何类似 NumPy 的数组对象上都能很好地工作。
NumPy 允许一个类通过以下接口指示它想通过自定义方式处理计算:
__array_ufunc__
:允许第三方对象支持和覆盖 ufuncs。__array_function__
:用于处理通用函数的 NumPy 功能的总称,该功能不受通用函数协议__array_ufunc__
的覆盖。
只要外部对象实现了 __array_ufunc__
或 __array_function__
协议,就可以在它们上操作而无需进行显式转换。
__array_ufunc__
协议
通用函数(ufunc 简写)是一个“向量化”包装器,用于接收固定数量的特定输入并产生固定数量的特定输出的函数。如果不是所有的输入参数都是 ndarray,ufunc 的输出(及其方法)不一定是 ndarray。实际上,如果任何输入定义了 __array_ufunc__
方法,控制权将完全传递给该函数,即通用函数被覆盖。在该(非 ndarray)对象上定义的 __array_ufunc__
方法可以访问 NumPy ufunc。由于通用函数有明确定义的结构,外部的 __array_ufunc__
方法可以依赖于类似 .at()
、.reduce()
等的 ufunc 属性。
通过重写默认的 ndarray.__array_ufunc__
方法,子类可以覆盖在其上执行 NumPy ufuncs 时的操作方式。这个方法会代替 ufunc 被执行,并应该返回操作的结果,或者在请求的操作未被实现时返回 NotImplemented
。
__array_function__
协议
为了实现足够的 NumPy API 覆盖范围以支持下游项目,需要超出__array_ufunc__
并实现一种协议,允许 NumPy 函数的参数控制并将执行转移到另一个函数(例如 GPU 或并行实现),以一种安全和一致的方式跨项目进行。
__array_function__
的语义与 __array_ufunc__
非常相似,只是操作由任意可调用对象指定,而不是 ufunc 实例和方法。具体详情请参见NEP 18 — NumPy 高级数组函数的调度机制。
__array_ufunc__
协议
通用函数(或简称 ufunc)是一个对函数进行“矢量化”封装的函数,它接受固定数量的特定输入,并产生固定数量的特定输出。 如果所有输入参数都不是 ndarray,则 ufunc 的输出(及其方法)未必是 ndarray。 实际上,如果任何输入定义了__array_ufunc__
方法,则完全将控制权传递给该函数,即 ufunc 被覆盖。定义在那个(非 ndarray)对象上的__array_ufunc__
方法可以访问 NumPy ufunc。 由于 ufuncs 有明确定义的结构,外部__array_ufunc__
方法可以依赖于 ufunc 属性,例如.at()
、.reduce()
和其他属性。
子类可以通过覆盖默认的ndarray.__array_ufunc__
方法来在执行 NumPy ufuncs 时改写其行为。这个方法将代替 ufunc 的执行,并应该返回操作的结果,或者如果请求的操作未实现,则返回NotImplemented
。
__array_function__
协议
为了实现足够的 NumPy API 覆盖范围以支持下游项目,需要超出__array_ufunc__
并实现一种协议,允许 NumPy 函数的参数控制并将执行转移到另一个函数(例如 GPU 或并行实现),以一种安全和一致的方式跨项目进行。
__array_function__
的语义与 __array_ufunc__
非常相似,只是操作由任意可调用对象指定,而不是 ufunc 实例和方法。具体详情请参见NEP 18 — NumPy 高级数组函数的调度机制。
3. 返回外部对象
第三种特性集意在使用 NumPy 函数实现,然后将返回值转换回外部对象的实例。__array_finalize__
和__array_wrap__
方法在幕后起作用,以确保可以根据需要指定 NumPy 函数的返回类型。
__array_finalize__
方法是 NumPy 提供的机制,允许子类处理创建新实例的各种方式。每当系统从数组的子类(子类型)的对象内部分配新数组时,就会调用此方法。它可用于在构造后更改属性,或者从“父级”更新元信息。
__array_wrap__
方法“包装了操作”,在允许任何对象(如用户定义的函数)设置其返回值类型和更新属性和元数据方面发挥作用。这可以看作是__array__
方法的相反。在实现__array_wrap__
的每个对象的末尾,将对具有最高数组优先级的输入对象调用此方法,或者如果指定了输出对象,则在输出对象上调用此方法。__array_priority__
属性用于确定在返回对象的 Python 类型存在多种可能性的情况下应返回什么类型的对象。例如,子类可能选择使用此方法将输出数组变换为子类实例并在返回数组给用户之前更新元数据。
有关这些方法的更多信息,请参阅子类化 ndarray 和 ndarray 子类型的特定特征。
互操作性示例
例子:Pandas Series
对象
考虑以下内容:
>>> import pandas as pd >>> ser = pd.Series([1, 2, 3, 4]) >>> type(ser) pandas.core.series.Series
现在,ser
不是一个 ndarray,但由于它实现了 array_ufunc 协议,我们可以将 ufuncs 应用于它,就好像它是一个 ndarray 一样:
>>> np.exp(ser) 0 2.718282 1 7.389056 2 20.085537 3 54.598150 dtype: float64 >>> np.sin(ser) 0 0.841471 1 0.909297 2 0.141120 3 -0.756802 dtype: float64
我们甚至可以对其他 ndarray 执行操作:
>>> np.add(ser, np.array([5, 6, 7, 8])) 0 6 1 8 2 10 3 12 dtype: int64 >>> f(ser) 21.1977562209304 >>> result = ser.__array__() >>> type(result) numpy.ndarray
例子:PyTorch 张量
PyTorch是一个针对使用 GPU 和 CPU 进行深度学习的优化张量库。PyTorch 数组通常被称为张量。张量类似于 NumPy 的 ndarray,只不过张量可以在 GPU 或其他硬件加速器上运行。实际上,张量和 NumPy 数组通常可以共享相同的底层内存,消除了复制数据的需求。
>>> import torch >>> data = [[1, 2],[3, 4]] >>> x_np = np.array(data) >>> x_tensor = torch.tensor(data)
注意x_np
和x_tensor
是不同类型的对象:
>>> x_np array([[1, 2], [3, 4]]) >>> x_tensor tensor([[1, 2], [3, 4]])
但是,我们可以将 PyTorch 张量视为 NumPy 数组,而无需显式转换:
>>> np.exp(x_tensor) tensor([[ 2.7183, 7.3891], [20.0855, 54.5982]], dtype=torch.float64)
此外,还要注意此函数的返回类型与初始数据类型兼容。
警告
虽然混合使用 ndarray 和张量可能很方便,但不建议这样做。它对非 CPU 张量不起作用,并且在一些特殊情况下会产生意外行为。用户应优先显式将 ndarray 转换为张量。
注意
PyTorch 不实现__array_function__
或__array_ufunc__
。在底层,Tensor.__array__()
方法返回一个 NumPy ndarray,作为张量数据缓冲区的视图。有关详细信息,请参阅此问题和torch_function 实现。
还要注意,即使torch.Tensor
不是 ndarray 的子类,我们也可以在这里看到__array_wrap__
的实际运行:
>>> import torch >>> t = torch.arange(4) >>> np.abs(t) tensor([0, 1, 2, 3])
PyTorch 实现了__array_wrap__
以便从 NumPy 函数获取张量,并且我们可以直接修改它以控制从这些函数返回哪种类型的对象。
例:CuPy 数组
CuPy 是用于 GPU 加速计算的 NumPy/SciPy 兼容数组库。CuPy 通过实现cupy.ndarray
实现了 NumPy 接口的子集,与 NumPy ndarrays 对应。
>>> import cupy as cp >>> x_gpu = cp.array([1, 2, 3, 4])
cupy.ndarray
对象实现了__array_ufunc__
接口。这使得可以对 CuPy 数组应用 NumPy ufunc(这将将操作推迟到对应的 CuPy CUDA/ROCm 实现的 ufunc):
>>> np.mean(np.exp(x_gpu)) array(21.19775622)
请注意,这些操作的返回类型仍与初始类型保持一致:
>>> arr = cp.random.randn(1, 2, 3, 4).astype(cp.float32) >>> result = np.sum(arr) >>> print(type(result)) <class 'cupy._core.core.ndarray'>
cupy.ndarray
还实现了__array_function__
接口,这意味着可以进行诸如
>>> a = np.random.randn(100, 100) >>> a_gpu = cp.asarray(a) >>> qr_gpu = np.linalg.qr(a_gpu)
CuPy 在cupy.ndarray
对象上实现了许多 NumPy 函数,但不是全部。有关详细信息,请参阅CuPy 文档。
例:Dask 数组
Dask 是 Python 中用于并行计算的灵活库。Dask 数组使用分块算法实现了 NumPy ndarray 接口的子集,将大数组切割成许多小数组。这允许使用多个核心对大于内存的数组进行计算。
Dask 支持__array__()
和__array_ufunc__
。
>>> import dask.array as da >>> x = da.random.normal(1, 0.1, size=(20, 20), chunks=(10, 10)) >>> np.mean(np.exp(x)) dask.array<mean_agg-aggregate, shape=(), dtype=float64, chunksize=(), chunktype=numpy.ndarray> >>> np.mean(np.exp(x)).compute() 5.090097550553843
请注意
Dask 是延迟评估的,直到通过调用compute()
要求计算结果才会计算。
有关详细信息,请参阅Dask 数组文档以及Dask 数组与 NumPy 数组的互操作性范围。
例:DLPack
几个 Python 数据科学库实现了__dlpack__
协议。其中包括PyTorch和CuPy。可以在DLPack 文档的此页面找到实现此协议的库的完整列表。
将 PyTorch CPU 张量转换为 NumPy 数组:
>>> import torch >>> x_torch = torch.arange(5) >>> x_torch tensor([0, 1, 2, 3, 4]) >>> x_np = np.from_dlpack(x_torch) >>> x_np array([0, 1, 2, 3, 4]) >>> # note that x_np is a view of x_torch >>> x_torch[1] = 100 >>> x_torch tensor([ 0, 100, 2, 3, 4]) >>> x_np array([ 0, 100, 2, 3, 4])
导入的数组是只读的,因此写入或原地操作将失败:
>>> x.flags.writeable False >>> x_np[1] = 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: assignment destination is read-only
必须创建副本才能对导入的数组进行原地操作,但这将意味着复制内存。对于非常大的数组不要这样做:
>>> x_np_copy = x_np.copy() >>> x_np_copy.sort() # works
请注意
请注意,GPU 张量不能转换为 NumPy 数组,因为 NumPy 不支持 GPU 设备:
>>> x_torch = torch.arange(5, device='cuda') >>> np.from_dlpack(x_torch) Traceback (most recent call last): File "<stdin>", line 1, in <module> RuntimeError: Unsupported device in DLTensor.
但是,如果这两个库都支持数据缓冲区所在的设备,则可以使用__dlpack__
协议(例如PyTorch和CuPy):
>>> x_torch = torch.arange(5, device='cuda') >>> x_cupy = cupy.from_dlpack(x_torch)
同样,NumPy 数组可以转换为 PyTorch 张量:
>>> x_np = np.arange(5) >>> x_torch = torch.from_dlpack(x_np)
只读数组无法导出:
>>> x_np = np.arange(5) >>> x_np.flags.writeable = False >>> torch.from_dlpack(x_np) Traceback (most recent call last): File "<stdin>", line 1, in <module> File ".../site-packages/torch/utils/dlpack.py", line 63, in from_dlpack dlpack = ext_tensor.__dlpack__() TypeError: NumPy currently only supports dlpack for writeable arrays
示例:Pandas Series
对象
考虑以下内容:
>>> import pandas as pd >>> ser = pd.Series([1, 2, 3, 4]) >>> type(ser) pandas.core.series.Series
现在,ser
不是一个 ndarray,但因为它实现了__array_ufunc__
协议,我们就可以像处理 ndarray 一样对其应用 ufunc:
>>> np.exp(ser) 0 2.718282 1 7.389056 2 20.085537 3 54.598150 dtype: float64 >>> np.sin(ser) 0 0.841471 1 0.909297 2 0.141120 3 -0.756802 dtype: float64
我们甚至可以对其他 ndarrays 执行操作:
>>> np.add(ser, np.array([5, 6, 7, 8])) 0 6 1 8 2 10 3 12 dtype: int64 >>> f(ser) 21.1977562209304 >>> result = ser.__array__() >>> type(result) numpy.ndarray
示例:PyTorch 张量
PyTorch是一个用于在 GPU 和 CPU 上进行深度学习的优化张量库。PyTorch 数组通常被称为张量。张量类似于 NumPy 的 ndarrays,唯一的区别在于张量可以在 GPU 或其他硬件加速器上运行。实际上,张量和 NumPy 数组通常可以共享相同的底层存储器,消除了复制数据的需求。
>>> import torch >>> data = [[1, 2],[3, 4]] >>> x_np = np.array(data) >>> x_tensor = torch.tensor(data)
注意x_np
和x_tensor
是不同类型的对象:
>>> x_np array([[1, 2], [3, 4]]) >>> x_tensor tensor([[1, 2], [3, 4]])
然而,我们可以将 PyTorch 张量视为 NumPy 数组,无需进行显式转换:
>>> np.exp(x_tensor) tensor([[ 2.7183, 7.3891], [20.0855, 54.5982]], dtype=torch.float64)
同样,请注意此函数的返回类型与初始数据类型兼容。
警告
虽然混合使用 ndarrays 和张量可能很方便,但不建议这样做。它不适用于非 CPU 张量,并且在极端情况下会出现意外行为。用户应该更倾向于显式将 ndarray 转换为张量。
注意
PyTorch 不实现__array_function__
或__array_ufunc__
。在内部,Tensor.__array__()
方法返回一个 NumPy ndarray 作为张量数据缓冲区的视图。有关详细信息,请参见此问题和torch_function 实现。
还要注意,即使torch.Tensor
不是 ndarray 的子类,我们也可以在这里看到__array_wrap__
的功能:
>>> import torch >>> t = torch.arange(4) >>> np.abs(t) tensor([0, 1, 2, 3])
PyTorch 实现了__array_wrap__
以便能够从 NumPy 函数中取回张量,并且我们可以直接修改它以控制从这些函数返回哪种类型的对象。
示例:CuPy 数组
CuPy 是一个用于 GPU 加速计算的与 NumPy/SciPy 兼容的数组库。CuPy 通过实现cupy.ndarray
,与 NumPy ndarrays 对应的对象实现了 NumPy 接口的子集。
>>> import cupy as cp >>> x_gpu = cp.array([1, 2, 3, 4])
cupy.ndarray
对象实现了__array_ufunc__
接口。这使得可以将 NumPy ufuncs 应用于 CuPy 数组(这将将操作延迟到 ufunc 的匹配 CuPy CUDA/ROCm 实现):
>>> np.mean(np.exp(x_gpu)) array(21.19775622)
请注意这些操作的返回类型仍与初始类型一致:
>>> arr = cp.random.randn(1, 2, 3, 4).astype(cp.float32) >>> result = np.sum(arr) >>> print(type(result)) <class 'cupy._core.core.ndarray'>
有关详细信息,请参见CuPy 文档中的此页面。
cupy.ndarray
还实现了__array_function__
接口,这意味着可以进行诸如
>>> a = np.random.randn(100, 100) >>> a_gpu = cp.asarray(a) >>> qr_gpu = np.linalg.qr(a_gpu)
CuPy 在cupy.ndarray
对象上实现了许多 NumPy 函数,但并非都实现。有关详细信息,请参阅CuPy 文档。
示例:Dask 数组
Dask 是 Python 中用于并行计算的灵活库。Dask Array 使用分块算法实现了 NumPy ndarray 接口的子集,将大数组切分为许多小数组。这使得可以使用多个核心对大于内存大小的数组进行计算。
Dask 支持__array__()
和__array_ufunc__
。
>>> import dask.array as da >>> x = da.random.normal(1, 0.1, size=(20, 20), chunks=(10, 10)) >>> np.mean(np.exp(x)) dask.array<mean_agg-aggregate, shape=(), dtype=float64, chunksize=(), chunktype=numpy.ndarray> >>> np.mean(np.exp(x)).compute() 5.090097550553843
注意
Dask 是惰性计算的,计算的结果直到通过调用compute()
来要求计算时才计算。
有关详细信息,请参阅 Dask 数组文档 和 Dask 数组与 NumPy 数组互操作性的范围。
示例:DLPack
几个 Python 数据科学库都实现了__dlpack__
协议,其中包括PyTorch和CuPy。可以在DLPack 文档的此页面找到实现此协议的库的完整列表。
将 PyTorch CPU 张量转换为 NumPy 数组:
>>> import torch >>> x_torch = torch.arange(5) >>> x_torch tensor([0, 1, 2, 3, 4]) >>> x_np = np.from_dlpack(x_torch) >>> x_np array([0, 1, 2, 3, 4]) >>> # note that x_np is a view of x_torch >>> x_torch[1] = 100 >>> x_torch tensor([ 0, 100, 2, 3, 4]) >>> x_np array([ 0, 100, 2, 3, 4])
导入的数组是只读的,因此写入或就地操作将失败:
>>> x.flags.writeable False >>> x_np[1] = 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: assignment destination is read-only
为了就地操作导入的数组,必须创建副本,但这将意味着内存复制。对于非常大的数组不要这样做:
>>> x_np_copy = x_np.copy() >>> x_np_copy.sort() # works
注意
注意 GPU 张量无法转换为 NumPy 数组,因为 NumPy 不支持 GPU 设备:
>>> x_torch = torch.arange(5, device='cuda') >>> np.from_dlpack(x_torch) Traceback (most recent call last): File "<stdin>", line 1, in <module> RuntimeError: Unsupported device in DLTensor.
但是,如果两个库都支持数据缓冲区所在的设备,则可以使用__dlpack__
协议(例如 PyTorch 和 CuPy):
>>> x_torch = torch.arange(5, device='cuda') >>> x_cupy = cupy.from_dlpack(x_torch)
同样,可以将 NumPy 数组转换为 PyTorch 张量:
>>> x_np = np.arange(5) >>> x_torch = torch.from_dlpack(x_np)
只读数组无法导出:
>>> x_np = np.arange(5) >>> x_np.flags.writeable = False >>> torch.from_dlpack(x_np) Traceback (most recent call last): File "<stdin>", line 1, in <module> File ".../site-packages/torch/utils/dlpack.py", line 63, in from_dlpack dlpack = ext_tensor.__dlpack__() TypeError: NumPy currently only supports dlpack for writeable arrays
进一步阅读
- 数组接口协议
- 编写自定义数组容器
- 特殊属性和方法(关于
__array_ufunc__
和__array_function__
协议的详细信息) - 子类化 ndarray(关于
__array_wrap__
和__array_finalize__
方法的详细信息) - ndarray 子类型化的特定特性(关于
__array_finalize__
、__array_wrap__
和__array_priority__
实现的更多详细信息) - NumPy 路线图:互操作性
- PyTorch 与 NumPy 之间的桥接文档