如何从多维缓冲区初始化NumPy数组?

时间:2017-10-11 15:24:15

标签: python c++ numpy eigen python-c-api

numpy.frombuffer函数的文档特别指出生成的数组将是一维的:

  

将缓冲区解释为1维数组。

我不确定这句话的后果。文档告诉我生成的数组将是一维的,但从不说输入缓冲区必须描述一维对象。

我在C ++中有一个(2D)Eigen matrix。我想创建一个描述矩阵内容的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)

     

从对象buf构造单一类型的一维ndarray,它导出(单段)缓冲区协议(或者具有返回导出缓冲区的对象的__buffer__属性协议)。首先尝试可写缓冲区,然后是只读缓冲区。返回数组的NPY_ARRAY_WRITEABLE标志将反映哪一个成功。假设数据从对象的存储器位置的起始处的偏移字节开始。将根据数据类型描述符dtype解释缓冲区中数据的类型。如果count为负,则将根据缓冲区的大小和请求的itemsize确定,否则count表示应从缓冲区转换的元素数。

是"单段"是否意味着它不能包含使用的填充,例如,对齐矩阵的行?在那种情况下,我搞砸了,因为我的矩阵可以很好地使用需要填充的对齐策略。

回到原来的问题:

有没有办法让我创建一个与预先存在的缓冲区共享内存的NumPy数组?

备注:github上有一个名为Eigen3ToPython的项目,旨在将eigen与python连接,但它不允许内存共享(强调我的):

  

此库允许:[...]以透明方式转换为Numpy数组(np.array)(但内存不在两个表示之间共享

修改 有人可能会指出类似标题的问题Numpy 2D- Array from Buffer?。不幸的是,那里给出的解决方案似乎不适用于我的情况,因为生成的2D数组不与原始缓冲区共享内存。

编辑:如何在Eigen中组织数据

使用跨步访问,在1D内存缓冲区中对2个矩阵进行特征映射。例如,双精度3×2矩阵需要6个双精度,即48个字节。分配48字节的缓冲区。此缓冲区中的第一个元素表示矩阵中的[0, 0]条目。

为了访问元素[i, j],使用以下公式:

double* v = matrix.data() + i*matrix.rowStride() + j*matrix.colStride()

,其中matrix是矩阵对象,其成员函数data()rowStride()colStride()分别返回缓冲区的起始地址,两者之间的距离连续行和两个连续列之间的距离(浮点格式大小的倍数)。

默认情况下,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()成员函数将返回3的地址。)在这种情况下,行和列的步幅是

rowStride: 1
colStride: 4

(在未对齐的情况下,他们分别是1和3。)

Numpy期望一个C连续缓冲区,即没有填充的行主结构。如果Eigen没有插入填充,那么行主要要求的问题可以很容易地解决一个列主要的特征矩阵:一个将缓冲区传递给一个numpy数组,结果ndarray被重新整形和转置。我设法完美地完成了这项工作。

但是在Eigen插入填充的情况下,使用这种技术无法解决问题,因为ndarray仍会看到数据中的零并认为它们是矩阵的一部分,同时丢弃一些数组末尾的值。并且这个是我要求解决方案的问题。

现在,作为一个侧面评论,由于我们有幸在循环中拥有@ggael,谁可以放弃一些亮点,我不得不承认我从来没有让Eigen在我的矩阵中插入任何填充。而且我似乎没有在Eigen文档中找到任何填充。但是,我希望对齐策略可以对齐每一列(或行),而不仅仅是第一列。我的期望错了吗?如果我,那么整个问题不适用于Eigen。但它适用于我使用的其他库,我使用了上面描述的对齐策略,所以在回答问题时请不要考虑最后一段。

1 个答案:

答案 0 :(得分:1)

我在这里回答了我自己的问题。感谢@ user2357112指向正确的方向:我需要的是PyArray_NewFromDescr

以下Python对象是特征矩阵的包装器:

struct PyEigenMatrix {
    PyObject_HEAD
    Eigen::Matrix<RealT, Eigen::Dynamic, Eigen::Dynamic> matrix;
};

RealT是我使用的浮点类型(float)。

为了返回np.ndarray对象,我在类中添加了一个成员函数:

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 (?)
        );
}

typenumnumpy type id number

此调用创建一个新的numpy数组,为其提供一个缓冲区(通过data参数),使用dimsstrides参数描述缓冲区(前者也设置形状)返回数组),描述数据dype,将矩阵设置为读写(通过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,它与Eigen矩阵完全对应。当我修改aIn[7])时,不仅修改了numpy数组(Out[8]),还修改了底层的Eigen数组(Out[9]),表明这两个对象共享相同的内存。

编辑 @ user2357112是正确的两次。他在评论中提出的第二种方法也适用。如果类型PyEigenMatrix导出缓冲区接口(我的类型),那么解决方案就像创建memoryview对象一样简单,in Python或使用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。矩阵类型只需要支持缓冲协议,这比直接依赖numpy的方法更通用。