在SciPy中如何使用截断多元正态分布?

10
我正在尝试自动化一个过程,其中需要从截断的多元正态分布中提取样本。也就是说,它是一个多元正态分布(即高斯分布),但变量受到立方体的限制。我的给定输入是完整多元正态分布的均值和协方差,但我需要在我的箱子里得到样本。
到目前为止,我一直在拒绝箱子外的样本,并根据需要重新采样,但我发现我的过程有时会给我(a)较大的协方差和(b)接近边缘的平均值。这两个事件合谋影响了我的系统速度。
所以我想要做的是在第一次正确地对分布进行采样。谷歌搜索只导致了这个讨论或者scipy.stats中的truncnorm distribution。前者不确定,后者似乎只适用于一个变量。是否有任何本地多元截断正态分布?它会比拒绝样本更好,还是我应该做一些更聪明的事情?
我将开始着手解决自己的问题,即将未被截断的高斯函数旋转到其主轴上(使用SVD分解或其他方法),使用截断高斯函数的乘积来采样分布,然后再将样本旋转回来,并根据需要拒绝/重新采样。如果截断采样更有效,我认为这样可以更快地采样所需的分布。

1
这并不是一个Python答案,但你可以从R中翻译这个:https://cran.r-project.org/web/packages/tmvtnorm - Alexander McFarlane
5个回答

8
我已经重新实现了一个算法,它不依赖于MCMC,但可以从截断多元正态分布中创建独立同分布(iid)样本。拥有iid样本非常有用!我曾经也像Warrick的答案中描述的那样使用emcee,但在更高维度下需要的样本数量爆炸性增长,使其在我的用例中变得不切实际。
该算法由Botev (2016)介绍,使用基于minimax指数倾斜的接受-拒绝算法。它最初是在MATLAB中实现的,但为Python重新实现后,与在Python中使用MATLAB引擎运行相比,性能显著提高。它在更高维度下也很好地工作且速度快。
代码可在https://github.com/brunzema/truncated-mvn-sampler找到。
一个例子:
d = 10  # dimensions

# random mu and cov
mu = np.random.rand(d)
cov = 0.5 - np.random.rand(d ** 2).reshape((d, d))
cov = np.triu(cov)
cov += cov.T - np.diag(cov.diagonal())
cov = np.dot(cov, cov)

# constraints
lb = np.zeros_like(mu) - 1
ub = np.ones_like(mu) * np.inf

# create truncated normal and sample from it
n_samples = 100000
tmvn = TruncatedMVN(mu, cov, lb, ub)
samples = tmvn.sample(n_samples)

绘制第一维的结果为:

First dimension of truncated MVN


参考文献:

Botev, Z. I., (2016), The normal law under linear restrictions: simulation and estimation via minimax tilting, Journal of the Royal Statistical Society Series B, 79, issue 1, p. 125-148


7
根据维基百科文章,抽样多元截尾正态分布(MTND)更加困难。最终我采用相对简单的方法,使用MCMC采样器将初始猜测放松到MTND中,具体如下。
我使用emcee进行MCMC计算。我发现这个软件包非常易于使用。它只需要一个返回所需分布的对数概率的函数。因此,我定义了这个函数。
from numpy.linalg import inv

def lnprob_trunc_norm(x, mean, bounds, C):
    if np.any(x < bounds[:,0]) or np.any(x > bounds[:,1]):
        return -np.inf
    else:
        return -0.5*(x-mean).dot(inv(C)).dot(x-mean)

这里的C是多元正态分布的协方差矩阵。然后,您可以运行类似以下的内容。
S = emcee.EnsembleSampler(Nwalkers, Ndim, lnprob_trunc_norm, args = (mean, bounds, C))

pos, prob, state = S.run_mcmc(pos, Nsteps)

对于给定的meanboundsC。你需要一个行走者位置的初始猜测pos,可以是围绕平均值的球形。
pos = emcee.utils.sample_ball(mean, np.sqrt(np.diag(C)), size=Nwalkers)

或从未截断的多元正态分布中进行采样,

pos = numpy.random.multivariate_normal(mean, C, size=Nwalkers)

等等。我个人首先会进行数千步的样本丢弃,因为这很快,然后将剩余的异常值强制限制在范围内,然后运行MCMC采样。

收敛所需的步骤取决于您。

还要注意,emcee通过将参数threads=Nthreads添加到EnsembleSampler初始化中轻松支持基本并行化。因此,您可以使其运行得非常快。


2

我编写了一个脚本来测量迄今为止提供的解决方案的运行时间。 绘制50维度分布的100个样本时,运行时间如下:

  1. Botev 2016 implementation 0.029579秒
  2. Li and Ghosh 2015 implementation 2.597150秒
  3. 使用emcee 217.914969秒

为比较而言,不使用任何截断的Cholesky分解采样需要0.000201秒。

对于这种特定情况,Botev 2016 implementation 是从截断的多元正态分布中采样的最快方法。 emcee 方法要慢得多,但也许可以调整以提高性能。

该输出是在具有6个内核的x86机器上使用以下代码生成的。


import emcee
from minimax_tilting_sampler import TruncatedMVN
from trun_mvnt import rtmvn
import numpy as np
from scipy import linalg
from multiprocessing import Pool

import time

def log_prob_mvtnd(x, mean, lb, ub, vcm):
    if np.any(np.less(x,lb)) or np.any(np.greater(x,ub)):
        return -np.inf
    else:
        diff = x - mean
        return -0.5 * np.dot(diff, np.linalg.solve(vcm, diff))

def tmvnd_emcee(vcm,lb,ub,n,mean=None):
    
    ndim = vcm.shape[0]
    
    if not mean:
        mean = np.zeros(ndim)
    
    with Pool() as pool:
        sampler = emcee.EnsembleSampler(nwalkers=n,
                                        ndim=ndim,
                                        log_prob_fn=log_prob_mvtnd,
                                        args = (mean, lb,ub, vcm),
                                        pool=pool
                                        )
    
        p0 = np.random.multivariate_normal(mean, vcm, size=n)
        # p0 = emcee.utils.sample_ball(mean, np.sqrt(np.diag(vcm)), size=n)
        
        state = sampler.run_mcmc(p0, 10)
        sampler.reset()
        sampler.run_mcmc(state, 100);

    return sampler.get_last_sample().coords

def mvnd_chol(vcm,n):
    gauss_samples = np.random.randn(vcm.shape[0],n)
    R = linalg.cholesky(vcm, lower=True)
    return np.dot(R,gauss_samples)
    
def tmvnd_botev16(vcm,lb,ub,n):
    tmvn = TruncatedMVN(np.zeros(vcm.shape[0]), vcm, lb, ub)
    return tmvn.sample(n)

def tmvnd_Li_Ghosh_15(vcm,lb,ub,n):
    burn = 100 # burn-in first 100 iterates
    thin = 1 # thinning for Gibbs

    D =np.eye((vcm.shape[0]))
    return rtmvn(n, np.zeros(vcm.shape[0]), vcm, D, lb, ub, burn, thin).T 

if __name__ == '__main__':
    
    ndim=50
    nsamples = 100
    sigma = np.exp(-1*np.linspace(0, 10, ndim))
    vcm = linalg.toeplitz(sigma)
    
    trunc_sigma = 2
    ub = np.sqrt(np.diag(vcm))*trunc_sigma
    lb = -ub
    
    
    samplers = { 'not truncated' : lambda vcm,lb,ub,n: mvnd_chol(vcm,n),
                 'emcee' : lambda vcm,lb,ub,n: tmvnd_emcee(vcm,lb,ub,n),
                 'Botev 2016': lambda vcm,lb,ub,n: tmvnd_botev16(vcm,lb,ub,n),
                 'Li and Ghosh 2015': lambda vcm,lb,ub,n: tmvnd_Li_Ghosh_15(vcm,lb,ub,n)}
    
    samples = []
    for name, sampler in samplers.items():
        start = time.time()
        samples.append(sampler(vcm,lb,ub,nsamples))
        end = time.time()
        print("%s %f s" %(name, end - start))

1
模拟截断多元正态分布可能很棘手,通常需要通过MCMC进行一些条件抽样。
我的简短回答是,您可以使用我的代码(https://github.com/ralphma1203/trun_mvnt)!它实现了来自 Li and Ghosh (2015) 的 Gibbs采样算法,可以处理形如foo+bar的一般线性约束,即使您有非满秩D和比维数更多的约束。
import numpy as np
from trun_mvnt import rtmvn, rtmvt

########## Traditional problem, probably what you need... ##########
##### lower < X < upper #####
# So D = identity matrix

D = np.diag(np.ones(4))
lower = np.array([-1,-2,-3,-4])
upper = -lower
Mean = np.zeros(4)
Sigma = np.diag([1,2,3,4])

n = 10 # want 500 final sample
burn = 100 # burn-in first 100 iterates
thin = 1 # thinning for Gibbs


random_sample = rtmvn(n, Mean, Sigma, D, lower, upper, burn, thin) 
# Numpy array n-by-p as result!
random_sample

########## Non-full rank problem (more constraints than dimension) ##########
Mean = np.array([0,0])
Sigma = np.array([1, 0.5, 0.5, 1]).reshape((2,2)) # bivariate normal

D = np.array([1,0,0,1,1,-1]).reshape((3,2)) # non-full rank problem
lower = np.array([-2,-1,-2])
upper = np.array([2,3,5])

n = 500 # want 500 final sample
burn = 100 # burn-in first 100 iterates
thin = 1 # thinning for Gibbs

random_sample = rtmvn(n, Mean, Sigma, D, lower, upper, burn, thin) # Numpy array n-by-p as result!

-3
有点晚了,但是为了记录,你可以使用哈密顿蒙特卡罗算法。Matlab中有一个名为HMC exact的模块。将其翻译成Py应该不会太难。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接