快速入门指南

什么是 HpBandSter?

HpBandSter (HyperBand 的增强版,STERoids 指增强) 实现了最近发表的用于优化机器学习算法超参数的方法。我们设计 HpBandSter 的初衷是使其能够从本地机器的顺序运行扩展到分布式系统的并行运行

其中一种实现的算法是 BOHB,它结合了贝叶斯优化和 HyperBand,可以有效地搜索性能良好的配置。您可以通过阅读我们发表在 ICML 2018 的论文来了解更多关于这种方法的信息

如何安装 HpBandSter

HpBandSter 可以通过 pip 在 python3 环境下安装

pip install hpbandster

如果您想基于代码进行开发,可以通过以下方式安装

git clone git@github.com:automl/HpBandSter.git
cd HpBandSter
python3 setup.py develop --user

注意

HpBandSter 仅支持 Python3!

基本组成部分

无论您是喜欢在本地机器上使用 HpBandSter,还是在集群上使用,基本设置始终相同。现在,让我们重点关注将优化器应用于新问题所需的最重要组成部分

实现 Worker
Worker 负责在给定预算下,使用单个配置评估给定的模型。

定义搜索空间

接下来,需要定义待优化的参数。HpBandSter 依赖于 ConfigSpace 包来实现此功能。
选择预算和迭代次数
为了获得良好的性能,HpBandSter 需要知道可用的有意义的预算。您还需要指定优化器执行的迭代次数。

1. 实现 Worker

Worker" 负责评估超参数设置并返回最小化的相关损失。通过从 基类 派生,编码新问题包括实现两个方法:__init__compute。前者允许在 Worker 启动时执行初始计算,例如加载数据集,而后者在优化过程中重复调用,评估给定配置并产生相关的损失。
下面的 Worker 演示了这个概念。它实现了一个简单的玩具问题,配置中有一个参数 x,我们试图最小化它。函数评估会受到高斯噪声的干扰,噪声会随着预算的增加而减小。
import numpy
import time

import ConfigSpace as CS
from hpbandster.core.worker import Worker


class MyWorker(Worker):

    def __init__(self, *args, sleep_interval=0, **kwargs):
        super().__init__(*args, **kwargs)

        self.sleep_interval = sleep_interval

    def compute(self, config, budget, **kwargs):
        """
        Simple example for a compute function
        The loss is just a the config + some noise (that decreases with the budget)

        For dramatization, the function can sleep for a given interval to emphasizes
        the speed ups achievable with parallel workers.

        Args:
            config: dictionary containing the sampled configurations by the optimizer
            budget: (float) amount of time/epochs/etc. the model can use to train

        Returns:
            dictionary with mandatory fields:
                'loss' (scalar)
                'info' (dict)
        """

        res = numpy.clip(config['x'] + numpy.random.randn()/budget, config['x']/2, 1.5*config['x'])
        time.sleep(self.sleep_interval)

        return({
                    'loss': float(res),  # this is the a mandatory field to run hyperband
                    'info': res  # can be used for any user-defined information - also mandatory
                })
    
    @staticmethod
    def get_configspace():
        config_space = CS.ConfigurationSpace()

2. 搜索空间定义

每个问题都需要完整的搜索空间描述。在 HpBandSter 中,一个 ConfigurationSpace 对象定义了所有超参数、它们的范围以及它们之间潜在的依赖关系。在我们的玩具示例中,搜索空间包含一个介于零和一之间的连续参数 x。为了方便起见,我们将配置空间定义作为静态方法附加到 Worker 上。这样,Worker 的计算函数及其参数就可以整齐地组合在一起。

class MyWorker(Worker):
    @staticmethod
    def get_configspace():
        config_space = CS.ConfigurationSpace()
        config_space.add_hyperparameter(CS.UniformFloatHyperparameter('x', lower=0, upper=1))
        return(config_space)

注意

我们也支持整数和分类超参数。为了表达依赖关系,ConfigSpace 包还可以表达参数之间的条件和禁止关系。有关更多示例,请参阅 ConfigSpace 的文档,或者请查看高级示例

3. 合理的预算和迭代次数

为了利用较低保真度的近似值,即低于 max_budget 的预算,这些 较低精度 的评估必须有意义。由于这些预算可能意味着非常不同的事物(神经网络训练的 epoch 数、训练模型的数据点数或交叉验证折叠数等等),因此必须由用户指定。这是通过所有优化器的两个参数 min_budgetmax_budget 来完成的。为了更好地加速,较低的预算应该尽可能小,同时仍然具有信息量。所谓信息量,是指其性能是更高预算下损失的 尚可的 指标。对于一般情况,很难更具体。这两个预算取决于具体问题,需要一些领域知识。
迭代次数通常是一个更容易选择的参数。根据优化器的不同,一次迭代需要的计算预算相当于在 max_budget 上进行几次函数评估。一般来说,迭代次数越多越好,当多个 Worker 并行运行时,事情会变得更复杂。目前,迭代次数仅控制评估的配置数量。

第一个玩具示例

现在让我们在几种不同的设置下使用上面的 Worker 及其搜索空间。具体来说,我们将运行
  1. 本地顺序运行
  2. 本地并行运行(基于线程)
  3. 本地并行运行(基于进程)
  4. 在集群环境中分布式运行
每个示例都展示了如何在不同的环境中设置 HpBandSter,并突出其具体细节。每个计算环境都略有不同,但应该很容易从其中一个示例开始并根据任何特定需求进行调整。第一个示例慢慢介绍了任何 HpBandSter 运行的主要工作流程。接下来的示例通过包含更多功能逐渐增加复杂性。

1. 本地顺序运行

现在我们准备看第一个真实的示例来演示如何使用 HpBandSter。每次运行都包含相同的 5 个基本步骤,我们现在将介绍这些步骤。

步骤 1:启动 Nameserver

为了启动 Worker 和优化器之间的通信,HpBandSter 需要一个 Nameserver。这是一个小型服务,用于跟踪所有正在运行的进程及其 IP 地址和端口。它是 HpBandSter 从 Pyro4 继承的一个构建块。在第一个示例中,我们将使用回环接口和 IP 127.0.0.1 运行它。使用参数 port=None 将使其使用默认端口 9090。run_id 用于识别单个运行,也需要提供给所有其他组件(见下文)。现在,我们将其固定为 example1
NS = hpns.NameServer(run_id='example1', host='127.0.0.1', port=None)
NS.start()

步骤 2:启动 Worker

Worker 实现了要优化的实际问题。通过从 基本 Worker 派生并实现 compute 方法,它可以轻松地实例化,包含您的特定 __init__ 所需的所有参数以及基类的附加参数。最基本的是 Nameserver 的位置和 run_id
w = MyWorker(sleep_interval = 0, nameserver='127.0.0.1',run_id='example1')
w.run(background=True)

步骤 3:运行 Optimizer

优化器决定评估哪些配置以及如何分配预算。除了随机搜索HyperBand之外,还有BOHB,这是我们自己结合 Hyperband 和贝叶斯优化的方法,我们将在此处使用它。查看可用优化器列表了解更多信息。
至少,我们必须提供搜索空间的描述、run_id、Nameserver 和预算。当调用 run 方法时,优化开始,迭代次数是唯一的强制参数。
bohb = BOHB(  configspace = w.get_configspace(),
              run_id = 'example1', nameserver='127.0.0.1',
              min_budget=args.min_budget, max_budget=args.max_budget
           )
res = bohb.run(n_iterations=args.n_iterations)

步骤 4:停止所有服务

运行完成后,需要关闭上面启动的服务。这确保 Worker、Nameserver 和 Master 都正常退出,并且之后没有(守护)线程继续运行。特别是,我们关闭优化器(它会关闭所有 Worker)和 Nameserver。
bohb.shutdown(shutdown_workers=True)
NS.shutdown()

步骤 5:结果分析

运行完成后,人们可能会对各种信息感兴趣。HpBandSter 提供对所有已评估配置的完全访问,包括计时信息以及失败运行的潜在错误消息。在第一个示例中,我们简单地查找最佳配置(称为 incumbent),计算配置和评估的数量,以及花费的总预算。有关更多详细信息,请参阅其他一些示例以及 Result 类的文档。
id2config = res.get_id2config_mapping()
incumbent = res.get_incumbent_id()

print('Best found configuration:', id2config[incumbent]['config'])
print('A total of %i unique configurations where sampled.' % len(id2config.keys()))
print('A total of %i runs where executed.' % len(res.get_all_runs()))
print('Total budget corresponds to %.1f full function evaluations.'%(sum([r.budget for r in res.get_all_runs()])/args.max_budget))
此示例的完整源代码可以在此处找到

2. 使用线程的本地并行运行

现在让我们扩展这个示例,启动多个 Worker,每个 Worker 在一个单独的线程中。如果各个 Worker 能够避开 Python 的全局解释器锁,这是一种利用多核 CPU 系统的好模式。例如,许多 scikit learn 算法将繁重的计算外包给某些 C 模块,即使是线程化的,也能使其真正并行运行。
下面,我们可以实例化指定数量的 Worker。为了强调效果,我们引入了一个一秒的 sleep_interval,这使得每次函数评估都需要花费一些时间。请注意额外的 id 参数,它有助于区分各个 Worker。这是必需的,因为此处所有线程都使用其进程 ID,它们是相同的。
# Step 2: Start the workers
# Now we can instantiate the specified number of workers. To emphasize the effect,
# we introduce a sleep_interval of one second, which makes every function evaluation
# take a bit of time. Note the additional id argument that helps separating the
# individual workers. This is necessary because every worker uses its processes
启动优化器时,我们可以向 run 方法添加 min_n_workers 参数,以使优化器等待所有 Worker 启动。这不是强制性的,Worker 可以随时添加,但如果运行的 timing 很重要,可以使用此参数在启动时同步所有 Worker。
# at any time, but if the timing of the run is essential, this can be used to
源代码可以在此处找到。尝试通过更改命令行参数 –n_worker 来使用不同数量的 Worker 运行它。

3. 使用不同进程的本地并行运行

在我们转向分布式系统之前,我们首先将我们的玩具示例扩展到在不同的进程中运行。为此,我们添加 –worker 标志
parser.add_argument('--max_budget',   type=float, help='Maximum budget used during the optimization.',    default=243)
这将允许我们为专用的 Worker 运行相同的脚本。这些 Worker 只需实例化 Worker 类并调用其 run 方法,但这一次 Worker 在前台运行。在处理完所有配置并接收到 Master 的关闭信号后,Worker 就会简单地退出。



if args.worker:
您可以在此处下载源代码。尝试在三个不同的 shell 中运行此脚本,其中两次带有 –worker 标志。为了查看发生了什么,此脚本的日志级别设置为 INFO,因此会显示来自优化器和 Worker 的消息。

4. 在共享文件系统的集群上分布式运行

示例 3 已经接近分布式环境的设置。唯一缺少的是提供一个唯一的 run id,查找主机名并将 Nameserver 信息分发给所有进程。到目前为止,run id 总是硬编码的,并且 Nameserver 在默认端口上运行在 localhost(127.0.0.1,这也是主机名)上。现在我们必须告诉所有进程要使用哪个网络接口卡 (NIC) 以及 Nameserver 位于何处。为此,我们引入了三个新的命令行参数
parser.add_argument('--run_id', type=str, help='A unique run id for this optimization run. An easy option is to use the job id of the clusters scheduler.')
parser.add_argument('--nic_name',type=str, help='Which network interface to use for communication.')
parser.add_argument('--shared_directory',type=str, help='A directory that is accessible for all processes, e.g. a NFS share.')
前两个是自解释的,我们将使用一个共享目录来将 Nameserver 信息分发给每个 Worker。

注意

这不是分发此信息的唯一方法,但根据我们的经验,几乎所有集群都提供可由每个计算节点访问的共享文件系统。因此,我们为此场景实现了一个简单的解决方案。如果这不涵盖您的用例,您必须找到另一种方式将 Nameserver 信息分发给所有 Worker。此时,一个选项可能是启动一个静态 Nameserver,例如在集群的提交节点上。这样,您可以将信息硬编码到脚本中。

为了找到有效的主机名,我们可以使用便利函数 nic_to_host,它会查找给定 NIC 的有效主机名。
host = hpns.nic_name_to_host(args.nic_name)
创建 Nameserver 时,我们可以提供 working_directory 参数,使其在启动时存储其主机名和端口。这两个值也由 start 方法返回,以便我们可以直接在 Master 中使用它们。
NS = hpns.NameServer(run_id=args.run_id, host=host, port=0, working_directory=args.shared_directory)
ns_host, ns_port = NS.start()
然后 Worker 只需从磁盘加载即可检索该信息
if args.worker:
	time.sleep(5)	# short artificial delay to make sure the nameserver is already running
	w = MyWorker(sleep_interval = 0.5,run_id=args.run_id, host=host)
	w.load_nameserver_credentials(working_directory=args.shared_directory)
	w.run(background=False)
	exit(0)
对于 Master,我们通常可以在后台运行一个 Worker,因为大多数优化器的开销很小。
w = MyWorker(sleep_interval = 0.5,run_id=args.run_id, host=host, nameserver=ns_host, nameserver_port=ns_port)
w.run(background=True)
我们还向优化器提供 hostnameservernameserver_port 参数。运行完成后,我们通常不想打印出任何信息,而是将结果存储起来供以后分析。Pickle 优化器运行返回的对象是一种非常简单的方法。
with open(os.path.join(args.shared_directory, 'results.pkl'), 'wb') as fh:
	pickle.dump(res, fh)
完整示例可以在此处找到。在那里您还可以找到一个示例 shell 脚本,用于在运行 Sun Grid Engine 的集群上提交程序。