Python——Hydra库的使用


整体说明

  • Hydra 是一个开源的 Python 框架 ,旨在简化复杂应用程序的配置管理
  • Hydra 的核心功能是能够通过组合动态创建分层配置 ,并且可以通过配置文件命令行轻松覆盖这些配置
  • Hydra 的名字来源于神话中的九头蛇(Hydra) ,象征着它能够轻松地使用不同配置运行多个相似的作业(即 Multirun 功能),这在机器学习和科学实验中尤其有用
  • Hydra 的主要特点总结如下
    • 分层配置 (Hierarchical Configuration): 配置可以从多个独立的配置文件组合而成
    • 命令行覆盖 (Command-Line Overrides): 能够通过命令行参数轻松修改配置的任何部分
    • 多任务运行 (Multirun): 使用一个命令就能运行多次实验,每次实验使用不同的配置组合
    • 配置快照 (Configuration Snapshots): 自动保存每次运行的完整配置,确保结果的可复现性
    • 工作目录管理 (Working Directory Management): 每次运行都会在 outputs/multirun/ 目录下创建一个以日期和时间命名的新目录,将运行结果和日志隔离
  • Hydra 常常和 omegaconf 包一起使用

Hydra 安装

  • 通过 pip 安装 hydra-core

    1
    pip install hydra-core --upgrade
    • 依赖的 omegaconf 包会自动安装

常用示例(必会)

  • 文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    tree
    .
    ├── config
    │   ├── color
    │   │   ├── blue.yaml
    │   │   └── green.yaml
    │   ├── config.yaml
    │   ├── config2.yaml
    │   └── person
    │   ├── alice.yaml
    │   └── bob.yaml
    └── hydra_demo.py
  • ./config/color/blue.yaml文件内容

    1
    2
    favorite_color: blue
    time: 10
  • ./config/color/green.yaml文件内容

    1
    favorite_color: green
  • ./config/person/alice.yaml文件内容

    1
    2
    name: Alice
    age: 30
  • ./config/person/bob.yaml文件内容

    1
    2
    name: Bob
    age: 25
  • config/config2.yaml 文件内容:

    1
    name_aux: 100
  • config/config.yaml 文件内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 定义到 defaults 的一定是配置文件,没有配置文件会出错,索引方式见下图
    defaults:
    # - _self_ # 放到最前面则用下面的默认参数覆盖当前文件定义(比如 person:name:0)
    - person: alice # 索引 ./person/alice.yaml,也可以被参数覆盖
    - color: blue # 索引 ./blue/blue.yaml,直接效果与 - color/blue 等价,但 - color/blue 覆盖参数需要使用 `+`,不建议使用
    - person@aux_person: bob # 索引 ./person/bob.yaml,同时重命名为 aux_person,后续通过 "aux_person" 替换 ”person" 作为引用
    - config2 # 直接引用同步目录下的其他文件,相关字段会被 config2.yaml 更新
    - _self_ # 放到最后则用当前文件定义覆盖前面的默认参数(比如 person:name:0)
    # 可以在这里添加其他全局配置
    full_name: "${person.name} Li" # 全局参数,要等到所有解析完成才解析这里,所以不用担心先后顺序,这个总是最后执行的
    modes: ??? # ??? 的变量比较特殊,在通过命令行传入该参数值前,无法直接使用,否则会报错:omegaconf.errors.MissingMandatoryValue: Missing mandatory value: modes
    person:
    name: "lilian" # 当前文件定义参数,是否覆盖引入的默认值与 `_self_` 的位置有关
    ENV_PATH: ${oc.env:PATH} # 读取环境变量 $PATH,环境变量不存在会出错
  • hydra_demo.py 文件内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import hydra
    from omegaconf import OmegaConf
    import json

    @hydra.main(config_path="config", config_name="config", version_base=None)
    def main(cfg):
    print("===== to yaml =====:")
    print(OmegaConf.to_yaml(cfg))

    print("===== parse to json =====:")
    dict_obj = OmegaConf.to_container(cfg, resolve=True)
    json_str = json.dumps(dict_obj, indent=4, ensure_ascii=False)
    print(json_str)

    if __name__ == '__main__':
    main()
  • 执行命令1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    python hydra_demo.py

    # ===== to yaml =====:
    # person:
    # name: lilian
    # age: 30
    # color:
    # favorite_color: blue
    # time: 10
    # aux_person:
    # name: Bob
    # age: 25
    # name_aux: 100
    # full_name: ${person.name} Li
    # modes: ???
    # ENV_PATH: ${oc.env:PATH}
    #
    # ===== parse to json =====:
    # {
    # "person": {
    # "name": "lilian",
    # "age": 30
    # },
    # "color": {
    # "favorite_color": "blue",
    # "time": 10
    # },
    # "aux_person": {
    # "name": "Bob",
    # "age": 25
    # },
    # "name_aux": 100,
    # "full_name": "lilian Li",
    # "modes": "???",
    # "ENV_PATH": "/Users/jiahong/.nvm/versions/node/v12.14.0/bin:/usr/local/opt/node@16/bin:/Users/jiahong/anaconda3/envs/torch_py310/bin:/Users/jiahong/anaconda3/condabin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin"
    # }
  • 执行命令2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    python hydra_demo.py +new_name=Joey person=bob color.time=15

    # ===== to yaml =====:
    # person:
    # name: lilian
    # age: 25
    # color:
    # favorite_color: blue
    # time: 15
    # aux_person:
    # name: Bob
    # age: 25
    # name_aux: 100
    # full_name: ${person.name} Li
    # modes: ???
    # ENV_PATH: ${oc.env:PATH}
    # new_name: Joey
    #
    # ===== parse to json =====:
    # {
    # "person": {
    # "name": "lilian",
    # "age": 25
    # },
    # "color": {
    # "favorite_color": "blue",
    # "time": 15
    # },
    # "aux_person": {
    # "name": "Bob",
    # "age": 25
    # },
    # "name_aux": 100,
    # "full_name": "lilian Li",
    # "modes": "???",
    # "ENV_PATH": "/Users/jiahong/.nvm/versions/node/v12.14.0/bin:/usr/local/opt/node@16/bin:/Users/jiahong/anaconda3/envs/torch_py310/bin:/Users/jiahong/anaconda3/condabin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin",
    # "new_name": "Joey"
    # }

Multi-run:启动多个配置运行

  • 启动方式:

    1
    2
    3
    # 两种启动方式等价
    python my_app.py --multirun db=mysql,postgresql schema=warehouse,support,school
    python my_app.py -m db=mysql,postgresql schema=warehouse,support,school
    • 以上启动会生成6份任务,且串行执行
  • 使用 --multirun 启动的任务配置记录在 multirun/ 文件夹下(单任务启动方式的记录在 outputs/ 下)

Multi-run 的高阶用法

  • 通过覆盖 hydra.sweeper.param 实现启动多个任务

    1
    2
    3
    4
    5
    hydra:
    sweeper:
    params:
    db: mysql,postgresql
    schema: warehouse,support,school
  • 启动命令:

    1
    2
    3
    4
    5
    python my_app.py -m db=mysql
    # [2021-01-20 17:25:03,317][HYDRA] Launching 3 jobs locally
    # [2021-01-20 17:25:03,318][HYDRA] #0 : db=mysql schema=warehouse
    # [2021-01-20 17:25:03,458][HYDRA] #1 : db=mysql schema=support
    # [2021-01-20 17:25:03,602][HYDRA] #2 : db=mysql schema=school

日志文件说明

  • 每次执行命令后都会按照时间生成日志文件

    1
    2
    3
    4
    5
    6
    7
    $ tree outputs/2024-09-25/15-16-17
    outputs/2024-09-25/15-16-17
    ├── .hydra
    │ ├── config.yaml
    │ ├── hydra.yaml
    │ └── overrides.yaml
    └── my_app.log
  • config.yaml: A dump of the user specified configuration

  • hydra.yaml: A dump of the Hydra configuration

  • overrides.yaml: The command line overrides used

  • my_app.log: A log file created for this run

    • 用 Python 文件命令的日志文件,记录被 @hydra.main 注解过的函数中的 log 对象输出
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      import logging

      log = logging.getLogger(__name__)

      @hydra.main(config_path="config", config_name="config", version_base=None)
      def main(config):
      log.info("Info level message")
      log.debug("Debug level message") # 若输出日志的等级包含 debug,则这句话也会输出到日志文件
      pass

      if __name__ == '__main__':
      log.info("out info") # 不会输出到日志文件中(因为不在 `@hydra.main` 注解过的函数中)
      main()

特别需要注意的点

  • 参数覆盖规则:
    • 传入的参数 > 后定义的参数 > 先定义的参数
  • 传入参数的规则:
    • 被覆盖的参数必须是存在的,如 name=Joe 要求 name 已经存在,若不存在则会报错
    • 不存在的参数就需要使用 + 增加参数,如 +name=Joe (少用)
    • 如果存在的参数上使用 +name=Joe 也会出现错误(不可以同时出现两个相同的 key)
    • 注:由于传入的参数会影响生效的子配置文件,自配置文件的参数配置命名上可能不同,所以参数的判定有一定的复杂性
  • 对于子配置可以使用动态方式添加(+),但建议使用 defaults 关键字定义,方便管理,定义后可以被正常覆盖(不再需要 +

附录:使用 Structured Config

  • 在新增加文件的情况下,也可以使用 Python 类定义对象实现类似 yaml 文件的效果(不常用)

  • 示例(无需任何 yaml 文件配置):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from dataclasses import dataclass
    import hydra
    from hydra.core.config_store import ConfigStore

    @dataclass
    class MySQLConfig:
    host: str = "localhost"
    port: int = 3306

    cs = ConfigStore.instance()
    # Registering the Config class with the name 'config'.
    cs.store(name="config", node=MySQLConfig)

    @hydra.main(version_base=None, config_name="config")
    def my_app(cfg: MySQLConfig) -> None:
    if cfg.port == 80:
    print("Is this a webserver?!")

    if __name__ == "__main__":
    my_app()
    • 等价于有了 config.yaml 配置文件,写入了下面的信息
      1
      2
      3
      # config.yaml
      'host': 'localhost'
      'port': 3306
  • 更高阶的层级示例(参考自:https://hydra.cc/docs/tutorials/structured_config/hierarchical_static_config/):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    from dataclasses import dataclass

    import hydra
    from hydra.core.config_store import ConfigStore

    @dataclass
    class MySQLConfig:
    host: str = "localhost"
    port: int = 3306

    @dataclass
    class UserInterface:
    title: str = "My app"
    width: int = 1024
    height: int = 768

    @dataclass
    class MyConfig:
    db: MySQLConfig = field(default_factory=MySQLConfig)
    ui: UserInterface = field(default_factory=UserInterface)

    cs = ConfigStore.instance()
    cs.store(name="config", node=MyConfig)

    @hydra.main(version_base=None, config_name="config")
    def my_app(cfg: MyConfig) -> None:
    print(f"Title={cfg.ui.title}, size={cfg.ui.width}x{cfg.ui.height} pixels")

    if __name__ == "__main__":
    my_app()
  • 更多详情参考:


附录:运行时文件工作路径获取


附录:调试参数配置情况

  • 在命令中添加 --cfg job 等来输出自己的配置
    • job: 个人配置参数生效情况,包括命令行传入的参数,这里是最终生效参数情况
    • hydra: Hydra’s config
    • all: The full config, which is a union of job and hydra. 二者融合
  • 参考链接:https://hydra.cc/docs/tutorials/basic/running_your_app/debugging/