scipy Python中的Copula实现超慢

yks3o0rb  于 2023-08-05  发布在  Python
关注(0)|答案(3)|浏览(176)

在风险管理模型的背景下计算VaR,我正在实现一个例程来创建具有用户定义边际的多元分布。
我将提供一个代码示例,使用高斯copula和gamma marginals:

# generate a multivariate normal distribution with a given input correlation:
mv = scipy.stats.multivariate_normal.rvs(mean = np.zeros(10), cov = correlation, size = (50000, 50, 10))

# transform the sample to a uniform with the above correlation:
unif = scipy.stats.norm.cdf(mv)

# apply the marginal:
mv_fin = scipy.stats.gamma.ppf(unif, a=1)

字符串
它运行良好,并产生预期的结果。关键问题是它非常慢,这三个步骤占用了整个模型运行时间的90%。我怎样才能加快速度?谢啦,谢啦

wsxa1bj1

wsxa1bj11#

我有一些想法,这当我帮助充分为我:
首先将计算并行化:
您正在生成大量的随机variates并转换它们,这是一个令人尴尬的 * 并行 * 问题。考虑使用joblib或多处理库来并行化随机 * 变量 * 的生成和转换。
第二个优化阵列操作:
我们使用scipy.stats.norm.cdf(mv)来转换整个 mv 数组。该操作在计算上可能是昂贵的,尤其是对于大型阵列。您可以尝试通过使用NumPy的ndarray方法将scipy.stats.norm.cdf替换为矢量化操作来优化它。
第三,使用更快的库生成随机数:
SciPy * 不是生成随机数 * 的最快库。如果速度是一个关键因素,请考虑使用NumPy等库,甚至是GPU加速库,如 CuPy或JAX,以生成随机变量。
forth分析你的代码:
使用像 cProfile 这样的分析工具或行分析器来确定代码的哪些部分占用的时间最多。这将帮助您将优化工作集中在代码中最耗时的部分。
这是一个示例代码

from joblib import Parallel, delayed
import numpy as np
import scipy.stats as stats

def generate_and_transform(i):
    mv = stats.multivariate_normal.rvs(mean=np.zeros(10), cov=correlation)
    unif = stats.norm.cdf(mv)
    mv_fin = stats.gamma.ppf(unif, a=1)
    return mv_fin

num_samples = 50000
results = Parallel(n_jobs=-1)(delayed(generate_and_transform)(i) for i in range(num_samples))

字符串
我认为JIT编译与Numba是帮助充分,但你应该文本和mesure他们

jmo0nnb3

jmo0nnb32#

对于scipy.stats.norm.cdfnumba-stats包提供了一个函数,用于计算显然相同的事情。默认参数需要显式地提供给这个函数,而不是Scipy。您可以使用numba_stats.stats.norm_cdf(mv, 0, 1)来调用它。这是2. 2倍快,在我的机器上与i5- 9600 KF。速度的提升主要来自于多线程的使用(与Scipy实现相比,Scipy实现目前为止是顺序的)。
关于scipy.stats.gamma.ppf,事情更加复杂,因为Numba包似乎没有提供任何替代方案(即gamma子模块似乎到目前为止还没有实现)。Scipy的实现是顺序的。慢的部分似乎来自于(scipy.special模块的)gammaincinv函数,该函数被调用用于unif的所有值(1用于第一个参数)。此函数调用igami,该函数似乎来自Cephes库。来源可以在这里找到。此函数使用了许多昂贵的数学函数,如powexplog(以及用于计算幂/无症状级数的基本循环)。库(和Scipy函数)似乎
经过优化,以提供相当高的准确性,但性能相对较差

有趣的是,在同一台机器上,使用相同版本的Scipy(1.10.1),在Linux上调用这个函数的速度比在Windows上快2.6倍。我认为这是由于在Linux上比在Windows上更有效地实现了默认的数学库。您可以尝试使用优化的数学库,例如英特尔的数学库,以加快计算速度。请注意,可能需要在目标计算机上重新生成Scipy(和Cephes)以提高性能。一个更简单的方法当然是尝试Intel Distribution for Python,假设您使用的是英特尔处理器(或者可能是AMD处理器,因为大多数英特尔工具都可以在上面工作,尽管性能可能不如英特尔处理器)。
另一种加速计算的可能方法是在计算函数scipy.stats.gamma.ppf时使用多线程。不幸的是,Scipy目前还不支持此功能。话虽如此,人们可以重新实现Scipy函数并调用gammaincinv函数,尽管这肯定是一项重要的工作。一个更简单(丑陋)的替代解决方案是简单地挂接函数scipy.stats.gamma._ppf,以便使用多个线程。实际上,这是一个纯Python函数,并且这样的函数可以重新赋值。在我的6核机器上,该策略将计算速度提高了3.75倍。这种优化可以与更好的数学库相结合。
下面是最终脚本:

from scipy.stats import multivariate_normal
import numpy as np
from numba_stats import norm
import scipy.stats
import scipy.special

import multiprocessing.pool
from types import MethodType
from joblib import Parallel, delayed

def multithreaded_gamma_ppf(self, q, a):
    cpu_count = multiprocessing.pool.get_context().cpu_count()
    size = q.shape[0]
    q_parts = [q[size*i//cpu_count:size*(i+1)//cpu_count] for i in range(cpu_count)]
    a_parts = [a[size*i//cpu_count:size*(i+1)//cpu_count] for i in range(cpu_count)]
    tmp = Parallel(n_jobs=cpu_count)(delayed(scipy.special.gammaincinv)(a_part, q_part) for a_part, q_part in zip(a_parts, q_parts))
    return np.concatenate(tmp)

# See https://stackoverflow.com/questions/10374527
scipy.stats.gamma._ppf = MethodType(multithreaded_gamma_ppf, scipy.stats.gamma)

if __name__ == '__main__':
    # Fake parameter just for a simple test
    correlation = 0.5

    # Windows:  5.95 s
    # Linux:    5.25 s
    mv = multivariate_normal.rvs(mean = np.zeros(10), cov = correlation, size = (50000, 50, 10))

    # Windows:  7.83 s => 3.53 s
    # Linux:    5.75 s => 2.21 s
    unif = norm.cdf(mv, 0, 1)

    # Windows:  2 min 48 s => 44.9 s
    # Linux:    1 min  6 s => 17.2 s
    mv_fin = scipy.stats.gamma.ppf(unif, a=1)

字符串
正如您所看到的,将多个线程与快速数学库相结合(或者只是为了简单起见而使用Linux)可以显著提高速度。优化后的实现时间从Windows上的181.78 s下降到Linux上的24.66 s。
这可是7.4倍的加速!

qkf9rpyu

qkf9rpyu3#

通过将gamma概率点函数替换为-ln(1 - x),可以将计算时间减少4倍。
我在试图找到逆不完全伽马函数的快速近似值时偶然发现了这一点:scipy.stats.gamma.ppf(unif, a=1)非常接近等于-np.log(1 - unif)。这是两个人的情节。
x1c 0d1x的数据
这是两个函数之间的差异图。请注意,Y轴的单位为10-15。



换句话说,这通常精确到小数点后15位。
使用gamma PPF的这种快速近似值,您可以像这样编辑原始代码。

mv = scipy.stats.multivariate_normal.rvs(mean = np.zeros(10), cov = correlation, size = (50000, 50, 10))
unif = scipy.stats.norm.cdf(mv)
mv_fin = -np.log(1 - unif)

字符串
在我的测试中,这大约是整体速度的4倍。
其他答案也解释了如何并行运行计算。这个想法可以与那些想法结合起来--既可以使用成本较低的计算,也可以并行运行。

相关问题