- 参考链接:Hydra 文档官网 hydra.cc/docs/intro
整体说明
- 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
12tree
.
├── config
│ ├── color
│ │ ├── blue.yaml
│ │ └── green.yaml
│ ├── config.yaml
│ ├── config2.yaml
│ └── person
│ ├── alice.yaml
│ └── bob.yaml
└── hydra_demo.py./config/color/blue.yaml文件内容1
2favorite_color: blue
time: 10./config/color/green.yaml文件内容1
favorite_color: green
./config/person/alice.yaml文件内容1
2name: Alice
age: 30./config/person/bob.yaml文件内容1
2name: Bob
age: 25config/config2.yaml文件内容:1
name_aux: 100
config/config.yaml文件内容: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# 定义到 defaults 的一定是配置文件,没有配置文件会出错,索引方式见下图
defaults:
# - _self_ # 放到最前面则用下面的默认参数覆盖当前文件定义(比如 person.name: "lilian" 会被其他文件覆盖)
- person: alice # 索引 ./person/alice.yaml,也可以被参数覆盖,注意:这里 person 是文件夹,可以理解为 Hydra 的包的概念
- color: blue # 索引 ./blue/blue.yaml,直接效果与 - color/blue 等价,但 - color/blue 覆盖参数需要使用 `+`,不建议使用
- person@aux_person: bob # 索引 ./person/bob.yaml,同时重命名为 aux_person,后续通过 "aux_person" 替换 ”person" 作为引用
# 引用示例:
# cfg.person.name 访问来自 alice.yaml 的数据;
# cfg.aux_person.name 访问来自 bob.yaml 的数据;
# 理解替换逻辑:先看有没有完整匹配 defaults 指定的列表参数的,如 person@aux_person 必须是完整参数匹配,然后再按照正常的参数逻辑匹配具体参数(第一轮替换是全量匹配 defaults 中的列表参数:
# 若要覆盖 aux_person 导入的对象,需要在命令行使用 person@aux_person=alice 来替换(不能是 person=alice 也不能是 aux_person=alice)
# 若要覆盖内部的具体参数,则只能使用类似 aux_person.name=Tom 来替换(不能是 person.name 也不能是 person@aux_person.name)
- person@aux.person: bob # 同上索引,但命名为类似 cfg.aux.person.name ,注意这里的 aux.person 会解析为 ["aux"]["person"]
- config2 # 直接引用同步目录下的其他文件,相关参数字段会被 config2.yaml 文件内容更新,注:这里的 config2.yaml 不在任何包下
- _self_ # 放到最后则用当前文件定义覆盖前面的默认参数(比如 person.name: "lilian" 会覆盖其他文件,本文件优先!)
# 可以在这里添加其他全局配置
full_name: "${person.name} Li" # 全局参数,要等到所有解析完成才解析这里,所以不用担心先后顺序,这个总是最后执行的
modes: ??? # ??? 的变量比较特殊,在通过命令行传入该参数值前,无法直接使用,否则会报错:omegaconf.errors.MissingMandatoryValue: Missing mandatory value: modes
person:
name: "lilian" # 当前文件定义参数,是否覆盖引入的默认值与 `_self_` 的位置有关
ENV_PATH: ${oc.env:PATH} # 读取环境变量 $PATH,环境变量不存在会出错
work: # 这里
_target_: demo.test.Work # 这是一个类,可以将下面的参数传入 demo.test.Work 构造函数构造对象(注意:类定义和 yaml 参数配置要对齐,yaml 参数可以少(但不能多),因为可以有默认参数,而且若在本配置文件中定义 _partial_: true ,则可以进一步少传非默认参数);
# 这个定义下,后续初始化对象的使用方式是 `work = hydra.utils.instantiate(cfg.work)`
# 注:也可以整个配置文件上定义 _target_,此时整个文件就是一个对象 `obj = instantiate(cfg)`
# 注:可以嵌套,即 定义了 _target_ 时的参数可以继续时 target,从而实现对象的嵌套引用
# 若在本配置文件中定义 _partial_: true,则 `work = hydra.utils.instantiate(cfg.work)` 得到的是 'functools.partial' 对象,需要调用 `work(time=8)` 等补充必要参数来得到最终对象
# _target_ 本身只是一个申明,如果 hydra 中定义了 _target_ 但是没有调用,相当于没定义
salary: 100
time: 8容易出错的地方:
想要定义一个 .person.name 的变量时必须使用下面的写法:
1
2
3
4
5
6
7
8
9
10
11person:
name: "lilian"
## 解析结果:
# {
# "person": {
# "name": "lilian" # person 是字典,name 是其子字段
# }
# }
# 引用方式:
# * 正确:`cfg.person.name` 或 `cfg["person"]["name"]`
# * 错误:`cfg["person.name"]`错误写法是:
1
2
3
4
5
6
7
8
9
10"person.name": "lilian"
# or 等价定义:
person.name: "lilian"
## 解析结果: 一个扁平的字符串 key
# {
# "person.name": "lilian" # key 是字面字符串 "person.name",没有层级
# }
# 引用方式是:
# * 正确:`cfg["person.name"]`
# * 错误:`cfg.person.name` 或 `cfg["person"]["name"]`特别说明:
- 虽然在参数直接定义时使用
person.name的语法会被解析为"person.name",但是在 hydra 的 default 别名(@后的名称)中可以使用x.y这样的重命名方式,这时候可以解析为["x"]["y"] - 示例:
1
2defaults:
- person@private.person: text # 解析为 ["private"]["person"]
- 虽然在参数直接定义时使用
hydra_demo.py文件内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import hydra
from omegaconf import OmegaConf
import json
# 注意:这里的 config_path 和 config_name 也可以被覆盖
# 覆盖方式:
# * `config_path`:只能用 `--config-path` 覆盖,建议使用绝对路径
# * `config_name`:直接作为位置参数传入(如 `python app.py prod`),也可用 `--config-name`(或 `-cn`)显式指定(建议显示制定)
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
37python hydra_demo.py
# 等价于 python hydra_demo.py --config-path=config --config-name=config
# ===== 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/sanye/.nvm/versions/node/v12.14.0/bin:/usr/local/opt/node@16/bin:/Users/sanye/anaconda3/envs/torch_py310/bin:/Users/sanye/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
38python 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/sanye/.nvm/versions/node/v12.14.0/bin:/usr/local/opt/node@16/bin:/Users/sanye/anaconda3/envs/torch_py310/bin:/Users/sanye/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
5hydra:
sweeper:
params:
db: mysql,postgresql
schema: warehouse,support,school启动命令:
1
2
3
4
5python 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.logconfig.yaml: A dump of the user specified configurationhydra.yaml: A dump of the Hydra configurationoverrides.yaml: The command line overrides usedmy_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
13import logging
log = logging.getLogger(__name__)
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()
- 用 Python 文件命令的日志文件,记录被
特别需要注意的点
- 参数覆盖规则:
- 传入的参数 > 后定义的参数 > 先定义的参数
- 传入参数的规则:
- 被覆盖的参数必须是存在的,如
name=Joe要求name已经存在,若不存在则会报错 - 不存在的参数就需要添加:
- 使用
+增加参数,如+name=Joe
- 使用
- 如果存在的参数上使用
+name=Joe会出现错误(不可以同时出现两个相同的 key) - 如果要覆盖已有的参数,可以使用
++name=Joe这样的句子,++的含义是 不管这个配置原来有没有,都设成这个值- 作为对比,
+号只能添加不存在的参数(存在就会报错);++则可以新加参数,也可以覆盖参数
- 作为对比,
- 注:由于传入的参数会影响生效的子配置文件,自配置文件的参数配置命名上可能不同,所以参数的判定有一定的复杂性
- 被覆盖的参数必须是存在的,如
- 对于子配置可以使用动态方式添加(
+),但建议使用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
20from dataclasses import dataclass
import hydra
from hydra.core.config_store import ConfigStore
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)
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
30from dataclasses import dataclass
import hydra
from hydra.core.config_store import ConfigStore
class MySQLConfig:
host: str = "localhost"
port: int = 3306
class UserInterface:
title: str = "My app"
width: int = 1024
height: int = 768
class MyConfig:
db: MySQLConfig = field(default_factory=MySQLConfig)
ui: UserInterface = field(default_factory=UserInterface)
cs = ConfigStore.instance()
cs.store(name="config", node=MyConfig)
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()
附录:Hydra/OmegaConf 的动态配置引用语法
${oc.select:path,default}是 Hydra/OmegaConf 提供的安全配置引用机制:- 作用是从配置的其他位置读取值,避免重复定义
- 路径不存在时使用默认值,不会导致配置加载失败
- 大型配置中复用参数(如 Actor 和 Critic 共享模型配置)
- 推荐总是使用
oc.select而不是直接插值,以提高配置的鲁棒性
示例语法:
1
diy_flag: ${oc.select:demo_config.diy_flag,false}
- 用于从配置的其他位置读取值,并支持默认值
- **
${...}**:Hydra/OmegaConf 的变量插值语法 - **
oc.select:**:OmegaConf 的select函数,用于安全地访问嵌套配置 - **
demo_config.diy_flag**:要读取的配置路径(点分隔) - **
,false**:如果路径不存在时的默认值
- **
- 等价逻辑
1
2
3
4
5# 这行配置相当于:
try:
diy_flag = config.demo_config.diy_flag
except (AttributeError, KeyError, OmegaConf.errors.ConfigAttributeError):
diy_flag = False
- 用于从配置的其他位置读取值,并支持默认值
基础用法
示例:
1
2
3
4
5
6
7
8
9# config.yaml
demo_config:
diy_flag: true
hidden_size: 768
# 其他地方引用
trainer:
diy_flag: ${oc.select:demo_config.diy_flag,false}
use_attention_mask: ${oc.select:demo_config.use_attention_mask,true}加载后:
1
2
3cfg = OmegaConf.load("config.yaml")
print(cfg.trainer.diy_flag) # True (从 demo_config 读取)
print(cfg.trainer.use_attention_mask) # True (使用默认值,因为路径不存在)oc.selectvs 普通插值普通插值(不推荐,路径不存在会报错)
1
2# 如果路径不存在,会抛出异常
diy_flag: ${demo_config.diy_flag}oc.select(推荐,安全)1
2# 路径不存在时使用默认值,不会报错
diy_flag: ${oc.select:demo_config.diy_flag,false}
高级用法
默认值本身也可以是变量
1
2default_value: ${oc.select:global.default_padding,true}
diy_flag: ${oc.select:demo_config.diy_flag,${default_value}}可以配合类型转换
1
2diy_flag: ${oc.select:demo_config.diy_flag,false}
# 即使配置中是字符串 "true",也会正确转换为布尔值多层回退
1
2# 尝试多个路径,逐个回退
diy_flag: ${oc.select:${nested.select.path},${oc.select:global.fallback,false}}注:命令行可以直接覆盖这个值
1
2
3
4python main.py trainer.diy_flag=true
# 或者覆盖源值
python main.py demo_config.diy_flag=true
注意事项
避免循环引用
1
2
3
4
5
6
7
8
9# 错误:循环引用
model:
param1: ${oc.select:model.param2,1}
param2: ${oc.select:model.param1,2} # 循环引用!
# 最好是单向依赖
model:
base_param: 10
derived_param: ${oc.select:model.base_param,5}如果键名本身包含点号,需要加引号转义
1
value: ${oc.select:'level1.key.with.dots',default}
默认值类型
1
2
3
4
5
6
7
8# 默认值可以是各种类型
string: ${oc.select:path,hello_world} # string: ${oc.select:path,"hello world!"}
int: ${oc.select:path,100}
float: ${oc.select:path,3.14}
bool: ${oc.select:path,false}
list: ${oc.select:path,[1,2,3]}
dict: ${oc.select:path,{a:1,b:2}}
null: ${oc.select:path,null}调试技巧
1
2
3
4
5
6
7
8
9
10
11
12from omegaconf import OmegaConf
# 查看解析后的配置
cfg = OmegaConf.load("config.yaml")
print(OmegaConf.to_yaml(cfg, resolve=True)) # 显示解析后的值
# 检查特定路径是否存在
path_exists = OmegaConf.select(cfg, "demo_config.diy_flag") is not None
print(f"Path exists: {path_exists}")
# 安全获取值(Python 代码中)
value = OmegaConf.select(cfg, "trainer.diy_flag", default=False)
附录:运行时文件工作路径获取
- 使用 Python 命令获取,详情见原始路径
- 参考链接:https://hydra.cc/docs/tutorials/basic/running_your_app/working_directory/
附录:调试参数配置情况
- 在命令中添加
--cfg job等来输出自己的配置job: 个人配置参数生效情况,包括命令行传入的参数,这里是最终生效参数情况hydra: Hydra’s configall: The full config, which is a union of job and hydra. 二者融合
- 参考链接:https://hydra.cc/docs/tutorials/basic/running_your_app/debugging/
附录:_target_ 的继承用法
_target_只是一个字符串路径 ,继承机制完全由 Python 的类继承处理,Hydra 只负责根据路径去实例化基本继承示例
1
2
3
4
5
6
7
8
9
10
11
12
13# models.py
class BaseModel:
def __init__(self, lr: float = 0.001):
self.lr = lr
class ResNet50(BaseModel):
def __init__(self, lr: float = 0.001, layers: int = 50):
super().__init__(lr)
self.layers = layers
class ResNet101(ResNet50):
def __init__(self, lr: float = 0.001, layers: int = 101):
super().__init__(lr, layers)- 对应的 yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14# config.yaml
base_model:
_target_: models.BaseModel
lr: 0.001
resnet50:
_target_: models.ResNet50
lr: 0.001
layers: 50
resnet101:
_target_: models.ResNet101
lr: 0.001
layers: 101
- 对应的 yaml
配置级别的“继承”(推荐)
Hydra 中更常见的是配置文件之间的继承,而不是在
_target_路径上体现:文件一:
1
2
3
4# config/model/base.yaml
_target_: models.BaseModel
lr: 0.001
name: base文件二:
1
2
3
4
5
6# config/model/resnet50.yaml
defaults:
- base # 继承 base.yaml
_target_: models.ResNet50 # 覆盖 _target_
layers: 50 # 添加新字段文件三:
1
2
3
4
5
6# config/model/resnet101.yaml
defaults:
- resnet50 # 继承 resnet50.yaml
_target_: models.ResNet101 # 覆盖 _target_
layers: 101 # 覆盖 layers最终效果:
1
2
3cfg = OmegaConf.load("config/model/resnet101.yaml")
model = instantiate(cfg)
# 等价于:ResNet101(lr=0.001, name="base", layers=101)
运行时切换对象
这是 Hydr 最强大的用法:通过配置切换实现类 :
Python 文件
1
2
3
4
5
6
7
8
9# models.py
class Backbone:
def forward(self, x): pass
class ResNet(Backbone):
def __init__(self, depth: int): ...
class ViT(Backbone):
def __init__(self, patch_size: int): ...yaml 文件
1
2
3
4
5
6
7
8
9
10
11# config.yaml
defaults:
- backbone: resnet50 # 默认使用 ResNet50
backbone:
resnet50:
_target_: models.ResNet
depth: 50
vit_base:
_target_: models.ViT
patch_size: 16运行时切换
1
2
3# 运行时切换
cfg.backbone = "vit_base" # 或通过命令行 backbone=vit_base
backbone = instantiate(cfg.backbone) # 自动创建对应类型