定义一个待最小化的函数

Hyperopt 在指定一个待最小化的目标函数时,提供了几个不同级别的灵活性/复杂性。作为设计者,需要思考以下问题:

  • 除了函数的返回值,你是否还想保存其他信息,例如在计算目标函数过程中收集的其他统计信息和诊断信息?
  • 你是否想使用需要比函数值更多的优化算法?
  • 你是否想在并行进程之间进行通信?(例如,其他工作进程或最小化算法)

接下来的几节将探讨实现一个目标函数的几种不同方式,该目标函数用于最小化单个变量上的二次目标函数。在每一节中,我们将在从 -10 到 +10 的有界范围内进行搜索,我们可以用一个搜索空间来描述:

space = hp.uniform('x', -10, 10)

有关如何指定更复杂的搜索空间的信息,请参阅此页面

最简单的情况

Hyperopt 的优化算法与目标函数之间最简单的通信协议是,目标函数接收搜索空间中的一个有效点,并返回与该点相关的浮点数损失(即负效用)。

from hyperopt import fmin, tpe, hp
best = fmin(fn=lambda x: x ** 2,
            space=hp.uniform('x', -10, 10),
            algo=tpe.suggest,
            max_evals=100)
print(best)

这种协议的优点是非常易读且快速输入。如你所见,它几乎是一行代码。这种协议的缺点是: (1) 这种函数无法将每次评估的额外信息返回给 trials 数据库, (2) 这种函数无法与搜索算法或其他并发函数评估进行交互。 你将在接下来的示例中看到为什么你可能想要做这些事情。

通过 Trials 对象附加额外信息

如果你的目标函数复杂且运行时间较长,你几乎肯定会希望保存比最终输出的一个浮点数损失更多的统计信息和诊断信息。对于这种情况,fmin 函数被设计为处理字典返回值。其思想是,你的损失函数可以返回一个嵌套字典,其中包含你想要的所有统计信息和诊断信息。然而,实际情况比这稍微不那么灵活:例如,当使用 MongoDB 时,字典必须是一个有效的 JSON 文档。尽管如此,仍然有很多灵活性来存储特定领域的辅助结果。

当目标函数返回一个字典时,fmin 函数会在返回值中查找一些特殊的键值对,并将其传递给优化算法。有两个强制性的键值对:

  • status - hyperopt.STATUS_STRINGS 中的一个键,例如 'ok' 表示成功完成,'fail' 表示函数未定义的情况。
  • loss - 你试图最小化的浮点数值,如果状态为 'ok',则必须存在此值。

fmin 函数还响应一些可选键:

  • attachments - 一个键值对的字典,其键是短字符串(如文件名),其值是可能较长的字符串(如文件内容),不应每次访问记录时都从数据库加载。(此外,MongoDB 限制了普通键值对的长度,因此一旦你的值达到兆字节级别,你可能必须将其设为附件。)
  • loss_variance - 浮点数 - 随机目标函数的不确定性
  • true_loss - 浮点数 - 在进行超参数优化时,如果你以这个名字存储模型的泛化误差,那么有时可以从内置的绘图例程中获得更漂亮的输出。
  • true_loss_variance - 浮点数 - 泛化误差的不确定性

由于字典旨在与各种后端存储机制一起使用,因此你应该确保它是 JSON 兼容的。只要它是一个由字典、列表、元组、数字、字符串和日期时间组成的树状结构图,你就会没问题。

提示: 要存储 numpy 数组,请将其序列化为字符串,并考虑将其存储为附件。

提示: 如果你需要复现随机搜索的结果(例如用于演示),请将类型为 np.random.Generator 的对象传递给 fmin 函数,使用 rstate 可选参数。

以字典返回风格编写上述函数,它看起来像这样:

from hyperopt import fmin, tpe, hp, STATUS_OK


def objective(x):
    return {'loss': x ** 2, 'status': STATUS_OK }

best = fmin(objective,
            space=hp.uniform('x', -10, 10),
            algo=tpe.suggest,
            max_evals=100)

print(best)

Trials 对象

要真正理解返回字典的目的,让我们修改目标函数以返回更多内容,并将显式的 trials 参数传递给 fmin

import pickle
import time
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials


def objective(x):
    return {
        'loss': x ** 2,
        'status': STATUS_OK,
        # -- 像这样存储其他结果
        'eval_time': time.time(),
        'other_stuff': {'type': None, 'value': [0, 1, 2]},
        # -- 附件的处理方式不同
        'attachments':
            {'time_module': pickle.dumps(time.time)}
        }
trials = Trials()
best = fmin(objective,
            space=hp.uniform('x', -10, 10),
            algo=tpe.suggest,
            max_evals=100,
            trials=trials)

print(best)

在这种情况下,对 fmin 的调用与之前一样进行,但通过直接传递 trials 对象,我们可以检查在实验期间计算的所有返回值。

例如:

  • trials.trials - 一个表示搜索中所有内容的字典列表
  • trials.results - 搜索期间由 'objective' 返回的字典列表
  • trials.losses() - 损失列表(每个 'ok' 试验的浮点数)
  • trials.statuses() - 状态字符串列表

这个 trials 对象可以保存、传递给内置的绘图例程,或使用自定义代码进行分析。以下是一个简单的示例,展示了一种保存和随后加载 trials 对象的方法。

import pickle
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK


def objective(x):
    return {'loss': x ** 2, 'status': STATUS_OK }

# 初始化一个空的 trials 数据库
trials = Trials()

# 在搜索空间上执行 100 次评估
best = fmin(objective,
            space=hp.uniform('x', -10, 10),
            algo=tpe.suggest,
            trials=trials,
            max_evals=100)

# trials 数据库现在包含 100 个条目,可以使用 pickle 或其他方法保存/重新加载
pickle.dump(trials, open("my_trials.pkl", "wb"))
trials = pickle.load(open("my_trials.pkl", "rb"))

# 执行额外的 100 次评估
# 注意 max_evals 设置为 200,因为数据库中已经存在 100 个条目
best = fmin(objective,
    space=hp.uniform('x', -10, 10),
    algo=tpe.suggest,
    trials=trials,
    max_evals=200)

print(best)

附件 由一种特殊机制处理,使得相同的代码可以用于 TrialsMongoTrials

你可以像这样检索试验附件,这将检索第 5 个试验的 'time_module' 附件:

msg = trials.trial_attachments(trials.trials[5])['time_module']
time_module = pickle.loads(msg)

语法有些复杂,因为附件是大字符串,所以在使用 MongoTrials 时,我们不希望下载超过必要的内容。字符串也可以通过 trials.attachments 全局附加到整个 trials 对象,其行为类似于字符串到字符串的字典。

注意 目前,试验特定的附件会被放入同一个全局 trials 附件字典中,但这在未来可能会改变,并且对于 MongoTrials 来说并非如此。

用于与 MongoDB 实时通信的 Ctrl 对象

fmin() 可以为你的目标函数提供一个与并行实验使用的 mongodb 的句柄。此机制使得可以用部分结果更新数据库,并与评估不同点的其他并发进程进行通信。你的目标函数甚至可以添加新的搜索点,就像 rand.suggest 一样。

基本技术包括:

  • 使用 fmin_pass_expr_memo_ctrl 装饰器
  • 在你的函数中调用 pyll.rec_eval 以从 exprmemo 构建搜索空间点
  • 使用 ctrl,一个 hyperopt.Ctrl 的实例,与实时 trials 对象通信

如果这个简短的教程对你来说没有太大意义,这是正常的,但我希望提到当前代码库中可能实现的功能,并提供一些在 hyperopt 源码、单元测试和示例项目(如 hyperopt-convnet)中可以查找的术语。如果你需要帮助来掌握这部分代码,请给我发邮件或在 GitHub 上提交问题。