开发指南
注意
此开发指南页面仍在积极更新中。我们希望让**添加新的黑盒优化器**尽可能简单。考虑到黑盒优化器在高维问题上运行时间相对较长,当添加任何新的黑盒优化器时,本库至少会有两名核心开发人员**手动**检查源代码并运行测试代码,以检验其编程的正确性。
在阅读本页之前,需要先阅读用户指南,以了解关于这个开源 Python 库 PyPop7 的一些基本信息。请注意,由于本主题主要面向高级开发人员,最终用户可以随意跳过此页面。
文档字符串约定
关于**文档字符串约定**,本库首先采用了 PEP 257。由于本库建立在 NumPy 生态系统之上,我们进一步使用了来自 numpydoc 的文档字符串约定。
此外,现在 PEP 465 被用作**矩阵乘法**的专用中缀运算符。我们正在修改所有现有的 Python 代码,以在 PEP 465 下简化它们。
库依赖
这个开源 Python 库严重依赖于三个核心的科学计算 Python 库,即 NumPy、SciPy 和 Scikit-Learn。更具体地说,对于所有优化器,numpy.array 数据结构被选为存储和操作种群(例如,采样、更新、索引和排序)的基本方式,这带来了显著的速度提升。有时,如果可能,会利用 Numba 来进一步加速大规模黑盒优化的实际运行时间。使用 NumPy 作为核心计算引擎的一个明显优势是,Pypop7 可以无缝集成到 NumPy 生态系统中,因为到目前为止,SciPy 涵盖的基于种群的黑盒优化算法数量有限。
关于此 Python 库的 **PyPI 安装**,使用的是 setup.cfg 文件。
关于此 Python 库的**开发**,使用的是 requirements.txt 文件。
统一的 API
对于 PyPop7,我们使用流行的面向对象编程(OOP)范式来构建所有优化器,这可以提供一致性、灵活性和简易性。我们没有采用另一种流行的面向过程编程范式。然而,在未来的版本中,我们可能会仅在最终用户层面(而不是开发层面)提供这样的接口。
为了提供统一的 API,所有优化器都需要继承名为 Optimizer 的抽象类。
优化器选项的初始化
对于优化器选项的初始化,应继承 Optimizer 的以下函数 __init__
def __init__(self, problem, options): # here all members will be inherited by any subclass of `Optimizer`
每个子类的所有*专属*成员将在继承 Optimizer 的上述函数后定义。
种群的初始化
我们分开了*优化器选项*和*种群*(一组个体)的初始化,以获得更好的灵活性。为实现这一点,应修改以下函数 initialize
def initialize(self): # for population initialization raise NotImplementedError # need to be implemented in any subclass of `Optimizer`
其另一个目标是最小化类成员的数量,使最终用户易于设置,但这会略微增加开发人员对变量的控制成本。
每一代的计算
通过修改以下函数 iterate 来更新每一代(迭代)
def iterate(self): # for one generation (iteration) raise NotImplementedError # need to be implemented in any subclass of `Optimizer`
整个优化过程的控制
通过修改以下函数 optimize 来控制整个搜索过程
def optimize(self, fitness_function=None): # entire optimization process return None # `None` should be replaced in any subclass of `Optimizer`
通常,常见的辅助任务(例如,打印详细信息、重启)都在此函数内部执行。
使用纯随机搜索作为说明性示例
在下面的 Python 代码中,我们使用纯随机搜索(PRS),这可能是最简单的黑盒优化器,作为一个说明性示例。
import numpy as np from pypop7.optimizers.core.optimizer import Optimizer # base class of all black-box optimizers class PRS(Optimizer): """Pure Random Search (PRS). .. note:: `PRS` is one of the *simplest* and *earliest* black-box optimizers, dating back to at least `1950s <https://pubsonline.informs.org/doi/abs/10.1287/opre.6.2.244>`_. Here we include it mainly for *benchmarking* purpose. As pointed out in `Probabilistic Machine Learning <https://probml.github.io/pml-book/book2.html>`_, *this should always be tried as a baseline*. Parameters ---------- problem : dict problem arguments with the following common settings (`keys`): * 'fitness_function' - objective function to be **minimized** (`func`), * 'ndim_problem' - number of dimensionality (`int`), * 'upper_boundary' - upper boundary of search range (`array_like`), * 'lower_boundary' - lower boundary of search range (`array_like`). options : dict optimizer options with the following common settings (`keys`): * 'max_function_evaluations' - maximum of function evaluations (`int`, default: `np.inf`), * 'max_runtime' - maximal runtime to be allowed (`float`, default: `np.inf`), * 'seed_rng' - seed for random number generation needed to be *explicitly* set (`int`); and with the following particular setting (`key`): * 'x' - initial (starting) point (`array_like`). Attributes ---------- x : `array_like` initial (starting) point. Examples -------- Use the `PRS` optimizer to minimize the well-known test function `Rosenbrock <http://en.wikipedia.org/wiki/Rosenbrock_function>`_: .. code-block:: python :linenos: >>> import numpy >>> from pypop7.benchmarks.base_functions import rosenbrock # function to be minimized >>> from pypop7.optimizers.rs.prs import PRS >>> problem = {'fitness_function': rosenbrock, # define problem arguments ... 'ndim_problem': 2, ... 'lower_boundary': -5.0*numpy.ones((2,)), ... 'upper_boundary': 5.0*numpy.ones((2,))} >>> options = {'max_function_evaluations': 5000, # set optimizer options ... 'seed_rng': 2022} >>> prs = PRS(problem, options) # initialize the optimizer class >>> results = prs.optimize() # run the optimization process >>> print(results) For its correctness checking of coding, refer to `this code-based repeatability report <https://tinyurl.com/mrx2kffy>`_ for more details. References ---------- Bergstra, J. and Bengio, Y., 2012. Random search for hyper-parameter optimization. Journal of Machine Learning Research, 13(2). https://www.jmlr.org/papers/v13/bergstra12a.html Schmidhuber, J., Hochreiter, S. and Bengio, Y., 2001. Evaluating benchmark problems by random guessing. A Field Guide to Dynamical Recurrent Networks, pp.231-235. https://ml.jku.at/publications/older/ch9.pdf Brooks, S.H., 1958. A discussion of random methods for seeking maxima. Operations Research, 6(2), pp.244-251. https://pubsonline.informs.org/doi/abs/10.1287/opre.6.2.244 """ def __init__(self, problem, options): """Initialize the class with two inputs (problem arguments and optimizer options).""" Optimizer.__init__(self, problem, options) self.x = options.get('x') # initial (starting) point self.verbose = options.get('verbose', 1000) self._n_generations = 0 # number of generations def _sample(self, rng): x = rng.uniform(self.initial_lower_boundary, self.initial_upper_boundary) return x def initialize(self): """Only for the initialization stage.""" if self.x is None: x = self._sample(self.rng_initialization) else: x = np.copy(self.x) assert len(x) == self.ndim_problem return x def iterate(self): """Only for the iteration stage.""" return self._sample(self.rng_optimization) def _print_verbose_info(self, fitness, y): """Save fitness and control console verbose information.""" if self.saving_fitness: if not np.isscalar(y): fitness.extend(y) else: fitness.append(y) if self.verbose and ((not self._n_generations % self.verbose) or (self.termination_signal > 0)): info = ' * Generation {:d}: best_so_far_y {:7.5e}, min(y) {:7.5e} & Evaluations {:d}' print(info.format(self._n_generations, self.best_so_far_y, np.min(y), self.n_function_evaluations)) def _collect(self, fitness, y=None): """Collect necessary output information.""" if y is not None: self._print_verbose_info(fitness, y) results = Optimizer._collect(self, fitness) results['_n_generations'] = self._n_generations return results def optimize(self, fitness_function=None, args=None): # for all iterations (generations) """For the entire optimization/evolution stage: initialization + iteration.""" fitness = Optimizer.optimize(self, fitness_function) x = self.initialize() # population initialization y = self._evaluate_fitness(x, args) # to evaluate fitness of starting point while not self._check_terminations(): self._print_verbose_info(fitness, y) # to save fitness and control console verbose information x = self.iterate() y = self._evaluate_fitness(x, args) # to evaluate each new point self._n_generations += 1 results = self._collect(fitness, y) # to collect all necessary output information return results
我们已决定采用*积极的*开发/维护模式,也就是说,**一旦添加了新的黑盒优化器或修复了严重的错误,我们将很快发布一个新的 PyPI 版本**。
可复现性代码/报告
优化器 |
可复现性代码 |
生成的图/数据 |
|---|---|---|
MMES |
||
FCMAES |
||
LMMAES |
||
LMCMA |
||
LMCMAES |
||
RMES |
||
R1ES |
||
VKDCMA |
||
VDCMA |
||
CCMAES2016 |
||
OPOA2015 |
||
OPOA2010 |
||
CCMAES2009 |
||
OPOC2009 |
||
OPOC2006 |
||
SEPCMAES |
||
DDCMA |
||
MAES |
||
FMAES |
||
CMAES |
||
SAMAES |
||
SAES |
||
CSAES |
||
DSAES |
||
SSAES |
||
RES |
||
R1NES |
||
SNES |
||
XNES |
||
ENES |
||
ONES |
||
SGES |
||
RPEDA |
||
UMDA |
||
AEMNA |
||
EMNA |
||
DCEM |
||
DSCEM |
||
MRAS |
||
SCEM |
||
SHADE |
||
JADE |
||
CODE |
||
TDE |
||
CDE |
||
CCPSO2 |
||
IPSO |
||
CLPSO |
||
CPSO |
||
SPSOL |
||
SPSO |
||
HCC |
不适用 |
不适用 |
COCMA |
不适用 |
不适用 |
COEA |
||
COSYNE |
||
增强模拟退火 (ESA) |
||
经典模拟退火 (CSA) |
||
噪声模拟退火 (NSA) |
不适用 |
不适用 |
ASGA |
||
GL25 |
||
G3PCX |
||
GENITOR |
不适用 |
不适用 |
LEP |
||
FEP |
||
CEP |
||
鲍威尔法 (POWELL) |
||
GPS |
不适用 |
不适用 |
NM |
||
HJ |
||
CS |
不适用 |
不适用 |
BES |
||
GS |
||
SRS |
不适用 |
不适用 |
ARHC |
||
RHC |
||
PRS |
用于开发的 Python IDE
尽管其他 Python IDE(例如 Spyder、Visual Studio)也可用于开发,但目前我们主要使用 PyCharm 社区版和 Anaconda 来开发我们的开源库。我们非常感谢 **jetbrains** 和 **anaconda** 提供这两个免费的开发工具。请注意,我们不排斥任何其他开发选择。