├── .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 | [](https://travis-ci.org/GPflow/GPflowOpt)
9 | [](https://codecov.io/gh/GPflow/GPflowOpt)
10 | [](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 | "Name | Type | Values |
x1 | Continuous | [-5. 10.] |
x2 | Continuous | [ 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 | "Name | Type | Values |
x1 | Continuous | [-2. 2.] |
x2 | Continuous | [-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: "{0} | ".format(l), columns))
119 | header += "
"
120 | html.append(header)
121 |
122 | # Add parameters
123 | html.append(self._html_table_rows())
124 | html.append("
")
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]
--------------------------------------------------------------------------------