准蒙特卡罗方法#

在讨论准蒙特卡罗(QMC)方法之前,先简单介绍一下蒙特卡罗(MC)方法。蒙特卡罗方法,或蒙特卡罗实验,是一大类计算算法,它们依赖于重复随机抽样来获得数值结果。其基本概念是使用随机性来解决原则上可能是确定性的问题。它们通常用于物理和数学问题,并且在难以或不可能使用其他方法时最有用。蒙特卡罗方法主要用于三个问题类别:优化、数值积分以及从概率分布中生成样本。

生成具有特定属性的随机数比听起来更复杂。简单的蒙特卡罗方法旨在对点进行采样,使其独立且同分布(IID)。但是,生成多组随机点可能会产生截然不同的结果。

" "

在上面两个图中,点的生成都是随机的,不了解先前绘制的点。很明显,空间中的某些区域未被探索,这可能会在模拟中引起问题,因为一组特定的点可能会触发完全不同的行为。

蒙特卡罗方法的一大优点是它具有已知的收敛特性。让我们看一下 5 维中平方和的均值

\[f(\mathbf{x}) = \left( \sum_{j=1}^{5}x_j \right)^2,\]

其中 \(x_j \sim \mathcal{U}(0,1)\)。它有一个已知的平均值,\(\mu = 5/3+5(5-1)/4\)。使用蒙特卡罗抽样,我们可以用数值方法计算该平均值,并且近似误差遵循 \(O(n^{-1/2})\) 的理论速率。

" "

尽管收敛性得到保证,但从业人员往往希望有一个更具确定性的探索过程。使用普通的蒙特卡罗方法,可以使用种子来获得可重复的过程。但是,固定种子会破坏收敛特性:给定的种子可能适用于一类问题,而对于另一类问题则可能失效。

通常为了以确定性方式遍历空间,使用跨越所有参数维度的规则网格,也称为饱和设计。让我们考虑单位超立方体,所有边界的范围从 0 到 1。现在,点之间的距离为 0.1,则填充单位区间所需的点数为 10。在二维超立方体中,相同的间距将需要 100 个点,而在三维中则需要 1000 个点。随着维数的增加,填充空间所需的实验次数会随着空间维数的增加而呈指数增长。这种指数增长被称为“维数灾难”。

>>> import numpy as np
>>> disc = 10
>>> x1 = np.linspace(0, 1, disc)
>>> x2 = np.linspace(0, 1, disc)
>>> x3 = np.linspace(0, 1, disc)
>>> x1, x2, x3 = np.meshgrid(x1, x2, x3)
" "

为了缓解这个问题,设计了 QMC 方法。它们是确定性的,对空间具有良好的覆盖率,并且其中一些方法可以继续使用并保持良好的属性。与蒙特卡罗方法的主要区别在于,点不是 IID 的,而是了解之前的点。因此,某些方法也称为序列。

" "

此图显示了 2 组 256 个点。左边的设计是普通的蒙特卡罗方法,而右边的设计是使用 Sobol' 方法的 QMC 设计。我们清楚地看到,QMC 版本更加均匀。这些点在边界附近采样得更好,并且集群或间隙更少。

评估均匀性的一种方法是使用称为差异的度量。这里,Sobol' 点的差异比粗略的蒙特卡罗方法要好。

回到平均值的计算,QMC 方法的误差收敛率也更好。对于此函数,它们可以达到 \(O(n^{-1})\),对于非常平滑的函数,甚至可以达到更好的速率。下图显示 Sobol' 方法的速率为 \(O(n^{-1})\)

" "

有关更多数学细节,请参阅 scipy.stats.qmc 的文档。

计算差异#

让我们考虑两组点。从下图可以看出,左边的设计比右边的设计覆盖了更多的空间。这可以使用 scipy.stats.qmc.discrepancy 度量来量化。差异越小,样本越均匀。

>>> import numpy as np
>>> from scipy.stats import qmc
>>> space_1 = np.array([[1, 3], [2, 6], [3, 2], [4, 5], [5, 1], [6, 4]])
>>> space_2 = np.array([[1, 5], [2, 4], [3, 3], [4, 2], [5, 1], [6, 6]])
>>> l_bounds = [0.5, 0.5]
>>> u_bounds = [6.5, 6.5]
>>> space_1 = qmc.scale(space_1, l_bounds, u_bounds, reverse=True)
>>> space_2 = qmc.scale(space_2, l_bounds, u_bounds, reverse=True)
>>> qmc.discrepancy(space_1)
0.008142039609053464
>>> qmc.discrepancy(space_2)
0.010456854423869011
" "

使用 QMC 引擎#

实现了几个 QMC 采样器/引擎。这里我们看看两种最常用的 QMC 方法:scipy.stats.qmc.Sobolscipy.stats.qmc.Halton 序列。

"""Sobol' and Halton sequences."""
from scipy.stats import qmc
import numpy as np

import matplotlib.pyplot as plt


rng = np.random.default_rng()

n_sample = 256
dim = 2

sample = {}

# Sobol'
engine = qmc.Sobol(d=dim, seed=rng)
sample["Sobol'"] = engine.random(n_sample)

# Halton
engine = qmc.Halton(d=dim, seed=rng)
sample["Halton"] = engine.random(n_sample)

fig, axs = plt.subplots(1, 2, figsize=(8, 4))

for i, kind in enumerate(sample):
    axs[i].scatter(sample[kind][:, 0], sample[kind][:, 1])

    axs[i].set_aspect('equal')
    axs[i].set_xlabel(r'$x_1$')
    axs[i].set_ylabel(r'$x_2$')
    axs[i].set_title(f'{kind}—$C^2 = ${qmc.discrepancy(sample[kind]):.2}')

plt.tight_layout()
plt.show()
" "

警告

QMC 方法需要特别注意,用户必须阅读文档以避免常见的陷阱。例如,Sobol' 需要点的数量为 2 的幂。此外,稀疏化、燃烧或其他点选择可能会破坏序列的属性,并导致一组不如蒙特卡罗方法的点。

QMC 引擎是状态感知的。这意味着您可以继续该序列,跳过一些点或重置它。让我们从 scipy.stats.qmc.Halton 中取 5 个点。然后要求第二组 5 个点

>>> from scipy.stats import qmc
>>> engine = qmc.Halton(d=2)
>>> engine.random(5)
array([[0.22166437, 0.07980522],  # random
       [0.72166437, 0.93165708],
       [0.47166437, 0.41313856],
       [0.97166437, 0.19091633],
       [0.01853937, 0.74647189]])
>>> engine.random(5)
array([[0.51853937, 0.52424967],  # random
       [0.26853937, 0.30202745],
       [0.76853937, 0.857583  ],
       [0.14353937, 0.63536078],
       [0.64353937, 0.01807683]])

现在我们重置序列。要求 5 个点会导致相同的前 5 个点

>>> engine.reset()
>>> engine.random(5)
array([[0.22166437, 0.07980522],  # random
       [0.72166437, 0.93165708],
       [0.47166437, 0.41313856],
       [0.97166437, 0.19091633],
       [0.01853937, 0.74647189]])

这里我们推进序列以获得相同的第二组 5 个点

>>> engine.reset()
>>> engine.fast_forward(5)
>>> engine.random(5)
array([[0.51853937, 0.52424967],  # random
       [0.26853937, 0.30202745],
       [0.76853937, 0.857583  ],
       [0.14353937, 0.63536078],
       [0.64353937, 0.01807683]])

注意

默认情况下,scipy.stats.qmc.Sobolscipy.stats.qmc.Halton 都是加扰的。收敛特性更好,并且它可以防止在高维度中出现条纹或明显的点模式。应该没有实际理由不使用加扰版本。

制作 QMC 引擎,即,子类化 QMCEngine#

要制作自己的 scipy.stats.qmc.QMCEngine,必须定义一些方法。以下是包装 numpy.random.Generator 的示例。

>>> import numpy as np
>>> from scipy.stats import qmc
>>> class RandomEngine(qmc.QMCEngine):
...     def __init__(self, d, seed=None):
...         super().__init__(d=d, seed=seed)
...         self.rng = np.random.default_rng(self.rng_seed)
...
...
...     def _random(self, n=1, *, workers=1):
...         return self.rng.random((n, self.d))
...
...
...     def reset(self):
...         self.rng = np.random.default_rng(self.rng_seed)
...         self.num_generated = 0
...         return self
...
...
...     def fast_forward(self, n):
...         self.random(n)
...         return self

然后我们像使用其他 QMC 引擎一样使用它

>>> engine = RandomEngine(2)
>>> engine.random(5)
array([[0.22733602, 0.31675834],  # random
       [0.79736546, 0.67625467],
       [0.39110955, 0.33281393],
       [0.59830875, 0.18673419],
       [0.67275604, 0.94180287]])
>>> engine.reset()
>>> engine.random(5)
array([[0.22733602, 0.31675834],  # random
       [0.79736546, 0.67625467],
       [0.39110955, 0.33281393],
       [0.59830875, 0.18673419],
       [0.67275604, 0.94180287]])

使用 QMC 的指南#

  • QMC 有规则!请务必阅读文档,否则您可能无法获得比蒙特卡罗方法更好的结果。

  • 如果您需要恰好 \(2^m\) 个点,请使用 scipy.stats.qmc.Sobol

  • scipy.stats.qmc.Halton 允许采样或跳过任意数量的点。这是以比 Sobol' 更慢的收敛速度为代价的。

  • 永远不要删除序列的第一个点。它会破坏属性。

  • 加扰总是更好。

  • 如果您使用基于 LHS 的方法,则不能在不丢失 LHS 属性的情况下添加点。(有一些方法可以做到这一点,但尚未实现。)