👩💻Hydra로 실험관리 하기
ML/DL 실험에서는 다양한 실험 파라미터들을 관리해야 합니다. 이를 위해 여러가지 방법이 있지만, 이번 포스팅에서는 FacebookResearch에서 만든 Hydra를 사용해보자 합니다.
Hydra 소개
Hydra-core는 OmegaConf를 기반으로 하여 더 편리한 구성 관리와 실험 재현성을 지원하는 상위 레벨의 프레임워크라고 할 수 있습니다.
Hydra와 OmegaConf는 밀접하게 연관되어 있지만, 각각의 역할과 목적이 다릅니다.
OmegaConf는 YAML 기반의 계층적 구성 관리 라이브러리로, 설정 파일을 파싱하고 여러 소스(파일, CLI 인수, 환경 변수 등)로부터 설정을 병합하는 기능을 제공합니다. 이를 통해 프로그램의 설정을 외부 파일로 분리하여 관리할 수 있으며, 실행 중 설정을 변경하거나 추가하는 것도 가능합니다. 설정 파일은 YAML 또는 JSON 형식으로 저장할 수 있으며, 특정 키를 conf.pop("키명")
또는 del conf["키명"]
으로 삭제할 수 있습니다. 또한, 여러 설정을 결합하여 사용할 수 있어, 기본 설정에 서버 설정, 사이트별 설정 등을 추가하거나 머신러닝에서 데이터셋, 모델, 옵티마이저 설정 정보를 통합하는 데 유용합니다.
Hydra는 애플리케이션의 구성 관리를 쉽게 할 수 있도록 돕는 프레임워크로, 내부적으로 OmegaConf를 활용하여 설정 데이터를 관리합니다. 이를 기반으로 구성 파일의 계층적 조합, 명령줄 인수를 통한 설정 오버라이드, 동적 설정 합성 등의 기능을 제공합니다.
쉽게 말해, OmegaConf가 설정 관리의 기본 기능을 제공한다면, Hydra는 여기에 애플리케이션 특화 기능을 추가한 프레임워크라고 볼 수 있습니다.
Structured Config 예제
Hydra는 DRL 프로젝트처럼 많은 파라미터 설정이 필요한 프로젝트를 효율적으로 구성할 수 있도록 도와줍니다. 이번 포스팅에서는 Hydra가 설정(Config)을 관리하는 방식과 이를 활용하는 방법을 간단한 예제를 통해 살펴보겠습니다.
아래 예제들은 버튼을 클릭하시면 실행하면서 살펴보실 수 있습니다.
Hydra는 .yaml
형식의 설정 파일을 사용하며, 이번 Notebook 예제(Google Colaboratory 기준)에서는 이러한 파일들이 /content/configs/
폴더에 저장됩니다. 서브폴더는 프레임워크의 특정 부분에 대한 설정을 정의하며, 최종적으로 main.yaml
파일에서 이 설정들이 조합됩니다. 이번 튜토리얼에서는 다양한 설정 파일을 활용하는 방법과 새로운 파라미터를 추가하는 방법을 살펴보겠습니다.
우선 Hydra를 설치해줍니다.
필요한 모듈들을 import하고 Google Colaboratory에서 예제코드들을 저장하는 경로 ROOT_DIR
을 지정해줍니다.
/content/configs
main.yaml
Checking that main.yaml is in: ['main.yaml']
아래 명령어로 main.yaml의 내용을 확인해보겠습니다. 아직 어떠한 내용도 없는 빈 파일임을 확인할 수 있습니다.
가상의 강화힉습 프로젝트를 위한 Config를 작성한다고 생각하며 아래의 내용처럼 main.yaml
파일을 작성합니다.
%%writefile main.yaml
# This is the main configuration file for the RL project.
# It combines settings for the environment, agent, and training.
env:
name: "CartPole-v1" # Environment name (e.g., Gym environment)
seed: 42 # Random seed for reproducibility
max_steps: 200 # Maximum steps per episode
agent:
type: "DQN" # Agent type (e.g., DQN)
hidden_layers: [64, 64] # Network architecture: two hidden layers with 64 units each
activation: "relu" # Activation function
learning_rate: 0.001 # Learning rate for the optimizer
gamma: 0.99 # Discount factor
training:
episodes: 1000 # Number of training episodes
batch_size: 32 # Batch size for learning
replay_buffer_size: 10000 # Size of the replay buffer
Overwriting main.yaml
작성한 후, main.yaml 내용을 다시 확인하면 위에서 작성한 내용을 확인할 수 있습니다.
# This is the main configuration file for the RL project.
# It combines settings for the environment, agent, and training.
env:
name: "CartPole-v1" # Environment name (e.g., Gym environment)
seed: 42 # Random seed for reproducibility
max_steps: 200 # Maximum steps per episode
agent:
type: "DQN" # Agent type (e.g., DQN)
hidden_layers: [64, 64] # Network architecture: two hidden layers with 64 units each
activation: "relu" # Activation function
learning_rate: 0.001 # Learning rate for the optimizer
gamma: 0.99 # Discount factor
training:
episodes: 1000 # Number of training episodes
batch_size: 32 # Batch size for learning
replay_buffer_size: 10000 # Size of the replay buffer
Config 초기화
Hydra에서 설정(Config) 파일을 초기화하는 방법에는 여러 가지가 있습니다. 공식 문서 Initialization methods에서 확인할 수 있듯이, 총 세 가지 방법이 있으며, 각각의 방법을 예제를 통해 살펴보겠습니다.
Hydra의 설정 초기화 방법
initialize()
: 호출하는 코드의 상대 경로를 기준으로 설정 파일을 초기화합니다.
initialize_config_module()
: 절대 경로를 사용하여 설정 모듈(config_module
)을 기반으로 초기화합니다.
initialize_config_dir()
: 파일 시스템의 절대 경로를 사용하여 설정 디렉터리(config_dir
)를 기반으로 초기화합니다.
이 세 가지 방법은 (1)함수 호출 방식과 (2)컨텍스트(context) 방식으로 사용할 수 있습니다.
- 함수 호출 방식으로 사용하면 Hydra를 전역적(global)으로 초기화하며, 한 번만 호출해야 합니다.
- 반면, 컨텍스트 방식으로 사용하면 특정 블록 내에서만 Hydra를 초기화할 수 있으며, 여러 번 사용할 수도 있습니다.
방법1 initialize()
Hydra를 초기화하고 config_path
를 설정 검색 경로에 추가합니다.
config_path
는 호출하는 코드의 parent 디렉터리를 기준으로 한 상대 경로이며, 이 경우에는 현재 노트북이 위치한 디렉터리를 기준으로 설정됩니다.
%cd /content/
with initialize(version_base=None, config_path="configs"):
# Compose the configuration by selecting the main configuration.
cfg_1 = compose(config_name="main")
# full configuration을 YAML 형식으로 출력하여 쉽게 검사할 수 있습니다.
print(OmegaConf.to_yaml(cfg_1))
/content
env:
name: CartPole-v1
seed: 42
max_steps: 200
agent:
type: DQN
hidden_layers:
- 64
- 64
activation: relu
learning_rate: 0.001
gamma: 0.99
training:
episodes: 1000
batch_size: 32
replay_buffer_size: 10000
방법2 initialize_config_module()
Hydra를 초기화하고 config_module
을 설정 검색 경로에 추가합니다.
config_module
은 반드시 import 가능한 형태여야 하며, 최상위 디렉터리에 __init__.py
파일이 존재해야 합니다. 이번 예제에서는 module
이라는 폴더를 만들어서 import 가능한 디렉토리로 만들어 줍니다. 그리고 첫번째 만들었던 main.yaml
파일을 복사하여 module/main_2.yaml
파일로 만들어 줍니다.
%cd /content/configs
%mkdir -p /content/configs/module
!touch /content/configs/module/__init__.py
%cd /content
!cp ./configs/main.yaml configs/module/main_2.yaml
/content/configs
/content
잘 복사가 되었는지 확인해보겠습니다.
# This is the main configuration file for the RL project.
# It combines settings for the environment, agent, and training.
env:
name: "CartPole-v1" # Environment name (e.g., Gym environment)
seed: 42 # Random seed for reproducibility
max_steps: 200 # Maximum steps per episode
agent:
type: "DQN" # Agent type (e.g., DQN)
hidden_layers: [64, 64] # Network architecture: two hidden layers with 64 units each
activation: "relu" # Activation function
learning_rate: 0.001 # Learning rate for the optimizer
gamma: 0.99 # Discount factor
training:
episodes: 1000 # Number of training episodes
batch_size: 32 # Batch size for learning
replay_buffer_size: 10000 # Size of the replay buffer
이번에는 2번째 방법인 initialize_config_module()
함수를 이용하여 복사했던 main_2.yaml
파일을 이용하여 compose 해보겠습니다.
%cd /content
with initialize_config_module(version_base=None, config_module="configs.module"):
cfg_2 = compose(config_name="main_2")
print(cfg_2)
/content
{'env': {'name': 'CartPole-v1', 'seed': 42, 'max_steps': 200}, 'agent': {'type': 'DQN', 'hidden_layers': [64, 64], 'activation': 'relu', 'learning_rate': 0.001, 'gamma': 0.99}, 'training': {'episodes': 1000, 'batch_size': 32, 'replay_buffer_size': 10000}}
아래와 같이 config의 키들을 확인해볼 수도 있습니다.
방법3 initialize_config_dir()
Hydra를 초기화하고 config_path
를 설정 검색 경로에 추가합니다. config_path
는 파일 시스템 상의 절대 경로여야 합니다. 미리 만들어 놓았던 main.yaml
파일을 이용하여 config를 구성해보겠습니다.
{'env': {'name': 'CartPole-v1', 'seed': 42, 'max_steps': 200}, 'agent': {'type': 'DQN', 'hidden_layers': [64, 64], 'activation': 'relu', 'learning_rate': 0.001, 'gamma': 0.99}, 'training': {'episodes': 1000, 'batch_size': 32, 'replay_buffer_size': 10000}}
여기까지 Hydra의 세 가지 방법을 사용하여 설정(Config)을 초기화하는 방법을 살펴보았습니다. 이제 이렇게 생성된 설정이 어떻게 구성되어 있는지 확인해보겠습니다. 대표적인 설정 예제로 cfg_1
을 살펴보겠습니다.
객체의 타입을 확인해보면, 이는 Omegaconf에서 제공하는 DictConfig
객체임을 알 수 있습니다. DictConfig
는 딕셔너리 형태의 설정을 계층적으로 관리할 수 있도록 해주는 데이터 구조로, YAML 설정 파일을 로드하거나 동적으로 구성 값을 변경할 때 유용하게 활용됩니다.
omegaconf.dictconfig.DictConfig
def __init__(content: Union[Dict[DictKeyType, Any], 'DictConfig', Any], key: Any=None, parent: Optional[Box]=None, ref_type: Union[Any, Type[Any]]=Any, key_type: Union[Any, Type[Any]]=Any, element_type: Union[Any, Type[Any]]=Any, is_optional: bool=True, flags: Optional[Dict[str, bool]]=None) -> None
Container tagging interface
Config 객체의 키들은 다음과 같이 확인할 수 있습니다.
DictConfig
의 키는 config_name.key_name
또는 config_name["key_name"]
형태로 접근할 수 있습니다. 이를 통해 일반적인 딕셔너리처럼 키를 참조하거나 점 표기법(dot notation)을 사용하여 계층적인 설정 값을 쉽게 조회할 수 있습니다.
하위 키들도 동일한 방식으로 접근할 수 있으며, config_name.key_name.sub_key_name
또는 config_name["key_name"]["sub_key_name"]
형태로 호출할 수 있습니다. 이를 활용하면 계층적으로 구성된 설정에서 원하는 값을 직관적으로 조회할 수 있습니다.
Override
이번에는 초기화로 만든 config를 override하는 예제를 살펴보겠습니다. 강화학습에서 여러개의 environment를 병렬로 학습하기 위해 env
하위에 num_envs
config를 1000개로 추가하는 override를 진행해보겠습니다.
%cd /content/
with initialize(version_base=None, config_path="configs"):
# 기존 설정을 유지하면서 새로운 설정을 추가
cfg = compose(config_name="main", overrides=["+env.num_envs=1000"])
# 기존 설정과 오버라이드된 설정이 함께 출력됨
print(OmegaConf.to_yaml(cfg))
/content
env:
name: CartPole-v1
seed: 42
max_steps: 200
num_envs: 1000
agent:
type: DQN
hidden_layers:
- 64
- 64
activation: relu
learning_rate: 0.001
gamma: 0.99
training:
episodes: 1000
batch_size: 32
replay_buffer_size: 10000
override는 기존의 main.yaml
파일의 내용을 변경하지 않고 항목을 추가할 수 있습니다. 다시한번 main.yaml
내용을 확인해보면 num_envs
가 없음을 알 수 있습니다.
# This is the main configuration file for the RL project.
# It combines settings for the environment, agent, and training.
env:
name: "CartPole-v1" # Environment name (e.g., Gym environment)
seed: 42 # Random seed for reproducibility
max_steps: 200 # Maximum steps per episode
agent:
type: "DQN" # Agent type (e.g., DQN)
hidden_layers: [64, 64] # Network architecture: two hidden layers with 64 units each
activation: "relu" # Activation function
learning_rate: 0.001 # Learning rate for the optimizer
gamma: 0.99 # Discount factor
training:
episodes: 1000 # Number of training episodes
batch_size: 32 # Batch size for learning
replay_buffer_size: 10000 # Size of the replay buffer
하지만 override된 cfg의 키에는 env.num_envs
가 있습니다.
Resolver
Hydra는 Omegaconf의 Resolver 기능을 활용할 수 있습니다. OmegaConf.register_new_resolver()
를 사용하여 커스텀 resolver를 등록하면, 새로운 interpolation 타입을 추가할 수 있으며, 설정(Config) 노드가 접근될 때 해당 resolver가 호출됩니다.
Resolver 등록 및 기능
eq
: 두 문자열을 소문자로 변환한 후, 동일한지 비교합니다.
contains
: 첫 번째 문자열이 두 번째 문자열에 포함되어 있는지 검사합니다.
if
: 주어진 조건에 따라 두 값 중 하나를 선택합니다.
resolve_default
: 인자가 빈 문자열이면 기본값을 사용하고, 그렇지 않으면 인자 값을 반환합니다.
이러한 resolver를 활용하면 설정 파일 내에서 조건부 로직, 문자열 비교, 기본값 처리 등을 동적으로 적용할 수 있습니다.
예제로 아래와 같이 Resolver를 등록해보겠습니다.
# Hydra 설정에서 사용할 Resolver들을 등록합니다.
OmegaConf.register_new_resolver("eq", lambda x, y: x.lower() == y.lower())
OmegaConf.register_new_resolver("contains", lambda x, y: x.lower() in y.lower())
OmegaConf.register_new_resolver("if", lambda pred, a, b: a if pred else b)
OmegaConf.register_new_resolver("resolve_default", lambda default, arg: default if arg == "" else arg)
이번 예제에서 사용할 Config는 위의 예제 Config에서 Resolver를 확인하기 위해 아래 내용을 더 추가하여 구성해보겠습니다.
yaml_config = """
env:
name: "CartPole-v1"
seed: 42
agent:
type: "DQN"
hidden_layers: [64, 64]
activation: "relu"
learning_rate: 0.001 # experiment의 default_lr 기준값으로 설정
gamma: 0.99
training:
episodes: 1000
batch_size: ${if:${experiment.is_test}, 128, 32} # 테스트 모드일 경우 128, 아니면 32
replay_buffer_size: 10000
# Resolver를 활용한 동적 설정
experiment:
mode: "test"
is_test: ${eq:${experiment.mode}, "test"}
default_lr: ${resolve_default:0.001, ${agent.learning_rate}}
is_debug: ${contains:${experiment.mode}, "debug"} # "debug" 포함 여부 확인
"""
config를 로드하고 Resolver가 적용된 값을 출력합니다.
cfg = OmegaConf.create(yaml_config)
# Resolver가 적용된 값 출력
print("실험 모드:", cfg.experiment.mode)
print("is_test:", cfg.experiment.is_test)
print("배치 크기:", cfg.training.batch_size)
print("default_lr:", cfg.experiment.default_lr)
print("디버그 모드 여부):", cfg.experiment.is_debug)
실험 모드: test
is_test: True
배치 크기: 128
default_lr: 0.001
디버그 모드 여부): False
예제 출력을 하나씩 살펴보겠습니다.
${eq:${experiment.mode}, "test"}
→experiment.mode
가"test"
이면True
, 아니면False
eq(x, y)
Resolver는 두 값을 비교하여 같으면True
, 다르면False
를 반환합니다.${experiment.mode}
값이"test"
인지 확인하는 역할을 합니다.
위 설정에서
experiment.mode
값이"test"
로 설정되어 있기 때문에,${eq:${experiment.mode}, "test"}
는"test"
와"test"
를 비교하는 형태가 됩니다.eq
함수는 대소문자를 구분하지 않고 두 문자열이 같은지 확인하는 역할을 하므로,"test"
는"test"
와 일치하여True
를 반환합니다. 따라서experiment.is_test
의 값은True
로 설정됩니다.반면, 만약
experiment.mode
값이"train"
이었다면,${eq:${experiment.mode}, "test"}
는"train"
과"test"
를 비교하게 됩니다. 이 두 값은 서로 다르므로eq
함수는False
를 반환하게 되고, 결과적으로experiment.is_test
값은False
로 설정됩니다. 이를 통해 설정 값에 따라 특정 변수를 자동으로 조정할 수 있으며, 이를 활용하면 실험 모드에 따라 설정을 다르게 적용할 수 있습니다.
${if:${experiment.is_test}, 128, 32}
→experiment.is_test
가True
이면128
, 아니면32
if(condition, true_value, false_value)
Resolver는condition
이True
일 때true_value
를,False
일 때false_value
를 반환합니다.experiment.is_test
값이True
인지 확인하여, 이에 따라 다른 값을 할당합니다.
${if:${experiment.is_test}, 128, 32}
구문을 살펴보면,if
함수는 첫 번째 인자로 주어진 조건이True
일 경우 두 번째 인자인128
을 반환하고,False
일 경우 세 번째 인자인32
를 반환하는 역할을 합니다. 현재experiment.is_test
가True
이므로if(True, 128, 32)
는128
을 반환하고, 결과적으로training.batch_size
값이128
이 됩니다.반면, 만약
experiment.mode
가"train"
등 다른 값으로 설정되어 있다면,eq("train", "test")
의 결과는False
가 되어experiment.is_test
가False
로 설정됩니다. 이 경우,if(False, 128, 32)
는False
에 해당하는 세 번째 값인32
를 반환하게 되며,training.batch_size
값이32
로 설정됩니다.이러한 방식은 training과 test에서 배치 크기를 다르게 설정할 때 유용합니다. 예를 들어, 테스트 환경에서는 더 큰 배치 크기를 사용하여 빠르게 결과를 확인하고, 훈련 환경에서는 적절한 배치 크기를 유지하여 안정적인 학습이 가능하도록 조정할 수 있습니다. 이를 통해 설정 파일을 동적으로 관리할 수 있으며, 실험 조건에 따라 유연하게 설정을 변경할 수 있습니다.
${resolve_default:0.001, ${agent.learning_rate}}
→agent.learning_rate
가 설정되지 않았으면 기본값0.001
사용resolve_default(default, arg)
Resolver는arg
값이 비어 있거나 설정되지 않았을 경우default
값을 반환합니다.agent.learning_rate
값이 존재하면 그대로 사용하고, 없다면 기본값0.001
을 사용합니다.
agent: learning_rate: 0.001 # experiment의 default_lr 기준값으로 설정 experiment: default_lr: ${resolve_default:0.001, ${agent.learning_rate}}
- 이 방식은 설정 파일에서 특정 값이 누락되었을 때 기본값을 자동으로 적용하는 데 매우 유용합니다. 예를 들어,
learning_rate
값을 실험마다 다르게 설정할 수 있도록 설정 파일에서 값을 명시적으로 지정할 수도 있지만, 실수로 빠뜨렸을 경우에도resolve_default
를 사용하면 안전하게 기본값을 사용할 수 있습니다. 이를 통해 설정을 더욱 견고하게 만들고, 코드의 예외 처리를 간결하게 할 수 있습니다.
${contains:${experiment.mode}, "debug"}
→experiment.mode
에 “debug”라는 글자가 포함되어 있는지 여부 확인experiment.mode
값이"test"
라면"debug"
가 포함되지 않았으므로contains("test", "debug")
는False
를 반환합니다.- 만약
experiment.mode
값이"test_debug"
라면"debug"
라는 문자열이 포함되어 있으므로contains("test_debug", "debug")
는True
를 반환합니다. - 이를 통해 실험 모드에 따라 자동으로 디버깅 기능을 활성화하거나 로그 출력을 조정할 수 있습니다.
이러한 Resolver 기능을 활용하면 설정 파일을 더욱 동적으로 관리할 수 있습니다!
Conclusion
사실 이번 포스팅에서 Hydra에 대해 정리하게 된 계기는 참고하고 있는 많은 오픈소스 프로젝트들이 Hydra를 가지고 프로젝트를 관리하고 있기 때문에 코드를 이해하기 위해 포스팅을 작성하게 되었습니다. 많은 실험 변수들이 있는 ML/AI 프로젝트들에서 Config 관리는 필수적이며 Hydra라는 툴을 이용하여 편하게 Config들을 조정할 수 있음을 알 수 있었습니다. 대표적으로 Hydra를 가지고 실험 Config를 조정하는 몇가지 오픈 소스들을 소개하며 이번 포스팅을 마치겠습니다.