CQF预备知识:Python相关库 -- 准蒙特卡洛方法 scipy.stats
文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。
准蒙特卡洛方法
在讨论准蒙特卡洛(Quasi-Monte Carlo,QMC)之前,先简要介绍蒙特卡洛(Monte Carlo,MC)方法。蒙特卡洛方法,或称蒙特卡洛实验,是一类依赖于重复随机抽样以获得数值结果的计算算法。其核心思想是利用随机性来解决本质上可能是确定性的问题。这些方法通常用于物理和数学问题,尤其在难以或无法使用其他方法时最为有用。蒙特卡洛方法主要用于以下三类问题:优化、数值积分以及从概率分布中生成样本。
生成具有特定属性的随机数比听起来要复杂得多。简单的蒙特卡洛方法旨在生成独立同分布(IID)的样本点。但生成多组随机点可能会产生截然不同的结果。
在上述图中,两种情况下点都是随机生成的,且不考虑之前已抽取的点。显然,空间的某些区域未被探索到——这在模拟中可能会引发问题,因为特定的一组点可能会导致完全不同的行为。
蒙特卡洛方法的一大优势在于其具有已知的收敛特性。让我们来看一个五维空间中的平方和的均值问题:
f ( x ) = ( ∑ j = 1 5 x j ) 2 , f(\mathbf{x}) = \left( \sum_{j=1}^{5}x_j \right)^2, f(x)=(j=1∑5xj)2,
其中 x j ∼ U ( 0 , 1 ) x_j \sim \mathcal{U}(0,1) xj∼U(0,1)。该函数的均值是已知的,为 μ = 5 3 + 5 ( 5 − 1 ) 4 \mu = \frac{5}{3} + \frac{5(5-1)}{4} μ=35+45(5−1)。通过蒙特卡洛抽样,我们可以数值计算该均值,其近似误差的理论收敛率为 O ( n − 1 / 2 ) O(n^{-1/2}) 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)方法。这些方法是确定性的,能够很好地覆盖空间,并且其中一些方法可以继续使用并保持良好的特性。与蒙特卡洛方法的主要区别在于,QMC 方法生成的点并非独立同分布,而是知道之前的点。因此,某些方法也被称为序列。
上图展示了两组各包含 256 个点的样本。左侧是普通蒙特卡洛设计,而右侧是使用 Sobol’ 方法的 QMC 设计。我们可以清楚地看到,QMC 版本更加均匀。点在边界附近采样得更好,且几乎没有聚类或间隙。
评估均匀性的一种方法是使用一种称为“离散度”的度量。在这里,Sobol’ 点的离散度优于普通蒙特卡洛。
回到均值计算的问题,QMC 方法的误差收敛率也更好。对于此类函数,它们可以达到 O ( n − 1 ) O(n^{-1}) O(n−1) 的收敛率,对于非常光滑的函数,收敛率甚至更好。下图显示了 Sobol’ 方法的收敛率为 O ( n − 1 ) O(n^{-1}) 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 方法:scipy.stats.qmc.Sobol
和 scipy.stats.qmc.Halton
序列。
"""Sobol' 和 Halton 序列。"""
from scipy.stats import qmc
import numpy as npimport matplotlib.pyplot as pltrng = np.random.default_rng()n_sample = 256
dim = 2sample = {}# 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.Sobol
和 scipy.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 2^m 2m 个点,请使用
scipy.stats.qmc.Sobol
。 -
scipy.stats.qmc.Halton
允许采样或跳过任意数量的点,但代价是收敛速度比 Sobol’ 更慢。 -
永远不要删除序列的前几个点。这会破坏其特性。
-
总是使用随机化版本。
-
如果使用基于拉丁超立方采样的方法,则无法在不失去拉丁超立方特性的情况下添加点。(有一些方法可以做到,但尚未实现。)
风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。