定义搜索空间
搜索空间由嵌套的函数表达式组成,其中包括随机表达式。随机表达式即超参数。从这个嵌套的随机程序中采样定义了随机搜索算法。超参数优化算法通过用自适应探索策略替换正常的“采样”逻辑来工作,这些策略并不会试图实际从搜索空间中指定的分布中采样。
最好将搜索空间视为随机参数采样程序。例如
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' - 一个在“情况 1”中使用的正值参数
- 'c2' - 一个在“情况 2”中使用的有界实值参数
这里需要注意的一点是,每个可优化的随机表达式都将一个 label 作为第一个参数。这些标签用于将参数选择返回给调用者,并在内部以各种方式使用。
需要注意的第二点是,我们在图中(在“情况 1”和“情况 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)
-
返回其中一个选项,该选项应为列表或元组。
options
的元素本身可以是[嵌套的]随机表达式。在这种情况下,仅出现在某些选项中的随机选择成为 条件 参数。 -
hp.pchoice(label, p_list)
-
返回其中一个选项,其中 p_list 是一个 (概率, 选项) 对的列表。options 的元素本身可以是[嵌套的]随机表达式。在这种情况下,仅出现在某些选项中的随机选择成为条件参数。
-
hp.randint(label[, low], upper)
-
返回范围 [low, upper) 内的随机整数。默认的 low 值为 0。此分布的语义是,与更远的整数值相比,损失函数在附近的整数值之间 没有 更多相关性。例如,这是一种适合描述随机种子的分布。如果损失函数在附近的整数值之间可能更相关,那么你可能应该使用“量化”连续分布之一,例如
quniform
、qloguniform
、qnormal
或qlognormal
。 -
hp.uniform(label, low, high)
- 返回
low
和high
之间的均匀分布值。 -
优化时,此变量被约束在一个双边区间内。
-
hp.quniform(label, low, high, q)
- 返回一个值,类似于 round(uniform(low, high) / q) * q
-
适用于目标函数对其仍然有点“平滑”,但应同时有上下界的离散值。
-
hp.quniformint(label, low, high)
或hp.uniformint(label, low, high, q)
- 返回一个值,类似于 round(uniform(low, high) / q) * q
- 参数
q
将始终设置为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 issue。
简而言之,你只需装饰一个顶级(即 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
# -- this will print 0, foo is called as usual.
print(foo(0))
# In describing search spaces you can use `foo` as you
# would in normal Python. These two calls will not actually call foo,
# they just record that foo should be called to evaluate the graph.
space1 = scope.foo(hp.uniform('a', 0, 10))
space2 = scope.foo(hp.uniform('a', 0, 10), hp.normal('b', 0, 1))
# -- this will print an pyll.Apply node
print(space1)
# -- this will draw a sample by running foo()
print(hyperopt.pyll.stochastic.sample(space1))
添加新型超参数
如果可能,应避免添加新型随机表达式来描述参数搜索空间。为了使所有搜索算法都能在所有空间上工作,搜索算法必须对描述空间的超参数种类达成一致。作为该库的维护者,我乐意考虑不时添加某些类型的表达式的可能性,但就像我说的,我希望尽可能避免这样做。添加新型随机表达式并不是 Hyperopt 旨在具备可扩展性的方式之一。