Configuration

Training config is defined in yaml formatted files. See data/config_lorem_ipsum.yaml. These configs are very explicit specifying all training parameters to keep model trainings as transparent and reproducible as possible. Each config setting is reflected in pydantic classes in src/modalities/config/*.py. In the config you need to define which config classes to load in field type_hint. This specifies the concrete class. A second parameter, config, then takes all the constructor arguments for that config class. This way it is easy to change i.e. DataLoaders while still having input validation in place.

Pydantic and ClassResolver

The mechanismn introduced to instantiate classes via type_hint in the config.yaml, utilizes

  1. Omegaconf to load the config yaml file

  2. Pydantic for the validation of the config

  3. ClassResolver to instantiate the correct, concrete class of a class hierarchy.

Firstly, Omegaconf loads the config yaml file and resolves internal refrences such as ${subconfig.attribue}.

Then, Pydantic validates the whole config as is and checks that each of the sub-configs are pydantic.BaseModel classes. For configs, which allow different concrete classes to be instantiated by ClassResolver, the special member names type_hint and config are introduced. With this we utilize Pydantics feature to auto-select a fitting type based on the keys in the config yaml file.

ClassResolver replaces large if-else control structures to infer the correct concrete type with a type_hint used for correct class selection:

activation_resolver = ClassResolver(
  [nn.ReLU, nn.Tanh, nn.Hardtanh],
  base=nn.Module,
  default=nn.ReLU,
)
type_hint="ReLU"
activation_kwargs={...}
activation_resolver.make(type_hint, activation_kwargs),

In our implmentation we go a step further, as both,

  • a type_hint in a BaseModel config must be of type modalities.config.lookup_types.LookupEnum and

  • config is a union of allowed concrete configs of base type BaseModel.

config hereby replaces activation_kwargs in the example above, and replaces it with pydantic-validated BaseModel configs.

With this, a mapping between type hint strings needed for class-resolver, and the concrete class is introduced, while allowing pydantic to select the correct concrete config:

from enum import Enum
from typing import Annotated
from pydantic import BaseModel, PositiveInt, PositiveFloat, Field

class LookupEnum(Enum):
    @classmethod
    def _missing_(cls, value: str) -> type:
        """constructs Enum by member name, if not constructable by value"""
        return cls.__dict__[value]

class SchedulerTypes(LookupEnum):
    StepLR = torch.optim.lr_scheduler.StepLR
    ConstantLR = torch.optim.lr_scheduler.ConstantLR

class StepLRConfig(BaseModel):
    step_size: Annotated[int, Field(strict=True, ge=1)]
    gamma: Annotated[float, Field(strict=True, ge=0.0)]


class ConstantLRConfig(BaseModel):
    factor: PositiveFloat
    total_iters: PositiveInt


class SchedulerConfig(BaseModel):
    type_hint: SchedulerTypes
    config: StepLRConfig | ConstantLRConfig

To allow a user-friendly instantiation, all class resolvers are defined in the ResolverRegistry and build_component_by_config as convenience function is introduced. Dependecies can be passed-through with the extra_kwargs argument:

resolvers = ResolverRegister(config=config)
optimizer = ...  # our example dependency
scheduler = resolvers.build_component_by_config(config=config.scheduler, extra_kwargs=dict(optimizer=optimizer))

To add a new resolver use add_resolver, and the corresponding added resolver will be accessible by the register_key given during adding.

For access use the build_component_by_key_query function of the ResolverRegistry.