Facebook Hydra 允许开发人员通过编写和覆盖配置来简化 Python 应用程序(尤其是机器学习方面)的开发。开发人员可以借助Hydra,通过更改配置文件来更改产品的行为方式,而不是通过更改代码来适应新的用例。
本文通过几个示例为大家展示如何使用。
在机器学习的开发中,经常会遇到各种调整参数,各种比较性能的情况。所以开发者经常会迷惑:
这些问题,Facebook的开发人员早已遭遇过,深受其害的他们于是开发出来了 Hydra 来解决这些问题。
Hydra提供了一种灵活的方法来开发和维护代码及配置,从而加快了机器学习研究等领域中复杂应用程序的开发。 它允许开发人员从命令行或配置文件“组合”应用程序的配置。这解决了在修改配置时可能出现的问题,例如:
Hydra承诺的其他好处包括:
下面我们通过几个简单例子给大家演示下如何使用。
项目地址位于:https://github.com/facebookresearch/hydra
安装方式如下:
pip install --upgrade hydra-core -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com
示例代码如下
import hydra
@hydra.main()
def app(cfg):
print(cfg.pretty())
print("The user is : " + cfg.user)
if __name__ == "__main__":
app()
运行如下:
python3 test_hydra.py +user=ua +pwd=pa
输出如下:
Use OmegaConf.to_yaml(cfg)
category=UserWarning,
user: ua
pwd: pa
The user is : ua
常见的一个机器学习程序中,是用如下代码来处理输入和各种参数。
parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
parser.add_argument('--batch-size', type=int, default=64, metavar='N',
help='input batch size for training (default: 64)')
parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
help='input batch size for testing (default: 1000)')
parser.add_argument('--epochs', type=int, default=10, metavar='N',
help='number of epochs to train (default: 10)')
parser.add_argument('--lr', type=float, default=0.01, metavar='LR',
help='learning rate (default: 0.01)')
通过示例代码我们可以看出来,在 hydra 之中,我们直接使用 cfg.user 就可以。
而且还可以通过配置文件来直接处理参数,比如:
@hydra.main(config_path="conf", config_name="config")
def my_app(cfg: DictConfig) -> None:
print(OmegaConf.to_yaml(cfg))
人们在做研究时经常遇到的一个问题是如何保存输出。典型的解决方案是传入一个指定输出目录的命令行标志,但这很快会变得乏味。当你希望同时运行多项任务,并且必须为每个任务传递不同的输出目录时,这尤其令人恼火。
Hydra 通过为每次运行生成输出目录,并在运行代码之前更改当前工作目录来解决此问题。这样可以很好地将来自同一 sweep 的任务分组在一起,同时保持每个任务与其他任务的输出分离。
我们可以简单的来看看目录的变化,可以看到,在当前目录下生成了一个 outputs 目录。
其内部组织是按照时间来进行,把每次运行的输出,log 和 配置都归类在一起。
├── outputs
│ └── 2021-03-21
│ ├── 11-52-35
│ │ ├── .hydra
│ │ │ ├── config.yaml
│ │ │ ├── hydra.yaml
│ │ │ └── overrides.yaml
│ │ └── test_hydra.log
│ └── 11-57-55
│ ├── .hydra
│ │ ├── config.yaml
│ │ ├── hydra.yaml
│ │ └── overrides.yaml
│ └── test_hydra.log
├── test_hydra.py
我们分别打开两个.hydra目录下的config.yaml文件看看。
可以看到,每次运行时候,对应的参数配置都保存在其中。这样极大的方便了用户的比对和分析。
$ cat outputs/2021-03-21/11-52-35/.hydra/config.yaml
user: ua
pwd: pa
$ cat outputs/2021-03-21/11-57-55/.hydra/config.yaml
user: ub
pwd: pb
Multirun 是 Hydra 的一种功能,它可以多次运行你的函数,每次都组成一个不同的配置对象。这是一个自然的扩展,可以轻松地组合复杂的配置,并且非常方便地进行参数扫描,而无需编写冗长的脚本。
例如,对于两种参数,我们可以扫描所有 4 个组合,一个命令就是会完成所有组合的执行:
python test_hydra.py --multirun user=ua,ub pwd=pa,pb
得到输出如下:
[2021-03-27 11:57:54,435][HYDRA] Launching 4 jobs locally
[2021-03-27 11:57:54,435][HYDRA] #0 : +user=ua +pwd=pa
user: ua
pwd: pa
[2021-03-27 11:57:54,723][HYDRA] #1 : +user=ua +pwd=pb
user: ua
pwd: pb
[2021-03-27 11:57:54,992][HYDRA] #2 : +user=ub +pwd=pa
user: ub
pwd: pa
[2021-03-27 11:57:55,248][HYDRA] #3 : +user=ub +pwd=pb
user: ub
pwd: pb
可以看到生成如下目录树,每个参数组合对应了一个目录。
├── multirun
│ └── 2021-03-27
│ └── 11-57-53
│ ├── 0
│ │ ├── .hydra
│ │ │ ├── config.yaml
│ │ │ ├── hydra.yaml
│ │ │ └── overrides.yaml
│ │ └── test_hydra.log
│ ├── 1
│ │ ├── .hydra
│ │ │ ├── config.yaml
│ │ │ ├── hydra.yaml
│ │ │ └── overrides.yaml
│ │ └── test_hydra.log
│ ├── 2
│ │ ├── .hydra
│ │ │ ├── config.yaml
│ │ │ ├── hydra.yaml
│ │ │ └── overrides.yaml
│ │ └── test_hydra.log
│ ├── 3
│ │ ├── .hydra
│ │ │ ├── config.yaml
│ │ │ ├── hydra.yaml
│ │ │ └── overrides.yaml
│ │ └── test_hydra.log
│ └── multirun.yaml
对于一般的机器学习运行和普通python程序,hydra是非常好用的,因为可以使用 装饰器 来直接作用于 python 函数。
但是如果遇到了复杂情况,比如spark-submit,我们该如何处理?因为 spark-submit 是没办法用 hydra 来装饰。
比如:
spark-submit cut_words.py
这样就hydra就没办法截取 spark 的输入,输出。
遇到这个情况,我是使用 python 文件内部 调用 linux命令行,然后在spark-submit之前就处理其参数,在 spark 运行时候 转发程序输出的办法来解决(如果哪位同学有更好的办法,可以告诉我,谢谢)。
Python subprocess 允许你去创建一个新的进程让其执行另外的程序,并与它进行通信,获取标准的输入、标准输出、标准错误以及返回码等。
subprocess模块中定义了一个Popen类,通过它可以来创建进程,并与其进行复杂的交互。Popen 是 subprocess的核心,子进程的创建和管理都靠它处理。
构造函数:
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0,restore_signals=True, start_new_session=False, pass_fds=(),*, encoding=None, errors=None)
常用参数:
下面例子很简陋,不能直接运行,只是给大家演示下大致思路,还请根据具体情况做相关调整。
这样,spark的输出就可以被hydra捕获,从而整合到hydra log体系之中。
import shlex
import subprocess
import hydra
import logging
log = logging.getLogger(__name__)
@hydra.main()
def app(cfg):
# 可以在这里事先处理参数,被hydra处理之后,也成为 spark 和 python 的输入,进行处理
shell_cmd = 'spark-submit cut_words.py' + cfg.xxxxxx # 假如cut_words有参数
cmd = shlex.split(shell_cmd)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while p.poll() is None:
line = p.stdout.readline()
line = line.strip()
if line:
log.info('Subprogram output: [{}]'.format(line))
if p.returncode == 0:
log.info('Subprogram success')
else:
log.info('Subprogram failed')
if __name__ == '__main__':
app()
以下就是我采取办法的流程示例。
具体如下图:
Input Input
Hydra +----------+ +------------------------v
| ^ |
| | |
| | |
+-------------------------+ v
| | | | +------+-------------+
| v +----------> | | Spark |
| | | |
| Parent Python Process | | Business Python |
| | | |
| +<-----------^ | | |
| | | | | |
+-------------------------+ +------+-------------+
| | |
| | |
| | |
Hydra <---------<+ +------------------------+
Logging Output
现在 Hydra 统一保存 配置 到独立的配置文件之中。如果可以把某些输出也按照统一格式保存在配置文件中就更好了。这样我们就可以把这些配置文件统一处理,比较,图形化。直接把配置和输出结合起来,更加直观。
这里只是简单给出了三个例子,Hydra还有众多的用法等待大家去探究。相信大家的很多痛点都可以用它来解决,赶紧试试吧。