定义搜索空间

搜索空间由嵌套的函数表达式组成,包括随机表达式。 这些随机表达式就是超参数。 从这个嵌套的随机程序中进行采样定义了随机搜索算法。 超参数优化算法通过用自适应探索策略替换正常的“采样”逻辑来工作,这些策略并不试图实际从搜索空间中指定的分布中进行采样。

最好将搜索空间视为随机参数采样程序。例如:

from hyperopt import hp
space = hp.choice('a',
    [
        ('case 1', 1 + hp.lognormal('c1', 0, 1)),
        ('case 2', hp.uniform('c2', -10, 10))
    ])

运行此代码片段的结果是一个名为 space 的变量,它引用了一个表达式标识符及其参数的图。 实际上并没有进行任何采样,它只是一个描述如何采样点的图。 处理这种表达式图的代码在 hyperopt.pyll 中,我将这些图称为 pyll 图pyll 程序

如果你愿意,可以通过从中采样来评估样本空间。

import hyperopt.pyll.stochastic
print(hyperopt.pyll.stochastic.sample(space))

space 描述的搜索空间有 3 个参数:

  • 'a' - 选择案例
  • 'c1' - 用于 'case 1' 的正值参数
  • 'c2' - 用于 'case 2' 的有界实值参数

这里需要注意的一件事是,每个可优化的随机表达式都有一个 标签 作为第一个参数。 这些标签用于将参数选择返回给调用者,并且在内部也以各种方式使用。

第二件需要注意的事情是,我们在图的中间使用了元组(围绕每个 'case 1' 和 'case 2')。 列表、字典和元组都被升级为“确定性函数表达式”,以便它们可以成为搜索空间随机程序的一部分。

第三件需要注意的事情是嵌入到搜索空间描述中的数值表达式 1 + hp.lognormal('c1', 0, 1)。 就优化算法而言,在搜索空间中直接加上 1 和在目标函数逻辑中加上 1 之间没有区别。 作为设计者,你可以选择将这种处理放在哪里,以实现你想要的模块化。 请注意,搜索空间中的中间表达式结果可以是任意 Python 对象,即使在并行优化时使用 mongodb 也是如此。 很容易向搜索空间描述添加新的非随机表达式类型,请参见下文(第 2.3 节)了解如何操作。

第四件需要注意的事情是 'c1' 和 'c2' 是我们称之为 条件参数 的例子。 'c1' 和 'c2' 各自仅在 'a' 的特定值下才会在返回的样本中起作用。 如果 'a' 是 0,则使用 'c1' 而不是 'c2'。 如果 'a' 是 1,则使用 'c2' 而不是 'c1'。 只要合理,你应该将参数编码为这种方式的条件参数,而不是在目标函数中简单地忽略参数。 如果你暴露了 'c1' 有时对目标函数没有影响的事实(因为它对目标函数的参数没有影响),那么搜索可以更有效地进行信用分配。

参数表达式

hyperopt 的优化算法目前识别的随机表达式有:

  • hp.choice(label, options)
  • 返回选项之一,选项应为列表或元组。 选项的元素本身可以是 [嵌套的] 随机表达式。 在这种情况下,仅出现在某些选项中的随机选择成为 条件 参数。

  • hp.pchoice(label, p_list)

  • 返回选项之一,其中 p_list 是 (概率, 选项) 对的列表。 选项的元素本身可以是 [嵌套的] 随机表达式。 在这种情况下,仅出现在某些选项中的随机选择成为条件参数。

  • hp.randint(label[, low], upper)

  • 返回范围 [low, upper) 内的随机整数。默认的 low 值为 0。 此分布的语义是,与更远的整数值相比,相邻整数值之间的损失函数中没有更多的相关性。 这是一种描述随机种子的适当分布。 如果损失函数对相邻整数值更有可能相关,那么你应该使用一种“量化”的连续分布,例如 quniformqloguniformqnormalqlognormal

  • hp.uniform(label, low, high)

  • 返回 lowhigh 之间均匀分布的值。
  • 在优化时,此变量被约束在双边区间内。

  • hp.quniform(label, low, high, q)

  • 返回类似 round(uniform(low, high) / q) * q 的值。
  • 适用于目标函数相对于某个离散值仍然“平滑”,但该值应在上下限之间的情况。

  • hp.uniformint(label, low, high)

  • 返回一个在 lowhigh 之间均匀分布的整数值
  • 等同于 hp.quniform(label, low, high, 1.0)
  • 适用于目标函数相对于某个离散整数值仍然“平滑”,但该值应在上下限之间的情况。

  • hp.loguniform(label, low, high)

  • 返回一个根据 exp(uniform(low, high)) 分布的值,使得返回值的对数均匀分布。
  • 在优化时,该变量被限制在区间 [exp(low), exp(high)] 内。

  • hp.qloguniform(label, low, high, q)

  • 返回一个类似 round(exp(uniform(low, high)) / q) * q 的值
  • 适用于目标函数相对于某个离散变量“平滑”且随着变量值的大小变得更平滑,但该值应在上下限之间的情况。

  • hp.normal(label, mu, sigma)

  • 返回一个均值为 mu,标准差为 sigma 的正态分布实数值。在优化时,这是一个无约束变量。

  • hp.qnormal(label, mu, sigma, q)

  • 返回一个类似 round(normal(mu, sigma) / q) * q 的值
  • 适用于可能取值在 mu 附近的离散变量,但本质上是无界的。

  • hp.lognormal(label, mu, sigma)

  • 返回一个根据 exp(normal(mu, sigma)) 分布的值,使得返回值的对数正态分布。
  • 在优化时,该变量被限制为正值。

  • hp.qlognormal(label, mu, sigma, q)

  • 返回一个类似 round(exp(normal(mu, sigma)) / q) * q 的值
  • 适用于目标函数相对于某个离散变量平滑且随着变量值的大小变得更平滑,但该值仅在一侧有界的情况。

搜索空间示例:scikit-learn

为了实际看到这些可能性,让我们看看如何描述 scikit-learn 中分类算法的超参数空间。 (这一想法正在 hyperopt-sklearn 中开发)

from hyperopt import hp
space = hp.choice('classifier_type', [
    {
        'type': 'naive_bayes',
    },
    {
        'type': 'svm',
        'C': hp.lognormal('svm_C', 0, 1),
        'kernel': hp.choice('svm_kernel', [
            {'ktype': 'linear'},
            {'ktype': 'RBF', 'width': hp.lognormal('svm_rbf_width', 0, 1)},
            ]),
    },
    {
        'type': 'dtree',
        'criterion': hp.choice('dtree_criterion', ['gini', 'entropy']),
        'max_depth': hp.choice('dtree_max_depth',
                     [None, hp.qlognormal('dtree_max_depth_int', 3, 1, 1)]),
        'min_samples_split': hp.qlognormal('dtree_min_samples_split', 2, 1, 1),
    },
    ])

使用 pyll 添加非随机表达式

你可以将这些节点作为 pyll 函数的参数(参见 pyll)。 如果你想知道更多关于这方面的信息,请提交一个 github 问题。

简而言之,你只需要装饰一个顶层(即 pickle 友好的)函数,以便可以通过 scope 对象使用它。

import hyperopt.pyll
from hyperopt.pyll import scope


@scope.define
def foo(a, b=0):
     print('runing foo', a, b)
     return a + b / 2

# -- 这将打印 0,foo 像往常一样被调用。
print(foo(0))

# 在描述搜索空间时,你可以像在普通 Python 中一样使用 `foo`。这些调用实际上不会调用 foo,
# 它们只是记录应该调用 foo 来评估图。

space1 = scope.foo(hp.uniform('a', 0, 10))
space2 = scope.foo(hp.uniform('a', 0, 10), hp.normal('b', 0, 1))

# -- 这将打印一个 pyll.Apply 节点
print(space1)

# -- 这将通过运行 foo() 来抽取一个样本
print(hyperopt.pyll.stochastic.sample(space1))

添加新的超参数类型

应尽量避免添加新的随机表达式来描述参数搜索空间。 为了使所有搜索算法都能在所有空间上工作,搜索算法必须就描述空间的超参数类型达成一致。 作为库的维护者,我愿意不时地添加一些新的表达式类型,但正如我所说,我希望尽可能避免这种情况。 添加新的随机表达式并不是 hyperopt 旨在扩展的方式之一。