开发指南

注意

开发指南页面仍在积极更新中。我们希望让**添加新的黑盒优化器**尽可能简单。考虑到黑盒优化器在高维问题上运行时间相对较长,当添加任何新的黑盒优化器时,本库至少会有两名核心开发人员**手动**检查源代码并运行测试代码,以检验其编程的正确性。

在阅读本页之前,需要先阅读用户指南,以了解关于这个开源 Python 库 PyPop7 的一些基本信息。请注意,由于本主题主要面向高级开发人员,最终用户可以随意跳过此页面。

文档字符串约定

关于**文档字符串约定**,本库首先采用了 PEP 257。由于本库建立在 NumPy 生态系统之上,我们进一步使用了来自 numpydoc 的文档字符串约定。

此外,现在 PEP 465 被用作**矩阵乘法**的专用中缀运算符。我们正在修改所有现有的 Python 代码,以在 PEP 465 下简化它们。

库依赖

这个开源 Python 库严重依赖于三个核心的科学计算 Python 库,即 NumPySciPyScikit-Learn。更具体地说,对于所有优化器,numpy.array 数据结构被选为存储和操作种群(例如,采样、更新、索引和排序)的基本方式,这带来了显著的速度提升。有时,如果可能,会利用 Numba 来进一步加速大规模黑盒优化的实际运行时间。使用 NumPy 作为核心计算引擎的一个明显优势是,Pypop7 可以无缝集成到 NumPy 生态系统中,因为到目前为止,SciPy 涵盖的基于种群的黑盒优化算法数量有限。

  • 关于此 Python 库的 **PyPI 安装**,使用的是 setup.cfg 文件。

  • 关于此 Python 库的**开发**,使用的是 requirements.txt 文件。

统一的 API

对于 PyPop7,我们使用流行的面向对象编程(OOP)范式来构建所有优化器,这可以提供一致性、灵活性和简易性。我们没有采用另一种流行的面向过程编程范式。然而,在未来的版本中,我们可能会仅在最终用户层面(而不是开发层面)提供这样的接口。

为了提供统一的 API,所有优化器都需要继承名为 Optimizer 的抽象类。

  • 所有优化器共享的所有成员(例如,fitness_functionndim_problem 等)都应在该类的 __init__ 方法中定义。

  • 除特殊情况外,所有对最终用户公开的方法都应在该类中定义。

  • 所有与公平基准比较相关的设置(例如,max_function_evaluationsmax_runtimefitness_threshold)都应在该类的 __init__ 方法中定义。

优化器选项的初始化

对于优化器选项的初始化,应继承 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

_repeat_mmes.py

figures

FCMAES

_repear_fcmaes.py

figures

LMMAES

_repeat_lmmaes.py

figures

LMCMA

_repeat_lmcma.py

figures

LMCMAES

_repeat_lmcmaes.py

data

RMES

_repeat_rmes.py

figures

R1ES

_repeat_r1es.py

figures

VKDCMA

_repeat_vkdcma.py

data

VDCMA

_repeat_vdcma.py

data

CCMAES2016

_repeat_ccmaes2016.py

figures

OPOA2015

_repeat_opoa2015.py

figures

OPOA2010

_repeat_opoa2010.py

figures

CCMAES2009

_repeat_ccmaes2009.py

figures

OPOC2009

_repeat_opoc2009.py

figures

OPOC2006

_repeat_opoc2006.py

figures

SEPCMAES

_repeat_sepcmaes.py

data

DDCMA

_repeat_ddcma.py

data

MAES

_repeat_maes.py

figures

FMAES

_repeat_fmaes.py

figures

CMAES

_repeat_cmaes.py

data

SAMAES

_repeat_samaes.py

figures

SAES

_repeat_saes.py

data

CSAES

_repeat_csaes.py

figures

DSAES

_repeat_dsaes.py

figures

SSAES

_repeat_ssaes.py

figures

RES

_repeat_res.py

figures

R1NES

_repeat_r1nes.py

data

SNES

_repeat_snes.py

data

XNES

_repeat_xnes.py

data

ENES

_repeat_enes.py

data

ONES

_repeat_ones.py

data

SGES

_repeat_sges.py

data

RPEDA

_repeat_rpeda.py

data

UMDA

_repeat_umda.py

data

AEMNA

_repeat_aemna.py

data

EMNA

_repeat_emna.py

data

DCEM

_repeat_dcem.py

data

DSCEM

_repeat_dscem.py

data

MRAS

_repeat_mras.py

data

SCEM

_repeat_scem.py

data

SHADE

_repeat_shade.py

data

JADE

_repeat_jade.py

data

CODE

_repeat_code.py

data

TDE

_repeat_tde.py

figures

CDE

_repeat_cde.py

data

CCPSO2

_repeat_ccpso2.py

data

IPSO

_repeat_ipso.py

data

CLPSO

_repeat_clpso.py

data

CPSO

_repeat_cpso.py

data

SPSOL

_repeat_spsol.py

data

SPSO

_repeat_spso.py

data

HCC

不适用

不适用

COCMA

不适用

不适用

COEA

_repeat_coea.py

figures

COSYNE

_repeat_cosyne.py

data

增强模拟退火 (ESA)

_repeat_esa.py

data

经典模拟退火 (CSA)

_repeat_csa.py

data

噪声模拟退火 (NSA)

不适用

不适用

ASGA

_repeat_asga.py

data

GL25

_repeat_gl25.py

data

G3PCX

_repeat_g3pcx.py

figures

GENITOR

不适用

不适用

LEP

_repeat_lep.py

data

FEP

_repeat_fep.py

data

CEP

_repeat_cep.py

data

鲍威尔法 (POWELL)

_repeat_powell.py

data

GPS

不适用

不适用

NM

_repeat_nm.py

data

HJ

_repeat_hj.py

data

CS

不适用

不适用

BES

_repeat_bes.py

figures

GS

_repeat_gs.py

figures

SRS

不适用

不适用

ARHC

_repeat_arhc.py

data

RHC

_repeat_rhc.py

data

PRS

_repeat_prs.py

figures

用于开发的 Python IDE

尽管其他 Python IDE(例如 SpyderVisual Studio)也可用于开发,但目前我们主要使用 PyCharm 社区版Anaconda 来开发我们的开源库。我们非常感谢 **jetbrains** 和 **anaconda** 提供这两个免费的开发工具。请注意,我们不排斥任何其他开发选择。