使用 HyperOpt 运行 Tune 实验#
本教程将介绍 HyperOpt,并在运行一个简单的 Ray Tune 实验。Tune 的搜索算法与 HyperOpt 集成,因此您可以无缝地扩展 Hyperopt 的优化过程,而不会牺牲性能。
HyperOpt 提供无梯度/导数的优化,能够处理目标景观上的噪声,包括进化、老虎机和贝叶斯优化算法。HyperOpt 内部支持连续、离散或两者的混合搜索空间。它还提供了一个函数库,用于测试优化算法并与其他基准进行比较。
在本例中,我们通过 HyperOptSearch 来最小化一个简单的目标函数,以简要演示 HyperOpt 与 Ray Tune 的用法。需要注意的是,尽管这里强调的是机器学习实验,但 Ray Tune 可以优化任何隐式或显式的目标。我们假设已安装 hyperopt==0.2.5 库。如需了解更多信息,请参阅 HyperOpt 网站。
我们包含了一个关于条件搜索空间的重要示例(将超参数之间的关系串联起来)。
背景信息
必要要求
pip install "ray[tune]" hyperopt==0.2.5
显示代码单元格内容
# install in a hidden cell
# !pip install "ray[tune]"
!pip install hyperopt==0.2.5
Requirement already satisfied: hyperopt==0.2.5 in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (0.2.5)
Requirement already satisfied: numpy in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (2.2.3)
Requirement already satisfied: scipy in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (1.15.2)
Requirement already satisfied: six in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (1.17.0)
Requirement already satisfied: networkx>=2.2 in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (3.4.2)
Requirement already satisfied: future in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (1.0.0)
Requirement already satisfied: tqdm in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (4.67.1)
Requirement already satisfied: cloudpickle in /opt/miniconda3/envs/hyperopt_example/lib/python3.11/site-packages (from hyperopt==0.2.5) (3.1.1)
点击下方查看本示例所需的所有导入。
显示代码单元格源代码
import time
import ray
from ray import tune
from ray.tune.search import ConcurrencyLimiter
from ray.tune.search.hyperopt import HyperOptSearch
from hyperopt import hp
让我们从定义一个简单的评估函数开始。我们人为地休眠一小段时间(0.1 秒)来模拟一个长时间运行的机器学习实验。此设置假定我们正在运行一个实验的多个 steps,并尝试调整两个超参数:width 和 height。
def evaluate(step, width, height):
time.sleep(0.1)
return (0.1 + width * step / 100) ** (-1) + height * 0.1
接下来,我们的 objective 函数接收一个 Tune config,在训练循环中评估实验的 score,并使用 tune.report 将 score 报告给 Tune。
def objective(config):
for step in range(config["steps"]):
score = evaluate(step, config["width"], config["height"])
tune.report({"iterations": step, "mean_loss": score})
ray.init(configure_logging=False)
在定义搜索算法时,我们可以选择提供一组我们认为特别有希望或信息量大的初始超参数,并将此信息作为 HyperOptSearch 对象的有用起点。
我们还使用 ConcurrencyLimiter 将最大并发任务数设置为 4。
initial_params = [
{"width": 1, "height": 2, "activation": "relu"},
{"width": 4, "height": 2, "activation": "tanh"},
]
algo = HyperOptSearch(points_to_evaluate=initial_params)
algo = ConcurrencyLimiter(algo, max_concurrent=4)
样本数是将被尝试的超参数组合的数量。此 Tune 运行设置为 1000 个样本。(如果您的机器上运行时间过长,可以减少此数量)。
num_samples = 1000
接下来,我们定义一个搜索空间。关键假设是最优超参数存在于这个空间中。但是,如果空间非常大,那么在短时间内找到这些超参数可能会很困难。
search_config = {
"steps": 100,
"width": tune.uniform(0, 20),
"height": tune.uniform(-100, 100),
"activation": tune.choice(["relu", "tanh"])
}
最后,我们运行实验,通过 algo,执行 num_samples 次,来 "min"(最小化)objective 的“mean_loss”。前一句完整地描述了我们要解决的搜索问题。考虑到这一点,请注意执行 tuner.fit() 的效率。
tuner = tune.Tuner(
objective,
tune_config=tune.TuneConfig(
metric="mean_loss",
mode="min",
search_alg=algo,
num_samples=num_samples,
),
param_space=search_config,
)
results = tuner.fit()
Tune 状态
| 当前时间 | 2025-02-18 13:14:59 |
| 运行中 | 00:00:36.03 |
| 内存 | 22.1/36.0 GiB |
系统信息
正在使用 FIFO 调度算法。逻辑资源使用情况:1.0/12 CPUs, 0/0 GPUs
试验状态
| 试验名称 | 状态 | 位置 | activation | height | steps | width | 损失 | 迭代 | 总时间 (秒) | iterations |
|---|---|---|---|---|---|---|---|---|---|---|
| objective_5b05c00a | 已终止 | 127.0.0.1:50205 | relu | 2 | 100 | 1 | 1.11743 | 100 | 10.335 | 99 |
| objective_b813f49d | 已终止 | 127.0.0.1:50207 | tanh | 2 | 100 | 4 | 0.446305 | 100 | 10.3299 | 99 |
| objective_6dadd2bd | 已终止 | 127.0.0.1:50212 | tanh | -40.9318 | 100 | 6.20615 | -3.93303 | 100 | 10.3213 | 99 |
| objective_9faffc0f | 已终止 | 127.0.0.1:50217 | tanh | 91.9688 | 100 | 9.25147 | 9.30488 | 100 | 10.353 | 99 |
| objective_7834e74c | 已终止 | 127.0.0.1:50266 | tanh | -17.9521 | 100 | 11.436 | -1.70766 | 100 | 10.3753 | 99 |
| objective_741253c7 | 已终止 | 127.0.0.1:50271 | tanh | 58.1279 | 100 | 0.737879 | 7.01689 | 100 | 10.3565 | 99 |
| objective_39682bcf | 已终止 | 127.0.0.1:50272 | tanh | -31.2589 | 100 | 4.89265 | -2.92361 | 100 | 10.3225 | 99 |
| objective_bfc7e150 | 已终止 | 127.0.0.1:50274 | tanh | -14.7877 | 100 | 7.36477 | -1.34347 | 100 | 10.3744 | 99 |
| objective_e1f1a193 | 已终止 | 127.0.0.1:50314 | tanh | 50.9579 | 100 | 17.4499 | 5.15334 | 100 | 10.3675 | 99 |
| objective_1192dd4e | 已终止 | 127.0.0.1:50316 | tanh | 66.5306 | 100 | 16.9549 | 6.71228 | 100 | 10.3478 | 99 |
以下是最小化已定义目标平均损失的超参数。
print("Best hyperparameters found were: ", results.get_best_result().config)
Best hyperparameters found were: {'steps': 100, 'width': 6.206149011253133, 'height': -40.93182668460948, 'activation': 'tanh'}
条件搜索空间#
有时我们可能希望构建一个更复杂的搜索空间,该空间具有与其他超参数的条件依赖关系。在这种情况下,我们将一个嵌套字典传递给 objective_two,它已从 objective 中进行了微调,以处理条件搜索空间。
def evaluation_fn(step, width, height, mult=1):
return (0.1 + width * step / 100) ** (-1) + height * 0.1 * mult
def objective_two(config):
width, height = config["width"], config["height"]
sub_dict = config["activation"]
mult = sub_dict.get("mult", 1)
for step in range(config["steps"]):
intermediate_score = evaluation_fn(step, width, height, mult)
tune.report({"iterations": step, "mean_loss": intermediate_score})
time.sleep(0.1)
conditional_space = {
"activation": hp.choice(
"activation",
[
{"activation": "relu", "mult": hp.uniform("mult", 1, 2)},
{"activation": "tanh"},
],
),
"width": hp.uniform("width", 0, 20),
"height": hp.uniform("height", -100, 100),
"steps": 100,
}
现在,我们定义一个基于 HyperOptSearch 并受 ConcurrencyLimiter 约束的搜索算法。当超参数搜索空间是条件的时,我们将其(conditional_space)传递给 HyperOptSearch。
algo = HyperOptSearch(space=conditional_space, metric="mean_loss", mode="min")
algo = ConcurrencyLimiter(algo, max_concurrent=4)
现在我们运行实验,这次使用一个空的 config,因为我们已经将 space 提供了给 HyperOptSearch search_alg。
tuner = tune.Tuner(
objective_two,
tune_config=tune.TuneConfig(
metric="mean_loss",
mode="min",
search_alg=algo,
num_samples=num_samples,
),
)
results = tuner.fit()
Tune 状态
| 当前时间 | 2025-02-18 13:15:34 |
| 运行中 | 00:00:34.71 |
| 内存 | 22.9/36.0 GiB |
系统信息
正在使用 FIFO 调度算法。逻辑资源使用情况:1.0/12 CPUs, 0/0 GPUs
试验状态
| 试验名称 | 状态 | 位置 | activation/activatio n | activation/mult | height | steps | width | 损失 | 迭代 | 总时间 (秒) | iterations |
|---|---|---|---|---|---|---|---|---|---|---|---|
| objective_two_13de5867 | 已终止 | 127.0.0.1:50350 | tanh | -35.0329 | 100 | 13.2254 | -3.42749 | 100 | 10.2102 | 99 | |
| objective_two_3100f5ee | 已终止 | 127.0.0.1:50355 | relu | 1.44584 | 76.2581 | 100 | 0.123165 | 15.5316 | 100 | 10.2683 | 99 |
| objective_two_6b4044aa | 已终止 | 127.0.0.1:50356 | relu | 1.67475 | 57.9612 | 100 | 19.4794 | 9.75866 | 100 | 10.2724 | 99 |
| objective_two_4aa1269b | 已终止 | 127.0.0.1:50357 | tanh | -9.95686 | 100 | 12.9749 | -0.918437 | 100 | 10.2373 | 99 | |
| objective_two_d8c42c9f | 已终止 | 127.0.0.1:50402 | tanh | -96.6184 | 100 | 8.03869 | -9.53774 | 100 | 10.2407 | 99 | |
| objective_two_de956d10 | 已终止 | 127.0.0.1:50404 | relu | 1.06986 | 9.64996 | 100 | 0.672962 | 2.3375 | 100 | 10.2427 | 99 |
| objective_two_0e2b2751 | 已终止 | 127.0.0.1:50413 | tanh | -82.9292 | 100 | 17.4889 | -8.2355 | 100 | 10.293 | 99 | |
| objective_two_dad93a03 | 已终止 | 127.0.0.1:50415 | relu | 1.85364 | -63.6309 | 100 | 16.6414 | -11.7345 | 100 | 10.285 | 99 |
| objective_two_e472727a | 已终止 | 127.0.0.1:50442 | relu | 1.2359 | -75.2253 | 100 | 7.49782 | -9.16415 | 100 | 10.2506 | 99 |
| objective_two_5d1eacff | 已终止 | 127.0.0.1:50449 | tanh | -20.158 | 100 | 6.18643 | -1.85514 | 100 | 10.2566 | 99 |
最后,我们再次展示了最小化上面目标函数得分所定义的平均损失的超参数。
print("Best hyperparameters found were: ", results.get_best_result().config)
Best hyperparameters found were: {'activation': {'activation': 'relu', 'mult': 1.8536380640438768}, 'height': -63.630920754630125, 'steps': 100, 'width': 16.641403933591928}