diff --git a/docs/doc_yamls/architecture_search_space.py b/docs/doc_yamls/architecture_search_space.py new file mode 100644 index 00000000..36f8bb38 --- /dev/null +++ b/docs/doc_yamls/architecture_search_space.py @@ -0,0 +1,97 @@ +from __future__ import annotations +from torch import nn +import neps +from neps.search_spaces.architecture import primitives as ops +from neps.search_spaces.architecture import topologies as topos +from neps.search_spaces.architecture.primitives import AbstractPrimitive + + +class DownSampleBlock(AbstractPrimitive): + def __init__(self, in_channels: int, out_channels: int): + super().__init__(locals()) + self.conv_a = ReLUConvBN( + in_channels, out_channels, kernel_size=3, stride=2, padding=1 + ) + self.conv_b = ReLUConvBN( + out_channels, out_channels, kernel_size=3, stride=1, padding=1 + ) + self.downsample = nn.Sequential( + nn.AvgPool2d(kernel_size=2, stride=2, padding=0), + nn.Conv2d( + in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False + ), + ) + + def forward(self, inputs): + basicblock = self.conv_a(inputs) + basicblock = self.conv_b(basicblock) + residual = self.downsample(inputs) + return residual + basicblock + + +class ReLUConvBN(AbstractPrimitive): + def __init__(self, in_channels, out_channels, kernel_size, stride, padding): + super().__init__(locals()) + + self.kernel_size = kernel_size + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d( + in_channels, + out_channels, + kernel_size, + stride=stride, + padding=padding, + dilation=1, + bias=False, + ), + nn.BatchNorm2d(out_channels, affine=True, track_running_stats=True), + ) + + def forward(self, x): + return self.op(x) + + +class AvgPool(AbstractPrimitive): + def __init__(self, **kwargs): + super().__init__(kwargs) + self.op = nn.AvgPool2d(3, stride=1, padding=1, count_include_pad=False) + + def forward(self, x): + return self.op(x) + + +primitives = { + "Sequential15": topos.get_sequential_n_edge(15), + "DenseCell": topos.get_dense_n_node_dag(4), + "down": {"op": DownSampleBlock}, + "avg_pool": {"op": AvgPool}, + "id": {"op": ops.Identity}, + "conv3x3": {"op": ReLUConvBN, "kernel_size": 3, "stride": 1, "padding": 1}, + "conv1x1": {"op": ReLUConvBN, "kernel_size": 1, "stride": 1, "padding": 0}, +} + + +structure = { + "S": ["Sequential15(C, C, C, C, C, down, C, C, C, C, C, down, C, C, C, C, C)"], + "C": ["DenseCell(OPS, OPS, OPS, OPS, OPS, OPS)"], + "OPS": ["id", "conv3x3", "conv1x1", "avg_pool"], +} + + +def set_recursive_attribute(op_name, predecessor_values): + in_channels = 16 if predecessor_values is None else predecessor_values["out_channels"] + out_channels = in_channels * 2 if op_name == "DownSampleBlock" else in_channels + return dict(in_channels=in_channels, out_channels=out_channels) + + +pipeline_space = dict( + architecture=neps.ArchitectureParameter( + set_recursive_attribute=set_recursive_attribute, + structure=structure, + primitives=primitives, + ), + optimizer=neps.CategoricalParameter(choices=["sgd", "adam"]), + learning_rate=neps.FloatParameter(lower=10e-7, upper=10e-3, log=True), +) + diff --git a/docs/doc_yamls/customizing_neps_optimizer.yaml b/docs/doc_yamls/customizing_neps_optimizer.yaml new file mode 100644 index 00000000..b079193e --- /dev/null +++ b/docs/doc_yamls/customizing_neps_optimizer.yaml @@ -0,0 +1,24 @@ +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + optimizer: + choices: [adam, sgd, adamw] + epochs: 50 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget +searcher: + algorithm: bayesian_optimization # name linked with neps keywords, more information click here..? + # Specific arguments depending on the searcher + initial_design_size: 7 + surrogate_model: gp + acquisition: EI + acquisition_sampler: random + random_interleave_prob: 0.1 + diff --git a/docs/doc_yamls/defining_hooks.yaml b/docs/doc_yamls/defining_hooks.yaml new file mode 100644 index 00000000..186acaab --- /dev/null +++ b/docs/doc_yamls/defining_hooks.yaml @@ -0,0 +1,24 @@ +# Basic NEPS Configuration Example +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + epochs: + lower: 5 + upper: 20 + is_fidelity: True + optimizer: + choices: [adam, sgd, adamw] + batch_size: 64 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget + +pre_load_hooks: + hook1: path/to/your/hooks.py # (function_name: Path to the function's file) + hook2: path/to/your/hooks.py # Different function name from the same file source diff --git a/docs/doc_yamls/full_configuration_template.yaml b/docs/doc_yamls/full_configuration_template.yaml new file mode 100644 index 00000000..c68b7570 --- /dev/null +++ b/docs/doc_yamls/full_configuration_template.yaml @@ -0,0 +1,42 @@ +# Full Configuration Template for NePS +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + epochs: + lower: 5 + upper: 20 + is_fidelity: True + optimizer: + choices: [adam, sgd, adamw] + batch_size: 64 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget +max_cost_total: + +# Debug and Monitoring +overwrite_working_directory: True +post_run_summary: True +development_stage_id: +task_id: + +# Parallelization Setup +max_evaluations_per_run: +continue_until_max_evaluation_completed: False + +# Error Handling +loss_value_on_error: +cost_value_on_error: +ignore_errors: + +# Customization Options +searcher: bayesian_optimization # Internal key to select a NePS optimizer. + +# Hooks +pre_load_hooks: diff --git a/docs/doc_yamls/loading_own_optimizer.yaml b/docs/doc_yamls/loading_own_optimizer.yaml new file mode 100644 index 00000000..26897062 --- /dev/null +++ b/docs/doc_yamls/loading_own_optimizer.yaml @@ -0,0 +1,22 @@ +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + optimizer: + choices: [adam, sgd, adamw] + epochs: 50 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget +searcher: + path: path/to/your/searcher.py # Path to the class + name: CustomOptimizer # class name within the file + # Specific arguments depending on your searcher + initial_design_size: 7 + surrogate_model: gp + acquisition: EI diff --git a/docs/doc_yamls/loading_pipeline_space_dict.yaml b/docs/doc_yamls/loading_pipeline_space_dict.yaml new file mode 100644 index 00000000..a8185c79 --- /dev/null +++ b/docs/doc_yamls/loading_pipeline_space_dict.yaml @@ -0,0 +1,11 @@ +# Loading pipeline space from a python dict +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + path: path/to/your/search_space.py # Path to the dict file + name: pipeline_space # Name of the dict instance + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget diff --git a/docs/doc_yamls/outsourcing_optimizer.yaml b/docs/doc_yamls/outsourcing_optimizer.yaml new file mode 100644 index 00000000..e6a3cea5 --- /dev/null +++ b/docs/doc_yamls/outsourcing_optimizer.yaml @@ -0,0 +1,18 @@ +# Optimizer settings from YAML configuration +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + optimizer: + choices: [adam, sgd, adamw] + epochs: 50 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget + +searcher: path/to/your/searcher_setup.yaml diff --git a/docs/doc_yamls/outsourcing_pipeline_space.yaml b/docs/doc_yamls/outsourcing_pipeline_space.yaml new file mode 100644 index 00000000..68558569 --- /dev/null +++ b/docs/doc_yamls/outsourcing_pipeline_space.yaml @@ -0,0 +1,10 @@ +# Pipeline space settings from YAML +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: path/to/your/pipeline_space.yaml + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget + diff --git a/docs/doc_yamls/pipeline_space.yaml b/docs/doc_yamls/pipeline_space.yaml new file mode 100644 index 00000000..a4782db5 --- /dev/null +++ b/docs/doc_yamls/pipeline_space.yaml @@ -0,0 +1,22 @@ +# pipeline_space including priors and fidelity +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + default: 1e-2 + default_confidence: "medium" + epochs: + lower: 5 + upper: 20 + is_fidelity: True + dropout_rate: + lower: 0.1 + upper: 0.5 + default: 0.2 + default_confidence: "high" + optimizer: + choices: [adam, sgd, adamw] + default: adam + # default confidence low + batch_size: 64 diff --git a/docs/doc_yamls/run_pipeline.py b/docs/doc_yamls/run_pipeline.py new file mode 100644 index 00000000..cfea0feb --- /dev/null +++ b/docs/doc_yamls/run_pipeline.py @@ -0,0 +1,7 @@ + + +def example_pipeline(learning_rate, optimizer, epochs): + model = initialize_model() + training_loss = train_model(model, optimizer, learning_rate, epochs) + evaluation_loss = evaluate_model(model) + return {"loss": evaluation_loss, "training_loss": training_loss} diff --git a/docs/doc_yamls/run_pipeline_architecture.py b/docs/doc_yamls/run_pipeline_architecture.py new file mode 100644 index 00000000..f113f5f5 --- /dev/null +++ b/docs/doc_yamls/run_pipeline_architecture.py @@ -0,0 +1,24 @@ +from torch import nn + + +def example_pipeline(architecture, optimizer, learning_rate): + in_channels = 3 + base_channels = 16 + n_classes = 10 + out_channels_factor = 4 + + # E.g., in shape = (N, 3, 32, 32) => out shape = (N, 10) + model = architecture.to_pytorch() + model = nn.Sequential( + nn.Conv2d(in_channels, base_channels, 3, padding=1, bias=False), + nn.BatchNorm2d(base_channels), + model, + nn.BatchNorm2d(base_channels * out_channels_factor), + nn.ReLU(inplace=True), + nn.AdaptiveAvgPool2d(1), + nn.Flatten(), + nn.Linear(base_channels * out_channels_factor, n_classes), + ) + training_loss = train_model(model, optimizer, learning_rate) + evaluation_loss = evaluate_model(model) + return {"loss": evaluation_loss, "training_loss": training_loss} diff --git a/docs/doc_yamls/run_pipeline_big_search_space.py b/docs/doc_yamls/run_pipeline_big_search_space.py new file mode 100644 index 00000000..542283f0 --- /dev/null +++ b/docs/doc_yamls/run_pipeline_big_search_space.py @@ -0,0 +1,6 @@ + +def example_pipeline(learning_rate, optimizer, epochs, batch_size, dropout_rate): + model = initialize_model(dropout_rate) + training_loss = train_model(model, optimizer, learning_rate, epochs, batch_size) + evaluation_loss = evaluate_model(model) + return {"loss": evaluation_loss, "training_loss": training_loss} diff --git a/docs/doc_yamls/run_pipeline_extended.py b/docs/doc_yamls/run_pipeline_extended.py new file mode 100644 index 00000000..c891f4d9 --- /dev/null +++ b/docs/doc_yamls/run_pipeline_extended.py @@ -0,0 +1,6 @@ + +def example_pipeline(learning_rate, optimizer, epochs, batch_size): + model = initialize_model() + training_loss = train_model(model, optimizer, learning_rate, epochs, batch_size) + evaluation_loss = evaluate_model(model) + return {"loss": evaluation_loss, "training_loss": training_loss} diff --git a/docs/doc_yamls/set_up_optimizer.yaml b/docs/doc_yamls/set_up_optimizer.yaml new file mode 100644 index 00000000..9d7e27f7 --- /dev/null +++ b/docs/doc_yamls/set_up_optimizer.yaml @@ -0,0 +1,11 @@ +algorithm: bayesian_optimization +# Specific arguments depending on the searcher +initial_design_size: 7 +surrogate_model: gp +acquisition: EI +log_prior_weighted: false +acquisition_sampler: random +random_interleave_prob: 0.1 +disable_priors: false +prior_confidence: high +sample_default_first: false diff --git a/docs/doc_yamls/simple_example.yaml b/docs/doc_yamls/simple_example.yaml new file mode 100644 index 00000000..5c4758a2 --- /dev/null +++ b/docs/doc_yamls/simple_example.yaml @@ -0,0 +1,16 @@ +# Basic NePS Configuration Example +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + epochs: + lower: 5 + upper: 20 + is_fidelity: True + optimizer: + choices: [adam, sgd, adamw] + batch_size: 64 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget diff --git a/docs/doc_yamls/simple_example_including_run_pipeline.yaml b/docs/doc_yamls/simple_example_including_run_pipeline.yaml new file mode 100644 index 00000000..52c52e19 --- /dev/null +++ b/docs/doc_yamls/simple_example_including_run_pipeline.yaml @@ -0,0 +1,20 @@ +# Simple NePS configuration including run_pipeline +run_pipeline: + path: path/to/your/run_pipeline.py # Path to the function file + name: example_pipeline # Function name within the file + +pipeline_space: + learning_rate: + lower: 1e-5 + upper: 1e-1 + log: True # Log scale for learning rate + epochs: + lower: 5 + upper: 20 + is_fidelity: True + optimizer: + choices: [adam, sgd, adamw] + batch_size: 64 + +root_directory: path/to/results # Directory for result storage +max_evaluations_total: 20 # Budget diff --git a/docs/reference/declarative_usage.md b/docs/reference/declarative_usage.md index 16c54b6e..d9ea503c 100644 --- a/docs/reference/declarative_usage.md +++ b/docs/reference/declarative_usage.md @@ -9,24 +9,34 @@ This approach simplifies sharing, reproducing, and modifying configurations. Below is a straightforward YAML configuration example for NePS covering the required arguments. === "config.yaml" ```yaml - --8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/simple_example.yaml" + --8<-- "docs/doc_yamls/simple_example.yaml" ``` === "run_neps.py" ```python import neps + def run_pipeline(learning_rate, optimizer, epochs): - pass - neps.run(run_pipeline, run_args="path/to/your/config.yaml") + model = initialize_model() + training_loss = train_model(model, optimizer, learning_rate, epochs) + evaluation_loss = evaluate_model(model) + return {"loss": evaluation_loss, "training_loss": training_loss} + + if __name__ == "__main__": + neps.run(run_pipeline, run_args="path/to/your/config.yaml") ``` -#### Advanced Configuration with External Pipeline +#### Including `run_pipeline` in config.yaml for External Referencing In addition to setting experimental parameters via YAML, this configuration example also specifies the pipeline function and its location, enabling more flexible project structures. === "config.yaml" ```yaml - --8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/simple_example_including_run_pipeline.yaml" + --8<-- "docs/doc_yamls/simple_example_including_run_pipeline.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline_extended.py" ``` === "run_neps.py" ```python @@ -35,14 +45,17 @@ and its location, enabling more flexible project structures. neps.run(run_args="path/to/your/config.yaml") ``` -#### Extended Configuration +#### Comprehensive YAML Configuration Template This example showcases a more comprehensive YAML configuration, which includes not only the essential parameters -but also advanced settings for more complex setups: +but also advanced settings for more complex setups. === "config.yaml" ```yaml - --8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/full_configuration_template.yaml" + --8<-- "docs/doc_yamls/full_configuration_template.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline_extended.py" ``` - === "run_neps.py" ```python import neps @@ -58,50 +71,120 @@ through `neps.run`. For a detailed list of integrated optimizers, see [here](opt omitted. ## Different Use Cases -### Customizing neps optimizer -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/customizing_neps_optimizer.yaml" -``` -TODO -information where to find parameters of included searcher, where to find optimizers names...link - - -### Load your own optimizer -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/loading_own_optimizer.yaml" -``` -### How to define hooks? - -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/defining_hooks.yaml" -``` -### What if your search space is big? -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/outsourcing_pipeline_space.yaml" -``` - -pipeline_space.yaml -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/pipeline_space.yaml" -``` - -### If you experimenting a lot with different optimizer settings -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/outsourcing_optimizer.yaml" -``` - -searcher_setup.yaml: -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/set_up_optimizer.yaml" -``` - -### Architecture search space (Loading Dict) -```yaml ---8<-- "tests/test_yaml_run_args/test_declarative_usage_docs/loading_pipeline_space_dict.yaml" -``` -search_space.py -```python -search_space = {} -``` +### Customizing NePS optimizer +Customize an internal NePS optimizer by specifying its parameters directly under the key `searcher` in the +`config.yaml` file. +=== "config.yaml" + ```yaml + --8<-- "docs/doc_yamls/customizing_neps_optimizer.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline.py" + ``` +=== "run_neps.py" + ```python + import neps + neps.run(run_args="path/to/your/config.yaml") + ``` + +For detailed information about the available optimizers and their parameters, please visit the [optimizer page](optimizers.md#list-available-searching-algorithms) + + +### Testing Multiple Optimizer Configurations +Simplify experiments with multiple optimizer settings by outsourcing the optimizer configuration. +=== "config.yaml" + ```yaml + --8<-- "docs/doc_yamls/outsourcing_optimizer.yaml" + ``` +=== "searcher_setup.yaml" + ```yaml + --8<-- "docs/doc_yamls/set_up_optimizer.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline.py" + ``` +=== "run_neps.py" + ```python + import neps + neps.run(run_args="path/to/your/config.yaml") + ``` + +### Handling Large Search Spaces +Manage large search spaces by outsourcing the pipeline space configuration in a separate YAML file or for keeping track +of your experiments. +=== "config.yaml" + ```yaml + --8<-- "docs/doc_yamls/outsourcing_pipeline_space.yaml" + ``` + +=== "pipeline_space.yaml" + ```yaml + --8<-- "docs/doc_yamls/pipeline_space.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline_big_search_space.py" + ``` +=== "run_neps.py" + ```python + import neps + neps.run(run_args="path/to/your/config.yaml") + ``` +### Using Architecture Search Spaces +Since the option for defining the search space via YAML is limited to HPO, grammar-based search spaces or architecture +search spaces must be loaded via a dictionary, which is then referenced in the `config.yaml`. +=== "config.yaml" + ```yaml + --8<-- "docs/doc_yamls/loading_pipeline_space_dict.yaml" + ``` + +=== "search_space.py" + ```python + --8<-- "docs/doc_yamls/architecture_search_space.py" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline_architecture.py" + ``` +=== "run_neps.py" + ```python + import neps + neps.run(run_args="path/to/your/config.yaml") + ``` + + +### Integrating Custom Optimizers +You can also load your own custom optimizer and change its arguments in `config.yaml`. +=== "config.yaml" + ```yaml + --8<-- "docs/doc_yamls/loading_own_optimizer.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline.py" + ``` +=== "run_neps.py" + ```python + import neps + neps.run(run_args="path/to/your/config.yaml") + ``` + +### Adding Custom Hooks to Your Configuration +Define hooks in your YAML configuration to extend the functionality of your experiment. +=== "config.yaml" + ```yaml + --8<-- "docs/doc_yamls/defining_hooks.yaml" + ``` +=== "run_pipeline.py" + ```python + --8<-- "docs/doc_yamls/run_pipeline_extended.py" + ``` +=== "run_neps.py" + ```python + import neps + neps.run(run_args="path/to/your/config.yaml") + ``` diff --git a/docs/reference/pipeline_space.md b/docs/reference/pipeline_space.md index 594f0265..1e52199e 100644 --- a/docs/reference/pipeline_space.md +++ b/docs/reference/pipeline_space.md @@ -109,9 +109,7 @@ Create a YAML file (e.g., `./pipeline_space.yaml`) with the parameter definition type: categorical choices: ["adam", "sgd", "rmsprop"] - dropout_rate: - type: constant - value: 0.5 + dropout_rate: 0.5 ``` === "`run.py`" @@ -120,11 +118,6 @@ Create a YAML file (e.g., `./pipeline_space.yaml`) with the parameter definition neps.run(.., pipeline_space="./pipeline_space.yaml") ``` -!!! tip - - Ensure your YAML file starts with `pipeline_space:`. - This is the root key under which all parameter configurations are defined. - When defining the `pipeline_space` using a YAML file, if the `type` argument is not specified, the NePS will automatically infer the data type based on the value provided. diff --git a/neps/api.py b/neps/api.py index 0438a136..664c163f 100644 --- a/neps/api.py +++ b/neps/api.py @@ -292,8 +292,7 @@ def run( # special case if you load your own optimizer via run_args if inspect.isclass(searcher): if issubclass(searcher, BaseOptimizer): - search_space = pipeline_space_from_yaml(pipeline_space) - search_space = SearchSpace(**search_space) + search_space = SearchSpace(**pipeline_space) searcher = searcher(search_space) else: # Raise an error if searcher is not a subclass of BaseOptimizer diff --git a/neps/search_spaces/search_space.py b/neps/search_spaces/search_space.py index 23f247a6..889938fa 100644 --- a/neps/search_spaces/search_space.py +++ b/neps/search_spaces/search_space.py @@ -27,7 +27,11 @@ from neps.search_spaces.parameter import MutatableParameter, Parameter, ParameterWithPrior from neps.search_spaces.yaml_search_space_utils import ( SearchSpaceFromYamlFileError, - deduce_and_validate_param_type, + deduce_type, + formatting_cat, + formatting_const, + formatting_float, + formatting_int, ) from neps.utils.types import NotSet, _NotSet @@ -90,58 +94,45 @@ def pipeline_space_from_configspace( return pipeline_space -def pipeline_space_from_yaml(yaml_file_path: str | Path) -> dict[str, Parameter]: - """Reads configuration details from a YAML file and constructs a pipeline space - dictionary. - - This function extracts parameter configurations from a YAML file, validating and - translating them into corresponding parameter objects. The resulting dictionary - maps parameter names to their respective configuration objects. +def pipeline_space_from_yaml( # noqa: C901, PLR0912 + config: str | Path | dict, +) -> dict[str, Parameter]: + """Reads configuration details from a YAML file or a dictionary and constructs a + pipeline space dictionary. Args: - yaml_file_path (str | Path): Path to the YAML file containing parameter - configurations. + config (str | Path | dict): Path to the YAML file or a dictionary containing + parameter configurations. Returns: - dict: A dictionary where keys are parameter names and values are parameter - objects (like IntegerParameter, FloatParameter, etc.). + dict[str, Parameter]: A dictionary where keys are parameter names and values + are parameter objects. Raises: - SearchSpaceFromYamlFileError: This custom exception is raised if there are issues - with the YAML file's format or contents. It encapsulates underlying exceptions - (KeyError, TypeError, ValueError) that occur during the processing of the YAML - file. This approach localizes error handling, providing clearer context and - diagnostics. The raised exception includes the type of the original error and - a descriptive message. - - Note: - The YAML file should be properly structured with valid keys and values as per the - expected parameter types. The function employs modular validation and type - deduction logic, ensuring each parameter's configuration adheres to expected - formats and constraints. Any deviation results in an appropriately raised error, - which is then captured by SearchSpaceFromYamlFileError for streamlined error - handling. - - Example: - To use this function with a YAML file 'config.yaml', you can do: - pipeline_space = pipeline_space_from_yaml('config.yaml') + SearchSpaceFromYamlFileError: Raised if there are issues with the YAML file's + format, contents, or if the dictionary is invalid. """ - yaml_file_path = Path(yaml_file_path) try: - # try to load the YAML file - try: - with yaml_file_path.open("r") as file: - config = yaml.safe_load(file) - except FileNotFoundError as e: - raise FileNotFoundError( - f"Unable to find the specified file for 'pipeline_space' at " - f"'{yaml_file_path}'. Please verify the path specified in the " - f"'pipeline_space' argument and try again." - ) from e - except yaml.YAMLError as e: - raise ValueError( - f"The file at {yaml_file_path!s} is not a valid YAML file." - ) from e + if isinstance(config, (str, Path)): + # try to load the YAML file + try: + yaml_file_path = Path(config) + with yaml_file_path.open("r") as file: + config = yaml.safe_load(file) + if not isinstance(config, dict): + raise ValueError( + "The loaded pipeline_space is not a valid dictionary. Please " + "ensure that you use a proper structure. See the documentation " + "for more details." + ) + except FileNotFoundError as e: + raise FileNotFoundError( + f"Unable to find the specified file for 'pipeline_space' at " + f"'{config}'. Please verify the path specified in the " + f"'pipeline_space' argument and try again." + ) from e + except yaml.YAMLError as e: + raise ValueError(f"The file at {config} is not a valid YAML file.") from e # Initialize the pipeline space pipeline_space: dict[str, Parameter] = {} @@ -149,48 +140,34 @@ def pipeline_space_from_yaml(yaml_file_path: str | Path) -> dict[str, Parameter] # Iterate over the items in the YAML configuration for name, details in config.items(): # get parameter type - param_type = deduce_and_validate_param_type(name, details) + param_type = deduce_type(name, details) # init parameter by checking type if param_type in ("int", "integer"): # Integer Parameter - pipeline_space[name] = IntegerParameter( - lower=details["lower"], - upper=details["upper"], - log=details.get("log", False), - is_fidelity=details.get("is_fidelity", False), - default=details.get("default", None), - default_confidence=details.get("default_confidence", "low"), - ) + formatted_details = formatting_int(name, details) + pipeline_space[name] = IntegerParameter(**formatted_details) elif param_type == "float": # Float Parameter - pipeline_space[name] = FloatParameter( - lower=details["lower"], - upper=details["upper"], - log=details.get("log", False), - is_fidelity=details.get("is_fidelity", False), - default=details.get("default", None), - default_confidence=details.get("default_confidence", "low"), - ) + formatted_details = formatting_float(name, details) + pipeline_space[name] = FloatParameter(**formatted_details) elif param_type in ("cat", "categorical"): # Categorical parameter - pipeline_space[name] = CategoricalParameter( - choices=details["choices"], - default=details.get("default", None), - default_confidence=details.get("default_confidence", "low"), - ) - elif param_type in ("const", "constant"): + formatted_details = formatting_cat(name, details) + pipeline_space[name] = CategoricalParameter(**formatted_details) + elif param_type == "const": # Constant parameter - pipeline_space[name] = ConstantParameter(value=details["value"]) + formatted_details = formatting_const(details) + pipeline_space[name] = ConstantParameter(formatted_details) else: # Handle unknown parameter type raise TypeError( - f"Unsupported parameter type{details['type']} for '{name}'.\n" + f"Unsupported parameter with details: {details} for '{name}'.\n" f"Supported Types for argument type are:\n" "For integer parameter: int, integer\n" "For float parameter: float\n" "For categorical parameter: cat, categorical\n" - "For constant parameter: const, constant\n" + "Constant parameter was not detect\n" ) except (KeyError, TypeError, ValueError, FileNotFoundError) as e: raise SearchSpaceFromYamlFileError(e) from e diff --git a/neps/search_spaces/yaml_search_space_utils.py b/neps/search_spaces/yaml_search_space_utils.py index d8063519..8606a3e5 100644 --- a/neps/search_spaces/yaml_search_space_utils.py +++ b/neps/search_spaces/yaml_search_space_utils.py @@ -1,5 +1,4 @@ from __future__ import annotations - import logging import re @@ -82,32 +81,44 @@ def __init__(self, exception): super().__init__(self.message) -def deduce_and_validate_param_type( - name: str, details: dict[str, str | int | float] +def deduce_type( + name: str, details: dict[str, str | int | float] | str | int | float ) -> str: """ - Deduces the parameter type from details and validates them. + Deduces the parameter type from details. Args: name (str): The name of the parameter. - details (dict): A dictionary containing parameter specifications. + details (dict | str | int | float): A dictionary containing parameter + specifications or a direct value (string, integer, or float). Returns: str: The deduced parameter type ('int', 'float', 'categorical', or 'constant'). Raises: TypeError: If the type cannot be deduced or the details don't align with expected - constraints. - """ - # Deduce type - if "type" in details: - param_type = details["type"].lower() - else: - # Logic to infer type if not explicitly provided - param_type = deduce_param_type(name, details) - - # Validate details of a parameter based on (deduced) type - validate_param_details(name, param_type, details) + constraints. + """ + try: + # Deduce type + if "type" in details: + param_type = details.pop("type").lower() + else: + # because details could be string + if isinstance(details, (str, int, float)): + param_type = "const" + return param_type + else: + param_type = deduce_param_type(name, details) + except TypeError as e: + # because details could be int, float + if isinstance(details, (str, int, float)): + param_type = "const" + return param_type + else: + raise TypeError( + f"Unable to deduce parameter type for '{name}' with details '{details}'.") \ + from e return param_type @@ -117,7 +128,7 @@ def deduce_param_type(name: str, details: dict[str, int | str | float]) -> str: The function interprets the 'details' dictionary to determine the parameter type. The dictionary should include key-value pairs that describe the parameter's - characteristics, such as lower, upper, default value, or possible choices. + characteristics, such as lower, upper and choices. Args: @@ -126,7 +137,7 @@ def deduce_param_type(name: str, details: dict[str, int | str | float]) -> str: specifications. Returns: - str: The deduced parameter type ('int', 'float', 'categorical', or 'constant'). + str: The deduced parameter type ('int', 'float' or 'categorical'). Raises: TypeError: If the parameter type cannot be deduced from the details, or if the @@ -172,10 +183,6 @@ def deduce_param_type(name: str, details: dict[str, int | str | float]) -> str: # check for categorical condition elif "choices" in details: param_type = "categorical" - - # check for constant condition - elif "value" in details: - param_type = "constant" else: raise KeyError( f"Unable to deduce parameter type from {name} " @@ -183,98 +190,30 @@ def deduce_param_type(name: str, details: dict[str, int | str | float]) -> str: "Supported parameters:\n" "Float and Integer: Expected keys: 'lower', 'upper'\n" "Categorical: Expected keys: 'choices'\n" - "Constant: Expected keys: 'value'" ) return param_type -def validate_param_details( - name: str, param_type: str, details: dict[str, int | str | float] -): - """ - Validates the details of a parameter based on its type. - - This function checks the format and type-specific details of a parameter - specified in a YAML file. It ensures that the 'name' of the parameter is a string - and its 'details' are provided as a dictionary. Depending on the parameter type, - it delegates the validation to the appropriate type-specific validation function. - - Parameters: - name (str): The name of the parameter. It should be a string. - param_type (str): The type of the parameter. Supported types are 'int' (or 'integer'), - 'float', 'cat' (or 'categorical'), and 'const' (or 'constant'). - details (dict): The detailed configuration of the parameter, which includes its - attributes like 'lower', 'upper', 'default', etc. - - Raises: - KeyError: If the 'name' is not a string or 'details' is not a dictionary, or if - the necessary keys in the 'details' are missing based on the parameter type. - TypeError: If the 'param_type' is not one of the supported types. - - Example Usage: - validate_param_details("learning_rate", "float", {"lower": 0.01, "upper": 0.1, - "default": 0.05}) +def formatting_int(name: str, details: dict[str, str | int | float]): """ - if not (isinstance(name, str) and isinstance(details, dict)): - raise KeyError( - f"Invalid format for {name} in YAML file. " - f"Expected 'name' as string and corresponding 'details' as a " - f"dictionary. Found 'name' type: {type(name).__name__}, 'details' " - f"type: {type(details).__name__}." - ) - param_type = param_type.lower() - # init parameter by checking type - if param_type in ("int", "integer"): - validate_integer_parameter(name, details) - - elif param_type == "float": - validate_float_parameter(name, details) + Converts scientific notation values to integers. - elif param_type in ("cat", "categorical"): - validate_categorical_parameter(name, details) - - elif param_type in ("const", "constant"): - validate_constant_parameter(name, details) - else: - # Handle unknown parameter types - raise TypeError( - f"Unsupported parameter type'{details['type']}' for '{name}'.\n" - f"Supported Types for argument type are:\n" - "For integer parameter: int, integer\n" - "For float parameter: float\n" - "For categorical parameter: cat, categorical\n" - "For constant parameter: const, constant\n" - ) - - -def validate_integer_parameter(name: str, details: dict[str, str | int | float]): - """ - Validates and processes an integer parameter's details, converting scientific - notation to integers where necessary. - - This function checks the type of 'lower' and 'upper', and the 'default' - value (if present) for an integer parameter. It also handles conversion of values - in scientific notation (e.g., 1e2) to integers. + This function converts the 'lower' and 'upper' bounds, as well as the 'default' + value (if present), from scientific notation to integers. Args: name (str): The name of the integer parameter. details (dict[str, str | int | float]): A dictionary containing the parameter's specifications. Expected keys include - 'lower', 'upper', and optionally 'default', - among others. + 'lower', 'upper', and optionally 'default'. Raises: - TypeError: If 'lower', 'upper', or 'default' are not valid integers or cannot - be converted from scientific notation to integers. - """ - # check if all keys are allowed to use and if the mandatory ones are provided - check_keys( - name, - details, - {"lower", "upper", "type", "log", "is_fidelity", "default", "default_confidence"}, - {"lower", "upper"}, - ) + TypeError: If 'lower', 'upper', or 'default' cannot be converted from scientific + notation to integers. + Returns: + The dictionary with the converted integer parameter details. + """ if not isinstance(details["lower"], int) or not isinstance(details["upper"], int): try: # for numbers like 1e2 and 10^ @@ -284,7 +223,7 @@ def validate_integer_parameter(name: str, details: dict[str, str | int | float]) upper, flag_upper = convert_scientific_notation( details["upper"], show_usage_flag=True ) - # check if one value format is e notation and if its an integer + # check if one value format is e notation and if it's an integer if flag_lower or flag_upper: if lower == int(lower) and upper == int(upper): details["lower"] = int(lower) @@ -311,33 +250,28 @@ def validate_integer_parameter(name: str, details: dict[str, str | int | float]) f"default value {details['default']} " f"must be integer for integer parameter {name}" ) from e + return details -def validate_float_parameter(name: str, details: dict[str, str | int | float]): +def formatting_float(name: str, details: dict[str, str | int | float]): """ - Validates and processes a float parameter's details, converting scientific - notation values to float where necessary. + Converts scientific notation values to floats. - This function checks the type of 'lower' and 'upper', and the 'default' - value (if present) for a float parameter. It handles conversion of values in - scientific notation (e.g., 1e-5) to float. + This function converts the 'lower' and 'upper' bounds, as well as the 'default' + value (if present), from scientific notation to floats. Args: name: The name of the float parameter. details: A dictionary containing the parameter's specifications. Expected keys - include 'lower', 'upper', and optionally 'default', among others. + include 'lower', 'upper', and optionally 'default'. Raises: - TypeError: If 'lower', 'upper', or 'default' are not valid floats or cannot - be converted from scientific notation to floats. + TypeError: If 'lower', 'upper', or 'default' cannot be converted from scientific + notation to floats. + + Returns: + The dictionary with the converted float parameter details. """ - # check if all keys are allowed to use and if the mandatory ones are provided - check_keys( - name, - details, - {"lower", "upper", "type", "log", "is_fidelity", "default", "default_confidence"}, - {"lower", "upper"}, - ) if not isinstance(details["lower"], float) or not isinstance(details["upper"], float): try: @@ -346,7 +280,7 @@ def validate_float_parameter(name: str, details: dict[str, str | int | float]): details["upper"] = convert_scientific_notation(details["upper"]) except ValueError as e: raise TypeError( - f"'lower' and 'upper' must be integer for " f"integer parameter '{name}'." + f"'lower' and 'upper' must be float for " f"float parameter '{name}'." ) from e if "default" in details: if not isinstance(details["default"], float): @@ -357,33 +291,26 @@ def validate_float_parameter(name: str, details: dict[str, str | int | float]): f" default'{details['default']}' must be float for float " f"parameter {name} " ) from e + return details -def validate_categorical_parameter(name: str, details: dict[str, str | int | float]): +def formatting_cat(name: str, details: dict[str, str | int | float]): """ - Validates a categorical parameter, including conversion of scientific notation - values to floats within the choices. - This function ensures that the 'choices' key in the details is a list and attempts - to convert any elements in scientific notation to floats. It also handles the - 'default' value, converting it from scientific notation if necessary. + to convert any elements expressed in scientific notation to floats. It also handles + the 'default' value, converting it from scientific notation if necessary. Args: name: The name of the categorical parameter. - details: A dictionary containing the parameter's specifications. Required key - is 'choices', with 'default' being optional. + details: A dictionary containing the parameter's specifications. The required key + is 'choices', which must be a list. The 'default' key is optional. Raises: - TypeError: If 'choices' is not a list - """ - # check if all keys are allowed to use and if the mandatory ones are provided - check_keys( - name, - details, - {"choices", "type", "is_fidelity", "default", "default_confidence"}, - {"choices"}, - ) + TypeError: If 'choices' is not a list. + Returns: + The validated and possibly converted categorical parameter details. + """ if not isinstance(details["choices"], list): raise TypeError(f"The 'choices' for '{name}' must be a list.") for i, element in enumerate(details["choices"]): @@ -408,62 +335,37 @@ def validate_categorical_parameter(name: str, details: dict[str, str | int | flo pass # if default value is not in a numeric format, Value Error occurs if e_flag is True: details["default"] = default + return details -def validate_constant_parameter(name: str, details: dict[str, str | int | float]): +def formatting_const(details: str | int | float): """ - Validates a constant parameter, including conversion of values in scientific - notation to floats. + Validates and converts a constant parameter. - This function checks the 'value' key in the details dictionary and converts any - value expressed in scientific notation to a float. It ensures that the mandatory - 'value' key is provided and appropriately formatted. + This function checks if the 'details' parameter contains a value expressed in + scientific notation and converts it to a float. It ensures that the input + is appropriately formatted, either as a string, integer, or float. Args: - name: The name of the constant parameter. - details: A dictionary containing the parameter's specifications. The required - key is 'value'. + details: A constant parameter that can be a string, integer, or float. + If the value is in scientific notation, it will be converted to a float. + + Returns: + The validated and possibly converted constant parameter. """ - # check if all keys are allowed to use and if the mandatory ones are provided - check_keys(name, details, {"value", "type", "is_fidelity"}, {"value"}) # check for e notation and convert it to float e_flag = False try: converted_value, e_flag = convert_scientific_notation( - details["value"], show_usage_flag=True + details, show_usage_flag=True ) except ValueError: # if the value is not able to convert to float a ValueError get raised by # convert_scientific_notation function pass if e_flag: - details["value"] = converted_value + details = converted_value + return details -def check_keys( - name: str, - details: dict[str, str | int | float], - allowed_keys: set, - mandatory_keys: set, -): - """ - Checks if all keys in 'my_dict' are contained in the set 'allowed_keys' and - if all keys in 'mandatory_keys' are present in 'my_dict'. - Raises an exception if an unallowed key is found or if a mandatory key is missing. - """ - # Check for unallowed keys - unallowed_keys = [key for key in details if key not in allowed_keys] - if unallowed_keys: - unallowed_keys_str = ", ".join(unallowed_keys) - raise KeyError( - f"Unallowed key(s) '{unallowed_keys_str}' found for parameter '" f"{name}'." - ) - - # Check for missing mandatory keys - missing_mandatory_keys = [key for key in mandatory_keys if key not in details] - if missing_mandatory_keys: - missing_keys_str = ", ".join(missing_mandatory_keys) - raise KeyError( - f"Missing mandatory key(s) '{missing_keys_str}' for parameter '" f"{name}'." - ) diff --git a/neps/utils/run_args_from_yaml.py b/neps/utils/run_args_from_yaml.py index add5dcdf..1f290423 100644 --- a/neps/utils/run_args_from_yaml.py +++ b/neps/utils/run_args_from_yaml.py @@ -5,6 +5,7 @@ from neps.optimizers.base_optimizer import BaseOptimizer from typing import Callable, Optional, Dict, Tuple, List, Any import inspect +from neps.search_spaces.search_space import pipeline_space_from_yaml logger = logging.getLogger("neps") @@ -189,9 +190,9 @@ def handle_special_argument_cases(settings: Dict, special_configs: Dict) -> None """ # Load the value of each key from a dictionary specifying "path" and "name". process_config_key( - settings, special_configs, [PIPELINE_SPACE, SEARCHER, RUN_PIPELINE] + settings, special_configs, [SEARCHER, RUN_PIPELINE] ) - + process_pipeline_space(PIPELINE_SPACE, special_configs, settings) if special_configs[SEARCHER_KWARGS] is not None: configs = {} # Check if values of keys is not None and then add the dict to settings @@ -229,11 +230,10 @@ def process_config_key(settings: Dict, special_configs: Dict, keys: List) -> Non if special_configs.get(key) is not None: value = special_configs[key] if isinstance(value, str) and key != RUN_PIPELINE: - # pipeline_space and searcher can also be a string + # searcher can be a string settings[key] = value elif isinstance(value, dict): # dict that should contain 'path' and 'name' for loading value - # (function, dict, class) try: func = load_and_return_object(value["path"], value["name"], key) settings[key] = func @@ -256,6 +256,34 @@ def process_config_key(settings: Dict, special_configs: Dict, keys: List) -> Non ) +def process_pipeline_space(key, special_configs, settings): + if special_configs.get(key) is not None: + pipeline_space = special_configs[key] + if isinstance(pipeline_space, dict): + # TODO: was ist wenn ein argument fehlt + + # determine if dict contains path_loading or the actual search space + expected_keys = {"path", "name"} + actual_keys = set(pipeline_space.keys()) + if expected_keys != actual_keys: + # pipeline_space directly defined in run_args yaml + processed_pipeline_space = pipeline_space_from_yaml(pipeline_space) + else: + # pipeline_space stored in a python dict, not using a yaml + processed_pipeline_space = load_and_return_object(pipeline_space["path"], + pipeline_space[ + "name"], PIPELINE_SPACE) + elif isinstance(pipeline_space, str): + # load yaml from path + processed_pipeline_space = pipeline_space_from_yaml(pipeline_space) + else: + raise TypeError( + f"Value for {PIPELINE_SPACE} must be a string or a dictionary, " + f"but got {type(pipeline_space).__name__}." + ) + settings[key] = processed_pipeline_space + + def load_and_return_object(module_path: str, object_name: str, key: str) -> object: """ Dynamically loads an object from a given module file path. diff --git a/tests/test_neps_api/testing_yaml/optimizer_test.yaml b/tests/test_neps_api/testing_yaml/optimizer_test.yaml index cad2221d..96c585a7 100644 --- a/tests/test_neps_api/testing_yaml/optimizer_test.yaml +++ b/tests/test_neps_api/testing_yaml/optimizer_test.yaml @@ -9,4 +9,4 @@ searcher_kwargs: # Specific arguments depending on the searcher random_interleave_prob: 0.1 disable_priors: false prior_confidence: high - sample_default_first: false \ No newline at end of file + sample_default_first: false diff --git a/tests/test_yaml_run_args/pipeline_space.yaml b/tests/test_yaml_run_args/pipeline_space.yaml new file mode 100644 index 00000000..cd41210f --- /dev/null +++ b/tests/test_yaml_run_args/pipeline_space.yaml @@ -0,0 +1,9 @@ +lr: + lower: 1e-3 + upper: 0.1 +epochs: + lower: 1 + upper: 10 +optimizer: + choices: [adam, sgd, adamw] +batch_size: 64 diff --git a/tests/test_yaml_run_args/run_args_full.yaml b/tests/test_yaml_run_args/run_args_full.yaml index 72184bf8..4c87974c 100644 --- a/tests/test_yaml_run_args/run_args_full.yaml +++ b/tests/test_yaml_run_args/run_args_full.yaml @@ -1,7 +1,7 @@ run_pipeline: path: "tests/test_yaml_run_args/test_yaml_run_args.py" name: run_pipeline -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" budget: diff --git a/tests/test_yaml_run_args/run_args_full_same_level.yaml b/tests/test_yaml_run_args/run_args_full_same_level.yaml index bfba826b..e621c892 100644 --- a/tests/test_yaml_run_args/run_args_full_same_level.yaml +++ b/tests/test_yaml_run_args/run_args_full_same_level.yaml @@ -1,7 +1,7 @@ run_pipeline: path: "tests/test_yaml_run_args/test_yaml_run_args" # check if without .py also works name: "run_pipeline" -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" max_evaluations_total: 20 max_cost_total: 4.2 diff --git a/tests/test_yaml_run_args/run_args_invalid_key.yaml b/tests/test_yaml_run_args/run_args_invalid_key.yaml index 31944e7b..b478abb1 100644 --- a/tests/test_yaml_run_args/run_args_invalid_key.yaml +++ b/tests/test_yaml_run_args/run_args_invalid_key.yaml @@ -1,7 +1,7 @@ run_pipelin: # typo in key path: "tests/test_yaml_run_args/test_yaml_run_args.py" name: run_pipeline -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" budget: diff --git a/tests/test_yaml_run_args/run_args_invalid_type.yaml b/tests/test_yaml_run_args/run_args_invalid_type.yaml index a30392e2..f0e819a3 100644 --- a/tests/test_yaml_run_args/run_args_invalid_type.yaml +++ b/tests/test_yaml_run_args/run_args_invalid_type.yaml @@ -1,4 +1,4 @@ -pipeline_space: "/path" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" budget: diff --git a/tests/test_yaml_run_args/run_args_key_missing.yaml b/tests/test_yaml_run_args/run_args_key_missing.yaml index ba2fefef..db3ca836 100644 --- a/tests/test_yaml_run_args/run_args_key_missing.yaml +++ b/tests/test_yaml_run_args/run_args_key_missing.yaml @@ -1,7 +1,7 @@ run_pipeline: path: "tests/test_yaml_run_args/test_yaml_run_args.py" # key name is missing -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" max_evaluations_total: 20 max_cost_total: 4.2 diff --git a/tests/test_yaml_run_args/run_args_partial.yaml b/tests/test_yaml_run_args/run_args_partial.yaml index 89b54d11..2137bce6 100644 --- a/tests/test_yaml_run_args/run_args_partial.yaml +++ b/tests/test_yaml_run_args/run_args_partial.yaml @@ -1,4 +1,4 @@ -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" budget: diff --git a/tests/test_yaml_run_args/run_args_wrong_name.yaml b/tests/test_yaml_run_args/run_args_wrong_name.yaml index 12f5a83b..0b187332 100644 --- a/tests/test_yaml_run_args/run_args_wrong_name.yaml +++ b/tests/test_yaml_run_args/run_args_wrong_name.yaml @@ -1,7 +1,7 @@ run_pipeline: path: "tests/test_yaml_run_args/test_yaml_run_args.py" name: run_pipelin # typo in name -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" budget: diff --git a/tests/test_yaml_run_args/run_args_wrong_path.yaml b/tests/test_yaml_run_args/run_args_wrong_path.yaml index c76c3bc8..47abfe04 100644 --- a/tests/test_yaml_run_args/run_args_wrong_path.yaml +++ b/tests/test_yaml_run_args/run_args_wrong_path.yaml @@ -1,7 +1,7 @@ run_pipeline: path: "tests/test_yaml_run_args/test_yaml_ru_args.py" # typo in path name: run_pipeline -pipeline_space: "pipeline_space.yaml" +pipeline_space: "tests/test_yaml_run_args/pipeline_space.yaml" root_directory: "test_yaml" budget: diff --git a/tests/test_yaml_run_args/test_declarative_usage_docs/full_configuration_template.yaml b/tests/test_yaml_run_args/test_declarative_usage_docs/full_configuration_template.yaml index f083bd5f..eee0b0fe 100644 --- a/tests/test_yaml_run_args/test_declarative_usage_docs/full_configuration_template.yaml +++ b/tests/test_yaml_run_args/test_declarative_usage_docs/full_configuration_template.yaml @@ -1,6 +1,6 @@ # Full Configuration Template for NePS run_pipeline: - path: path/to/your/run_pipeline.py # Path to the Python file that contains your run_pipeline + path: tests/test_yaml_run_args/test_declarative_usage_docs/run_pipeline.py # Path to the Python file that contains your run_pipeline name: run_pipeline # Name of your run_pipeline function within this file pipeline_space: diff --git a/tests/test_yaml_run_args/test_declarative_usage_docs/neps_run.py b/tests/test_yaml_run_args/test_declarative_usage_docs/neps_run.py new file mode 100644 index 00000000..7829225d --- /dev/null +++ b/tests/test_yaml_run_args/test_declarative_usage_docs/neps_run.py @@ -0,0 +1,21 @@ +import argparse +import numpy as np +import neps + + +def run_pipeline(learning_rate, optimizer, epochs): + """func for test loading of run_pipeline""" + if optimizer == "a": + eval_score = np.random.choice([learning_rate, epochs], 1) + else: + eval_score = 5.0 + return {"loss": eval_score} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Run NEPS optimization with run_args.yml." + ) + parser.add_argument("run_args", type=str, help="Path to the YAML configuration file.") + args = parser.parse_args() + neps.run(run_args=args.run_args) diff --git a/tests/test_yaml_run_args/test_declarative_usage_docs/run_pipeline.py b/tests/test_yaml_run_args/test_declarative_usage_docs/run_pipeline.py new file mode 100644 index 00000000..b0b26c3d --- /dev/null +++ b/tests/test_yaml_run_args/test_declarative_usage_docs/run_pipeline.py @@ -0,0 +1,11 @@ +import numpy as np + + +def run_pipeline(learning_rate, optimizer, epochs): + """func for test loading of run_pipeline""" + if optimizer == "a": + eval_score = np.random.choice([learning_rate, epochs], 1) + else: + eval_score = 5.0 + return {"loss": eval_score} + diff --git a/tests/test_yaml_run_args/test_declarative_usage_docs/simple_example_including_run_pipeline.yaml b/tests/test_yaml_run_args/test_declarative_usage_docs/simple_example_including_run_pipeline.yaml index bf26d491..d46f95f4 100644 --- a/tests/test_yaml_run_args/test_declarative_usage_docs/simple_example_including_run_pipeline.yaml +++ b/tests/test_yaml_run_args/test_declarative_usage_docs/simple_example_including_run_pipeline.yaml @@ -1,7 +1,7 @@ # Extended NePS Configuration Example with External Pipeline Function run_pipeline: - path: path/to/your/run_pipeline.py # Path to the Python file that contains your run_pipeline - name: run_pipeline # Name of your run_pipeline function within this file + path: tests/test_yaml_run_args/test_declarative_usage_docs/run_pipeline.py + name: run_pipeline pipeline_space: learning_rate: diff --git a/tests/test_yaml_run_args/test_declarative_usage_docs/test_declarative_usage_docs.py b/tests/test_yaml_run_args/test_declarative_usage_docs/test_declarative_usage_docs.py index e69de29b..c8459b6c 100644 --- a/tests/test_yaml_run_args/test_declarative_usage_docs/test_declarative_usage_docs.py +++ b/tests/test_yaml_run_args/test_declarative_usage_docs/test_declarative_usage_docs.py @@ -0,0 +1,22 @@ +import pytest +import subprocess +import os +import sys +BASE_PATH = "tests/test_yaml_run_args/test_declarative_usage_docs/" + + +@pytest.mark.neps_api +@pytest.mark.parametrize("yaml_file", [ + "simple_example_including_run_pipeline.yaml", + "full_configuration_template.yaml" +]) +def test_run_with_yaml(yaml_file: str) -> None: + """Test "neps.run" with various run_args.yaml settings to simulate loading options + for variables.""" + assert os.path.exists(BASE_PATH + yaml_file), f"{yaml_file} does not exist." + + try: + subprocess.check_call([sys.executable, BASE_PATH + 'neps_run.py', BASE_PATH + + yaml_file]) + except subprocess.CalledProcessError: + pytest.fail(f"NePS run failed for configuration: {yaml_file}") diff --git a/tests/test_yaml_run_args/test_run_args_by_neps_run/search_space.yaml b/tests/test_yaml_run_args/test_run_args_by_neps_run/search_space.yaml index 7e5ecd55..9a728d74 100644 --- a/tests/test_yaml_run_args/test_run_args_by_neps_run/search_space.yaml +++ b/tests/test_yaml_run_args/test_run_args_by_neps_run/search_space.yaml @@ -8,5 +8,4 @@ learning_rate: log: False optimizer: choices: ["a", "b", "c"] -batch_size: - value: 64 +batch_size: 64 diff --git a/tests/test_yaml_run_args/test_yaml_run_args.py b/tests/test_yaml_run_args/test_yaml_run_args.py index 911aa64d..c9d3901e 100644 --- a/tests/test_yaml_run_args/test_yaml_run_args.py +++ b/tests/test_yaml_run_args/test_yaml_run_args.py @@ -5,8 +5,12 @@ from typing import Union, Callable, Dict, List, Type BASE_PATH = "tests/test_yaml_run_args/" -pipeline_space = dict(lr=neps.FloatParameter(lower=1.2, upper=4.2), - epochs=neps.IntegerParameter(lower=1, upper=10)) +pipeline_space = dict(lr=neps.FloatParameter(lower=1e-3, upper=0.1), + optimizer=neps.CategoricalParameter(choices=["adam", "sgd", + "adamw"]), + epochs=neps.IntegerParameter(lower=1, upper=10), + batch_size=neps.ConstantParameter(value=64)) + def run_pipeline(): @@ -95,7 +99,7 @@ def are_functions_equivalent(f1: Union[Callable, List[Callable]], "run_args_full.yaml", { "run_pipeline": run_pipeline, - "pipeline_space": "pipeline_space.yaml", + "pipeline_space": pipeline_space, "root_directory": "test_yaml", "max_evaluations_total": 20, "max_cost_total": 3, @@ -118,7 +122,7 @@ def are_functions_equivalent(f1: Union[Callable, List[Callable]], "run_args_full_same_level.yaml", { "run_pipeline": run_pipeline, - "pipeline_space": "pipeline_space.yaml", + "pipeline_space": pipeline_space, "root_directory": "test_yaml", "max_evaluations_total": 20, "max_cost_total": 4.2, @@ -140,7 +144,7 @@ def are_functions_equivalent(f1: Union[Callable, List[Callable]], ( "run_args_partial.yaml", { - "pipeline_space": "pipeline_space.yaml", + "pipeline_space": pipeline_space, "root_directory": "test_yaml", "max_evaluations_total": 20, "overwrite_working_directory": True, diff --git a/tests/test_yaml_search_space/config_including_unknown_types.yaml b/tests/test_yaml_search_space/config_including_unknown_types.yaml index 9095e780..778fa2f1 100644 --- a/tests/test_yaml_search_space/config_including_unknown_types.yaml +++ b/tests/test_yaml_search_space/config_including_unknown_types.yaml @@ -14,6 +14,4 @@ optimizer: type: numerical choices: ["adam", "sgd", "rmsprop"] -dropout_rate: - type: numerical - value: 0.5 +dropout_rate: 0.5 diff --git a/tests/test_yaml_search_space/config_including_wrong_types.yaml b/tests/test_yaml_search_space/config_including_wrong_types.yaml index f790d374..62a852c7 100644 --- a/tests/test_yaml_search_space/config_including_wrong_types.yaml +++ b/tests/test_yaml_search_space/config_including_wrong_types.yaml @@ -1,5 +1,5 @@ learning_rate: - type: int + type: int # wrong type lower: 0.00001 upper: 0.1 log: true @@ -14,6 +14,4 @@ optimizer: type: cat choices: ["adam", "sgd", "rmsprop"] -dropout_rate: - type: const - value: 0.5 +dropout_rate: 0.5 diff --git a/tests/test_yaml_search_space/correct_config.yaml b/tests/test_yaml_search_space/correct_config.yaml index 33767ed7..1264127a 100644 --- a/tests/test_yaml_search_space/correct_config.yaml +++ b/tests/test_yaml_search_space/correct_config.yaml @@ -23,9 +23,6 @@ param_float2: param_cat: choices: [2, "sgd", 10e-3] -param_const1: - value: 0.5 +param_const1: 0.5 -param_const2: - value: 1e3 - is_fidelity: TRUE +param_const2: 1e3 diff --git a/tests/test_yaml_search_space/correct_config_including_priors.yml b/tests/test_yaml_search_space/correct_config_including_priors.yml index 7248b98a..1c771110 100644 --- a/tests/test_yaml_search_space/correct_config_including_priors.yml +++ b/tests/test_yaml_search_space/correct_config_including_priors.yml @@ -9,13 +9,10 @@ num_epochs: lower: 3 upper: 30 is_fidelity: True - default: 1e1 optimizer: choices: [adam, 90E-3, rmsprop] default: 90E-3 default_confidence: "medium" -dropout_rate: - value: 1E3 - is_fidelity: true +dropout_rate: 1E3 diff --git a/tests/test_yaml_search_space/correct_config_including_types.yaml b/tests/test_yaml_search_space/correct_config_including_types.yaml index ee5348b3..95a38074 100644 --- a/tests/test_yaml_search_space/correct_config_including_types.yaml +++ b/tests/test_yaml_search_space/correct_config_including_types.yaml @@ -26,11 +26,6 @@ param_cat: type: cat choices: [2, "sgd", 10E-3] -param_const1: - type: const - value: 0.5 +param_const1: 0.5 -param_const2: - type: const - value: 1e3 - is_fidelity: true +param_const2: 1e3 diff --git a/tests/test_yaml_search_space/inconsistent_types_config.yml b/tests/test_yaml_search_space/inconsistent_types_config.yml index 253281b0..509dcb9e 100644 --- a/tests/test_yaml_search_space/inconsistent_types_config.yml +++ b/tests/test_yaml_search_space/inconsistent_types_config.yml @@ -11,6 +11,4 @@ num_epochs: optimizer: choices: ["adam", "sgd", "rmsprop"] -dropout_rate: - value: 0.5 - is_fidelity: True +dropout_rate: 0.5 diff --git a/tests/test_yaml_search_space/inconsistent_types_config2.yml b/tests/test_yaml_search_space/inconsistent_types_config2.yml index b6ed791d..cd4517f3 100644 --- a/tests/test_yaml_search_space/inconsistent_types_config2.yml +++ b/tests/test_yaml_search_space/inconsistent_types_config2.yml @@ -12,6 +12,4 @@ num_epochs: optimizer: choices: ["adam", "sgd", "rmsprop"] -dropout_rate: - value: 0.5 - is_fidelity: True +dropout_rate: 0.5 diff --git a/tests/test_yaml_search_space/missing_key_config.yml b/tests/test_yaml_search_space/missing_key_config.yml index 692a7b84..94e56c9c 100644 --- a/tests/test_yaml_search_space/missing_key_config.yml +++ b/tests/test_yaml_search_space/missing_key_config.yml @@ -10,5 +10,4 @@ num_epochs: optimizer: choices: ["adam", "sgd", "rmsprop"] -dropout_rate: - value: 0.5 +dropout_rate: 0.5 diff --git a/tests/test_yaml_search_space/test_search_space.py b/tests/test_yaml_search_space/test_search_space.py index 53f6c84d..b92289f5 100644 --- a/tests/test_yaml_search_space/test_search_space.py +++ b/tests/test_yaml_search_space/test_search_space.py @@ -45,7 +45,7 @@ def test_correct_including_priors_yaml_file(): assert isinstance(pipeline_space, dict) float1 = FloatParameter(0.00001, 0.1, log=True, is_fidelity=False, default=3.3e-2, default_confidence="high") assert float1.__eq__(pipeline_space["learning_rate"]) is True - int1 = IntegerParameter(3, 30, log=False, is_fidelity=True, default=10) + int1 = IntegerParameter(3, 30, log=False, is_fidelity=True) assert int1.__eq__(pipeline_space["num_epochs"]) is True cat1 = CategoricalParameter(["adam", 90e-3, "rmsprop"], default=90e-3, default_confidence="medium") assert cat1.__eq__(pipeline_space["optimizer"]) is True @@ -86,8 +86,8 @@ def test_yaml_file_including_wrong_types(): """Test the function with a YAML file that defines the wrong but existing type int to float as an optional argument""" with pytest.raises(SearchSpaceFromYamlFileError) as excinfo: - pipeline_space_from_yaml(BASE_PATH + "config_including_wrong_types.yaml") - assert excinfo.value.exception_type == "TypeError" + pipeline_space_from_yaml(Path(BASE_PATH + "inconsistent_types_config2.yml")) + assert excinfo.value.exception_type == "TypeError" @pytest.mark.neps_api @@ -105,7 +105,7 @@ def test_yaml_file_including_not_allowed_parameter_keys(): argument""" with pytest.raises(SearchSpaceFromYamlFileError) as excinfo: pipeline_space_from_yaml(BASE_PATH + "not_allowed_key_config.yml") - assert excinfo.value.exception_type == "KeyError" + assert excinfo.value.exception_type == "TypeError" @pytest.mark.neps_api @@ -136,7 +136,6 @@ def test_float_is_fidelity_not_boolean(): assert excinfo.value.exception_type == "TypeError" - @pytest.mark.neps_api def test_categorical_default_value_not_in_choices(): """Test if a ValueError is raised when the default value is not in the choices