├── .coveragerc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── doc ├── Makefile └── source │ ├── acquisition.rst │ ├── apiAndArchitecture.rst │ ├── bayesianoptimizer.rst │ ├── conf.py │ ├── designs.rst │ ├── index.rst │ ├── interfaces.rst │ ├── intro.rst │ ├── notebooks │ ├── airline.npz │ ├── constrained_bo.ipynb │ ├── firststeps.ipynb │ ├── hyperopt.ipynb │ ├── mes_benchmark.ipynb │ ├── multiobjective.ipynb │ ├── new_acquisition.ipynb │ └── structure.ipynb │ ├── transforms.rst │ └── tutorialsAndExamples.rst ├── docs_require.txt ├── gpflowopt ├── __init__.py ├── _version.py ├── acquisition │ ├── __init__.py │ ├── acquisition.py │ ├── ei.py │ ├── hvpoi.py │ ├── lcb.py │ ├── mes.py │ ├── pof.py │ └── poi.py ├── bo.py ├── design.py ├── domain.py ├── models.py ├── objective.py ├── optim.py ├── pareto.py ├── scaling.py └── transforms.py ├── nox.py ├── setup.cfg ├── setup.py └── testing ├── __init__.py ├── data ├── lhd.npz └── vlmop.npz ├── system ├── __init__.py └── test_notebooks.py ├── unit ├── __init__.py ├── test_acquisition.py ├── test_datascaler.py ├── test_design.py ├── test_domain.py ├── test_implementations.py ├── test_modelwrapper.py ├── test_objective.py ├── test_optimizers.py ├── test_pareto.py ├── test_regression.py └── test_transforms.py └── utility.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | show_missing = True 3 | exclude_lines = 4 | pragma: no cover 5 | def __repr__ 6 | def __str__ 7 | def _repr_html_ 8 | def _html_table_rows 9 | if self.debug: 10 | if settings.DEBUG 11 | raise AssertionError 12 | raise NotImplementedError 13 | if __name__ == .__main__.: 14 | print 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # nox 82 | .nox 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Pycharm IDE directory 95 | .idea 96 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: python 4 | python: 5 | - 2.7 6 | - 3.5 7 | - 3.6 8 | cache: pip 9 | install: 10 | - pip install -U pip wheel codecov nox-automation 11 | script: 12 | - nox 13 | after_success: 14 | - codecov 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GPflowOpt 2 | This file contains notes for potential contributors to GPflowOpt, as well as some notes that may be helpful for maintenance. As part of the GPflow organisation, GPflowOpt follows the same philosophy and principles with regards to scope and code quality as GPflow. 3 | 4 | ## Project scope 5 | We do welcome contributions to GPflowOpt. However, the project is deliberately of limited scope, to try to ensure a high quality codebase: if you'd like to contribute a feature, please raise discussion via a GitHub issue. 6 | 7 | Due to limited scope we may not be able to review and merge every feature, however useful it may be. Particularly large contributions or changes to core code are harder to justify against the scope of the project or future development plans. For these contributions, we suggest you publish them as a separate package based on GPflowOpt's interfaces. We can link to your project from an issue discussing the topic or within the repository. Discussing a possible contribution in an issue should give an indication to how broadly it is supported to bring it into the codebase. 8 | 9 | ## Code Style 10 | - Python code should follow roughly the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style. We allow exceptions for, e.g., capital (single-letter) variable names to correspond to the notation of a paper (matrices, vectors, etc.). To help with this, we suggest using a plugin for your editor (we use pycharm). 11 | - Practise good code as far as is reasonable. Simpler is usually better. Avoid using complicated language features. Reading the existing GPflowOpt code should give a good idea of the expected style. 12 | 13 | ## Pull requests and the master branch 14 | All code that is destined for the master branch of GPflowOpt goes through a PR and will be reviewed. Only a small number of people can merge PRs onto the master branch (currently Joachim van der Herten and Ivo Couckuyt). 15 | 16 | ## Tests and continuous integration 17 | GPflowOpt is 99% covered by the testing suite. We expect changes to code to pass these tests, and for new code to be covered by new tests. Currently, tests are run by travis (python 2.7, 3.5 and 3.6), coverage is reported by codecov. 18 | 19 | By default, all tests are run on Travis except for the most expensive notebooks. 20 | 21 | ## Documentation 22 | GPflowOpt's documentation is not comprehensive, but covers enough to get users started. We expect that new features have documentation that can help other get up to speed. The docs are mostly Jupyter notebooks that compile into html via sphinx, using nbsphinx. 23 | 24 | ## Keeping up with GPflow and TensorFlow 25 | 26 | GPflowOpt currently tries to keep up with the GPflow master, though at some point we will start depending on the latest released version. Hence, GPflowOpt also adheres to the api of the TensorFlow version as required by GPflow. In practice this hopefully means we will support the latest (stable) TensorFlow, which is supported by GPflow. Any change in the supported version of GPflow or TensorFlow will bump the minor version number of GPflowOpt. 27 | 28 | Changing the minimum required version of TensorFlow that we're compatible with requires a few tasks: 29 | - update versions in `setup.py` 30 | - update versions used on travis via `.travis.yml` 31 | - update version used by readthedocs.org via `docsrequire.txt` 32 | - Increment the GPflowOpt version (see below). 33 | 34 | ## Version numbering 35 | The main purpose of versioning GPflowOpt is user convenience: to keep the number of releases down, we try to combine several PRs into one increment. 36 | When incrementing the version number, the following tasks are required: 37 | - Update the version in `GPflowOpt/_version.py` 38 | - Add a note to `RELEASE.md` 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note: This package is for use with GPFlow 1.** 2 | 3 | For Bayesian optimization using GPFlow 2 please see [Trieste](https://github.com/secondmind-labs/trieste), a joint effort with Secondmind. 4 | 5 | # GPflowOpt 6 | GPflowOpt is a python package for Bayesian Optimization using [GPflow](https://github.com/GPflow/GPflow), and uses [TensorFlow](http://www.tensorflow.org). It was [initiated](https://github.com/GPflow/GPflow/issues/397) and is currently maintained by [Joachim van der Herten](http://sumo.intec.ugent.be/members?q=jvanderherten) and [Ivo Couckuyt](http://sumo.intec.ugent.be/icouckuy). The full list of contributors (in alphabetical order) is Ivo Couckuyt, Tom Dhaene, James Hensman, Nicolas Knudde, Alexander G. de G. Matthews and Joachim van der Herten. Special thanks also to all [GPflow contributors](http://github.com/GPflow/GPflow/graphs/contributors) as this package would not be able to exist without their effort. 7 | 8 | [![Build Status](https://travis-ci.org/GPflow/GPflowOpt.svg?branch=master)](https://travis-ci.org/GPflow/GPflowOpt) 9 | [![Coverage Status](https://codecov.io/gh/GPflow/GPflowOpt/branch/master/graph/badge.svg)](https://codecov.io/gh/GPflow/GPflowOpt) 10 | [![Documentation Status](https://readthedocs.org/projects/gpflowopt/badge/?version=latest)](http://gpflowopt.readthedocs.io/en/latest/?badge=latest) 11 | 12 | # Install 13 | 14 | The easiest way to install GPflowOpt involves cloning this repository and running 15 | ``` 16 | pip install . --process-dependency-links 17 | ``` 18 | in the source directory. This also installs all required dependencies (including TensorFlow, if needed). For more detailed installation instructions, see the [documentation](https://gpflowopt.readthedocs.io/en/latest/intro.html#install). 19 | 20 | # Contributing 21 | If you are interested in contributing to this open source project, contact us through an issue on this repository. For more information, see the [notes for contributors](contributing.md). 22 | 23 | # Citing GPflowOpt 24 | 25 | To cite GPflowOpt, please reference the preliminary arXiv paper. Sample Bibtex is given below: 26 | 27 | ``` 28 | @ARTICLE{GPflowOpt2017, 29 | author = {Knudde, Nicolas and {van der Herten}, Joachim and Dhaene, Tom and Couckuyt, Ivo}, 30 | title = "{{GP}flow{O}pt: {A} {B}ayesian {O}ptimization {L}ibrary using Tensor{F}low}", 31 | journal = {arXiv preprint -- arXiv:1711.03845}, 32 | year = {2017}, 33 | url = {https://arxiv.org/abs/1711.03845} 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 0.1.0 2 | Initial release of GPflowOpt 3 | 4 | # Release 0.1.1 5 | Small bugfix release 6 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = GPflowOpt 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /doc/source/acquisition.rst: -------------------------------------------------------------------------------- 1 | Acquisition functions 2 | ======================== 3 | 4 | The gpflowopt package currently supports a limited number of popular acquisition functions. These are 5 | summarized in the table below. Detailed description for each can be found below. 6 | 7 | .. automodule:: gpflowopt.acquisition 8 | 9 | +----------------------------------------------------------+-----------+-------------+-----------+ 10 | | Method | Objective | Constraint | # Outputs | 11 | +==========================================================+===========+=============+===========+ 12 | | :class:`gpflowopt.acquisition.ExpectedImprovement` | ✔ | | 1 | 13 | +----------------------------------------------------------+-----------+-------------+-----------+ 14 | | :class:`gpflowopt.acquisition.ProbabilityOfFeasibility` | | ✔ | 1 | 15 | +----------------------------------------------------------+-----------+-------------+-----------+ 16 | | :class:`gpflowopt.acquisition.ProbabilityOfImprovement` | ✔ | | 1 | 17 | +----------------------------------------------------------+-----------+-------------+-----------+ 18 | | :class:`gpflowopt.acquisition.LowerConfidenceBound` | ✔ | | 1 | 19 | +----------------------------------------------------------+-----------+-------------+-----------+ 20 | | :class:`gpflowopt.acquisition.MinValueEntropySearch` | ✔ | | 1 | 21 | +----------------------------------------------------------+-----------+-------------+-----------+ 22 | | :class:`gpflowopt.acquisition.HVProbabilityOfImprovement`| ✔ | | > 1 | 23 | +----------------------------------------------------------+-----------+-------------+-----------+ 24 | 25 | Single-objective 26 | ---------------- 27 | 28 | Expected Improvement 29 | ^^^^^^^^^^^^^^^^^^^^ 30 | 31 | .. autoclass:: gpflowopt.acquisition.ExpectedImprovement 32 | :members: 33 | :special-members: 34 | 35 | Probability of Feasibility 36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 37 | 38 | .. autoclass:: gpflowopt.acquisition.ProbabilityOfFeasibility 39 | :members: 40 | :special-members: 41 | 42 | Probability of Improvement 43 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 44 | 45 | .. autoclass:: gpflowopt.acquisition.ProbabilityOfImprovement 46 | :members: 47 | :special-members: 48 | 49 | Lower Confidence Bound 50 | ^^^^^^^^^^^^^^^^^^^^^^ 51 | 52 | .. autoclass:: gpflowopt.acquisition.LowerConfidenceBound 53 | :members: 54 | :special-members: 55 | 56 | Min-Value Entropy Search 57 | ^^^^^^^^^^^^^^^^^^^^^^^^ 58 | 59 | .. autoclass:: gpflowopt.acquisition.MinValueEntropySearch 60 | :members: 61 | :special-members: 62 | 63 | 64 | Multi-objective 65 | ---------------- 66 | 67 | Hypervolume-based Probability of Improvement 68 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 69 | 70 | .. autoclass:: gpflowopt.acquisition.HVProbabilityOfImprovement 71 | :members: 72 | :special-members: 73 | 74 | Pareto module 75 | ^^^^^^^^^^^^^ 76 | 77 | .. automodule:: gpflowopt.pareto 78 | :members: 79 | .. automethod:: gpflowopt.pareto.Pareto.hypervolume 80 | -------------------------------------------------------------------------------- /doc/source/apiAndArchitecture.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API and architecture 4 | ================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | notebooks/structure 10 | bayesianoptimizer 11 | acquisition 12 | designs 13 | transforms 14 | interfaces 15 | -------------------------------------------------------------------------------- /doc/source/bayesianoptimizer.rst: -------------------------------------------------------------------------------- 1 | Bayesian Optimizer 2 | ================== 3 | 4 | .. automodule:: gpflowopt.bo 5 | .. autoclass:: gpflowopt.BayesianOptimizer 6 | :members: 7 | :special-members: 8 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # GPflowOpt documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Apr 30 20:34:41 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | from gpflowopt import __version__ 22 | 23 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 24 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 25 | 26 | if not on_rtd: # only import and set the theme if we're building docs locally 27 | import sphinx_rtd_theme 28 | html_theme = 'sphinx_rtd_theme' 29 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 30 | 31 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 32 | 33 | 34 | # -- General configuration ------------------------------------------------ 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.autosummary', 46 | 'sphinx.ext.todo', 47 | 'sphinx.ext.mathjax', 48 | 'sphinx.ext.viewcode', 49 | 'numpydoc', 50 | 'nbsphinx', 51 | 'IPython.sphinxext.ipython_console_highlighting' 52 | ] 53 | 54 | numpydoc_show_class_members = True 55 | numpydoc_show_inherited_class_members = True 56 | numpydoc_class_members_toctree = False 57 | 58 | #autoclass_content = 'both' 59 | 60 | # Add any paths that contain templates here, relative to this directory. 61 | templates_path = ['_templates'] 62 | 63 | # The suffix(es) of source filenames. 64 | # You can specify multiple suffix as a list of string: 65 | # 66 | # source_suffix = ['.rst', '.md'] 67 | source_suffix = '.rst' 68 | 69 | # The master toctree document. 70 | master_doc = 'index' 71 | 72 | # General information about the project. 73 | project = 'GPflowOpt' 74 | copyright = '2017, Joachim van der Herten' 75 | author = 'Joachim van der Herten, Ivo Couckuyt' 76 | 77 | # The version info for the project you're documenting, acts as replacement for 78 | # |version| and |release|, also used in various other places throughout the 79 | # built documents. 80 | # 81 | # The short X.Y version. 82 | version = __version__ 83 | # The full version, including alpha/beta/rc tags. 84 | release = version 85 | 86 | # The language for content autogenerated by Sphinx. Refer to documentation 87 | # for a list of supported languages. 88 | # 89 | # This is also used if you do content translation via gettext catalogs. 90 | # Usually you set "language" from the command line for these cases. 91 | language = None 92 | 93 | # List of patterns, relative to source directory, that match files and 94 | # directories to ignore when looking for source files. 95 | # This patterns also effect to html_static_path and html_extra_path 96 | exclude_patterns = [] 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = 'sphinx' 100 | 101 | # If true, `todo` and `todoList` produce output, else they produce nothing. 102 | todo_include_todos = True 103 | 104 | 105 | # -- Options for HTML output ---------------------------------------------- 106 | 107 | # The theme to use for HTML and HTML Help pages. See the documentation for 108 | # a list of builtin themes. 109 | # 110 | #html_theme = 'alabaster' 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | # 116 | # html_theme_options = {} 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = [] 122 | 123 | 124 | # -- Options for HTMLHelp output ------------------------------------------ 125 | 126 | # Output file base name for HTML help builder. 127 | htmlhelp_basename = 'GPflowOptdoc' 128 | 129 | 130 | # -- Options for LaTeX output --------------------------------------------- 131 | 132 | latex_elements = { 133 | # The paper size ('letterpaper' or 'a4paper'). 134 | # 135 | # 'papersize': 'letterpaper', 136 | 137 | # The font size ('10pt', '11pt' or '12pt'). 138 | # 139 | # 'pointsize': '10pt', 140 | 141 | # Additional stuff for the LaTeX preamble. 142 | # 143 | # 'preamble': '', 144 | 145 | # Latex figure (float) alignment 146 | # 147 | # 'figure_align': 'htbp', 148 | } 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, 152 | # author, documentclass [howto, manual, or own class]). 153 | latex_documents = [ 154 | (master_doc, 'gpflowopt.tex', 'GPflowOpt Documentation', 155 | 'Joachim van der Herten', 'manual'), 156 | ] 157 | 158 | 159 | # -- Options for manual page output --------------------------------------- 160 | 161 | # One entry per manual page. List of tuples 162 | # (source start file, name, description, authors, manual section). 163 | man_pages = [ 164 | (master_doc, 'GPflowOpt', 'GPflowOpt Documentation', 165 | [author], 1) 166 | ] 167 | 168 | 169 | # -- Options for Texinfo output ------------------------------------------- 170 | 171 | # Grouping the document tree into Texinfo files. List of tuples 172 | # (source start file, target name, title, author, 173 | # dir menu entry, description, category) 174 | texinfo_documents = [ 175 | (master_doc, 'GPflowOpt', 'GPflowOpt Documentation', 176 | author, 'GPflowOpt', 'One line description of project.', 177 | 'Miscellaneous'), 178 | ] 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /doc/source/designs.rst: -------------------------------------------------------------------------------- 1 | Initial Designs 2 | =============== 3 | 4 | .. automodule:: gpflowopt.design 5 | 6 | Latin Hypercube design 7 | ---------------------- 8 | .. autoclass:: gpflowopt.design.LatinHyperCube 9 | :members: 10 | :special-members: 11 | 12 | Factorial design 13 | ---------------- 14 | .. autoclass:: gpflowopt.design.FactorialDesign 15 | :members: 16 | :special-members: 17 | 18 | Random design 19 | ------------- 20 | .. autoclass:: gpflowopt.design.RandomDesign 21 | :members: 22 | :special-members: 23 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. GPflowOpt documentation master file, created by 2 | sphinx-quickstart on Sun Apr 30 20:34:41 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | GPflowOpt Documentation 7 | ===================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | :caption: Contents: 12 | 13 | intro 14 | tutorialsAndExamples 15 | apiAndArchitecture 16 | -------------------------------------------------------------------------------- /doc/source/interfaces.rst: -------------------------------------------------------------------------------- 1 | Interfaces 2 | ========== 3 | 4 | Domain 5 | ------- 6 | .. automodule:: gpflowopt.domain 7 | :special-members: 8 | .. autoclass:: gpflowopt.domain.Domain 9 | :special-members: 10 | .. autoclass:: gpflowopt.domain.Parameter 11 | :special-members: 12 | 13 | Optimizer 14 | ---------- 15 | .. automodule:: gpflowopt.optim 16 | .. autoclass:: gpflowopt.optim.Optimizer 17 | :special-members: 18 | 19 | Acquisition 20 | ------------ 21 | .. automodule:: gpflowopt.acquisition 22 | :special-members: 23 | .. autoclass:: gpflowopt.acquisition.Acquisition 24 | :special-members: 25 | 26 | Design 27 | ------- 28 | .. automodule:: gpflowopt.design 29 | :special-members: 30 | .. autoclass:: gpflowopt.design.Design 31 | :special-members: 32 | 33 | Transform 34 | --------- 35 | .. automodule:: gpflowopt.transforms 36 | :special-members: 37 | .. autoclass:: gpflowopt.transforms.DataTransform 38 | :special-members: 39 | 40 | ModelWrapper 41 | ------------ 42 | .. automodule:: gpflowopt.models 43 | :special-members: 44 | .. autoclass:: gpflowopt.models.ModelWrapper 45 | :members: 46 | :special-members: 47 | -------------------------------------------------------------------------------- /doc/source/intro.rst: -------------------------------------------------------------------------------- 1 | ------------ 2 | Introduction 3 | ------------ 4 | 5 | `GPflowOpt `_ is a library for Bayesian Optimization with `GPflow `_. 6 | It makes use of TensorFlow for computation of acquisition functions, to offer scalability, and avoid implementation of gradients. 7 | The package was created, and is currently maintained by `Joachim van der Herten `_ and `Ivo Couckuyt `_ 8 | 9 | The project is open source: if you feel you have some relevant skills and are interested in 10 | contributing then please contact us on `GitHub `_ by opening an issue or pull request. 11 | 12 | Install 13 | ------- 14 | 1. Install package 15 | 16 | A straightforward way to install GPflowOpt is to clone its repository and run 17 | 18 | ``pip install . --process-dependency-links`` 19 | 20 | in the root folder. This also installs required dependencies including TensorFlow. 21 | For alternative TensorFlow installations (e.g., gpu), please see the instructions on the main `TensorFlow webpage `_. 22 | 23 | 2. Development 24 | 25 | GPflowOpt is a pure python library so you could just add it to your python path. We use 26 | 27 | ``pip install -e . --process-dependency-links`` 28 | 29 | 3. Testing 30 | 31 | For testing, GPflowOpt uses `nox `_ to automatically create a virtualenv and 32 | install the additional test dependencies. To install nox: 33 | 34 | ``pip install nox-automation`` 35 | 36 | followed by 37 | 38 | ``nox`` 39 | 40 | to run all test sessions. 41 | 42 | 4. Documentation 43 | 44 | To build the documentation, first install the extra dependencies with 45 | ``pip install -e .[docs]``. Then proceed with ``python setup.py build_sphinx``. 46 | 47 | Getting started 48 | --------------- 49 | 50 | A simple example of Bayesian optimization to get up and running is provided by the 51 | :ref:`first steps into Bayesian optimization ` notebook 52 | 53 | For more advanced use cases have a look at the other :ref:`tutorial ` notebooks and the :ref:`api`. 54 | 55 | Citing GPflowOpt 56 | ----------------- 57 | 58 | To cite GPflowOpt, please reference the preliminary arXiv paper. Sample Bibtex is given below: 59 | 60 | | @ARTICLE{GPflowOpt2017, 61 | | author = {Knudde, Nicolas and {van der Herten}, Joachim and Dhaene, Tom and Couckuyt, Ivo}, 62 | | title = "{{GP}flow: A {G}aussian process library using {T}ensor{F}low}", 63 | | journal = {arXiv preprint -- arXiv:1711.03845}, 64 | | year = {2017}, 65 | | url = {https://arxiv.org/abs/1711.03845} 66 | | } 67 | 68 | Acknowledgements 69 | ----------------- 70 | Joachim van der Herten and Ivo Couckuyt are Ghent University - imec postdoctoral fellows. Ivo Couckuyt is supported 71 | by FWO Vlaanderen. 72 | -------------------------------------------------------------------------------- /doc/source/notebooks/airline.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPflow/GPflowOpt/3d86bcc000b0367f19e9f03f4458f5641e5dde60/doc/source/notebooks/airline.npz -------------------------------------------------------------------------------- /doc/source/notebooks/firststeps.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true 7 | }, 8 | "source": [ 9 | "# First steps into Bayesian optimization\n", 10 | "*Ivo Couckuyt*, *Joachim van der Herten*" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "## Introduction\n", 18 | "\n", 19 | "Bayesian optimization is particularly useful for expensive optimization problems. This includes optimization problems where the objective (and constraints) are time-consuming to evaluate: measurements, engineering simulations, hyperparameter optimization of deep learning models, etc. Another area where Bayesian optimization may provide a benefit is in the presence of (a lot of) noise. If your problem does not satisfy these requirements other optimization algorithms might be better suited.\n", 20 | "\n", 21 | "To setup a Bayesian optimization scheme with GPflowOpt you have to:\n", 22 | "\n", 23 | "- define your objective and specify the optimization domain\n", 24 | "- setup a GPflow model and choose an acquisition function\n", 25 | "- create a BayesianOptimizer" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## Objective function" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 1, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "data": { 42 | "text/html": [ 43 | "
NameTypeValues
x1Continuous[-5. 10.]
x2Continuous[ 0. 15.]
" 44 | ], 45 | "text/plain": [ 46 | "" 47 | ] 48 | }, 49 | "execution_count": 1, 50 | "metadata": {}, 51 | "output_type": "execute_result" 52 | } 53 | ], 54 | "source": [ 55 | "import numpy as np\n", 56 | "from gpflowopt.domain import ContinuousParameter\n", 57 | "\n", 58 | "def branin(x):\n", 59 | " x = np.atleast_2d(x)\n", 60 | " x1 = x[:, 0]\n", 61 | " x2 = x[:, 1]\n", 62 | " a = 1.\n", 63 | " b = 5.1 / (4. * np.pi ** 2)\n", 64 | " c = 5. / np.pi\n", 65 | " r = 6.\n", 66 | " s = 10.\n", 67 | " t = 1. / (8. * np.pi)\n", 68 | " ret = a * (x2 - b * x1 ** 2 + c * x1 - r) ** 2 + s * (1 - t) * np.cos(x1) + s\n", 69 | " return ret[:, None]\n", 70 | "\n", 71 | "domain = ContinuousParameter('x1', -5, 10) + \\\n", 72 | " ContinuousParameter('x2', 0, 15)\n", 73 | "domain" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## Bayesian optimizer" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 8, 86 | "metadata": { 87 | "scrolled": true 88 | }, 89 | "outputs": [ 90 | { 91 | "name": "stdout", 92 | "output_type": "stream", 93 | "text": [ 94 | "iter # 0 - MLL [-13.1] - fmin [4.42]\n", 95 | "iter # 1 - MLL [-13.4] - fmin [4.42]\n", 96 | "iter # 2 - MLL [-10.6] - fmin [0.723]\n", 97 | "iter # 3 - MLL [-9.09] - fmin [0.486]\n", 98 | "iter # 4 - MLL [-7.01] - fmin [0.486]\n", 99 | "iter # 5 - MLL [-2.69] - fmin [0.446]\n", 100 | "iter # 6 - MLL [1.96] - fmin [0.446]\n", 101 | "iter # 7 - MLL [4.6] - fmin [0.446]\n", 102 | "iter # 8 - MLL [7.37] - fmin [0.4]\n", 103 | "iter # 9 - MLL [12.6] - fmin [0.4]\n", 104 | " constraints: array([], dtype=float64)\n", 105 | " fun: array([0.39970302])\n", 106 | " message: 'OK'\n", 107 | " nfev: 10\n", 108 | " success: True\n", 109 | " x: array([[9.40798299, 2.43938799]])\n" 110 | ] 111 | } 112 | ], 113 | "source": [ 114 | "import gpflow\n", 115 | "from gpflowopt.bo import BayesianOptimizer\n", 116 | "from gpflowopt.design import LatinHyperCube\n", 117 | "from gpflowopt.acquisition import ExpectedImprovement\n", 118 | "from gpflowopt.optim import SciPyOptimizer, StagedOptimizer, MCOptimizer\n", 119 | "\n", 120 | "# Use standard Gaussian process Regression\n", 121 | "lhd = LatinHyperCube(21, domain)\n", 122 | "X = lhd.generate()\n", 123 | "Y = branin(X)\n", 124 | "model = gpflow.gpr.GPR(X, Y, gpflow.kernels.Matern52(2, ARD=True))\n", 125 | "model.kern.lengthscales.transform = gpflow.transforms.Log1pe(1e-3)\n", 126 | "\n", 127 | "# Now create the Bayesian Optimizer\n", 128 | "alpha = ExpectedImprovement(model)\n", 129 | "\n", 130 | "acquisition_opt = StagedOptimizer([MCOptimizer(domain, 200),\n", 131 | " SciPyOptimizer(domain)])\n", 132 | "\n", 133 | "optimizer = BayesianOptimizer(domain, alpha, optimizer=acquisition_opt, verbose=True)\n", 134 | "\n", 135 | "# Run the Bayesian optimization\n", 136 | "r = optimizer.optimize(branin, n_iter=10)\n", 137 | "print(r)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "That's all! Your objective function has now been optimized for 10 iterations." 145 | ] 146 | } 147 | ], 148 | "metadata": { 149 | "kernelspec": { 150 | "display_name": "Python 3", 151 | "language": "python", 152 | "name": "python3" 153 | }, 154 | "language_info": { 155 | "codemirror_mode": { 156 | "name": "ipython", 157 | "version": 3 158 | }, 159 | "file_extension": ".py", 160 | "mimetype": "text/x-python", 161 | "name": "python", 162 | "nbconvert_exporter": "python", 163 | "pygments_lexer": "ipython3", 164 | "version": "3.6.6" 165 | } 166 | }, 167 | "nbformat": 4, 168 | "nbformat_minor": 1 169 | } 170 | -------------------------------------------------------------------------------- /doc/source/notebooks/structure.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true 7 | }, 8 | "source": [ 9 | "# The structure of GPflowOpt\n", 10 | "*Joachim van der Herten*" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "In this document, the structure of the GPflowOpt library is explained, including some small examples. First the `Domain` and `Optimizer` interfaces are shortly illustrated, followed by a description of the `BayesianOptimizer`. At the end, a step-by-step walkthrough of the `BayesianOptimizer` is given." 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "## Optimization\n", 25 | "The underlying design principles of GPflowOpt were chosen to address the following task: optimizing problems of the form\n", 26 | "$$\\underset{\\boldsymbol{x} \\in \\mathcal{X}}{\\operatorname{argmin}} f(\\boldsymbol{x}).$$ The *objective function* $f: \\mathcal{X} \\rightarrow \\mathbb{R}^p$ maps a candidate optimum to a score (or multiple). Here $\\mathcal{X}$ represents the input domain. This domain encloses all candidate solutions to the optimization problem and can be entirely continuous (i.e., a $d$-dimensional hypercube) but may also consist of discrete and categorical parameters. \n", 27 | "\n", 28 | "In GPflowOpt, the `Domain` and `Optimizer` interfaces and corresponding subclasses are used to explicitly represent the optimization problem.\n", 29 | "\n", 30 | "### Objective function\n", 31 | "The objective function itself is provided and must be implemented as any python callable (function or object with implemented call operator), accepting a two dimensional numpy array with shape $(n, d)$ as an input, with $n$ the batch size. It returns a tuple of numpy arrays: the first element of the tuple has shape $(n, p)$ and returns the objective scores for each point to evaluate. The second element is the gradient in every point, shaped either $(n, d)$ if we have a single-objective optimization problem, or $(n, d, p)$ in case of a multi-objective function. If the objective function is passed on to a gradient-free optimization method, the gradient is automatically discarded. GPflowOpt provides decorators which handle batch application of a function along the n points of the input matrix, or dealing with functions which accept each feature as function argument.\n", 32 | "\n", 33 | "Here, we define a simple quadratic objective function:" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 5, 39 | "metadata": { 40 | "collapsed": true 41 | }, 42 | "outputs": [], 43 | "source": [ 44 | "import numpy as np\n", 45 | "\n", 46 | "def fx(X):\n", 47 | " X = np.atleast_2d(X)\n", 48 | " # Return objective & gradient\n", 49 | " return np.sum(np.square(X), axis=1)[:,None], 2*X" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "### Domain\n", 57 | "Then, we represent $\\mathcal{X}$ by composing parameters. This is how a simple continuous square domain is defined:" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 6, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "data": { 67 | "text/html": [ 68 | "
NameTypeValues
x1Continuous[-2. 2.]
x2Continuous[-1. 2.]
" 69 | ], 70 | "text/plain": [ 71 | "" 72 | ] 73 | }, 74 | "execution_count": 6, 75 | "metadata": {}, 76 | "output_type": "execute_result" 77 | } 78 | ], 79 | "source": [ 80 | "from gpflowopt.domain import ContinuousParameter\n", 81 | "domain = ContinuousParameter('x1', -2, 2) + ContinuousParameter('x2', -1, 2)\n", 82 | "domain" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "### Optimize\n", 90 | "Based on the domain and a valid objective function, we can now easily apply one of the included optimizers to optimize objective functions. GPflowOpt defines an intuitive `Optimizer` interface which can be used to specify the domain, the initial point(s), constraints (to be implemented) etc. Some popular optimization approaches are provided.\n", 91 | "Here is how our function is optimized using one of the available methods of SciPy's minimize:" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 7, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "data": { 101 | "text/plain": [ 102 | " fun: 0.0\n", 103 | " jac: array([ 0., 0.])\n", 104 | " message: 'Optimization terminated successfully.'\n", 105 | " nfev: 3\n", 106 | " nit: 2\n", 107 | " njev: 2\n", 108 | " status: 0\n", 109 | " success: True\n", 110 | " x: array([[ 0., 0.]])" 111 | ] 112 | }, 113 | "execution_count": 7, 114 | "metadata": {}, 115 | "output_type": "execute_result" 116 | } 117 | ], 118 | "source": [ 119 | "from gpflowopt.optim import SciPyOptimizer\n", 120 | "\n", 121 | "optimizer = SciPyOptimizer(domain, method='SLSQP')\n", 122 | "optimizer.set_initial([-1,-1])\n", 123 | "optimizer.optimize(fx)" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "And here is how we optimize it Monte-Carlo. We can pass the same function as the gradients are automatically discarded." 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 8, 136 | "metadata": {}, 137 | "outputs": [ 138 | { 139 | "data": { 140 | "text/plain": [ 141 | " fun: array([[ 0.02115951]])\n", 142 | " message: 'OK'\n", 143 | " nfev: 200\n", 144 | " success: True\n", 145 | " x: array([[ 0.05731395, -0.13369599]])" 146 | ] 147 | }, 148 | "execution_count": 8, 149 | "metadata": {}, 150 | "output_type": "execute_result" 151 | } 152 | ], 153 | "source": [ 154 | "from gpflowopt.optim import MCOptimizer\n", 155 | "optimizer = MCOptimizer(domain, 200)\n", 156 | "optimizer.optimize(fx)" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "## Bayesian Optimization" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "metadata": { 169 | "collapsed": true 170 | }, 171 | "source": [ 172 | "In Bayesian Optimization (BO), the typical assumption is that $f$ is expensive to evaluate and no gradients are available. The typical approach is to sequentially select a limited set of decisions $\\boldsymbol{x}_0, \\boldsymbol{x}_1, ... \\boldsymbol{x}_{n-1}$ using a sampling policy. Hence each decision $\\boldsymbol{x}_i \\in \\mathcal{X}$ itself is the result of an optimization problem\n", 173 | "$$\\boldsymbol{x}_i = \\underset{\\boldsymbol{x}}{\\operatorname{argmax}} \\alpha_i(\\boldsymbol{x})$$\n", 174 | "\n", 175 | "Each iteration, a function $\\alpha_i$ which is cheap-to-evaluate acts as a surrogate for the expensive function. It is typically a mapping of the predictive distribution of a (Bayesian) model built on all decisions and their corresponding (noisy) evaluations. The mapping introduces an order in $\\mathcal{X}$ to obtain a certain goal. The typical goal within the context of BO is the search for *optimality* or *feasibility* while keeping the amount of required evaluations ($n$) a small number. As we can have several functions $f$ representing objectives and constraints, BO may invoke several models and mappings $\\alpha$. These mappings are typically referred to as *acquisition functions* (or *infill criteria*). GPflowOpt defines an `Acquisition` interface to implement these mappings and provides implementations of some default choices. In combination with a special `Optimizer` implementation for BO, following steps are required for a typical workflow: \n", 176 | "\n", 177 | "1) Define the **problem domain**. Its dimensionality matches the input to the objective and constraint functions. (like normal optimization)\n", 178 | "\n", 179 | "2) Specify the **(GP) models** for the constraints and objectives. This involves choice of kernels, priors, fixes, transforms... this step follows the standard way of setting up GPflow models. GPflowOpt does not further wrap models hence it is possible to implement custom models in GPflow and use them directly in GPflowOpt\n", 180 | "\n", 181 | "3) Set up the **acquisition function(s)** using the available built-in implementations in GPflowOpt, or design your own by implementing the `Acquisition` interface.\n", 182 | "\n", 183 | "4) Set up an **optimizer** for the acquisition function.\n", 184 | "\n", 185 | "5) Run the **high-level** `BayesianOptimizer` which implements a typical BO flow. `BayesianOptimizer` in itself is compliant with the `Optimizer` interface. Exceptionally, the `BayesianOptimizer` requires that the objective function returns **no gradients**.\n", 186 | "\n", 187 | "Alternatively, advanced users requiring finer control can easily implement their own flow based on the low-level interfaces of GPflowOpt, as the coupling between these objects was intentionally kept loose.\n", 188 | "\n", 189 | "As illustration of the described flow, the previous example is optimized using Bayesian optimization instead, with the well-known Expected Improvement acquisition function:" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 9, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "name": "stdout", 199 | "output_type": "stream", 200 | "text": [ 201 | " fun: array([ 0.17684308])\n", 202 | " message: 'OK'\n", 203 | " nfev: 15\n", 204 | " success: True\n", 205 | " x: array([[ 0. , 0.42052714]])\n" 206 | ] 207 | } 208 | ], 209 | "source": [ 210 | "from gpflowopt.bo import BayesianOptimizer\n", 211 | "from gpflowopt.design import FactorialDesign\n", 212 | "from gpflowopt.acquisition import ExpectedImprovement\n", 213 | "import gpflow\n", 214 | "\n", 215 | "# The Bayesian Optimizer does not expect gradients to be returned\n", 216 | "def fx(X):\n", 217 | " X = np.atleast_2d(X)\n", 218 | " # Return objective & gradient\n", 219 | " return np.sum(np.square(X), axis=1)[:,None]\n", 220 | "\n", 221 | " \n", 222 | "X = FactorialDesign(2, domain).generate()\n", 223 | "Y = fx(X)\n", 224 | "\n", 225 | "# initializing a standard BO model, Gaussian Process Regression with\n", 226 | "# Matern52 ARD Kernel\n", 227 | "model = gpflow.gpr.GPR(X, Y, gpflow.kernels.Matern52(2, ARD=True))\n", 228 | "alpha = ExpectedImprovement(model)\n", 229 | "\n", 230 | "# Now we must specify an optimization algorithm to optimize the acquisition \n", 231 | "# function, each iteration. \n", 232 | "acqopt = SciPyOptimizer(domain)\n", 233 | "\n", 234 | "# Now create the Bayesian Optimizer\n", 235 | "optimizer = BayesianOptimizer(domain, alpha, optimizer=acqopt)\n", 236 | "with optimizer.silent():\n", 237 | " r = optimizer.optimize(fx, n_iter=15)\n", 238 | "print(r)" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "This brief snippet code starts from a 2-level grid (corner points of the domain) and uses a GP model to model the response surface of the objective function. The `BayesianOptimizer` follows the same interface as other optimizers and is initialized with a domain, the acquisition function and an additional optimization method to optimize the acquisition function each iteration. Finally, the optimizer performs 10 iterations to optimize fx.\n", 246 | "\n", 247 | "The code to evaluate the acquisition function on the model is written in TensorFlow, allowing gradient-based optimization without additional effort due to the automated differentation." 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "### Step-by-step description of the BayesianOptimizer" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": { 260 | "collapsed": true 261 | }, 262 | "source": [ 263 | "Prior to running `BayesianOptimizer.optimize()`, the acquisition function is initialized with an underlying model. Any data previously included in the model (through the `GPModel.__init__` constructor in GPflow) is used as initial data. When `optimize(function, n_iter)` is called:\n", 264 | "\n", 265 | "1) Any data points returned by `get_initial()` are evaluated. Afterwards the evaluated points are added to the models by calling `_update_model_data()`. \n", 266 | "\n", 267 | "2) `n_iter` iterations are performed. Each iteration the acquisition function is optimized, and the models are updated by calling `_update_model_data()`\n", 268 | "\n", 269 | "The updating of a model through `_update_model_data()` calls `set_data(X, Y)` on the acquisition function. This covers following aspects:\n", 270 | "\n", 271 | "* `GPModel.X` and `GPModel.Y` are updated\n", 272 | "\n", 273 | "* Each of the contained models are returned to the state when the acquisition function was initialized and optimized. If the `optimize_restarts` parameter of the `Acquisition.__init__()` was set to $n>1$, the state of the model is randomized and optimized $n-1$ times. Finally, the state resulting in the best `log_likelihood()` is the new model state \n", 274 | "\n", 275 | "* Call `Acquisition.setup()` to perform any pre-calculation of quantities independent of candidate points, which can be used in `build_acquisition()`." 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": {}, 281 | "source": [ 282 | "## The GPflow tree\n", 283 | "The `Acquisition` interface, mapping the belief of the model(s) to a score indicating areas of optimality/feasibility, is implemented as part of the [GPflow tree structure](https://gpflow.readthedocs.io/en/latest/notebooks/structure.html). More specifically it implements the `Parameterized` interface permitting the use of the useful `AutoFlow` decorator. The `build_acquisition()` method to be implemented by subclasses is a TensorFlow method, allowing automated differentiation of the acquisition function which enables gradient-based optimization thereof (not of the objective!). It may directly access the graph for computing the predictive distribution of a model by calling `build_predict()`." 284 | ] 285 | } 286 | ], 287 | "metadata": { 288 | "kernelspec": { 289 | "display_name": "Python 3", 290 | "language": "python", 291 | "name": "python3" 292 | }, 293 | "language_info": { 294 | "codemirror_mode": { 295 | "name": "ipython", 296 | "version": 3 297 | }, 298 | "file_extension": ".py", 299 | "mimetype": "text/x-python", 300 | "name": "python", 301 | "nbconvert_exporter": "python", 302 | "pygments_lexer": "ipython3", 303 | "version": "3.5.2" 304 | } 305 | }, 306 | "nbformat": 4, 307 | "nbformat_minor": 1 308 | } 309 | -------------------------------------------------------------------------------- /doc/source/transforms.rst: -------------------------------------------------------------------------------- 1 | Data Transformations 2 | ==================== 3 | 4 | Transforms 5 | ---------- 6 | .. automodule:: gpflowopt.transforms 7 | .. autoclass:: gpflowopt.transforms.LinearTransform 8 | :special-members: 9 | 10 | Normalizer 11 | ---------- 12 | .. automodule:: gpflowopt.scaling 13 | .. autoclass:: gpflowopt.scaling.DataScaler 14 | -------------------------------------------------------------------------------- /doc/source/tutorialsAndExamples.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials: 2 | 3 | Tutorials and examples 4 | ================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | notebooks/firststeps 10 | notebooks/constrained_bo 11 | notebooks/new_acquisition 12 | notebooks/hyperopt 13 | notebooks/multiobjective 14 | -------------------------------------------------------------------------------- /docs_require.txt: -------------------------------------------------------------------------------- 1 | ipykernel==4.3.1 2 | ipython==4.2.0 3 | ipython-genutils==0.2.0 4 | jupyter==1.0.0 5 | jupyter-client==4.3.0 6 | jupyter-console==4.1.1 7 | jupyter-contrib-core==0.3.0 8 | jupyter-core==4.4.0 9 | jupyter-nbextensions-configurator==0.2.1 10 | nbsphinx==0.3.4 11 | numpydoc==0.8.0 12 | Pygments==2.2.0 13 | scipy==0.18.0 14 | sphinx_rtd_theme==0.1.9 15 | https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-1.0.0-cp35-cp35m-linux_x86_64.whl -------------------------------------------------------------------------------- /gpflowopt/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import acquisition 16 | from . import domain 17 | from .bo import BayesianOptimizer 18 | from . import optim 19 | from . import design 20 | from . import transforms 21 | from . import scaling 22 | from . import objective 23 | from . import pareto 24 | from . import models 25 | 26 | from ._version import __version__ 27 | -------------------------------------------------------------------------------- /gpflowopt/_version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "0.1.1" # pragma: no cover 16 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Framework components and interfaces 16 | from .acquisition import Acquisition, AcquisitionAggregation, AcquisitionProduct, AcquisitionSum, MCMCAcquistion 17 | 18 | # Single objective 19 | from .ei import ExpectedImprovement 20 | from .poi import ProbabilityOfImprovement 21 | from .lcb import LowerConfidenceBound 22 | from .mes import MinValueEntropySearch 23 | 24 | # Multiobjective 25 | from .hvpoi import HVProbabilityOfImprovement 26 | 27 | # Black-box constraint 28 | from .pof import ProbabilityOfFeasibility 29 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/ei.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .acquisition import Acquisition 16 | 17 | from gpflow.model import Model 18 | from gpflow.param import DataHolder 19 | from gpflow import settings 20 | 21 | import numpy as np 22 | import tensorflow as tf 23 | 24 | stability = settings.numerics.jitter_level 25 | 26 | 27 | class ExpectedImprovement(Acquisition): 28 | """ 29 | Expected Improvement acquisition function for single-objective global optimization. 30 | Introduced by (Mockus et al, 1975). 31 | 32 | Key reference: 33 | 34 | :: 35 | 36 | @article{Jones:1998, 37 | title={Efficient global optimization of expensive black-box functions}, 38 | author={Jones, Donald R and Schonlau, Matthias and Welch, William J}, 39 | journal={Journal of Global optimization}, 40 | volume={13}, 41 | number={4}, 42 | pages={455--492}, 43 | year={1998}, 44 | publisher={Springer} 45 | } 46 | 47 | This acquisition function is the expectation of the improvement over the current best observation 48 | w.r.t. the predictive distribution. The definition is closely related to the :class:`.ProbabilityOfImprovement`, 49 | but adds a multiplication with the improvement w.r.t the current best observation to the integral. 50 | 51 | .. math:: 52 | \\alpha(\\mathbf x_{\\star}) = \\int \\max(f_{\\min} - f_{\\star}, 0) \\, p( f_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} ) \\, d f_{\\star} 53 | """ 54 | 55 | def __init__(self, model): 56 | """ 57 | :param model: GPflow model (single output) representing our belief of the objective 58 | """ 59 | super(ExpectedImprovement, self).__init__(model) 60 | self.fmin = DataHolder(np.zeros(1)) 61 | self._setup() 62 | 63 | def _setup(self): 64 | super(ExpectedImprovement, self)._setup() 65 | # Obtain the lowest posterior mean for the previous - feasible - evaluations 66 | feasible_samples = self.data[0][self.highest_parent.feasible_data_index(), :] 67 | samples_mean, _ = self.models[0].predict_f(feasible_samples) 68 | self.fmin.set_data(np.min(samples_mean, axis=0)) 69 | 70 | def build_acquisition(self, Xcand): 71 | # Obtain predictive distributions for candidates 72 | candidate_mean, candidate_var = self.models[0].build_predict(Xcand) 73 | candidate_var = tf.maximum(candidate_var, stability) 74 | 75 | # Compute EI 76 | normal = tf.contrib.distributions.Normal(candidate_mean, tf.sqrt(candidate_var)) 77 | t1 = (self.fmin - candidate_mean) * normal.cdf(self.fmin) 78 | t2 = candidate_var * normal.prob(self.fmin) 79 | return tf.add(t1, t2, name=self.__class__.__name__) 80 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/hvpoi.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten, Ivo Couckuyt 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .acquisition import Acquisition 16 | from ..pareto import Pareto 17 | 18 | from gpflow.param import DataHolder 19 | from gpflow import settings 20 | 21 | import numpy as np 22 | import tensorflow as tf 23 | 24 | stability = settings.numerics.jitter_level 25 | float_type = settings.dtypes.float_type 26 | 27 | 28 | class HVProbabilityOfImprovement(Acquisition): 29 | """ 30 | Hypervolume-based Probability of Improvement. 31 | 32 | A multiobjective acquisition function for multiobjective optimization. It is used to identify a complete Pareto set 33 | of non-dominated solutions. 34 | 35 | Key reference: 36 | 37 | :: 38 | 39 | @article{Couckuyt:2014, 40 | title={Fast calculation of multiobjective probability of improvement and expected improvement criteria for Pareto optimization}, 41 | author={Couckuyt, Ivo and Deschrijver, Dirk and Dhaene, Tom}, 42 | journal={Journal of Global Optimization}, 43 | volume={60}, 44 | number={3}, 45 | pages={575--594}, 46 | year={2014}, 47 | publisher={Springer} 48 | } 49 | 50 | For a Pareto set :math:`\\mathcal{P}`, the non-dominated section of the objective space is denoted by :math:`A`. 51 | The :meth:`~..pareto.Pareto.hypervolume` of the dominated part of the space is denoted by :math:`\\mathcal{H}` 52 | and can be used as indicator for the optimality of the Pareto set (the higher the better). 53 | 54 | .. math:: 55 | \\boldsymbol{\\mu} &= \\left[ \\mathbb{E} \\left[ f^{(1)}_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} \\right], 56 | ..., \\mathbb{E} \\left[ f^{(p)}_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} \\right]\\right] \\\\ 57 | I\\left(\\boldsymbol{\\mu}, \\mathcal{P}\\right) &= 58 | \\begin{cases} \\left( \\mathcal{H} \\left( \\mathcal{P} \\cup \\boldsymbol{\\mu} \\right) - \\mathcal{H} 59 | \\left( \\mathcal{P} \\right)) \\right) ~ if ~ \\boldsymbol{\\mu} \\in A 60 | \\\\ 0 ~ \\mbox{otherwise} \\end{cases} \\\\ 61 | \\alpha(\\mathbf x_{\\star}) &= I\\left(\\boldsymbol{\\mu}, \\mathcal{P}\\right) p\\left(\\mathbf x_{\\star} \\in A \\right) 62 | 63 | Attributes: 64 | pareto: An instance of :class:`~..pareto.Pareto`. 65 | """ 66 | 67 | def __init__(self, models): 68 | """ 69 | :param models: A list of (possibly multioutput) GPflow representing our belief of the objectives. 70 | """ 71 | super(HVProbabilityOfImprovement, self).__init__(models) 72 | num_objectives = self.data[1].shape[1] 73 | assert num_objectives > 1 74 | 75 | # Keep empty for now - it is updated in _setup() 76 | self.pareto = Pareto(np.empty((0, num_objectives))) 77 | self.reference = DataHolder(np.ones((1, num_objectives))) 78 | 79 | def _estimate_reference(self): 80 | pf = self.pareto.front.value 81 | f = np.max(pf, axis=0, keepdims=True) - np.min(pf, axis=0, keepdims=True) 82 | return np.max(pf, axis=0, keepdims=True) + 2 * f / pf.shape[0] 83 | 84 | def _setup(self): 85 | """ 86 | Pre-computes the Pareto set and cell bounds for integrating over the non-dominated region. 87 | """ 88 | super(HVProbabilityOfImprovement, self)._setup() 89 | 90 | # Obtain hypervolume cell bounds, use prediction mean 91 | feasible_samples = self.data[0][self.highest_parent.feasible_data_index(), :] 92 | F = np.hstack((m.predict_f(feasible_samples)[0] for m in self.models)) 93 | self.pareto.update(F) 94 | 95 | # Calculate reference point. 96 | self.reference = self._estimate_reference() 97 | 98 | def build_acquisition(self, Xcand): 99 | outdim = tf.shape(self.data[1])[1] 100 | num_cells = tf.shape(self.pareto.bounds.lb)[0] 101 | N = tf.shape(Xcand)[0] 102 | 103 | # Extended Pareto front 104 | pf_ext = tf.concat([-np.inf * tf.ones([1, outdim], dtype=float_type), self.pareto.front, self.reference], 0) 105 | 106 | # Predictions for candidates, concatenate columns 107 | preds = [m.build_predict(Xcand) for m in self.models] 108 | candidate_mean, candidate_var = (tf.concat(moment, 1) for moment in zip(*preds)) 109 | candidate_var = tf.maximum(candidate_var, stability) # avoid zeros 110 | 111 | # Calculate the cdf's for all candidates for every predictive distribution in the data points 112 | normal = tf.contrib.distributions.Normal(candidate_mean, tf.sqrt(candidate_var)) 113 | Phi = tf.transpose(normal.cdf(tf.expand_dims(pf_ext, 1)), [1, 0, 2]) # N x pf_ext_size x outdim 114 | 115 | # tf.gather_nd indices for bound points 116 | col_idx = tf.tile(tf.range(outdim), (num_cells,)) 117 | ub_idx = tf.stack((tf.reshape(self.pareto.bounds.ub, [-1]), col_idx), axis=1) # (num_cells*outdim x 2) 118 | lb_idx = tf.stack((tf.reshape(self.pareto.bounds.lb, [-1]), col_idx), axis=1) # (num_cells*outdim x 2) 119 | 120 | # Calculate PoI 121 | P1 = tf.transpose(tf.gather_nd(tf.transpose(Phi, perm=[1, 2, 0]), ub_idx)) # N x num_cell*outdim 122 | P2 = tf.transpose(tf.gather_nd(tf.transpose(Phi, perm=[1, 2, 0]), lb_idx)) # N x num_cell*outdim 123 | P = tf.reshape(P1 - P2, [N, num_cells, outdim]) 124 | PoI = tf.reduce_sum(tf.reduce_prod(P, axis=2), axis=1, keep_dims=True) # N x 1 125 | 126 | # Calculate Hypervolume contribution of points Y 127 | ub_points = tf.reshape(tf.gather_nd(pf_ext, ub_idx), [num_cells, outdim]) 128 | lb_points = tf.reshape(tf.gather_nd(pf_ext, lb_idx), [num_cells, outdim]) 129 | 130 | splus_valid = tf.reduce_all(tf.tile(tf.expand_dims(ub_points, 1), [1, N, 1]) > candidate_mean, 131 | axis=2) # num_cells x N 132 | splus_idx = tf.expand_dims(tf.cast(splus_valid, dtype=float_type), -1) # num_cells x N x 1 133 | splus_lb = tf.tile(tf.expand_dims(lb_points, 1), [1, N, 1]) # num_cells x N x outdim 134 | splus_lb = tf.maximum(splus_lb, candidate_mean) # num_cells x N x outdim 135 | splus_ub = tf.tile(tf.expand_dims(ub_points, 1), [1, N, 1]) # num_cells x N x outdim 136 | splus = tf.concat([splus_idx, splus_ub - splus_lb], axis=2) # num_cells x N x (outdim+1) 137 | Hv = tf.transpose(tf.reduce_sum(tf.reduce_prod(splus, axis=2), axis=0, keep_dims=True)) # N x 1 138 | 139 | # return HvPoI 140 | return tf.multiply(Hv, PoI) 141 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/lcb.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .acquisition import Acquisition 16 | 17 | from gpflow.param import DataHolder 18 | import numpy as np 19 | 20 | import tensorflow as tf 21 | 22 | 23 | class LowerConfidenceBound(Acquisition): 24 | """ 25 | Lower confidence bound acquisition function for single-objective global optimization. 26 | 27 | Key reference: 28 | 29 | :: 30 | 31 | @inproceedings{Srinivas:2010, 32 | author = "Srinivas, Niranjan and Krause, Andreas and Seeger, Matthias and Kakade, Sham M.", 33 | booktitle = "{Proceedings of the 27th International Conference on Machine Learning (ICML-10)}", 34 | editor = "F{\"u}rnkranz, Johannes and Joachims, Thorsten", 35 | pages = "1015--1022", 36 | publisher = "Omnipress", 37 | title = "{Gaussian Process Optimization in the Bandit Setting: No Regret and Experimental Design}", 38 | year = "2010" 39 | } 40 | 41 | .. math:: 42 | \\alpha(\\mathbf x_{\\star}) =\\mathbb{E} \\left[ f_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} \\right] 43 | - \\sigma \\mbox{Var} \\left[ f_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} \\right] 44 | """ 45 | 46 | def __init__(self, model, sigma=2.0): 47 | """ 48 | :param model: GPflow model (single output) representing our belief of the objective 49 | :param sigma: See formula, the higher the more exploration 50 | """ 51 | super(LowerConfidenceBound, self).__init__(model) 52 | self.sigma = DataHolder(np.array(sigma)) 53 | 54 | def build_acquisition(self, Xcand): 55 | candidate_mean, candidate_var = self.models[0].build_predict(Xcand) 56 | candidate_var = tf.maximum(candidate_var, 0) 57 | return tf.subtract(candidate_mean, self.sigma * tf.sqrt(candidate_var), name=self.__class__.__name__) 58 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/mes.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten, Nicolas Knudde 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .acquisition import Acquisition 16 | from ..design import RandomDesign 17 | 18 | from gpflow import settings 19 | from gpflow.param import DataHolder 20 | from gpflow.model import Model 21 | 22 | import numpy as np 23 | from scipy.stats import norm 24 | from scipy.optimize import bisect 25 | import tensorflow as tf 26 | 27 | float_type = settings.dtypes.float_type 28 | stability = settings.numerics.jitter_level 29 | np_float_type = np.float32 if float_type is tf.float32 else np.float64 30 | 31 | 32 | class MinValueEntropySearch(Acquisition): 33 | """ 34 | Max-value entropy search acquisition function for single-objective global optimization. 35 | Introduced by (Wang et al., 2017). 36 | 37 | Key reference: 38 | 39 | :: 40 | @InProceedings{Wang:2017, 41 | title = {Max-value Entropy Search for Efficient {B}ayesian Optimization}, 42 | author = {Zi Wang and Stefanie Jegelka}, 43 | booktitle = {Proceedings of the 34th International Conference on Machine Learning}, 44 | pages = {3627--3635}, 45 | year = {2017}, 46 | editor = {Doina Precup and Yee Whye Teh}, 47 | volume = {70}, 48 | series = {Proceedings of Machine Learning Research}, 49 | address = {International Convention Centre, Sydney, Australia}, 50 | month = {06--11 Aug}, 51 | publisher = {PMLR}, 52 | } 53 | """ 54 | 55 | def __init__(self, model, domain, gridsize=10000, num_samples=10): 56 | assert isinstance(model, Model) 57 | super(MinValueEntropySearch, self).__init__(model) 58 | assert self.data[1].shape[1] == 1 59 | self.gridsize = gridsize 60 | self.num_samples = num_samples 61 | self.samples = DataHolder(np.zeros(num_samples, dtype=np_float_type)) 62 | self._domain = domain 63 | 64 | def _setup(self): 65 | super(MinValueEntropySearch, self)._setup() 66 | 67 | # Apply Gumbel sampling 68 | m = self.models[0] 69 | valid = self.feasible_data_index() 70 | 71 | # Work with feasible data 72 | X = self.data[0][valid, :] 73 | N = np.shape(X)[0] 74 | Xrand = RandomDesign(self.gridsize, self._domain).generate() 75 | fmean, fvar = m.predict_f(np.vstack((X, Xrand))) 76 | idx = np.argmin(fmean[:N]) 77 | right = fmean[idx].flatten()# + 2*np.sqrt(fvar[idx]).flatten() 78 | left = right 79 | probf = lambda x: np.exp(np.sum(norm.logcdf(-(x - fmean) / np.sqrt(fvar)), axis=0)) 80 | 81 | i = 0 82 | while probf(left) < 0.75: 83 | left = 2. ** i * np.min(fmean - 5. * np.sqrt(fvar)) + (1. - 2. ** i) * right 84 | i += 1 85 | 86 | # Binary search for 3 percentiles 87 | q1, med, q2 = map(lambda val: bisect(lambda x: probf(x) - val, left, right, maxiter=10000, xtol=0.01), 88 | [0.25, 0.5, 0.75]) 89 | beta = (q1 - q2) / (np.log(np.log(4. / 3.)) - np.log(np.log(4.))) 90 | alpha = med + beta * np.log(np.log(2.)) 91 | 92 | # obtain samples from y* 93 | mins = -np.log(-np.log(np.random.rand(self.num_samples).astype(np_float_type))) * beta + alpha 94 | self.samples.set_data(mins) 95 | 96 | def build_acquisition(self, Xcand): 97 | fmean, fvar = self.models[0].build_predict(Xcand) 98 | norm = tf.contrib.distributions.Normal(tf.constant(0.0, dtype=float_type), tf.constant(1.0, dtype=float_type)) 99 | gamma = (fmean - tf.expand_dims(self.samples, axis=0)) / tf.sqrt(fvar) 100 | 101 | return tf.reduce_sum(gamma * norm.prob(gamma) / (2. * norm.cdf(gamma)) - norm.log_cdf(gamma), 102 | axis=1, keep_dims=True) / self.num_samples 103 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/pof.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .acquisition import Acquisition 16 | 17 | from gpflow import settings 18 | 19 | import numpy as np 20 | import tensorflow as tf 21 | 22 | float_type = settings.dtypes.float_type 23 | stability = settings.numerics.jitter_level 24 | 25 | 26 | class ProbabilityOfFeasibility(Acquisition): 27 | """ 28 | Probability of Feasibility acquisition function for sampling feasible regions. Standard acquisition function for 29 | Bayesian Optimization with black-box expensive constraints. 30 | 31 | Key reference: 32 | 33 | :: 34 | 35 | @article{Schonlau:1997, 36 | title={Computer experiments and global optimization}, 37 | author={Schonlau, Matthias}, 38 | year={1997}, 39 | publisher={University of Waterloo} 40 | } 41 | 42 | The acquisition function measures the probability of the latent function 43 | being smaller than a threshold for a candidate point. 44 | 45 | .. math:: 46 | \\alpha(\\mathbf x_{\\star}) = \\int_{-\\infty}^{0} \\, p(f_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} ) \\, d f_{\\star} 47 | """ 48 | 49 | def __init__(self, model, threshold=0.0, minimum_pof=0.5): 50 | """ 51 | :param model: GPflow model (single output) representing our belief of the constraint 52 | :param threshold: Observed values lower than the threshold are considered valid 53 | :param minimum_pof: minimum pof score required for a point to be valid. 54 | For more information, see docstring of feasible_data_index 55 | """ 56 | super(ProbabilityOfFeasibility, self).__init__(model) 57 | self.threshold = threshold 58 | self.minimum_pof = minimum_pof 59 | 60 | def constraint_indices(self): 61 | return np.arange(self.data[1].shape[1]) 62 | 63 | def feasible_data_index(self): 64 | """ 65 | Returns a boolean array indicating which points are feasible (True) and which are not (False). 66 | 67 | Answering the question *which points are feasible?* is slightly troublesome in case noise is present. 68 | Directly relying on the noisy data and comparing it to self.threshold does not make much sense. 69 | 70 | Instead, we rely on the model belief using the PoF (a probability between 0 and 1). 71 | As the implementation of the PoF corresponds to the cdf of the (normal) predictive distribution in 72 | a point evaluated at the threshold, requiring a minimum pof of 0.5 implies the mean of the predictive 73 | distribution is below the threshold, hence it is marked as feasible. A minimum pof of 0 marks all points valid. 74 | Setting it to 1 results in all invalid. 75 | 76 | :return: boolean ndarray (size N) 77 | """ 78 | pred = self.evaluate(self.data[0]) 79 | return pred.ravel() > self.minimum_pof 80 | 81 | def build_acquisition(self, Xcand): 82 | candidate_mean, candidate_var = self.models[0].build_predict(Xcand) 83 | candidate_var = tf.maximum(candidate_var, stability) 84 | normal = tf.contrib.distributions.Normal(candidate_mean, tf.sqrt(candidate_var)) 85 | return normal.cdf(tf.constant(self.threshold, dtype=float_type), name=self.__class__.__name__) 86 | -------------------------------------------------------------------------------- /gpflowopt/acquisition/poi.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .acquisition import Acquisition 16 | 17 | from gpflow.param import DataHolder 18 | from gpflow import settings 19 | 20 | import numpy as np 21 | import tensorflow as tf 22 | 23 | stability = settings.numerics.jitter_level 24 | 25 | 26 | class ProbabilityOfImprovement(Acquisition): 27 | """ 28 | Probability of Improvement acquisition function for single-objective global optimization. 29 | 30 | Key reference: 31 | 32 | :: 33 | 34 | @article{Kushner:1964, 35 | author = "Kushner, Harold J", 36 | journal = "Journal of Basic Engineering", 37 | number = "1", 38 | pages = "97--106", 39 | publisher = "American Society of Mechanical Engineers", 40 | title = "{A new method of locating the maximum point of an arbitrary multipeak curve in the presence of noise}", 41 | volume = "86", 42 | year = "1964" 43 | } 44 | 45 | .. math:: 46 | \\alpha(\\mathbf x_{\\star}) = \\int_{-\\infty}^{f_{\\min}} \\, p( f_{\\star}\\,|\\, \\mathbf x, \\mathbf y, \\mathbf x_{\\star} ) \\, d f_{\\star} 47 | """ 48 | 49 | def __init__(self, model): 50 | """ 51 | :param model: GPflow model (single output) representing our belief of the objective 52 | """ 53 | super(ProbabilityOfImprovement, self).__init__(model) 54 | self.fmin = DataHolder(np.zeros(1)) 55 | self._setup() 56 | 57 | def _setup(self): 58 | super(ProbabilityOfImprovement, self)._setup() 59 | feasible_samples = self.data[0][self.highest_parent.feasible_data_index(), :] 60 | samples_mean, _ = self.models[0].predict_f(feasible_samples) 61 | self.fmin.set_data(np.min(samples_mean, axis=0)) 62 | 63 | def build_acquisition(self, Xcand): 64 | candidate_mean, candidate_var = self.models[0].build_predict(Xcand) 65 | candidate_var = tf.maximum(candidate_var, stability) 66 | normal = tf.contrib.distributions.Normal(candidate_mean, tf.sqrt(candidate_var)) 67 | return normal.cdf(self.fmin, name=self.__class__.__name__) 68 | -------------------------------------------------------------------------------- /gpflowopt/bo.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from contextlib import contextmanager 16 | 17 | import numpy as np 18 | from scipy.optimize import OptimizeResult 19 | import tensorflow as tf 20 | from gpflow.gpr import GPR 21 | 22 | from .acquisition import Acquisition, MCMCAcquistion 23 | from .design import Design, EmptyDesign 24 | from .objective import ObjectiveWrapper 25 | from .optim import Optimizer, SciPyOptimizer 26 | from .pareto import non_dominated_sort 27 | from .models import ModelWrapper 28 | 29 | 30 | def jitchol_callback(models): 31 | """ 32 | Increase the likelihood in case of Cholesky failures. 33 | 34 | This is similar to the use of jitchol in GPy. Default callback for BayesianOptimizer. 35 | Only usable on GPR models, other types are ignored. 36 | """ 37 | for m in np.atleast_1d(models): 38 | if isinstance(m, ModelWrapper): 39 | jitchol_callback(m.wrapped) # pragma: no cover 40 | 41 | if not isinstance(m, GPR): 42 | continue 43 | 44 | s = m.get_free_state() 45 | eKdiag = np.mean(np.diag(m.kern.compute_K_symm(m.X.value))) 46 | for e in [0] + [10**ex for ex in range(-6,-1)]: 47 | try: 48 | m.likelihood.variance = m.likelihood.variance.value + e * eKdiag 49 | m.optimize(maxiter=5) 50 | break 51 | except tf.errors.InvalidArgumentError: # pragma: no cover 52 | m.set_state(s) 53 | 54 | 55 | class BayesianOptimizer(Optimizer): 56 | """ 57 | A traditional Bayesian optimization framework implementation. 58 | 59 | Like other optimizers, this optimizer is constructed for optimization over a domain. 60 | Additionally, it is configured with a separate optimizer for the acquisition function. 61 | """ 62 | 63 | def __init__(self, domain, acquisition, optimizer=None, initial=None, scaling=True, hyper_draws=None, 64 | callback=jitchol_callback, verbose=False): 65 | """ 66 | :param Domain domain: The optimization space. 67 | :param Acquisition acquisition: The acquisition function to optimize over the domain. 68 | :param Optimizer optimizer: (optional) optimization approach for the acquisition function. 69 | If not specified, :class:`~.optim.SciPyOptimizer` is used. 70 | This optimizer will run on the same domain as the :class:`.BayesianOptimizer` object. 71 | :param Design initial: (optional) The initial design of candidates to evaluate 72 | before the optimization loop runs. Note that if the underlying model contains already some data from 73 | an initial design, it is augmented with the evaluations obtained by evaluating 74 | the points as specified by the design. 75 | :param bool scaling: (boolean, default true) if set to true, the outputs are normalized, and the inputs are 76 | scaled to a unit cube. This only affects model training: calls to acquisition.data, as well as 77 | returned optima are unscaled (see :class:`~.DataScaler` for more details.). Note, the models contained by 78 | acquisition are modified directly, and so the references to the model outside of BayesianOptimizer now point 79 | to scaled models. 80 | :param int hyper_draws: (optional) Enable marginalization of model hyperparameters. By default, point estimates are 81 | used. If this parameter set to n, n hyperparameter draws from the likelihood distribution 82 | are obtained using Hamiltonian MC. 83 | (see `GPflow documentation `_ for details) for each model. 84 | The acquisition score is computed for each draw, and averaged. 85 | :param callable callback: (optional) this function or object will be called, after the 86 | data of all models has been updated with all models as retrieved by acquisition.models as argument without 87 | the wrapping model handling any scaling . This allows custom model optimization strategies to be implemented. 88 | All manipulations of GPflow models are permitted. Combined with the optimize_restarts parameter of 89 | :class:`~.Acquisition` this allows several scenarios: do the optimization manually from the callback 90 | (optimize_restarts equals 0), or choose the starting point + some random restarts (optimize_restarts > 0). 91 | """ 92 | assert isinstance(acquisition, Acquisition) 93 | assert hyper_draws is None or hyper_draws > 0 94 | assert optimizer is None or isinstance(optimizer, Optimizer) 95 | assert initial is None or isinstance(initial, Design) 96 | super(BayesianOptimizer, self).__init__(domain, exclude_gradient=True) 97 | 98 | self._scaling = scaling 99 | if self._scaling: 100 | acquisition.enable_scaling(domain) 101 | 102 | self.acquisition = acquisition if hyper_draws is None else MCMCAcquistion(acquisition, hyper_draws) 103 | 104 | self.optimizer = optimizer or SciPyOptimizer(domain) 105 | self.optimizer.domain = domain 106 | initial = initial or EmptyDesign(domain) 107 | self.set_initial(initial.generate()) 108 | 109 | self._model_callback = callback 110 | self.verbose = verbose 111 | 112 | @Optimizer.domain.setter 113 | def domain(self, dom): 114 | assert self.domain.size == dom.size 115 | super(BayesianOptimizer, self.__class__).domain.fset(self, dom) 116 | if self._scaling: 117 | self.acquisition.enable_scaling(dom) 118 | 119 | def _update_model_data(self, newX, newY): 120 | """ 121 | Update the underlying models of the acquisition function with new data. 122 | 123 | :param newX: samples, size N x D 124 | :param newY: values obtained by evaluating the objective and constraint functions, size N x R 125 | """ 126 | assert self.acquisition.data[0].shape[1] == newX.shape[-1] 127 | assert self.acquisition.data[1].shape[1] == newY.shape[-1] 128 | assert newX.shape[0] == newY.shape[0] 129 | if newX.size == 0: 130 | return 131 | X = np.vstack((self.acquisition.data[0], newX)) 132 | Y = np.vstack((self.acquisition.data[1], newY)) 133 | self.acquisition.set_data(X, Y) 134 | 135 | def _evaluate_objectives(self, X, fxs): 136 | """ 137 | Evaluates a list of n functions on X. 138 | 139 | Returns a matrix, size N x sum(Q0,...Qn-1) 140 | with Qi the number of columns obtained by evaluating the i-th function. 141 | 142 | :param X: input points, size N x D 143 | :param fxs: functions, size n 144 | :return: tuple: 145 | (0) the evaluations Y, size N x sum(Q0,...Qn-1). 146 | (1) Not used, size N x 0. Bayesian Optimizer is gradient-free, however calling optimizer of the parent class 147 | expects a gradient. Will be discarded further on. 148 | """ 149 | if X.size > 0: 150 | evaluations = np.hstack(map(lambda f: f(X), fxs)) 151 | assert evaluations.shape[1] == self.acquisition.data[1].shape[1] 152 | return evaluations, np.zeros((X.shape[0], 0)) 153 | else: 154 | return np.empty((0, self.acquisition.data[1].shape[1])), np.zeros((0, 0)) 155 | 156 | def _create_bo_result(self, success, message): 157 | """ 158 | Analyzes all data evaluated during the optimization, and return an `OptimizeResult`. Constraints are taken 159 | into account. The contents of x, fun, and constraints depend on the detected scenario: 160 | - single-objective: the best optimum of the feasible samples (if none, optimum of the infeasible samples) 161 | - multi-objective: the Pareto set of the feasible samples 162 | - only constraints: all the feasible samples (can be empty) 163 | 164 | In all cases, if not one sample satisfies all the constraints a message will be given and success=False. 165 | 166 | Do note that the feasibility check is based on the model predictions, but the constrained field contains 167 | actual data values. 168 | 169 | :param success: Optimization successful? (True/False) 170 | :param message: return message 171 | :return: OptimizeResult object 172 | """ 173 | X, Y = self.acquisition.data 174 | 175 | # Filter on constraints 176 | valid = self.acquisition.feasible_data_index() 177 | 178 | # Extract the samples that satisfies all constraints 179 | if np.any(valid): 180 | X = X[valid, :] 181 | Y = Y[valid, :] 182 | else: 183 | success = False 184 | message = "No evaluations satisfied all the constraints" 185 | 186 | # Split between objectives and constraints 187 | Yo = Y[:, self.acquisition.objective_indices()] 188 | Yc = Y[:, self.acquisition.constraint_indices()] 189 | 190 | # Differentiate between different scenarios 191 | if Yo.shape[1] == 1: # Single-objective: minimum 192 | idx = np.argmin(Yo) 193 | elif Yo.shape[1] > 1: # Multi-objective: Pareto set 194 | _, dom = non_dominated_sort(Yo) 195 | idx = dom == 0 196 | else: # Constraint satisfaction problem: all samples satisfying the constraints 197 | idx = np.arange(Yc.shape[0]) 198 | 199 | return OptimizeResult(x=X[idx, :], 200 | success=success, 201 | fun=Yo[idx, :], 202 | constraints=Yc[idx, :], 203 | message=message) 204 | 205 | def optimize(self, objectivefx, n_iter=20): 206 | """ 207 | Run Bayesian optimization for a number of iterations. 208 | 209 | Before the loop is initiated, first all points retrieved by :meth:`~.optim.Optimizer.get_initial` are evaluated 210 | on the objective and black-box constraints. These points are then added to the acquisition function 211 | by calling :meth:`~.acquisition.Acquisition.set_data` (and hence, the underlying models). 212 | 213 | Each iteration a new data point is selected for evaluation by optimizing an acquisition function. This point 214 | updates the models. 215 | 216 | :param objectivefx: (list of) expensive black-box objective and constraint functions. For evaluation, the 217 | responses of all the expensive functions are aggregated column wise. 218 | Unlike the typical :class:`~.optim.Optimizer` interface, these functions should not return gradients. 219 | :param n_iter: number of iterations to run 220 | :return: OptimizeResult object 221 | """ 222 | fxs = np.atleast_1d(objectivefx) 223 | return super(BayesianOptimizer, self).optimize(lambda x: self._evaluate_objectives(x, fxs), n_iter=n_iter) 224 | 225 | def _optimize(self, fx, n_iter): 226 | """ 227 | Internal optimization function. Receives an ObjectiveWrapper as input. As exclude_gradient is set to true, 228 | the placeholder created by :meth:`_evaluate_objectives` will not be returned. 229 | 230 | :param fx: :class:`.objective.ObjectiveWrapper` object wrapping expensive black-box objective and constraint functions 231 | :param n_iter: number of iterations to run 232 | :return: OptimizeResult object 233 | """ 234 | assert isinstance(fx, ObjectiveWrapper) 235 | 236 | # Evaluate and add the initial design (if any) 237 | initial = self.get_initial() 238 | values = fx(initial) 239 | self._update_model_data(initial, values) 240 | 241 | # Remove initial design for additional calls to optimize to proceed optimization 242 | self.set_initial(EmptyDesign(self.domain).generate()) 243 | 244 | def inverse_acquisition(x): 245 | return tuple(map(lambda r: -r, self.acquisition.evaluate_with_gradients(np.atleast_2d(x)))) 246 | 247 | # Optimization loop 248 | for i in range(n_iter): 249 | # If a callback is specified, and acquisition has the setup flag enabled (indicating an upcoming 250 | # compilation), run the callback. 251 | with self.silent(): 252 | if self._model_callback and self.acquisition._needs_setup: 253 | self._model_callback([m.wrapped for m in self.acquisition.models]) 254 | 255 | result = self.optimizer.optimize(inverse_acquisition) 256 | self._update_model_data(result.x, fx(result.x)) 257 | 258 | if self.verbose: 259 | metrics = [] 260 | 261 | with self.silent(): 262 | bo_result = self._create_bo_result(True, 'Monitor') 263 | metrics += ['MLL [' + ', '.join('{:.3}'.format(model.compute_log_likelihood()) for model in self.acquisition.models) + ']'] 264 | 265 | # fmin 266 | n_points = bo_result.fun.shape[0] 267 | if n_points > 0: 268 | funs = np.atleast_1d(np.min(bo_result.fun, axis=0)) 269 | fmin = 'fmin [' + ', '.join('{:.3}'.format(fun) for fun in funs) + ']' 270 | if n_points > 1: 271 | fmin += ' (size {0})'.format(n_points) 272 | 273 | metrics += [fmin] 274 | 275 | # constraints 276 | n_points = bo_result.constraints.shape[0] 277 | if n_points > 0: 278 | constraints = np.atleast_1d(np.min(bo_result.constraints, axis=0)) 279 | metrics += ['constraints [' + ', '.join('{:.3}'.format(constraint) for constraint in constraints) + ']'] 280 | 281 | # error messages 282 | metrics += [r.message.decode('utf-8') if isinstance(r.message, bytes) else r.message for r in [bo_result, result] if not r.success] 283 | 284 | print('iter #{0:>3} - {1}'.format( 285 | i, 286 | ' - '.join(metrics))) 287 | 288 | return self._create_bo_result(True, "OK") 289 | 290 | @contextmanager 291 | def failsafe(self): 292 | """ 293 | Context to provide a safe way for optimization. 294 | 295 | If a RuntimeError is generated, the data of the acquisition object is saved to the disk. 296 | in the current directory. This allows the data to be re-used (which makes sense for expensive data). 297 | 298 | The data can be used to experiment with fitting a GPflow model first (analyse the data, set sensible initial 299 | hyperparameter values and hyperpriors) before retrying Bayesian Optimization again. 300 | """ 301 | try: 302 | yield 303 | except Exception as e: 304 | np.savez('failed_bopt_{0}'.format(id(e)), X=self.acquisition.data[0], Y=self.acquisition.data[1]) 305 | raise 306 | -------------------------------------------------------------------------------- /gpflowopt/design.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | from scipy.spatial.distance import cdist, pdist 17 | import tensorflow as tf 18 | 19 | from gpflow import settings 20 | 21 | from .domain import ContinuousParameter 22 | 23 | 24 | float_type = settings.dtypes.float_type 25 | stability = settings.numerics.jitter_level 26 | np_float_type = np.float32 if float_type is tf.float32 else np.float64 27 | 28 | 29 | class Design(object): 30 | """ 31 | Design of size N (number of points) generated within a D-dimensional domain. 32 | 33 | Users should call generate() which auto-scales the design to the domain specified in the constructor. 34 | To implement new design methodologies subclasses should implement create_design(), 35 | which returns the design on the domain specified by the generative_domain method (which defaults to a unit cube). 36 | """ 37 | 38 | def __init__(self, size, domain): 39 | """ 40 | :param size: number of data points to generate 41 | :param domain: domain to generate data points in. 42 | """ 43 | super(Design, self).__init__() 44 | self.size = size 45 | self.domain = domain 46 | 47 | @property 48 | def generative_domain(self): 49 | """ 50 | :return: Domain object representing the domain associated with the points generated in create_design(). 51 | Defaults to [0,1]^D, can be overwritten by subclasses 52 | """ 53 | return np.sum([ContinuousParameter('d{0}'.format(i), 0, 1) for i in np.arange(self.domain.size)]) 54 | 55 | def generate(self): 56 | """ 57 | Creates the design in the domain specified during construction. 58 | 59 | It is guaranteed that all data points satisfy this domain 60 | 61 | :return: data matrix, size N x D 62 | """ 63 | Xs = self.create_design() 64 | assert (Xs in self.generative_domain) 65 | assert (Xs.shape == (self.size, self.domain.size)) 66 | transform = self.generative_domain >> self.domain 67 | # X = np.clip(transform.forward(Xs), self.domain.lower, self.domain.upper) 68 | X = transform.forward(Xs) 69 | assert (X in self.domain) 70 | return X 71 | 72 | def create_design(self): 73 | """ 74 | Returns a design generated in the `generative` domain. 75 | 76 | This method should be implemented in the subclasses. 77 | 78 | :return: data matrix, N x D 79 | """ 80 | raise NotImplementedError 81 | 82 | 83 | class RandomDesign(Design): 84 | """ 85 | Random space-filling design. 86 | 87 | Generates points drawn from the standard uniform distribution U(0,1). 88 | """ 89 | 90 | def __init__(self, size, domain): 91 | super(RandomDesign, self).__init__(size, domain) 92 | 93 | def create_design(self): 94 | return np.random.rand(self.size, self.domain.size).astype(np_float_type) 95 | 96 | 97 | class FactorialDesign(Design): 98 | """ 99 | A k-level grid-based design. 100 | 101 | Design with the optimal minimal distance between points (a simple grid), however it risks collapsing points when 102 | removing parameters. Its size is a power of the domain dimensionality. 103 | """ 104 | 105 | def __init__(self, levels, domain): 106 | self.levels = levels 107 | size = levels ** domain.size 108 | super(FactorialDesign, self).__init__(size, domain) 109 | 110 | @Design.generative_domain.getter 111 | def generative_domain(self): 112 | return self.domain 113 | 114 | def create_design(self): 115 | Xs = np.meshgrid(*[np.linspace(l, u, self.levels) for l, u in zip(self.domain.lower, self.domain.upper)]) 116 | return np.vstack(map(lambda X: X.ravel(), Xs)).T 117 | 118 | 119 | class EmptyDesign(Design): 120 | """ 121 | No design, can be used as placeholder. 122 | """ 123 | 124 | def __init__(self, domain): 125 | super(EmptyDesign, self).__init__(0, domain) 126 | 127 | def create_design(self): 128 | return np.empty((0, self.domain.size)) 129 | 130 | 131 | class LatinHyperCube(Design): 132 | """ 133 | Latin hypercube with optimized minimal distance between points. 134 | 135 | Created with the Translational Propagation algorithm to avoid lengthy generation procedures. 136 | For dimensions smaller or equal to 6, this algorithm finds the quasi-optimal LHD with overwhelming probability. 137 | To increase this probability, if a design for a domain with dimensionality D is requested, 138 | D different designs are generated using seed sizes 1,2,...D (unless a maximum seed size 1<= S <= D is specified. 139 | The seeds themselves are small Latin hypercubes generated with the same algorithm. 140 | 141 | Beyond 6D, the probability of finding the optimal LHD fades, although the resulting designs are still acceptable. 142 | Somewhere beyond 15D this algorithm tends to slow down a lot and become very memory demanding. Key reference is 143 | 144 | :: 145 | 146 | @article{Viana:2010, 147 | title={An algorithm for fast optimal Latin hypercube design of experiments}, 148 | author={Viana, Felipe AC and Venter, Gerhard and Balabanov, Vladimir}, 149 | journal={International Journal for Numerical Methods in Engineering}, 150 | volume={82}, 151 | number={2}, 152 | pages={135--156}, 153 | year={2010}, 154 | publisher={John Wiley & Sons, Ltd.} 155 | } 156 | 157 | For pre-generated LHDs please see the `following website `_. 158 | """ 159 | 160 | def __init__(self, size, domain, max_seed_size=None): 161 | """ 162 | :param size: requested size N for the LHD 163 | :param domain: domain to generate the LHD for, must be continuous 164 | :param max_seed_size: the maximum size 1 <= S <= D for the seed, . If unspecified, equals the dimensionality D 165 | of the domain. During generation, S different designs are generated. Seeds with sizes 1,2,...S are used. 166 | Each seed itself is a small LHD. 167 | """ 168 | super(LatinHyperCube, self).__init__(size, domain) 169 | self._max_seed_size = np.round(max_seed_size or domain.size) 170 | assert (1 <= np.round(self._max_seed_size) <= domain.size) 171 | 172 | @Design.generative_domain.getter 173 | def generative_domain(self): 174 | """ 175 | :return: Domain object representing [1, N]^D, the generative domain for the TPLHD algorithm. 176 | """ 177 | return np.sum([ContinuousParameter('d{0}'.format(i), 1, self.size) for i in np.arange(self.domain.size)]) 178 | 179 | def create_design(self): 180 | """ 181 | Generate several LHDs with increasing seed. 182 | 183 | Maximum S = min(dimensionality,max_seed_size). 184 | From S candidate designs, the one with the best intersite distance is returned 185 | 186 | :return: data matrix, size N x D. 187 | """ 188 | candidates = [] 189 | scores = [] 190 | 191 | for i in np.arange(1, min(self.size, self._max_seed_size) + 1): 192 | if i < 3: 193 | # Hardcoded seeds for 1 or two points. 194 | seed = np.arange(1, i + 1)[:, None] * np.ones((1, self.domain.size)) 195 | else: 196 | # Generate larger seeds recursively by creating small TPLHD's 197 | seed = LatinHyperCube(i, self.domain, max_seed_size=i - 1).generate() 198 | 199 | # Create all designs and compute score 200 | X = self._tplhd_design(seed) 201 | candidates.append(X) 202 | scores.append(np.min(pdist(X))) 203 | 204 | # Transform best design (highest score) to specified domain 205 | return candidates[np.argmax(scores)] 206 | 207 | def _tplhd_design(self, seed): 208 | """ 209 | Creates an LHD with the Translational propagation algorithm. 210 | 211 | Uses the specified seed and design size N specified during construction. 212 | 213 | :param seed: seed design, size S x D 214 | :return: data matrix, size N x D 215 | """ 216 | ns, nv = seed.shape 217 | 218 | # Start by computing two quantities. 219 | # 1) the number of translation steps in each dimension 220 | nd = np.power(self.size / float(ns), 1 / float(nv)) 221 | ndStar = np.ceil(nd) 222 | 223 | # 2) the total amount of points we'll be generating. 224 | # Typically, npStar > self.size, although sometimes npStar == self.size 225 | npStar = np.power(ndStar, nv) * ns if ndStar > nd else self.size 226 | 227 | # First rescale the seed, then perform translations and propagations. 228 | seed = self._rescale_seed(seed, npStar, ndStar) 229 | X = self._translate_propagate(seed, npStar, ndStar) 230 | 231 | # In case npStar > N, shrink the design to the requested size specified in __init__ 232 | return self._shrink(X, self.size) 233 | 234 | @staticmethod 235 | def _rescale_seed(seed, npStar, ndStar): 236 | """ 237 | Rescales the seed design 238 | 239 | :param seed: seed design, size S x D 240 | :param npStar: size of the LHD to be generated. N* >= N 241 | :param ndStar: number of translation steps for the seed in each dimension 242 | :return: rescaled seeds, size S x D 243 | """ 244 | ns, nv = seed.shape 245 | if ns == 1: 246 | seed = np.ones((1, nv)) 247 | return seed 248 | uf = ns * np.ones(nv) 249 | ut = ((npStar / ndStar) - ndStar * (nv - 1) + 1) * np.ones(nv) 250 | a = (ut - 1) / (uf - 1) 251 | b = ut - a * uf 252 | 253 | return np.round(a * seed + b) 254 | 255 | @staticmethod 256 | def _translate_propagate(seed, npStar, ndStar): 257 | """ 258 | Translates and propagates the seed design to a LHD of size npStar (which might exceed the requested size N) 259 | 260 | :param seed: seed design, size S x D 261 | :param npStar: size of the LHD to be generated (N*). 262 | :param ndStar: number of translation steps for the seed in each dimension 263 | :return: LHD data matrix, size N* x D (still to be shrinked). 264 | """ 265 | nv = seed.shape[1] 266 | X = seed 267 | 268 | for c1 in range(0, nv): 269 | # Propagation step 270 | seed = X 271 | # Define translation 272 | d = np.concatenate((np.power(ndStar, c1 - 1) * np.ones(np.max((c1, 0))), 273 | [npStar / ndStar], 274 | np.power(ndStar, c1) * np.ones(nv - np.max((c1, 0)) - 1))) 275 | for c2 in np.arange(1, ndStar): 276 | # Translation steps 277 | seed = seed + d 278 | X = np.vstack((X, seed)) 279 | 280 | assert (X.shape == (npStar, nv)) 281 | return X 282 | 283 | @staticmethod 284 | def _shrink(X, npoints): 285 | """ 286 | When designs are generated that are larger than the requested number of points (N* > N), resize them. 287 | If the size was correct all along, the LHD is returned unchanged. 288 | 289 | :param X: Generated LHD, size N* x D, with N* >= N 290 | :param npoints: What size to resize to (N) 291 | :return: LHD data matrix, size N x D 292 | """ 293 | npStar, nv = X.shape 294 | 295 | # Pick N samples nearest to centre of X 296 | centre = npStar * np.ones((1, nv)) / 2. 297 | distances = cdist(X, centre).ravel() 298 | idx = np.argsort(distances) 299 | X = X[idx[:npoints], :] 300 | 301 | # Translate to origin 302 | X -= np.min(X, axis=0) - 1 303 | 304 | # Collapse gaps in the design to assure all cell projections onto axes have 1 sample 305 | Xs = np.argsort(X, axis=0) 306 | X[Xs, np.arange(nv)] = np.tile(np.arange(1, npoints + 1), (nv, 1)).T 307 | assert (X.shape[0] == npoints) 308 | return X 309 | -------------------------------------------------------------------------------- /gpflowopt/domain.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | from itertools import chain 17 | from gpflow.param import Parentable 18 | 19 | from .transforms import LinearTransform 20 | 21 | 22 | class Domain(Parentable): 23 | """ 24 | A domain representing the mathematical space over which is optimized. 25 | """ 26 | 27 | def __init__(self, parameters): 28 | super(Domain, self).__init__() 29 | self._parameters = parameters 30 | 31 | @property 32 | def lower(self): 33 | """ 34 | Lower bound of the domain, corresponding to a numpy array with the lower value of each parameter 35 | """ 36 | return np.array(list(map(lambda param: param.lower, self._parameters))).flatten() 37 | 38 | @property 39 | def upper(self): 40 | """ 41 | Upper bound of the domain, corresponding to a numpy array with the upper value of each parameter 42 | """ 43 | return np.array(list(map(lambda param: param.upper, self._parameters))).flatten() 44 | 45 | def __add__(self, other): 46 | assert isinstance(other, Domain) 47 | return Domain(self._parameters + other._parameters) 48 | 49 | @property 50 | def size(self): 51 | """ 52 | Returns the dimensionality of the domain 53 | """ 54 | return sum(map(lambda param: param.size, self._parameters)) 55 | 56 | def __setattr__(self, key, value): 57 | super(Domain, self).__setattr__(key, value) 58 | if key is not '_parent': 59 | if isinstance(value, Parentable): 60 | value._parent = self 61 | if isinstance(value, list): 62 | for val in (x for x in value if isinstance(x, Parentable)): 63 | val._parent = self 64 | 65 | def __eq__(self, other): 66 | return self._parameters == other._parameters 67 | 68 | def __contains__(self, X): 69 | X = np.atleast_2d(X) 70 | if X.shape[1] is not self.size: 71 | return False 72 | return np.all(np.logical_and(np.logical_or(self.lower < X, np.isclose(self.lower, X)), 73 | np.logical_or(X < self.upper, np.isclose(self.upper, X)))) 74 | 75 | def __iter__(self): 76 | for v in chain(*map(iter, self._parameters)): 77 | yield v 78 | 79 | def __getitem__(self, items): 80 | if isinstance(items, list): 81 | return np.sum([self[item] for item in items]) 82 | 83 | if isinstance(items, str): 84 | labels = [param.label for param in self._parameters] 85 | items = labels.index(items) 86 | 87 | return self._parameters[items] 88 | 89 | def __rshift__(self, other): 90 | assert(self.size == other.size) 91 | A = (other.upper - other.lower) / (self.upper - self.lower) 92 | b = -self.upper * A + other.upper 93 | return LinearTransform(A, b) 94 | 95 | @property 96 | def value(self): 97 | return np.vstack(map(lambda p: p.value, self._parameters)).T 98 | 99 | @value.setter 100 | def value(self, x): 101 | x = np.atleast_2d(x) 102 | assert (len(x.shape) == 2) 103 | assert (x.shape[1] == self.size) 104 | offset = 0 105 | for p in self._parameters: 106 | p.value = x[:, offset:offset + p.size] 107 | offset += p.size 108 | 109 | def _repr_html_(self): 110 | """ 111 | Build html string for table display in jupyter notebooks. 112 | """ 113 | html = [""] 114 | 115 | # Table header 116 | columns = ['Name', 'Type', 'Values'] 117 | header = "" 118 | header += ''.join(map(lambda l: "".format(l), columns)) 119 | header += "" 120 | html.append(header) 121 | 122 | # Add parameters 123 | html.append(self._html_table_rows()) 124 | html.append("
{0}
") 125 | 126 | return ''.join(html) 127 | 128 | def _html_table_rows(self): 129 | return ''.join(map(lambda l: l._html_table_rows(), self._parameters)) 130 | 131 | 132 | class Parameter(Domain): 133 | """ 134 | Abstract class representing a parameter (which corresponds to a one-dimensional domain) 135 | This class can be derived for continuous, discrete and categorical parameters 136 | """ 137 | 138 | def __init__(self, label, xinit): 139 | super(Parameter, self).__init__([self]) 140 | self.label = label 141 | self._x = np.atleast_1d(xinit) 142 | 143 | @Domain.size.getter 144 | def size(self): 145 | """ 146 | One parameter has a dimensionality of 1 147 | :return: 1 148 | """ 149 | return 1 150 | 151 | def __iter__(self): 152 | yield self 153 | 154 | @Domain.value.getter 155 | def value(self): 156 | return self._x 157 | 158 | @value.setter 159 | def value(self, x): 160 | x = np.atleast_1d(x) 161 | self._x = x.ravel() 162 | 163 | def _html_table_rows(self): 164 | """ 165 | Html row representation of a Parameter. Should be overwritten in subclasses objects. 166 | """ 167 | return "{0}{1}{2}".format(self.label, 'N/A', 'N/A') 168 | 169 | 170 | class ContinuousParameter(Parameter): 171 | def __init__(self, label, lb, ub, xinit=None): 172 | self._range = np.array([lb, ub], dtype=float) 173 | super(ContinuousParameter, self).__init__(label, xinit or ((ub + lb) / 2.0)) 174 | 175 | @Parameter.lower.getter 176 | def lower(self): 177 | return np.array([self._range[0]]) 178 | 179 | @Parameter.upper.getter 180 | def upper(self): 181 | return np.array([self._range[1]]) 182 | 183 | @lower.setter 184 | def lower(self, value): 185 | self._range[0] = value 186 | 187 | @upper.setter 188 | def upper(self, value): 189 | self._range[1] = value 190 | 191 | def __eq__(self, other): 192 | return isinstance(other, ContinuousParameter) and self.lower == other.lower and self.upper == other.upper 193 | 194 | def _html_table_rows(self): 195 | """ 196 | Html row representation of a ContinuousParameter. 197 | """ 198 | return "{0}{1}{2}".format(self.label, 'Continuous', str(self._range)) 199 | 200 | 201 | class UnitCube(Domain): 202 | """ 203 | The unit domain [0, 1]^d 204 | """ 205 | def __init__(self, n_inputs): 206 | params = [ContinuousParameter('u{0}'.format(i), 0, 1) for i in np.arange(n_inputs)] 207 | super(UnitCube, self).__init__(params) 208 | -------------------------------------------------------------------------------- /gpflowopt/models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from gpflow.param import Parameterized 15 | from gpflow.model import Model 16 | 17 | 18 | class ParentHook(object): 19 | """ 20 | Temporary solution for fixing the recompilation issues (#37, GPflow issue #442). 21 | 22 | An object of this class is returned when highest_parent is called on a model, which holds references to the highest 23 | parentable, as well as the highest model class. When setting the needs recompile flag, this is intercepted and 24 | performed on the model. At the same time, kill autoflow is called on the highest parent. 25 | """ 26 | def __init__(self, highest_parent, highest_model): 27 | self._hp = highest_parent 28 | self._hm = highest_model 29 | 30 | def __getattr__(self, item): 31 | if item is '_needs_recompile': 32 | return getattr(self._hm, item) 33 | return getattr(self._hp, item) 34 | 35 | def __setattr__(self, key, value): 36 | if key in ['_hp', '_hm']: 37 | object.__setattr__(self, key, value) 38 | return 39 | if key is '_needs_recompile': 40 | setattr(self._hm, key, value) 41 | if value: 42 | self._hp._kill_autoflow() 43 | else: 44 | setattr(self._hp, key, value) 45 | 46 | 47 | class ModelWrapper(Parameterized): 48 | """ 49 | Class for fast implementation of a wrapper for models defined in GPflow. 50 | 51 | Once wrapped, all lookups for attributes which are not found in the wrapper class are automatically forwarded 52 | to the wrapped model. To influence the I/O of methods on the wrapped class, simply implement the method in the 53 | wrapper and call the appropriate methods on the wrapped class. Specific logic is included to make sure that if 54 | AutoFlow methods are influenced following this pattern, the original AF storage (if existing) is unaffected and a 55 | new storage is added to the subclass. 56 | """ 57 | 58 | def __init__(self, model): 59 | """ 60 | :param model: model to be wrapped 61 | """ 62 | super(ModelWrapper, self).__init__() 63 | 64 | assert isinstance(model, (Model, ModelWrapper)) 65 | #: Wrapped model 66 | self.wrapped = model 67 | 68 | def __getattr__(self, item): 69 | """ 70 | If an attribute is not found in this class, it is searched in the wrapped model 71 | """ 72 | # Exception for AF storages, if a method with the same name exists in this class, do not find the cache 73 | # in the wrapped model. 74 | if item.endswith('_AF_storage'): 75 | method = item[1:].rstrip('_AF_storage') 76 | if method in dir(self): 77 | raise AttributeError("{0} has no attribute {1}".format(self.__class__.__name__, item)) 78 | 79 | return getattr(self.wrapped, item) 80 | 81 | def __setattr__(self, key, value): 82 | """ 83 | 1) If setting :attr:`wrapped` attribute, point parent to this object (the ModelWrapper). 84 | 2) Setting attributes in the right objects. The following rules are processed in order: 85 | (a) If attribute exists in wrapper, set in wrapper. 86 | (b) If no object has been wrapped (wrapper is None), set attribute in the wrapper. 87 | (c) If attribute is found in the wrapped object, set it there. This rule is ignored for AF storages. 88 | (d) Set attribute in wrapper. 89 | """ 90 | if key is 'wrapped': 91 | object.__setattr__(self, key, value) 92 | value.__setattr__('_parent', self) 93 | return 94 | 95 | try: 96 | # If attribute is in this object, set it. Test by using getattribute instead of hasattr to avoid lookup in 97 | # wrapped object. 98 | self.__getattribute__(key) 99 | super(ModelWrapper, self).__setattr__(key, value) 100 | except AttributeError: 101 | # Attribute is not in wrapper. 102 | # In case no wrapped object is set yet (e.g. constructor), set in wrapper. 103 | if 'wrapped' not in self.__dict__: 104 | super(ModelWrapper, self).__setattr__(key, value) 105 | return 106 | 107 | if hasattr(self, key): 108 | # Now use hasattr, we know getattribute already failed so if it returns true, it must be in the wrapped 109 | # object. Hasattr is called on self instead of self.wrapped to account for the different handling of 110 | # AF storages. 111 | # Prefer setting the attribute in the wrapped object if exists. 112 | setattr(self.wrapped, key, value) 113 | else: 114 | # If not, set in wrapper nonetheless. 115 | super(ModelWrapper, self).__setattr__(key, value) 116 | 117 | def __eq__(self, other): 118 | return self.wrapped == other 119 | 120 | @Parameterized.name.getter 121 | def name(self): 122 | name = super(ModelWrapper, self).name 123 | return ".".join([name, str.lower(self.__class__.__name__)]) 124 | 125 | @Parameterized.highest_parent.getter 126 | def highest_parent(self): 127 | """ 128 | Returns an instance of the ParentHook instead of the usual reference to a Parentable. 129 | """ 130 | original_hp = super(ModelWrapper, self).highest_parent 131 | return original_hp if isinstance(original_hp, ParentHook) else ParentHook(original_hp, self) 132 | -------------------------------------------------------------------------------- /gpflowopt/objective.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import numpy as np 15 | from functools import wraps 16 | from gpflow import model 17 | 18 | 19 | def batch_apply(fun): 20 | """ 21 | Decorator which applies a function along the first dimension of a given data matrix (the batch dimension). 22 | 23 | The most common use case is to convert a function designed to operate on a single input vector, and 24 | to compute its response (and possibly gradient) for each row of a matrix. 25 | 26 | :param fun: function accepting an input vector of size D and returns a vector of size R (number of 27 | outputs) and (optionally) a gradient of size D x R (or size D if R == 1) 28 | :return: a function wrapper which calls fun on each row of a given N* x D matrix. Here N* represents the batch 29 | dimension. The wrapper returns N* x R and optionally a matrix, size N* x D x R matrix (or size N* x D if R == 1) 30 | """ 31 | @wraps(fun) 32 | def batch_wrapper(X): 33 | responses = (fun(x) for x in np.atleast_2d(X)) 34 | sep = tuple(zip(*(r if isinstance(r, tuple) else (r,) for r in responses))) 35 | f = np.vstack(sep[0]) 36 | if len(sep) == 1: 37 | return f 38 | 39 | # for each point, the gradient is either (D,) or (D, R) shaped. 40 | g_stacked = np.stack((r for r in sep[1]), axis=0) # N x D or N x D x R 41 | # Get rid of last dim = 1 in case R = 1 42 | g = np.squeeze(g_stacked, axis=2) if len(g_stacked.shape) == 3 and g_stacked.shape[2] == 1 else g_stacked 43 | return f, g 44 | 45 | return batch_wrapper 46 | 47 | 48 | def to_args(fun): 49 | """ 50 | Decorator for calling an objective function which has each feature as separate input parameter. 51 | 52 | The data matrix is split column wise and passed as arguments. Can be combined with batch apply. 53 | 54 | :param fun: function accepting D N*-dimensional vectors (each representing a feature and returns a a matrix of 55 | size N* x R and optionally a gradient of size N x D x R (or size N x D if R == 1) 56 | :return: a function wrapper which splits a given data matrix into its columns to call fun. 57 | """ 58 | @wraps(fun) 59 | def args_wrapper(X): 60 | X = np.atleast_2d(X) 61 | return fun(*X.T) 62 | 63 | return args_wrapper 64 | 65 | 66 | class to_kwargs(object): 67 | """ 68 | Decorator for calling an objective function which has each feature as separate keyword argument. 69 | 70 | The data matrix is split column wise and passed as keyword arguments. Can be combined with batch apply. 71 | 72 | This decorator is particularly useful for fixing parameters of the optimization domain to fixed values. This can 73 | be achieved by assigning default values to the keyword arguments. By adding/removing a parameter from the 74 | optimization domain, the parameter is included or excluded. 75 | 76 | :param domain: optimization domain, 77 | labels of the parameters are the keyword arguments to calling the objective function. 78 | """ 79 | def __init__(self, domain): 80 | self.labels = [p.label for p in domain] 81 | 82 | def __call__(self, fun): 83 | """ 84 | :param fun: function accepting D N*-dimensional vectors as keyword arguments (each representing a feature, 85 | and returns a a matrix of size N* x R and optionally a gradient of size N* x D x R (or N* x D if R == 1) 86 | :return: a function wrapper which splits a given data matrix into its columns to call fun. 87 | """ 88 | @wraps(fun) 89 | def kwargs_wrapper(X): 90 | X = np.atleast_2d(X) 91 | return fun(**dict(zip(self.labels, X.T))) 92 | 93 | return kwargs_wrapper 94 | 95 | 96 | class ObjectiveWrapper(model.ObjectiveWrapper): 97 | """ 98 | A wrapper for objective functions. 99 | 100 | Filters out gradient information if necessary and keeps a count of the number of function evaluations. 101 | """ 102 | def __init__(self, objective, exclude_gradient): 103 | super(ObjectiveWrapper, self).__init__(objective) 104 | self._no_gradient = exclude_gradient 105 | self.counter = 0 106 | 107 | def __call__(self, x): 108 | x = np.atleast_2d(x) 109 | f, g = super(ObjectiveWrapper, self).__call__(x) 110 | self.counter += x.shape[0] 111 | if self._no_gradient: 112 | return f 113 | return f, g 114 | 115 | -------------------------------------------------------------------------------- /gpflowopt/optim.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import contextlib 16 | import os 17 | import sys 18 | import warnings 19 | 20 | import numpy as np 21 | from gpflow import settings 22 | from scipy.optimize import OptimizeResult, minimize 23 | 24 | from .design import RandomDesign 25 | from .objective import ObjectiveWrapper 26 | 27 | 28 | class Optimizer(object): 29 | """ 30 | An optimization algorithm. 31 | 32 | Starts from an initial (set of) point(s) it performs an optimization over a domain. 33 | May be gradient-based or gradient-free. 34 | """ 35 | 36 | def __init__(self, domain, exclude_gradient=False): 37 | super(Optimizer, self).__init__() 38 | self._domain = domain 39 | self._initial = domain.value 40 | self._wrapper_args = dict(exclude_gradient=exclude_gradient) 41 | 42 | @property 43 | def domain(self): 44 | """ 45 | The current domain the optimizer operates on. 46 | 47 | :return: :class:'~.domain.Domain` object 48 | """ 49 | return self._domain 50 | 51 | @domain.setter 52 | def domain(self, dom): 53 | """ 54 | Sets a new domain for the optimizer. 55 | 56 | Resets the initial points to the middle of the domain. 57 | 58 | :param dom: new :class:'~.domain.Domain` 59 | """ 60 | self._domain = dom 61 | self.set_initial(dom.value) 62 | 63 | def optimize(self, objectivefx, **kwargs): 64 | """ 65 | Optimize a given function f over a domain. 66 | 67 | The optimizer class supports interruption. If during the optimization ctrl+c is pressed, the last best point is 68 | returned. 69 | 70 | The actual optimization routine is implemented in _optimize, to be implemented in subclasses. 71 | 72 | :param objectivefx: callable, taking one argument: a 2D numpy array. The number of columns correspond to the 73 | dimensionality of the input domain. 74 | :return: OptimizeResult reporting the results. 75 | """ 76 | objective = ObjectiveWrapper(objectivefx, **self._wrapper_args) 77 | try: 78 | result = self._optimize(objective, **kwargs) 79 | except KeyboardInterrupt: 80 | result = OptimizeResult(x=objective._previous_x, 81 | success=False, 82 | message="Caught KeyboardInterrupt, returning last good value.") 83 | result.x = np.atleast_2d(result.x) 84 | result.nfev = objective.counter 85 | return result 86 | 87 | def get_initial(self): 88 | """ 89 | Return the initial set of points. 90 | 91 | :return: initial set of points, size N x D 92 | """ 93 | return self._initial 94 | 95 | def set_initial(self, initial): 96 | """ 97 | Set the initial set of points. 98 | 99 | The dimensionality should match the domain dimensionality, and all points should 100 | be within the domain. 101 | 102 | :param initial: initial points, should all be within the domain of the optimizer. 103 | """ 104 | initial = np.atleast_2d(initial) 105 | assert (initial in self.domain) 106 | self._initial = initial 107 | 108 | def gradient_enabled(self): 109 | """ 110 | Returns if the optimizer is a gradient-based algorithm or not. 111 | """ 112 | return not self._wrapper_args['exclude_gradient'] 113 | 114 | @contextlib.contextmanager 115 | def silent(self): 116 | """ 117 | Context for performing actions on an optimizer (such as optimize) with all stdout discarded. 118 | Usage example: 119 | 120 | >>> opt = BayesianOptimizer(domain, acquisition, optimizer) 121 | >>> with opt.silent(): 122 | >>> # Run without printing anything 123 | >>> opt.optimize(fx, n_iter=2) 124 | """ 125 | save_stdout = sys.stdout 126 | sys.stdout = open(os.devnull, 'w') 127 | yield 128 | sys.stdout = save_stdout 129 | 130 | 131 | class MCOptimizer(Optimizer): 132 | """ 133 | Optimization of an objective function by evaluating a set of random points. 134 | 135 | Note: each call to optimize, a different set of random points is evaluated. 136 | """ 137 | 138 | def __init__(self, domain, nsamples): 139 | """ 140 | :param domain: Optimization :class:`~.domain.Domain`. 141 | :param nsamples: number of random points to use 142 | """ 143 | super(MCOptimizer, self).__init__(domain, exclude_gradient=True) 144 | self._nsamples = nsamples 145 | # Clear the initial data points 146 | self.set_initial(np.empty((0, self.domain.size))) 147 | 148 | @Optimizer.domain.setter 149 | def domain(self, dom): 150 | self._domain = dom 151 | 152 | def _get_eval_points(self): 153 | return RandomDesign(self._nsamples, self.domain).generate() 154 | 155 | def _optimize(self, objective): 156 | points = self._get_eval_points() 157 | evaluations = objective(points) 158 | idx_best = np.argmin(evaluations, axis=0) 159 | 160 | return OptimizeResult(x=points[idx_best, :], 161 | success=True, 162 | fun=evaluations[idx_best, :], 163 | nfev=points.shape[0], 164 | message="OK") 165 | 166 | def set_initial(self, initial): 167 | initial = np.atleast_2d(initial) 168 | if initial.size > 0: 169 | warnings.warn("Initial points set in {0} are ignored.".format(self.__class__.__name__), UserWarning) 170 | return 171 | 172 | super(MCOptimizer, self).set_initial(initial) 173 | 174 | 175 | class CandidateOptimizer(MCOptimizer): 176 | """ 177 | Optimization of an objective function by evaluating a set of pre-defined candidate points. 178 | 179 | Returns the point with minimal objective value. 180 | """ 181 | 182 | def __init__(self, domain, candidates): 183 | """ 184 | :param domain: Optimization :class:`~.domain.Domain`. 185 | :param candidates: candidate points, should be within the optimization domain. 186 | """ 187 | super(CandidateOptimizer, self).__init__(domain, candidates.shape[0]) 188 | assert (candidates in domain) 189 | self.candidates = candidates 190 | 191 | def _get_eval_points(self): 192 | return self.candidates 193 | 194 | @MCOptimizer.domain.setter 195 | def domain(self, dom): 196 | t = self.domain >> dom 197 | super(CandidateOptimizer, self.__class__).domain.fset(self, dom) 198 | self.candidates = t.forward(self.candidates) 199 | 200 | 201 | class SciPyOptimizer(Optimizer): 202 | """ 203 | Wraps SciPy's minimize function. 204 | """ 205 | 206 | def __init__(self, domain, method='L-BFGS-B', tol=None, maxiter=1000): 207 | super(SciPyOptimizer, self).__init__(domain) 208 | options = dict(disp=settings.verbosity.optimisation_verb, 209 | maxiter=maxiter) 210 | self.config = dict(tol=tol, 211 | method=method, 212 | options=options) 213 | 214 | def _optimize(self, objective): 215 | """ 216 | Calls scipy.optimize.minimize. 217 | """ 218 | objective1d = lambda X: tuple(map(lambda arr: arr.ravel(), objective(X))) 219 | result = minimize(fun=objective1d, 220 | x0=self.get_initial(), 221 | jac=self.gradient_enabled(), 222 | bounds=list(zip(self.domain.lower, self.domain.upper)), 223 | **self.config) 224 | return result 225 | 226 | 227 | class StagedOptimizer(Optimizer): 228 | """ 229 | An optimization pipeline of multiple optimizers called in succession. 230 | 231 | A list of optimizers can be specified (all on the same domain). The optimal 232 | solution of the an optimizer is used as an initial point for the next optimizer. 233 | """ 234 | 235 | def __init__(self, optimizers): 236 | assert all(map(lambda opt: optimizers[0].domain == opt.domain, optimizers)) 237 | no_gradient = any(map(lambda opt: not opt.gradient_enabled(), optimizers)) 238 | super(StagedOptimizer, self).__init__(optimizers[0].domain, exclude_gradient=no_gradient) 239 | self.optimizers = optimizers 240 | del self._initial 241 | 242 | @Optimizer.domain.setter 243 | def domain(self, domain): 244 | self._domain = domain 245 | for optimizer in self.optimizers: 246 | optimizer.domain = domain 247 | 248 | def _best_x(self, results): 249 | best_idx = np.argmin([r.fun for r in results if r.success]) 250 | return results[best_idx].x, results[best_idx].fun 251 | 252 | def optimize(self, objectivefx): 253 | """ 254 | The StagedOptimizer overwrites the default behaviour of optimize(). It passes the best point of the previous 255 | stage to the next stage. If the optimization is interrupted or fails, this process stops and the OptimizeResult 256 | is returned. 257 | """ 258 | 259 | results = [] 260 | for current, following in zip(self.optimizers[:-1], self.optimizers[1:]): 261 | result = current.optimize(objectivefx) 262 | results.append(result) 263 | if not result.success: 264 | result.message += " StagedOptimizer interrupted after {0}.".format(current.__class__.__name__) 265 | break 266 | following.set_initial(self._best_x(results)[0]) 267 | 268 | if result.success: 269 | result = self.optimizers[-1].optimize(objectivefx) 270 | results.append(result) 271 | 272 | result.nfev = sum(r.nfev for r in results) 273 | result.nstages = len(results) 274 | if any(r.success for r in results): 275 | result.x, result.fun = self._best_x(results) 276 | return result 277 | 278 | def get_initial(self): 279 | return self.optimizers[0].get_initial() 280 | 281 | def set_initial(self, initial): 282 | self.optimizers[0].set_initial(initial) 283 | -------------------------------------------------------------------------------- /gpflowopt/pareto.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten, Ivo Couckuyt 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from gpflow.param import Parameterized, DataHolder, AutoFlow 16 | from gpflow import settings 17 | from scipy.spatial.distance import pdist, squareform 18 | import numpy as np 19 | import tensorflow as tf 20 | 21 | np_int_type = np_float_type = np.int32 if settings.dtypes.int_type is tf.int32 else np.int64 22 | float_type = settings.dtypes.float_type 23 | stability = settings.numerics.jitter_level 24 | 25 | 26 | class BoundedVolumes(Parameterized): 27 | 28 | @classmethod 29 | def empty(cls, dim, dtype): 30 | """ 31 | Returns an empty bounded volume (hypercube). 32 | 33 | :param dim: dimension of the volume 34 | :param dtype: dtype of the coordinates 35 | :return: an empty :class:`.BoundedVolumes` 36 | """ 37 | setup_arr = np.zeros((0, dim), dtype=dtype) 38 | return cls(setup_arr.copy(), setup_arr.copy()) 39 | 40 | def __init__(self, lb, ub): 41 | """ 42 | Construct bounded volumes. 43 | 44 | :param lb: the lowerbounds of the volumes 45 | :param ub: the upperbounds of the volumes 46 | """ 47 | super(BoundedVolumes, self).__init__() 48 | assert np.all(lb.shape == ub.shape) 49 | self.lb = DataHolder(np.atleast_2d(lb), 'pass') 50 | self.ub = DataHolder(np.atleast_2d(ub), 'pass') 51 | 52 | def append(self, lb, ub): 53 | """ 54 | Add new bounded volumes. 55 | 56 | :param lb: the lowerbounds of the volumes 57 | :param ub: the upperbounds of the volumes 58 | """ 59 | self.lb = np.vstack((self.lb.value, lb)) 60 | self.ub = np.vstack((self.ub.value, ub)) 61 | 62 | def clear(self): 63 | """ 64 | Clears all stored bounded volumes 65 | """ 66 | dtype = self.lb.value.dtype 67 | outdim = self.lb.shape[1] 68 | self.lb = np.zeros((0, outdim), dtype=dtype) 69 | self.ub = np.zeros((0, outdim), dtype=dtype) 70 | 71 | def size(self): 72 | """ 73 | :return: volume of each bounded volume 74 | """ 75 | return np.prod(self.ub.value - self.lb.value, axis=1) 76 | 77 | 78 | def non_dominated_sort(objectives): 79 | """ 80 | Computes the non-dominated set for a set of data points 81 | 82 | :param objectives: data points 83 | :return: tuple of the non-dominated set and the degree of dominance, 84 | dominances gives the number of dominating points for each data point 85 | """ 86 | extended = np.tile(objectives, (objectives.shape[0], 1, 1)) 87 | dominance = np.sum(np.logical_and(np.all(extended <= np.swapaxes(extended, 0, 1), axis=2), 88 | np.any(extended < np.swapaxes(extended, 0, 1), axis=2)), axis=1) 89 | 90 | return objectives[dominance == 0], dominance 91 | 92 | 93 | class Pareto(Parameterized): 94 | def __init__(self, Y, threshold=0): 95 | """ 96 | Construct a Pareto set. 97 | 98 | Stores a Pareto set and calculates the cell bounds covering the non-dominated region. 99 | The latter is needed for certain multiobjective acquisition functions. 100 | E.g., the :class:`~.acquisition.HVProbabilityOfImprovement`. 101 | 102 | :param Y: output data points, size N x R 103 | :param threshold: approximation threshold for the generic divide and conquer strategy 104 | (default 0: exact calculation) 105 | """ 106 | super(Pareto, self).__init__() 107 | self.threshold = threshold 108 | self.Y = Y 109 | 110 | # Setup data structures 111 | self.bounds = BoundedVolumes.empty(Y.shape[1], np_int_type) 112 | self.front = DataHolder(np.zeros((0, Y.shape[1])), 'pass') 113 | 114 | # Initialize 115 | self.update() 116 | 117 | @staticmethod 118 | def _is_test_required(smaller): 119 | """ 120 | Tests if a point augments or dominates the Pareto set. 121 | 122 | :param smaller: a boolean ndarray storing test point < Pareto front 123 | :return: True if the test point dominates or augments the Pareto front (boolean) 124 | """ 125 | # if and only if the test point is at least in one dimension smaller for every point in the Pareto set 126 | idx_dom_augm = np.any(smaller, axis=1) 127 | is_dom_augm = np.all(idx_dom_augm) 128 | 129 | return is_dom_augm 130 | 131 | def _update_front(self): 132 | """ 133 | Calculate the non-dominated set of points based on the latest data. 134 | 135 | The stored Pareto set is sorted on the first objective in ascending order. 136 | 137 | :return: boolean, whether the Pareto set has actually changed since the last iteration 138 | """ 139 | current = self.front.value 140 | pf, _ = non_dominated_sort(self.Y) 141 | 142 | self.front = pf[pf[:, 0].argsort(), :] 143 | 144 | return not np.array_equal(current, self.front.value) 145 | 146 | def update(self, Y=None, generic_strategy=False): 147 | """ 148 | Update with new output data. 149 | 150 | Computes the Pareto set and if it has changed recalculates the cell bounds covering the non-dominated region. 151 | For the latter, a direct algorithm is used for two objectives, otherwise a 152 | generic divide and conquer strategy is employed. 153 | 154 | :param Y: output data points 155 | :param generic_strategy: Force the generic divide and conquer strategy regardless of the number of objectives 156 | (default False) 157 | """ 158 | self.Y = Y if Y is not None else self.Y 159 | 160 | # Find (new) set of non-dominated points 161 | changed = self._update_front() 162 | 163 | # Recompute cell bounds if required 164 | # Note: if the Pareto set is based on model predictions it will almost always change in between optimizations 165 | if changed: 166 | # Clear data container 167 | self.bounds.clear() 168 | if generic_strategy: 169 | self.divide_conquer_nd() 170 | else: 171 | self.bounds_2d() if self.Y.shape[1] == 2 else self.divide_conquer_nd() 172 | 173 | def divide_conquer_nd(self): 174 | """ 175 | Divide and conquer strategy to compute the cells covering the non-dominated region. 176 | 177 | Generic version: works for an arbitrary number of objectives. 178 | """ 179 | outdim = self.Y.shape[1] 180 | 181 | # The divide and conquer algorithm operates on a pseudo Pareto set 182 | # that is a mapping of the real Pareto set to discrete values 183 | pseudo_pf = np.argsort(self.front.value, axis=0) + 1 # +1 as index zero is reserved for the ideal point 184 | 185 | # Extend front with the ideal and anti-ideal point 186 | min_pf = np.min(self.front.value, axis=0) - 1 187 | max_pf = np.max(self.front.value, axis=0) + 1 188 | 189 | pf_ext = np.vstack((min_pf, self.front.value, max_pf)) # Needed for early stopping check (threshold) 190 | pf_ext_idx = np.vstack((np.zeros(outdim, dtype=np_int_type), 191 | pseudo_pf, 192 | np.ones(outdim, dtype=np_int_type) * self.front.shape[0] + 1)) 193 | 194 | # Start with one cell covering the whole front 195 | dc = [(np.zeros(outdim, dtype=np_int_type), 196 | (int(pf_ext_idx.shape[0]) - 1) * np.ones(outdim, dtype=np_int_type))] 197 | 198 | total_size = np.prod(max_pf - min_pf) 199 | 200 | # Start divide and conquer until we processed all cells 201 | while dc: 202 | # Process test cell 203 | cell = dc.pop() 204 | 205 | arr = np.arange(outdim) 206 | lb = pf_ext[pf_ext_idx[cell[0], arr], arr] 207 | ub = pf_ext[pf_ext_idx[cell[1], arr], arr] 208 | 209 | # Acceptance test: 210 | if self._is_test_required((ub - stability) < self.front.value): 211 | # Cell is a valid integral bound: store 212 | self.bounds.append(pf_ext_idx[cell[0], np.arange(outdim)], 213 | pf_ext_idx[cell[1], np.arange(outdim)]) 214 | # Reject test: 215 | elif self._is_test_required((lb + stability) < self.front.value): 216 | # Cell can not be discarded: calculate the size of the cell 217 | dc_dist = cell[1] - cell[0] 218 | hc = BoundedVolumes(pf_ext[pf_ext_idx[cell[0], np.arange(outdim)], np.arange(outdim)], 219 | pf_ext[pf_ext_idx[cell[1], np.arange(outdim)], np.arange(outdim)]) 220 | 221 | # Only divide when it is not an unit cell and the volume is above the approx. threshold 222 | if np.any(dc_dist > 1) and np.all((hc.size()[0] / total_size) > self.threshold): 223 | # Divide the test cell over its largest dimension 224 | edge_size, idx = np.max(dc_dist), np.argmax(dc_dist) 225 | edge_size1 = int(np.round(edge_size / 2.0)) 226 | edge_size2 = edge_size - edge_size1 227 | 228 | # Store divided cells 229 | ub = np.copy(cell[1]) 230 | ub[idx] -= edge_size1 231 | dc.append((np.copy(cell[0]), ub)) 232 | 233 | lb = np.copy(cell[0]) 234 | lb[idx] += edge_size2 235 | dc.append((lb, np.copy(cell[1]))) 236 | # else: cell can be discarded 237 | 238 | def bounds_2d(self): 239 | """ 240 | Computes the cells covering the non-dominated region for the specific case of only two objectives. 241 | 242 | Assumes the Pareto set has been sorted in ascending order on the first objective. 243 | This implies the second objective is sorted in descending order. 244 | """ 245 | outdim = self.Y.shape[1] 246 | assert outdim == 2 247 | 248 | pf_idx = np.argsort(self.front.value, axis=0) 249 | pf_ext_idx = np.vstack((np.zeros(outdim, dtype=np_int_type), 250 | pf_idx + 1, 251 | np.ones(outdim, dtype=np_int_type) * self.front.shape[0] + 1)) 252 | 253 | for i in range(pf_ext_idx[-1, 0]): 254 | self.bounds.append((i, 0), 255 | (i+1, pf_ext_idx[-i-1, 1])) 256 | 257 | @AutoFlow((float_type, [None])) 258 | def hypervolume(self, reference): 259 | """ 260 | Autoflow method to calculate the hypervolume indicator 261 | 262 | The hypervolume indicator is the volume of the dominated region. 263 | 264 | :param reference: reference point to use 265 | Should be equal or bigger than the anti-ideal point of the Pareto set 266 | For comparing results across runs the same reference point must be used 267 | :return: hypervolume indicator (the higher the better) 268 | """ 269 | 270 | min_pf = tf.reduce_min(self.front, 0, keep_dims=True) 271 | R = tf.expand_dims(reference, 0) 272 | pseudo_pf = tf.concat((min_pf, self.front, R), 0) 273 | D = tf.shape(pseudo_pf)[1] 274 | N = tf.shape(self.bounds.ub)[0] 275 | 276 | idx = tf.tile(tf.expand_dims(tf.range(D), -1),[1, N]) 277 | ub_idx = tf.reshape(tf.stack([tf.transpose(self.bounds.ub), idx], axis=2), [N * D, 2]) 278 | lb_idx = tf.reshape(tf.stack([tf.transpose(self.bounds.lb), idx], axis=2), [N * D, 2]) 279 | ub = tf.reshape(tf.gather_nd(pseudo_pf, ub_idx), [D, N]) 280 | lb = tf.reshape(tf.gather_nd(pseudo_pf, lb_idx), [D, N]) 281 | hv = tf.reduce_sum(tf.reduce_prod(ub - lb, 0)) 282 | return tf.reduce_prod(R - min_pf) - hv 283 | -------------------------------------------------------------------------------- /gpflowopt/scaling.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from gpflow.param import DataHolder, AutoFlow 16 | from gpflow import settings 17 | import numpy as np 18 | from .transforms import LinearTransform, DataTransform 19 | from .domain import UnitCube 20 | from .models import ModelWrapper 21 | 22 | float_type = settings.dtypes.float_type 23 | 24 | 25 | class DataScaler(ModelWrapper): 26 | """ 27 | Model-wrapping class, primarily intended to assure the data in GPflow models is scaled. 28 | 29 | One DataScaler wraps one GPflow model, and can scale the input as well as the output data. By default, 30 | if any kind of object attribute is not found in the datascaler object, it is searched on the wrapped model. 31 | 32 | The datascaler supports both input as well as output scaling, although both scalings are set up differently: 33 | 34 | - For input, the transform is not automatically generated. By default, the input transform is the identity 35 | transform. The input transform can be set through the setter property, or by specifying a domain in the 36 | constructor. For the latter, the input transform will be initialized as the transform from the specified domain to 37 | a unit cube. When X is updated, the transform does not change. 38 | 39 | - If enabled: for output the data is always scaled to zero mean and unit variance. This means that if the Y property 40 | is set, the output transform is first calculated, then the data is scaled. 41 | 42 | 43 | By default, :class:`~.acquisition.Acquisition` objects will always wrap each model received. However, the input and output transforms 44 | will be the identity transforms, and output normalization is switched off. It is up to the user (or 45 | specialized classes such as the BayesianOptimizer) to correctly configure the datascalers involved. 46 | 47 | By carrying out the scaling at such a deep level in the framework, it is possible to keep the scaling 48 | hidden throughout the rest of GPflowOpt. This means that, during implementation of acquisition functions it is safe 49 | to assume the data is not scaled, and is within the configured optimization domain. There is only one exception: 50 | the hyperparameters are determined on the scaled data, and are NOT automatically unscaled by this class because the 51 | datascaler does not know what model is wrapped and what kernels are used. Should hyperparameters of the model be 52 | required, it is the responsibility of the implementation to rescale the hyperparameters. Additionally, applying 53 | hyperpriors should anticipate for the scaled data. 54 | """ 55 | 56 | def __init__(self, model, domain=None, normalize_Y=False): 57 | """ 58 | :param model: model to be wrapped 59 | :param domain: (default: None) if supplied, the input transform is configured from the supplied domain to 60 | :class:`.UnitCube`. If None, the input transform defaults to the identity transform. 61 | :param normalize_Y: (default: False) enable automatic scaling of output values to zero mean and unit 62 | variance. 63 | """ 64 | # model sanity checks, slightly stronger conditions than the wrapper 65 | super(DataScaler, self).__init__(model) 66 | 67 | # Initial configuration of the datascaler 68 | n_inputs = model.X.shape[1] 69 | n_outputs = model.Y.shape[1] 70 | self._input_transform = (domain or UnitCube(n_inputs)) >> UnitCube(n_inputs) 71 | self._normalize_Y = normalize_Y 72 | self._output_transform = LinearTransform(np.ones(n_outputs), np.zeros(n_outputs)) 73 | 74 | self.X = model.X.value 75 | self.Y = model.Y.value 76 | 77 | @property 78 | def input_transform(self): 79 | """ 80 | Get the current input transform 81 | 82 | :return: :class:`.DataTransform` input transform object 83 | """ 84 | return self._input_transform 85 | 86 | @input_transform.setter 87 | def input_transform(self, t): 88 | """ 89 | Configure a new input transform. 90 | 91 | Data in the wrapped model is automatically updated with the new transform. 92 | 93 | :param t: :class:`.DataTransform` object: the new input transform. 94 | """ 95 | assert isinstance(t, DataTransform) 96 | X = self.X.value # unscales the data 97 | self._input_transform.assign(t) 98 | self.X = X # scales the back using the new input transform 99 | 100 | @property 101 | def output_transform(self): 102 | """ 103 | Get the current output transform 104 | 105 | :return: :class:`.DataTransform` output transform object 106 | """ 107 | return self._output_transform 108 | 109 | @output_transform.setter 110 | def output_transform(self, t): 111 | """ 112 | Configure a new output transform. Data in the model is automatically updated with the new transform. 113 | 114 | :param t: :class:`.DataTransform` object: the new output transform. 115 | """ 116 | assert isinstance(t, DataTransform) 117 | Y = self.Y.value 118 | self._output_transform.assign(t) 119 | self.Y = Y 120 | 121 | @property 122 | def normalize_output(self): 123 | """ 124 | :return: boolean, indicating if output is automatically scaled to zero mean and unit variance. 125 | """ 126 | return self._normalize_Y 127 | 128 | @normalize_output.setter 129 | def normalize_output(self, flag): 130 | """ 131 | Enable/disable automated output scaling. If switched off, the output transform becomes the identity transform. 132 | If enabled, data will be automatically scaled to zero mean and unit variance. When the output normalization is 133 | switched on or off, the data in the model is automatically adapted. 134 | 135 | :param flag: boolean, turn output scaling on or off 136 | """ 137 | 138 | self._normalize_Y = flag 139 | if not flag: 140 | # Output normalization turned off. Reset transform to identity 141 | self.output_transform = LinearTransform(np.ones(self.Y.value.shape[1]), np.zeros(self.Y.value.shape[1])) 142 | else: 143 | # Output normalization enabled. Trigger scaling. 144 | self.Y = self.Y.value 145 | 146 | # Methods overwriting methods of the wrapped model. 147 | @property 148 | def X(self): 149 | """ 150 | Returns the input data of the model, unscaled. 151 | 152 | :return: :class:`.DataHolder`: unscaled input data 153 | """ 154 | return DataHolder(self.input_transform.backward(self.wrapped.X.value)) 155 | 156 | @property 157 | def Y(self): 158 | """ 159 | Returns the output data of the wrapped model, unscaled. 160 | 161 | :return: :class:`.DataHolder`: unscaled output data 162 | """ 163 | return DataHolder(self.output_transform.backward(self.wrapped.Y.value)) 164 | 165 | @X.setter 166 | def X(self, x): 167 | """ 168 | Set the input data. Applies the input transform before setting the data of the wrapped model. 169 | """ 170 | self.wrapped.X = self.input_transform.forward(x.value if isinstance(x, DataHolder) else x) 171 | 172 | @Y.setter 173 | def Y(self, y): 174 | """ 175 | Set the output data. In case normalize_Y=True, the appropriate output transform is updated. It is then 176 | applied on the data before setting the data of the wrapped model. 177 | """ 178 | value = y.value if isinstance(y, DataHolder) else y 179 | if self.normalize_output: 180 | self.output_transform.assign(~LinearTransform(value.std(axis=0), value.mean(axis=0))) 181 | self.wrapped.Y = self.output_transform.forward(value) 182 | 183 | def build_predict(self, Xnew, full_cov=False): 184 | """ 185 | build_predict builds the TensorFlow graph for prediction. Similar to the method in the wrapped model, however 186 | the input points are transformed using the input transform. The returned mean and variance are transformed 187 | backward using the output transform. 188 | """ 189 | f, var = self.wrapped.build_predict(self.input_transform.build_forward(Xnew), full_cov=full_cov) 190 | return self.output_transform.build_backward(f), self.output_transform.build_backward_variance(var) 191 | 192 | @AutoFlow((float_type, [None, None])) 193 | def predict_f(self, Xnew): 194 | """ 195 | Compute the mean and variance of held-out data at the points Xnew 196 | """ 197 | return self.build_predict(Xnew) 198 | 199 | @AutoFlow((float_type, [None, None])) 200 | def predict_f_full_cov(self, Xnew): 201 | """ 202 | Compute the mean and variance of held-out data at the points Xnew 203 | """ 204 | return self.build_predict(Xnew, full_cov=True) 205 | 206 | @AutoFlow((float_type, [None, None])) 207 | def predict_y(self, Xnew): 208 | """ 209 | Compute the mean and variance of held-out data at the points Xnew 210 | """ 211 | f, var = self.wrapped.build_predict(self.input_transform.build_forward(Xnew)) 212 | f, var = self.likelihood.predict_mean_and_var(f, var) 213 | return self.output_transform.build_backward(f), self.output_transform.build_backward_variance(var) 214 | 215 | @AutoFlow((float_type, [None, None]), (float_type, [None, None])) 216 | def predict_density(self, Xnew, Ynew): 217 | """ 218 | Compute the (log) density of the data Ynew at the points Xnew 219 | """ 220 | mu, var = self.wrapped.build_predict(self.input_transform.build_forward(Xnew)) 221 | Ys = self.output_transform.build_forward(Ynew) 222 | return self.likelihood.predict_density(mu, var, Ys) 223 | -------------------------------------------------------------------------------- /gpflowopt/transforms.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from gpflow import settings 17 | from gpflow.param import Parameterized, DataHolder, AutoFlow 18 | import numpy as np 19 | import tensorflow as tf 20 | 21 | float_type = settings.dtypes.float_type 22 | 23 | 24 | class DataTransform(Parameterized): 25 | """ 26 | Maps data in :class:`.Domain` U to :class:`.Domain` V. 27 | 28 | Useful for scaling of data between domains. 29 | """ 30 | 31 | @AutoFlow((float_type, [None, None])) 32 | def forward(self, X): 33 | """ 34 | Performs the transformation of U -> V 35 | """ 36 | return self.build_forward(X) 37 | 38 | def build_forward(self, X): 39 | """ 40 | Tensorflow graph for the transformation of U -> V 41 | 42 | :param X: N x P tensor 43 | :return: N x Q tensor 44 | """ 45 | raise NotImplementedError 46 | 47 | def backward(self, Y): 48 | """ 49 | Performs the transformation of V -> U. By default, calls the :meth:`.forward` transform on the inverted 50 | transform object which requires implementation of __invert__. The method can be overwritten in subclasses if a 51 | more efficient (direct) transformation is possible. 52 | 53 | :param Y: N x Q matrix 54 | :return: N x P matrix 55 | """ 56 | return (~self).forward(Y) 57 | 58 | def assign(self, other): 59 | raise NotImplementedError 60 | 61 | def __invert__(self): 62 | """ 63 | Return a :class:`.DataTransform` object implementing the reverse transform V -> U 64 | """ 65 | raise NotImplementedError 66 | 67 | 68 | class LinearTransform(DataTransform): 69 | """ 70 | A simple linear transform of the form 71 | 72 | .. math:: 73 | \\mathbf Y = (\\mathbf A \\mathbf X^{T})^{T} + \\mathbf b \\otimes \\mathbf 1_{N}^{T} 74 | 75 | """ 76 | 77 | def __init__(self, A, b): 78 | """ 79 | :param A: scaling matrix. Either a P-dimensional vector, or a P x P transformation matrix. For the latter, 80 | the inverse and backward methods are not guaranteed to work as A must be invertible. 81 | 82 | It is also possible to specify a matrix with size P x Q with Q != P to achieve 83 | a lower dimensional representation of X. 84 | In this case, A is not invertible, hence inverse and backward transforms are not supported. 85 | :param b: A P-dimensional offset vector. 86 | """ 87 | super(LinearTransform, self).__init__() 88 | assert A is not None 89 | assert b is not None 90 | 91 | b = np.atleast_1d(b) 92 | A = np.atleast_1d(A) 93 | if len(A.shape) == 1: 94 | A = np.diag(A) 95 | 96 | assert (len(b.shape) == 1) 97 | assert (len(A.shape) == 2) 98 | 99 | self.A = DataHolder(A) 100 | self.b = DataHolder(b) 101 | 102 | def build_forward(self, X): 103 | return tf.matmul(X, tf.transpose(self.A)) + self.b 104 | 105 | @AutoFlow((float_type, [None, None])) 106 | def backward(self, Y): 107 | """ 108 | Overwrites the default backward approach, to avoid an explicit matrix inversion. 109 | """ 110 | return self.build_backward(Y) 111 | 112 | def build_backward(self, Y): 113 | """ 114 | TensorFlow implementation of the inverse mapping 115 | """ 116 | L = tf.cholesky(tf.transpose(self.A)) 117 | XT = tf.cholesky_solve(L, tf.transpose(Y-self.b)) 118 | return tf.transpose(XT) 119 | 120 | def build_backward_variance(self, Yvar): 121 | """ 122 | Additional method for scaling variance backward (used in :class:`.Normalizer`). Can process both the diagonal 123 | variances returned by predict_f, as well as full covariance matrices. 124 | 125 | :param Yvar: size N x N x P or size N x P 126 | :return: Yvar scaled, same rank and size as input 127 | """ 128 | rank = tf.rank(Yvar) 129 | # Because TensorFlow evaluates both fn1 and fn2, the transpose can't be in the same line. If a full cov 130 | # matrix is provided fn1 turns it into a rank 4, then tries to transpose it as a rank 3. 131 | # Splitting it in two steps however works fine. 132 | Yvar = tf.cond(tf.equal(rank, 2), lambda: tf.matrix_diag(tf.transpose(Yvar)), lambda: Yvar) 133 | Yvar = tf.cond(tf.equal(rank, 2), lambda: tf.transpose(Yvar, perm=[1, 2, 0]), lambda: Yvar) 134 | 135 | N = tf.shape(Yvar)[0] 136 | D = tf.shape(Yvar)[2] 137 | L = tf.cholesky(tf.square(tf.transpose(self.A))) 138 | Yvar = tf.reshape(Yvar, [N * N, D]) 139 | scaled_var = tf.reshape(tf.transpose(tf.cholesky_solve(L, tf.transpose(Yvar))), [N, N, D]) 140 | return tf.cond(tf.equal(rank, 2), lambda: tf.reduce_sum(scaled_var, axis=1), lambda: scaled_var) 141 | 142 | def assign(self, other): 143 | """ 144 | Assign the parameters of another :class:`LinearTransform`. 145 | 146 | Useful to avoid graph re-compilation. 147 | 148 | :param other: :class:`.LinearTransform` object 149 | """ 150 | assert other is not None 151 | assert isinstance(other, LinearTransform) 152 | self.A.set_data(other.A.value) 153 | self.b.set_data(other.b.value) 154 | 155 | def __invert__(self): 156 | A_inv = np.linalg.inv(self.A.value.T) 157 | return LinearTransform(A_inv, -np.dot(self.b.value, A_inv)) 158 | 159 | -------------------------------------------------------------------------------- /nox.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Joachim van der Herten 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import nox 16 | 17 | TEST_DEPS = ('six',) 18 | SYSTEM_TEST_DEPS = ('nbconvert', 'nbformat', 'jupyter', 'jupyter_client', 'matplotlib') 19 | 20 | 21 | @nox.session 22 | def unit(session): 23 | session.install('pytest', 'pytest-cov', *TEST_DEPS) 24 | session.install('-e', '.', '--process-dependency-links') 25 | 26 | # Run py.test against the unit tests. 27 | session.run( 28 | 'py.test', 29 | '--cov-report=', 30 | '--cov-append', 31 | '--cov=gpflowopt', 32 | '--color=yes', 33 | '--cov-config=.coveragerc', 34 | 'testing/unit' 35 | ) 36 | 37 | 38 | @nox.session 39 | def system(session): 40 | session.install('pytest', 'pytest-cov', *(TEST_DEPS + SYSTEM_TEST_DEPS)) 41 | session.install('-e', '.', '--process-dependency-links') 42 | 43 | # Run py.test against the unit tests. 44 | session.run( 45 | 'py.test', 46 | '--cov-report=', 47 | '--cov-append', 48 | '--cov=gpflowopt', 49 | '--cov-config=.coveragerc', 50 | 'testing/system' 51 | ) 52 | 53 | 54 | @nox.session 55 | def cover(session): 56 | session.install('coverage', 'pytest-cov') 57 | session.run('coverage', 'report') 58 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2017 Joachim van der Herten 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import re 19 | 20 | from pkg_resources import parse_version 21 | from setuptools import setup 22 | 23 | VERSIONFILE = "gpflowopt/_version.py" 24 | verstrline = open(VERSIONFILE, "rt").read() 25 | VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" 26 | mo = re.search(VSRE, verstrline, re.M) 27 | if mo: 28 | verstr = mo.group(1) 29 | else: 30 | raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) 31 | 32 | # Dependencies of GPflowOpt 33 | dependencies = ['numpy>=1.9', 'scipy>=0.16', 'GPflow==0.5.0'] 34 | min_tf_version = '1.0.0' 35 | 36 | # Detect if TF is installed or outdated. 37 | # If the right version is installed, do not list as requirement to avoid installing over e.g. tensorflow-gpu 38 | # To avoid this, rely on importing rather than the package name (like pip). 39 | try: 40 | # If tf not installed, import raises ImportError 41 | import tensorflow as tf 42 | 43 | if parse_version(tf.__version__) < parse_version(min_tf_version): 44 | # TF pre-installed, but below the minimum required version 45 | raise DeprecationWarning("TensorFlow version below minimum requirement") 46 | except (ImportError, DeprecationWarning) as e: 47 | # Add TensorFlow to dependencies to trigger installation/update 48 | dependencies.append('tensorflow>={0}'.format(min_tf_version)) 49 | 50 | setup(name='gpflowopt', 51 | version=verstr, 52 | author="Joachim van der Herten, Ivo Couckuyt", 53 | author_email="joachim.vanderherten@ugent.be", 54 | description=("Bayesian Optimization with GPflow"), 55 | license="Apache License 2.0", 56 | keywords="machine-learning bayesian-optimization tensorflow", 57 | url="http://github.com/gpflow/gpflowopt", 58 | package_data={}, 59 | include_package_data=True, 60 | ext_modules=[], 61 | packages=["gpflowopt", "gpflowopt.acquisition"], 62 | package_dir={'gpflowopt': 'gpflowopt'}, 63 | py_modules=['gpflowopt.__init__'], 64 | test_suite='testing', 65 | install_requires=dependencies, 66 | extras_require={ 67 | 'gpu': ['tensorflow-gpu>=1.0.0'], 68 | 'docs': ['sphinx==1.7.8', 'sphinx_rtd_theme', 'numpydoc==0.8.0', 'nbsphinx==0.3.4', 'jupyter'], 69 | }, 70 | dependency_links=['https://github.com/GPflow/GPflow/archive/0.5.0.tar.gz#egg=GPflow-0.5.0'], 71 | classifiers=['License :: OSI Approved :: Apache Software License', 72 | 'Natural Language :: English', 73 | 'Operating System :: POSIX :: Linux', 74 | 'Programming Language :: Python :: 2.7', 75 | 'Programming Language :: Python :: 3.5', 76 | 'Programming Language :: Python :: 3.6', 77 | 'Intended Audience :: Science/Research', 78 | 'Intended Audience :: Developers', 79 | 'Topic :: Scientific/Engineering :: Artificial Intelligence'] 80 | ) 81 | -------------------------------------------------------------------------------- /testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPflow/GPflowOpt/3d86bcc000b0367f19e9f03f4458f5641e5dde60/testing/__init__.py -------------------------------------------------------------------------------- /testing/data/lhd.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPflow/GPflowOpt/3d86bcc000b0367f19e9f03f4458f5641e5dde60/testing/data/lhd.npz -------------------------------------------------------------------------------- /testing/data/vlmop.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPflow/GPflowOpt/3d86bcc000b0367f19e9f03f4458f5641e5dde60/testing/data/vlmop.npz -------------------------------------------------------------------------------- /testing/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPflow/GPflowOpt/3d86bcc000b0367f19e9f03f4458f5641e5dde60/testing/system/__init__.py -------------------------------------------------------------------------------- /testing/system/test_notebooks.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import nbformat 3 | from nbconvert.preprocessors import ExecutePreprocessor 4 | from nbconvert.preprocessors.execute import CellExecutionError 5 | import glob 6 | import traceback 7 | import sys 8 | import time 9 | import os 10 | import pytest 11 | 12 | this_dir = os.path.dirname(__file__) 13 | nbpath = os.path.join(this_dir, '../../doc/source/notebooks/') 14 | blacklist = ['hyperopt.ipynb', 'mes_benchmark.ipynb', 'constrained_bo_mes.ipynb'] 15 | lfiles = [f for f in glob.glob(nbpath+"*.ipynb") if f not in map(lambda b: nbpath+b, blacklist)] 16 | 17 | 18 | def _exec_notebook(notebook_filename, nbpath): 19 | pythonkernel = 'python' + str(sys.version_info[0]) 20 | ep = ExecutePreprocessor(timeout=600, kernel_name=pythonkernel, interrupt_on_timeout=True) 21 | with open(notebook_filename) as f: 22 | nb = nbformat.read(f, as_version=nbformat.current_nbformat) 23 | try: 24 | ep.preprocess(nb, {'metadata': {'path': nbpath}}) 25 | except CellExecutionError: 26 | print('-' * 60) 27 | traceback.print_exc(file=sys.stdout) 28 | print('-' * 60) 29 | assert False, 'Error executing the notebook %s. See above for error.' % notebook_filename 30 | 31 | 32 | @pytest.mark.parametrize('notebook', lfiles) 33 | def test_notebook(notebook): 34 | t = time.time() 35 | _exec_notebook(notebook, nbpath) 36 | print(notebook, 'took %g seconds.' % (time.time()-t)) 37 | -------------------------------------------------------------------------------- /testing/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPflow/GPflowOpt/3d86bcc000b0367f19e9f03f4458f5641e5dde60/testing/unit/__init__.py -------------------------------------------------------------------------------- /testing/unit/test_datascaler.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import numpy as np 3 | from gpflowopt.scaling import DataScaler 4 | from ..utility import GPflowOptTestCase, create_parabola_model, parabola2d 5 | 6 | 7 | class TestDataScaler(GPflowOptTestCase): 8 | 9 | @property 10 | def domain(self): 11 | return np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 12 | 13 | def test_object_integrity(self): 14 | with self.test_session(): 15 | m = create_parabola_model(self.domain) 16 | Xs, Ys = m.X.value, m.Y.value 17 | n = DataScaler(m, self.domain) 18 | 19 | self.assertTrue(np.allclose(Xs, n.X.value)) 20 | self.assertTrue(np.allclose(Ys, n.Y.value)) 21 | 22 | def test_enabling_transforms(self): 23 | with self.test_session(): 24 | m = create_parabola_model(self.domain) 25 | normY = (m.Y.value - np.mean(m.Y.value, axis=0)) / np.std(m.Y.value, axis=0) 26 | scaledX = (m.X.value + 1) / 2 27 | 28 | n1 = DataScaler(m, normalize_Y=False) 29 | self.assertFalse(n1.normalize_output) 30 | self.assertTrue(np.allclose(m.X.value, n1.X.value)) 31 | self.assertTrue(np.allclose(m.Y.value, n1.Y.value)) 32 | n1.input_transform = self.domain >> gpflowopt.domain.UnitCube(self.domain.size) 33 | self.assertTrue(np.allclose(m.X.value, scaledX)) 34 | self.assertTrue(np.allclose(m.Y.value, n1.Y.value)) 35 | n1.normalize_output = True 36 | self.assertTrue(n1.normalize_output) 37 | self.assertTrue(np.allclose(m.Y.value, normY)) 38 | 39 | m = create_parabola_model(self.domain) 40 | n2 = DataScaler(m, self.domain, normalize_Y=False) 41 | self.assertTrue(np.allclose(m.X.value, scaledX)) 42 | self.assertTrue(np.allclose(m.Y.value, n2.Y.value)) 43 | n2.normalize_output = True 44 | self.assertTrue(np.allclose(m.Y.value, normY)) 45 | n2.input_transform = gpflowopt.domain.UnitCube(self.domain.size) >> gpflowopt.domain.UnitCube(self.domain.size) 46 | self.assertTrue(np.allclose(m.X.value, n1.X.value)) 47 | 48 | m = create_parabola_model(self.domain) 49 | n3 = DataScaler(m, normalize_Y=True) 50 | self.assertTrue(np.allclose(m.X.value, n3.X.value)) 51 | self.assertTrue(np.allclose(m.Y.value, normY)) 52 | n3.normalize_output = False 53 | self.assertTrue(np.allclose(m.Y.value, n3.Y.value)) 54 | 55 | m = create_parabola_model(self.domain) 56 | n4 = DataScaler(m, self.domain, normalize_Y=True) 57 | self.assertTrue(np.allclose(m.X.value, scaledX)) 58 | self.assertTrue(np.allclose(m.Y.value, normY)) 59 | n4.normalize_output = False 60 | self.assertTrue(np.allclose(m.Y.value, n3.Y.value)) 61 | 62 | m = create_parabola_model(self.domain) 63 | Y = m.Y.value 64 | n5 = DataScaler(m, self.domain, normalize_Y=False) 65 | n5.output_transform = gpflowopt.transforms.LinearTransform(2, 0) 66 | self.assertTrue(np.allclose(m.X.value, scaledX)) 67 | self.assertTrue(np.allclose(n5.Y.value, Y)) 68 | self.assertTrue(np.allclose(m.Y.value, Y*2)) 69 | 70 | def test_predict_scaling(self): 71 | with self.test_session(): 72 | m = create_parabola_model(self.domain) 73 | n = DataScaler(create_parabola_model(self.domain), self.domain, normalize_Y=True) 74 | m.optimize() 75 | n.optimize() 76 | 77 | Xt = gpflowopt.design.RandomDesign(20, self.domain).generate() 78 | fr, vr = m.predict_f(Xt) 79 | fs, vs = n.predict_f(Xt) 80 | self.assertTrue(np.allclose(fr, fs, atol=1e-3)) 81 | self.assertTrue(np.allclose(vr, vs, atol=1e-3)) 82 | 83 | fr, vr = m.predict_y(Xt) 84 | fs, vs = n.predict_y(Xt) 85 | self.assertTrue(np.allclose(fr, fs, atol=1e-3)) 86 | self.assertTrue(np.allclose(vr, vs, atol=1e-3)) 87 | 88 | fr, vr = m.predict_f_full_cov(Xt) 89 | fs, vs = n.predict_f_full_cov(Xt) 90 | self.assertTrue(np.allclose(fr, fs, atol=1e-3)) 91 | self.assertTrue(np.allclose(vr, vs, atol=1e-3)) 92 | 93 | Yt = parabola2d(Xt) 94 | fr = m.predict_density(Xt, Yt) 95 | fs = n.predict_density(Xt, Yt) 96 | np.testing.assert_allclose(fr, fs, rtol=1e-2) 97 | 98 | 99 | -------------------------------------------------------------------------------- /testing/unit/test_design.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import numpy as np 3 | import os 4 | from ..utility import GPflowOptTestCase 5 | 6 | 7 | class _TestDesign(object): 8 | 9 | @property 10 | def designs(self): 11 | raise NotImplementedError() 12 | 13 | @property 14 | def domains(self): 15 | createfx = lambda j: np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -i, 2 * i) for i in range(1, j + 1)]) 16 | return list(map(createfx, np.arange(1, 6))) 17 | 18 | def test_design_compliance(self): 19 | points = [design.generate() for design in self.designs] 20 | for p, d in zip(points, self.designs): 21 | self.assertTupleEqual(p.shape, (d.size, d.domain.size), msg="Generated design does match specifications") 22 | self.assertIn(p, d.domain, "Not all generated points are generated within the domain") 23 | 24 | def test_create_to_generate(self): 25 | X = [design.create_design() for design in self.designs] 26 | Xt = [design.generate() for design in self.designs] 27 | transforms = [design.generative_domain >> design.domain for design in self.designs] 28 | 29 | Xs = [t.forward(p) for p, t in zip(X, transforms)] 30 | Xr = [t.backward(p) for p, t in zip(Xt, transforms)] 31 | 32 | for generated, scaled in zip(Xs, Xt): 33 | np.testing.assert_allclose(generated, scaled, atol=1e-4, 34 | err_msg="Incorrect scaling from generative domain to domain") 35 | 36 | for generated, scaled in zip(Xr, X): 37 | np.testing.assert_allclose(generated, scaled, atol=1e-4, 38 | err_msg="Incorrect scaling from generative domain to domain") 39 | 40 | 41 | class TestRandomDesign(_TestDesign, GPflowOptTestCase): 42 | 43 | @_TestDesign.designs.getter 44 | def designs(self): 45 | return [gpflowopt.design.RandomDesign(200, domain) for domain in self.domains] 46 | 47 | def test_create_to_generate(self): 48 | pass 49 | 50 | 51 | class TestEmptyDesign(_TestDesign, GPflowOptTestCase): 52 | @_TestDesign.designs.getter 53 | def designs(self): 54 | return [gpflowopt.design.EmptyDesign(domain) for domain in self.domains] 55 | 56 | 57 | class TestFactorialDesign(_TestDesign, GPflowOptTestCase): 58 | @_TestDesign.designs.getter 59 | def designs(self): 60 | return [gpflowopt.design.FactorialDesign(4, domain) for domain in self.domains] 61 | 62 | def test_validity(self): 63 | for design in self.designs: 64 | A = design.generate() 65 | for i, l, u in zip(range(1, design.domain.size + 1), design.domain.lower, design.domain.upper): 66 | self.assertTrue(np.all(np.any(np.abs(A[:,i - 1] - np.linspace(l, u, 4)[:, None]) < 1e-4, axis=0)), 67 | msg="Generated off-grid.") 68 | 69 | 70 | class TestLatinHyperCubeDesign(_TestDesign, GPflowOptTestCase): 71 | 72 | @_TestDesign.designs.getter 73 | def designs(self): 74 | return [gpflowopt.design.LatinHyperCube(20, domain) for domain in self.domains] 75 | 76 | def test_validity(self): 77 | groundtruth = np.load(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'lhd.npz')) 78 | points = [lhd.generate() for lhd in self.designs] 79 | lhds = map(lambda file: groundtruth[file], groundtruth.files) 80 | idx = np.argsort([lhd.shape[-1] for lhd in lhds]) 81 | for generated, real in zip(points, map(lambda file: groundtruth[file], np.array(groundtruth.files)[idx])): 82 | self.assertTrue(np.allclose(generated, real), msg="Generated LHD does not correspond to the groundtruth") -------------------------------------------------------------------------------- /testing/unit/test_domain.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import numpy as np 3 | from ..utility import GPflowOptTestCase 4 | 5 | 6 | class TestContinuousParameter(GPflowOptTestCase): 7 | 8 | def test_simple(self): 9 | p = gpflowopt.domain.ContinuousParameter("x1", 0, 1) 10 | self.assertTrue(np.allclose(p._range, [0,1]), msg="Internal storage of object incorrect") 11 | self.assertEqual(p.lower, 0, msg="Lower should equal 0") 12 | self.assertEqual(p.upper, 1, msg="Upper should equal 1") 13 | self.assertEqual(p.size, 1, msg="Size of parameter should equal 1") 14 | 15 | p.upper = 2 16 | self.assertEqual(p.upper, 2, msg="After assignment, upper should equal 2") 17 | p.lower = 1 18 | self.assertEqual(p.lower, 1, msg="After assignment, lower should equal 2") 19 | 20 | p = np.sum([gpflowopt.domain.ContinuousParameter("x1", 0, 1)]) 21 | self.assertTrue(p.size == 1, msg="Construction of domain by list using sum failed") 22 | 23 | def test_equality(self): 24 | p = gpflowopt.domain.ContinuousParameter("x1", 0, 1) 25 | pne = gpflowopt.domain.ContinuousParameter("x1", 0, 2) 26 | self.assertNotEqual(p, pne, msg="Should not be equal (invalid upper)") 27 | pne = gpflowopt.domain.ContinuousParameter("x1", -1, 1) 28 | self.assertNotEqual(p, pne, msg="Should not be equal (invalid lower)") 29 | pne = gpflowopt.domain.ContinuousParameter("x1", -1, 2) 30 | self.assertNotEqual(p, pne, msg="Should not be equal (invalid lower/upper)") 31 | p.lower = -1 32 | p.upper = 2 33 | self.assertEqual(p, pne, msg="Should be equal after adjusting bounds") 34 | 35 | def test_indexing(self): 36 | p = np.sum([gpflowopt.domain.ContinuousParameter("x1", 0, 1), 37 | gpflowopt.domain.ContinuousParameter("x2", 0, 1), 38 | gpflowopt.domain.ContinuousParameter("x3", 0, 1), 39 | gpflowopt.domain.ContinuousParameter("x4", 0, 1)]) 40 | 41 | subdomain = p[['x4', 'x1', 2]] 42 | self.assertTrue(subdomain.size == 3, msg="Subdomain should have size 3") 43 | self.assertTrue(subdomain[0].label == 'x4', msg="Subdomain's first parameter should be 'x4'") 44 | self.assertTrue(subdomain[1].label == 'x1', msg="Subdomain's second parameter should be 'x1'") 45 | self.assertTrue(subdomain[2].label == 'x3', msg="Subdomain's third parameter should be 'x3'") 46 | 47 | def test_containment(self): 48 | p = gpflowopt.domain.ContinuousParameter("x1", 0, 1) 49 | self.assertIn(0, p, msg="Point is within domain") 50 | self.assertIn(0.5, p, msg="Point is within domain") 51 | self.assertIn(1, p, msg="Point is within domain") 52 | self.assertNotIn(1.1, p, msg="Point is not within domain") 53 | self.assertNotIn(-0.5, p, msg="Point is not within domain") 54 | 55 | def test_value(self): 56 | p = gpflowopt.domain.ContinuousParameter("x1", 0, 1) 57 | self.assertTupleEqual(p.value.shape, (1,), msg="Default value has incorrect shape.") 58 | self.assertTrue(np.allclose(p.value, 0.5), msg="Parameter has incorrect default value") 59 | 60 | p.value = 0.8 61 | self.assertTrue(np.allclose(p.value, 0.8), msg="Parameter has incorrect value after update") 62 | 63 | p.value = [0.6, 0.8] 64 | self.assertTupleEqual(p.value.shape, (2,), msg="Default value has incorrect shape.") 65 | np.testing.assert_allclose(p.value, np.array([0.6, 0.8]), err_msg="Parameter has incorrect value after update") 66 | 67 | p = gpflowopt.domain.ContinuousParameter("x1", 0, 1, 0.2) 68 | self.assertTupleEqual(p.value.shape, (1,), msg="Default value has incorrect shape.") 69 | self.assertTrue(np.allclose(p.value, 0.2), msg="Parameter has incorrect initialized value") 70 | 71 | 72 | class TestHypercubeDomain(GPflowOptTestCase): 73 | 74 | def setUp(self): 75 | self.domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 4)]) 76 | 77 | def test_object_integrity(self): 78 | self.assertEqual(len(self.domain._parameters), 3) 79 | 80 | def test_simple(self): 81 | self.assertEqual(self.domain.size, 3, msg="Size of domain should equal 3") 82 | self.assertTrue(np.allclose(self.domain.lower, -1.0), msg="Lower of domain should equal -1 for all parameters") 83 | self.assertTrue(np.allclose(self.domain.upper, 1.0), msg="Lower of domain should equal 1 for all parameters") 84 | 85 | def test_equality(self): 86 | dne = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)] + 87 | [gpflowopt.domain.ContinuousParameter("x3", -3, 1)]) 88 | self.assertNotEqual(self.domain, dne, msg="One lower bound mismatch, should not be equal.") 89 | dne = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)] + 90 | [gpflowopt.domain.ContinuousParameter("x3", -1, 2)]) 91 | self.assertNotEqual(self.domain, dne, msg="One upper bound mismatch, should not be equal.") 92 | dne = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 93 | self.assertNotEqual(self.domain, dne, msg="Size mismatch") 94 | de = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 4)]) 95 | self.assertEqual(self.domain, de, msg="No mismatches, should be equal") 96 | 97 | def test_parenting(self): 98 | for p in self.domain: 99 | self.assertEqual(id(p._parent), id(self.domain), "Misspecified parent link detected") 100 | 101 | def test_access(self): 102 | for i in range(self.domain.size): 103 | self.assertEqual(self.domain[i].label, "x{0}".format(i+1), "Accessing parameters, encountering " 104 | "incorrect labels") 105 | 106 | self.domain[2].lower = -2 107 | de = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)] + 108 | [gpflowopt.domain.ContinuousParameter("x3", -2, 1)]) 109 | 110 | self.assertEqual(self.domain, de, msg="No mismatches, should be equal") 111 | 112 | def test_containment(self): 113 | A = np.random.rand(50,3)*2-1 114 | self.assertTrue(A in self.domain, msg="Generated random points within domain") 115 | 116 | A = np.vstack((A, np.array([-2, -2, -2]))) 117 | self.assertFalse(A in self.domain, msg="One of the points was not in the domain") 118 | 119 | A = np.random.rand(50,4)*2-1 120 | self.assertFalse(A in self.domain, msg="Generated random points have different dimensionality") 121 | 122 | def test_value(self): 123 | self.assertTupleEqual(self.domain.value.shape, (1, 3), msg="Default value has incorrect shape.") 124 | np.testing.assert_allclose(self.domain.value, np.array([[0, 0, 0]]), err_msg="Parameter has incorrect initial value") 125 | 126 | A = np.random.rand(10, 3) * 2 - 1 127 | self.domain.value = A 128 | self.assertTupleEqual(self.domain.value.shape, (10, 3), msg="Assigned value has incorrect shape.") 129 | np.testing.assert_allclose(self.domain.value, A, err_msg="Parameter has incorrect value after assignment") 130 | 131 | def test_transformation(self): 132 | X = np.random.rand(50,3)*2-1 133 | target = gpflowopt.domain.UnitCube(3) 134 | transform = self.domain >> target 135 | self.assertTrue(np.allclose(transform.forward(X), (X + 1) / 2), msg="Transformation to [0,1] incorrect") 136 | self.assertTrue(np.allclose(transform.backward(transform.forward(X)), X), 137 | msg="Transforming back and forth yields different result") 138 | 139 | inv_transform = target >> self.domain 140 | self.assertTrue(np.allclose(transform.backward(transform.forward(X)), 141 | inv_transform.forward(transform.forward(X))), 142 | msg="Inverse transform yields different results") 143 | self.assertTrue(np.allclose((~transform).A.value, inv_transform.A.value)) 144 | self.assertTrue(np.allclose((~transform).b.value, inv_transform.b.value)) 145 | 146 | def test_unitcube(self): 147 | domain = gpflowopt.domain.UnitCube(3) 148 | self.assertTrue(np.allclose(domain.lower, 0)) 149 | self.assertTrue(np.allclose(domain.upper, 1)) 150 | self.assertEqual(domain.size, 3) 151 | 152 | -------------------------------------------------------------------------------- /testing/unit/test_implementations.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import numpy as np 3 | import pytest 4 | import tensorflow as tf 5 | from ..utility import create_parabola_model, create_plane_model, create_vlmop2_model, parabola2d, load_data, GPflowOptTestCase 6 | 7 | domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 8 | 9 | acquisitions = [gpflowopt.acquisition.ExpectedImprovement(create_parabola_model(domain)), 10 | gpflowopt.acquisition.ProbabilityOfImprovement(create_parabola_model(domain)), 11 | gpflowopt.acquisition.ProbabilityOfFeasibility(create_parabola_model(domain)), 12 | gpflowopt.acquisition.LowerConfidenceBound(create_parabola_model(domain)), 13 | gpflowopt.acquisition.HVProbabilityOfImprovement([create_parabola_model(domain), 14 | create_parabola_model(domain)]), 15 | gpflowopt.acquisition.MinValueEntropySearch(create_parabola_model(domain), domain) 16 | ] 17 | 18 | 19 | @pytest.mark.parametrize('acquisition', acquisitions) 20 | def test_acquisition_evaluate(acquisition): 21 | with tf.Session(graph=tf.Graph()): 22 | X = gpflowopt.design.RandomDesign(10, domain).generate() 23 | p = acquisition.evaluate(X) 24 | assert isinstance(p, np.ndarray) 25 | assert p.shape == (10, 1) 26 | 27 | q = acquisition.evaluate_with_gradients(X) 28 | assert isinstance(q, tuple) 29 | assert len(q) == 2 30 | assert all(isinstance(q[i], np.ndarray) for i in range(2)) 31 | assert q[0].shape == (10, 1) 32 | assert q[1].shape == (10, 2) 33 | np.testing.assert_allclose(p, q[0]) 34 | 35 | 36 | class TestExpectedImprovement(GPflowOptTestCase): 37 | 38 | def setUp(self): 39 | self.domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 40 | self.model = create_parabola_model(self.domain) 41 | self.acquisition = gpflowopt.acquisition.ExpectedImprovement(self.model) 42 | 43 | def test_objective_indices(self): 44 | with self.test_session(): 45 | self.assertEqual(self.acquisition.objective_indices(), np.arange(1, dtype=int), 46 | msg="ExpectedImprovement returns all objectives") 47 | 48 | def test_setup(self): 49 | with self.test_session(): 50 | self.acquisition._optimize_models() 51 | self.acquisition._setup() 52 | fmin = np.min(self.acquisition.data[1]) 53 | self.assertGreater(self.acquisition.fmin.value, 0, msg="The minimum (0) is not amongst the design.") 54 | self.assertTrue(np.allclose(self.acquisition.fmin.value, fmin, atol=1e-2), msg="fmin computed incorrectly") 55 | 56 | # Now add the actual minimum 57 | p = np.array([[0.0, 0.0]]) 58 | self.acquisition.set_data(np.vstack((self.acquisition.data[0], p)), 59 | np.vstack((self.acquisition.data[1], parabola2d(p)))) 60 | self.acquisition._optimize_models() 61 | self.acquisition._setup() 62 | self.assertTrue(np.allclose(self.acquisition.fmin.value, 0, atol=1e-1), msg="fmin not updated") 63 | 64 | def test_ei_validity(self): 65 | with self.test_session(): 66 | Xcenter = np.random.rand(20, 2) * 0.25 - 0.125 67 | X = np.random.rand(100, 2) * 2 - 1 68 | hor_idx = np.abs(X[:, 0]) > 0.8 69 | ver_idx = np.abs(X[:, 1]) > 0.8 70 | Xborder = np.vstack((X[hor_idx, :], X[ver_idx, :])) 71 | ei1 = self.acquisition.evaluate(Xborder) 72 | ei2 = self.acquisition.evaluate(Xcenter) 73 | self.assertGreater(np.min(ei2), np.max(ei1)) 74 | self.assertTrue(np.all(self.acquisition.feasible_data_index()), msg="EI does never invalidate points") 75 | 76 | 77 | class TestProbabilityOfImprovement(GPflowOptTestCase): 78 | 79 | def setUp(self): 80 | self.domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 81 | self.model = create_parabola_model(self.domain) 82 | self.acquisition = gpflowopt.acquisition.ProbabilityOfImprovement(self.model) 83 | 84 | def test_objective_indices(self): 85 | with self.test_session(): 86 | self.assertEqual(self.acquisition.objective_indices(), np.arange(1, dtype=int), 87 | msg="PoI returns all objectives") 88 | 89 | def test_setup(self): 90 | with self.test_session(): 91 | self.acquisition._optimize_models() 92 | self.acquisition._setup() 93 | fmin = np.min(self.acquisition.data[1]) 94 | self.assertGreater(self.acquisition.fmin.value, 0, msg="The minimum (0) is not amongst the design.") 95 | self.assertTrue(np.allclose(self.acquisition.fmin.value, fmin, atol=1e-2), msg="fmin computed incorrectly") 96 | 97 | # Now add the actual minimum 98 | p = np.array([[0.0, 0.0]]) 99 | self.acquisition.set_data(np.vstack((self.acquisition.data[0], p)), 100 | np.vstack((self.acquisition.data[1], parabola2d(p)))) 101 | self.acquisition._optimize_models() 102 | self.acquisition._setup() 103 | self.assertTrue(np.allclose(self.acquisition.fmin.value, 0, atol=1e-1), msg="fmin not updated") 104 | 105 | def test_poi_validity(self): 106 | with self.test_session(): 107 | Xcenter = np.random.rand(20, 2) * 0.25 - 0.125 108 | X = np.random.rand(100, 2) * 2 - 1 109 | hor_idx = np.abs(X[:, 0]) > 0.8 110 | ver_idx = np.abs(X[:, 1]) > 0.8 111 | Xborder = np.vstack((X[hor_idx, :], X[ver_idx, :])) 112 | poi1 = self.acquisition.evaluate(Xborder) 113 | poi2 = self.acquisition.evaluate(Xcenter) 114 | self.assertGreater(np.min(poi2), np.max(poi1)) 115 | self.assertTrue(np.all(self.acquisition.feasible_data_index()), msg="EI does never invalidate points") 116 | 117 | 118 | class TestProbabilityOfFeasibility(GPflowOptTestCase): 119 | 120 | def setUp(self): 121 | self.domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 122 | self.model = create_plane_model(self.domain) 123 | self.acquisition = gpflowopt.acquisition.ProbabilityOfFeasibility(self.model) 124 | 125 | def test_constraint_indices(self): 126 | with self.test_session(): 127 | self.assertEqual(self.acquisition.constraint_indices(), np.arange(1, dtype=int), 128 | msg="PoF returns all constraints") 129 | 130 | def test_pof_validity(self): 131 | with self.test_session(): 132 | X1 = np.random.rand(10, 2) / 4 133 | X2 = np.random.rand(10, 2) / 4 + 0.75 134 | self.assertTrue(np.all(self.acquisition.evaluate(X1) > 0.85), msg="Left half of plane is feasible") 135 | self.assertTrue(np.all(self.acquisition.evaluate(X2) < 0.15), msg="Right half of plane is feasible") 136 | self.assertTrue(np.all(self.acquisition.evaluate(X1) > self.acquisition.evaluate(X2).T)) 137 | 138 | 139 | class TestLowerConfidenceBound(GPflowOptTestCase): 140 | 141 | def setUp(self): 142 | self.domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 143 | self.model = create_plane_model(self.domain) 144 | self.acquisition = gpflowopt.acquisition.LowerConfidenceBound(self.model, 3.2) 145 | 146 | def test_objective_indices(self): 147 | with self.test_session(): 148 | self.assertEqual(self.acquisition.objective_indices(), np.arange(1, dtype=int), 149 | msg="LCB returns all objectives") 150 | 151 | def test_object_integrity(self): 152 | with self.test_session(): 153 | self.assertEqual(self.acquisition.sigma.value, 3.2) 154 | 155 | def test_lcb_validity(self): 156 | with self.test_session(): 157 | design = gpflowopt.design.RandomDesign(200, self.domain).generate() 158 | q = self.acquisition.evaluate(design) 159 | p = self.acquisition.models[0].predict_f(design)[0] 160 | np.testing.assert_array_less(q, p) 161 | 162 | def test_lcb_validity_2(self): 163 | with self.test_session(): 164 | design = gpflowopt.design.RandomDesign(200, self.domain).generate() 165 | self.acquisition.sigma = 0 166 | q = self.acquisition.evaluate(design) 167 | p = self.acquisition.models[0].predict_f(design)[0] 168 | np.testing.assert_allclose(q, p) 169 | 170 | 171 | class TestHVProbabilityOfImprovement(GPflowOptTestCase): 172 | 173 | def setUp(self): 174 | self.model = create_vlmop2_model() 175 | self.data = load_data('vlmop.npz') 176 | self.acquisition = gpflowopt.acquisition.HVProbabilityOfImprovement(self.model) 177 | 178 | def test_object_integrity(self): 179 | with self.test_session(): 180 | self.assertEqual(len(self.acquisition.models), 2, msg="Model list has incorrect length.") 181 | for m1, m2 in zip(self.acquisition.models, self.model): 182 | self.assertEqual(m1, m2, msg="Incorrect model stored in ExpectedImprovement") 183 | 184 | def test_HvPoI_validity(self): 185 | with self.test_session(): 186 | scores = self.acquisition.evaluate(self.data['candidates']) 187 | np.testing.assert_almost_equal(scores, self.data['scores'], decimal=2) 188 | 189 | 190 | class TestMinValueEntropySearch(GPflowOptTestCase): 191 | def setUp(self): 192 | super(TestMinValueEntropySearch, self).setUp() 193 | self.domain = np.sum([gpflowopt.domain.ContinuousParameter("x{0}".format(i), -1, 1) for i in range(1, 3)]) 194 | self.model = create_parabola_model(self.domain) 195 | self.acquisition = gpflowopt.acquisition.MinValueEntropySearch(self.model, self.domain) 196 | 197 | def test_objective_indices(self): 198 | self.assertEqual(self.acquisition.objective_indices(), np.arange(1, dtype=int), 199 | msg="MinValueEntropySearch returns all objectives") 200 | 201 | def test_setup(self): 202 | fmin = np.min(self.acquisition.data[1]) 203 | self.assertGreater(fmin, 0, msg="The minimum (0) is not amongst the design.") 204 | self.assertTrue(self.acquisition.samples.shape == (self.acquisition.num_samples,), 205 | msg="fmin computed incorrectly") 206 | 207 | def test_MES_validity(self): 208 | with self.test_session(): 209 | Xcenter = np.random.rand(20, 2) * 0.25 - 0.125 210 | X = np.random.rand(100, 2) * 2 - 1 211 | hor_idx = np.abs(X[:, 0]) > 0.8 212 | ver_idx = np.abs(X[:, 1]) > 0.8 213 | Xborder = np.vstack((X[hor_idx, :], X[ver_idx, :])) 214 | ei1 = self.acquisition.evaluate(Xborder) 215 | ei2 = self.acquisition.evaluate(Xcenter) 216 | self.assertGreater(np.min(ei2) + 1E-6, np.max(ei1)) 217 | self.assertTrue(np.all(self.acquisition.feasible_data_index()), msg="MES does never invalidate points") 218 | -------------------------------------------------------------------------------- /testing/unit/test_modelwrapper.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import gpflow 3 | import numpy as np 4 | from ..utility import create_parabola_model, GPflowOptTestCase 5 | 6 | float_type = gpflow.settings.dtypes.float_type 7 | 8 | 9 | class MethodOverride(gpflowopt.models.ModelWrapper): 10 | 11 | def __init__(self, m): 12 | super(MethodOverride, self).__init__(m) 13 | self.A = gpflow.param.DataHolder(np.array([1.0])) 14 | 15 | @gpflow.param.AutoFlow((float_type, [None, None])) 16 | def predict_f(self, Xnew): 17 | """ 18 | Compute the mean and variance of held-out data at the points Xnew 19 | """ 20 | m, v = self.build_predict(Xnew) 21 | return self.A * m, v 22 | 23 | @property 24 | def X(self): 25 | return self.wrapped.X 26 | 27 | @X.setter 28 | def X(self, Xc): 29 | self.wrapped.X = Xc 30 | 31 | @property 32 | def foo(self): 33 | return 1 34 | 35 | @foo.setter 36 | def foo(self, val): 37 | self.wrapped.foo = val 38 | 39 | 40 | class TestModelWrapper(GPflowOptTestCase): 41 | 42 | def setUp(self): 43 | self.m = create_parabola_model(gpflowopt.domain.UnitCube(2)) 44 | 45 | def test_object_integrity(self): 46 | w = gpflowopt.models.ModelWrapper(self.m) 47 | self.assertEqual(w.wrapped, self.m) 48 | self.assertEqual(self.m._parent, w) 49 | self.assertEqual(w.optimize, self.m.optimize) 50 | 51 | def test_optimize(self): 52 | with self.test_session(): 53 | w = gpflowopt.models.ModelWrapper(self.m) 54 | logL = self.m.compute_log_likelihood() 55 | self.assertTrue(np.allclose(logL, w.compute_log_likelihood())) 56 | 57 | # Check if compiled & optimized, verify attributes are set in the right object. 58 | w.optimize(maxiter=5) 59 | self.assertTrue(hasattr(self.m, '_minusF')) 60 | self.assertFalse('_minusF' in w.__dict__) 61 | self.assertGreater(self.m.compute_log_likelihood(), logL) 62 | 63 | def test_af_storage_detection(self): 64 | with self.test_session(): 65 | # Regression test for a bug with predict_f/predict_y... etc. 66 | x = np.random.rand(10,2) 67 | self.m.predict_f(x) 68 | self.assertTrue(hasattr(self.m, '_predict_f_AF_storage')) 69 | w = MethodOverride(self.m) 70 | self.assertFalse(hasattr(w, '_predict_f_AF_storage')) 71 | w.predict_f(x) 72 | self.assertTrue(hasattr(w, '_predict_f_AF_storage')) 73 | 74 | def test_set_wrapped_attributes(self): 75 | # Regression test for setting certain keys in the right object 76 | w = gpflowopt.models.ModelWrapper(self.m) 77 | w._needs_recompile = False 78 | self.assertFalse('_needs_recompile' in w.__dict__) 79 | self.assertTrue('_needs_recompile' in self.m.__dict__) 80 | self.assertFalse(w._needs_recompile) 81 | self.assertFalse(self.m._needs_recompile) 82 | 83 | def test_double_wrap(self): 84 | with self.test_session(): 85 | n = gpflowopt.models.ModelWrapper(MethodOverride(self.m)) 86 | n.optimize(maxiter=10) 87 | Xt = np.random.rand(10, 2) 88 | n.predict_f(Xt) 89 | self.assertFalse('_predict_f_AF_storage' in n.__dict__) 90 | self.assertTrue('_predict_f_AF_storage' in n.wrapped.__dict__) 91 | self.assertFalse('_predict_f_AF_storage' in n.wrapped.wrapped.__dict__) 92 | 93 | n = MethodOverride(gpflowopt.models.ModelWrapper(self.m)) 94 | Xn = np.random.rand(10, 2) 95 | Yn = np.random.rand(10, 1) 96 | n.X = Xn 97 | n.Y = Yn 98 | self.assertTrue(np.allclose(Xn, n.wrapped.wrapped.X.value)) 99 | self.assertTrue(np.allclose(Yn, n.wrapped.wrapped.Y.value)) 100 | self.assertFalse('Y' in n.wrapped.__dict__) 101 | self.assertFalse('X' in n.wrapped.__dict__) 102 | 103 | n.foo = 5 104 | self.assertTrue('foo' in n.wrapped.__dict__) 105 | self.assertFalse('foo' in n.wrapped.wrapped.__dict__) 106 | 107 | def test_name(self): 108 | with self.test_session(): 109 | n = gpflowopt.models.ModelWrapper(self.m) 110 | self.assertEqual(n.name, 'unnamed.modelwrapper') 111 | p = gpflow.param.Parameterized() 112 | p.model = n 113 | self.assertEqual(n.name, 'model.modelwrapper') 114 | n = MethodOverride(create_parabola_model(gpflowopt.domain.UnitCube(2))) 115 | self.assertEqual(n.name, 'unnamed.methodoverride') 116 | 117 | def test_parent_hook(self): 118 | with self.test_session(): 119 | self.m.optimize(maxiter=5) 120 | w = gpflowopt.models.ModelWrapper(self.m) 121 | self.assertTrue(isinstance(self.m.highest_parent, gpflowopt.models.ParentHook)) 122 | self.assertEqual(self.m.highest_parent._hp, w) 123 | self.assertEqual(self.m.highest_parent._hm, w) 124 | 125 | w2 = gpflowopt.models.ModelWrapper(w) 126 | self.assertEqual(self.m.highest_parent._hp, w2) 127 | self.assertEqual(self.m.highest_parent._hm, w2) 128 | 129 | p = gpflow.param.Parameterized() 130 | p.model = w2 131 | self.assertEqual(self.m.highest_parent._hp, p) 132 | self.assertEqual(self.m.highest_parent._hm, w2) 133 | 134 | p.predictor = create_parabola_model(gpflowopt.domain.UnitCube(2)) 135 | p.predictor.predict_f(p.predictor.X.value) 136 | self.assertTrue(hasattr(p.predictor, '_predict_f_AF_storage')) 137 | self.assertFalse(self.m._needs_recompile) 138 | self.m.highest_parent._needs_recompile = True 139 | self.assertFalse('_needs_recompile' in p.__dict__) 140 | self.assertFalse('_needs_recompile' in w.__dict__) 141 | self.assertFalse('_needs_recompile' in w2.__dict__) 142 | self.assertTrue(self.m._needs_recompile) 143 | self.assertFalse(hasattr(p.predictor, '_predict_f_AF_storage')) 144 | 145 | self.assertEqual(self.m.highest_parent.get_free_state, p.get_free_state) 146 | self.m.highest_parent._needs_setup = True 147 | self.assertTrue(hasattr(p, '_needs_setup')) 148 | self.assertTrue(p._needs_setup) 149 | 150 | -------------------------------------------------------------------------------- /testing/unit/test_objective.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import numpy as np 3 | import pytest 4 | 5 | # For to_kwargs 6 | domain = gpflowopt.domain.ContinuousParameter('x', 0, 1) + gpflowopt.domain.ContinuousParameter('y', 0, 1) 7 | 8 | 9 | # This is what we expect the versions applying the decorators to produce (simple additions) 10 | def _ref_function(X): 11 | X = np.atleast_2d(X) 12 | return np.sum(X, axis=1, keepdims=True), X 13 | 14 | 15 | def _check_reference(f, g, X): 16 | np.testing.assert_almost_equal(f, _ref_function(X)[0]) 17 | np.testing.assert_almost_equal(g, _ref_function(X)[1]) 18 | 19 | 20 | # Some versions 21 | @gpflowopt.objective.to_args 22 | def add_to_args(x, y): 23 | return _ref_function(np.vstack((x, y)).T) 24 | 25 | 26 | @gpflowopt.objective.to_kwargs(domain) 27 | def add_to_kwargs(x=None, y=None): 28 | return _ref_function(np.vstack((x, y)).T) 29 | 30 | 31 | @gpflowopt.objective.batch_apply 32 | def add_batch_apply(Xflat): 33 | f, g = _ref_function(Xflat) 34 | return f, g[0, :] 35 | 36 | 37 | @gpflowopt.objective.batch_apply 38 | def add_batch_apply_no_dims(Xflat): 39 | return np.sum(Xflat), Xflat 40 | 41 | 42 | @gpflowopt.objective.batch_apply 43 | @gpflowopt.objective.to_args 44 | def add_batch_apply_to_args(x, y): 45 | f, g = _ref_function(np.vstack((x, y)).T) 46 | return f, g[0, :] 47 | 48 | 49 | @gpflowopt.objective.batch_apply 50 | @gpflowopt.objective.to_kwargs(domain) 51 | def add_batch_apply_to_kwargs(x=None, y=None): 52 | f, g = _ref_function(np.vstack((x, y)).T) 53 | return f, g[0, :] 54 | 55 | 56 | @gpflowopt.objective.batch_apply 57 | def triple_objective(Xflat): 58 | f1, g1 = _ref_function(Xflat) 59 | f2, g2 = _ref_function(2 * Xflat) 60 | f3, g3 = _ref_function(0.5 * Xflat) 61 | return np.hstack((f1, f2, f3)), np.vstack((g1, g2, g3)).T 62 | 63 | 64 | @gpflowopt.objective.batch_apply 65 | def add_batch_apply_no_grad(Xflat): 66 | f, g = _ref_function(Xflat) 67 | return f 68 | 69 | 70 | @pytest.mark.parametrize('fun', [add_to_args, add_to_kwargs, add_batch_apply, add_batch_apply_no_dims, 71 | add_batch_apply_to_args, add_batch_apply_to_kwargs]) 72 | def test_one_point(fun): 73 | X = np.random.rand(2) 74 | f, g = fun(X) 75 | assert f.shape == (1, 1) 76 | assert g.shape == (1, 2) 77 | _check_reference(f, g, X) 78 | 79 | 80 | @pytest.mark.parametrize('fun', [add_to_args, add_to_kwargs, add_batch_apply, add_batch_apply_no_dims, 81 | add_batch_apply_to_args, add_batch_apply_to_kwargs]) 82 | def test_multiple_points(fun): 83 | X = np.random.rand(5, 2) 84 | f, g = fun(X) 85 | assert f.shape == (5, 1) 86 | assert g.shape == (5, 2) 87 | _check_reference(f, g, X) 88 | 89 | 90 | def test_multiple_objectives(): 91 | X = np.random.rand(5, 2) 92 | f, g = triple_objective(X) 93 | assert f.shape == (5, 3) 94 | assert g.shape == (5, 2, 3) 95 | _check_reference(f[:, [0]], g[..., 0], X) 96 | _check_reference(f[:, [1]], g[..., 1], 2 * X) 97 | _check_reference(f[:, [2]], g[..., 2], 0.5 * X) 98 | 99 | 100 | def test_no_grad(): 101 | X = np.random.rand(5, 2) 102 | f = add_batch_apply_no_grad(X) 103 | assert f.shape == (5, 1) 104 | np.testing.assert_almost_equal(f, _ref_function(X)[0]) 105 | 106 | -------------------------------------------------------------------------------- /testing/unit/test_pareto.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import gpflowopt 3 | from ..utility import GPflowOptTestCase 4 | 5 | 6 | class TestUtilities(GPflowOptTestCase): 7 | 8 | def test_nonDominatedSort(self): 9 | scores = np.array([[0.9575, 0.4218], [0.9649, 0.9157], [0.1576, 0.7922], [0.9706, 0.9595], [0.9572, 0.6557], 10 | [0.4854, 0.0357], [0.8003, 0.8491], [0.1419, 0.9340]]) 11 | d1, d2 = gpflowopt.pareto.non_dominated_sort(scores) 12 | np.testing.assert_almost_equal(d1, [[0.1576, 0.7922], [0.4854, 0.0357], [0.1419, 0.934 ]], err_msg='Returned incorrect Pareto set.') 13 | np.testing.assert_almost_equal(d2, [1, 5, 0, 7, 1, 0, 2, 0], err_msg='Returned incorrect dominance') 14 | 15 | 16 | class TestPareto(GPflowOptTestCase): 17 | 18 | def setUp(self): 19 | objective_scores = np.array([[0.9575, 0.4218], 20 | [0.9649, 0.9157], 21 | [0.1576, 0.7922], 22 | [0.9706, 0.9595], 23 | [0.9572, 0.6557], 24 | [0.4854, 0.0357], 25 | [0.8003, 0.8491], 26 | [0.1419, 0.9340]]) 27 | self.p_2d = gpflowopt.pareto.Pareto(objective_scores) 28 | self.p_generic = gpflowopt.pareto.Pareto(np.zeros((1, 2))) 29 | self.p_generic.update(objective_scores, generic_strategy=True) 30 | 31 | scores_3d_set1 = np.array([[2.0, 2.0, 0.0], 32 | [2.0, 0.0, 1.0], 33 | [3.0, 1.0, 0.0]]) 34 | scores_3d_set2 = np.array([[2.0, 0.0, 1.0], 35 | [2.0, 2.0, 0.0], 36 | [3.0, 1.0, 0.0]]) 37 | self.p_3d_set1 = gpflowopt.pareto.Pareto(np.zeros((1, 3))) 38 | self.p_3d_set1.update(scores_3d_set1) 39 | self.p_3d_set2 = gpflowopt.pareto.Pareto(np.zeros((1, 3))) 40 | self.p_3d_set2.update(scores_3d_set2) 41 | 42 | def test_update(self): 43 | np.testing.assert_almost_equal(self.p_2d.bounds.lb.value, np.array([[0, 0], [1, 0], [2, 0], [3, 0]]), 44 | err_msg='LBIDX incorrect.') 45 | np.testing.assert_almost_equal(self.p_2d.bounds.ub.value, np.array([[1, 4], [2, 1], [3, 2], [4, 3]]), 46 | err_msg='UBIDX incorrect.') 47 | np.testing.assert_almost_equal(self.p_2d.front.value, np.array([[0.1419, 0.9340], [0.1576, 0.7922], 48 | [0.4854, 0.0357]]), decimal=4, 49 | err_msg='PF incorrect.') 50 | 51 | np.testing.assert_almost_equal(self.p_generic.bounds.lb.value, np.array([[3, 0], [2, 0], [1, 2], [0, 2], [0, 0]]), 52 | err_msg='LBIDX incorrect.') 53 | np.testing.assert_almost_equal(self.p_generic.bounds.ub.value, np.array([[4, 3], [3, 2], [2, 1], [1, 4], [2, 2]]), 54 | err_msg='UBIDX incorrect.') 55 | np.testing.assert_almost_equal(self.p_generic.front.value, np.array([[0.1419, 0.9340], [0.1576, 0.7922], 56 | [0.4854, 0.0357]]), decimal=4, 57 | err_msg='PF incorrect.') 58 | 59 | self.assertFalse(np.array_equal(self.p_2d.bounds.lb, self.p_generic.bounds.lb), msg='Cell lowerbounds are exactly the same for all strategies.') 60 | self.assertFalse(np.array_equal(self.p_2d.bounds.ub, self.p_generic.bounds.ub), msg='Cell upperbounds are exactly the same for all strategies.') 61 | 62 | def test_hypervolume(self): 63 | np.testing.assert_almost_equal(self.p_2d.hypervolume([2, 2]), 3.3878, decimal=2, err_msg='hypervolume incorrect.') 64 | np.testing.assert_almost_equal(self.p_generic.hypervolume([2, 2]), 3.3878, decimal=2, err_msg='hypervolume incorrect.') 65 | 66 | np.testing.assert_almost_equal(self.p_2d.hypervolume([1, 1]), self.p_generic.hypervolume([1, 1]), decimal=20, 67 | err_msg='hypervolume of different strategies incorrect.') 68 | 69 | np.testing.assert_equal(self.p_3d_set1.hypervolume([4, 4, 4]), 29.0, err_msg='3D hypervolume incorrect.') 70 | np.testing.assert_equal(self.p_3d_set2.hypervolume([4, 4, 4]), 29.0, err_msg='3D hypervolume incorrect.') 71 | -------------------------------------------------------------------------------- /testing/unit/test_regression.py: -------------------------------------------------------------------------------- 1 | import gpflow 2 | import gpflowopt 3 | import numpy as np 4 | from ..utility import GPflowOptTestCase 5 | 6 | 7 | class TestRecompile(GPflowOptTestCase): 8 | """ 9 | Regression test for #37 10 | """ 11 | def test_vgp(self): 12 | with self.test_session(): 13 | domain = gpflowopt.domain.UnitCube(2) 14 | X = gpflowopt.design.RandomDesign(10, domain).generate() 15 | Y = np.sin(X[:,[0]]) 16 | m = gpflow.vgp.VGP(X, Y, gpflow.kernels.RBF(2), gpflow.likelihoods.Gaussian()) 17 | acq = gpflowopt.acquisition.ExpectedImprovement(m) 18 | m.compile() 19 | self.assertFalse(m._needs_recompile) 20 | acq.evaluate(gpflowopt.design.RandomDesign(10, domain).generate()) 21 | self.assertTrue(hasattr(acq, '_evaluate_AF_storage')) 22 | 23 | Xnew = gpflowopt.design.RandomDesign(5, domain).generate() 24 | Ynew = np.sin(Xnew[:,[0]]) 25 | acq.set_data(np.vstack((X, Xnew)), np.vstack((Y, Ynew))) 26 | self.assertFalse(hasattr(acq, '_needs_recompile')) 27 | self.assertFalse(hasattr(acq, '_evaluate_AF_storage')) 28 | acq.evaluate(gpflowopt.design.RandomDesign(10, domain).generate()) -------------------------------------------------------------------------------- /testing/unit/test_transforms.py: -------------------------------------------------------------------------------- 1 | import gpflowopt 2 | import tensorflow as tf 3 | from gpflow import settings 4 | import numpy as np 5 | import pytest 6 | 7 | float_type = settings.dtypes.float_type 8 | np_float_type = np.float32 if float_type is tf.float32 else np.float64 9 | 10 | 11 | class DummyTransform(gpflowopt.transforms.DataTransform): 12 | """ 13 | As linear transform overrides backward/build_backward, create a different transform to obtain coverage of the 14 | default implementations 15 | """ 16 | 17 | def __init__(self, c): 18 | super(DummyTransform, self).__init__() 19 | self.value = c 20 | 21 | def build_forward(self, X): 22 | return X * self.value 23 | 24 | def __invert__(self): 25 | return DummyTransform(1 / self.value) 26 | 27 | def __str__(self): 28 | return '(dummy)' 29 | 30 | 31 | transforms = [DummyTransform(2.0), gpflowopt.transforms.LinearTransform([2.0, 3.5], [1.2, 0.7])] 32 | 33 | 34 | @pytest.mark.parametrize('t', transforms) 35 | def test_forward_backward(t): 36 | x_np = np.random.rand(10, 2).astype(np_float_type) 37 | t._kill_autoflow() 38 | with tf.Session(graph=tf.Graph()): 39 | y = t.forward(x_np) 40 | x = t.backward(y) 41 | np.testing.assert_allclose(x, x_np) 42 | 43 | 44 | @pytest.mark.parametrize('t', transforms) 45 | def test_invert_np(t): 46 | x_np = np.random.rand(10, 2).astype(np_float_type) 47 | t._kill_autoflow() 48 | with tf.Session(graph=tf.Graph()): 49 | y = t.forward(x_np) 50 | x = t.backward(y) 51 | xi = (~t).forward(y) 52 | np.testing.assert_allclose(x, x_np) 53 | np.testing.assert_allclose(xi, x_np) 54 | np.testing.assert_allclose(x, xi) 55 | 56 | 57 | def test_backward_variance_full_cov(): 58 | with tf.Session(graph=tf.Graph()) as session: 59 | t = ~gpflowopt.transforms.LinearTransform([2.0, 1.0], [1.2, 0.7]) 60 | x = tf.placeholder(float_type, [10, 10, 2]) 61 | y = tf.placeholder(float_type, [None]) 62 | t.make_tf_array(y) 63 | 64 | A = np.random.rand(10, 10) 65 | B1 = np.dot(A, A.T) 66 | A = np.random.rand(10, 10) 67 | B2 = np.dot(A, A.T) 68 | B = np.dstack((B1, B2)) 69 | with t.tf_mode(): 70 | scaled = t.build_backward_variance(x) 71 | feed_dict_keys = t.get_feed_dict_keys() 72 | feed_dict = {} 73 | t.update_feed_dict(feed_dict_keys, feed_dict) 74 | session.run(tf.global_variables_initializer(), feed_dict=feed_dict) 75 | feed_dict = {x: B, y: t.get_free_state()} 76 | t.update_feed_dict(feed_dict_keys, feed_dict) 77 | Bs = session.run(scaled, feed_dict=feed_dict) 78 | np.testing.assert_allclose(Bs[:, :, 0] / 4.0, B1) 79 | np.testing.assert_allclose(Bs[:, :, 1], B2) 80 | 81 | 82 | def test_backward_variance(): 83 | with tf.Session(graph=tf.Graph()) as session: 84 | t = ~gpflowopt.transforms.LinearTransform([2.0, 1.0], [1.2, 0.7]) 85 | x = tf.placeholder(float_type, [10, 2]) 86 | y = tf.placeholder(float_type, [None]) 87 | t.make_tf_array(y) 88 | 89 | B = np.random.rand(10, 2) 90 | with t.tf_mode(): 91 | scaled = t.build_backward_variance(x) 92 | feed_dict_keys = t.get_feed_dict_keys() 93 | feed_dict = {} 94 | t.update_feed_dict(feed_dict_keys, feed_dict) 95 | session.run(tf.global_variables_initializer(), feed_dict=feed_dict) 96 | feed_dict = {x: B, y: t.get_free_state()} 97 | t.update_feed_dict(feed_dict_keys, feed_dict) 98 | Bs = session.run(scaled, feed_dict=feed_dict) 99 | np.testing.assert_allclose(Bs, B * np.array([4, 1])) 100 | 101 | 102 | def test_assign(): 103 | with tf.Session(graph=tf.Graph()): 104 | t1 = gpflowopt.transforms.LinearTransform([2.0, 1.0], [1.2, 0.7]) 105 | t2 = gpflowopt.transforms.LinearTransform([1.0, 1.0], [0, 0]) 106 | t1.assign(t2) 107 | np.testing.assert_allclose(t1.A.value, t2.A.value) 108 | np.testing.assert_allclose(t1.b.value, t2.b.value) 109 | -------------------------------------------------------------------------------- /testing/utility.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import gpflow 3 | import gpflowopt 4 | import os 5 | import tensorflow as tf 6 | 7 | 8 | class GPflowOptTestCase(tf.test.TestCase): 9 | """ 10 | Wrapper for TestCase to avoid massive duplication of resetting 11 | Tensorflow Graph. 12 | """ 13 | 14 | _multiprocess_can_split_ = True 15 | 16 | def tearDown(self): 17 | tf.reset_default_graph() 18 | super(GPflowOptTestCase, self).tearDown() 19 | 20 | 21 | def parabola2d(X): 22 | return np.atleast_2d(np.sum(X ** 2, axis=1)).T 23 | 24 | 25 | def plane(X): 26 | return X[:, [0]] - 0.5 27 | 28 | 29 | def vlmop2(x): 30 | transl = 1 / np.sqrt(2) 31 | part1 = (x[:, [0]] - transl) ** 2 + (x[:, [1]] - transl) ** 2 32 | part2 = (x[:, [0]] + transl) ** 2 + (x[:, [1]] + transl) ** 2 33 | y1 = 1 - np.exp(-1 * part1) 34 | y2 = 1 - np.exp(-1 * part2) 35 | return np.hstack((y1, y2)) 36 | 37 | 38 | def load_data(file): 39 | path = os.path.dirname(os.path.realpath(__file__)) 40 | return np.load(os.path.join(path, 'data', file)) 41 | 42 | 43 | def create_parabola_model(domain, design=None): 44 | if design is None: 45 | design = gpflowopt.design.LatinHyperCube(16, domain) 46 | X, Y = design.generate(), parabola2d(design.generate()) 47 | m = gpflow.gpr.GPR(X, Y, gpflow.kernels.RBF(2, ARD=True)) 48 | return m 49 | 50 | 51 | def create_plane_model(domain, design=None): 52 | if design is None: 53 | design = gpflowopt.design.LatinHyperCube(25, domain) 54 | X, Y = design.generate(), plane(design.generate()) 55 | m = gpflow.gpr.GPR(X, Y, gpflow.kernels.RBF(2, ARD=True)) 56 | return m 57 | 58 | 59 | def create_vlmop2_model(): 60 | data = load_data('vlmop.npz') 61 | m1 = gpflow.gpr.GPR(data['X'], data['Y'][:, [0]], kern=gpflow.kernels.Matern32(2)) 62 | m2 = gpflow.gpr.GPR(data['X'], data['Y'][:, [1]], kern=gpflow.kernels.Matern32(2)) 63 | return [m1, m2] --------------------------------------------------------------------------------