0x00 摘要
Facebook Hydra 允许开发人员通过编写和覆盖配置来简化 Python 应用程序(尤其是机器学习方面)的开发。开发人员可以借助Hydra,通过更改配置文件来更改产品的行为方式,而不是通过更改代码来适应新的用例。
本文通过几个示例为大家展示如何使用。
0x01 问题描述
在机器学习的开发中,经常会遇到各种调整参数,各种比较性能的情况。所以开发者经常会迷惑:
- 我现在这两个模型都使用的是什么参数来着?
- 我需要添加几个参数,又要修改代码,应该如何防止搞乱代码?
- 可以使用配置文件,但是如果希望新添加一个参数,则各个配置文件之间很难同步,我如何处理配置文件?
- 我今天跑了十几个模型,一不小心把他们的输出给冲掉了,我该怎么办?
- 十几个模型的log也容易被误删除,如何防止彼此冲突?
- 我在哪里?我在做什么?
这些问题,Facebook的开发人员早已遭遇过,深受其害的他们于是开发出来了 Hydra 来解决这些问题。
0x02 概述
Hydra提供了一种灵活的方法来开发和维护代码及配置,从而加快了机器学习研究等领域中复杂应用程序的开发。 它允许开发人员从命令行或配置文件“组合”应用程序的配置。这解决了在修改配置时可能出现的问题,例如:
- 维护配置的稍微不同的副本或添加逻辑以覆盖配置值。
- 可以在运行应用程序之前就组成和覆盖配置。
- 动态命令行选项卡完成功能可帮助开发人员发现复杂配置并减少错误。
- 可以在本地或远程启动应用程序,使用户可以利用更多的本地资源。
Hydra承诺的其他好处包括:
- 使为新用例和需求的项目添加功能变得更加容易,而无需重写大量代码。
- 减少了复杂应用程序中常见的一些样板代码,例如处理配置文件,配置日志记录和定义命令行标志。
下面我们通过几个简单例子给大家演示下如何使用。
0x03 使用
3.1 安装
项目地址位于:https://github.com/facebookresearch/hydra
安装方式如下:
代码语言:javascript复制pip install --upgrade hydra-core -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com
3.2 示例
3.2.1 示例代码
示例代码如下
代码语言:javascript复制import hydra
@hydra.main()
def app(cfg):
print(cfg.pretty())
print("The user is : " cfg.user)
if __name__ == "__main__":
app()
运行如下:
代码语言:javascript复制python3 test_hydra.py user=ua pwd=pa
输出如下:
代码语言:javascript复制Use OmegaConf.to_yaml(cfg)
category=UserWarning,
user: ua
pwd: pa
The user is : ua
3.2.2 简化参数处理
常见的一个机器学习程序中,是用如下代码来处理输入和各种参数。
代码语言:javascript复制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 就可以。
而且还可以通过配置文件来直接处理参数,比如:
代码语言:javascript复制@hydra.main(config_path="conf", config_name="config")
def my_app(cfg: DictConfig) -> None:
print(OmegaConf.to_yaml(cfg))
3.2.3 输出目录
人们在做研究时经常遇到的一个问题是如何保存输出。典型的解决方案是传入一个指定输出目录的命令行标志,但这很快会变得乏味。当你希望同时运行多项任务,并且必须为每个任务传递不同的输出目录时,这尤其令人恼火。
Hydra 通过为每次运行生成输出目录,并在运行代码之前更改当前工作目录来解决此问题。这样可以很好地将来自同一 sweep 的任务分组在一起,同时保持每个任务与其他任务的输出分离。
我们可以简单的来看看目录的变化,可以看到,在当前目录下生成了一个 outputs 目录。
其内部组织是按照时间来进行,把每次运行的输出,log 和 配置都归类在一起。
代码语言:javascript复制├── 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
3.2.4 配置所在
我们分别打开两个.hydra目录下的config.yaml文件看看。
可以看到,每次运行时候,对应的参数配置都保存在其中。这样极大的方便了用户的比对和分析。
代码语言:javascript复制$ 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
0x04 Multirun 处理组合情况
Multirun 是 Hydra 的一种功能,它可以多次运行你的函数,每次都组成一个不同的配置对象。这是一个自然的扩展,可以轻松地组合复杂的配置,并且非常方便地进行参数扫描,而无需编写冗长的脚本。
例如,对于两种参数,我们可以扫描所有 4 个组合,一个命令就是会完成所有组合的执行:
代码语言:javascript复制python test_hydra.py --multirun user=ua,ub pwd=pa,pb
得到输出如下:
代码语言:javascript复制[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
可以看到生成如下目录树,每个参数组合对应了一个目录。
代码语言:javascript复制├── 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
0x05 处理复杂情况
对于一般的机器学习运行和普通python程序,hydra是非常好用的,因为可以使用 装饰器 来直接作用于 python 函数。
但是如果遇到了复杂情况,比如spark-submit,我们该如何处理?因为 spark-submit 是没办法用 hydra 来装饰。
比如:
代码语言:javascript复制spark-submit cut_words.py
这样就hydra就没办法截取 spark 的输入,输出。
遇到这个情况,我是使用 python 文件内部 调用 linux命令行,然后在spark-submit之前就处理其参数,在 spark 运行时候 转发程序输出的办法来解决(如果哪位同学有更好的办法,可以告诉我,谢谢)。
5.1 Python subprocess
Python subprocess 允许你去创建一个新的进程让其执行另外的程序,并与它进行通信,获取标准的输入、标准输出、标准错误以及返回码等。
subprocess模块中定义了一个Popen类,通过它可以来创建进程,并与其进行复杂的交互。Popen 是 subprocess的核心,子进程的创建和管理都靠它处理。
构造函数:
代码语言:javascript复制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)
常用参数:
- args:shell命令,可以是字符串或者序列类型(如:list,元组)
- bufsize:缓冲区大小。当创建标准流的管道对象时使用,默认-1。 0:不使用缓冲区 1:表示行缓冲,仅当universal_newlines=True时可用,也就是文本模式 正数:表示缓冲区大小 负数:表示使用系统默认的缓冲区大小。
- stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
- preexec_fn:只在 Unix 平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
- shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。
- cwd:用于设置子进程的当前目录。
- env:用于指定子进程的环境变量。如果 env = None,子进程的环境变量将从父进程中继承。
5.2 具体例子
下面例子很简陋,不能直接运行,只是给大家演示下大致思路,还请根据具体情况做相关调整。
- 我们通过subprocess.Popen启动了spark;
- hydra 的输入 可以转换为 spark 和 python 的输入;
- 然后读取子进程的stdout;
- 逐次使用log.info来打印转发的stdout,这样spark的输出就被转发到了hydra的输出之中;
这样,spark的输出就可以被hydra捕获,从而整合到hydra log体系之中。
代码语言:javascript复制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()
5.3 流程示例
以下就是我采取办法的流程示例。
- Input 由 hydra 处理之后,由 python 父进程 转发给 spark 和 我们的python 商业逻辑;
- 具体spark 的输出,由 python 父进程转发给 Hydra logging;
具体如下图:
代码语言:javascript复制 Input Input
Hydra ---------- ------------------------v
| ^ |
| | |
| | |
------------------------- v
| | | | ------ -------------
| v ----------> | | Spark |
| | | |
| Parent Python Process | | Business Python |
| | | |
| <-----------^ | | |
| | | | | |
------------------------- ------ -------------
| | |
| | |
| | |
Hydra <---------< ------------------------
Logging Output
5.4 期待
现在 Hydra 统一保存 配置 到独立的配置文件之中。如果可以把某些输出也按照统一格式保存在配置文件中就更好了。这样我们就可以把这些配置文件统一处理,比较,图形化。直接把配置和输出结合起来,更加直观。
0x06 总结
这里只是简单给出了三个例子,Hydra还有众多的用法等待大家去探究。相信大家的很多痛点都可以用它来解决,赶紧试试吧。
0xFF 参考
机器学习项目配置太复杂怎么办?Facebook 开发了 Hydra 来帮你
Python 从subprocess运行的子进程中实时获取输出的例子