使用装饰器元问题#
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的文档中报告。