在 Cython 中调用点积和线性代数运算?
- 2025-04-10 09:45:00
- admin 原创
- 20
问题描述:
我正在尝试使用点积、矩阵求逆和其他可在 Cython 中从 numpy 获得的基本线性代数运算。函数如numpy.linalg.inv
(求逆)、numpy.dot
(点积)、X.t
(矩阵/数组的转置)。从 Cython 函数调用会产生很大的开销,numpy.*
而其余函数都是用 Cython 编写的,所以我想避免这种情况。
如果我假设用户已经numpy
安装,有没有办法做类似的事情:
#include "numpy/npy_math.h"
作为extern
,并调用这些函数?或者直接调用 BLAS(或 numpy 为这些核心操作调用的任何函数)?
举个例子,假设你在 Cython 中有一个函数,它可以做很多事情,最后需要进行涉及点积和矩阵逆的计算:
cdef myfunc(...):
# ... do many things faster than Python could
# ...
# compute one value using dot products and inv
# without using
# import numpy as np
# np.*
val = gammaln(sum(v)) - sum(gammaln(v)) + dot((v - 1).T, log(x).T)
如何做到这一点?如果 Cython 中已经有一个实现这些的库,我也可以使用它,但还没有找到任何东西。即使这些程序的优化程度不如 BLAS 直接优化,但无需numpy
从 Cython 调用 Python 模块的开销仍会使整体速度更快。
我想要调用的示例函数:
点积 (
np.dot
)矩阵求逆 (
np.linalg.inv
)矩阵乘法
采取转置(相当于
x.T
在 numpy 中)gammaln 函数(类似
scipy.gammaln
等效函数,在 C 中可用)
我意识到,正如 numpy 邮件列表 ( https://groups.google.com/forum/?fromgroups=#!topic/cython-users/XZjMVSIQnTE ) 上所说,如果在大型矩阵上调用这些函数,那么从 Cython 执行此操作是没有意义的,因为从 numpy 调用它只会导致大部分时间花在 numpy 调用的优化 C 代码上。但是,就我而言,我在小矩阵上多次调用这些线性代数运算- 在这种情况下,反复从 Cython 返回 numpy 并返回 Cython 所带来的开销将远远超过实际从 BLAS 计算操作所花费的时间。因此,对于这些简单的操作,我想将所有内容保留在 C/Cython 级别,而不是通过 python。
我宁愿不通过 GSL,因为这会增加另一个依赖项,而且不清楚 GSL 是否得到积极维护。由于我假设代码的用户已经安装了 scipy/numpy,我可以放心地假设他们拥有与这些库相关的所有 C 代码,所以我只想能够利用该代码并从 Cython 调用它。
编辑:我找到了一个在 Cython 中包装 BLAS 的库(https://github.com/tokyo/tokyo),它很接近但不是我想要的。我想直接调用 numpy/scipy C 函数(我假设用户已经安装了这些函数。)
解决方案 1:
调用与 Scipy 捆绑在一起的 BLAS“相当”简单,这里有一个调用 DGEMM 来计算矩阵乘法的示例:https://gist.github.com/pv/5437087 请注意,BLAS 和 LAPACK 期望所有数组都是 Fortran 连续的(模数 lda/b/c 参数),因此order="F"
这double[::1,:]
对于正确运行是必需的。
通过在单位矩阵上应用 LAPACK 函数,可以类似地计算逆矩阵dgesv
。有关签名,请参见此处。所有这些都需要降到相当低级的编码,您需要自己分配临时工作数组等等。---但是,这些可以封装到您自己的便利函数中,或者只需通过用上述方式从 Scipy 获得的函数指针tokyo
替换函数来重用代码。lib_*
如果您使用 Cython 的 memoryview 语法 ( double[::1,:]
),您的转置与平常相同x.T
。或者,您可以通过编写自己的函数来计算转置,该函数在对角线上交换数组的元素。Numpy 实际上不包含此操作,x.T
只会更改数组的步幅,而不会移动数据。
可能可以重写tokyo
模块以使用 Scipy 导出的 BLAS/LAPACK 并将其捆绑在 中scipy.linalg
,这样您就可以直接执行from scipy.linalg.blas cimport dgemm
。如果有人想深入研究它,可以接受拉取请求。
如您所见,这一切都归结为传递函数指针。如上所述,Cython 确实提供了自己的函数指针交换协议。例如,考虑from scipy.spatial import qhull; print(qhull.__pyx_capi__)
--- 这些函数可以通过 Cython 访问from scipy.spatial.qhull cimport XXXX
(但它们是私有的,所以不要这样做)。
但是,目前scipy.special
不提供此 C-API。不过,提供它其实相当简单,因为 scipy.special 中的接口模块是用 Cython 编写的。
我认为目前还没有任何合理且可移植的方法来访问执行繁重工作的函数gamln
(尽管您可以窥探 UFunc 对象,但这不是一个合理的解决方案:),因此目前最好只是从 scipy.special 中获取相关部分源代码并将其与您的项目捆绑在一起,或者使用 GSL。
解决方案 2:
如果您确实接受使用 GSL,那么最简单的方法可能是使用此 GSL->cython 接口https://github.com/twiecki/CythonGSL并从那里调用 BLAS(参见示例https://github.com/twiecki/CythonGSL/blob/master/examples/blas2.pyx)。它还应该处理 Fortran 与 C 的排序。GSL 的新特性并不多,但您可以放心地认为它是积极维护的。与 tokyo 相比,CythonGSL 更完整;例如,它具有 numpy 中没有的对称矩阵乘积。
解决方案 3:
由于我刚刚遇到了同样的问题,并编写了一些其他函数,因此我会将它们包含在这里,以防其他人发现它们有用。我编写了一些矩阵乘法,还调用了 LAPACK 函数进行矩阵求逆、行列式和 Cholesky 分解。但是,如果有循环,您应该考虑尝试在任何循环之外进行线性代数运算,就像我在这里做的那样。顺便说一句,如果您有建议,这里的行列式函数不太管用。另外,请注意,我不会检查输入是否符合要求。
from scipy.linalg.cython_lapack cimport dgetri, dgetrf, dpotrf
cpdef void double[:, ::1] inv_c(double[:, ::1] A, double[:, ::1] B,
double[:, ::1] work, double[::1] ipiv):
'''invert float type square matrix A
Parameters
----------
A : memoryview (numpy array)
n x n array to invert
B : memoryview (numpy array)
n x n array to use within the function, function
will modify this matrix in place to become the inverse of A
work : memoryview (numpy array)
n x n array to use within the function
ipiv : memoryview (numpy array)
length n array to use within function
'''
cdef int n = A.shape[0], info, lwork
B[...] = A
dgetrf(&n, &n, &B[0, 0], &n, &ipiv[0], &info)
dgetri(&n, &B[0,0], &n, &ipiv[0], &work[0,0], &lwork, &info)
cpdef double det_c(double[:, ::1] A, double[:, ::1] work, double[::1] ipiv):
'''obtain determinant of float type square matrix A
Notes
-----
As is, this function is not yet computing the sign of the determinant
correctly, help!
Parameters
----------
A : memoryview (numpy array)
n x n array to compute determinant of
work : memoryview (numpy array)
n x n array to use within function
ipiv : memoryview (numpy array)
length n vector use within function
Returns
-------
detval : float
determinant of matrix A
'''
cdef int n = A.shape[0], info
work[...] = A
dgetrf(&n, &n, &work[0,0], &n, &ipiv[0], &info)
cdef double detval = 1.
cdef int j
for j in range(n):
if j != ipiv[j]:
detval = -detval*work[j, j]
else:
detval = detval*work[j, j]
return detval
cdef void chol_c(double[:, ::1] A, double[:, ::1] B):
'''cholesky factorization of real symmetric positive definite float matrix A
Parameters
----------
A : memoryview (numpy array)
n x n matrix to compute cholesky decomposition
B : memoryview (numpy array)
n x n matrix to use within function, will be modified
in place to become cholesky decomposition of A. works
similar to np.linalg.cholesky
'''
cdef int n = A.shape[0], info
cdef char uplo = 'U'
B[...] = A
dpotrf(&uplo, &n, &B[0,0], &n, &info)
cdef int i, j
for i in range(n):
for j in range(n):
if j > i:
B[i, j] = 0
cpdef void dotmm_c(double[:, :] A, double[:, :] B, double[:, :] out):
'''matrix multiply matrices A (n x m) and B (m x l)
Parameters
----------
A : memoryview (numpy array)
n x m left matrix
B : memoryview (numpy array)
m x r right matrix
out : memoryview (numpy array)
n x r output matrix
'''
cdef Py_ssize_t i, j, k
cdef double s
cdef Py_ssize_t n = A.shape[0], m = A.shape[1]
cdef Py_ssize_t l = B.shape[0], r = B.shape[1]
for i in range(n):
for j in range(r):
s = 0
for k in range(m):
s += A[i, k]*B[k, j]
out[i, j] = s
扫码咨询,免费领取项目管理大礼包!