From 562716db8f7597c9f4bc4538b8646efb970660ca Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Thu, 28 Mar 2024 11:41:59 +0100 Subject: [PATCH] fix pre-commit hook errors --- .figs/hai_kit_logos.svg | 2 +- CONTRIBUTING.md | 24 +++---- README.md | 88 +++++++++++------------- RELEASE.md | 2 +- docs/_static/css/custom.css | 2 +- docs/algos_explained.rst | 2 +- docs/cmaes.rst | 2 +- docs/eas.rst | 2 +- docs/install.rst | 2 +- docs/island_model.rst | 2 +- docs/overview_pop.rst | 2 +- docs/parallel_pop.rst | 2 +- docs/pop.rst | 2 +- docs/pso.rst | 2 +- docs/quickstart.rst | 2 +- docs/requirements.txt | 2 +- docs/tut_islands.rst | 1 - docs/tut_multi_rank_worker.rst | 2 +- docs/tut_propulator.rst | 4 +- docs/usage.rst | 2 +- propulate/__init__.py | 7 +- propulate/islands.py | 2 +- propulate/migrator.py | 5 +- propulate/pollinator.py | 2 +- propulate/population.py | 27 +++----- propulate/propagators/__init__.py | 39 +++++------ propulate/propagators/base.py | 2 +- propulate/propagators/cmaes.py | 11 ++- propulate/propagators/ga.py | 6 +- propulate/propagators/pso.py | 14 ++-- propulate/propulator.py | 2 +- propulate/surrogate.py | 35 ++++------ propulate/utils.py | 6 +- tests/test_cmaes.py | 4 +- tests/test_propulator_sphere.py | 4 +- tests/test_pso.py | 5 +- tutorials/cmaes_example.py | 9 ++- tutorials/function_benchmark.py | 27 ++++---- tutorials/islands_example.py | 4 +- tutorials/minimum_working_example.py | 6 +- tutorials/multi_rank_workers_example.py | 7 +- tutorials/propulator_example.py | 5 +- tutorials/pso_example.py | 14 ++-- tutorials/surrogate/__init__.py | 4 +- tutorials/surrogate/cifar10_example.py | 31 ++++----- tutorials/surrogate/dynamic_surrogate.py | 37 +++++----- tutorials/surrogate/log_surrogate.py | 13 ++-- tutorials/surrogate/mnist_example.py | 30 +++----- tutorials/surrogate/mock_surrogate.py | 9 ++- tutorials/surrogate/static_surrogate.py | 23 +++---- tutorials/torch_example.py | 25 ++++--- 51 files changed, 264 insertions(+), 300 deletions(-) diff --git a/.figs/hai_kit_logos.svg b/.figs/hai_kit_logos.svg index 47440fb1..31778803 100644 --- a/.figs/hai_kit_logos.svg +++ b/.figs/hai_kit_logos.svg @@ -1,4 +1,4 @@ - \ No newline at end of file + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b53b112..1a57c23d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to Propulate -Welcome to ``Propulate``! We're thrilled that you're interested in contributing to our open-source project. -By participating, you can help improve the project and make it even better. +Welcome to ``Propulate``! We're thrilled that you're interested in contributing to our open-source project. +By participating, you can help improve the project and make it even better. ## How to Contribute @@ -17,13 +17,13 @@ By participating, you can help improve the project and make it even better. git checkout -b your-feature-name ``` -4. **Make Changes**: Make your desired changes to the codebase. Please stick to the following guidelines: +4. **Make Changes**: Make your desired changes to the codebase. Please stick to the following guidelines: * `Propulate` uses [*Black*](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html) code style and so should you if you would like to contribute. * Please use type hints in all function definitions. * Please use American English for all comments and docstrings in the code. - * `Propulate` uses Sphinx autoapi to automatically create API reference documentation from docstrings in the code. + * `Propulate` uses Sphinx autoapi to automatically create API reference documentation from docstrings in the code. Please use the [NumPy Docstring Standard](https://numpydoc.readthedocs.io/en/latest/format.html) for your docstrings: - + ```python """ Short Description @@ -72,7 +72,7 @@ By participating, you can help improve the project and make it even better. ``` When applicable, please make references to parent modules and classes using ```:class:`ParentClassName` ``` as follows: - + ```python """ This is the docstring for MyClass. @@ -97,26 +97,26 @@ as follows: """ The docstring for the parent class. """ - + class MyClass(ParentClassName): """ The docstring for MyClass. - + Parameters ---------- param2 : type Description of param2. - + Attributes ---------- attr2 : type Description of attr2. """ ``` - In the example above, ``` :class:`ParentClassName` ``` is used to create a reference to the parent class `ParentClassName`. + In the example above, ``` :class:`ParentClassName` ``` is used to create a reference to the parent class `ParentClassName`. Sphinx autoapi will automatically generate links to the parent class documentation. - - + + 5. **Commit Changes**: Commit your changes with a clear and concise commit message: ```bash git commit -m "Add your commit message here" diff --git a/README.md b/README.md index 00157bd2..4919c8b2 100644 --- a/README.md +++ b/README.md @@ -14,38 +14,38 @@ [![Documentation Status](https://readthedocs.org/projects/propulate/badge/?version=latest)](https://propulate.readthedocs.io/en/latest/?badge=latest) ![](./coverage.svg) -# **Click [here](https://www.scc.kit.edu/en/aboutus/16956.php) to watch our 3 min introduction video!** +# **Click [here](https://www.scc.kit.edu/en/aboutus/16956.php) to watch our 3 min introduction video!** ## What `Propulate` can do for you -`Propulate` is an HPC-tailored software for solving optimization problems in parallel. It is openly accessible and easy -to use. Compared to a widely used competitor, `Propulate` is consistently faster - at least an order of magnitude for a +`Propulate` is an HPC-tailored software for solving optimization problems in parallel. It is openly accessible and easy +to use. Compared to a widely used competitor, `Propulate` is consistently faster - at least an order of magnitude for a set of typical benchmarks - and in some cases even more accurate. -Inspired by biology, `Propulate` borrows mechanisms from biological evolution, such as selection, recombination, and -mutation. Evolution begins with a population of solution candidates, each with randomly initialized genes. It is an -iterative "survival of the fittest" process where the population at each iteration can be viewed as a generation. For -each generation, the fitness of each candidate in the population is evaluated. The genes of the fittest candidates are +Inspired by biology, `Propulate` borrows mechanisms from biological evolution, such as selection, recombination, and +mutation. Evolution begins with a population of solution candidates, each with randomly initialized genes. It is an +iterative "survival of the fittest" process where the population at each iteration can be viewed as a generation. For +each generation, the fitness of each candidate in the population is evaluated. The genes of the fittest candidates are incorporated in the next generation. -Like in nature, `Propulate` does not wait for all compute units to finish the evaluation of the current generation. -Instead, the compute units communicate the currently available information and use that to breed the next candidate +Like in nature, `Propulate` does not wait for all compute units to finish the evaluation of the current generation. +Instead, the compute units communicate the currently available information and use that to breed the next candidate immediately. This avoids waiting idly for other units and thus a load imbalance. -Each unit is responsible for evaluating a single candidate. The result is a fitness level corresponding with that -candidate’s genes, allowing us to compare and rank all candidates. This information is sent to other compute units as +Each unit is responsible for evaluating a single candidate. The result is a fitness level corresponding with that +candidate’s genes, allowing us to compare and rank all candidates. This information is sent to other compute units as soon as it becomes available. -When a unit is finished evaluating a candidate and communicating the resulting fitness, it breeds the candidate for the -next generation using the fitness values of all candidates it evaluated and received from other units so far. +When a unit is finished evaluating a candidate and communicating the resulting fitness, it breeds the candidate for the +next generation using the fitness values of all candidates it evaluated and received from other units so far. -`Propulate` can be used for hyperparameter optimization and neural architecture search. -It was already successfully applied in several accepted scientific publications. Applications include grid load +`Propulate` can be used for hyperparameter optimization and neural architecture search. +It was already successfully applied in several accepted scientific publications. Applications include grid load forecasting, remote sensing, and structural molecular biology. ## In more technical terms -``Propulate`` is a massively parallel evolutionary hyperparameter optimizer based on the island model with asynchronous +``Propulate`` is a massively parallel evolutionary hyperparameter optimizer based on the island model with asynchronous propagation of populations and asynchronous migration. -In contrast to classical GAs, ``Propulate`` maintains a continuous population of already evaluated individuals with a +In contrast to classical GAs, ``Propulate`` maintains a continuous population of already evaluated individuals with a softened notion of the typically strictly separated, discrete generations. Our contributions include: - A novel parallel genetic algorithm based on a fully asynchronized island model with independently processing workers. @@ -53,40 +53,40 @@ Our contributions include: - Optimized use efficiency of parallel hardware by minimizing idle times in distributed computing environments. To be more efficient, the generations are less well separated than they usually are in evolutionary algorithms. -New individuals are generated from a pool of currently active, already evaluated individuals that may be from any +New individuals are generated from a pool of currently active, already evaluated individuals that may be from any generation. Individuals may be removed from the breeding population based on different criteria. -You can find the corresponding publication [here](https://doi.org/10.1007/978-3-031-32041-5_6): -> Taubert, O. *et al.* (2023). Massively Parallel Genetic Optimization Through Asynchronous Propagation of Populations. -> In: Bhatele, A., Hammond, J., Baboulin, M., Kruse, C. (eds) High Performance Computing. ISC High Performance 2023. -> Lecture Notes in Computer Science, vol 13948. Springer, Cham. +You can find the corresponding publication [here](https://doi.org/10.1007/978-3-031-32041-5_6): +> Taubert, O. *et al.* (2023). Massively Parallel Genetic Optimization Through Asynchronous Propagation of Populations. +> In: Bhatele, A., Hammond, J., Baboulin, M., Kruse, C. (eds) High Performance Computing. ISC High Performance 2023. +> Lecture Notes in Computer Science, vol 13948. Springer, Cham. > [doi.org/10.1007/978-3-031-32041-5_6](https://doi.org/10.1007/978-3-031-32041-5_6) ## Documentation -Check out the full documentation at [https://propulate.readthedocs.io/](https://propulate.readthedocs.io/) :rocket:! Here you can find installation +Check out the full documentation at [https://propulate.readthedocs.io/](https://propulate.readthedocs.io/) :rocket:! Here you can find installation instructions, tutorials, theoretical background, and API references. -**:point_right: If you have any questions or run into any challenges while using `Propulate`, don't hesitate to post an -[issue](https://github.com/Helmholtz-AI-Energy/propulate/issues) :bookmark:, reach out via [GitHub -discussions](https://github.com/Helmholtz-AI-Energy/propulate/discussions) :octocat:, or contact us directly via e-mail -:email: to [propulate@lists.kit.edu](mailto:propulate@lists.kit.edu).** +**:point_right: If you have any questions or run into any challenges while using `Propulate`, don't hesitate to post an +[issue](https://github.com/Helmholtz-AI-Energy/propulate/issues) :bookmark:, reach out via [GitHub +discussions](https://github.com/Helmholtz-AI-Energy/propulate/discussions) :octocat:, or contact us directly via e-mail +:email: to [propulate@lists.kit.edu](mailto:propulate@lists.kit.edu).** ## Installation -You can install the latest stable release from PyPI: ``pip install propulate`` +You can install the latest stable release from PyPI: ``pip install propulate`` If you need the latest updates, you can also install ``Propulate`` directly from the master branch at you own risk. -Pull and run ``pip install -e .`` or ``python setup.py develop``. -``Propulate`` depends on [``mpi4py``](https://mpi4py.readthedocs.io/en/stable/) and requires an MPI implementation under +Pull and run ``pip install -e .`` or ``python setup.py develop``. +``Propulate`` depends on [``mpi4py``](https://mpi4py.readthedocs.io/en/stable/) and requires an MPI implementation under the hood. Currently, it is only tested with [OpenMPI](https://www.open-mpi.org/). ## Quickstart -*Below, you can find a quick recipe for how to use `Propulate` in general. Check out the official -[ReadTheDocs](https://propulate.readthedocs.io/en/latest/tut_propulator.html) documentation for more detailed tutorials +*Below, you can find a quick recipe for how to use `Propulate` in general. Check out the official +[ReadTheDocs](https://propulate.readthedocs.io/en/latest/tut_propulator.html) documentation for more detailed tutorials and explanations.* -Let's minimize the sphere function $f_\text{sphere}\left(x,y\right)=x^2 +y^2$ with `Propulate` as a quick example. The +Let's minimize the sphere function $f_\text{sphere}\left(x,y\right)=x^2 +y^2$ with `Propulate` as a quick example. The minimum is at $\left(x, y\right)=\left(0,0\right)$ at the orange star. ![](./docs/images/sphere.png) First, we need to define the key ingredients that define our optimization problem: @@ -95,7 +95,7 @@ First, we need to define the key ingredients that define our optimization proble - A tuple of `float` for a continuous parameter, e.g., `{"learning_rate": (0.0001, 0.01)}` - A tuple of `int` for an ordinal parameter, e.g., `{"conv_layers": (2, 10)}` - A tuple of `str` for a categorical parameter, e.g., `{"activation": ("relu", "sigmoid", "tanh")}` - + Thus, an exemplary search space might look like this: ```python search_space = { @@ -105,19 +105,19 @@ First, we need to define the key ingredients that define our optimization proble } ``` - The sphere function has two continuous parameters, $x$ and $y$, and we consider $x,y\in\left[-5.12,5.12\right]$. The + The sphere function has two continuous parameters, $x$ and $y$, and we consider $x,y\in\left[-5.12,5.12\right]$. The search space in our example thus looks like this: ```python limits = { "x": (-5.12, 5.12), "y": (-5.12, 5.12) - } + } ``` -- The **loss function**. This is the function we want to minimize in order to find the best parameters. It can be any +- The **loss function**. This is the function we want to minimize in order to find the best parameters. It can be any `Python` function that - takes a set of parameters as a `Python` dictionary as an input. - returns a scalar loss value that determines how good the tested parameter set is. - + In this example, the loss function whose minimum we want to find is the sphere function: ```python def sphere(params: Dict[str, float]) -> float: @@ -131,7 +131,7 @@ First, we need to define the key ingredients that define our optimization proble ---------- params: Dict[str, float] The function parameters. - + Returns ------- float @@ -139,7 +139,7 @@ First, we need to define the key ingredients that define our optimization proble """ return numpy.sum(numpy.array(list(params.values())) ** 2).item() ``` -Next, we need to define the **evolutionary operator** or propagator that we want to use to breed new individuals during the +Next, we need to define the **evolutionary operator** or propagator that we want to use to breed new individuals during the optimization process. `Propulate` provides a reasonable default propagator via a utility function: ```python # Set up logger for Propulate optimization. @@ -166,7 +166,7 @@ propulator = propulate.Propulator( checkpoint_path=config.checkpoint, ) ``` -Now we can run the actual optimization. Overall, ``generations * mpi4py.MPI.COMM_WORLD.size`` evaluations will be +Now we can run the actual optimization. Overall, ``generations * mpi4py.MPI.COMM_WORLD.size`` evaluations will be performed: ```python # Run optimization and print summary of results. @@ -219,9 +219,3 @@ Do the following to run the [example script](https://github.com/Helmholtz-AI-Ene ## Acknowledgments *This work is supported by the Helmholtz AI platform grant.* ![](./.figs/hai_kit_logos.svg) - - - - - - diff --git a/RELEASE.md b/RELEASE.md index 2e461c05..1f961e7a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,4 +5,4 @@ The current workflow for releasing a new version of `Propulate` is as follows: 2. On the master branch, update the version number in `setup.cfg`. We use semantic versioning. 3. Rebase release branch onto current master branch. 4. Make Github release from current master, including corresponding version tag. -5. Push release branch. This will trigger a Github action publishing the new release on PyPI. \ No newline at end of file +5. Push release branch. This will trigger a Github action publishing the new release on PyPI. diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 82a5c4b4..b2a4b4ad 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -7493,4 +7493,4 @@ span[id*='MathJax-Span'] { */ .rst-content dl.field-list > dd {margin-top: 5px !important; margin-bottom: 4px !important} .rst-content dl.field-list > dt {margin-top: 5px !important; margin-bottom: 4px !important} -.rst-content dl dd > p:last-child {margin-top: 4px !important} \ No newline at end of file +.rst-content dl dd > p:last-child {margin-top: 4px !important} diff --git a/docs/algos_explained.rst b/docs/algos_explained.rst index 7058d163..6d39181b 100644 --- a/docs/algos_explained.rst +++ b/docs/algos_explained.rst @@ -28,4 +28,4 @@ in ``Propulate``. overview_pop eas pso - cmaes \ No newline at end of file + cmaes diff --git a/docs/cmaes.rst b/docs/cmaes.rst index b6644c52..5966a70a 100644 --- a/docs/cmaes.rst +++ b/docs/cmaes.rst @@ -4,4 +4,4 @@ Covariance Matrix Adaption Evolution Strategy ============================================= .. note:: - The documentation for Covariance Matrix Adaption Evolution Strategy (CMA-ES) will be available soon. \ No newline at end of file + The documentation for Covariance Matrix Adaption Evolution Strategy (CMA-ES) will be available soon. diff --git a/docs/eas.rst b/docs/eas.rst index faaeecf8..b6a00441 100644 --- a/docs/eas.rst +++ b/docs/eas.rst @@ -68,4 +68,4 @@ Mutation :width: 76 % :align: center -| \ No newline at end of file +| diff --git a/docs/install.rst b/docs/install.rst index fe97aba8..e3e40478 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -30,7 +30,7 @@ with ``pip`` or via ``setup.py``: $ git clone https://github.com/Helmholtz-AI-Energy/propulate $ pip install -e . - + Alternatively: .. code-block:: console diff --git a/docs/island_model.rst b/docs/island_model.rst index 73fcbc73..e919bbec 100644 --- a/docs/island_model.rst +++ b/docs/island_model.rst @@ -41,4 +41,4 @@ from island 2. :width: 100 % :align: center -| \ No newline at end of file +| diff --git a/docs/overview_pop.rst b/docs/overview_pop.rst index 7b5de319..518d8f0f 100644 --- a/docs/overview_pop.rst +++ b/docs/overview_pop.rst @@ -51,4 +51,4 @@ Cuckoo Search (CS) Cuckoo search simulates the behavior of cuckoo birds' reproduction strategy. Each solution corresponds to a cuckoo egg, and each nest represents a potential solution. Cuckoos lay eggs in nests, and the nests with better eggs (solutions) survive and reproduce. The algorithm involves creating new solutions by combining existing ones and - occasionally replacing solutions to maintain diversity. \ No newline at end of file + occasionally replacing solutions to maintain diversity. diff --git a/docs/parallel_pop.rst b/docs/parallel_pop.rst index 724dbbe5..a3520f60 100644 --- a/docs/parallel_pop.rst +++ b/docs/parallel_pop.rst @@ -56,4 +56,4 @@ Worker 2 receives light the blue :math:`\small\mathrm{ind}_{g3}` only after fini :width: 100 % :align: center -| \ No newline at end of file +| diff --git a/docs/pop.rst b/docs/pop.rst index d9fe7c6a..c3ee9fb2 100644 --- a/docs/pop.rst +++ b/docs/pop.rst @@ -32,4 +32,4 @@ HOW? .. warning:: As with all metaheuristics, these algorithms have hyperparameters themselves and their effectiveness strongly depends - on choosing proper hyperparameters and problem-specific considerations. \ No newline at end of file + on choosing proper hyperparameters and problem-specific considerations. diff --git a/docs/pso.rst b/docs/pso.rst index c393557c..05ba149d 100644 --- a/docs/pso.rst +++ b/docs/pso.rst @@ -4,4 +4,4 @@ Particle Swarm Optimization =========================== .. note:: - The documentation for particle swarm optimization (PSO) will be available soon. \ No newline at end of file + The documentation for particle swarm optimization (PSO) will be available soon. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2795f526..dbec9ced 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -66,4 +66,4 @@ documentation |:raised_hands:|. .. Links -.. _subfolder: https://github.com/Helmholtz-AI-Energy/propulate/tree/master/tutorials \ No newline at end of file +.. _subfolder: https://github.com/Helmholtz-AI-Energy/propulate/tree/master/tutorials diff --git a/docs/requirements.txt b/docs/requirements.txt index 7fbf53e4..802fef4f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ sphinx-autoapi==3.0.0 sphinx-rtd-theme==1.2.0 sphinxcontrib-napoleon -sphinxemoji \ No newline at end of file +sphinxemoji diff --git a/docs/tut_islands.rst b/docs/tut_islands.rst index 02a7c6ed..14d8a507 100644 --- a/docs/tut_islands.rst +++ b/docs/tut_islands.rst @@ -145,4 +145,3 @@ With ten MPI ranks and two islands with five workers each, the output looks like [2024-03-13 12:30:46,611][propulate.propulator][INFO] - Top 1 result(s) on island 0: (1): [{'a': '-2.83E-4', 'b': '1.04E-3'}, loss 1.16E-6, island 0, worker 3, generation 901] - diff --git a/docs/tut_multi_rank_worker.rst b/docs/tut_multi_rank_worker.rst index 2db2fabc..0f2d9a3f 100644 --- a/docs/tut_multi_rank_worker.rst +++ b/docs/tut_multi_rank_worker.rst @@ -11,4 +11,4 @@ In addition to the already explained functionality, ``Propulate`` enables using parallelized evaluation of the loss function. This is useful for, e.g., data-parallel training of neural networks during the hyperparameter optimization, where each individual network is trained on multiple GPUs. -A more detailed explanation of the tutorial will be available soon |:rocket:|. \ No newline at end of file +A more detailed explanation of the tutorial will be available soon |:rocket:|. diff --git a/docs/tut_propulator.rst b/docs/tut_propulator.rst index d7e1d0fb..4cd705a8 100644 --- a/docs/tut_propulator.rst +++ b/docs/tut_propulator.rst @@ -88,7 +88,7 @@ As the very first step, we need to define the key ingredients that define the op ---------- params: Dict[str, float] The function parameters. - + Returns ------- float @@ -223,4 +223,4 @@ will use your current working directory. .. warning:: If you start an optimization run from existing checkpoints, those checkpoints must be compatible with your current parallel computing environment. This means that if you use a checkpoint created in a setting with 20 processing - elements in a different computing environment with, e.g., 10 processing elements, the behaviour is undefined. \ No newline at end of file + elements in a different computing environment with, e.g., 10 processing elements, the behaviour is undefined. diff --git a/docs/usage.rst b/docs/usage.rst index 876b4b6c..d61cfda9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -15,4 +15,4 @@ If you want to use ``Propulate`` for your own applications, you can use these sc tut_multi_rank_worker .. Links -.. _Github: https://github.com/Helmholtz-AI-Energy/propulate \ No newline at end of file +.. _Github: https://github.com/Helmholtz-AI-Energy/propulate diff --git a/propulate/__init__.py b/propulate/__init__.py index 68384b3a..172eef6e 100644 --- a/propulate/__init__.py +++ b/propulate/__init__.py @@ -10,16 +10,15 @@ finally: del get_distribution, DistributionNotFound +from . import propagators from .islands import Islands -from .population import Individual, Particle -from .propulator import Propulator from .migrator import Migrator from .pollinator import Pollinator +from .population import Individual, Particle +from .propulator import Propulator from .surrogate import Surrogate from .utils import get_default_propagator, set_logger_config -from . import propagators - __all__ = [ "Islands", "Individual", diff --git a/propulate/islands.py b/propulate/islands.py index 4207c170..d6a846f1 100644 --- a/propulate/islands.py +++ b/propulate/islands.py @@ -10,7 +10,7 @@ from .migrator import Migrator from .pollinator import Pollinator from .population import Individual -from .propagators import Propagator, SelectMin, SelectMax +from .propagators import Propagator, SelectMax, SelectMin from .surrogate import Surrogate log = logging.getLogger(__name__) # Get logger instance. diff --git a/propulate/migrator.py b/propulate/migrator.py index 17c7edf4..313230c5 100644 --- a/propulate/migrator.py +++ b/propulate/migrator.py @@ -1,17 +1,16 @@ import copy -import random import logging +import random from pathlib import Path from typing import Callable, Generator, Optional, Type, Union import numpy as np from mpi4py import MPI +from ._globals import MIGRATION_TAG, SYNCHRONIZATION_TAG from .propagators import Propagator, SelectMin from .propulator import Propulator from .surrogate import Surrogate -from ._globals import MIGRATION_TAG, SYNCHRONIZATION_TAG - log = logging.getLogger(__name__) diff --git a/propulate/pollinator.py b/propulate/pollinator.py index 99ec8e55..89b05b87 100644 --- a/propulate/pollinator.py +++ b/propulate/pollinator.py @@ -10,7 +10,7 @@ from ._globals import MIGRATION_TAG, SYNCHRONIZATION_TAG from .population import Individual -from .propagators import Propagator, SelectMin, SelectMax +from .propagators import Propagator, SelectMax, SelectMin from .propulator import Propulator from .surrogate import Surrogate diff --git a/propulate/population.py b/propulate/population.py index 8100c893..253a70dd 100644 --- a/propulate/population.py +++ b/propulate/population.py @@ -4,9 +4,7 @@ class Individual(dict): - """ - An individual represents a candidate solution to the considered optimization problem. - """ + """An individual represents a candidate solution to the considered optimization problem.""" def __init__(self, generation: int = -1, rank: int = -1) -> None: """ @@ -14,10 +12,10 @@ def __init__(self, generation: int = -1, rank: int = -1) -> None: Parameters ---------- - generation: int - current generation (-1 if unset) - rank: int - rank (-1 if unset) + generation : int + The current generation (-1 if unset). + rank : int + The rank (-1 if unset). """ super(Individual, self).__init__(list()) self.generation = generation # Equals each worker's iteration for continuous population in Propulate. @@ -32,9 +30,7 @@ def __init__(self, generation: int = -1, rank: int = -1) -> None: self.evalperiod = None # evaluation duration def __repr__(self) -> str: - """ - String representation of an ``Individual`` instance. - """ + """Return string representation of an ``Individual`` instance.""" rep = { key: ( f"{Decimal(self[key]):.2E}" @@ -139,13 +135,12 @@ def equals(self, other) -> bool: class Particle(Individual): """ - Child class of ``Individual`` with additional properties required for PSO, i.e., an array-type velocity field and - a (redundant) array-type position field. - - Note that Propulate relies on ``Individual``s being dictionaries. + Child class of ``Individual`` with additional properties required for PSO. - When defining new propagators, users of the ``Particle`` class thus need to ensure that a ``Particle``'s position - always matches its dict contents and vice versa. + Particles additionally feature an array-type velocity field and a (redundant) array-type position field. + Note that Propulate relies on individuals being dictionaries. When defining new propagators, users of the + ``Particle`` class thus need to ensure that a ``Particle``'s position always matches its dict contents and vice + versa. This class also contains an attribute field called ``global_rank``. It contains the global rank of the propagator that created it. This is for purposes of better (or at all) retrieval in multi swarm case. diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 1293e952..f6c81b14 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -1,39 +1,36 @@ """This package bundles all classes that are used as propagators in Propulate's optimization routine.""" from .base import ( - Propagator, - Stochastic, - Conditional, Compose, - SelectMin, + Conditional, + InitUniform, + Propagator, SelectMax, + SelectMin, SelectUniform, - InitUniform, + Stochastic, +) +from .cmaes import ( + ActiveCMA, + BasicCMA, + CMAAdapter, + CMAParameter, + CMAPropagator, ) - from .ga import ( - PointMutation, - RandomPointMutation, - IntervalMutationNormal, - CrossoverUniform, CrossoverMultiple, CrossoverSigmoid, + CrossoverUniform, + IntervalMutationNormal, + PointMutation, + RandomPointMutation, ) - from .pso import ( BasicPSO, - VelocityClampingPSO, - ConstrictionPSO, CanonicalPSO, + ConstrictionPSO, StatelessPSO, -) - -from .cmaes import ( - CMAAdapter, - CMAParameter, - CMAPropagator, - BasicCMA, - ActiveCMA, + VelocityClampingPSO, ) __all__ = [ diff --git a/propulate/propagators/base.py b/propulate/propagators/base.py index a5402731..7f711dd8 100644 --- a/propulate/propagators/base.py +++ b/propulate/propagators/base.py @@ -1,5 +1,5 @@ import random -from typing import List, Dict, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union from ..population import Individual diff --git a/propulate/propagators/cmaes.py b/propulate/propagators/cmaes.py index c3107202..cd0b5d0c 100644 --- a/propulate/propagators/cmaes.py +++ b/propulate/propagators/cmaes.py @@ -1,10 +1,10 @@ import random -from typing import List, Dict, Optional, Tuple +from typing import Dict, List, Optional, Tuple import numpy as np -from .base import Propagator, SelectMax, SelectMin, SelectUniform from ..population import Individual +from .base import Propagator, SelectMax, SelectMin, SelectUniform class CMAParameter: @@ -268,8 +268,7 @@ def _decompose_co_matrix(self, new_co_matrix: np.ndarray) -> None: def _limit_condition(self, limit: float) -> None: """ - Limit the condition (square of ratio largest to smallest eigenvalue) of the covariance matrix if it exceeds a - threshold. + Limit the condition (squared ratio largest / smallest eigenvalue) of the cov. matrix if it exceeds a threshold. Credits on how to limit the condition: https://github.com/CMA-ES/pycma/blob/development/cma/sampler.py @@ -795,14 +794,14 @@ def __init__( def __call__(self, inds: List[Individual]) -> Individual: """ - The skeleton of the CMA-ES algorithm using the template method design pattern. + Define the skeleton of the CMA-ES algorithm using the template method design pattern. Sampling individuals and adapting the strategy parameters. Template methods are ``update_mean``, ``update_covariance_matrix``, and ``update_step_size``. Parameters ---------- - inds: List[propulate.Individual] + inds : List[propulate.Individual] Available individuals. Returns diff --git a/propulate/propagators/ga.py b/propulate/propagators/ga.py index 4f136142..a67949ab 100644 --- a/propulate/propagators/ga.py +++ b/propulate/propagators/ga.py @@ -4,8 +4,8 @@ import numpy as np -from .base import Stochastic from ..population import Individual +from .base import Stochastic class PointMutation(Stochastic): @@ -310,8 +310,8 @@ def __call__(self, ind: Individual) -> Individual: for i in to_mutate: min_val, max_val = self.limits[i] # Determine interval boundaries. sigma = ( - max_val - min_val - ) * self.sigma_factor # Determine std from interval boundaries and sigma factor. + (max_val - min_val) * self.sigma_factor + ) # Determine std from interval boundaries and sigma factor. ind[i] = self.rng.gauss( ind[i], sigma ) # Sample new value from Gaussian centered around current value. diff --git a/propulate/propagators/pso.py b/propulate/propagators/pso.py index 8fe1999f..cc54bc9f 100644 --- a/propulate/propagators/pso.py +++ b/propulate/propagators/pso.py @@ -1,11 +1,11 @@ import logging from random import Random -from typing import Dict, Tuple, Union, List +from typing import Dict, List, Tuple, Union import numpy as np +from ..population import Individual, Particle from ..propagators import Propagator, Stochastic -from ..population import Particle, Individual from ..utils import make_particle @@ -126,6 +126,8 @@ def _prepare_data( self, individuals: List[Individual] ) -> Tuple[Particle, Particle, Particle]: """ + Get the particle to be updated on this rank, its current personal best, and the swarm's current global best. + Given a list of ``Individual`` or ``Particle`` objects, determine the particle to be updated on this rank, its current personal best, and the currently known global best of the swarm to perform a particle update step. @@ -598,12 +600,10 @@ def __call__(self, individuals: List[Individual]) -> Particle: class StatelessPSO(Propagator): """ - This propagator performs PSO without the need of Particles, but as a consequence, also without velocity. - Thus, it is called stateless. - - As this propagator works without velocity, there is also no inertia weight used. + The stateless propagator performs PSO without the need of Particles, but as a consequence, also without velocity. - It uses only classes provided by vanilla Propulate. + As this propagator works without velocity, there is also no inertia weight used. It uses only classes provided by + vanilla Propulate. Attributes ---------- diff --git a/propulate/propulator.py b/propulate/propulator.py index e73fa765..d0ffad8d 100644 --- a/propulate/propulator.py +++ b/propulate/propulator.py @@ -1,10 +1,10 @@ import copy +import inspect import logging import os import pickle import random import time -import inspect from operator import attrgetter from pathlib import Path from typing import Callable, Final, Generator, List, Optional, Tuple, Type, Union diff --git a/propulate/surrogate.py b/propulate/surrogate.py index 29ea4edd..3854db04 100644 --- a/propulate/surrogate.py +++ b/propulate/surrogate.py @@ -1,5 +1,5 @@ -from typing import TypeVar, Generic from abc import ABC, abstractmethod +from typing import Generic, TypeVar from propulate.population import Individual @@ -36,13 +36,11 @@ def __init__(self) -> None: @abstractmethod def start_run(self, ind: Individual) -> None: """ - Signals that a new run is about to start. - This is called before the first yield from the ``loss_fn``. - It is assumed that the individual is freshly created. - Keep in mind that there might be (private) keys - that are not related to limits, like the surrogate key '_s'; - key names related to limits could be provided to - the Surrogate constructor if necessary. + Signal that a new run is about to start. + + This is called before the first yield from the ``loss_fn``. It is assumed that the individual is freshly + created. Keep in mind that there might be (private) keys that are not related to limits, like the surrogate key + ``_s``; key names related to limits could be provided to the ``Surrogate`` constructor if necessary. Parameters ---------- @@ -55,6 +53,7 @@ def start_run(self, ind: Individual) -> None: def update(self, loss: float) -> None: """ Update the surrogate model with the final loss. + Indicative that the current run has finished. Parameters @@ -68,6 +67,7 @@ def update(self, loss: float) -> None: def cancel(self, loss: float) -> bool: """ Evaluate surrogate to check if the current run should be cancelled. + This will be called after every yield from the ``loss_fn``. Parameters @@ -86,34 +86,29 @@ def cancel(self, loss: float) -> bool: @abstractmethod def merge(self, data: T) -> None: """ - Merges the results of another surrogate model into itself. + Merge the results of another surrogate model into itself. + Used to synchronize surrogate models from different Propulators. - Implementation of merge has to be commutative! - Otherwise the different surrogate models will diverge. + Implementation of merge has to be commutative! Otherwise, the different surrogate models will diverge. Parameters ---------- data : T - All relevant information to update its model - to the same state as the origin of the data. + All relevant information to update its model to the same state as the origin of the data. """ pass @abstractmethod def data(self) -> T: """ - Returns all relevant information about the surrogate model - for merging with another surrogate + Return all relevant information about the surrogate model for merging with another surrogate. - It most likely only needs to return the most recent loss - from ``update()``. + It most likely only needs to return the most recent loss from ``update()``. Returns ------- T - All relevant information to convey the current state - of the surrogate model. - + All relevant information to convey the current state of the surrogate model. """ pass diff --git a/propulate/utils.py b/propulate/utils.py index 6efaa117..52b05436 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -3,7 +3,7 @@ import random import sys from pathlib import Path -from typing import Dict, Optional, Union, Tuple +from typing import Dict, Optional, Tuple, Union import colorlog import numpy as np @@ -13,9 +13,9 @@ from .propagators import ( Compose, Conditional, + CrossoverUniform, InitUniform, IntervalMutationNormal, - CrossoverUniform, PointMutation, Propagator, SelectMin, @@ -164,7 +164,7 @@ def make_particle(individual: Individual) -> Particle: An individual to be converted to a particle. Returns - -------- + ------- propulate.Particle The converted individual. """ diff --git a/tests/test_cmaes.py b/tests/test_cmaes.py index bb0ac39b..34638d39 100644 --- a/tests/test_cmaes.py +++ b/tests/test_cmaes.py @@ -5,12 +5,12 @@ import numpy as np from propulate import Propulator -from propulate.propagators import CMAPropagator, BasicCMA +from propulate.propagators import BasicCMA, CMAPropagator def sphere(params: Dict[str, float]) -> float: """ - Sphere function: continuous, convex, separable, differentiable, unimodal + Sphere function: continuous, convex, separable, differentiable, unimodal. Input domain: -5.12 <= x, y <= 5.12 Global minimum 0 at (x, y) = (0, 0) diff --git a/tests/test_propulator_sphere.py b/tests/test_propulator_sphere.py index d843d5f0..d7b8e97a 100644 --- a/tests/test_propulator_sphere.py +++ b/tests/test_propulator_sphere.py @@ -1,7 +1,7 @@ +import logging import random import tempfile from typing import Dict -import logging import numpy as np @@ -11,7 +11,7 @@ def sphere(params: Dict[str, float]) -> float: """ - Sphere function: continuous, convex, separable, differentiable, unimodal + Sphere function: continuous, convex, separable, differentiable, unimodal. Input domain: -5.12 <= x, y <= 5.12 Global minimum 0 at (x, y) = (0, 0) diff --git a/tests/test_pso.py b/tests/test_pso.py index e89289f9..a19da15d 100644 --- a/tests/test_pso.py +++ b/tests/test_pso.py @@ -2,8 +2,8 @@ import tempfile from typing import Dict -from mpi4py import MPI import numpy as np +from mpi4py import MPI from propulate import Propulator from propulate.propagators import Conditional @@ -12,7 +12,7 @@ def sphere(params: Dict[str, float]) -> float: """ - Sphere function: continuous, convex, separable, differentiable, unimodal + Sphere function: continuous, convex, separable, differentiable, unimodal. Input domain: -5.12 <= x, y <= 5.12 Global minimum 0 at (x, y) = (0, 0) @@ -21,6 +21,7 @@ def sphere(params: Dict[str, float]) -> float: ---------- params : Dict[str, float] The function parameters. + Returns ------- float diff --git a/tutorials/cmaes_example.py b/tutorials/cmaes_example.py index aca7765f..02a1628b 100644 --- a/tutorials/cmaes_example.py +++ b/tutorials/cmaes_example.py @@ -1,14 +1,13 @@ -"""Simple example script using CMA-ES""" -import random +"""Simple example script using CMA-ES.""" import pathlib +import random +from function_benchmark import get_function_search_space, parse_arguments from mpi4py import MPI from propulate import Propulator -from propulate.propagators import BasicCMA, ActiveCMA, CMAPropagator +from propulate.propagators import ActiveCMA, BasicCMA, CMAPropagator from propulate.utils import set_logger_config -from function_benchmark import get_function_search_space, parse_arguments - if __name__ == "__main__": comm = MPI.COMM_WORLD diff --git a/tutorials/function_benchmark.py b/tutorials/function_benchmark.py index 8a704d40..05d277b2 100644 --- a/tutorials/function_benchmark.py +++ b/tutorials/function_benchmark.py @@ -3,8 +3,8 @@ import logging from typing import Callable, Dict, Tuple -from mpi4py import MPI import numpy as np +from mpi4py import MPI def rosenbrock(params: Dict[str, float]) -> float: @@ -83,7 +83,7 @@ def quartic(params: Dict[str, float]) -> float: def rastrigin(params: Dict[str, float]) -> float: """ - Rastrigin function: continuous, non-convex, separable, differentiable, multimodal + Rastrigin function: continuous, non-convex, separable, differentiable, multimodal. A non-linear and highly multimodal function. Its surface is determined by two external variables, controlling the modulation’s amplitude and frequency. The local minima are located at a rectangular grid with size 1. @@ -137,7 +137,7 @@ def griewank(params: Dict[str, float]) -> float: def schwefel(params: Dict[str, float]) -> float: """ - Schwefel 2.20 function: continuous, convex, separable, non-differentiable, non-multimodal + Schwefel 2.20 function: continuous, convex, separable, non-differentiable, non-multimodal. This function has a second-best minimum far away from the global optimum. @@ -238,7 +238,7 @@ def birastrigin(params: Dict[str, float]) -> float: def bukin_n6(params: Dict[str, float]) -> float: """ - Bukin N.6 function: continuous, convex, non-separable, non-differentiable, multimodal + Bukin N.6 function: continuous, convex, non-separable, non-differentiable, multimodal. Input domain: -15 <= x <= -5, -3 <= y <= 3 Global minimum 0 at (x, y) = (-10, 1) @@ -261,7 +261,7 @@ def bukin_n6(params: Dict[str, float]) -> float: def egg_crate(params: Dict[str, float]) -> float: """ - Egg-crate function: continuous, non-convex, separable, differentiable, multimodal + Egg-crate function: continuous, non-convex, separable, differentiable, multimodal. Input domain: -5 <= x, y <= 5 Global minimum -1 at (x, y) = (0, 0) @@ -286,7 +286,7 @@ def egg_crate(params: Dict[str, float]) -> float: def himmelblau(params: Dict[str, float]) -> float: """ - Himmelblau function: continuous, non-convex, non-separable, differentiable, multimodal + Himmelblau function: continuous, non-convex, non-separable, differentiable, multimodal. Input domain: -6 <= x, y <= 6 Global minimum 0 at (x, y) = (3, 2) @@ -309,13 +309,13 @@ def himmelblau(params: Dict[str, float]) -> float: def keane(params: Dict[str, float]) -> float: """ - Keane function: continuous, non-convex, non-separable, differentiable, multimodal + Keane function: continuous, non-convex, non-separable, differentiable, multimodal. Input domain: -10 <= x, y <= 10 Global minimum 0.6736675 at (x, y) = (1.3932491, 0) and (x, y) = (0, 1.3932491) Parameters - ------ + ---------- params : Dict[str, float] The function parameters. @@ -326,7 +326,7 @@ def keane(params: Dict[str, float]) -> float: """ params = np.array(list(params.values())) return ( - -np.sin(params[0] - params[1]) ** 2 + -(np.sin(params[0] - params[1]) ** 2) * np.sin(params[0] + params[1]) ** 2 / np.sqrt(params[0] ** 2 + params[1] ** 2) ) @@ -334,7 +334,7 @@ def keane(params: Dict[str, float]) -> float: def leon(params: Dict[str, float]) -> float: """ - Leon function: continuous, non-convex, non-separable, differentiable, non-multimodal, non-random, non-parametric + Leon function: continuous, non-convex, non-separable, differentiable, non-multimodal, non-random, non-parametric. Input domain: 0 <= x, y <= 10 Global minimum 0 at (x, y) =(1, 1) @@ -355,7 +355,7 @@ def leon(params: Dict[str, float]) -> float: def sphere(params: Dict[str, float]) -> float: """ - Sphere function: continuous, convex, separable, differentiable, unimodal + Sphere function: continuous, convex, separable, differentiable, unimodal. Input domain: -5.12 <= x, y <= 5.12 Global minimum 0 at (x, y) = (0, 0) @@ -694,10 +694,7 @@ def parse_arguments( } class ParamSettingCatcher(argparse.Action): - """ - This class extends ``argparse``'s ``Action`` class in order to allow for an action that logs if one of the PSO - hyperparameters was actually set. - """ + """Extend ``argparse``'s ``Action`` class to enable logging if one of the PSO hyperparameters was set.""" def __call__(self, parser, namespace, values, option_string=None): hp_set[self.dest] = True diff --git a/tutorials/islands_example.py b/tutorials/islands_example.py index 71720391..9799c45a 100755 --- a/tutorials/islands_example.py +++ b/tutorials/islands_example.py @@ -3,12 +3,12 @@ import random import numpy as np +from function_benchmark import get_function_search_space, parse_arguments from mpi4py import MPI from propulate import Islands -from propulate.propagators import SelectMin, SelectMax +from propulate.propagators import SelectMax, SelectMin from propulate.utils import get_default_propagator, set_logger_config -from function_benchmark import get_function_search_space, parse_arguments if __name__ == "__main__": comm = MPI.COMM_WORLD diff --git a/tutorials/minimum_working_example.py b/tutorials/minimum_working_example.py index 1b5e081f..f7b07635 100644 --- a/tutorials/minimum_working_example.py +++ b/tutorials/minimum_working_example.py @@ -1,8 +1,10 @@ """Minimum working example showing how to use Propulate.""" -import propulate -from mpi4py import MPI import random +from mpi4py import MPI + +import propulate + # Set the communicator and the optimization parameters comm = MPI.COMM_WORLD rng = random.Random(MPI.COMM_WORLD.rank) diff --git a/tutorials/multi_rank_workers_example.py b/tutorials/multi_rank_workers_example.py index 446d46f0..c72937d9 100644 --- a/tutorials/multi_rank_workers_example.py +++ b/tutorials/multi_rank_workers_example.py @@ -1,20 +1,21 @@ """ Multi-rank workers in Propulate using the example of a parallel sphere function. + Tested with 8 processes overall, 2 islands, and 2 ranks per worker, where each worker calculates one of the squared terms in the (in this case) two-dimensional sphere function. In general, the parallel sphere function's dimension should equal the number of ranks per worker. """ -import random import pathlib +import random from typing import Dict import numpy as np +from function_benchmark import parse_arguments from mpi4py import MPI from propulate import Islands -from propulate.propagators import SelectMin, SelectMax +from propulate.propagators import SelectMax, SelectMin from propulate.utils import get_default_propagator, set_logger_config -from function_benchmark import parse_arguments def parallel_sphere(params: Dict[str, float], comm: MPI.Comm = MPI.COMM_SELF) -> float: diff --git a/tutorials/propulator_example.py b/tutorials/propulator_example.py index f0d13cf3..c58d7853 100755 --- a/tutorials/propulator_example.py +++ b/tutorials/propulator_example.py @@ -1,11 +1,12 @@ """Simple Propulator example script using the default genetic propagator.""" -import random import pathlib +import random +from function_benchmark import get_function_search_space, parse_arguments from mpi4py import MPI + from propulate import Propulator from propulate.utils import get_default_propagator, set_logger_config -from function_benchmark import get_function_search_space, parse_arguments if __name__ == "__main__": comm = MPI.COMM_WORLD diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index 5f70f576..51015ec7 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -1,22 +1,24 @@ """ -This files contains an example use case for the PSO propagators. Here, you can choose between benchmark functions and -optimize them. The example shows how to set up Propulate in order to use it with PSO. +Example use case for the PSO propagators. + +You can choose between benchmark functions and optimize them. The example shows how to set up Propulate in order to use +it with PSO. """ import pathlib import random +from function_benchmark import get_function_search_space, parse_arguments from mpi4py import MPI -from propulate import set_logger_config, Propulator +from propulate import Propulator, set_logger_config from propulate.propagators import Conditional, Propagator from propulate.propagators.pso import ( BasicPSO, - VelocityClampingPSO, - ConstrictionPSO, CanonicalPSO, + ConstrictionPSO, InitUniformPSO, + VelocityClampingPSO, ) -from function_benchmark import get_function_search_space, parse_arguments if __name__ == "__main__": comm = MPI.COMM_WORLD diff --git a/tutorials/surrogate/__init__.py b/tutorials/surrogate/__init__.py index 410175ab..4295c527 100644 --- a/tutorials/surrogate/__init__.py +++ b/tutorials/surrogate/__init__.py @@ -1,7 +1,7 @@ -from .mock_surrogate import MockSurrogate +from .dynamic_surrogate import DynamicSurrogate from .log_surrogate import LogSurrogate +from .mock_surrogate import MockSurrogate from .static_surrogate import StaticSurrogate -from .dynamic_surrogate import DynamicSurrogate __all__ = [ "MockSurrogate", diff --git a/tutorials/surrogate/cifar10_example.py b/tutorials/surrogate/cifar10_example.py index 2a45240e..7db00c99 100755 --- a/tutorials/surrogate/cifar10_example.py +++ b/tutorials/surrogate/cifar10_example.py @@ -1,26 +1,22 @@ +import enum import logging -import random import os +import random import sys -import enum -from typing import Union, Dict, Tuple, Generator +from typing import Dict, Generator, Tuple, Union import torch +from mpi4py import MPI from torch import nn from torch.utils.data import DataLoader, TensorDataset - -from torchvision.datasets import CIFAR10 -from torchvision.transforms import Compose, ToTensor, Normalize from torchmetrics import Accuracy +from torchvision.datasets import CIFAR10 +from torchvision.transforms import Compose, Normalize, ToTensor -from mpi4py import MPI - +import tutorials.surrogate from propulate import Islands from propulate.utils import get_default_propagator -import tutorials.surrogate - - GPUS_PER_NODE: int = 1 log_path = "torch_ckpts" @@ -32,8 +28,8 @@ class Permutation(enum.Enum): """ Enum class for permutations of layers in convolution block. - Each letter represents a layer in the block. - The default permutation is ABC. + + Each letter represents a layer in the block. The default permutation is "ABC". """ ABC = (0, 1, 2) @@ -90,7 +86,8 @@ def conv_block( pool: bool = False, ) -> nn.Sequential: """ - Create a ResNet block. The block consists of the convolution layer, batch normalization, and ReLU activation function. + Create a ResNet block, consisting of the convolution layer, batch normalization, and ReLU activation function. + The order of these layers can be shuffled according to a given permutation. Parameters @@ -131,6 +128,8 @@ def conv_block( class Net(nn.Module): + """Residual neural network class.""" + def __init__( self, in_channels: int, @@ -437,9 +436,7 @@ def ind_loss( def set_seeds(seed_value=42): - """ - Set seed for reproducibility. - """ + """Set seed for reproducibility.""" random.seed(seed_value) # Python random module torch.manual_seed(seed_value) # pytorch random number generator for CPU torch.cuda.manual_seed(seed_value) # pytorch random number generator for all GPUs diff --git a/tutorials/surrogate/dynamic_surrogate.py b/tutorials/surrogate/dynamic_surrogate.py index 0a02c524..b94e84b9 100644 --- a/tutorials/surrogate/dynamic_surrogate.py +++ b/tutorials/surrogate/dynamic_surrogate.py @@ -1,8 +1,10 @@ import random +from typing import Dict, Tuple, Union + import GPy -from mpi4py import MPI import numpy as np -from typing import Tuple, Dict, Union +from mpi4py import MPI + from propulate import Surrogate from propulate.population import Individual @@ -10,8 +12,8 @@ # Gaussian Process Regression with an Exponential Decay Kernel Surrogate class DynamicSurrogate(Surrogate): """ - Surrogate model using a two-step Gaussian Process Regression - to predict the final loss of a configuration. + Surrogate model using a two-step Gaussian Process Regression to predict the final loss of a configuration. + The first global model is trained on the configurations with their final losses. The second local model is trained on the interim losses of the current run. The local model uses the global model's prediction as the mean function. @@ -85,9 +87,10 @@ def __init__( ], ) -> None: """ - Initialize the DynamicSurrogate with the configuration space limits. - Set the global and local kernels for Gaussian Process Regression. - All other needed attributes are initialized as empty arrays or None. + Initialize a dynamic surrogate with the configuration space limits. + + Set the global and local kernels for Gaussian process regression. All other needed attributes are initialized as + empty arrays or None. Parameters ---------- @@ -130,12 +133,11 @@ def __init__( def start_run(self, ind: Individual) -> None: """ - Encode the configuration given as individual and create the mean function - for the local model by using the global model's prediction. + Encode the config. given as individual and create the local GP's mean function from the global GP's prediction. Parameters ---------- - ind : Individual + ind : propulate.Individual The individual containing the current configuration. """ self.current_encoding = self.encode_configuration(ind) @@ -155,8 +157,7 @@ def start_run(self, ind: Individual) -> None: def update(self, loss: float) -> None: """ - Update the model with the final loss of the current run - and retrain the global Gaussian Process with the new data. + Update the model with the current run's final loss and retrain the global Gaussian Process with the new data. Parameters ---------- @@ -202,8 +203,7 @@ def update(self, loss: float) -> None: def cancel(self, loss: float) -> bool: """ - Cancel the current run if the local Gaussian Process predicts a final loss - that is higher than the best known loss with an allowed margin. + Cancel the run if the local GP predicts a final loss higher than the best known loss with an allowed margin. Parameters ---------- @@ -255,8 +255,7 @@ def cancel(self, loss: float) -> bool: def merge(self, data: Tuple[np.ndarray, float]) -> None: """ - Merge a configuration and loss tuple into the history arrays. - Then retrain the global Gaussian Process with the new data. + Merge a configuration and loss tuple into the history arrays and retrain the global GP with the new data. Parameters ---------- @@ -314,8 +313,7 @@ def _create_encoding( ], ) -> Dict[str, Dict[str, int]]: """ - Create a mapping from categorical values to integers. - Gaussian Process Regression only accepts numerical values. + Create a mapping from categorical values to integers. Gaussian Process Regression only accepts numerical values. Parameters ---------- @@ -343,8 +341,7 @@ def encode_configuration( ], ) -> np.ndarray: """ - Encode a configuration dictionary into a 2D array. - This is the format required by the Gaussian Process Regression model. + Encode a configuration dictionary into a 2D array (required by the Gaussian Process Regression model). Parameters ---------- diff --git a/tutorials/surrogate/log_surrogate.py b/tutorials/surrogate/log_surrogate.py index 55f25c60..87ad3679 100644 --- a/tutorials/surrogate/log_surrogate.py +++ b/tutorials/surrogate/log_surrogate.py @@ -4,11 +4,10 @@ class LogSurrogate(Surrogate): """ - Surrogate model with just a few debug prints to see what's going on - when debugging. Prints once for every method call. - Additionally it keeps track of the best final loss so far - and an id that increments for every cancel call. - This makes it easier to read the debug prints. + Surrogate model with just a few debug prints to see what's going on when debugging. + + Prints once for every method call. Additionally, it keeps track of the best final loss so far and an id that + increments for every cancel call. This makes it easier to read the debug prints. Attributes ---------- @@ -40,9 +39,7 @@ class LogSurrogate(Surrogate): """ def __init__(self) -> None: - """ - Initialize the LogSurrogate with a synthetic id and a high best loss. - """ + """Initialize the ``LogSurrogate`` with a synthetic id and a high best loss.""" super().__init__() self.synthetic_id: int = 0 self.best_loss: float = 10000.0 diff --git a/tutorials/surrogate/mnist_example.py b/tutorials/surrogate/mnist_example.py index 47c2969a..a444aa65 100755 --- a/tutorials/surrogate/mnist_example.py +++ b/tutorials/surrogate/mnist_example.py @@ -1,25 +1,21 @@ import logging -import random import os +import random import sys -from typing import Union, Dict, Tuple, Generator +from typing import Dict, Generator, Tuple, Union import torch +from mpi4py import MPI from torch import nn from torch.utils.data import DataLoader, TensorDataset - -from torchvision.datasets import MNIST -from torchvision.transforms import Compose, ToTensor, Normalize from torchmetrics import Accuracy +from torchvision.datasets import MNIST +from torchvision.transforms import Compose, Normalize, ToTensor -from mpi4py import MPI - +import tutorials.surrogate from propulate import Islands from propulate.utils import get_default_propagator -import tutorials.surrogate - - GPUS_PER_NODE: int = 1 log_path = "torch_ckpts" @@ -29,6 +25,8 @@ class Net(nn.Module): + """Convolutional neural network class.""" + def __init__( self, conv_layers: int, activation: nn.Module, lr: float, loss_fn: nn.Module ) -> None: @@ -51,9 +49,7 @@ def __init__( self.lr = lr # Set learning rate. self.loss_fn = loss_fn # Set the loss function used for training the model. self.best_accuracy = 0.0 # Initialize the model's best validation accuracy. - layers = ( - [] - ) # Set up the model architecture (depending on number of convolutional layers specified). + layers = [] # Set up the model architecture (depending on number of convolutional layers specified). layers += [ nn.Sequential( nn.Conv2d(in_channels=1, out_channels=10, kernel_size=3, padding=1), @@ -93,9 +89,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.fc(x) return x - def training_step( - self, batch: Tuple[torch.Tensor, torch.Tensor] - ) -> torch.Tensor: + def training_step(self, batch: Tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: """ Calculate loss for training step in Lightning train loop. @@ -114,9 +108,7 @@ def training_step( loss_val = self.loss_fn(pred, y) return loss_val - def validation_step( - self, batch: Tuple[torch.Tensor, torch.Tensor] - ) -> torch.Tensor: + def validation_step(self, batch: Tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: """ Calculate loss for validation step in Lightning validation loop during training. diff --git a/tutorials/surrogate/mock_surrogate.py b/tutorials/surrogate/mock_surrogate.py index edc1a0f2..aab520b5 100644 --- a/tutorials/surrogate/mock_surrogate.py +++ b/tutorials/surrogate/mock_surrogate.py @@ -5,8 +5,8 @@ class MockSurrogate(Surrogate): """ This surrogate model does nothing. - It exists for testing purposes and comparing performance decline - due to the additional overhead of using a surrogate. + + It exists for testing purposes and comparing performance decline due to the overhead caused by using a surrogate. Methods ------- @@ -34,16 +34,21 @@ def __init__(self) -> None: super().__init__() def start_run(self, ind: Individual) -> None: + """Do nothing.""" pass def update(self, loss: float) -> None: + """Do nothing.""" pass def cancel(self, loss: float) -> bool: + """Never cancel.""" return False def merge(self, data: float) -> None: + """Do nothing.""" pass def data(self) -> float: + """Do nothing.""" return 0.0 diff --git a/tutorials/surrogate/static_surrogate.py b/tutorials/surrogate/static_surrogate.py index e4743774..a429f6e1 100644 --- a/tutorials/surrogate/static_surrogate.py +++ b/tutorials/surrogate/static_surrogate.py @@ -1,4 +1,5 @@ import numpy as np + from propulate import Surrogate from propulate.population import Individual @@ -6,12 +7,12 @@ class StaticSurrogate(Surrogate): """ Surrogate model using the best known run as baseline. - After the first run, each subsequent loss is compared to the baseline. - Every run with a loss outside the margin of the baseline is cancelled. - This model creates an internal index for the yielded losses during each run. - That means the yield order and frquency has to be consistent between runs. - Otherwise, the indices of the baseline run won't match. + After the first run, each subsequent loss is compared to the baseline. Every run with a loss outside the margin of + the baseline is cancelled. + + This model creates an internal index for the yielded losses during each run. That means the yield order and + frequency has to be consistent between runs. Otherwise, the indices of the baseline run won't match. Loosely based on the paper: Use of Static Surrogates in Hyperparameter Optimization @@ -22,7 +23,7 @@ class StaticSurrogate(Surrogate): synthetic_id : int An internal running index to keep track of the current runs loss index. margin : float - A margin that gets multiplied with the the new incoming losses to compare them to the baseline. + A margin to be multiplied with the new incoming losses to compare them to the baseline. first_run : bool Flag for the first complete run. baseline : np.ndarray @@ -45,7 +46,6 @@ class StaticSurrogate(Surrogate): data() Return the loss series of the best known run so far. - Notes ----- The ``StaticSurrogate`` class implements all methods from the ``Surrogate`` class. @@ -57,8 +57,7 @@ class StaticSurrogate(Surrogate): def __init__(self, margin: float = 0.8) -> None: """ - Initialize the StaticSurrogate with a synthetic id, a margin - and empty arrays for the baseline and the current run. + Initialize a static surrogate with a synthetic id, a margin, and empty arrays for baseline and current run. Parameters ---------- @@ -95,8 +94,7 @@ def start_run(self, ind: Individual) -> None: def update(self, loss: float) -> None: """ - Replace the baseline with the current run if the final loss is better, - or if there is no prior run. + Replace the baseline with the current run if the final loss is better, or if there is no prior run. Parameters ---------- @@ -144,8 +142,7 @@ def cancel(self, loss: float) -> bool: def merge(self, data: np.ndarray) -> None: """ - Replace the baseline with the incoming run if the final loss is better, - or if there is no prior run. + Replace the baseline with the incoming run if the final loss is better, or if there is no prior run. Parameters ---------- diff --git a/tutorials/torch_example.py b/tutorials/torch_example.py index d6ceb16d..850e9317 100755 --- a/tutorials/torch_example.py +++ b/tutorials/torch_example.py @@ -1,27 +1,26 @@ """ -Toy example for hyperparameter optimization / NAS in Propulate, using a simple convolutional network trained on the -MNIST dataset. +Toy example for HP optimization / NAS in Propulate, using a simple CNN trained on the MNIST dataset. This script was tested on a single compute node with 4 GPUs. Note that you need to adapt ``GPUS_PER_NODE`` (see ll. 25). """ import logging import pathlib import random -from typing import Union, Dict, Tuple +from typing import Dict, Tuple, Union import torch -from torch import nn -from torch.utils.data import DataLoader from lightning.pytorch import LightningModule, Trainer, loggers from lightning.pytorch.callbacks.early_stopping import EarlyStopping +from mpi4py import MPI +from torch import nn +from torch.utils.data import DataLoader from torchmetrics import Accuracy from torchvision.datasets import MNIST -from torchvision.transforms import Compose, ToTensor, Normalize -from mpi4py import MPI +from torchvision.transforms import Compose, Normalize, ToTensor + from propulate import Propulator from propulate.utils import get_default_propagator, set_logger_config - GPUS_PER_NODE: int = 4 # This example script was tested on a single node with 4 GPUs. NUM_WORKERS: int = ( 2 # Set this to the recommended number of workers in the PyTorch dataloader. @@ -91,9 +90,7 @@ def __init__( self.lr = lr # Set learning rate. self.loss_fn = loss_fn # Set the loss function used for training the model. self.best_accuracy = 0.0 # Initialize the model's best validation accuracy. - layers = ( - [] - ) # Set up the model architecture (depending on number of convolutional layers specified). + layers = [] # Set up the model architecture (depending on number of convolutional layers specified). layers += [ nn.Sequential( nn.Conv2d(in_channels=1, out_channels=10, kernel_size=3, padding=1), @@ -365,8 +362,10 @@ def ind_loss(params: Dict[str, Union[int, float, str]]) -> float: # Run optimization and print summary of results. propulator.propulate( - logging_interval=1, debug=2 # Logging interval and verbosity level + logging_interval=1, + debug=2, # Logging interval and verbosity level ) propulator.summarize( - top_n=1, debug=2 # Print top-n best individuals on each island in summary. + top_n=1, + debug=2, # Print top-n best individuals on each island in summary. )