Эффективные матричные операции в Cython без объектов Python - PullRequest
0 голосов
/ 26 апреля 2018

Я пишу функцию, которую я хотел бы перевести на C, насколько это возможно, используя Cython.Для этого мне нужно будет использовать операции линейной алгебры.Вот моя функция. РЕДАКТИРОВАТЬ: урок, который я усвоил, состоит в том, чтобы попытаться позаботиться о линейной алгебре вне циклов, что я в основном смог сделать.В противном случае прибегните к переносу 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

Для этого мне понадобятся функции для умножения матриц, транспонирования, детерминанта, инверсии матриц и холески.разложение,.Я видел некоторые сообщения об использовании 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 сейчас.Также, если у вас есть другие предложения, основанные на новой реализации, пожалуйста, сообщите мне.

enter image description here

Например, в строке 221 я получаю это сообщение прикомпиляция: «Индекс должен быть напечатан для более эффективного доступа».Но я думал, что набрал индекс k.Кроме того, поскольку 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]

1 Ответ

0 голосов
/ 28 апреля 2018

Несколько небольших замечаний относительно ваших оптимизированных функций 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.Я бы по-прежнему оставлял такие вещи, как возвращаемые значения, как массивы, так как подсчет ссылок и возможность отправлять их обратно в Python очень полезны.

 cdef double* work = <double*>malloc(n*n*sizeof(double))
 try:
     # rest of function
 finally:
     free(work)

Вам необходимо импортировать 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
    # ...

Преимущество этого состоит в том, что если вам нужно вызывать это в цикле с входами идентичного размера, то вам нужно сделать только одно распределение для всего цикла.Вы можете расширить это, включив и рабочие массивы, хотя это может быть немного сложнее.

...