如何从多维缓冲区初始化 NumPy 数组?
How can I initialize a NumPy array from a multidimensional buffer?
numpy.frombuffer
函数的文档明确指出生成的数组将是一维的:
Interpret a buffer as a 1-dimensional array.
我不确定这句话的后果。文档只告诉我生成的数组将是一维的,但从未说输入缓冲区必须描述一维对象。
我有一个描述矩阵内容的 (2D) Eigen matrix in C++. I would like to create a Python buffer。然后,我想使用此缓冲区以某种方式初始化我的 NumPy 数组并使其可用于我的 python 脚本。目标是在不复制数据的情况下将信息传递给 Python 并允许 python 修改矩阵(例如初始化矩阵)。
numpy.frombuffer
的 C-API 等价于 PyArray_FromBuffer
,它也共享单维短语,但它有更多的文档(强调我的):
PyObject* PyArray_FromBuffer(PyObject* buf, PyArray_Descr* dtype, npy_intp count, npy_intp offset)
Construct a one-dimensional ndarray of a single type from an object, buf, that exports the (single-segment) buffer protocol (or has an attribute __buffer__ that returns an object that exports the buffer protocol). A writeable buffer will be tried first followed by a read- only buffer. The NPY_ARRAY_WRITEABLE flag of the returned array will reflect which one was successful. The data is assumed to start at offset bytes from the start of the memory location for the object. The type of the data in the buffer will be interpreted depending on the data- type descriptor, dtype. If count is negative then it will be determined from the size of the buffer and the requested itemsize, otherwise, count represents how many elements should be converted from the buffer.
"single-segment"是否意味着它不能包含使用的填充,例如,对齐矩阵的行?在那种情况下,我搞砸了,因为我的矩阵可以很好地使用需要填充的对齐策略。
回到原问题:
有没有办法让我创建一个与预先存在的缓冲区共享内存的 NumPy 数组?
备注:github上有一个项目叫做Eigen3ToPython,旨在连接eigen和python,但它不允许内存共享(强调我的):
This library allows to: [...] Convert to/from Numpy arrays (np.array
) in a transparent manner (however, memory is not shared between both representations)
编辑
有人可能会指出同名问题 Numpy 2D- Array from Buffer?。不幸的是,那里给出的解决方案似乎对我的情况无效,因为生成的二维数组不与原始缓冲区共享内存。
编辑:Eigen 中的数据是如何组织的
Eigen 使用跨步访问在一维内存缓冲区中映射二维矩阵。例如,双精度 3x2 矩阵需要 6 个双精度数,即 48 个字节。分配了一个 48 字节的缓冲区。此缓冲区中的第一个元素表示矩阵中的 [0, 0]
条目。
为了访问元素[i, j]
,使用了以下公式:
double* v = matrix.data() + i*matrix.rowStride() + j*matrix.colStride()
,其中matrix
是矩阵对象及其成员函数data()
,rowStride()
和colStride()
return,分别是矩阵的起始地址缓冲区,两个连续行之间的距离和两个连续列之间的距离(浮点格式大小的倍数)。
默认情况下,Eigen 使用列优先格式,因此 rowStride() == 1
,但它也可以配置为使用行优先格式,colStride() == 1
.
另一个重要的配置选项是对齐方式。数据缓冲区很可能包含一些不需要的值(即不属于矩阵的值),以便使列或行从对齐的地址开始。这使得对矩阵的操作可向量化。在上面的例子中,假设列优先格式和 16 字节对齐,下面的矩阵
3 7
1 -2
4 5
可以存储在以下缓冲区中:
0 0 3 1 4 0 7 -2 5 0
0 值称为填充。开头的两个 0 可能是必要的,以确保实际数据的开头与同一边界对齐。 (注意 data()
成员函数将 return 3 的地址。)在这种情况下,行和列的步幅为
rowStride: 1
colStride: 4
(在未对齐的情况下,它们分别为 1 和 3。)
Numpy 需要一个 C 连续缓冲区,即没有填充的行优先结构。如果 Eigen 没有插入填充,那么行优先要求的问题可以很容易地解决列优先 Eigen 矩阵的问题:将缓冲区传递给 numpy 数组,结果 ndarray
被重塑并转置。我设法完美地完成了这项工作。
但如果 Eigen 确实插入填充,则无法使用此技术解决问题,因为 ndarray
仍会在数据中看到零并认为它们是矩阵的一部分,同时丢弃数组末尾的一些值。 这个就是我要解决的问题。
现在,作为附带说明,由于我们有幸在循环中拥有@ggael,他可能会阐明一些问题,我不得不承认我从未让 Eigen 在我的矩阵中插入任何填充。而且我似乎没有在 Eigen 文档中找到任何关于填充的提及。但是,我希望对齐策略能够对齐每一列(或行),而不仅仅是第一列。我的期望错了吗?如果我是,那么整个问题不适用于 Eigen。但这将适用于我正在使用的其他库,这些库应用了我上面描述的对齐策略,所以请在回答问题时不要考虑最后一段。
我在这里回答我自己的问题。感谢@user2357112 指出了正确的方向:我需要的是 PyArray_NewFromDescr
.
以下 Python 对象是 Eigen 矩阵的包装器:
struct PyEigenMatrix {
PyObject_HEAD
Eigen::Matrix<RealT, Eigen::Dynamic, Eigen::Dynamic> matrix;
};
RealT
是我使用的浮点类型(float
在我的例子中)。
为了return一个np.ndarray
对象,我在class中添加了一个成员函数:
static PyObject*
PyEigenMatrix_as_ndarray(PyEigenMatrix* self, PyObject* args, PyObject* kwds)
{
// Extract number of rows and columns from Eigen matrix
npy_intp dims[] = { self->matrix.rows(), self->matrix.cols() };
// Extract strides from Eigen Matrix (multiply by type size to get bytes)
npy_intp strides[] = {
self->matrix.rowStride() * (npy_intp)sizeof(RealT),
self->matrix.colStride() * (npy_intp)sizeof(RealT)
};
// Create and return the ndarray
return PyArray_NewFromDescr(
&PyArray_Type, // Standard type
PyArray_DescrFromType(typenum), // Numpy type id
2, // Number of dimensions
dims, // Dimension array
strides, // Strides array
self->matrix.data(), // Pointer to data
NPY_ARRAY_WRITEABLE, // Flags
(PyObject*)self // obj (?)
);
}
typenum
就是 numpy type id number.
这个调用创建一个新的 numpy 数组,给它一个缓冲区(通过 data
参数),使用 dims
和 strides
参数描述缓冲区(前者还设置returned 数组的形状),描述数据类型,将矩阵设置为可读写(通过 flags
参数。
不过我不确定最后一个参数 obj
是什么意思。文档仅在类型不同于 PyArray_Type
.
的情况下提及它
为了说明这在实践中是如何工作的,让我展示一些 python 代码。
In [3]: m = Matrix(7, 3)
In [4]: m
Out[4]:
0.680375 -0.211234 0.566198
0.59688 0.823295 -0.604897
-0.329554 0.536459 -0.444451
0.10794 -0.0452059 0.257742
-0.270431 0.0268018 0.904459
0.83239 0.271423 0.434594
-0.716795 0.213938 -0.967399
In [5]: a = m.as_ndarray()
In [6]: a
Out[6]:
array([[ 0.68 , -0.211, 0.566],
[ 0.597, 0.823, -0.605],
[-0.33 , 0.536, -0.444],
[ 0.108, -0.045, 0.258],
[-0.27 , 0.027, 0.904],
[ 0.832, 0.271, 0.435],
[-0.717, 0.214, -0.967]], dtype=float32)
In [7]: a[2, 1] += 4
In [8]: a
Out[8]:
array([[ 0.68 , -0.211, 0.566],
[ 0.597, 0.823, -0.605],
[-0.33 , 4.536, -0.444],
[ 0.108, -0.045, 0.258],
[-0.27 , 0.027, 0.904],
[ 0.832, 0.271, 0.435],
[-0.717, 0.214, -0.967]], dtype=float32)
In [9]: m
Out[9]:
0.680375 -0.211234 0.566198
0.59688 0.823295 -0.604897
-0.329554 4.53646 -0.444451
0.10794 -0.0452059 0.257742
-0.270431 0.0268018 0.904459
0.83239 0.271423 0.434594
-0.716795 0.213938 -0.967399
Matrix
是我的 PyEigenMatrix
类型。我添加了一个 __repr__
函数,它使用 Eigen 的流运算符打印矩阵。我可以有一个 ndarray
a
与特征矩阵完全对应。当我修改 a
(In[7]
) 时,不仅修改了 numpy 数组 (Out[8]
),还修改了底层 Eigen 数组 (Out[9]
),表明这两个对象共享相同的内存。
EDIT @user2357112 说对了两次。他在评论中提出的第二种方法也适用。如果类型 PyEigenMatrix
导出缓冲区接口(我的类型就是这样做的),那么解决方案就像创建一个 memoryview
对象一样简单,或者 in Python or using the C-API,然后将该对象传递给 np.array
函数,同时指定 copy=False
.
这是它的工作原理:
In [2]: m = Matrix(7, 3)
In [3]: mv = memoryview(m)
In [4]: a = np.array(mv, copy=False)
In [5]: m
Out[5]:
0.680375 0.536459 0.904459
-0.211234 -0.444451 0.83239
0.566198 0.10794 0.271423
0.59688 -0.0452059 0.434594
0.823295 0.257742 -0.716795
-0.604897 -0.270431 0.213938
-0.329554 0.0268018 -0.967399
In [6]: a
Out[6]:
array([[ 0.68 , 0.536, 0.904],
[-0.211, -0.444, 0.832],
[ 0.566, 0.108, 0.271],
[ 0.597, -0.045, 0.435],
[ 0.823, 0.258, -0.717],
[-0.605, -0.27 , 0.214],
[-0.33 , 0.027, -0.967]], dtype=float32)
In [7]: a [3, 1] += 2
In [8]: a
Out[8]:
array([[ 0.68 , 0.536, 0.904],
[-0.211, -0.444, 0.832],
[ 0.566, 0.108, 0.271],
[ 0.597, 1.955, 0.435],
[ 0.823, 0.258, -0.717],
[-0.605, -0.27 , 0.214],
[-0.33 , 0.027, -0.967]], dtype=float32)
In [9]: m
Out[9]:
0.680375 0.536459 0.904459
-0.211234 -0.444451 0.83239
0.566198 0.10794 0.271423
0.59688 1.95479 0.434594
0.823295 0.257742 -0.716795
-0.604897 -0.270431 0.213938
-0.329554 0.0268018 -0.967399
这种方法的优点是不需要numpy C-API。 matrix类型只要支持buffer协议即可,比直接依赖numpy的方法更通用
numpy.frombuffer
函数的文档明确指出生成的数组将是一维的:
Interpret a buffer as a 1-dimensional array.
我不确定这句话的后果。文档只告诉我生成的数组将是一维的,但从未说输入缓冲区必须描述一维对象。
我有一个描述矩阵内容的 (2D) Eigen matrix in C++. I would like to create a Python buffer。然后,我想使用此缓冲区以某种方式初始化我的 NumPy 数组并使其可用于我的 python 脚本。目标是在不复制数据的情况下将信息传递给 Python 并允许 python 修改矩阵(例如初始化矩阵)。
numpy.frombuffer
的 C-API 等价于 PyArray_FromBuffer
,它也共享单维短语,但它有更多的文档(强调我的):
PyObject* PyArray_FromBuffer(PyObject* buf, PyArray_Descr* dtype, npy_intp count, npy_intp offset)
Construct a one-dimensional ndarray of a single type from an object, buf, that exports the (single-segment) buffer protocol (or has an attribute __buffer__ that returns an object that exports the buffer protocol). A writeable buffer will be tried first followed by a read- only buffer. The NPY_ARRAY_WRITEABLE flag of the returned array will reflect which one was successful. The data is assumed to start at offset bytes from the start of the memory location for the object. The type of the data in the buffer will be interpreted depending on the data- type descriptor, dtype. If count is negative then it will be determined from the size of the buffer and the requested itemsize, otherwise, count represents how many elements should be converted from the buffer.
"single-segment"是否意味着它不能包含使用的填充,例如,对齐矩阵的行?在那种情况下,我搞砸了,因为我的矩阵可以很好地使用需要填充的对齐策略。
回到原问题:
有没有办法让我创建一个与预先存在的缓冲区共享内存的 NumPy 数组?
备注:github上有一个项目叫做Eigen3ToPython,旨在连接eigen和python,但它不允许内存共享(强调我的):
This library allows to: [...] Convert to/from Numpy arrays (
np.array
) in a transparent manner (however, memory is not shared between both representations)
编辑 有人可能会指出同名问题 Numpy 2D- Array from Buffer?。不幸的是,那里给出的解决方案似乎对我的情况无效,因为生成的二维数组不与原始缓冲区共享内存。
编辑:Eigen 中的数据是如何组织的
Eigen 使用跨步访问在一维内存缓冲区中映射二维矩阵。例如,双精度 3x2 矩阵需要 6 个双精度数,即 48 个字节。分配了一个 48 字节的缓冲区。此缓冲区中的第一个元素表示矩阵中的 [0, 0]
条目。
为了访问元素[i, j]
,使用了以下公式:
double* v = matrix.data() + i*matrix.rowStride() + j*matrix.colStride()
,其中matrix
是矩阵对象及其成员函数data()
,rowStride()
和colStride()
return,分别是矩阵的起始地址缓冲区,两个连续行之间的距离和两个连续列之间的距离(浮点格式大小的倍数)。
默认情况下,Eigen 使用列优先格式,因此 rowStride() == 1
,但它也可以配置为使用行优先格式,colStride() == 1
.
另一个重要的配置选项是对齐方式。数据缓冲区很可能包含一些不需要的值(即不属于矩阵的值),以便使列或行从对齐的地址开始。这使得对矩阵的操作可向量化。在上面的例子中,假设列优先格式和 16 字节对齐,下面的矩阵
3 7
1 -2
4 5
可以存储在以下缓冲区中:
0 0 3 1 4 0 7 -2 5 0
0 值称为填充。开头的两个 0 可能是必要的,以确保实际数据的开头与同一边界对齐。 (注意 data()
成员函数将 return 3 的地址。)在这种情况下,行和列的步幅为
rowStride: 1
colStride: 4
(在未对齐的情况下,它们分别为 1 和 3。)
Numpy 需要一个 C 连续缓冲区,即没有填充的行优先结构。如果 Eigen 没有插入填充,那么行优先要求的问题可以很容易地解决列优先 Eigen 矩阵的问题:将缓冲区传递给 numpy 数组,结果 ndarray
被重塑并转置。我设法完美地完成了这项工作。
但如果 Eigen 确实插入填充,则无法使用此技术解决问题,因为 ndarray
仍会在数据中看到零并认为它们是矩阵的一部分,同时丢弃数组末尾的一些值。 这个就是我要解决的问题。
现在,作为附带说明,由于我们有幸在循环中拥有@ggael,他可能会阐明一些问题,我不得不承认我从未让 Eigen 在我的矩阵中插入任何填充。而且我似乎没有在 Eigen 文档中找到任何关于填充的提及。但是,我希望对齐策略能够对齐每一列(或行),而不仅仅是第一列。我的期望错了吗?如果我是,那么整个问题不适用于 Eigen。但这将适用于我正在使用的其他库,这些库应用了我上面描述的对齐策略,所以请在回答问题时不要考虑最后一段。
我在这里回答我自己的问题。感谢@user2357112 指出了正确的方向:我需要的是 PyArray_NewFromDescr
.
以下 Python 对象是 Eigen 矩阵的包装器:
struct PyEigenMatrix {
PyObject_HEAD
Eigen::Matrix<RealT, Eigen::Dynamic, Eigen::Dynamic> matrix;
};
RealT
是我使用的浮点类型(float
在我的例子中)。
为了return一个np.ndarray
对象,我在class中添加了一个成员函数:
static PyObject*
PyEigenMatrix_as_ndarray(PyEigenMatrix* self, PyObject* args, PyObject* kwds)
{
// Extract number of rows and columns from Eigen matrix
npy_intp dims[] = { self->matrix.rows(), self->matrix.cols() };
// Extract strides from Eigen Matrix (multiply by type size to get bytes)
npy_intp strides[] = {
self->matrix.rowStride() * (npy_intp)sizeof(RealT),
self->matrix.colStride() * (npy_intp)sizeof(RealT)
};
// Create and return the ndarray
return PyArray_NewFromDescr(
&PyArray_Type, // Standard type
PyArray_DescrFromType(typenum), // Numpy type id
2, // Number of dimensions
dims, // Dimension array
strides, // Strides array
self->matrix.data(), // Pointer to data
NPY_ARRAY_WRITEABLE, // Flags
(PyObject*)self // obj (?)
);
}
typenum
就是 numpy type id number.
这个调用创建一个新的 numpy 数组,给它一个缓冲区(通过 data
参数),使用 dims
和 strides
参数描述缓冲区(前者还设置returned 数组的形状),描述数据类型,将矩阵设置为可读写(通过 flags
参数。
不过我不确定最后一个参数 obj
是什么意思。文档仅在类型不同于 PyArray_Type
.
为了说明这在实践中是如何工作的,让我展示一些 python 代码。
In [3]: m = Matrix(7, 3)
In [4]: m
Out[4]:
0.680375 -0.211234 0.566198
0.59688 0.823295 -0.604897
-0.329554 0.536459 -0.444451
0.10794 -0.0452059 0.257742
-0.270431 0.0268018 0.904459
0.83239 0.271423 0.434594
-0.716795 0.213938 -0.967399
In [5]: a = m.as_ndarray()
In [6]: a
Out[6]:
array([[ 0.68 , -0.211, 0.566],
[ 0.597, 0.823, -0.605],
[-0.33 , 0.536, -0.444],
[ 0.108, -0.045, 0.258],
[-0.27 , 0.027, 0.904],
[ 0.832, 0.271, 0.435],
[-0.717, 0.214, -0.967]], dtype=float32)
In [7]: a[2, 1] += 4
In [8]: a
Out[8]:
array([[ 0.68 , -0.211, 0.566],
[ 0.597, 0.823, -0.605],
[-0.33 , 4.536, -0.444],
[ 0.108, -0.045, 0.258],
[-0.27 , 0.027, 0.904],
[ 0.832, 0.271, 0.435],
[-0.717, 0.214, -0.967]], dtype=float32)
In [9]: m
Out[9]:
0.680375 -0.211234 0.566198
0.59688 0.823295 -0.604897
-0.329554 4.53646 -0.444451
0.10794 -0.0452059 0.257742
-0.270431 0.0268018 0.904459
0.83239 0.271423 0.434594
-0.716795 0.213938 -0.967399
Matrix
是我的 PyEigenMatrix
类型。我添加了一个 __repr__
函数,它使用 Eigen 的流运算符打印矩阵。我可以有一个 ndarray
a
与特征矩阵完全对应。当我修改 a
(In[7]
) 时,不仅修改了 numpy 数组 (Out[8]
),还修改了底层 Eigen 数组 (Out[9]
),表明这两个对象共享相同的内存。
EDIT @user2357112 说对了两次。他在评论中提出的第二种方法也适用。如果类型 PyEigenMatrix
导出缓冲区接口(我的类型就是这样做的),那么解决方案就像创建一个 memoryview
对象一样简单,或者 in Python or using the C-API,然后将该对象传递给 np.array
函数,同时指定 copy=False
.
这是它的工作原理:
In [2]: m = Matrix(7, 3)
In [3]: mv = memoryview(m)
In [4]: a = np.array(mv, copy=False)
In [5]: m
Out[5]:
0.680375 0.536459 0.904459
-0.211234 -0.444451 0.83239
0.566198 0.10794 0.271423
0.59688 -0.0452059 0.434594
0.823295 0.257742 -0.716795
-0.604897 -0.270431 0.213938
-0.329554 0.0268018 -0.967399
In [6]: a
Out[6]:
array([[ 0.68 , 0.536, 0.904],
[-0.211, -0.444, 0.832],
[ 0.566, 0.108, 0.271],
[ 0.597, -0.045, 0.435],
[ 0.823, 0.258, -0.717],
[-0.605, -0.27 , 0.214],
[-0.33 , 0.027, -0.967]], dtype=float32)
In [7]: a [3, 1] += 2
In [8]: a
Out[8]:
array([[ 0.68 , 0.536, 0.904],
[-0.211, -0.444, 0.832],
[ 0.566, 0.108, 0.271],
[ 0.597, 1.955, 0.435],
[ 0.823, 0.258, -0.717],
[-0.605, -0.27 , 0.214],
[-0.33 , 0.027, -0.967]], dtype=float32)
In [9]: m
Out[9]:
0.680375 0.536459 0.904459
-0.211234 -0.444451 0.83239
0.566198 0.10794 0.271423
0.59688 1.95479 0.434594
0.823295 0.257742 -0.716795
-0.604897 -0.270431 0.213938
-0.329554 0.0268018 -0.967399
这种方法的优点是不需要numpy C-API。 matrix类型只要支持buffer协议即可,比直接依赖numpy的方法更通用