[英]Efficient matrix operations in cython with no python objects
我正在編寫一個函數,我想盡可能多地使用 Cython 將其轉換為 C。 為此,我需要使用線性代數運算。 這是我的功能。 編輯:我學到的教訓是嘗試處理循環之外的線性代數,這在很大程度上是我能夠做到的。 否則,求助於包裝 LAPACK/BLAS 或編寫我自己的函數。
import numpy as np
from scipy.stats import multivariate_normal as mv
import itertools
def llf(data, rho, mu, sigma, A, V, n):
'''evaluate likelihood by guass-hermite quadrature
Parameters
----------
data : array
N x J matrix, columns are measurements
rho : array
length L vector of weights for mixture of normals
mu : array
L x K vector of means of mixture of normals
sigma : array
K x L*K matrix of variance matrices for mixture of normals
A : array
J x (K + 1) matrix of loadings
V : array
J x J variance matrix of measurement errors
n : int
number of sample points for quadrature
'''
N = data.shape[0]
L, K = mu.shape
# getting weights and sample points for approximating integral
v, w = np.polynomial.hermite.hermgauss(n)
totllf = 0
for i in range(N):
M_i = data[i, :]
totllf_i = 0
for l in range(L):
rho_l = rho[l]
sigma_l = sigma[:, K*l:K*(l+1)]
mu_l = mu[l, :]
chol_l = np.linalg.cholesky(sigma_l)
for ix in itertools.product(*(list(range(n)) for k in range(K))):
wt = np.prod(w[list(ix)])
pt = np.sqrt(2)*chol_l.dot(v[list(ix)]) + mu_l
totllf_i += wt*rho_l*mv.pdf(M_i, A[:, 0] + A[:, 1:].dot(pt), V)
totllf += np.log(totllf_i)
return totllf
為了實現這一點,我需要具有矩陣乘法、轉置、行列式、矩陣求逆和 cholesky 分解的函數。 我看過一些關於使用BLAS
函數的帖子,但我真的不清楚如何使用這些。
編輯 04/29/18
按照建議,我采用了內存視圖方法並在循環之前初始化了所有內容。 我的新函數寫成
def llf_c(double[:, ::1] data, double[::1] rho, double[:, ::1] mu,
double[:, ::1] sigma, double[:, ::1] A, double[:, ::1] V, int n):
'''evaluate likelihood by guass-hermite quadrature
Parameters
----------
data : array
N x J matrix, columns are measurements
rho : array
length L vector of weights for mixture of normals
mu : array
L x K vector of means of mixture of normals
sigma : array
K x L*K matrix of variance matrices for mixture of normals
A : array
J x (K + 1) matrix of loadings
V : array
J x J variance matrix of measurement errors
n : int
number of sample points for quadrature
'''
cdef Py_ssize_t N = data.shape[0], J = data.shape[1], L = mu.shape[0], K = mu.shape[1]
# initializing indexing variables
cdef Py_ssize_t i, l, j, k
# getting weights and sample points for approximating integral
v_a, w_a = np.polynomial.hermite.hermgauss(n)
cdef double[::1] v = v_a
cdef double[::1] w = w_a
cdef double[::1] v_ix = np.zeros(K, dtype=np.float)
# initializing memory views for cholesky decomposition of sigma matrices
sigma_chol_a = np.zeros((K, L*K), dtype=np.float)
for l in range(L):
sigma_chol_a[:, K*l:K*(l+1)] = np.linalg.cholesky(sigma[:, K*l:K*(l+1)])
cdef double[:, ::1] sigma_chol = sigma_chol_a
# intializing V inverse and determinant
cdef double[:, ::1] V_inv = np.linalg.inv(V)
cdef double V_det = np.linalg.det(V)
# initializing memoryviews for work matrices
cdef double[::1] work_K = np.zeros(K, dtype=np.float)
cdef double[::1] work_J = np.zeros(J, dtype=np.float)
# initializing memoryview for quadrature points
cdef double[::1] pt = np.zeros(K, dtype=np.float)
# initializing memorview for means for multivariate normal
cdef double[::1] loc = np.zeros(J, dtype=np.float)
# initializing values for loop
cdef double[::1] totllf = np.zeros(N, dtype=np.float)
cdef double wt, pdf_init = 1./sqrt(((2*pi)**J)*V_det)
cdef int[:, ::1] ix_vals = np.vstack(itertools.product(*(list(range(n)) for k in range(K)))).astype(np.int32)
cdef Py_ssize_t ix_len = ix_vals.shape[0]
for ix_row in range(ix_len):
ix = ix_vals[ix_row]
# weights and points for quadrature
wt = 1.
for k in range(K):
wt *= w[ix[k]]
v_ix[k] = v[ix[k]]
for l in range(L):
# change of variables
dotmv_c(sigma_chol[:, K*l:K*(l+1)], v_ix, work_K)
for k in range(K):
pt[k] = sqrt(2)*work_K[k]
addvv_c(pt, mu[l, :], pt)
for i in range(N):
# generating demeaned vector for multivariate normal pdf
dotmv_c(A[:, 1:], pt, work_J)
addvv_c(A[:, 0], work_J, work_J)
for j in range(J):
loc[j] = data[i, j] - work_J[j]
# performing matrix products in exponential
# print(wt, rho[l], np.asarray(work_J))
dotvm_c(loc, V_inv, work_J)
totllf[i] += wt*rho[l]*pdf_init*exp(-0.5*dotvv_c(work_J, loc))
return np.log(np.asarray(totllf)).sum()
dotvm_c
、 dotmv_c
和addvv_c
是執行向量與矩陣、矩陣與向量的矩陣乘法以及兩個向量的元素相加的函數。 我也用 Cython 寫過這些,但為了簡潔起見不包括在內。 我不再包裝任何 LAPACK 函數,因為我在使用 numpy 循環之前執行所有其他線性代數。 我還有幾個問題。 為什么我的循環中仍然有黃色? (見下面的簡介)。 我認為現在一切都應該在 C 中。 另外,如果您有任何其他基於新實現的建議,請告訴我。
例如,在第 221 行,我在編譯時收到此消息:“應鍵入索引以提高訪問效率。” 但我以為我輸入了索引 k。 另外,由於addvv_c
顯示為黃色,我將addvv_c
您展示它的定義。
cpdef void addvv_c(double[:] a, double[:] b, double[:] out):
'''add two vectors elementwise
'''
cdef Py_ssize_t i, n = a.shape[0]
for i in range(n):
out[i] = a[i] + b[i]
關於優化的 Cython/BLAS 函數的一些小要點:
ipiv_a = np.zeros(n).astype(np.int32)
cdef int[::1] ipiv = ipiv_a
可以有兩個簡單的改進:它不必通過臨時變量,您可以直接創建一個np.int32
類型的數組,而不是創建一個不同類型的數組然后進行轉換:
cdef int[::1] ipiv = np.zeros(n,dtype=np.int32)
同樣,在這兩個功能中,您可以通過執行以下操作以更少的步驟初始化B
cdef double[:, ::1] B = A.copy()
而不是創建零數組並復制
第二個(更重要的)變化是將 C 數組用於臨時變量,例如 Fortran 工作區。 我仍然會將返回值之類的東西保留為 numpy 數組,因為引用計數和將它們發送回 Python 的能力非常有用。
cdef double* work = <double*>malloc(n*n*sizeof(double))
try:
# rest of function
finally:
free(work)
您需要cimport malloc
和free
來自libc.stdlib
。 try: ... finally:
確保正確釋放內存。 不要過度使用這個 - 例如,如果釋放 C 數組的位置不明顯,那么只需使用 numpy.
要查看的最后一個選項是沒有返回值而是修改輸入:
cdef void inv_c(double[:,::1] A, double[:,::1] B):
# check that A and B are the right size, then just write into B
# ...
這樣做的好處是,如果您需要在具有相同大小輸入的循環中調用它,那么您只需為整個循環進行一次分配。 您也可以將其擴展為包括工作數組,盡管這可能會更復雜一些。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.