使用装饰器元问题#

decorator 类是一个 Python 元问题,可用于动态修改和自定义用户定义问题(UDP)的公共 API 中的任何方法。

回想一下,在PyGMO的术语中,元问题是那些以另一个UDP作为输入,并以各种(希望是)有用的方式修改其行为的UDP。例如,decompose元问题将多目标优化问题转化为单目标问题。这种效果是在decompose的适应度函数中实现的,该函数首先调用构造时使用的UDP的适应度函数(元问题的内部问题),然后将得到的多维适应度向量转化为标量适应度。从Python的角度来看,我们可以说decompose 装饰了内部问题的适应度函数,根据特定的规则将多目标适应度转化为标量适应度。decorator问题通过允许用户不仅装饰适应度函数,还可以装饰UDP公共API中的任何其他方法,从而推广了这一概念。

decorator 问题旨在用于那些需要快速、临时且非侵入性地改变UDP行为的场景,它不应作为其他元问题的替代品(例如,如果您需要分解多目标UDP,您应该使用decompose问题,它将表现得更好并提供更多功能,而无需编写任何额外的代码)。用户还应记住,Python提供了许多其他修改类行为的方式,根据具体情况,这些方式可能比decorator问题更合适(例如,子类化、猴子补丁等)。

你好,装饰过的世界!#

像所有的元问题一样,decorator_problem 接受一个UDP作为构造参数。 在这个例子中,我们将使用一个Rosenbrock 问题来进行说明 (并展示decorator_problem 也可以与暴露的C++问题一起工作):

>>> import pygmo as pg
>>> rb = pg.rosenbrock()

我们现在可以继续构建一个装饰过的Rosenbrock问题:

>>> drb = pg.problem(pg.decorator_problem(rb))

在这种情况下,我们没有向decorator_problem的构造函数传递任何装饰器,因此drb在功能上将等同于未装饰的Rosenbrock问题。唯一的区别是,将drb打印到屏幕上会告诉我们rb已被decorator_problem包装:

>>> drb 
Problem name: Multidimensional Rosenbrock Function [decorated]
        C++ class name: ...

        Global dimension:                       2
        Integer dimension:                      0
        Fitness dimension:                      1
        Number of objectives:                   1
        Equality constraints dimension:         0
        Inequality constraints dimension:       0
        Lower bounds: [-5, -5]
        Upper bounds: [10, 10]
        Has batch fitness evaluation: false

        Has gradient: true
        User implemented gradient sparsity: false
        Expected gradients: 2
        Has hessians: false
        User implemented hessians sparsity: false

        Fitness evaluations: 0
        Gradient evaluations: 0

        Thread safety: none

Extra info:
        No registered decorators.

到目前为止还不错,虽然并不是特别令人兴奋 :)

现在让我们编写我们的第一个装饰器。这个装饰器旨在应用于Rosenbrock问题的适应度函数。除了返回原始的适应度外,它还会在屏幕上显示计算所需的时间。代码如下:

>>> def f_decor(orig_fitness_function):
...     def new_fitness_function(self, dv):
...         import time
...         start = time.monotonic()
...         fitness = orig_fitness_function(self, dv)
...         print("Elapsed time: {} seconds".format(time.monotonic() - start))
...         return fitness
...     return new_fitness_function

装饰器 f_decor() 将原始适应度函数作为输入,并在内部定义一个新的适应度函数。new_fitness_function() 具有与 UDP 接口规定的完全相同的原型:它将调用 decorator_problem 对象 (self) 和决策向量 (dv) 作为输入参数,并返回通过 orig_fitness_function() 计算的适应度向量。对原始适应度函数的调用被夹在几行代码之间,这些代码通过 Python 的 time.monotonic() 函数测量经过的运行时间。

我们现在可以构建一个装饰过的Rosenbrock问题:

>>> drb = pg.problem(pg.decorator_problem(rb, fitness_decorator=f_decor))

如你所见,我们已经将我们的装饰器 f_decor 作为名为 fitness_decorator 的关键字参数传递给了 decorator_problem 的构造函数。所有装饰器都必须作为关键字参数传递,其名称以 _decorator 结尾,并以要装饰的 UDP 方法开头(在本例中为 fitness)。drb 的字符串表示现在将反映出 fitness 函数已被装饰:

>>> drb 
Problem name: Multidimensional Rosenbrock Function [decorated]
        C++ class name: ...

        Global dimension:                       2
        Integer dimension:                      0
        Fitness dimension:                      1
        Number of objectives:                   1
        Equality constraints dimension:         0
        Inequality constraints dimension:       0
        Lower bounds: [-5, -5]
        Upper bounds: [10, 10]
        Has batch fitness evaluation: false

        Has gradient: true
        User implemented gradient sparsity: false
        Expected gradients: 2
        Has hessians: false
        User implemented hessians sparsity: false

        Fitness evaluations: 0
        Gradient evaluations: 0

        Thread safety: none

Extra info:
        Registered decorators:
                fitness

现在让我们验证一下适应度函数是否如预期那样被装饰了:

>>> fv = drb.fitness([1, 2]) 
Elapsed time: ... seconds
>>> print(fv)
[100.]

耶!

记录健身评估#

在上一节中,我们看到了一个简单的无状态装饰器的例子。然而,装饰器不一定需要是无状态的:由于UDP API的所有函数都将调用问题作为第一个输入参数,我们可以实现改变问题本身状态的装饰器。作为一个具体的例子,我们现在将编写一个适应度函数装饰器,该装饰器会在调用问题中记录传递给适应度函数的所有决策向量。

健身日志装饰器相当简单:

>>> def f_log_decor(orig_fitness_function):
...     def new_fitness_function(self, dv):
...         if hasattr(self, "dv_log"):
...             self.dv_log.append(dv)
...         else:
...             self.dv_log = [dv]
...         return orig_fitness_function(self, dv)
...     return new_fitness_function

逻辑很简单:

  • 第一次调用装饰问题的适应度函数时,条件 hasattr(self, "dv_log") 将为 False,因为最初,装饰问题不包含任何日志结构。装饰后的适应度函数将继续向问题添加一个包含当前决策向量 dv 的 1 元素 list,称为 dv_log

  • 在后续调用装饰后的适应度函数时,当前的决策向量 dv 将被追加到 dv_log 列表中。

让我们看看日志装饰器的实际应用。首先,我们创建一个装饰后的问题:

>>> drb = pg.problem(pg.decorator_problem(rb, fitness_decorator=f_log_decor))

其次,我们验证drb内部的UDP尚未包含dv_log日志结构:

>>> hasattr(drb.extract(pg.decorator_problem), "dv_log")
False

接下来,我们多次调用适应度函数:

>>> drb.fitness([1, 2])
array([100.])
>>> drb.fitness([3, 4])
array([2504.])
>>> drb.fitness([5, 6])
array([36116.])

我们现在可以验证,所有迄今为止传递给适应度函数的决策向量都已被记录在内部的decorator_problem对象中:

>>> drb.extract(pg.decorator_problem).dv_log
[array([1., 2.]), array([3., 4.]), array([5., 6.])]

一切按计划进行!

当然,这里展示的日志记录相当简单。在实际应用中,人们可能希望依赖Python的logging模块,而不是使用临时的日志结构,也许还希望记录其他信息(例如,适应度向量)。

还有什么可以被装饰?#

在上面的例子中,我们专注于适应度函数的装饰,可以说,这是UDP中最重要的函数。decorator_problem 然而可以用来装饰属于UDP公共API的任何方法,包括梯度和Hessian计算、稀疏性相关方法、随机种子设置器和获取器等。可以在UDP中实现(和装饰)的方法的详尽列表在pygmo.problem的文档中报告。