关于机器学习:或许是东半球最好用的超参数优化框架2Optuna-进阶使用

58次阅读

共计 7796 个字符,预计需要花费 20 分钟才能阅读完成。

在上一篇文章中,咱们简略介绍了 Optuna 的入门应用办法,包含其简洁的 API 设计与丰盛的存储 / 剖析 / 可视化套件。而实际上 OPtuna 的性能远不止此。作为次要面向深度学习超参数调优开发的框架,Optuna 在实现之初就思考到了大型模型参数调优的各种理论状况,并逐个针对它们设计了解决方案。

分布式优化

一提到分布式算法,咱们想到的可能是麻烦的 debug 过程和分布式实现过程中各种线程锁之类的问题。你可能会好奇,带有 GIL 的 python 超参数优化库是怎么实现分布式优化的?实际上,通过抉择不同的共享参数的形式,Optuna 躲避了这一问题(optuna 在外部应用了 joblib, 然而依然受到 GIL 的限度)。在 Optuna 中,用户不是在单个脚本内派生出不同的线程,而是通过针对同一个 study 启动不同的优化过程来实现分布式优化的。

在一个分布式的 study 优化过程中,用户首先创立一个空 study:

$ optuna create-study --study-name "distributed-example" --storage "sqlite:///example.db"

\[I 2018\-10-31 18:21:57,885\] A new study created with name: distributed-example

这个命令会在当前目录下生成一个数据库文件

$ ls
example.db foo.py
(doc-optuna)

该 study 将会用于前期的参数和历史记录存储。一个中心化的参数存储是有必要的,因为在超参数优化过程中,一个后续的试验(trial)的参数采样范畴会受到后面的参数采样历史记录影响。而因为 Optuna 透过数据库接口来读写这些数据,因而多个 client 之间不会造成烦扰,用户只需在不同的过程中运行这个 study 即可:比方在不同的终端窗口中运行优化脚本。

如果优化脚本如下:

import optuna

def objective(trial):
    x = trial.suggest_uniform('x', -10, 10)
    return (x - 2) ** 2

if __name__ == '__main__':
    study = optuna.load_study(study_name='distributed-example', storage='sqlite:///example.db')
    study.optimize(objective, n_trials=100)

(留神,这里的 n_trials 不是指 对于该 study 总共会运行 100 次试验,而是每一个独自的优化过程都会跑 100 次试验,n 个过程下的总试验次数是 n x 100)

思考两个过程的优化,咱们能够在同一个目录下的两个终端窗口中运行:

$ python foo.py
[I 2018-10-31 18:46:44,308] Finished a trial resulted in value: 1.1097007755908204. Current best value is 0.00020881104123229936 with parameters: {'x': 2.014450295541348}.
[I 2018-10-31 18:46:44,361] Finished a trial resulted in value: 0.5186699439824186. Current best value is 0.00020881104123229936 with parameters: {'x': 2.014450295541348}.
...

等优化实现当前开启第三个脚本查看总的 trial 数,能够看到总试验次数是 200:

>>> len(study.trials)
200

此时,如果关上数据库,咱们会发现,试验和试验所用到的参数是离开存储的:红色是试验记录,蓝色是参数采样记录。通过分隔着两种记录,不同的过程从同一个空间中采样变得更加容易。

这种分布式优化的方法看起来仿佛有些“手动”,然而却是可扩展性最佳的做法之一。因为,你甚至能够将存储优化历史记录和参数采样的 study 设置成近程数据库存储。这样的话,即便要在不同的机器上同时对同一个模型进行调参也十分不便:拷贝一份模型代码,而后设置好数据库,启动优化过程即可。

在跑优化时还有一点须要留神:在大型的分布式超参优化过程中,最好不要用 SQLite 这种数据库,应用成熟的 MySQL 或者 PostgreSQL 是牢靠的抉择。

剪枝算法

在简单模型的优化过程中,即便在训练的晚期咱们也经常能看到有一些参数组合是显著有效的,此时如果对该参数组合持续进行优化毫无意义,对其进行提前终止能节俭大量的运算工夫,而各种剪枝算法也应运而生。用户当然能够自行实现这些算法,然而这通常要求对现存的模型代码进行大量的批改,如果剪枝的规范波及优化过程历史记录比拟的话(比方对中位数以下进行剪枝),实现起来则更加麻烦。

Optuna 为了解决这一问题,通过提供 Pruner 接口,最大水平地将剪枝的逻辑和模型逻辑进行理解耦。思考一个迭代式训练的过程,在每一步之后,你只需调用 report 和 should_prune 函数即可进行剪枝。其中 report 负责监测迭代过程中的指标函数两头值,而 should_prune 则进行判断,抉择是否触发一个异样,让 Optuna 终止该试验。

上面是一个来自官网文档的剪枝例子:

import sklearn.datasets
import sklearn.linear_model
import sklearn.model_selection

import optuna

def objective(trial):
    iris = sklearn.datasets.load_iris()
    classes = list(set(iris.target))
    train_x, valid_x, train_y, valid_y = \
    sklearn.model_selection.train_test_split(iris.data, iris.target, test_size=0.25, random_state=0)

    alpha = trial.suggest_loguniform('alpha', 1e-5, 1e-1)
    clf = sklearn.linear_model.SGDClassifier(alpha=alpha)

    for step in range(100):
        clf.partial_fit(train_x, train_y, classes=classes)

        # Report intermediate objective value.
        intermediate_value = 1.0 - clf.score(valid_x, valid_y)
        trial.report(intermediate_value, step)

    # Handle pruning based on the intermediate value.
    if trial.should_prune():
        raise optuna.TrialPruned()

    return 1.0 - clf.score(valid_x, valid_y)

# Set up the median stopping rule as the pruning condition.
study = optuna.create_study(pruner=optuna.pruners.MedianPruner())
study.optimize(objective, n_trials=20)

不难看出,整个剪枝算法的实现从指标函数中被抽离进去了,通过一个 Pruner 实例,每个 trial 只需发送两头指标函数值进行判断即可实现剪枝,原有的训练代码简直无需扭转。

该 study 的试验记录输入如下:

$ python pr.py
[I 2020-07-16 13:25:10,428] Trial 0 finished with value: 0.10526315789473684 and parameters: {'alpha': 9.761488546865748e-05}. Best is trial 0 with value: 0.10526315789473684.
[I 2020-07-16 13:25:10,595] Trial 1 finished with value: 0.02631578947368418 and parameters: {'alpha': 3.918322442650925e-05}. Best is trial 1 with value: 0.02631578947368418.
[I 2020-07-16 13:25:10,763] Trial 2 finished with value: 0.2894736842105263 and parameters: {'alpha': 3.561987583888278e-05}. Best is trial 1 with value: 0.02631578947368418.
[I 2020-07-16 13:25:10,942] Trial 3 finished with value: 0.052631578947368474 and parameters: {'alpha': 0.00569807382728555}. Best is trial 1 with value: 0.02631578947368418.
[I 2020-07-16 13:25:11,112] Trial 4 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.00019141260605620358}. Best is trial 1 with value: 0.02631578947368418.
[I 2020-07-16 13:25:11,119] Trial 5 pruned.
[I 2020-07-16 13:25:11,126] Trial 6 pruned.
[I 2020-07-16 13:25:11,316] Trial 7 finished with value: 0.1578947368421053 and parameters: {'alpha': 9.844155535043529e-05}. Best is trial 1 with value: 0.02631578947368418.
[I 2020-07-16 13:25:11,329] Trial 8 pruned.
[I 2020-07-16 13:25:11,334] Trial 9 pruned.
[I 2020-07-16 13:25:11,341] Trial 10 pruned.
[I 2020-07-16 13:25:11,356] Trial 11 pruned.
[I 2020-07-16 13:25:11,546] Trial 12 finished with value: 0.23684210526315785 and parameters: {'alpha': 0.0033635198424680182}. Best is trial 1 with value: 0.02631578947368418.
[I 2020-07-16 13:25:11,731] Trial 13 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.02974537607602637}. 
...

这个例子中采纳的是中位数剪枝,实际上 Optuna 已内置了多种能满足个别需要的 Pruner,比方百分位数 pruner,hyperband pruner 和针对个性变量监测的 Threshold pruner 等. 对于它们的具体细节请参考 https://zh-cn.optuna.org/reference/pruners.html

集成模块

在有些高度形象的机器学习库中,比方 keras 或者 Pytorch Ignite,训练过程自身被形象成了一个函数。此时像上例那样实现剪枝是不可能的。所幸大多数的库在这些办法中都提供了一个钩子接口,能够传入一个检测函数。Optuna 也为这类框架提供了集成的剪枝模块,比方 optuna.integration.PyTorchIgnitePruningHandler 和 optuna.integration.KerasPruningCallback . 这里有一个 Pruning 集成模块的残缺列表介绍。

上面咱们通过一个 PyTorch Ignite 的例子来看一下集成模块是如何在代码中发挥作用的。

def objective(trial):
    # Create a convolutional neural network.
    model = Net(trial)

    device = "cpu"
    if torch.cuda.is_available():
    device = "cuda"
    model.cuda(device)

    optimizer = Adam(model.parameters())
    trainer = create_supervised_trainer(model, optimizer, F.nll_loss, device=device)
    evaluator = create_supervised_evaluator(model, metrics={"accuracy": Accuracy()}, device=device)

    # Register a pruning handler to the evaluator.
    pruning_handler = optuna.integration.PyTorchIgnitePruningHandler(trial, "accuracy", trainer)
    evaluator.add_event_handler(Events.COMPLETED, pruning_handler)

    # Load MNIST dataset.
    train_loader, val_loader = get_data_loaders(TRAIN_BATCH_SIZE, VAL_BATCH_SIZE)

    @trainer.on(Events.EPOCH_COMPLETED)
    def log_results(engine):
    evaluator.run(val_loader)
    validation_acc = evaluator.state.metrics["accuracy"]
    print("Epoch: {} Validation accuracy: {:.2f}".format(engine.state.epoch, validation_acc))

    trainer.run(train_loader, max_epochs=EPOCHS)

    evaluator.run(val_loader)
    return evaluator.state.metrics["accuracy"]

(咱们省略了模型定义等局部)

能够看到,上例中 trainer.run 间接实现了训练过程,咱们并不能往其中插入一个 should_prune(),然而透过一个 PyTorchIgnitePruningHandler 实例,咱们实现了向 trainer 中注入一个 pruner,之后 evaluator 承受这个 pruner 作为参数,在每一个 engine 的 run 终止时 (Events.COMPLETED) 调用这个 pruning_handler.

不过要留神,并不是每一个机器学习库的集成模块调用形式都是雷同的。因为各个库裸露的 API 或者 API 格调不一样,相应的集成模块的实现和应用形式也不尽相同。比方在下面的 PyTorch Ignite 例子中,实际上你只需将 trial 一并传递到 handler 函数中即可,原本是能够通过往 evaluator.add_event_handler 的 handler 参数背地增加额定参数来实现的,然而为了代码的简洁,Optuna 采纳的是往 callable 类中提前传递 trial 作为类属性绕开这个办法,其外围和之前最简略的 pruning 例子是完全相同的:

def __init__(self, trial, metric, trainer):
    # type: (Trial, str, Engine) -> None

    _imports.check()

    self._trial = trial
    self._metric = metric
    self._trainer = trainer

def __call__(self, engine):
    # type: (Engine) -> None

    score = engine.state.metrics[self._metric]
    self._trial.report(score, self._trainer.state.epoch)
    if self._trial.should_prune():
    message = "Trial was pruned at {} epoch.".format(self._trainer.state.epoch)
    raise optuna.TrialPruned(message)

(PyTorchIgnitePruningHandler 类外部实现)

能够看到,PyTorchIgnitePruningHandler 只是给 should_prune 做一个包装而已。所以,如果你发现你罕用的库目前没有插件然而提供了对应的 callback 接口的话,Optuna 也欢送你作为开发者来提 PR!为一个有 callback 的库实现 pruner 并不是一件如许麻烦的事件,却能最大化升高现存代码的批改量。你能够参考 https://github.com/optuna/optuna/tree/master/examples#examples-with-ml-libraries 来找到你罕用的机器学习库的对应集成插件列表和利用案例。

其余可定制化的局部

顺便一提,Optuna 的可定制性十分强。比方,采样算法通常会影响参数空间中的搜寻效率,如果你发现目前的 sampler 满足不了你的需要的话,你能够自行定制对应的 uder defined sampler. Optuna 提供了一个 BaseSampler, 只有你的新 sampler 继承了它并且提供了几个固定的采样接口 (sampler_relative 和 sample_independent 等),就能够在优化中应用任何你想要的参数采样算法而无需改变指标函数代码了。对于 Sampler 工作原理的具体介绍,请看 https://zh-cn.optuna.org/reference/samplers.html

相似地,Optuna 还提供了一些更加玲珑的定制化办法,比方在 study 和 trial 上都能够应用的 set_user_attr. 这些属性能够让你更便捷地内部批改优化运行时的逻辑而无需改变代码。

Summary

如你所见,Optuna 专为超参数优化而生:不便的指标函数定义,多样化的采样算法和极其便捷的分布式优化让它有愧为大规模调参的首选。目前 Optuna 正处于疾速的开发中,但因为 1.0 早已公布,其大多数接口都保障了稳固,而许多新个性也将随着 Optuna2.0 的公布一起到来。如果你感觉目前应用的超参数优化形式并不是十分现实的话,无妨尝试一下 Optuna。以及,如果你有趣味进入社区参加 Optuna 的开发和保护工作的话,请查看代码标准。

正文完
 0