pax_global_header00006660000000000000000000000064144556607620014531gustar00rootroot0000000000000052 comment=4ebcdbd412333893bd2674bc694924e21abf8915 cmaes-0.10.0/000077500000000000000000000000001445566076200126775ustar00rootroot00000000000000cmaes-0.10.0/.github/000077500000000000000000000000001445566076200142375ustar00rootroot00000000000000cmaes-0.10.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001445566076200164225ustar00rootroot00000000000000cmaes-0.10.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010661445566076200211170ustar00rootroot00000000000000--- name: "Bug report" about: Create a bug report to improve cmaes title: "" labels: bug assignees: '' --- # Bug reports *Please file a bug report here.* ## Expected Behavior *Please describe the behavior you are expecting* ## Current Behavior and Steps to Reproduce *What is the current behavior? Please provide detailed steps or example for reproducing.* ## Context Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. * cmaes version or commit revision: cmaes-0.10.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000002711445566076200221470ustar00rootroot00000000000000--- name: "Feature request" about: Suggest an idea for new features in cmaes. title: "" labels: enhancement assignees: '' --- # Feature Request *Please write your suggestion here.* cmaes-0.10.0/.github/workflows/000077500000000000000000000000001445566076200162745ustar00rootroot00000000000000cmaes-0.10.0/.github/workflows/examples.yml000066400000000000000000000031301445566076200206320ustar00rootroot00000000000000name: Run examples on: pull_request: paths: - '.github/workflows/examples.yml' - 'examples/**.py' - 'cmaes/**.py' jobs: examples: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: x64 - name: Install dependencies run: | pip install -U pip setuptools pip install --progress-bar off optuna numpy scipy pip install --progress-bar off -U . - run: python examples/quadratic_function.py - run: python examples/ipop_cmaes.py - run: python examples/bipop_cmaes.py - run: python examples/ellipsoid_function.py - run: python examples/optuna_sampler.py - run: python examples/ws_cma_es.py - run: python examples/cmaes_with_margin_binary.py - run: python examples/cmaes_with_margin_integer.py examples-cmawm-without-scipy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.11 architecture: x64 check-latest: true - name: Install dependencies run: | pip install -U pip setuptools pip install --progress-bar off -U . - run: python examples/cmaes_with_margin_binary.py - run: python examples/cmaes_with_margin_integer.py cmaes-0.10.0/.github/workflows/pypi-publish.yml000066400000000000000000000026211445566076200214450ustar00rootroot00000000000000name: Publish distributions to TestPyPI and PyPI on: push: tags: - v*.*.* jobs: build-n-publish: name: Build and publish Python distributions to TestPyPI and PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install --progress-bar off twine wheel - name: Build distribution packages run: python setup.py sdist bdist_wheel - name: Verify the distributions run: twine check dist/* - uses: actions/upload-artifact@v2 with: name: distribution path: dist/ - name: Publish distribution to Test PyPI uses: pypa/gh-action-pypi-publish@v1.4.1 with: user: __token__ password: ${{ secrets.TEST_PYPI_PASSWORD }} repository_url: https://test.pypi.org/legacy/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@v1.4.1 with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} - name: Create GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | export TAGNAME=$(jq --raw-output .ref "$GITHUB_EVENT_PATH" | sed -e "s/refs\/tags\///") gh release create ${TAGNAME} --draft dist/* cmaes-0.10.0/.github/workflows/tests.yml000066400000000000000000000022731445566076200201650ustar00rootroot00000000000000name: Run tests and linters on: pull_request: paths: - '.github/workflows/tests.yml' - 'pyproject.toml' - '**.py' jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' architecture: x64 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools numpy pip install --progress-bar off -r requirements-dev.txt - run: flake8 . --show-source --statistics - run: black --check . - run: mypy cmaes test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - name: Setup Python${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: x64 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools numpy scipy hypothesis pip install --progress-bar off . - run: python -m unittest cmaes-0.10.0/.gitignore000066400000000000000000000001741445566076200146710ustar00rootroot00000000000000venv/ dist/ build/ __pycache__/ .mypy_cache/ *.pyc .eggs/ *.egg-info/ .hypothesis tmp/ benchmark/*.json *.stats *.sqlite3 cmaes-0.10.0/LICENSE000066400000000000000000000020661445566076200137100ustar00rootroot00000000000000MIT License Copyright (c) 2020-2021 CyberAgent, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cmaes-0.10.0/README.md000066400000000000000000000405771445566076200141730ustar00rootroot00000000000000# CMA-ES [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](./LICENSE) [![PyPI - Downloads](https://img.shields.io/pypi/dw/cmaes)](https://pypistats.org/packages/cmaes) Lightweight Covariance Matrix Adaptation Evolution Strategy (CMA-ES) [1] implementation. ![visualize-six-hump-camel](https://user-images.githubusercontent.com/5564044/73486622-db5cff00-43e8-11ea-98fb-8246dbacab6d.gif) ## News * **2023/05/23** Our paper, [M. Nomura, Y. Akimoto, and I. Ono, CMA-ES with Learning Rate Adaptation: Can CMA-ES with Default Population Size Solve Multimodal and Noisy Problems?](https://arxiv.org/abs/2304.03473), has been nominated for the Best Paper Award in the ENUM track at GECCO'23 :whale: * **2023/04/01** Two papers have been accepted to GECCO'23 ENUM Track: (1) [M. Nomura, Y. Akimoto, and I. Ono, CMA-ES with Learning Rate Adaptation: Can CMA-ES with Default Population Size Solve Multimodal and Noisy Problems?](https://arxiv.org/abs/2304.03473), and (2) [Y. Watanabe, K. Uchida, R. Hamano, S. Saito, M. Nomura, and S. Shirakawa, (1+1)-CMA-ES with Margin for Discrete and Mixed-Integer Problems](https://arxiv.org/abs/2305.00849) :tada: * **2022/05/13** The paper, ["CMA-ES with Margin: Lower-Bounding Marginal Probability for Mixed-Integer Black-Box Optimization"](https://arxiv.org/abs/2205.13482) written by Hamano, Saito, [@nomuramasahir0](https://github.com/nomuramasahir0) (the maintainer of this library), and Shirakawa, has been nominated as best paper at GECCO'22 ENUM track. * **2021/03/10** ["Introduction to CMA-ES sampler"](https://medium.com/optuna/introduction-to-cma-es-sampler-ee68194c8f88) is published at Optuna Medium Blog. This article explains when and how to make the best use out of CMA-ES sampler. Please check it out! * **2021/02/02** The paper ["Warm Starting CMA-ES for Hyperparameter Optimization"](https://arxiv.org/abs/2012.06932) written by [@nomuramasahir0](https://github.com/nomuramasahir0), the maintainer of this library, is accepted at AAAI 2021 :tada: * **2020/07/29** Optuna's built-in CMA-ES sampler which uses this library under the hood is stabled at Optuna v2.0. Please check out the [v2.0 release blog](https://medium.com/optuna/optuna-v2-3165e3f1fc2). ## Installation Supported Python versions are 3.7 or later. ``` $ pip install cmaes ``` Or you can install via [conda-forge](https://anaconda.org/conda-forge/cmaes). ``` $ conda install -c conda-forge cmaes ``` ## Usage This library provides an "ask-and-tell" style interface. ```python import numpy as np from cmaes import CMA def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 if __name__ == "__main__": optimizer = CMA(mean=np.zeros(2), sigma=1.3) for generation in range(50): solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = quadratic(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") optimizer.tell(solutions) ``` And you can use this library via [Optuna](https://github.com/optuna/optuna) [2], an automatic hyperparameter optimization framework. Optuna's built-in CMA-ES sampler which uses this library under the hood is available from [v1.3.0](https://github.com/optuna/optuna/releases/tag/v1.3.0) and stabled at [v2.0.0](https://github.com/optuna/optuna/releases/tag/v2.2.0). See [the documentation](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.CmaEsSampler.html) or [v2.0 release blog](https://medium.com/optuna/optuna-v2-3165e3f1fc2) for more details. ```python import optuna def objective(trial: optuna.Trial): x1 = trial.suggest_uniform("x1", -4, 4) x2 = trial.suggest_uniform("x2", -4, 4) return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 if __name__ == "__main__": sampler = optuna.samplers.CmaEsSampler() study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=250) ``` ## CMA-ES variants #### CMA-ES with Margin [3] CMA-ES with Margin introduces a lower bound on the marginal probability associated with each discrete dimension so that samples can avoid being fixed to a single point. It can be applied to mixed spaces of continuous (float) and discrete (including integer and binary). |CMA-ES|CMA-ESwM| |---|---| |![CMA-ES](https://raw.githubusercontent.com/EvoConJP/CMA-ES_with_Margin/main/fig/CMA-ES.gif)|![CMA-ESwM](https://raw.githubusercontent.com/EvoConJP/CMA-ES_with_Margin/main/fig/CMA-ESwM.gif)| The above figures are taken from [EvoConJP/CMA-ES_with_Margin](https://github.com/EvoConJP/CMA-ES_with_Margin).
Source code ```python import numpy as np from cmaes import CMAwM def ellipsoid_onemax(x, n_zdim): n = len(x) n_rdim = n - n_zdim r = 10 if len(x) < 2: raise ValueError("dimension must be greater one") ellipsoid = sum([(1000 ** (i / (n_rdim - 1)) * x[i]) ** 2 for i in range(n_rdim)]) onemax = n_zdim - (0.0 < x[(n - n_zdim) :]).sum() return ellipsoid + r * onemax def main(): binary_dim, continuous_dim = 10, 10 dim = binary_dim + continuous_dim bounds = np.concatenate( [ np.tile([-np.inf, np.inf], (continuous_dim, 1)), np.tile([0, 1], (binary_dim, 1)), ] ) steps = np.concatenate([np.zeros(continuous_dim), np.ones(binary_dim)]) optimizer = CMAwM(mean=np.zeros(dim), sigma=2.0, bounds=bounds, steps=steps) print(" evals f(x)") print("====== ==========") evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x_for_eval, x_for_tell = optimizer.ask() value = ellipsoid_onemax(x_for_eval, binary_dim) evals += 1 solutions.append((x_for_tell, value)) if evals % 300 == 0: print(f"{evals:5d} {value:10.5f}") optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() ``` Source code is also available [here](./examples/cmaes_with_margin.py).
#### Warm Starting CMA-ES [4] Warm Starting CMA-ES is a method to transfer prior knowledge on similar HPO tasks through the initialization of CMA-ES. Here is the result of an experiment that tuning LightGBM for Kaggle's Toxic Comment Classification Challenge data, a multilabel classification dataset. In this benchmark, we use 10% of a full dataset as the source task, and a full dataset as the target task. Please refer [the paper](https://arxiv.org/abs/2012.06932) and/or https://github.com/c-bata/benchmark-warm-starting-cmaes for more details of experiment settings. ![benchmark-lightgbm-toxic](https://github.com/c-bata/benchmark-warm-starting-cmaes/raw/main/result.png)
Source code ```python import numpy as np from cmaes import CMA, get_warm_start_mgd def source_task(x1: float, x2: float) -> float: b = 0.4 return (x1 - b) ** 2 + (x2 - b) ** 2 def target_task(x1: float, x2: float) -> float: b = 0.6 return (x1 - b) ** 2 + (x2 - b) ** 2 if __name__ == "__main__": # Generate solutions from a source task source_solutions = [] for _ in range(1000): x = np.random.random(2) value = source_task(x[0], x[1]) source_solutions.append((x, value)) # Estimate a promising distribution of the source task, # then generate parameters of the multivariate gaussian distribution. ws_mean, ws_sigma, ws_cov = get_warm_start_mgd( source_solutions, gamma=0.1, alpha=0.1 ) optimizer = CMA(mean=ws_mean, sigma=ws_sigma, cov=ws_cov) # Run WS-CMA-ES print(" g f(x1,x2) x1 x2 ") print("=== ========== ====== ======") while True: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = target_task(x[0], x[1]) solutions.append((x, value)) print( f"{optimizer.generation:3d} {value:10.5f}" f" {x[0]:6.2f} {x[1]:6.2f}" ) optimizer.tell(solutions) if optimizer.should_stop(): break ``` The full source code is available [here](./examples/ws_cma_es.py).
#### Separable CMA-ES [5] sep-CMA-ES is an algorithm which constrains the covariance matrix to be diagonal. Due to the reduction of the number of parameters, the learning rate for the covariance matrix can be increased. Consequently, this algorithm outperforms CMA-ES on separable functions.
Source code ```python import numpy as np from cmaes import SepCMA def ellipsoid(x): n = len(x) if len(x) < 2: raise ValueError("dimension must be greater one") return sum([(1000 ** (i / (n - 1)) * x[i]) ** 2 for i in range(n)]) if __name__ == "__main__": dim = 40 optimizer = SepCMA(mean=3 * np.ones(dim), sigma=2.0) print(" evals f(x)") print("====== ==========") evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ellipsoid(x) evals += 1 solutions.append((x, value)) if evals % 3000 == 0: print(f"{evals:5d} {value:10.5f}") optimizer.tell(solutions) if optimizer.should_stop(): break ``` Full source code is available [here](./examples/sepcma_ellipsoid_function.py).
#### IPOP-CMA-ES [6] IPOP-CMA-ES is a method to restart CMA-ES with increasing population size like below. ![visualize-ipop-cmaes-himmelblau](https://user-images.githubusercontent.com/5564044/88472274-f9e12480-cf4b-11ea-8aff-2a859eb51a15.gif)
Source code ```python import math import numpy as np from cmaes import CMA def ackley(x1, x2): # https://www.sfu.ca/~ssurjano/ackley.html return ( -20 * math.exp(-0.2 * math.sqrt(0.5 * (x1 ** 2 + x2 ** 2))) - math.exp(0.5 * (math.cos(2 * math.pi * x1) + math.cos(2 * math.pi * x2))) + math.e + 20 ) if __name__ == "__main__": bounds = np.array([[-32.768, 32.768], [-32.768, 32.768]]) lower_bounds, upper_bounds = bounds[:, 0], bounds[:, 1] mean = lower_bounds + (np.random.rand(2) * (upper_bounds - lower_bounds)) sigma = 32.768 * 2 / 5 # 1/5 of the domain width optimizer = CMA(mean=mean, sigma=sigma, bounds=bounds, seed=0) for generation in range(200): solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ackley(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") optimizer.tell(solutions) if optimizer.should_stop(): # popsize multiplied by 2 (or 3) before each restart. popsize = optimizer.population_size * 2 mean = lower_bounds + (np.random.rand(2) * (upper_bounds - lower_bounds)) optimizer = CMA(mean=mean, sigma=sigma, population_size=popsize) print(f"Restart CMA-ES with popsize={popsize}") ``` Full source code is available [here](./examples/ipop_cmaes.py).
#### BIPOP-CMA-ES [7] BIPOP-CMA-ES applies two interlaced restart strategies, one with an increasing population size and one with varying small population sizes. ![visualize-bipop-cmaes-himmelblau](https://user-images.githubusercontent.com/5564044/88471815-55111800-cf48-11ea-8933-5a4b48c49eba.gif)
Source code ```python import math import numpy as np from cmaes import CMA def ackley(x1, x2): # https://www.sfu.ca/~ssurjano/ackley.html return ( -20 * math.exp(-0.2 * math.sqrt(0.5 * (x1 ** 2 + x2 ** 2))) - math.exp(0.5 * (math.cos(2 * math.pi * x1) + math.cos(2 * math.pi * x2))) + math.e + 20 ) if __name__ == "__main__": bounds = np.array([[-32.768, 32.768], [-32.768, 32.768]]) lower_bounds, upper_bounds = bounds[:, 0], bounds[:, 1] mean = lower_bounds + (np.random.rand(2) * (upper_bounds - lower_bounds)) sigma = 32.768 * 2 / 5 # 1/5 of the domain width optimizer = CMA(mean=mean, sigma=sigma, bounds=bounds, seed=0) n_restarts = 0 # A small restart doesn't count in the n_restarts small_n_eval, large_n_eval = 0, 0 popsize0 = optimizer.population_size inc_popsize = 2 # Initial run is with "normal" population size; it is # the large population before first doubling, but its # budget accounting is the same as in case of small # population. poptype = "small" for generation in range(200): solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ackley(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") optimizer.tell(solutions) if optimizer.should_stop(): n_eval = optimizer.population_size * optimizer.generation if poptype == "small": small_n_eval += n_eval else: # poptype == "large" large_n_eval += n_eval if small_n_eval < large_n_eval: poptype = "small" popsize_multiplier = inc_popsize ** n_restarts popsize = math.floor( popsize0 * popsize_multiplier ** (np.random.uniform() ** 2) ) else: poptype = "large" n_restarts += 1 popsize = popsize0 * (inc_popsize ** n_restarts) mean = lower_bounds + (np.random.rand(2) * (upper_bounds - lower_bounds)) optimizer = CMA( mean=mean, sigma=sigma, bounds=bounds, population_size=popsize, ) print("Restart CMA-ES with popsize={} ({})".format(popsize, poptype)) ``` Full source code is available [here](./examples/bipop_cmaes.py).
## Benchmark results | [Rosenbrock function](https://www.sfu.ca/~ssurjano/rosen.html) | [Six-Hump Camel function](https://www.sfu.ca/~ssurjano/camel6.html) | | ------------------- | ----------------------- | | ![rosenbrock](https://user-images.githubusercontent.com/5564044/73486735-0cd5ca80-43e9-11ea-9e6e-35028edf4ee8.png) | ![six-hump-camel](https://user-images.githubusercontent.com/5564044/73486738-0e9f8e00-43e9-11ea-8e65-d60fd5853b8d.png) | This implementation (green) stands comparison with [pycma](https://github.com/CMA-ES/pycma) (blue). See [benchmark](./benchmark) for details. ## Links **Projects using cmaes:** * [Optuna](https://github.com/optuna/optuna) : A hyperparameter optimization framework that supports CMA-ES using this library under the hood. * (If you have a project which uses `cmaes` and want your own project to be listed here, please submit a GitHub issue.) **Other libraries:** I respect all libraries involved in CMA-ES. * [pycma](https://github.com/CMA-ES/pycma) : Most famous CMA-ES implementation by Nikolaus Hansen. * [pymoo](https://github.com/msu-coinlab/pymoo) : Multi-objective optimization in Python. * [evojax](https://github.com/google/evojax) : EvoJAX provides a JAX-port of this library. * [evosax](https://github.com/RobertTLange/evosax) : evosax provides JAX-based CMA-ES and sep-CMA-ES implementation, which is inspired by this library. **References:** * [1] [N. Hansen, The CMA Evolution Strategy: A Tutorial. arXiv:1604.00772, 2016.](https://arxiv.org/abs/1604.00772) * [2] [T. Akiba, S. Sano, T. Yanase, T. Ohta, M. Koyama, Optuna: A Next-generation Hyperparameter Optimization Framework, KDD, 2019.](https://dl.acm.org/citation.cfm?id=3330701) * [3] [R. Hamano, S. Saito, M. Nomura, S. Shirakawa, CMA-ES with Margin: Lower-Bounding Marginal Probability for Mixed-Integer Black-Box Optimization, GECCO, 2022.](https://arxiv.org/abs/2205.13482) * [4] [M. Nomura, S. Watanabe, Y. Akimoto, Y. Ozaki, M. Onishi, Warm Starting CMA-ES for Hyperparameter Optimization, AAAI, 2021.](https://arxiv.org/abs/2012.06932) * [5] [R. Ros, N. Hansen, A Simple Modification in CMA-ES Achieving Linear Time and Space Complexity, PPSN, 2008.](https://hal.inria.fr/inria-00287367/document) * [6] [A. Auger, N. Hansen, A restart CMA evolution strategy with increasing population size, CEC, 2005.](https://sci2s.ugr.es/sites/default/files/files/TematicWebSites/EAMHCO/contributionsCEC05/auger05ARCMA.pdf) * [7] [N. Hansen, Benchmarking a BI-Population CMA-ES on the BBOB-2009 Function Testbed, GECCO Workshop, 2009.](https://hal.inria.fr/inria-00382093/document) cmaes-0.10.0/benchmark/000077500000000000000000000000001445566076200146315ustar00rootroot00000000000000cmaes-0.10.0/benchmark/README.md000066400000000000000000000030521445566076200161100ustar00rootroot00000000000000# Continuous benchmarking using kurobako and GitHub Actions Benchmark scripts are built on [kurobako](https://github.com/sile/kurobako). See [Introduction to Kurobako: A Benchmark Tool for Hyperparameter Optimization Algorithms](https://medium.com/optuna/kurobako-a2e3f7b760c7) for more details. ## How to run benchmark scripts GitHub Actions continuously run the benchmark scripts and comment on your pull request. If you want to run on your local machines, please execute following after installed kurobako. ```console $ ./benchmark/runner.sh -h runner.sh is an entrypoint to run benchmarkers. Usage: $ runner.sh Problem: rosenbrock : https://www.sfu.ca/~ssurjano/rosen.html six-hump-camel : https://www.sfu.ca/~ssurjano/camel6.html himmelblau : https://en.wikipedia.org/wiki/Himmelblau%27s_function ackley : https://www.sfu.ca/~ssurjano/ackley.html rastrigin : https://www.sfu.ca/~ssurjano/rastr.html Options: --help, -h print this Example: $ runner.sh rosenbrock ./tmp/kurobako.json $ cat ./tmp/kurobako.json | kurobako plot curve --errorbar -o ./tmp $ ./benchmark/runner.sh rosenbrock ./tmp/kurobako.json $ cat ./tmp/kurobako.json | kurobako plot curve --errorbar -o ./tmp ``` `kurobako plot curve` requires gnuplot. If you want to run on Docker container, please execute following: ``` $ docker pull sile/kurobako $ ./benchmark/runner.sh rosenbrock ./tmp/kurobako.json $ cat ./tmp/kurobako.json | docker run -v $PWD/tmp/images/:/images/ --rm -i sile/kurobako plot curve ``` cmaes-0.10.0/benchmark/optuna_solver.py000066400000000000000000000116331445566076200201070ustar00rootroot00000000000000import argparse import optuna import warnings from kurobako import solver from kurobako.solver.optuna import OptunaSolverFactory warnings.filterwarnings( "ignore", category=optuna.exceptions.ExperimentalWarning, module="optuna.samplers._cmaes", ) parser = argparse.ArgumentParser() parser.add_argument( "sampler", choices=["cmaes", "sep-cmaes", "ipop-cmaes", "ipop-sep-cmaes", "pycma", "ws-cmaes"], ) parser.add_argument( "--loglevel", choices=["debug", "info", "warning", "error"], default="warning" ) parser.add_argument("--warm-starting-trials", type=int, default=0) args = parser.parse_args() if args.loglevel == "debug": optuna.logging.set_verbosity(optuna.logging.DEBUG) elif args.loglevel == "info": optuna.logging.set_verbosity(optuna.logging.INFO) elif args.loglevel == "warning": optuna.logging.set_verbosity(optuna.logging.WARNING) elif args.loglevel == "error": optuna.logging.set_verbosity(optuna.logging.ERROR) def create_cmaes_study(seed): sampler = optuna.samplers.CmaEsSampler(seed=seed, warn_independent_sampling=True) return optuna.create_study(sampler=sampler, pruner=optuna.pruners.NopPruner()) def create_sep_cmaes_study(seed): sampler = optuna.samplers.CmaEsSampler( seed=seed, warn_independent_sampling=True, use_separable_cma=True ) return optuna.create_study(sampler=sampler, pruner=optuna.pruners.NopPruner()) def create_ipop_cmaes_study(seed): sampler = optuna.samplers.CmaEsSampler( seed=seed, warn_independent_sampling=True, restart_strategy="ipop", inc_popsize=2, ) return optuna.create_study(sampler=sampler, pruner=optuna.pruners.NopPruner()) def create_ipop_sep_cmaes_study(seed): sampler = optuna.samplers.CmaEsSampler( seed=seed, warn_independent_sampling=True, restart_strategy="ipop", inc_popsize=2, use_separable_cma=True, ) return optuna.create_study(sampler=sampler, pruner=optuna.pruners.NopPruner()) def create_pycma_study(seed): sampler = optuna.integration.PyCmaSampler( seed=seed, warn_independent_sampling=True, ) return optuna.create_study(sampler=sampler, pruner=optuna.pruners.NopPruner()) class WarmStartingCmaEsSampler(optuna.samplers.BaseSampler): def __init__(self, seed, warm_starting_trials: int) -> None: self._seed = seed self._warm_starting = True self._warm_starting_trials = warm_starting_trials self._sampler = optuna.samplers.RandomSampler(seed=seed) self._source_trials = [] def infer_relative_search_space(self, study, trial): return self._sampler.infer_relative_search_space(study, trial) def sample_relative( self, study, trial, search_space, ): return self._sampler.sample_relative(study, trial, search_space) def sample_independent(self, study, trial, param_name, param_distribution): return self._sampler.sample_independent( study, trial, param_name, param_distribution ) def after_trial( self, study, trial, state, values, ): if not self._warm_starting: return self._sampler.after_trial(study, trial, state, values) if len(self._source_trials) < self._warm_starting_trials: assert state == optuna.trial.TrialState.PRUNED self._source_trials.append( optuna.create_trial( params=trial.params, distributions=trial.distributions, values=values, ) ) if len(self._source_trials) == self._warm_starting_trials: self._sampler = optuna.samplers.CmaEsSampler( seed=self._seed + 1, source_trials=self._source_trials or None ) self._warm_starting = False else: return self._sampler.after_trial(study, trial, state, values) def create_warm_start_study(seed): sampler = WarmStartingCmaEsSampler(seed, args.warm_starting_trials) return optuna.create_study(sampler=sampler, pruner=optuna.pruners.NopPruner()) if __name__ == "__main__": if args.sampler == "cmaes": factory = OptunaSolverFactory(create_cmaes_study) elif args.sampler == "sep-cmaes": factory = OptunaSolverFactory(create_sep_cmaes_study) elif args.sampler == "ipop-cmaes": factory = OptunaSolverFactory(create_ipop_cmaes_study) elif args.sampler == "ipop-sep-cmaes": factory = OptunaSolverFactory(create_ipop_sep_cmaes_study) elif args.sampler == "pycma": factory = OptunaSolverFactory(create_pycma_study) elif args.sampler == "ws-cmaes": factory = OptunaSolverFactory( create_warm_start_study, warm_starting_trials=args.warm_starting_trials ) else: raise ValueError("unsupported sampler") runner = solver.SolverRunner(factory) runner.run() cmaes-0.10.0/benchmark/problem_himmelblau.py000066400000000000000000000025701445566076200210460ustar00rootroot00000000000000from kurobako import problem from kurobako.problem import Problem from typing import List from typing import Optional class HimmelblauEvaluator(problem.Evaluator): def __init__(self, params: List[Optional[float]]): self._x1, self._x2 = params self._current_step = 0 def evaluate(self, next_step: int) -> List[float]: self._current_step = 1 value = (self._x1**2 + self._x2 - 11.0) ** 2 + ( self._x1 + self._x2**2 - 7.0 ) ** 2 return [value] def current_step(self) -> int: return self._current_step class HimmelblauProblem(problem.Problem): def create_evaluator( self, params: List[Optional[float]] ) -> Optional[problem.Evaluator]: return HimmelblauEvaluator(params) class HimmelblauProblemFactory(problem.ProblemFactory): def create_problem(self, seed: int) -> Problem: return HimmelblauProblem() def specification(self) -> problem.ProblemSpec: params = [ problem.Var("x1", problem.ContinuousRange(-4, 4)), problem.Var("x2", problem.ContinuousRange(-4, 4)), ] return problem.ProblemSpec( name="Himmelblau Function", params=params, values=[problem.Var("Himmelblau")], ) if __name__ == "__main__": runner = problem.ProblemRunner(HimmelblauProblemFactory()) runner.run() cmaes-0.10.0/benchmark/problem_rastrigin.py000066400000000000000000000030011445566076200207170ustar00rootroot00000000000000import sys import numpy as np from kurobako import problem from kurobako.problem import Problem from typing import List from typing import Optional class RastriginEvaluator(problem.Evaluator): def __init__(self, params: List[Optional[float]]): self.n = len(params) self.x = np.array(params, dtype=float) self._current_step = 0 def evaluate(self, next_step: int) -> List[float]: self._current_step = 1 value = 10 * self.n + np.sum(self.x**2 - 10 * np.cos(2 * np.pi * self.x)) return [value] def current_step(self) -> int: return self._current_step class RastriginProblem(problem.Problem): def create_evaluator( self, params: List[Optional[float]] ) -> Optional[problem.Evaluator]: return RastriginEvaluator(params) class RastriginProblemFactory(problem.ProblemFactory): def __init__(self, dim): self.dim = dim def create_problem(self, seed: int) -> Problem: return RastriginProblem() def specification(self) -> problem.ProblemSpec: params = [ problem.Var(f"x{i+1}", problem.ContinuousRange(-5.12, 5.12)) for i in range(self.dim) ] return problem.ProblemSpec( name=f"Rastrigin (dim={self.dim})", params=params, values=[problem.Var("Rastrigin")], ) if __name__ == "__main__": dim = int(sys.argv[1]) if len(sys.argv) == 2 else 2 runner = problem.ProblemRunner(RastriginProblemFactory(dim)) runner.run() cmaes-0.10.0/benchmark/problem_rosenbrock.py000066400000000000000000000026241445566076200210760ustar00rootroot00000000000000from kurobako import problem from kurobako.problem import Problem from typing import List from typing import Optional class RosenbrockEvaluator(problem.Evaluator): """ See https://www.sfu.ca/~ssurjano/rosen.html """ def __init__(self, params: List[Optional[float]]): self._x1, self._x2 = params self._current_step = 0 def evaluate(self, next_step: int) -> List[float]: self._current_step = 1 value = 100 * (self._x2 - self._x1**2) ** 2 + (self._x1 - 1) ** 2 return [value] def current_step(self) -> int: return self._current_step class RosenbrockProblem(problem.Problem): def create_evaluator( self, params: List[Optional[float]] ) -> Optional[problem.Evaluator]: return RosenbrockEvaluator(params) class RosenbrockProblemFactory(problem.ProblemFactory): def create_problem(self, seed: int) -> Problem: return RosenbrockProblem() def specification(self) -> problem.ProblemSpec: params = [ problem.Var("x1", problem.ContinuousRange(-5, 10)), problem.Var("x2", problem.ContinuousRange(-5, 10)), ] return problem.ProblemSpec( name="Rosenbrock Function", params=params, values=[problem.Var("Rosenbrock")], ) if __name__ == "__main__": runner = problem.ProblemRunner(RosenbrockProblemFactory()) runner.run() cmaes-0.10.0/benchmark/problem_six_hump_camel.py000066400000000000000000000030341445566076200217200ustar00rootroot00000000000000from kurobako import problem from kurobako.problem import Problem from typing import List from typing import Optional class SixHumpCamelEvaluator(problem.Evaluator): """ See https://www.sfu.ca/~ssurjano/camel6.html """ def __init__(self, params: List[Optional[float]]): self._x1, self._x2 = params self._current_step = 0 def evaluate(self, next_step: int) -> List[float]: self._current_step = 1 value = ( (4 - 2.1 * (self._x1**2) + (self._x1**4) / 3) * (self._x1**2) + self._x1 * self._x2 + (-4 + 4 * self._x2**2) * (self._x2**2) ) return [value] def current_step(self) -> int: return self._current_step class SixHumpCamelProblem(problem.Problem): def create_evaluator( self, params: List[Optional[float]] ) -> Optional[problem.Evaluator]: return SixHumpCamelEvaluator(params) class SixHumpCamelProblemFactory(problem.ProblemFactory): def create_problem(self, seed: int) -> Problem: return SixHumpCamelProblem() def specification(self) -> problem.ProblemSpec: params = [ problem.Var("x1", problem.ContinuousRange(-5, 10)), problem.Var("x2", problem.ContinuousRange(-5, 10)), ] return problem.ProblemSpec( name="Six-Hump Camel Function", params=params, values=[problem.Var("Six-Hump Camel")], ) if __name__ == "__main__": runner = problem.ProblemRunner(SixHumpCamelProblemFactory()) runner.run() cmaes-0.10.0/benchmark/runner.sh000077500000000000000000000065451445566076200165130ustar00rootroot00000000000000#!/bin/sh set -e KUROBAKO=${KUROBAKO:-kurobako} DIR=$(cd $(dirname $0); pwd) REPEATS=${REPEATS:-5} BUDGET=${BUDGET:-300} SEED=${SEED:-1} DIM=${DIM:-2} SURROGATE_ROOT=${SURROGATE_ROOT:-$(dirname $DIR)/tmp/surrogate-models} WARM_START=${WARM_START:-0} usage() { cat < Problem: rosenbrock : https://www.sfu.ca/~ssurjano/rosen.html six-hump-camel : https://www.sfu.ca/~ssurjano/camel6.html himmelblau : https://en.wikipedia.org/wiki/Himmelblau%27s_function ackley : https://www.sfu.ca/~ssurjano/ackley.html rastrigin : https://www.sfu.ca/~ssurjano/rastr.html toxic-lightgbm : https://github.com/c-bata/benchmark-warm-starting-cmaes Options: --help, -h print this Example: $ $(basename ${0}) rosenbrock ./tmp/kurobako.json $ cat ./tmp/kurobako.json | kurobako plot curve --errorbar -o ./tmp EOF } case "$1" in himmelblau) PROBLEM=$($KUROBAKO problem command python $DIR/problem_himmelblau.py) ;; rosenbrock) PROBLEM=$($KUROBAKO problem command python $DIR/problem_rosenbrock.py) ;; six-hump-camel) PROBLEM=$($KUROBAKO problem command python $DIR/problem_six_hump_camel.py) ;; ackley) PROBLEM=$($KUROBAKO problem sigopt --dim $DIM ackley) ;; rastrigin) # "kurobako problem sigopt --dim 8 rastrigin" only accepts 8-dim. PROBLEM=$($KUROBAKO problem command python $DIR/problem_rastrigin.py $DIM) ;; toxic-lightgbm) PROBLEM=$($KUROBAKO problem warm-starting \ $($KUROBAKO problem surrogate $SURROGATE_ROOT/wscmaes-toxic-source/) \ $($KUROBAKO problem surrogate $SURROGATE_ROOT/wscmaes-toxic-target/)) ;; help|--help|-h) usage exit 0 ;; *) echo "[Error] Invalid problem '${1}'" usage exit 1 ;; esac RANDOM_SOLVER=$($KUROBAKO solver random) CMAES_SOLVER=$($KUROBAKO solver --name 'cmaes' command -- python $DIR/optuna_solver.py cmaes) SEP_CMAES_SOLVER=$($KUROBAKO solver --name 'sep-cmaes' command -- python $DIR/optuna_solver.py sep-cmaes) IPOP_CMAES_SOLVER=$($KUROBAKO solver --name 'ipop-cmaes' command -- python $DIR/optuna_solver.py ipop-cmaes) IPOP_SEP_CMAES_SOLVER=$($KUROBAKO solver --name 'ipop-sep-cmaes' command -- python $DIR/optuna_solver.py ipop-sep-cmaes) PYCMA_SOLVER=$($KUROBAKO solver --name 'pycma' command -- python $DIR/optuna_solver.py pycma) WS_CMAES_SOLVER=$($KUROBAKO solver --name 'ws-cmaes' command -- python $DIR/optuna_solver.py ws-cmaes --warm-starting-trials $WARM_START) if [ $WARM_START -gt 0 ]; then $KUROBAKO studies \ --solvers $CMAES_SOLVER $WS_CMAES_SOLVER \ --problems $PROBLEM \ --seed $SEED --repeats $REPEATS --budget $BUDGET \ | $KUROBAKO run --parallelism 6 > $2 elif [ $BUDGET -le 500 ]; then $KUROBAKO studies \ --solvers $RANDOM_SOLVER $PYCMA_SOLVER $CMAES_SOLVER $SEP_CMAES_SOLVER \ --problems $PROBLEM \ --seed $SEED --repeats $REPEATS --budget $BUDGET \ | $KUROBAKO run --parallelism 4 > $2 else $KUROBAKO studies \ --solvers $RANDOM_SOLVER $CMAES_SOLVER $IPOP_SEP_CMAES_SOLVER $IPOP_CMAES_SOLVER $SEP_CMAES_SOLVER \ --problems $PROBLEM \ --seed $SEED --repeats $REPEATS --budget $BUDGET \ | $KUROBAKO run --parallelism 6 > $2 fi cmaes-0.10.0/cmaes/000077500000000000000000000000001445566076200137675ustar00rootroot00000000000000cmaes-0.10.0/cmaes/__init__.py000066400000000000000000000003661445566076200161050ustar00rootroot00000000000000from ._cma import CMA # NOQA from ._sepcma import SepCMA # NOQA from ._warm_start import get_warm_start_mgd # NOQA from ._cmawm import CMAwM # NOQA from ._xnes import XNES # NOQA from ._dxnesic import DXNESIC # NOQA __version__ = "0.10.0" cmaes-0.10.0/cmaes/_cma.py000066400000000000000000000447641445566076200152570ustar00rootroot00000000000000from __future__ import annotations import math import numpy as np from typing import Any from typing import cast from typing import Optional _EPS = 1e-8 _MEAN_MAX = 1e32 _SIGMA_MAX = 1e32 class CMA: """CMA-ES stochastic optimizer class with ask-and-tell interface. Example: .. code:: import numpy as np from cmaes import CMA def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 optimizer = CMA(mean=np.zeros(2), sigma=1.3) for generation in range(50): solutions = [] for _ in range(optimizer.population_size): # Ask a parameter x = optimizer.ask() value = quadratic(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") # Tell evaluation values. optimizer.tell(solutions) Args: mean: Initial mean vector of multi-variate gaussian distributions. sigma: Initial standard deviation of covariance matrix. bounds: Lower and upper domain boundaries for each parameter (optional). n_max_resampling: A maximum number of resampling parameters (default: 100). If all sampled parameters are infeasible, the last sampled one will be clipped with lower and upper bounds. seed: A seed number (optional). population_size: A population size (optional). cov: A covariance matrix (optional). lr_adapt: Flag for learning rate adaptation (optional; default=False) """ def __init__( self, mean: np.ndarray, sigma: float, bounds: Optional[np.ndarray] = None, n_max_resampling: int = 100, seed: Optional[int] = None, population_size: Optional[int] = None, cov: Optional[np.ndarray] = None, lr_adapt: bool = False, ): assert sigma > 0, "sigma must be non-zero positive value" assert np.all( np.abs(mean) < _MEAN_MAX ), f"Abs of all elements of mean vector must be less than {_MEAN_MAX}" n_dim = len(mean) assert n_dim > 1, "The dimension of mean must be larger than 1" if population_size is None: population_size = 4 + math.floor(3 * math.log(n_dim)) # (eq. 48) assert population_size > 0, "popsize must be non-zero positive value." mu = population_size // 2 # (eq.49) weights_prime = np.array( [ math.log((population_size + 1) / 2) - math.log(i + 1) for i in range(population_size) ] ) mu_eff = (np.sum(weights_prime[:mu]) ** 2) / np.sum(weights_prime[:mu] ** 2) mu_eff_minus = (np.sum(weights_prime[mu:]) ** 2) / np.sum( weights_prime[mu:] ** 2 ) # learning rate for the rank-one update alpha_cov = 2 c1 = alpha_cov / ((n_dim + 1.3) ** 2 + mu_eff) # learning rate for the rank-μ update cmu = min( 1 - c1 - 1e-8, # 1e-8 is for large popsize. alpha_cov * (mu_eff - 2 + 1 / mu_eff) / ((n_dim + 2) ** 2 + alpha_cov * mu_eff / 2), ) assert c1 <= 1 - cmu, "invalid learning rate for the rank-one update" assert cmu <= 1 - c1, "invalid learning rate for the rank-μ update" min_alpha = min( 1 + c1 / cmu, # eq.50 1 + (2 * mu_eff_minus) / (mu_eff + 2), # eq.51 (1 - c1 - cmu) / (n_dim * cmu), # eq.52 ) # (eq.53) positive_sum = np.sum(weights_prime[weights_prime > 0]) negative_sum = np.sum(np.abs(weights_prime[weights_prime < 0])) weights = np.where( weights_prime >= 0, 1 / positive_sum * weights_prime, min_alpha / negative_sum * weights_prime, ) cm = 1 # (eq. 54) # learning rate for the cumulation for the step-size control (eq.55) c_sigma = (mu_eff + 2) / (n_dim + mu_eff + 5) d_sigma = 1 + 2 * max(0, math.sqrt((mu_eff - 1) / (n_dim + 1)) - 1) + c_sigma assert ( c_sigma < 1 ), "invalid learning rate for cumulation for the step-size control" # learning rate for cumulation for the rank-one update (eq.56) cc = (4 + mu_eff / n_dim) / (n_dim + 4 + 2 * mu_eff / n_dim) assert cc <= 1, "invalid learning rate for cumulation for the rank-one update" self._n_dim = n_dim self._popsize = population_size self._mu = mu self._mu_eff = mu_eff self._cc = cc self._c1 = c1 self._cmu = cmu self._c_sigma = c_sigma self._d_sigma = d_sigma self._cm = cm # E||N(0, I)|| (p.28) self._chi_n = math.sqrt(self._n_dim) * ( 1.0 - (1.0 / (4.0 * self._n_dim)) + 1.0 / (21.0 * (self._n_dim**2)) ) self._weights = weights # evolution path self._p_sigma = np.zeros(n_dim) self._pc = np.zeros(n_dim) self._mean = mean.copy() if cov is None: self._C = np.eye(n_dim) else: assert cov.shape == (n_dim, n_dim), "Invalid shape of covariance matrix" self._C = cov self._sigma = sigma self._D: Optional[np.ndarray] = None self._B: Optional[np.ndarray] = None # bounds contains low and high of each parameter. assert bounds is None or _is_valid_bounds(bounds, mean), "invalid bounds" self._bounds = bounds self._n_max_resampling = n_max_resampling self._g = 0 self._rng = np.random.RandomState(seed) # for learning rate adaptation self._lr_adapt = lr_adapt self._alpha = 1.4 self._beta_mean = 0.1 self._beta_Sigma = 0.03 self._gamma = 0.1 self._Emean = np.zeros([self._n_dim, 1]) self._ESigma = np.zeros([self._n_dim * self._n_dim, 1]) self._Vmean = 0.0 self._VSigma = 0.0 self._eta_mean = 1.0 self._eta_Sigma = 1.0 # Termination criteria self._tolx = 1e-12 * sigma self._tolxup = 1e4 self._tolfun = 1e-12 self._tolconditioncov = 1e14 self._funhist_term = 10 + math.ceil(30 * n_dim / population_size) self._funhist_values = np.empty(self._funhist_term * 2) def __getstate__(self) -> dict[str, Any]: attrs = {} for name in self.__dict__: # Remove _rng in pickle serialized object. if name == "_rng": continue if name == "_C": sym1d = _compress_symmetric(self._C) attrs["_c_1d"] = sym1d continue attrs[name] = getattr(self, name) return attrs def __setstate__(self, state: dict[str, Any]) -> None: state["_C"] = _decompress_symmetric(state["_c_1d"]) del state["_c_1d"] self.__dict__.update(state) # Set _rng for unpickled object. setattr(self, "_rng", np.random.RandomState()) @property def dim(self) -> int: """A number of dimensions""" return self._n_dim @property def population_size(self) -> int: """A population size""" return self._popsize @property def generation(self) -> int: """Generation number which is monotonically incremented when multi-variate gaussian distribution is updated.""" return self._g def reseed_rng(self, seed: int) -> None: self._rng.seed(seed) def set_bounds(self, bounds: Optional[np.ndarray]) -> None: """Update boundary constraints""" assert bounds is None or _is_valid_bounds(bounds, self._mean), "invalid bounds" self._bounds = bounds def ask(self) -> np.ndarray: """Sample a parameter""" for i in range(self._n_max_resampling): x = self._sample_solution() if self._is_feasible(x): return x x = self._sample_solution() x = self._repair_infeasible_params(x) return x def _eigen_decomposition(self) -> tuple[np.ndarray, np.ndarray]: if self._B is not None and self._D is not None: return self._B, self._D self._C = (self._C + self._C.T) / 2 D2, B = np.linalg.eigh(self._C) D = np.sqrt(np.where(D2 < 0, _EPS, D2)) self._C = np.dot(np.dot(B, np.diag(D**2)), B.T) self._B, self._D = B, D return B, D def _sample_solution(self) -> np.ndarray: B, D = self._eigen_decomposition() z = self._rng.randn(self._n_dim) # ~ N(0, I) y = cast(np.ndarray, B.dot(np.diag(D))).dot(z) # ~ N(0, C) x = self._mean + self._sigma * y # ~ N(m, σ^2 C) return x def _is_feasible(self, param: np.ndarray) -> bool: if self._bounds is None: return True return cast( bool, np.all(param >= self._bounds[:, 0]) and np.all(param <= self._bounds[:, 1]), ) # Cast bool_ to bool. def _repair_infeasible_params(self, param: np.ndarray) -> np.ndarray: if self._bounds is None: return param # clip with lower and upper bound. param = np.where(param < self._bounds[:, 0], self._bounds[:, 0], param) param = np.where(param > self._bounds[:, 1], self._bounds[:, 1], param) return param def tell(self, solutions: list[tuple[np.ndarray, float]]) -> None: """Tell evaluation values""" assert len(solutions) == self._popsize, "Must tell popsize-length solutions." for s in solutions: assert np.all( np.abs(s[0]) < _MEAN_MAX ), f"Abs of all param values must be less than {_MEAN_MAX} to avoid overflow errors" self._g += 1 solutions.sort(key=lambda s: s[1]) # Stores 'best' and 'worst' values of the # last 'self._funhist_term' generations. funhist_idx = 2 * (self.generation % self._funhist_term) self._funhist_values[funhist_idx] = solutions[0][1] self._funhist_values[funhist_idx + 1] = solutions[-1][1] # Sample new population of search_points, for k=1, ..., popsize B, D = self._eigen_decomposition() self._B, self._D = None, None # keep old values for learning rate adaptation if self._lr_adapt: old_mean = np.copy(self._mean) old_sigma = self._sigma old_Sigma = self._sigma**2 * self._C old_invsqrtC = B @ np.diag(1 / D) @ B.T else: old_mean, old_sigma, old_Sigma, old_invsqrtC = None, None, None, None x_k = np.array([s[0] for s in solutions]) # ~ N(m, σ^2 C) y_k = (x_k - self._mean) / self._sigma # ~ N(0, C) # Selection and recombination y_w = np.sum(y_k[: self._mu].T * self._weights[: self._mu], axis=1) # eq.41 self._mean += self._cm * self._sigma * y_w # Step-size control C_2 = cast( np.ndarray, cast(np.ndarray, B.dot(np.diag(1 / D))).dot(B.T) ) # C^(-1/2) = B D^(-1) B^T self._p_sigma = (1 - self._c_sigma) * self._p_sigma + math.sqrt( self._c_sigma * (2 - self._c_sigma) * self._mu_eff ) * C_2.dot(y_w) norm_p_sigma = np.linalg.norm(self._p_sigma) self._sigma *= np.exp( (self._c_sigma / self._d_sigma) * (norm_p_sigma / self._chi_n - 1) ) self._sigma = min(self._sigma, _SIGMA_MAX) # Covariance matrix adaption h_sigma_cond_left = norm_p_sigma / math.sqrt( 1 - (1 - self._c_sigma) ** (2 * (self._g + 1)) ) h_sigma_cond_right = (1.4 + 2 / (self._n_dim + 1)) * self._chi_n h_sigma = 1.0 if h_sigma_cond_left < h_sigma_cond_right else 0.0 # (p.28) # (eq.45) self._pc = (1 - self._cc) * self._pc + h_sigma * math.sqrt( self._cc * (2 - self._cc) * self._mu_eff ) * y_w # (eq.46) w_io = self._weights * np.where( self._weights >= 0, 1, self._n_dim / (np.linalg.norm(C_2.dot(y_k.T), axis=0) ** 2 + _EPS), ) delta_h_sigma = (1 - h_sigma) * self._cc * (2 - self._cc) # (p.28) assert delta_h_sigma <= 1 # (eq.47) rank_one = np.outer(self._pc, self._pc) rank_mu = np.sum( np.array([w * np.outer(y, y) for w, y in zip(w_io, y_k)]), axis=0 ) self._C = ( ( 1 + self._c1 * delta_h_sigma - self._c1 - self._cmu * np.sum(self._weights) ) * self._C + self._c1 * rank_one + self._cmu * rank_mu ) # Learning rate adaptation: https://arxiv.org/abs/2304.03473 if self._lr_adapt: assert isinstance(old_mean, np.ndarray) assert isinstance(old_sigma, float) assert isinstance(old_Sigma, np.ndarray) assert isinstance(old_invsqrtC, np.ndarray) self._lr_adaptation(old_mean, old_sigma, old_Sigma, old_invsqrtC) def _lr_adaptation( self, old_mean: np.ndarray, old_sigma: float, old_Sigma: np.ndarray, old_invsqrtC: np.ndarray, ) -> None: # calculate one-step difference of the parameters Deltamean = (self._mean - old_mean).reshape([self._n_dim, 1]) Sigma = (self._sigma**2) * self._C # note that we use here matrix representation instead of vec one DeltaSigma = Sigma - old_Sigma # local coordinate old_inv_sqrtSigma = old_invsqrtC / old_sigma locDeltamean = old_inv_sqrtSigma.dot(Deltamean) locDeltaSigma = ( old_inv_sqrtSigma.dot(DeltaSigma.dot(old_inv_sqrtSigma)) ).reshape(self.dim * self.dim, 1) / np.sqrt(2) # moving average E and V self._Emean = ( 1 - self._beta_mean ) * self._Emean + self._beta_mean * locDeltamean self._ESigma = ( 1 - self._beta_Sigma ) * self._ESigma + self._beta_Sigma * locDeltaSigma self._Vmean = (1 - self._beta_mean) * self._Vmean + self._beta_mean * ( float(np.linalg.norm(locDeltamean)) ** 2 ) self._VSigma = (1 - self._beta_Sigma) * self._VSigma + self._beta_Sigma * ( float(np.linalg.norm(locDeltaSigma)) ** 2 ) # estimate SNR sqnormEmean = np.linalg.norm(self._Emean) ** 2 hatSNRmean = ( sqnormEmean - (self._beta_mean / (2 - self._beta_mean)) * self._Vmean ) / (self._Vmean - sqnormEmean) sqnormESigma = np.linalg.norm(self._ESigma) ** 2 hatSNRSigma = ( sqnormESigma - (self._beta_Sigma / (2 - self._beta_Sigma)) * self._VSigma ) / (self._VSigma - sqnormESigma) # update learning rate before_eta_mean = self._eta_mean relativeSNRmean = np.clip( (hatSNRmean / self._alpha / self._eta_mean) - 1, -1, 1 ) self._eta_mean = self._eta_mean * np.exp( min(self._gamma * self._eta_mean, self._beta_mean) * relativeSNRmean ) relativeSNRSigma = np.clip( (hatSNRSigma / self._alpha / self._eta_Sigma) - 1, -1, 1 ) self._eta_Sigma = self._eta_Sigma * np.exp( min(self._gamma * self._eta_Sigma, self._beta_Sigma) * relativeSNRSigma ) # cap self._eta_mean = min(self._eta_mean, 1.0) self._eta_Sigma = min(self._eta_Sigma, 1.0) # update parameters self._mean = old_mean + self._eta_mean * Deltamean.reshape(self._n_dim) Sigma = old_Sigma + self._eta_Sigma * DeltaSigma # decompose Sigma to sigma and C eigs, _ = np.linalg.eigh(Sigma) logeigsum = sum([np.log(e) for e in eigs]) self._sigma = np.exp(logeigsum / 2.0 / self._n_dim) self._sigma = min(self._sigma, _SIGMA_MAX) self._C = Sigma / (self._sigma**2) # step-size correction self._sigma *= before_eta_mean / self._eta_mean def should_stop(self) -> bool: B, D = self._eigen_decomposition() dC = np.diag(self._C) # Stop if the range of function values of the recent generation is below tolfun. if ( self.generation > self._funhist_term and np.max(self._funhist_values) - np.min(self._funhist_values) < self._tolfun ): return True # Stop if the std of the normal distribution is smaller than tolx # in all coordinates and pc is smaller than tolx in all components. if np.all(self._sigma * dC < self._tolx) and np.all( self._sigma * self._pc < self._tolx ): return True # Stop if detecting divergent behavior. if self._sigma * np.max(D) > self._tolxup: return True # No effect coordinates: stop if adding 0.2-standard deviations # in any single coordinate does not change m. if np.any(self._mean == self._mean + (0.2 * self._sigma * np.sqrt(dC))): return True # No effect axis: stop if adding 0.1-standard deviation vector in # any principal axis direction of C does not change m. "pycma" check # axis one by one at each generation. i = self.generation % self.dim if np.all(self._mean == self._mean + (0.1 * self._sigma * D[i] * B[:, i])): return True # Stop if the condition number of the covariance matrix exceeds 1e14. condition_cov = np.max(D) / np.min(D) if condition_cov > self._tolconditioncov: return True return False def _is_valid_bounds(bounds: Optional[np.ndarray], mean: np.ndarray) -> bool: if bounds is None: return True if (mean.size, 2) != bounds.shape: return False if not np.all(bounds[:, 0] <= mean): return False if not np.all(mean <= bounds[:, 1]): return False return True def _compress_symmetric(sym2d: np.ndarray) -> np.ndarray: assert len(sym2d.shape) == 2 and sym2d.shape[0] == sym2d.shape[1] n = sym2d.shape[0] dim = (n * (n + 1)) // 2 sym1d = np.zeros(dim) start = 0 for i in range(n): sym1d[start : start + n - i] = sym2d[i][i:] # noqa: E203 start += n - i return sym1d def _decompress_symmetric(sym1d: np.ndarray) -> np.ndarray: n = int(np.sqrt(sym1d.size * 2)) assert (n * (n + 1)) // 2 == sym1d.size R, C = np.triu_indices(n) out = np.zeros((n, n), dtype=sym1d.dtype) out[R, C] = sym1d out[C, R] = sym1d return out cmaes-0.10.0/cmaes/_cmawm.py000066400000000000000000000314251445566076200156110ustar00rootroot00000000000000from __future__ import annotations import functools import numpy as np from typing import cast from typing import Optional from cmaes import CMA from cmaes._cma import _is_valid_bounds try: from scipy import stats chi2_ppf = functools.partial(stats.chi2.ppf, df=1) norm_cdf = stats.norm.cdf except ImportError: from cmaes._stats import chi2_ppf # type: ignore from cmaes._stats import norm_cdf class CMAwM: """CMA-ES with Margin class with ask-and-tell interface. The code is adapted from https://github.com/EvoConJP/CMA-ES_with_Margin. Example: .. code:: import numpy as np from cmaes import CMAwM def ellipsoid_onemax(x, n_zdim): n = len(x) n_rdim = n - n_zdim ellipsoid = sum([(1000 ** (i / (n_rdim - 1)) * x[i]) ** 2 for i in range(n_rdim)]) onemax = n_zdim - (0. < x[(n - n_zdim):]).sum() return ellipsoid + 10 * onemax binary_dim, continuous_dim = 10, 10 dim = binary_dim + continuous_dim bounds = np.concatenate( [ np.tile([0, 1], (binary_dim, 1)), np.tile([-np.inf, np.inf], (continuous_dim, 1)), ] ) steps = np.concatenate([np.ones(binary_dim), np.zeros(continuous_dim)]) optimizer = CMAwM(mean=np.zeros(dim), sigma=2.0, bounds=bounds, steps=steps) evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x_for_eval, x_for_tell = optimizer.ask() value = ellipsoid_onemax(x_for_eval, binary_dim) evals += 1 solutions.append((x_for_tell, value)) optimizer.tell(solutions) if optimizer.should_stop(): break Args: mean: Initial mean vector of multi-variate gaussian distributions. sigma: Initial standard deviation of covariance matrix. bounds: Lower and upper domain boundaries for each parameter. steps: Each value represents a step of discretization for each dimension. Zero (or negative value) means a continuous space. n_max_resampling: A maximum number of resampling parameters (default: 100). If all sampled parameters are infeasible, the last sampled one will be clipped with lower and upper bounds. seed: A seed number (optional). population_size: A population size (optional). cov: A covariance matrix (optional). margin: A margin parameter (optional). """ # Paper: https://arxiv.org/abs/2205.13482 def __init__( self, mean: np.ndarray, sigma: float, bounds: np.ndarray, steps: np.ndarray, n_max_resampling: int = 100, seed: Optional[int] = None, population_size: Optional[int] = None, cov: Optional[np.ndarray] = None, margin: Optional[float] = None, ): # initialize `CMA` self._cma = CMA( mean, sigma, bounds, n_max_resampling, seed, population_size, cov ) n_dim = self._cma.dim population_size = self._cma.population_size self._n_max_resampling = n_max_resampling # split discrete space and continuous space assert len(bounds) == len(steps), "bounds and steps must be the same length" assert not np.isnan(steps).any(), "steps should not include NaN" self._discrete_idx = np.where(steps > 0)[0] discrete_list = [ np.arange(bounds[i][0], bounds[i][1] + steps[i] / 2, steps[i]) for i in self._discrete_idx ] max_discrete = max([len(discrete) for discrete in discrete_list], default=0) discrete_space = np.full((len(self._discrete_idx), max_discrete), np.nan) for i, discrete in enumerate(discrete_list): discrete_space[i, : len(discrete)] = discrete # continuous_space contains low and high of each parameter. self._continuous_idx = np.where(steps <= 0)[0] self._continuous_space = bounds[self._continuous_idx] assert _is_valid_bounds( self._continuous_space, mean[self._continuous_idx] ), "invalid bounds" # discrete_space self._n_zdim = len(discrete_space) if self._n_zdim == 0: return self.margin = margin if margin is not None else 1 / (n_dim * population_size) assert self.margin > 0, "margin must be non-zero positive value." self.z_space = discrete_space self.z_lim = (self.z_space[:, 1:] + self.z_space[:, :-1]) / 2 for i in range(self._n_zdim): self.z_space[i][np.isnan(self.z_space[i])] = np.nanmax(self.z_space[i]) self.z_lim[i][np.isnan(self.z_lim[i])] = np.nanmax(self.z_lim[i]) self.z_lim_low = np.concatenate( [self.z_lim.min(axis=1).reshape([self._n_zdim, 1]), self.z_lim], 1 ) self.z_lim_up = np.concatenate( [self.z_lim, self.z_lim.max(axis=1).reshape([self._n_zdim, 1])], 1 ) m_z = self._cma._mean[self._discrete_idx].reshape(([self._n_zdim, 1])) # m_z_lim_low ->| mean vector |<- m_z_lim_up self.m_z_lim_low = ( self.z_lim_low * np.where(np.sort(np.concatenate([self.z_lim, m_z], 1)) == m_z, 1, 0) ).sum(axis=1) self.m_z_lim_up = ( self.z_lim_up * np.where(np.sort(np.concatenate([self.z_lim, m_z], 1)) == m_z, 1, 0) ).sum(axis=1) self._A = np.full(n_dim, 1.0) @property def dim(self) -> int: """A number of dimensions""" return self._cma.dim @property def population_size(self) -> int: """A population size""" return self._cma.population_size @property def generation(self) -> int: """Generation number which is monotonically incremented when multi-variate gaussian distribution is updated.""" return self._cma.generation @property def _rng(self) -> np.random.RandomState: return self._cma._rng def reseed_rng(self, seed: int) -> None: self._cma.reseed_rng(seed) def ask(self) -> tuple[np.ndarray, np.ndarray]: """Sample a parameter and return (i) encoded x and (ii) raw x. The encoded x is used for the evaluation. The raw x is used for updating the distribution.""" for i in range(self._n_max_resampling): x = self._cma._sample_solution() if self._is_continuous_feasible(x[self._continuous_idx]): x_encoded = x.copy() if self._n_zdim > 0: x_encoded[self._discrete_idx] = self._encode_discrete_params( x[self._discrete_idx] ) return x_encoded, x x = self._cma._sample_solution() x[self._continuous_idx] = self._repair_continuous_params( x[self._continuous_idx] ) x_encoded = x.copy() if self._n_zdim > 0: x_encoded[self._discrete_idx] = self._encode_discrete_params( x[self._discrete_idx] ) return x_encoded, x def _is_continuous_feasible(self, continuous_param: np.ndarray) -> bool: if self._continuous_space is None: return True return cast( bool, np.all(continuous_param >= self._continuous_space[:, 0]) and np.all(continuous_param <= self._continuous_space[:, 1]), ) # Cast bool_ to bool. def _repair_continuous_params(self, continuous_param: np.ndarray) -> np.ndarray: if self._continuous_space is None: return continuous_param # clip with lower and upper bound. param = np.where( continuous_param < self._continuous_space[:, 0], self._continuous_space[:, 0], continuous_param, ) param = np.where( param > self._continuous_space[:, 1], self._continuous_space[:, 1], param ) return param def _encode_discrete_params(self, discrete_param: np.ndarray) -> np.ndarray: """Encode the values into discrete domain.""" mean = self._cma._mean x = (discrete_param - mean[self._discrete_idx]) * self._A[ self._discrete_idx ] + mean[self._discrete_idx] x = x.reshape([self._n_zdim, 1]) x_enc = ( self.z_space * np.where(np.sort(np.concatenate((self.z_lim, x), axis=1)) == x, 1, 0) ).sum(axis=1) return x_enc.reshape(self._n_zdim) def tell(self, solutions: list[tuple[np.ndarray, float]]) -> None: """Tell evaluation values""" self._cma.tell(solutions) mean = self._cma._mean sigma = self._cma._sigma C = self._cma._C if self._n_zdim == 0: return # margin correction updated_m_integer = mean[self._discrete_idx, np.newaxis] self.z_lim_low = np.concatenate( [self.z_lim.min(axis=1).reshape([self._n_zdim, 1]), self.z_lim], 1 ) self.z_lim_up = np.concatenate( [self.z_lim, self.z_lim.max(axis=1).reshape([self._n_zdim, 1])], 1 ) self.m_z_lim_low = ( self.z_lim_low * np.where( np.sort(np.concatenate([self.z_lim, updated_m_integer], 1)) == updated_m_integer, 1, 0, ) ).sum(axis=1) self.m_z_lim_up = ( self.z_lim_up * np.where( np.sort(np.concatenate([self.z_lim, updated_m_integer], 1)) == updated_m_integer, 1, 0, ) ).sum(axis=1) # calculate probability low_cdf := Pr(X <= m_z_lim_low) and up_cdf := Pr(m_z_lim_up < X) # sig_z_sq_Cdiag = self.model.sigma * self.model.A * np.sqrt(np.diag(self.model.C)) z_scale = ( sigma * self._A[self._discrete_idx] * np.sqrt(np.diag(C)[self._discrete_idx]) ) updated_m_integer = updated_m_integer.flatten() low_cdf = norm_cdf(self.m_z_lim_low, loc=updated_m_integer, scale=z_scale) up_cdf = 1.0 - norm_cdf(self.m_z_lim_up, loc=updated_m_integer, scale=z_scale) mid_cdf = 1.0 - (low_cdf + up_cdf) # edge case edge_mask = np.maximum(low_cdf, up_cdf) > 0.5 # otherwise side_mask = np.maximum(low_cdf, up_cdf) <= 0.5 if np.any(edge_mask): # modify mask (modify or not) modify_mask = np.minimum(low_cdf, up_cdf) < self.margin # modify sign modify_sign = np.sign(mean[self._discrete_idx] - self.m_z_lim_up) # distance from m_z_lim_up dist = ( sigma * self._A[self._discrete_idx] * np.sqrt( chi2_ppf(q=1.0 - 2.0 * self.margin) * np.diag(C)[self._discrete_idx] ) ) # modify mean vector mean[self._discrete_idx] = mean[ self._discrete_idx ] + modify_mask * edge_mask * ( self.m_z_lim_up + modify_sign * dist - mean[self._discrete_idx] ) # correct probability low_cdf = np.maximum(low_cdf, self.margin / 2.0) up_cdf = np.maximum(up_cdf, self.margin / 2.0) modified_low_cdf = low_cdf + (1.0 - low_cdf - up_cdf - mid_cdf) * ( low_cdf - self.margin / 2 ) / (low_cdf + mid_cdf + up_cdf - 3.0 * self.margin / 2) modified_up_cdf = up_cdf + (1.0 - low_cdf - up_cdf - mid_cdf) * ( up_cdf - self.margin / 2 ) / (low_cdf + mid_cdf + up_cdf - 3.0 * self.margin / 2) modified_low_cdf = np.clip(modified_low_cdf, 1e-10, 0.5 - 1e-10) modified_up_cdf = np.clip(modified_up_cdf, 1e-10, 0.5 - 1e-10) # modify mean vector and A (with sigma and C fixed) chi_low_sq = np.sqrt(chi2_ppf(q=1.0 - 2 * modified_low_cdf)) chi_up_sq = np.sqrt(chi2_ppf(q=1.0 - 2 * modified_up_cdf)) C_diag_sq = np.sqrt(np.diag(C))[self._discrete_idx] # simultaneous equations self._A[self._discrete_idx] = self._A[self._discrete_idx] + side_mask * ( (self.m_z_lim_up - self.m_z_lim_low) / ((chi_low_sq + chi_up_sq) * sigma * C_diag_sq) - self._A[self._discrete_idx] ) mean[self._discrete_idx] = mean[self._discrete_idx] + side_mask * ( (self.m_z_lim_low * chi_up_sq + self.m_z_lim_up * chi_low_sq) / (chi_low_sq + chi_up_sq) - mean[self._discrete_idx] ) def should_stop(self) -> bool: return self._cma.should_stop() cmaes-0.10.0/cmaes/_dxnesic.py000066400000000000000000000344251445566076200161450ustar00rootroot00000000000000from __future__ import annotations import math import sys import numpy as np from typing import cast from typing import Optional _EPS = 1e-8 _MEAN_MAX = 1e32 _SIGMA_MAX = 1e32 class DXNESIC: """DX-NES-IC stochastic optimizer class with ask-and-tell interface. Example: .. code:: import numpy as np from cmaes import DXNESIC def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 optimizer = DXNESIC(mean=np.zeros(2), sigma=1.3) for generation in range(50): solutions = [] for _ in range(optimizer.population_size): # Ask a parameter x = optimizer.ask() value = quadratic(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") # Tell evaluation values. optimizer.tell(solutions) Args: mean: Initial mean vector of multi-variate gaussian distributions. sigma: Initial standard deviation of covariance matrix. bounds: Lower and upper domain boundaries for each parameter (optional). n_max_resampling: A maximum number of resampling parameters (default: 100). If all sampled parameters are infeasible, the last sampled one will be clipped with lower and upper bounds. seed: A seed number (optional). population_size: A population size (optional). cov: A covariance matrix (optional). """ # Paper: https://ieeexplore.ieee.org/abstract/document/9504865 def __init__( self, mean: np.ndarray, sigma: float, bounds: Optional[np.ndarray] = None, n_max_resampling: int = 100, seed: Optional[int] = None, population_size: Optional[int] = None, ): assert sigma > 0, "sigma must be non-zero positive value" assert np.all( np.abs(mean) < _MEAN_MAX ), f"Abs of all elements of mean vector must be less than {_MEAN_MAX}" n_dim = len(mean) assert n_dim > 1, "The dimension of mean must be larger than 1" if population_size is None: population_size = 4 + math.floor(3 * math.log(n_dim)) assert population_size > 0, "popsize must be non-zero positive value." w_rank_hat = np.log(population_size / 2 + 1) - np.log( np.arange(1, population_size + 1) ) w_rank_hat[np.where(w_rank_hat < 0)] = 0 w_rank = w_rank_hat / sum(w_rank_hat) - (1.0 / population_size) mu_eff = 1 / sum((w_rank + (1.0 / population_size)) ** 2) # learning rate for the cumulation for the step-size control c_sigma = (mu_eff + 2) / (n_dim + mu_eff + 5) assert ( c_sigma < 1 ), "invalid learning rate for cumulation for the step-size control" # distance weight parameter h_inv = _get_h_inv(n_dim) self._n_dim = n_dim self._popsize = population_size self._mu_eff = mu_eff self._h_inv = h_inv self._c_sigma = c_sigma # E||N(0, I)|| self._chi_n = math.sqrt(self._n_dim) * ( 1.0 - (1.0 / (4.0 * self._n_dim)) + 1.0 / (21.0 * (self._n_dim**2)) ) # weights self._w_rank = w_rank self._w_rank_hat = w_rank_hat # for antithetic sampling self._zsym: Optional[np.ndarray] = None # learning rate self._eta_mean = 1.0 self._eta_move_sigma = 1.0 self._c_gamma = 1.0 / (3.0 * (n_dim - 1.0)) self._d_gamma = min(1.0, n_dim / population_size) self._gamma = 1.0 # evolution path self._p_sigma = np.zeros(n_dim) # distribution parameter self._mean = mean.copy() self._sigma = sigma self._B = np.eye(n_dim) # bounds contains low and high of each parameter. assert bounds is None or _is_valid_bounds(bounds, mean), "invalid bounds" self._bounds = bounds self._n_max_resampling = n_max_resampling self._g = 0 self._rng = np.random.RandomState(seed) # Termination criteria self._tolx = 1e-12 * sigma self._tolxup = 1e4 self._tolfun = 1e-12 self._tolconditioncov = 1e14 self._funhist_term = 10 + math.ceil(30 * n_dim / population_size) self._funhist_values = np.empty(self._funhist_term * 2) @property def dim(self) -> int: """A number of dimensions""" return self._n_dim @property def population_size(self) -> int: """A population size""" return self._popsize @property def generation(self) -> int: """Generation number which is monotonically incremented when multi-variate gaussian distribution is updated.""" return self._g def _alpha_dist(self, num_feasible: int) -> float: return ( self._h_inv * min(1.0, math.sqrt(float(self._popsize) / self._n_dim)) * math.sqrt(float(num_feasible) / self._popsize) ) def _w_dist_hat(self, z: np.ndarray, num_feasible: int) -> float: return math.exp(self._alpha_dist(num_feasible) * np.linalg.norm(z)) def _eta_stag_sigma(self, num_feasible: int) -> float: return math.tanh( (0.024 * num_feasible + 0.7 * self._n_dim + 20.0) / (self._n_dim + 12.0) ) def _eta_conv_sigma(self, num_feasible: int) -> float: return 2.0 * math.tanh( (0.025 * num_feasible + 0.75 * self._n_dim + 10.0) / (self._n_dim + 4.0) ) def _eta_move_B(self, num_feasible: int) -> float: return ( 180 * self._n_dim * math.tanh(0.02 * num_feasible) / (47 * (self._n_dim**2) + 6400) ) def _eta_stag_B(self, num_feasible: int) -> float: return ( 168 * self._n_dim * math.tanh(0.02 * num_feasible) / (47 * (self._n_dim**2) + 6400) ) def _eta_conv_B(self, num_feasible: int) -> float: return ( 12 * self._n_dim * math.tanh(0.02 * num_feasible) / (47 * (self._n_dim**2) + 6400) ) def reseed_rng(self, seed: int) -> None: self._rng.seed(seed) def set_bounds(self, bounds: Optional[np.ndarray]) -> None: """Update boundary constraints""" assert bounds is None or _is_valid_bounds(bounds, self._mean), "invalid bounds" self._bounds = bounds def ask(self) -> np.ndarray: """Sample a parameter""" for i in range(self._n_max_resampling): x = self._sample_solution() if self._is_feasible(x): return x x = self._sample_solution() x = self._repair_infeasible_params(x) return x def _sample_solution(self) -> np.ndarray: # antithetic sampling if self._zsym is None: z = self._rng.randn(self._n_dim) # ~ N(0, I) self._zsym = z else: z = -self._zsym self._zsym = None x = self._mean + self._sigma * self._B.dot(z) # ~ N(m, σ^2 B B^T) return x def _is_feasible(self, param: np.ndarray) -> bool: if self._bounds is None: return True return cast( bool, np.all(param >= self._bounds[:, 0]) and np.all(param <= self._bounds[:, 1]), ) # Cast bool_ to bool. def _repair_infeasible_params(self, param: np.ndarray) -> np.ndarray: if self._bounds is None: return param # clip with lower and upper bound. param = np.where(param < self._bounds[:, 0], self._bounds[:, 0], param) param = np.where(param > self._bounds[:, 1], self._bounds[:, 1], param) return param def tell(self, solutions: list[tuple[np.ndarray, float]]) -> None: """Tell evaluation values""" assert len(solutions) == self._popsize, "Must tell popsize-length solutions." for s in solutions: assert np.all( np.abs(s[0]) < _MEAN_MAX ), f"Abs of all param values must be less than {_MEAN_MAX} to avoid overflow errors" # counting # feasible solutions lamb_feas = len([s[1] for s in solutions if s[1] < sys.maxsize]) self._g += 1 solutions.sort(key=lambda s: s[1]) # Stores 'best' and 'worst' values of the # last 'self._funhist_term' generations. funhist_idx = 2 * (self.generation % self._funhist_term) self._funhist_values[funhist_idx] = solutions[0][1] self._funhist_values[funhist_idx + 1] = solutions[-1][1] z_k = np.array( [ np.linalg.inv(self._sigma * self._B).dot(s[0] - self._mean) for s in solutions ] ) # Evolution path z_w = np.sum(z_k.T * self._w_rank, axis=1) self._p_sigma = (1 - self._c_sigma) * self._p_sigma + math.sqrt( self._c_sigma * (2 - self._c_sigma) * self._mu_eff ) * z_w norm_p_sigma = np.linalg.norm(self._p_sigma) # switching learning rate depending on search situation movement_phase = norm_p_sigma >= self._chi_n # distance weight w_dist_tmp = np.array( [ self._w_rank_hat[i] * self._w_dist_hat(z_k[i, :], lamb_feas) for i in range(self.population_size) ] ) w_dist = w_dist_tmp / sum(w_dist_tmp) - 1.0 / self.population_size # switching weights and learning rate w = w_dist if movement_phase else self._w_rank eta_sigma = ( self._eta_move_sigma if norm_p_sigma >= self._chi_n else self._eta_stag_sigma(lamb_feas) if norm_p_sigma >= 0.1 * self._chi_n else self._eta_conv_sigma(lamb_feas) ) eta_B = ( self._eta_move_B(lamb_feas) if norm_p_sigma >= self._chi_n else self._eta_stag_B(lamb_feas) if norm_p_sigma >= 0.1 * self._chi_n else self._eta_conv_B(lamb_feas) ) # natural gradient estimation in local coordinate G_delta = np.sum( [w[i] * z_k[i, :] for i in range(self.population_size)], axis=0 ) G_M = np.sum( [ w[i] * (np.outer(z_k[i, :], z_k[i, :]) - np.eye(self._n_dim)) for i in range(self.population_size) ], axis=0, ) G_sigma = G_M.trace() / self._n_dim G_B = G_M - G_sigma * np.eye(self._n_dim) # parameter update bBBT = self._B @ self._B.T self._mean += self._eta_mean * self._sigma * np.dot(self._B, G_delta) self._sigma *= math.exp((eta_sigma / 2.0) * G_sigma) # self._B = self._B.dot(expm((eta_B / 2.0) * G_B)) self._B = self._B.dot(_expm((eta_B / 2.0) * G_B)) aBBT = self._B @ self._B.T # emphasizing expansion e, v = np.linalg.eigh(bBBT) tau_vec = [ (v[:, i].reshape(self._n_dim, 1).T @ aBBT @ v[:, i].reshape(self._n_dim, 1)) / ( v[:, i].reshape(self._n_dim, 1).T @ bBBT @ v[:, i].reshape(self._n_dim, 1) ) - 1 for i in range(self._n_dim) ] flg_tau = [1.0 if tau_vec[i] > 0 else 0.0 for i in range(self._n_dim)] tau = max(tau_vec) gamma = max( (1.0 - self._c_gamma) * self._gamma + self._c_gamma * math.sqrt(1.0 + self._d_gamma * tau), 1.0, ) if movement_phase: Q = (gamma - 1.0) * np.sum( [flg_tau[i] * np.outer(v[:, i], v[:, i]) for i in range(self._n_dim)], axis=0, ) + np.eye(self._n_dim) stepQ = math.pow(np.linalg.det(Q), 1.0 / self._n_dim) self._sigma *= stepQ self._B = Q @ self._B / stepQ def should_stop(self) -> bool: A = self._B.dot(self._B.T) A = (A + A.T) / 2 E2, V = np.linalg.eigh(A) E = np.sqrt(np.where(E2 < 0, _EPS, E2)) diagA = np.diag(A) # Stop if the range of function values of the recent generation is below tolfun. if ( self.generation > self._funhist_term and np.max(self._funhist_values) - np.min(self._funhist_values) < self._tolfun ): return True # Stop if detecting divergent behavior. if self._sigma * np.max(E) > self._tolxup: return True # No effect coordinates: stop if adding 0.2-standard deviations # in any single coordinate does not change m. if np.any(self._mean == self._mean + (0.2 * self._sigma * np.sqrt(diagA))): return True # No effect axis: stop if adding 0.1-standard deviation vector in # any principal axis direction of C does not change m. "pycma" check # axis one by one at each generation. i = self.generation % self.dim if np.all(self._mean == self._mean + (0.1 * self._sigma * E[i] * V[:, i])): return True # Stop if the condition number of the covariance matrix exceeds 1e14. condition_cov = np.max(E) / np.min(E) if condition_cov > self._tolconditioncov: return True return False def _is_valid_bounds(bounds: Optional[np.ndarray], mean: np.ndarray) -> bool: if bounds is None: return True if (mean.size, 2) != bounds.shape: return False if not np.all(bounds[:, 0] <= mean): return False if not np.all(mean <= bounds[:, 1]): return False return True def _get_h_inv(dim: int) -> float: def f(a: float) -> float: return ((1.0 + a * a) * math.exp(a * a / 2.0) / 0.24) - 10.0 - dim def f_prime(a: float) -> float: return (1.0 / 0.24) * a * math.exp(a * a / 2.0) * (3.0 + a * a) h_inv = 6.0 while abs(f(h_inv)) > 1e-10: last = h_inv h_inv = h_inv - 0.5 * (f(h_inv) / f_prime(h_inv)) if abs(h_inv - last) < 1e-16: # Exit early since no further improvements are happening break return h_inv def _expm(mat: np.ndarray) -> np.ndarray: D, U = np.linalg.eigh(mat) expD = np.exp(D) return U @ np.diag(expD) @ U.T cmaes-0.10.0/cmaes/_sepcma.py000066400000000000000000000273601445566076200157600ustar00rootroot00000000000000from __future__ import annotations import math import numpy as np from typing import Any from typing import cast from typing import Optional _EPS = 1e-8 _MEAN_MAX = 1e32 _SIGMA_MAX = 1e32 class SepCMA: """Separable CMA-ES stochastic optimizer class with ask-and-tell interface. Example: .. code:: import numpy as np from cmaes import SepCMA def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 optimizer = SepCMA(mean=np.zeros(2), sigma=1.3) for generation in range(50): solutions = [] for _ in range(optimizer.population_size): # Ask a parameter x = optimizer.ask() value = quadratic(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") # Tell evaluation values. optimizer.tell(solutions) Args: mean: Initial mean vector of multi-variate gaussian distributions. sigma: Initial standard deviation of covariance matrix. bounds: Lower and upper domain boundaries for each parameter (optional). n_max_resampling: A maximum number of resampling parameters (default: 100). If all sampled parameters are infeasible, the last sampled one will be clipped with lower and upper bounds. seed: A seed number (optional). population_size: A population size (optional). """ def __init__( self, mean: np.ndarray, sigma: float, bounds: Optional[np.ndarray] = None, n_max_resampling: int = 100, seed: Optional[int] = None, population_size: Optional[int] = None, ): assert sigma > 0, "sigma must be non-zero positive value" assert np.all( np.abs(mean) < _MEAN_MAX ), f"Abs of all elements of mean vector must be less than {_MEAN_MAX}" n_dim = len(mean) assert n_dim > 1, "The dimension of mean must be larger than 1" if population_size is None: population_size = 4 + math.floor(3 * math.log(n_dim)) # (eq. 48) assert population_size > 0, "popsize must be non-zero positive value." mu = population_size // 2 # (eq.49) weights_prime = np.array( [math.log(mu + 1) - math.log(i + 1) for i in range(mu)] ) weights = weights_prime / sum(weights_prime) mu_eff = 1 / sum(weights**2) # learning rate for the rank-one update alpha_cov = 2 c1 = alpha_cov / ((n_dim + 1.3) ** 2 + mu_eff) # learning rate for the rank-μ update cmu_full = 2 / mu_eff / ((n_dim + np.sqrt(2)) ** 2) + (1 - 1 / mu_eff) * min( 1, (2 * mu_eff - 1) / ((n_dim + 2) ** 2 + mu_eff) ) cmu = (n_dim + 2) / 3 * cmu_full cm = 1 # (eq. 54) # learning rate for the cumulation for the step-size control c_sigma = (mu_eff + 2) / (n_dim + mu_eff + 3) d_sigma = 1 + 2 * max(0, math.sqrt((mu_eff - 1) / (n_dim + 1)) - 1) + c_sigma assert ( c_sigma < 1 ), "invalid learning rate for cumulation for the step-size control" # learning rate for cumulation for the rank-one update cc = 4 / (n_dim + 4) assert cc <= 1, "invalid learning rate for cumulation for the rank-one update" self._n_dim = n_dim self._popsize = population_size self._mu = mu self._mu_eff = mu_eff self._cc = cc self._c1 = c1 self._cmu = cmu self._c_sigma = c_sigma self._d_sigma = d_sigma self._cm = cm # E||N(0, I)|| (p.28) self._chi_n = math.sqrt(self._n_dim) * ( 1.0 - (1.0 / (4.0 * self._n_dim)) + 1.0 / (21.0 * (self._n_dim**2)) ) self._weights = weights # evolution path self._p_sigma = np.zeros(n_dim) self._pc = np.zeros(n_dim) self._mean = mean self._sigma = sigma self._D: Optional[np.ndarray] = None self._C: np.ndarray = np.ones(n_dim) # bounds contains low and high of each parameter. assert bounds is None or _is_valid_bounds(bounds, mean), "invalid bounds" self._bounds = bounds self._n_max_resampling = n_max_resampling self._g = 0 self._rng = np.random.RandomState(seed) # Termination criteria self._tolx = 1e-12 * sigma self._tolxup = 1e4 self._tolfun = 1e-12 self._tolconditioncov = 1e14 self._funhist_term = 10 + math.ceil(30 * n_dim / population_size) self._funhist_values = np.empty(self._funhist_term * 2) @property def dim(self) -> int: """A number of dimensions""" return self._n_dim @property def population_size(self) -> int: """A population size""" return self._popsize @property def generation(self) -> int: """Generation number which is monotonically incremented when multi-variate gaussian distribution is updated.""" return self._g def reseed_rng(self, seed: int) -> None: self._rng.seed(seed) def __getstate__(self) -> dict[str, Any]: attrs = {} for name in self.__dict__: # Remove _rng in pickle serialized object. if name == "_rng": continue attrs[name] = getattr(self, name) return attrs def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) # Set _rng for unpickled object. setattr(self, "_rng", np.random.RandomState()) def set_bounds(self, bounds: Optional[np.ndarray]) -> None: """Update boundary constraints""" assert bounds is None or _is_valid_bounds(bounds, self._mean), "invalid bounds" self._bounds = bounds def ask(self) -> np.ndarray: """Sample a parameter""" for i in range(self._n_max_resampling): x = self._sample_solution() if self._is_feasible(x): return x x = self._sample_solution() x = self._repair_infeasible_params(x) return x def _eigen_decomposition(self) -> np.ndarray: if self._D is not None: return self._D self._D = np.sqrt(np.where(self._C < 0, _EPS, self._C)) return self._D def _sample_solution(self) -> np.ndarray: D = self._eigen_decomposition() z = self._rng.randn(self._n_dim) # ~ N(0, I) y = D * z # ~ N(0, C) x = self._mean + self._sigma * y # ~ N(m, σ^2 C) return x def _is_feasible(self, param: np.ndarray) -> bool: if self._bounds is None: return True return cast( bool, np.all(param >= self._bounds[:, 0]) and np.all(param <= self._bounds[:, 1]), ) # Cast bool_ to bool def _repair_infeasible_params(self, param: np.ndarray) -> np.ndarray: if self._bounds is None: return param # clip with lower and upper bound. param = np.where(param < self._bounds[:, 0], self._bounds[:, 0], param) param = np.where(param > self._bounds[:, 1], self._bounds[:, 1], param) return param def tell(self, solutions: list[tuple[np.ndarray, float]]) -> None: """Tell evaluation values""" assert len(solutions) == self._popsize, "Must tell popsize-length solutions." for s in solutions: assert np.all( np.abs(s[0]) < _MEAN_MAX ), f"Abs of all param values must be less than {_MEAN_MAX} to avoid overflow errors" self._g += 1 solutions.sort(key=lambda s: s[1]) # Stores 'best' and 'worst' values of the # last 'self._funhist_term' generations. funhist_idx = 2 * (self.generation % self._funhist_term) self._funhist_values[funhist_idx] = solutions[0][1] self._funhist_values[funhist_idx + 1] = solutions[-1][1] # Sample new population of search_points, for k=1, ..., popsize D = self._eigen_decomposition() self._D = None x_k = np.array([s[0] for s in solutions]) # ~ N(m, σ^2 C) y_k = (x_k - self._mean) / self._sigma # ~ N(0, C) # Selection and recombination y_w = np.sum(y_k[: self._mu].T * self._weights[: self._mu], axis=1) self._mean += self._cm * self._sigma * y_w # Step-size control self._p_sigma = (1 - self._c_sigma) * self._p_sigma + math.sqrt( self._c_sigma * (2 - self._c_sigma) * self._mu_eff ) * (y_w / D) norm_p_sigma = np.linalg.norm(self._p_sigma) self._sigma *= np.exp( (self._c_sigma / self._d_sigma) * (norm_p_sigma / self._chi_n - 1) ) self._sigma = min(self._sigma, _SIGMA_MAX) # Covariance matrix adaption h_sigma_cond_left = norm_p_sigma / math.sqrt( 1 - (1 - self._c_sigma) ** (2 * (self._g + 1)) ) h_sigma_cond_right = (1.4 + 2 / (self._n_dim + 1)) * self._chi_n h_sigma = 1.0 if h_sigma_cond_left < h_sigma_cond_right else 0.0 # (p.28) # (eq.45) self._pc = (1 - self._cc) * self._pc + h_sigma * math.sqrt( self._cc * (2 - self._cc) * self._mu_eff ) * y_w delta_h_sigma = (1 - h_sigma) * self._cc * (2 - self._cc) # (p.28) assert delta_h_sigma <= 1 # (eq.47) rank_one = self._pc**2 rank_mu = np.sum( np.array([w * (y**2) for w, y in zip(self._weights, y_k)]), axis=0 ) self._C = ( ( 1 + self._c1 * delta_h_sigma - self._c1 - self._cmu * np.sum(self._weights) ) * self._C + self._c1 * rank_one + self._cmu * rank_mu ) def should_stop(self) -> bool: D = self._eigen_decomposition() # Stop if the range of function values of the recent generation is below tolfun. if ( self.generation > self._funhist_term and np.max(self._funhist_values) - np.min(self._funhist_values) < self._tolfun ): return True # Stop if the std of the normal distribution is smaller than tolx # in all coordinates and pc is smaller than tolx in all components. if np.all(self._sigma * self._C < self._tolx) and np.all( self._sigma * self._pc < self._tolx ): return True # Stop if detecting divergent behavior. if self._sigma * np.max(D) > self._tolxup: return True # No effect coordinates: stop if adding 0.2-standard deviations # in any single coordinate does not change m. if np.any(self._mean == self._mean + (0.2 * self._sigma * np.sqrt(self._C))): return True # No effect axis: stop if adding 0.1-standard deviation vector in # any principal axis direction of C does not change m. "pycma" check # axis one by one at each generation. i = self.generation % self.dim if np.all( self._mean == self._mean + (0.1 * self._sigma * D[i] * np.ones(self._n_dim)) ): return True # Stop if the condition number of the covariance matrix exceeds 1e14. condition_cov = np.max(D) / np.min(D) if condition_cov > self._tolconditioncov: return True return False def _is_valid_bounds(bounds: Optional[np.ndarray], mean: np.ndarray) -> bool: if bounds is None: return True if (mean.size, 2) != bounds.shape: return False if not np.all(bounds[:, 0] <= mean): return False if not np.all(mean <= bounds[:, 1]): return False return True cmaes-0.10.0/cmaes/_stats.py000066400000000000000000000017511445566076200156420ustar00rootroot00000000000000import math import numpy as np @np.vectorize def norm_cdf(x: float, loc: float = 0.0, scale: float = 1.0) -> float: x = (x - loc) / scale x = x / 2**0.5 z = abs(x) if z < 1 / 2**0.5: y = 0.5 + 0.5 * math.erf(x) else: y = 0.5 * math.erfc(z) if x > 0: y = 1.0 - y return y @np.vectorize def chi2_ppf(q: float) -> float: """ only deal with the special case df=1, loc=0, scale=1 solve chi2.cdf(x; df=1) = erf(sqrt(x/2)) = q with bisection method """ if q == 0: return 0.0 if q == 1: return math.inf a, b = 0.0, 100.0 if q < 0.9: for _ in range(100): m = (a + b) / 2 if math.erf(math.sqrt(m / 2)) < q: a = m else: b = m else: for _ in range(100): m = (a + b) / 2 if math.erfc(math.sqrt(m / 2)) > 1.0 - q: a = m else: b = m return m cmaes-0.10.0/cmaes/_warm_start.py000066400000000000000000000046631445566076200166740ustar00rootroot00000000000000from __future__ import annotations import math import numpy as np def get_warm_start_mgd( source_solutions: list[tuple[np.ndarray, float]], gamma: float = 0.1, alpha: float = 0.1, ) -> tuple[np.ndarray, float, np.ndarray]: """Estimates a promising distribution of the source task, then returns a multivariate gaussian distribution (the mean vector and the covariance matrix) used for initialization of the CMA-ES. Args: source_solutions: List of solutions (parameter, value) on a source task. gamma: top-(gamma x 100)% solutions are selected from a set of solutions on a source task. (default: 0.1). alpha: prior parameter for the initial covariance matrix (default: 0.1). Returns: The tuple of mean vector, sigma, and covariance matrix. """ # Paper: https://arxiv.org/abs/2012.06932 assert 0 < gamma <= 1, "gamma should be in (0, 1]" if len(source_solutions) == 0: raise ValueError("solutions should contain one or more items.") # Select top-(gamma x 100)% solutions source_solutions = sorted(source_solutions, key=lambda t: t[1]) gamma_n = math.floor(len(source_solutions) * gamma) assert gamma_n >= 1, "One or more solutions must be selected from a source task" dim = len(source_solutions[0][0]) top_gamma_solutions = np.empty( shape=( gamma_n, dim, ), dtype=float, ) for i in range(gamma_n): top_gamma_solutions[i] = source_solutions[i][0] # Estimation of a Promising Distribution of a Source Task. first_term = alpha**2 * np.eye(dim) cov_term = np.zeros(shape=(dim, dim), dtype=float) for i in range(gamma_n): cov_term += np.dot( top_gamma_solutions[i, :].reshape(dim, 1), top_gamma_solutions[i, :].reshape(dim, 1).T, ) second_term = cov_term / gamma_n mean_term = np.zeros( shape=( dim, 1, ), dtype=float, ) for i in range(gamma_n): mean_term += top_gamma_solutions[i, :].reshape(dim, 1) mean_term /= gamma_n third_term = np.dot(mean_term, mean_term.T) mu = mean_term mean = mu[:, 0] Sigma = first_term + second_term - third_term det_sigma = np.linalg.det(Sigma) sigma = math.pow(det_sigma, 1.0 / 2.0 / dim) cov = Sigma / math.pow(det_sigma, 1.0 / dim) return mean, sigma, cov cmaes-0.10.0/cmaes/_xnes.py000066400000000000000000000215101445566076200154540ustar00rootroot00000000000000from __future__ import annotations import math import numpy as np from typing import cast from typing import Optional _EPS = 1e-8 _MEAN_MAX = 1e32 _SIGMA_MAX = 1e32 class XNES: """xNES stochastic optimizer class with ask-and-tell interface. Example: .. code:: import numpy as np from cmaes import XNES def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 optimizer = XNES(mean=np.zeros(2), sigma=1.3) for generation in range(50): solutions = [] for _ in range(optimizer.population_size): # Ask a parameter x = optimizer.ask() value = quadratic(x[0], x[1]) solutions.append((x, value)) print(f"#{generation} {value} (x1={x[0]}, x2 = {x[1]})") # Tell evaluation values. optimizer.tell(solutions) Args: mean: Initial mean vector of multi-variate gaussian distributions. sigma: Initial standard deviation of covariance matrix. bounds: Lower and upper domain boundaries for each parameter (optional). n_max_resampling: A maximum number of resampling parameters (default: 100). If all sampled parameters are infeasible, the last sampled one will be clipped with lower and upper bounds. seed: A seed number (optional). population_size: A population size (optional). """ # Paper: https://dl.acm.org/doi/10.1145/1830483.1830557 def __init__( self, mean: np.ndarray, sigma: float, bounds: Optional[np.ndarray] = None, n_max_resampling: int = 100, seed: Optional[int] = None, population_size: Optional[int] = None, ): assert sigma > 0, "sigma must be non-zero positive value" assert np.all( np.abs(mean) < _MEAN_MAX ), f"Abs of all elements of mean vector must be less than {_MEAN_MAX}" n_dim = len(mean) assert n_dim > 1, "The dimension of mean must be larger than 1" if population_size is None: population_size = 4 + math.floor(3 * math.log(n_dim)) assert population_size > 0, "popsize must be non-zero positive value." w_hat = np.log(population_size / 2 + 1) - np.log( np.arange(1, population_size + 1) ) w_hat[np.where(w_hat < 0)] = 0 weights = w_hat / sum(w_hat) - (1.0 / population_size) self._n_dim = n_dim self._popsize = population_size # weights self._weights = weights # learning rate self._eta_mean = 1.0 self._eta_sigma = (3 / 5) * (3 + math.log(n_dim)) / (n_dim * math.sqrt(n_dim)) self._eta_B = self._eta_sigma # distribution parameter self._mean = mean.copy() self._sigma = sigma self._B = np.eye(n_dim) # bounds contains low and high of each parameter. assert bounds is None or _is_valid_bounds(bounds, mean), "invalid bounds" self._bounds = bounds self._n_max_resampling = n_max_resampling self._g = 0 self._rng = np.random.RandomState(seed) # Termination criteria self._tolx = 1e-12 * sigma self._tolxup = 1e4 self._tolfun = 1e-12 self._tolconditioncov = 1e14 self._funhist_term = 10 + math.ceil(30 * n_dim / population_size) self._funhist_values = np.empty(self._funhist_term * 2) @property def dim(self) -> int: """A number of dimensions""" return self._n_dim @property def population_size(self) -> int: """A population size""" return self._popsize @property def generation(self) -> int: """Generation number which is monotonically incremented when multi-variate gaussian distribution is updated.""" return self._g def reseed_rng(self, seed: int) -> None: self._rng.seed(seed) def set_bounds(self, bounds: Optional[np.ndarray]) -> None: """Update boundary constraints""" assert bounds is None or _is_valid_bounds(bounds, self._mean), "invalid bounds" self._bounds = bounds def ask(self) -> np.ndarray: """Sample a parameter""" for i in range(self._n_max_resampling): x = self._sample_solution() if self._is_feasible(x): return x x = self._sample_solution() x = self._repair_infeasible_params(x) return x def _sample_solution(self) -> np.ndarray: z = self._rng.randn(self._n_dim) # ~ N(0, I) x = self._mean + self._sigma * self._B.dot(z) # ~ N(m, σ^2 B B^T) return x def _is_feasible(self, param: np.ndarray) -> bool: if self._bounds is None: return True return cast( bool, np.all(param >= self._bounds[:, 0]) and np.all(param <= self._bounds[:, 1]), ) # Cast bool_ to bool. def _repair_infeasible_params(self, param: np.ndarray) -> np.ndarray: if self._bounds is None: return param # clip with lower and upper bound. param = np.where(param < self._bounds[:, 0], self._bounds[:, 0], param) param = np.where(param > self._bounds[:, 1], self._bounds[:, 1], param) return param def tell(self, solutions: list[tuple[np.ndarray, float]]) -> None: """Tell evaluation values""" assert len(solutions) == self._popsize, "Must tell popsize-length solutions." for s in solutions: assert np.all( np.abs(s[0]) < _MEAN_MAX ), f"Abs of all param values must be less than {_MEAN_MAX} to avoid overflow errors" self._g += 1 solutions.sort(key=lambda s: s[1]) # Stores 'best' and 'worst' values of the # last 'self._funhist_term' generations. funhist_idx = 2 * (self.generation % self._funhist_term) self._funhist_values[funhist_idx] = solutions[0][1] self._funhist_values[funhist_idx + 1] = solutions[-1][1] z_k = np.array( [ np.linalg.inv(self._sigma * self._B).dot(s[0] - self._mean) for s in solutions ] ) # natural gradient estimation in local coordinate G_delta = np.sum( [self._weights[i] * z_k[i, :] for i in range(self.population_size)], axis=0 ) G_M = np.sum( [ self._weights[i] * (np.outer(z_k[i, :], z_k[i, :]) - np.eye(self._n_dim)) for i in range(self.population_size) ], axis=0, ) G_sigma = G_M.trace() / self._n_dim G_B = G_M - G_sigma * np.eye(self._n_dim) # parameter update self._mean += self._eta_mean * self._sigma * np.dot(self._B, G_delta) self._sigma *= math.exp((self._eta_sigma / 2.0) * G_sigma) self._B = self._B.dot(_expm((self._eta_B / 2.0) * G_B)) def should_stop(self) -> bool: A = self._B.dot(self._B.T) A = (A + A.T) / 2 E2, V = np.linalg.eigh(A) E = np.sqrt(np.where(E2 < 0, _EPS, E2)) diagA = np.diag(A) # Stop if the range of function values of the recent generation is below tolfun. if ( self.generation > self._funhist_term and np.max(self._funhist_values) - np.min(self._funhist_values) < self._tolfun ): return True # Stop if detecting divergent behavior. if self._sigma * np.max(E) > self._tolxup: return True # No effect coordinates: stop if adding 0.2-standard deviations # in any single coordinate does not change m. if np.any(self._mean == self._mean + (0.2 * self._sigma * np.sqrt(diagA))): return True # No effect axis: stop if adding 0.1-standard deviation vector in # any principal axis direction of C does not change m. "pycma" check # axis one by one at each generation. i = self.generation % self.dim if np.all(self._mean == self._mean + (0.1 * self._sigma * E[i] * V[:, i])): return True # Stop if the condition number of the covariance matrix exceeds 1e14. condition_cov = np.max(E) / np.min(E) if condition_cov > self._tolconditioncov: return True return False def _is_valid_bounds(bounds: Optional[np.ndarray], mean: np.ndarray) -> bool: if bounds is None: return True if (mean.size, 2) != bounds.shape: return False if not np.all(bounds[:, 0] <= mean): return False if not np.all(mean <= bounds[:, 1]): return False return True def _expm(mat: np.ndarray) -> np.ndarray: D, U = np.linalg.eigh(mat) expD = np.exp(D) return U @ np.diag(expD) @ U.T cmaes-0.10.0/cmaes/cma.py000066400000000000000000000003231445566076200150770ustar00rootroot00000000000000import warnings from ._cma import CMA __all__ = ["CMA"] warnings.warn( "This module is deprecated. Please import CMA class from the " "package root (ex: from cmaes import CMA).", FutureWarning, ) cmaes-0.10.0/examples/000077500000000000000000000000001445566076200145155ustar00rootroot00000000000000cmaes-0.10.0/examples/bipop_cmaes.py000066400000000000000000000046311445566076200173540ustar00rootroot00000000000000import math import numpy as np from cmaes import CMA def ackley(x1, x2): return ( -20 * math.exp(-0.2 * math.sqrt(0.5 * (x1**2 + x2**2))) - math.exp(0.5 * (math.cos(2 * math.pi * x1) + math.cos(2 * math.pi * x2))) + math.e + 20 ) def main(): seed = 0 rng = np.random.RandomState(0) bounds = np.array([[-32.768, 32.768], [-32.768, 32.768]]) lower_bounds, upper_bounds = bounds[:, 0], bounds[:, 1] mean = lower_bounds + (rng.rand(2) * (upper_bounds - lower_bounds)) sigma = 32.768 * 2 / 5 # 1/5 of the domain width optimizer = CMA(mean=mean, sigma=sigma, bounds=bounds, seed=0) n_restarts = 0 # A small restart doesn't count in the n_restarts small_n_eval, large_n_eval = 0, 0 popsize0 = optimizer.population_size inc_popsize = 2 # Initial run is with "normal" population size; it is # the large population before first doubling, but its # budget accounting is the same as in case of small # population. poptype = "small" while n_restarts <= 5: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ackley(x[0], x[1]) solutions.append((x, value)) # print("{:10.5f} {:6.2f} {:6.2f}".format(value, x[0], x[1])) optimizer.tell(solutions) if optimizer.should_stop(): seed += 1 n_eval = optimizer.population_size * optimizer.generation if poptype == "small": small_n_eval += n_eval else: # poptype == "large" large_n_eval += n_eval if small_n_eval < large_n_eval: poptype = "small" popsize_multiplier = inc_popsize**n_restarts popsize = math.floor( popsize0 * popsize_multiplier ** (rng.uniform() ** 2) ) else: poptype = "large" n_restarts += 1 popsize = popsize0 * (inc_popsize**n_restarts) mean = lower_bounds + (rng.rand(2) * (upper_bounds - lower_bounds)) optimizer = CMA( mean=mean, sigma=sigma, bounds=bounds, seed=seed, population_size=popsize, ) print("Restart CMA-ES with popsize={} ({})".format(popsize, poptype)) if __name__ == "__main__": main() cmaes-0.10.0/examples/cmaes_with_margin_binary.py000066400000000000000000000025151445566076200221160ustar00rootroot00000000000000import numpy as np from cmaes import CMAwM def ellipsoid_onemax(x, n_zdim): n = len(x) n_rdim = n - n_zdim r = 10 if len(x) < 2: raise ValueError("dimension must be greater one") ellipsoid = sum([(1000 ** (i / (n_rdim - 1)) * x[i]) ** 2 for i in range(n_rdim)]) onemax = n_zdim - (0.0 < x[(n - n_zdim) :]).sum() return ellipsoid + r * onemax def main(): binary_dim, continuous_dim = 10, 10 dim = binary_dim + continuous_dim bounds = np.concatenate( [ np.tile([-np.inf, np.inf], (continuous_dim, 1)), np.tile([0, 1], (binary_dim, 1)), ] ) steps = np.concatenate([np.zeros(continuous_dim), np.ones(binary_dim)]) optimizer = CMAwM(mean=np.zeros(dim), sigma=2.0, bounds=bounds, steps=steps) print(" evals f(x)") print("====== ==========") evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x_for_eval, x_for_tell = optimizer.ask() value = ellipsoid_onemax(x_for_eval, binary_dim) evals += 1 solutions.append((x_for_tell, value)) if evals % 300 == 0: print(f"{evals:5d} {value:10.5f}") optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() cmaes-0.10.0/examples/cmaes_with_margin_integer.py000066400000000000000000000023031445566076200222620ustar00rootroot00000000000000import numpy as np from cmaes import CMAwM def ellipsoid_int(x, _): n = len(x) if len(x) < 2: raise ValueError("dimension must be greater one") return sum([(1000 ** (i / (n - 1)) * x[i]) ** 2 for i in range(n)]) def main(): integer_dim, continuous_dim = 10, 10 dim = integer_dim + continuous_dim bounds = np.concatenate( [ np.tile([-np.inf, np.inf], (continuous_dim, 1)), np.tile([-10, 11], (integer_dim, 1)), ] ) steps = np.concatenate([np.zeros(continuous_dim), np.ones(integer_dim)]) optimizer = CMAwM(mean=5 * np.ones(dim), sigma=2.0, bounds=bounds, steps=steps) print(" evals f(x)") print("====== ==========") evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x_for_eval, x_for_tell = optimizer.ask() value = ellipsoid_int(x_for_eval, integer_dim) evals += 1 solutions.append((x_for_tell, value)) if evals % 300 == 0: print(f"{evals:5d} {value:10.5f}") optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() cmaes-0.10.0/examples/ellipsoid_function.py000066400000000000000000000014571445566076200207670ustar00rootroot00000000000000import numpy as np from cmaes import CMA def ellipsoid(x): n = len(x) if len(x) < 2: raise ValueError("dimension must be greater one") return sum([(1000 ** (i / (n - 1)) * x[i]) ** 2 for i in range(n)]) def main(): dim = 40 optimizer = CMA(mean=3 * np.ones(dim), sigma=2.0) print(" evals f(x)") print("====== ==========") evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ellipsoid(x) evals += 1 solutions.append((x, value)) if evals % 3000 == 0: print(f"{evals:5d} {value:10.5f}") optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() cmaes-0.10.0/examples/ipop_cmaes.py000066400000000000000000000032101445566076200172020ustar00rootroot00000000000000import math import numpy as np from cmaes import CMA def ackley(x1, x2): return ( -20 * math.exp(-0.2 * math.sqrt(0.5 * (x1**2 + x2**2))) - math.exp(0.5 * (math.cos(2 * math.pi * x1) + math.cos(2 * math.pi * x2))) + math.e + 20 ) def main(): seed = 0 rng = np.random.RandomState(1) bounds = np.array([[-32.768, 32.768], [-32.768, 32.768]]) lower_bounds, upper_bounds = bounds[:, 0], bounds[:, 1] mean = lower_bounds + (rng.rand(2) * (upper_bounds - lower_bounds)) sigma = 32.768 * 2 / 5 # 1/5 of the domain width optimizer = CMA(mean=mean, sigma=sigma, bounds=bounds, seed=0) # Multiplier for increasing population size before each restart. inc_popsize = 2 print(" g f(x1,x2) x1 x2 ") print("=== ========== ====== ======") for generation in range(200): solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ackley(x[0], x[1]) solutions.append((x, value)) print(f"{generation:3d} {value:10.5f} {x[0]:6.2f} {x[1]:6.2f}") optimizer.tell(solutions) if optimizer.should_stop(): seed += 1 popsize = optimizer.population_size * inc_popsize mean = lower_bounds + (rng.rand(2) * (upper_bounds - lower_bounds)) optimizer = CMA( mean=mean, sigma=sigma, bounds=bounds, seed=seed, population_size=popsize, ) print("Restart CMA-ES with popsize={}".format(popsize)) if __name__ == "__main__": main() cmaes-0.10.0/examples/optuna_sampler.py000066400000000000000000000006561445566076200201270ustar00rootroot00000000000000import optuna def objective(trial: optuna.Trial): x1 = trial.suggest_float("x1", -4, 4) x2 = trial.suggest_float("x2", -4, 4) return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 def main(): optuna.logging.set_verbosity(optuna.logging.INFO) study = optuna.create_study(sampler=optuna.samplers.CmaEsSampler()) study.optimize(objective, n_trials=250, gc_after_trial=False) if __name__ == "__main__": main() cmaes-0.10.0/examples/quadratic_function.py000066400000000000000000000013451445566076200207540ustar00rootroot00000000000000import numpy as np from cmaes import CMA def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 def main(): optimizer = CMA(mean=np.zeros(2), sigma=1.3) print(" g f(x1,x2) x1 x2 ") print("=== ========== ====== ======") while True: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = quadratic(x[0], x[1]) solutions.append((x, value)) print( f"{optimizer.generation:3d} {value:10.5f}" f" {x[0]:6.2f} {x[1]:6.2f}" ) optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() cmaes-0.10.0/examples/sepcma_ellipsoid_function.py000066400000000000000000000014651445566076200223160ustar00rootroot00000000000000import numpy as np from cmaes import SepCMA def ellipsoid(x): n = len(x) if len(x) < 2: raise ValueError("dimension must be greater one") return sum([(1000 ** (i / (n - 1)) * x[i]) ** 2 for i in range(n)]) def main(): dim = 40 optimizer = SepCMA(mean=3 * np.ones(dim), sigma=2.0) print(" evals f(x)") print("====== ==========") evals = 0 while True: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = ellipsoid(x) evals += 1 solutions.append((x, value)) if evals % 3000 == 0: print(f"{evals:5d} {value:10.5f}") optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() cmaes-0.10.0/examples/ws_cma_es.py000066400000000000000000000024421445566076200170310ustar00rootroot00000000000000import numpy as np from cmaes import CMA, get_warm_start_mgd def source_task(x1: float, x2: float) -> float: b = 0.4 return (x1 - b) ** 2 + (x2 - b) ** 2 def target_task(x1: float, x2: float) -> float: b = 0.6 return (x1 - b) ** 2 + (x2 - b) ** 2 def main() -> None: # Generate solutions from a source task source_solutions = [] for _ in range(1000): x = np.random.random(2) value = source_task(x[0], x[1]) source_solutions.append((x, value)) # Estimate a promising distribution of the source task ws_mean, ws_sigma, ws_cov = get_warm_start_mgd( source_solutions, gamma=0.1, alpha=0.1 ) optimizer = CMA(mean=ws_mean, sigma=ws_sigma, cov=ws_cov) # Run WS-CMA-ES print(" g f(x1,x2) x1 x2 ") print("=== ========== ====== ======") while True: solutions = [] for _ in range(optimizer.population_size): x = optimizer.ask() value = target_task(x[0], x[1]) solutions.append((x, value)) print( f"{optimizer.generation:3d} {value:10.5f}" f" {x[0]:6.2f} {x[1]:6.2f}" ) optimizer.tell(solutions) if optimizer.should_stop(): break if __name__ == "__main__": main() cmaes-0.10.0/fuzzing.py000066400000000000000000000020111445566076200147370ustar00rootroot00000000000000import sys import atheris import hypothesis.extra.numpy as npst from hypothesis import given, strategies as st from cmaes import CMA @given(data=st.data()) def test_cma_tell(data): dim = data.draw(st.integers(min_value=2, max_value=100)) mean = data.draw(npst.arrays(dtype=float, shape=dim)) sigma = data.draw(st.floats(min_value=1e-16)) n_iterations = data.draw(st.integers(min_value=1)) try: optimizer = CMA(mean, sigma) except AssertionError: return popsize = optimizer.population_size for _ in range(n_iterations): tell_solutions = data.draw( st.lists( st.tuples(npst.arrays(dtype=float, shape=dim), st.floats()), min_size=popsize, max_size=popsize, ) ) optimizer.ask() try: optimizer.tell(tell_solutions) except AssertionError: return optimizer.ask() atheris.Setup(sys.argv, test_cma_tell.hypothesis.fuzz_one_input) atheris.Fuzz() cmaes-0.10.0/pyproject.toml000066400000000000000000000025741445566076200156230ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" [project] name = "cmaes" description = "Lightweight Covariance Matrix Adaptation Evolution Strategy (CMA-ES) implementation for Python 3." readme = "README.md" authors = [ { name = "Masashi Shibata", "email" = "m.shibata1020@gmail.com" } ] maintainers = [ { name = "Masahiro Nomura", "email" = "nomura_masahiro@cyberagent.co.jp" } ] requires-python = ">=3.7" license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Science/Research", ] dependencies = ["numpy"] dynamic = ["version"] [project.optional-dependencies] cmawm = ["scipy"] [project.urls] "Homepage" = "https://github.com/CyberAgentAILab/cmaes" [tool.setuptools.dynamic] version = {attr = "cmaes.__version__"} [tool.setuptools] packages = ["cmaes"] include-package-data = false [tool.mypy] ignore_missing_imports = true disallow_untyped_defs = true cmaes-0.10.0/requirements-bench.txt000066400000000000000000000000241445566076200172340ustar00rootroot00000000000000kurobako cma optuna cmaes-0.10.0/requirements-dev.txt000066400000000000000000000001731445566076200167400ustar00rootroot00000000000000# install_requires numpy>=1.20.0 # visualization matplotlib scipy # Fuzzing hypothesis atheris # lint mypy flake8 black cmaes-0.10.0/setup.cfg000066400000000000000000000001501445566076200145140ustar00rootroot00000000000000[flake8] ignore = E203, W503 max-line-length = 100 statistics = True exclude = venv,build,.eggs cmaes-0.10.0/setup.py000066400000000000000000000001051445566076200144050ustar00rootroot00000000000000from setuptools import setup if __name__ == "__main__": setup() cmaes-0.10.0/tests/000077500000000000000000000000001445566076200140415ustar00rootroot00000000000000cmaes-0.10.0/tests/__init__.py000066400000000000000000000000001445566076200161400ustar00rootroot00000000000000cmaes-0.10.0/tests/test_boundary.py000066400000000000000000000041051445566076200172750ustar00rootroot00000000000000import numpy as np from unittest import TestCase from cmaes import CMA, SepCMA CMA_CLASSES = [CMA, SepCMA] class TestCMABoundary(TestCase): def test_valid_dimension(self): for CmaClass in CMA_CLASSES: with self.subTest(f"Class: {CmaClass.__name__}"): CmaClass( mean=np.zeros(2), sigma=1.3, bounds=np.array([[-10, 10], [-10, 10]]) ) def test_invalid_dimension(self): for CmaClass in CMA_CLASSES: with self.subTest(f"Class: {CmaClass.__name__}"): with self.assertRaises(AssertionError): CmaClass(mean=np.zeros(2), sigma=1.3, bounds=np.array([-10, 10])) def test_mean_located_out_of_bounds(self): mean = np.zeros(5) bounds = np.empty(shape=(5, 2)) bounds[:, 0], bounds[:, 1] = 1.0, 5.0 for CmaClass in CMA_CLASSES: with self.subTest(f"Class: {CmaClass.__name__}"): with self.assertRaises(AssertionError): CmaClass(mean=mean, sigma=1.3, bounds=bounds) def test_set_valid_bounds(self): for CmaClass in CMA_CLASSES: with self.subTest(f"Class: {CmaClass.__name__}"): optimizer = CmaClass(mean=np.zeros(2), sigma=1.3) optimizer.set_bounds(bounds=np.array([[-10, 10], [-10, 10]])) def test_set_invalid_bounds(self): for CmaClass in CMA_CLASSES: with self.subTest(f"Class: {CmaClass.__name__}"): optimizer = CmaClass(mean=np.zeros(2), sigma=1.3) with self.assertRaises(AssertionError): optimizer.set_bounds(bounds=np.array([-10, 10])) def test_set_bounds_which_does_not_contain_mean(self): for CmaClass in CMA_CLASSES: with self.subTest(f"Class: {CmaClass.__name__}"): optimizer = CmaClass(mean=np.zeros(2), sigma=1.3) bounds = np.empty(shape=(5, 2)) bounds[:, 0], bounds[:, 1] = 1.0, 5.0 with self.assertRaises(AssertionError): optimizer.set_bounds(bounds) cmaes-0.10.0/tests/test_cmawm.py000066400000000000000000000023151445566076200165570ustar00rootroot00000000000000import warnings import numpy as np from numpy.testing import assert_almost_equal from unittest import TestCase from cmaes import CMA, CMAwM class TestCMAwM(TestCase): def test_no_discrete_spaces(self): mean = np.zeros(2) bounds = np.array([[-10, 10], [-10, 10]]) steps = np.array([0, 0]) sigma = 1.3 seed = 1 cma_optimizer = CMA(mean=mean, sigma=sigma, bounds=bounds, seed=seed) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=UserWarning) cmawm_optimizer = CMAwM( mean=mean, sigma=sigma, bounds=bounds, steps=steps, seed=seed ) for i in range(100): solutions = [] for _ in range(cma_optimizer.population_size): cma_x = cma_optimizer.ask() cmawm_x_encoded, cmawm_x_for_tell = cmawm_optimizer.ask() assert_almost_equal(cma_x, cmawm_x_encoded) assert_almost_equal(cma_x, cmawm_x_for_tell) objective = (cma_x[0] - 3) ** 2 + cma_x[1] ** 2 solutions.append((cma_x, objective)) cma_optimizer.tell(solutions) cmawm_optimizer.tell(solutions) cmaes-0.10.0/tests/test_compress_symmetric.py000066400000000000000000000021611445566076200214010ustar00rootroot00000000000000import numpy as np from unittest import TestCase from cmaes._cma import _decompress_symmetric, _compress_symmetric class TestCompressSymmetric(TestCase): def test_compress_symmetric_odd(self): sym2d = np.array([[1, 2], [2, 3]]) actual = _compress_symmetric(sym2d) expected = np.array([1, 2, 3]) self.assertTrue(np.all(np.equal(actual, expected))) def test_compress_symmetric_even(self): sym2d = np.array([[1, 2, 3], [2, 4, 5], [3, 5, 6]]) actual = _compress_symmetric(sym2d) expected = np.array([1, 2, 3, 4, 5, 6]) self.assertTrue(np.all(np.equal(actual, expected))) def test_decompress_symmetric_odd(self): sym1d = np.array([1, 2, 3]) actual = _decompress_symmetric(sym1d) expected = np.array([[1, 2], [2, 3]]) self.assertTrue(np.all(np.equal(actual, expected))) def test_decompress_symmetric_even(self): sym1d = np.array([1, 2, 3, 4, 5, 6]) actual = _decompress_symmetric(sym1d) expected = np.array([[1, 2, 3], [2, 4, 5], [3, 5, 6]]) self.assertTrue(np.all(np.equal(actual, expected))) cmaes-0.10.0/tests/test_fuzzing.py000066400000000000000000000040031445566076200171430ustar00rootroot00000000000000import hypothesis.extra.numpy as npst import unittest from hypothesis import given, strategies as st from cmaes import CMA, SepCMA class TestFuzzing(unittest.TestCase): @given( data=st.data(), ) def test_cma_tell(self, data): dim = data.draw(st.integers(min_value=2, max_value=100)) mean = data.draw(npst.arrays(dtype=float, shape=dim)) sigma = data.draw(st.floats(min_value=1e-16)) n_iterations = data.draw(st.integers(min_value=1)) try: optimizer = CMA(mean, sigma) except AssertionError: return popsize = optimizer.population_size for _ in range(n_iterations): tell_solutions = data.draw( st.lists( st.tuples(npst.arrays(dtype=float, shape=dim), st.floats()), min_size=popsize, max_size=popsize, ) ) optimizer.ask() try: optimizer.tell(tell_solutions) except AssertionError: return optimizer.ask() @given( data=st.data(), ) def test_sepcma_tell(self, data): dim = data.draw(st.integers(min_value=2, max_value=100)) mean = data.draw(npst.arrays(dtype=float, shape=dim)) sigma = data.draw(st.floats(min_value=1e-16)) n_iterations = data.draw(st.integers(min_value=1)) try: optimizer = SepCMA(mean, sigma) except AssertionError: return popsize = optimizer.population_size for _ in range(n_iterations): tell_solutions = data.draw( st.lists( st.tuples(npst.arrays(dtype=float, shape=dim), st.floats()), min_size=popsize, max_size=popsize, ) ) optimizer.ask() try: optimizer.tell(tell_solutions) except AssertionError: return optimizer.ask() cmaes-0.10.0/tests/test_stats.py000066400000000000000000000027161445566076200166160ustar00rootroot00000000000000import math from unittest import TestCase from cmaes import _stats # Test Cases in this file is generated by SciPy v1.9.3 class TestNormCDF(TestCase): def test_standard_normal_distribution(self): self.assertAlmostEqual(_stats.norm_cdf(-30), 4.906713927147907e-198, places=205) self.assertAlmostEqual(_stats.norm_cdf(-10), 7.619853024160469e-24, places=30) self.assertAlmostEqual(_stats.norm_cdf(-1), 0.15865525393145707) self.assertAlmostEqual(_stats.norm_cdf(0), 0.5) self.assertAlmostEqual(_stats.norm_cdf(1), 0.8413447460685429) self.assertAlmostEqual( _stats.norm_cdf(8), 0.9999999999999993338661852249060757458209991455078125, places=30, ) self.assertAlmostEqual(_stats.norm_cdf(10), 1.0) def test_mu_and_sigma(self): self.assertAlmostEqual(_stats.norm_cdf(1, loc=2, scale=3), 0.36944134018176367) class TestChi2PPF(TestCase): def test(self): self.assertAlmostEqual(_stats.chi2_ppf(0.0), 0.0) self.assertAlmostEqual( _stats.chi2_ppf(0.00000001), 1.5707963267948962e-16, places=25 ) self.assertAlmostEqual(_stats.chi2_ppf(0.5), 0.454936423119572) self.assertAlmostEqual(_stats.chi2_ppf(0.99999999), 32.84125335146885) self.assertAlmostEqual( _stats.chi2_ppf(0.999999999999999777955395074969), 67.39648382445012 ) self.assertAlmostEqual(_stats.chi2_ppf(1.0), math.inf) cmaes-0.10.0/tests/test_termination_criterion.py000066400000000000000000000016121445566076200220610ustar00rootroot00000000000000import numpy as np from unittest import TestCase from cmaes import CMA class TestTerminationCriterion(TestCase): def test_stop_if_objective_values_are_not_changed(self): optimizer = CMA(mean=np.zeros(2), sigma=1.3) popsize = optimizer.population_size rng = np.random.RandomState(seed=1) for i in range(optimizer._funhist_term + 1): self.assertFalse(optimizer.should_stop()) optimizer.tell([(rng.randn(2), 0.01) for _ in range(popsize)]) self.assertTrue(optimizer.should_stop()) def test_stop_if_detect_divergent_behavior(self): optimizer = CMA(mean=np.zeros(2), sigma=1e-4) popsize = optimizer.population_size nd_rng = np.random.RandomState(1) solutions = [(100 * nd_rng.randn(2), 0.01) for _ in range(popsize)] optimizer.tell(solutions) self.assertTrue(optimizer.should_stop()) cmaes-0.10.0/tests/test_warm_start.py000066400000000000000000000006201445566076200176330ustar00rootroot00000000000000import numpy as np from unittest import TestCase from cmaes import CMA, get_warm_start_mgd class TestWarmStartCMA(TestCase): def test_dimension(self): optimizer = CMA(mean=np.zeros(10), sigma=1.3) source_solutions = [(optimizer.ask(), 0.0) for _ in range(100)] ws_mean, ws_sigma, ws_cov = get_warm_start_mgd(source_solutions) self.assertEqual(ws_mean.size, 10) cmaes-0.10.0/tools/000077500000000000000000000000001445566076200140375ustar00rootroot00000000000000cmaes-0.10.0/tools/cmaes_visualizer.py000066400000000000000000000200351445566076200177560ustar00rootroot00000000000000""" Usage: cmaes_visualizer.py OPTIONS Optional arguments: -h, --help show this help message and exit --function {quadratic,himmelblau,rosenbrock,six-hump-camel} --seed SEED --frames FRAMES --interval INTERVAL --pop-per-frame POP_PER_FRAME --restart-strategy {ipop,bipop} Example: python3 cmaes_visualizer.py --function six-hump-camel --pop-per-frame 2 python3 tools/cmaes_visualizer.py --function himmelblau \ --restart-strategy ipop --frames 500 --interval 10 --pop-per-frame 6 """ import argparse import math import numpy as np from scipy import stats from matplotlib.colors import LinearSegmentedColormap import matplotlib.pyplot as plt import matplotlib.animation as animation from pylab import rcParams from cmaes._cma import CMA parser = argparse.ArgumentParser() parser.add_argument( "--function", choices=["quadratic", "himmelblau", "rosenbrock", "six-hump-camel"], ) parser.add_argument( "--seed", type=int, default=1, ) parser.add_argument( "--frames", type=int, default=100, ) parser.add_argument( "--interval", type=int, default=20, ) parser.add_argument( "--pop-per-frame", type=int, default=1, ) parser.add_argument( "--restart-strategy", choices=["ipop", "bipop"], default="", ) args = parser.parse_args() rcParams["figure.figsize"] = 10, 5 fig, (ax1, ax2) = plt.subplots(1, 2) color_dict = { "red": ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), "green": ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), "blue": ((0.0, 1.0, 1.0), (1.0, 1.0, 1.0)), "yellow": ((1.0, 1.0, 1.0), (1.0, 1.0, 1.0)), } bw = LinearSegmentedColormap("BlueWhile", color_dict) def himmelbleu(x1, x2): return (x1**2 + x2 - 11.0) ** 2 + (x1 + x2**2 - 7.0) ** 2 def himmelbleu_contour(x1, x2): return np.log(himmelbleu(x1, x2) + 1) def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 def quadratic_contour(x1, x2): return np.log(quadratic(x1, x2) + 1) def rosenbrock(x1, x2): return 100 * (x2 - x1**2) ** 2 + (x1 - 1) ** 2 def rosenbrock_contour(x1, x2): return np.log(rosenbrock(x1, x2) + 1) def six_hump_camel(x1, x2): return ( (4 - 2.1 * (x1**2) + (x1**4) / 3) * (x1**2) + x1 * x2 + (-4 + 4 * x2**2) * (x2**2) ) def six_hump_camel_contour(x1, x2): return np.log(six_hump_camel(x1, x2) + 1.0316) function_name = "" if args.function == "quadratic": function_name = "Quadratic function" objective = quadratic contour_function = quadratic_contour global_minimums = [ (3.0, -2.0), ] # input domain x1_lower_bound, x1_upper_bound = -4, 4 x2_lower_bound, x2_upper_bound = -4, 4 elif args.function == "himmelblau": function_name = "Himmelblau function" objective = himmelbleu contour_function = himmelbleu_contour global_minimums = [ (3.0, 2.0), (-2.805118, 3.131312), (-3.779310, -3.283186), (3.584428, -1.848126), ] # input domain x1_lower_bound, x1_upper_bound = -4, 4 x2_lower_bound, x2_upper_bound = -4, 4 elif args.function == "rosenbrock": # https://www.sfu.ca/~ssurjano/rosen.html function_name = "Rosenbrock function" objective = rosenbrock contour_function = rosenbrock_contour global_minimums = [ (1, 1), ] # input domain x1_lower_bound, x1_upper_bound = -5, 10 x2_lower_bound, x2_upper_bound = -5, 10 elif args.function == "six-hump-camel": # https://www.sfu.ca/~ssurjano/camel6.html function_name = "Six-hump camel function" objective = six_hump_camel contour_function = six_hump_camel_contour global_minimums = [ (0.0898, -0.7126), (-0.0898, 0.7126), ] # input domain x1_lower_bound, x1_upper_bound = -3, 3 x2_lower_bound, x2_upper_bound = -2, 2 else: raise ValueError("invalid function type") seed = args.seed bounds = np.array([[x1_lower_bound, x1_upper_bound], [x2_lower_bound, x2_upper_bound]]) sigma = (x1_upper_bound - x2_lower_bound) / 5 optimizer = CMA(mean=np.zeros(2), sigma=sigma, bounds=bounds, seed=seed) solutions = [] trial_number = 0 rng = np.random.RandomState(seed) # Variables for IPOP and BIPOP inc_popsize = 2 n_restarts = 0 # A small restart doesn't count in the n_restarts small_n_eval, large_n_eval = 0, 0 popsize0 = optimizer.population_size poptype = "small" def init(): ax1.set_xlim(x1_lower_bound, x1_upper_bound) ax1.set_ylim(x2_lower_bound, x2_upper_bound) ax2.set_xlim(x1_lower_bound, x1_upper_bound) ax2.set_ylim(x2_lower_bound, x2_upper_bound) # Plot 4 local minimum value for m in global_minimums: ax1.plot(m[0], m[1], "y*", ms=10) ax2.plot(m[0], m[1], "y*", ms=10) # Plot contour of himmelbleu function x1 = np.arange(x1_lower_bound, x1_upper_bound, 0.01) x2 = np.arange(x2_lower_bound, x2_upper_bound, 0.01) x1, x2 = np.meshgrid(x1, x2) ax1.contour(x1, x2, contour_function(x1, x2), 30, cmap=bw) def get_next_popsize(): global optimizer, n_restarts, poptype, small_n_eval, large_n_eval if args.restart_strategy == "ipop": n_restarts += 1 popsize = optimizer.population_size * inc_popsize print(f"Restart CMA-ES with popsize={popsize} at trial={trial_number}") return popsize elif args.restart_strategy == "bipop": n_eval = optimizer.population_size * optimizer.generation if poptype == "small": small_n_eval += n_eval else: # poptype == "large" large_n_eval += n_eval if small_n_eval < large_n_eval: poptype = "small" popsize_multiplier = inc_popsize**n_restarts popsize = math.floor(popsize0 * popsize_multiplier ** (rng.uniform() ** 2)) else: poptype = "large" n_restarts += 1 popsize = popsize0 * (inc_popsize**n_restarts) print( f"Restart CMA-ES with popsize={popsize} ({poptype}) at trial={trial_number}" ) return raise Exception("must not reach here") def update(frame): global solutions, optimizer, trial_number if len(solutions) == optimizer.population_size: optimizer.tell(solutions) solutions = [] if optimizer.should_stop(): popsize = get_next_popsize() lower_bounds, upper_bounds = bounds[:, 0], bounds[:, 1] mean = lower_bounds + (rng.rand(2) * (upper_bounds - lower_bounds)) optimizer = CMA( mean=mean, sigma=sigma, bounds=bounds, seed=seed, population_size=popsize, ) n_sample = min(optimizer.population_size - len(solutions), args.pop_per_frame) for i in range(n_sample): x = optimizer.ask() evaluation = objective(x[0], x[1]) # Plot sample points ax1.plot(x[0], x[1], "o", c="r", label="2d", alpha=0.5) solution = ( x, evaluation, ) solutions.append(solution) trial_number += n_sample # Update title if args.restart_strategy == "ipop": fig.suptitle( f"IPOP-CMA-ES {function_name} trial={trial_number} " f"popsize={optimizer.population_size}" ) elif args.restart_strategy == "bipop": fig.suptitle( f"BIPOP-CMA-ES {function_name} trial={trial_number} " f"popsize={optimizer.population_size} ({poptype})" ) else: fig.suptitle(f"CMA-ES {function_name} trial={trial_number}") # Plot multivariate gaussian distribution of CMA-ES x, y = np.mgrid[ x1_lower_bound:x1_upper_bound:0.01, x2_lower_bound:x2_upper_bound:0.01 ] rv = stats.multivariate_normal(optimizer._mean, optimizer._C) pos = np.dstack((x, y)) ax2.contourf(x, y, rv.pdf(pos)) if frame % 50 == 0: print(f"Processing frame {frame}") def main(): ani = animation.FuncAnimation( fig, update, frames=args.frames, init_func=init, blit=False, interval=args.interval, ) ani.save(f"./tmp/{args.function}.mp4") if __name__ == "__main__": main() cmaes-0.10.0/tools/optuna_profile.py000066400000000000000000000021171445566076200174400ustar00rootroot00000000000000import argparse import cProfile import logging import pstats import optuna parser = argparse.ArgumentParser() parser.add_argument("--storage", choices=["memory", "sqlite"], default="memory") parser.add_argument("--params", type=int, default=100) parser.add_argument("--trials", type=int, default=1000) args = parser.parse_args() def objective(trial: optuna.Trial): val = 0 for i in range(args.params): xi = trial.suggest_uniform(str(i), -4, 4) val += (xi - 2) ** 2 return val def main(): logging.disable(level=logging.INFO) storage = None if args.storage == "sqlite": storage = f"sqlite:///db-{args.trials}-{args.params}.sqlite3" sampler = optuna.samplers.CmaEsSampler() study = optuna.create_study(sampler=sampler, storage=storage) profiler = cProfile.Profile() profiler.runcall( study.optimize, objective, n_trials=args.trials, gc_after_trial=False ) profiler.dump_stats("profile.stats") stats = pstats.Stats("profile.stats") stats.sort_stats("time").print_stats(5) if __name__ == "__main__": main() cmaes-0.10.0/tools/ws_cmaes_visualizer.py000066400000000000000000000160041445566076200204700ustar00rootroot00000000000000""" Usage: python3 tools/ws_cmaes_visualizer.py OPTIONS Optional arguments: -h, --help show this help message and exit --function {quadratic,himmelblau,rosenbrock,six-hump-camel,sphere,rot-ellipsoid} --seed SEED --alpha ALPHA --gamma GAMMA --frames FRAMES --interval INTERVAL --pop-per-frame POP_PER_FRAME Example: python3 ws_cmaes_visualizer.py --function rot-ellipsoid """ import argparse import math import numpy as np from scipy import stats from matplotlib.colors import LinearSegmentedColormap import matplotlib.pyplot as plt import matplotlib.animation as animation from pylab import rcParams from cmaes import get_warm_start_mgd parser = argparse.ArgumentParser() parser.add_argument( "--function", choices=[ "quadratic", "himmelblau", "rosenbrock", "six-hump-camel", "sphere", "rot-ellipsoid", ], ) parser.add_argument( "--seed", type=int, default=1, ) parser.add_argument( "--alpha", type=float, default=0.1, ) parser.add_argument( "--gamma", type=float, default=0.1, ) parser.add_argument( "--frames", type=int, default=100, ) parser.add_argument( "--interval", type=int, default=20, ) parser.add_argument( "--pop-per-frame", type=int, default=10, ) args = parser.parse_args() rcParams["figure.figsize"] = 10, 5 fig, (ax1, ax2) = plt.subplots(1, 2) color_dict = { "red": ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), "green": ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), "blue": ((0.0, 1.0, 1.0), (1.0, 1.0, 1.0)), "yellow": ((1.0, 1.0, 1.0), (1.0, 1.0, 1.0)), } bw = LinearSegmentedColormap("BlueWhile", color_dict) def himmelbleu(x1, x2): return (x1**2 + x2 - 11.0) ** 2 + (x1 + x2**2 - 7.0) ** 2 def himmelbleu_contour(x1, x2): return np.log(himmelbleu(x1, x2) + 1) def quadratic(x1, x2): return (x1 - 3) ** 2 + (10 * (x2 + 2)) ** 2 def quadratic_contour(x1, x2): return np.log(quadratic(x1, x2) + 1) def rosenbrock(x1, x2): return 100 * (x2 - x1**2) ** 2 + (x1 - 1) ** 2 def rosenbrock_contour(x1, x2): return np.log(rosenbrock(x1, x2) + 1) def six_hump_camel(x1, x2): return ( (4 - 2.1 * (x1**2) + (x1**4) / 3) * (x1**2) + x1 * x2 + (-4 + 4 * x2**2) * (x2**2) ) def six_hump_camel_contour(x1, x2): return np.log(six_hump_camel(x1, x2) + 1.0316) def sphere(x1, x2): offset = 0.6 return (x1 - offset) ** 2 + (x2 - offset) ** 2 def sphere_contour(x1, x2): return np.log(sphere(x1, x2) + 1) def ellipsoid(x1, x2): offset = 0.6 scale = 5**2 return (x1 - offset) ** 2 + scale * (x2 - offset) ** 2 def rot_ellipsoid(x1, x2): rot_x1 = math.sqrt(3.0) / 2.0 * x1 + 1.0 / 2.0 * x2 rot_x2 = 1.0 / 2.0 * x1 + math.sqrt(3.0) / 2.0 * x2 return ellipsoid(rot_x1, rot_x2) def rot_ellipsoid_contour(x1, x2): return np.log(rot_ellipsoid(x1, x2) + 1) function_name = "" if args.function == "quadratic": function_name = "Quadratic function" objective = quadratic contour_function = quadratic_contour global_minimums = [ (3.0, -2.0), ] # input domain x1_lower_bound, x1_upper_bound = -4, 4 x2_lower_bound, x2_upper_bound = -4, 4 elif args.function == "himmelblau": function_name = "Himmelblau function" objective = himmelbleu contour_function = himmelbleu_contour global_minimums = [ (3.0, 2.0), (-2.805118, 3.131312), (-3.779310, -3.283186), (3.584428, -1.848126), ] # input domain x1_lower_bound, x1_upper_bound = -4, 4 x2_lower_bound, x2_upper_bound = -4, 4 elif args.function == "rosenbrock": # https://www.sfu.ca/~ssurjano/rosen.html function_name = "Rosenbrock function" objective = rosenbrock contour_function = rosenbrock_contour global_minimums = [ (1, 1), ] # input domain x1_lower_bound, x1_upper_bound = -5, 10 x2_lower_bound, x2_upper_bound = -5, 10 elif args.function == "six-hump-camel": # https://www.sfu.ca/~ssurjano/camel6.html function_name = "Six-hump camel function" objective = six_hump_camel contour_function = six_hump_camel_contour global_minimums = [ (0.0898, -0.7126), (-0.0898, 0.7126), ] # input domain x1_lower_bound, x1_upper_bound = -3, 3 x2_lower_bound, x2_upper_bound = -2, 2 elif args.function == "sphere": function_name = "Sphere function with offset=0.6" objective = sphere contour_function = sphere_contour global_minimums = [ (0.6, 0.6), ] # input domain x1_lower_bound, x1_upper_bound = 0, 1 x2_lower_bound, x2_upper_bound = 0, 1 elif args.function == "rot-ellipsoid": function_name = "Rot Ellipsoid function with offset=0.6" objective = rot_ellipsoid contour_function = rot_ellipsoid_contour global_minimums = [] # input domain x1_lower_bound, x1_upper_bound = 0, 1 x2_lower_bound, x2_upper_bound = 0, 1 else: raise ValueError("invalid function type") seed = args.seed rng = np.random.RandomState(seed) solutions = [] def init(): ax1.set_xlim(x1_lower_bound, x1_upper_bound) ax1.set_ylim(x2_lower_bound, x2_upper_bound) ax2.set_xlim(x1_lower_bound, x1_upper_bound) ax2.set_ylim(x2_lower_bound, x2_upper_bound) # Plot 4 local minimum value for m in global_minimums: ax1.plot(m[0], m[1], "y*", ms=10) ax2.plot(m[0], m[1], "y*", ms=10) # Plot contour of the function x1 = np.arange(x1_lower_bound, x1_upper_bound, 0.01) x2 = np.arange(x2_lower_bound, x2_upper_bound, 0.01) x1, x2 = np.meshgrid(x1, x2) ax1.contour(x1, x2, contour_function(x1, x2), 30, cmap=bw) def update(frame): global solutions for i in range(args.pop_per_frame): x1 = (x1_upper_bound - x1_lower_bound) * rng.random() + x1_lower_bound x2 = (x2_upper_bound - x2_lower_bound) * rng.random() + x2_lower_bound evaluation = objective(x1, x2) # Plot sample points ax1.plot(x1, x2, "o", c="r", label="2d", alpha=0.5) solution = ( np.array([x1, x2], dtype=float), evaluation, ) solutions.append(solution) # Update title fig.suptitle( f"WS-CMA-ES {function_name} with alpha={args.alpha} and gamma={args.gamma} (frame={frame})" ) # Plot multivariate gaussian distribution of CMA-ES x, y = np.mgrid[ x1_lower_bound:x1_upper_bound:0.01, x2_lower_bound:x2_upper_bound:0.01 ] if math.floor(len(solutions) * args.alpha) > 1: mean, sigma, cov = get_warm_start_mgd( solutions, alpha=args.alpha, gamma=args.gamma ) rv = stats.multivariate_normal(mean, cov) pos = np.dstack((x, y)) ax2.contourf(x, y, rv.pdf(pos)) if frame % 50 == 0: print(f"Processing frame {frame}") def main(): ani = animation.FuncAnimation( fig, update, frames=args.frames, init_func=init, blit=False, interval=args.interval, ) ani.save(f"./tmp/{args.function}.mp4") if __name__ == "__main__": main()