Search
Duplicate

Hydra를 이용한 설정 관리 프로젝트(2) - 설정 구현

태그
Hydra
Pytorch-lightning
<Table of Contents> 설정 관리 개요
이번 포스팅에서는, Hydra 프레임워크를 이용하여 설정 파일을 어떻게 체계적으로 구성할 수 있는 지 확인하는 시간을 가져보도록 할게요
Hydra를 사용하게 된 이유 및 프로젝트 개요는 지난 포스팅을 확인해주세요!
프로젝트의 전체 코드는 아래 링크에서 확인하시면 됩니다 :)
hydra-project
jihoahn9303

설정 관리 개요

구체적인 내용에 앞서, 지난 시간에 설명했던 프로젝트 구조를 떠올려봅시다.
. ├── config_schemas (Using Hydra Structured configs) │ ├── config_schema.py │ ├── data_module_schema.py │ ├── task │ │ ├── loss_function_schema.py │ │ ├── model │ │ │ ├── adapter_schema.py │ │ │ ├── backbone_schema.py │ │ │ └── head_schema.py │ │ ├── model_schema.py │ │ └── optimizer_schema.py │ ├── task_schema.py │ └── trainer_schema.py ├── configs │ ├── config.yaml │ ├── data_module │ ├── task │ │ ├── loss_function │ │ ├── model │ │ │ ├── adapter │ │ │ ├── head │ │ └── optimizer │ └── trainer └── train.py
설정 관리는 크게 스키마(schema_schemas 폴더) 구성과 세부 구현(configs 폴더)로 구성하였습니다.
스키마를 구성하여 각 컴포넌트의 토대를 구성합니다.
세부 구현을 통해 각 컴포넌트를 구성하는 요소를 정의합니다.
다음 그림은 지난 포스팅에서 사용하였던 컴포넌트 구조도입니다.
분명한 이해를 위하여 다시 동일한 구조도를 가져왔으며, 화살표를 받는 컴포넌트는 다른 컴포넌트에 포함됨을 의미합니다.

Total task 컴포넌트

본 컴포넌트는 프로젝트 전반을 담당하는 컴포넌트입니다.
해당 컴포넌트는 다음과 같이 구성됩니다.
Data module 컴포넌트 → 어떠한 이미지 데이터를 사용할 것인가?
Task 컴포넌트 → 어떠한 이미지 분류 문제를 수행할 것인가?
Trainer 컴포넌트 → 어떠한 학습 장치를 사용할 것인가?

스키마 폴더

먼저, 스키마 측면에서 코드를 살펴보겠습니다.
# ./config_schemas/config_schema.py from omegaconf import MISSING from hydra.core.config_store import ConfigStore from pydantic.dataclasses import dataclass from config_schemas import ( data_module_schema, trainer_schema, task_schema ) @dataclass class Config: task: task_schema.TaskConfig = MISSING data_module: data_module_schema.DataModuleConfig = MISSING trainer: trainer_schema.TrainerConfig = MISSING def setup_config() -> None: cs = ConfigStore.instance() cs.store(name="config_schema", node=Config) task_schema.setup_config() data_module_schema.setup_config() trainer_schema.setup_config()
Python
복사
스키마 구성은 Hydra의 Structured config를 이용하였습니다.
Structured config를 사용하면, 각 컴포넌트를 하나의 노드로 관리할 수 있습니다. 따라서, 각 컴포넌트 간의 관계를 명확하게 할 수 있다는 장점이 있습니다. 마치 위에 제시한 구조도와 같이 말이죠!
또한, 원시 타입(Primitive type) 뿐만 아니라, 클래스도 컴포넌트의 요소로 지정 가능합니다. 이 점은, Argparse와 차별화되는 매우 중요한 특징이라고 생각합니다.
setup_config() 함수를 통해 Config store라는 곳에 모든 설정 정보를 저장합니다. 함수의 구체적인 내용은 다음과 같습니다.
Config store을 인스턴스화(instantiate)합니다. Config store는 프로젝트에 대한 모든 설정 노드 정보를 저장하기 위하여 사용됩니다. 일종의 설정 창고입니다.
노드의 이름은 config_schema이며, 설정 값으로서 Config 클래스를 채택합니다.
Config 클래스는 task, data_module, trainer 컴포넌트를 통해 구성됩니다. 각 컴포넌트의 세부 설정 값은 런타임 단계에서 정해집니다.
이때, MISSING 키워드를 통해 세부 구현을 뒤로 미룹니다. 지연 로딩(Lazy loading) 개념과 유사하다고 생각하시면 될 것 같아요!
MISSING 키워드가 표현되어 있을 시, 해당 컴포넌트의 설정 값은 반드시 런타임 과정에서 명시되어야 합니다. 그렇지 않으면, 설정을 인스턴스화 하여 실제 객체로 변환할 때 런타임 에러가 발생합니다.
마지막으로, task, data_module, trainer 컴포넌트의 설정 노드를 config store에 저장하는 함수를 호출합니다.

configs 폴더

그 다음은, 스키마에 대한 세부 구현에 대해 설명 드리겠습니다.
정말 간단한데요! YAML 파일을 통해 세부 사항을 구현하게 됩니다.
# ./configs/config.yaml defaults: - config_schema - task: cifar10_classification - data_module: cifar10 - trainer: gpu
YAML
복사
defaults 키워드를 통해 기본 설정을 정의합니다.
이때, config_schema 는 무엇인가? 앞서 Config store 설정 창고에 저장한 설정 노드의 이름입니다! config_schema 노드는 Config 클래스의 정보를 담고 있었지요. 결론적으로, 해당 YAML 파일에서 기본 골격으로 Config 클래스를 사용하겠다는 의미입니다.
아래 세 요소는 key-value 형식으로 구성된 것을 확인할 수 있는데요.
key의 경우 YAML 파일이 존재하는 폴더(컴포넌트)의 이름을, value의 경우 YAML 파일의 이름을 의미합니다.
이를 통해, 첫 줄에 제시한 config_schema 컴포넌트의 설정 값을 덮어 씌우는 원리로 동작하게 됩니다!
마지막으로, 런타임 때 각 설정 값으로 MISSING 키워드를 채우면서 인스턴스화를 수행합니다.

Data module 컴포넌트

다음은 Total task 컴포넌트 구성요소 중 하나인 Data module 컴포넌트에 대해 살펴보겠습니다.

스키마 폴더

스키마 코드는 다음과 같습니다.
# ./config_schemas/data_module_schema.py from omegaconf import MISSING from hydra.core.config_store import ConfigStore from pydantic.dataclasses import dataclass @dataclass class DataModuleConfig: _target_: str = MISSING pin_memory: bool = True drop_last: bool = True batch_size: int = MISSING num_workers: int = MISSING data_dir: str = MISSING @dataclass class MNISTDataModuleConfig(DataModuleConfig): _target_: str = "data_modules.MNISTDataModule" @dataclass class CIFAR10DataModuleConfig(DataModuleConfig): _target_: str = "data_modules.CIFAR10DataModule" def setup_config() -> None: cs = ConfigStore.instance() cs.store( group="data_module", name="mnist_data_module_schema", node=MNISTDataModuleConfig ) cs.store( group="data_module", name="cifar10_data_module_schema", node=CIFAR10DataModuleConfig )
Python
복사
본 코드에서는 눈여겨볼만한 점이 두 가지 있습니다
첫째, dataclass인 DataModuleConfig 클래스를 상속받는 클래스를 정의한 것입니다. 이게 무엇을 의미할까요?
Data module 컴포넌트는 어떠한 데이터를 사용할 것인가? 를 다룬다고 말씀드렸습니다. 그렇기 때문에, 이미지 분류에 사용되는 데이터가 다를 때, 새로운 클래스를 정의합니다. 본 프로젝트에서는 MNIST와 CIFAR10 데이터 셋을 사용할 것이므로, 두 개의 클래스를 정의했습니다.
cs.store() 함수 호출 시, group 파라미터 값으로 data_module 을 지정한 것에 주목하세요! 노드 그룹의 이름을 data_module 로 정의하고, 그룹에 속하는 노드 이름(name parameter)을 구분한 것입니다
둘째, _target_ 키워드입니다.
Hydra에서는 _target_ 키워드를 사용하여, 참조할 클래스를 지정할 수 있습니다. MNISTDataModuleConfig 클래스의 경우, data_modules.py 파일의 MNISTDataModule 클래스를 참조합니다.
또한, CIFAR10DataModuleConfig 클래스는 target으로 data_modules.py 파일의 CIFAR10DataModule 클래스를 참조합니다.
그 예시로, 아래 코드는 Pytorch-lighting 기반 MNISTDataModule 클래스의 실제 구현체입니다. 본 포스팅의 주요 목적은 설정 방법을 제시하는 것이므로, 자세한 설명은 넘어가도록 하겠습니다. 자세한 사항은 해당 레퍼런스를 참고해주세요!
# ./data_modules.py from pytorch_lightning import LightningDataModule from torch.utils.data import random_split, DataLoader from torchvision.transforms import transforms from torchvision.datasets import MNIST, CIFAR10 class MNISTDataModule(LightningDataModule): def __init__( self, batch_size: int, num_workers: int = 0, pin_memory: bool = False, drop_last: bool = False, data_dir: str = "./", ): super().__init__() self.data_dir = data_dir self.batch_size = batch_size self.num_workers = num_workers self.pin_memory = pin_memory self.drop_last = drop_last self.transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]) def prepare_data(self): MNIST(self.data_dir, train=True, download=True) MNIST(self.data_dir, train=False, download=True) def setup(self, stage=None): if stage == "fit" or stage is None: mnist_full = MNIST(self.data_dir, train=True, transform=self.transform) self.mnist_train, self.mnist_val = random_split(mnist_full, [55000, 5000]) if stage == "test" or stage is None: self.mnist_test = MNIST(self.data_dir, train=False, transform=self.transform) def train_dataloader(self): return DataLoader( self.mnist_train, batch_size=self.batch_size, num_workers=self.num_workers, pin_memory=self.pin_memory, drop_last=self.drop_last, shuffle=True, ) def val_dataloader(self): return DataLoader( self.mnist_val, batch_size=self.batch_size, num_workers=self.num_workers, pin_memory=self.pin_memory, drop_last=self.drop_last, shuffle=False, ) def test_dataloader(self): return DataLoader( self.mnist_test, batch_size=self.batch_size, num_workers=self.num_workers, pin_memory=self.pin_memory, drop_last=self.drop_last, shuffle=False, )
Python
복사

configs 폴더

MNIST 데이터 모듈을 예시로 설명하겠습니다. 코드는 다음과 같아요 :)
# ./configs/trainer/gpu.yaml defaults: - cifar10_data_module_schema batch_size: 64 num_workers: 8 data_dir: ./data/cifar10
YAML
복사
기본 스키마로서 cifar10_data_module_schema 노드 정보를 사용합니다.
defaults 키워드 외부에서 MISSING 상태였던 파라미터 값을 지정했습니다.

Trainer 컴포넌트

Trainer 컴포넌트는 Pytorch-lightning Trainer 클래스에 대한 설정 컴포넌트입니다.
Trainer 클래스 객체에 사용할 장비에 대한 스펙을 명시하는 부분으로 구성되어있습니다.

스키마 폴더

# ./config_schemas/trainer_schema.py @dataclass class TrainerConfig: _target_: str = "pytorch_lightning.Trainer" max_epochs: int = 10 precision: int = 16 log_every_n_steps: int = 10 accelerator: str = MISSING devices: str = "auto" def setup_config() -> None: cs = ConfigStore.instance() cs.store( group="trainer", name="trainer_schema", node=TrainerConfig )
Python
복사
TrainerConfig 클래스의 accelerator 변수가 MISSING 키워드로 정의되어 있습니다. 따라서, confings 폴더 내에서, 해당 변수의 값을 반드시 명시해주어야 합니다.

configs 폴더

# ./configs/trainer/gpu.yaml defaults: - trainer_schema accelerator: gpu
YAML
복사
gpu.yaml 파일을 예시로 준비했습니다.
trainer_schema 노드에 TrainerConfig 클래스 정보가 저장되어있고, accelerator 값이 MISSING 인 상태입니다. 따라서, 해당 YAML 파일에서 gpu 값을 할당한 것을 확인할 수 있습니다

Task 컴포넌트

Task 컴포넌트는 ‘어떠한 문제를 풀 것인가?’에 대한 내용을 정의하는 부분입니다. 구성 컴포넌트는 아래와 같아요!
Model
Loss function
Optimizer

스키마 폴더

# ./config_schemas/task_schema.py from config_schemas.task import ( loss_function_schema, model_schema, optimizer_schema ) @dataclass class TaskConfig: _target_: str = MISSING optimizer: optimizer_schema.OptimizerConfig = MISSING model: model_schema.ModelConfig = MISSING loss_function: loss_function_schema.LossFunctionConfig = MISSING @dataclass class MNISTClassificationTaskConfig(TaskConfig): _target_: str = "tasks.MNISTClassification" @dataclass class CIFAR10ClassificationTaskConfig(TaskConfig): _target_: str = "tasks.CIFAR10Classification" model: model_schema.ModelConfig = MISSING def setup_config() -> None: cs = ConfigStore.instance() cs.store( group="task", name="mnist_classfication_task_schema", node=MNISTClassificationTaskConfig ) cs.store( group="task", name="cifar10_classification_task_schema", node=CIFAR10ClassificationTaskConfig ) optimizer_schema.setup_config() model_schema.setup_config() loss_function_schema.setup_config()
Python
복사
크게 MNIST와 CIFAR10 이미지 분류 두 task에 대하여 설정 노드를 생성하였습니다.
MNIST → mnist_classfication_task_schema
CIFAR10 → cifar10_classification_task_schema
optimizer, loss_function, model 컴포넌트는 런타임 때 정의되는 부분이므로, MISSING 키워드를 사용하였습니다 (TaskConfig 클래스 정의).

configs 폴더

# ./configs/task/cifar10_classification.yaml defaults: - cifar10_classification_task_schema - model: cifar10_model - optimizer: sgd - loss_function: cross_entropy
YAML
복사
cifar10_classification.yaml 파일을 예시로 준비하였습니다.
cifar10_classification_task_schema 라는 이름을 가진 설정 노드의 값을 기본으로 합니다.
해당 노드의 기본 값은 모두 MISSING 이었으므로, model / optimizer / loss_function에 대응되는 값을 모두 정의하였습니다.
model 폴더 → cifar10_model.yaml 참조
optimizer 폴더 → sgd.yaml 참조
loss_function 폴더 → cross_entropy.yaml 참조

Loss function 컴포넌트

해당 컴포넌트는 학습 과정에서 모델의 파라미터를 갱신하기 위하여, 어떠한 손실 함수(Loss function)을 사용할 지 정의하는 부분입니다.

스키마 폴더

# ./config_schemas/task/loss_function_schema.py @dataclass class LossFunctionConfig: _target_: str = MISSING @dataclass class CrossEntropyLossFunctionConfig(LossFunctionConfig): _target_: str = "torch.nn.CrossEntropyLoss" def setup_config() -> None: cs = ConfigStore.instance() cs.store( group="task/loss_function", name="cross_entropy_loss_function_schema", node=CrossEntropyLossFunctionConfig )
Python
복사
여기서 주목할 점은 cs.store() 함수의 group 파라미터입니다!
Task 컴포넌트에서 설명드렸다시피, loss_function 컴포넌트는 task 컴포넌트 에 소속되었습니다.
따라서, group 파라미터를 명시할 때 task/loss_function 으로 정의하였습니다.
loss_function 는 Task 컴포넌트의 TaskConfig 클래스 변수임을 강조합니다!
Hydra에서는 그룹을 세분화(Top-down 방향)할 시, 반드시 / 문자를 사용해야합니다.
상위그룹/하위그룹task/loss_function

Optimizer 컴포넌트

해당 컴포넌트는 모델 학습 과정에서, 파라미터를 갱신하기 위해 필요한 최적화 알고리즘을 정의하는 부분입니다.설정과 관련하여 매우 중요한 내용이 있으니 천천히 읽어주시기 바랍니다!

스키마 폴더

# ./config_schemas/task/optmizer_schema @dataclass class OptimizerConfig: _target_: str = MISSING _partial_: bool = True lr: float = 0.0001 weight_decay: float = 0.0 @dataclass class AdamOptimizerConfig(OptimizerConfig): _target_: str = "torch.optim.Adam" betas: tuple[float, float] = (0.9, 0.999) eps: float = 1e-8 amsgrad: bool = False @dataclass class SGDOptimizerConfig(OptimizerConfig): _target_: str = "torch.optim.SGD" def setup_config() -> None: cs = ConfigStore.instance() cs.store( group="task/optimizer", name="adam_optimizer_schema", node=AdamOptimizerConfig ) cs.store( group="task/optimizer", name="sgd_optimizer_schema", node=SGDOptimizerConfig )
Python
복사
여기서 매우 특이한 키워드가 처음 등장하였습니다! 무엇일까요?
바로, _partial_ 키워드입니다! (OptimizerConfig 클래스)
왜 해당 키워드를 사용해야만 할까요? 한 번 아래의 코드를 보면서 설명 드리겠습니다.

task.py 파일 분석

# ./tasks.py from typing import Callable, Iterator from torch import Tensor, optim from torchmetrics import Accuracy from torch import nn from pytorch_lightning import LightningModule PARTIAL_OPTIMIZER_TYPE = Callable[[Iterator[nn.Parameter]], optim.Optimizer] class TrainingTask(LightningModule): def __init__(self, optimizer: PARTIAL_OPTIMIZER_TYPE) -> None: super().__init__() self.optimizer = optimizer def configure_optimizers(self) -> optim.Optimizer: return self.optimizer(params=self.parameters())
Python
복사
Pytorch-lightning의 LightningModule 클래스는 모델 학습의 방법을 정의하는 지침서 역할을 수행합니다.
어떠한 모델을 사용할 것인가?
모델의 입력과 출력을 어떻게 정의할 것인가?
모델 학습 과정을 어떻게 정의할 것인가?
따라서, LightningModule 클래스를 상속한 TrainingTask 클래스는 반드시 위 세 요소와 관련된 함수를 구현해야합니다. 자세한 내용은 레퍼런스를 확인해주세요!
저는 여기서, 모델 학습 정의와 관련한 configure_optimizers() 함수에 대해 설명하겠습니다. 함수 이름에서 알수 있지만, 최적화 알고리즘 객체를 생성하고 반환하는 역할을 수행해요.
이때, Pytorch (또는 Pytorch-lightning) 프레임워크의 특성 상, 최적화 객체를 정의할 때 반드시 모델의 파라미터를 params 인자로 받게됩니다.
하지만, 모델의 파라미터는 설정(Configuration) 단계가 아닌 런타임 단계에서 정의될 수 있어요
결과적으로, 설정 노드를 통해 최적화 알고리즘 객체를 만드는 시점은 모델이 정의된 시점 이후가 되어야 합니다!!!!
이 사실을 알려주는 키워드가 바로 OptimizerConfig 클래스의 _partial_ 키워드예요
tasks.py 파일에서도 TrainingTask__init__ 함수를 호출할 때, optimizer 파라미터를 Callable 객체로 받는 것을 확인할 수 있습니다. 실제 Optimizer 클래스 객체는 configure_optimizers 함수를 호출할 때 생성이 됩니다.

configs 폴더

# ./configs/task/optimizer/sgd.yaml defaults: - sgd_optimizer_schema lr: 5e-5 weight_decay: 0.2
YAML
복사
sgd.yaml 파일을 예시로 준비했어요.
task/optimizer 그룹에 속한 sgd_optimizer_schema 이름을 가진 노드의 값을 기본으로 하되, 일부 파라미터 값을 덮어 씌운 것을 확인할 수 있습니다.

Model 컴포넌트

Model 컴포넌트는 Task 컴포넌트에서 사용할 모델의 스펙을 정의하는 역할을 담당합니다. 크게 다음의 컴포넌트로 구성이 되어있어요.
Backbone
Adapter
Head

스키마 폴더

from config_schemas.task.model import ( adapter_schema, backbone_schema, head_schema ) @dataclass class ModelConfig: _target_: str = MISSING backbone: backbone_schema.BackboneConfig = MISSING adapter: adapter_schema.AdapterConfig = MISSING head: head_schema.HeadConfig = MISSING @dataclass class SimpleModelConfig(ModelConfig): _target_: str = "models.SimpleModel" @dataclass class CIFAR10ModelConfig(ModelConfig): _target_: str = "models.CIFAR10Model" def setup_config() -> None: cs = ConfigStore.instance() cs.store( group="task/model", name="simple_model_schema", node=SimpleModelConfig ) cs.store( group="task/model", name="cifar10_model_schema", node=CIFAR10ModelConfig ) backbone_schema.setup_config() adapter_schema.setup_config() head_schema.setup_config()
Python
복사

configs 폴더

CIFAR10 Task에 해당하는 모델을 예시로 들겠습니다.
defaults: - cifar10_model_schema - backbone: resnet18_schema - adapter: linear_512_10 - head: identity_head
YAML
복사
backbone, adapter, head 컴포넌트의 스펙을 해당 파일에서 정의해주는 것을 확인할 수 있습니다.
위 세 컴포넌트의 구조도 앞서 설명 드린 컴포넌트의 구조와 동일합니다.
따라서, 해당 포스팅에서는 위 세 컴포넌트 설정에 대하여 따로 다루지는 않겠습니다. 컴포넌트의 설정과 실제 구현체는 프로젝트 링크에서 확인해주세요!

정리

이번 시간에는 각 컴포넌트의 설정 구현에 대하여 설명드렸습니다.
다음 포스팅에서는 구성한 컴포넌트의 설정을 바탕으로, 머신러닝 task를 어떻게 수행하는 지를 다루겠습니다!!
그럼 안녕히 가세요
위로 올라가기
뒤로 가기