From 48e475cd9b55c390d28833a4d623226092519eef Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:03:11 -0400 Subject: [PATCH 01/30] Add full doc --- README.rst | 43 +++--- docs/requirements.txt | 5 + docs/src/conf.py | 350 ++++++++++++++++++++++++++++++++++++++++++ docs/src/index.rst | 17 ++ docs/src/usage.rst | 127 +++++++++++++++ tox.ini | 33 ++++ 6 files changed, 556 insertions(+), 19 deletions(-) create mode 100644 docs/requirements.txt create mode 100644 docs/src/conf.py create mode 100644 docs/src/index.rst create mode 100644 docs/src/usage.rst diff --git a/README.rst b/README.rst index 2186124..026d62d 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -=============== -orion.algo.robo -=============== +====================== +RoBO Wrapper for Oríon +====================== .. |pypi| image:: https://img.shields.io/pypi/v/orion.algo.robo @@ -29,34 +29,37 @@ orion.algo.robo ---- -This ``orion.algo`` plugin was generated with `Cookiecutter`_ along with `@Epistimio`_'s `cookiecutter-orion.algo`_ template. - -See Orion : https://github.com/Epistimio/orion +This wrapper provides access through `Oríon`_ to several Bayesian optimization algorithms +in the library `RoBO`_. +This ``orion.algo`` plugin was generated with `Cookiecutter`_ along with `@Epistimio`_'s `cookiecutter-orion.algo`_ template. Installation ------------ The RoBO wrapper is currently only supported on Linux. -Before installing RoBO, make sure you have libeigen and swig installed. On ubuntu, you can install -them with `apt-get`:: +Before installing RoBO, make sure you have ``libeigen`` and ``swig`` installed. +On ubuntu, you can install them with ``apt-get`` + +.. code-block:: console $ sudo apt-get install libeigen3-dev swig One of the dependencies of RoBO does not declare its dependencies and therefore we need -to install these dependencies first. The order of dependencies in `requirements.txt` reflects this -order. To install them sequentially, use the following command:: +to install these dependencies first. The order of dependencies in ``requirements.txt`` +reflects this order. To install them sequentially, use the following command + +.. code-block:: console $ curl -s https://git.io/JLnCA | grep -v "^#" | xargs -n 1 -L 1 pip install -Finally, you can install this package using PyPI:: +Finally, you can install this package using PyPI - $ pip install orion.algo.robo +.. code-block:: console + $ pip install orion.algo.robo -Usage ------ Contribute or Ask ----------------- @@ -76,18 +79,20 @@ Citation -------- If you use this wrapper for your publications, please cite both -`RoBO ` and -`Oríon `. - +`RoBO `__ and +`Oríon `__. Please also cite +the papers of the algorithms you used, such as DNGO or BOHAMIANN. See +the documentation of the algorithms to find corresponding original papers. License ------- Distributed under the terms of the BSD-3-Clause license, -"orion.algo.robo" is free and open source software. +``orion.algo.robo`` is free and open source software. .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter .. _`@Epistimio`: https://github.com/Epistimio .. _`cookiecutter-orion.algo`: https://github.com/Epistimio/cookiecutter-orion.algo -.. _`orion`: https://github.com/Epistimio/orion +.. _`Oríon`: https://github.com/Epistimio/orion +.. _`RoBO`: https://github.com/automl/robo diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..ff41c17 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +sphinx +sphinx_rtd_theme +sphinxcontrib.httpdomain +sphinx-autoapi +numpydoc diff --git a/docs/src/conf.py b/docs/src/conf.py new file mode 100644 index 0000000..ee4546f --- /dev/null +++ b/docs/src/conf.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +# +# orion.algo.robo documentation build configuration file. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import glob +import re +import sys +import os +import shlex + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +docs_src_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, docs_src_path) +src_path = os.path.abspath(os.path.join(docs_src_path, "..", "..", "src")) +sys.path.insert(0, src_path) + +import orion.algo.robo as algo_plugin + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "numpydoc", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", +] + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = u"orion.algo.robo" +_full_version = algo_plugin.__version__ +copyright = algo_plugin.__copyright__ +author = algo_plugin.__author__ + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +release = re.sub(r"(.*?)(?:\.dev\d+)?(?:\+.*)?", r"\1", _full_version) +# The short X.Y version. +version = re.sub(r"(\d+)(\.\d+)?(?:\.\d+)?(?:-.*)?(?:\.post\d+)?", r"\1\2", release) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +default_role = "autolink" + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "sphinx_rtd_theme" +html_theme_options = { + "canonical_url": "", + "analytics_id": "", + "logo_only": False, + "display_version": True, + "prev_next_buttons_location": "both", + # 'style_external_links': False, + # 'vcs_pageview_mode': '', + # Toc options + "collapse_navigation": False, + "sticky_navigation": True, + "navigation_depth": 2, + # 'includehidden': False, + # 'titles_only': False +} + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + "**": [ + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "robodoc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "orion.algo.robo.tex", + u"Oríon algo robo Documentation", + u"Xavier Bouthillier", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "projectname", u"Oríon algo robo Documentation", [author], 1)] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "robo", + u"Oríon algo robo Documentation", + author, + "robo", + algo_plugin.__descr__, + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# -- Autodoc configuration ----------------------------------------------- + +autodoc_mock_imports = ["_version", "utils._appdirs"] + +################################################################################ +# Numpy Doc Extension # +################################################################################ + +# sphinx.ext.autosummary will automatically be loaded as well. So: +# autosummary_generate = glob.glob("reference/*.rst") + +# Generate ``plot::`` for ``Examples`` sections which contain matplotlib +numpydoc_use_plots = False + +# Create a Sphinx table of contents for the lists of class methods and +# attributes. If a table of contents is made, Sphinx expects each entry to have +# a separate page. +numpydoc_class_members_toctree = False + +numpydoc_show_inherited_class_members = False diff --git a/docs/src/index.rst b/docs/src/index.rst new file mode 100644 index 0000000..2df8463 --- /dev/null +++ b/docs/src/index.rst @@ -0,0 +1,17 @@ +.. include:: ../../README.rst + +.. toctree:: + :caption: Welcome + + Introduction + +.. toctree:: + :caption: Algorithms + + usage + +.. toctree:: + :caption: External links + + Oríon documentation + Other algorithms diff --git a/docs/src/usage.rst b/docs/src/usage.rst new file mode 100644 index 0000000..ed29f7b --- /dev/null +++ b/docs/src/usage.rst @@ -0,0 +1,127 @@ +.. _robo_gp: + +RoBO Gaussian Process +--------------------- + +.. code-block:: yaml + + experiment: + algorithms: + RoBO_GP: + seed: 0 + n_initial_points: 20 + maximizer: 'random' + acquisition_func: 'log_ei' + normalize_input: True + normalize_output: False + + +.. autoclass:: orion.algo.robo.gp.RoBO_GP + + +.. _robo_gp_mcmc: + +RoBO Gaussian Process with MCMC +------------------------------- + +.. code-block:: yaml + + experiment: + algorithms: + RoBO_GP_MCMC: + seed: 0 + n_initial_points: 20 + maximizer: 'random' + acquisition_func: 'log_ei' + normalize_input: True + normalize_output: False + chain_length: 2000 + burnin_steps: 2000 + + +.. autoclass:: orion.algo.robo.gp.RoBO_GP_MCMC + :exclude-members: build_acquisition_func + +.. _robo_random_forest: + +RoBO Random Forest +------------------ + +.. code-block:: yaml + + experiment: + algorithms: + RoBO_RandomForest: + seed: 0 + n_initial_points: 20 + maximizer: 'random' + acquisition_func: 'log_ei' + num_trees: 30 + do_bootstrapping: True + n_points_per_tree: 0 + compute_oob_error: False + return_total_variance: True + + +.. autoclass:: orion.algo.robo.randomforest.RoBO_RandomForest + :exclude-members: build_acquisition_func + +.. _robo_dngo: + +RoBO DNGO +--------- + +.. code-block:: yaml + + experiment: + algorithms: + RoBO_DNGO: + seed: 0 + n_initial_points: 20 + maximizer: 'random' + acquisition_func: 'log_ei' + normalize_input: True + normalize_output: False + chain_length: 2000 + burnin_steps: 2000 + batch_size: 10 + num_epochs: 500 + learning_rate: 1e-2 + adapt_epoch: 5000 + + +.. autoclass:: orion.algo.robo.dngo.RoBO_DNGO + :exclude-members: build_acquisition_func + + +.. _robo_bohamiann: + + +RoBO BOHAMIANN +-------------- + +.. code-block:: yaml + + experiment: + algorithms: + RoBO_BOHAMIANN: + seed: 0 + n_initial_points: 20 + maximizer: 'random' + acquisition_func: 'log_ei' + normalize_input: True + normalize_output: False + burnin_steps: 2000 + sampling_method: "adaptive_sghmc" + use_double_precision: True + num_steps: null + keep_every: 100 + learning_rate: 1e-2 + batch_size: 20 + epsilon: 1e-10 + mdecay: 0.05 + verbose: False + + +.. autoclass:: orion.algo.robo.bohamiann.RoBO_BOHAMIANN + :exclude-members: build_acquisition_func diff --git a/tox.ini b/tox.ini index 18b6012..1769a25 100644 --- a/tox.ini +++ b/tox.ini @@ -131,6 +131,33 @@ usedevelop = True commands = python setup.py test --addopts '-vvv --exitfirst --looponfail {posargs}' +[testenv:doc8] +description = Impose standards on *.rst documentation files +basepython = python3 +skip_install = true +deps = + doc8 == 0.8.* +commands = + doc8 docs/ + +[testenv:docs] +description = Invoke sphinx to build documentation and API reference +basepython = python3 +deps = + -rrequirements.txt + -rdocs/requirements.txt +commands = + sphinx-build -b html -d build/doctrees -nWT docs/src/ docs/build/html + +[testenv:serve-docs] +description = Host the documentation of the project and API reference in localhost +basepython = python3 +skip_install = true +changedir = docs/build/html +deps = +commands = + python -m http.server 8000 --bind 127.0.0.1 + ## Release tooling (to be removed in favor of CI with CD) [testenv:build] @@ -177,3 +204,9 @@ exclude_lines = pass raise AssertionError raise NotImplementedError + + +# Doc8 configuration +[doc8] +max-line-length = 100 +file-encoding = utf-8 From 1487b2563e8d717b2b06efa7be7287bda717018e Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:06:28 -0400 Subject: [PATCH 02/30] Add many algorithms --- .github/workflows/build.yml | 4 +- setup.py | 8 +- src/orion/algo/robo/base.py | 432 ++++++++++++++++++++++++++++ src/orion/algo/robo/bohamiann.py | 268 +++++++++++++++++ src/orion/algo/robo/dngo.py | 200 +++++++++++++ src/orion/algo/robo/gp.py | 289 +++++++++++++++++++ src/orion/algo/robo/randomforest.py | 160 +++++++++++ tests/integration_test.py | 230 --------------- tests/test_integration.py | 242 ++++++++++++++++ 9 files changed, 1599 insertions(+), 234 deletions(-) create mode 100644 src/orion/algo/robo/base.py create mode 100644 src/orion/algo/robo/bohamiann.py create mode 100644 src/orion/algo/robo/dngo.py create mode 100644 src/orion/algo/robo/gp.py create mode 100644 src/orion/algo/robo/randomforest.py delete mode 100644 tests/integration_test.py create mode 100644 tests/test_integration.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72ba0f8..10cda30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toxenv: [black, isort, pylint] + toxenv: [black, isort, pylint, doc8, docs] steps: - uses: actions/checkout@v1 @@ -36,7 +36,7 @@ jobs: max-parallel: 4 matrix: platform: [ubuntu-latest] - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] env: PLATFORM: ${{ matrix.platform }} steps: diff --git a/setup.py b/setup.py index cce6b23..7ef79b0 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,11 @@ include_package_data=True, entry_points={ "OptimizationAlgorithm": [ - "robo = orion.algo.robo.rbayes:RoBO", + "robo_gp = orion.algo.robo.gp:RoBO_GP", + "robo_gp_mcmc = orion.algo.robo.gp:RoBO_GP_MCMC", + "robo_randomforest = orion.algo.robo.randomforest:RoBO_RandomForest", + "robo_dngo = orion.algo.robo.dngo:RoBO_DNGO", + "robo_bohamiann= orion.algo.robo.bohamiann:RoBO_BOHAMIANN", ], }, install_requires=["orion>=0.1.11", "numpy", "torch>=1.2.0"], @@ -58,7 +62,7 @@ "Operating System :: Unix", "Programming Language :: Python", "Topic :: Scientific/Engineering", -] + [("Programming Language :: Python :: %s" % x) for x in "3 3.6 3.7 3.8".split()] +] + [("Programming Language :: Python :: %s" % x) for x in "3 3.6 3.7 3.8 3.9".split()] if __name__ == "__main__": setup(**setup_args) diff --git a/src/orion/algo/robo/base.py b/src/orion/algo/robo/base.py new file mode 100644 index 0000000..eb82ab5 --- /dev/null +++ b/src/orion/algo/robo/base.py @@ -0,0 +1,432 @@ +import george +import numpy +from orion.algo.base import BaseAlgorithm +from robo.acquisition_functions.ei import EI +from robo.acquisition_functions.lcb import LCB +from robo.acquisition_functions.log_ei import LogEI +from robo.acquisition_functions.marginalization import MarginalizationGPMCMC +from robo.acquisition_functions.pi import PI +from robo.initial_design import init_latin_hypercube_sampling +from robo.maximizers.differential_evolution import DifferentialEvolution +from robo.maximizers.random_sampling import RandomSampling +from robo.maximizers.scipy_optimizer import SciPyOptimizer +from robo.priors.default_priors import DefaultPrior +from robo.solver.bayesian_optimization import BayesianOptimization + + +def build_bounds(space): + """ + Build bounds of optimization space + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Search space for the optimization. + + """ + lower = [] + upper = [] + for dim in space.values(): + low, high = dim.interval() + + shape = dim.shape + assert not shape or shape == [1] + + lower.append(low) + upper.append(high) + + return list(map(numpy.array, (lower, upper))) + + +def build_kernel(lower, upper): + """ + Build kernels for GPs. + + Parameters + ---------- + lower: numpy.ndarray (D,) + The lower bound of the search space + upper: numpy.ndarray (D,) + The upper bound of the search space + """ + + assert upper.shape[0] == lower.shape[0], "Dimension miss match" + assert numpy.all(lower < upper), "Lower bound >= upper bound" + + cov_amp = 2 + n_dims = lower.shape[0] + + initial_ls = numpy.ones([n_dims]) + exp_kernel = george.kernels.Matern52Kernel(initial_ls, ndim=n_dims) + kernel = cov_amp * exp_kernel + + return kernel + + +def infer_n_hypers(kernel): + """Infer number of MCMC chains that should be used based on size of kernel""" + n_hypers = 3 * len(kernel) + if n_hypers % 2 == 1: + n_hypers += 1 + + return n_hypers + + +def build_prior(kernel): + """Build default GP prior based on kernel""" + return DefaultPrior(len(kernel) + 1, numpy.random.RandomState(None)) + + +def build_acquisition_func(acquisition_func, model): + """ + Build acquisition function + + Parameters + ---------- + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + model: ``robo.models.base_model.BaseModel`` + Model used for the Bayesian optimization. + + """ + if acquisition_func == "ei": + acquisition_func = EI(model) + elif acquisition_func == "log_ei": + acquisition_func = LogEI(model) + elif acquisition_func == "pi": + acquisition_func = PI(model) + elif acquisition_func == "lcb": + acquisition_func = LCB(model) + else: + raise ValueError( + "'{}' is not a valid acquisition function".format(acquisition_func) + ) + + return acquisition_func + + +def build_optimizer(model, maximizer, acquisition_func): + """ + General interface for Bayesian optimization for global black box + optimization problems. + + Parameters + ---------- + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}`` + acquisition_func: + The instantiated acquisition function + + Returns + ------- + Optimizer + + """ + maximizer_rng = numpy.random.RandomState(maximizer_seed) + if maximizer == "random": + max_func = RandomSampling( + acquisition_func, model.lower, model.upper, rng=maximizer_rng + ) + elif maximizer == "scipy": + max_func = SciPyOptimizer( + acquisition_func, model.lower, model.upper, rng=maximizer_rng + ) + elif maximizer == "differential_evolution": + max_func = DifferentialEvolution( + acquisition_func, model.lower, model.upper, rng=maximizer_rng + ) + else: + raise ValueError( + "'{}' is not a valid function to maximize the " + "acquisition function".format(maximizer) + ) + + # NOTE: Internal RNG of BO won't be used. + # NOTE: Nb of initial points won't be used within BO, but rather outside + bo = BayesianOptimization( + lambda: None, + model.lower, + model.upper, + acquisition_func, + model, + max_func, + initial_points=None, + rng=None, + initial_design=init_latin_hypercube_sampling, + output_path=None, + ) + + return bo + + +class RoBO(BaseAlgorithm): + """ + Base class to wrap RoBO algorithms. + + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Optimisation space with priors for each dimension. + seed: None, int or sequence of int + Seed to sample initial points and candidates points. + Default: 0. + n_initial_points: int + Number of initial points randomly sampled. If new points + are requested and less than `n_initial_points` are observed, + the next points will also be sampled randomly instead of being + sampled from the parzen estimators. + Default: ``20`` + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}``. + Defaults to 'random' + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + **kwargs: + Arguments specific to each RoBO algorithms. These will be registered as part of + the algorithm's configuration. + + """ + + requires_type = "real" + requires_dist = "linear" + requires_shape = "flattened" + + # pylint:disable=too-many-arguments + def __init__( + self, + space, + seed=0, + n_init=20, + maximizer="random", + acquisition_func="log_ei", + **kwargs, + ): + + self.model = None + self.robo = None + self._bo_duplicates = [] + + super(RoBO, self).__init__( + space, + n_init=n_init, + maximizer=maximizer, + acquisition_func=acquisition_func, + seed=seed, + ) + + # Otherwise it is turned no 'random' because of BaseAlgorithms constructor... -_- + self.maximizer = maximizer + + self._param_names += list(kwargs.keys()) + for key, value in kwargs.items(): + setattr(self, key, value) + + @property + def space(self): + """Space of the optimizer""" + return self._space + + @space.setter + def space(self, space): + """Setter of optimizer's space. + + Side-effect: Will initialize optimizer. + """ + self._original = self._space + self._space = space + self._initialize() + + def _initialize_model(self): + """Build model and register it as ``self.model``""" + raise NotImplementedError() + + def build_acquisition_func(self): + """Build and return the acquisition function.""" + return build_acquisition_func(self.acquisition_func, self.model) + + def _initialize(self): + """Initialize the optimizer once the space is transformed""" + self._initialize_model() + self.robo = build_optimizer( + self.model, + maximizer=self.maximizer, + acquisition_func=self.build_acquisition_func(), + ) + + self.seed_rng(self.seed) + + # pylint:disable=invalid-name + @property + def X(self): + """Matrix containing trial points""" + ref_point = self.space.sample(1, seed=0)[0] + points = list(self._trials_info.values()) + self._bo_duplicates + points = list(filter(lambda point: point[1] is not None, points)) + X = numpy.zeros((len(points), len(ref_point))) + for i, (point, _result) in enumerate(points): + X[i] = point + + return X + + # pylint:disable=invalid-name + @property + def y(self): + """Vector containing trial results""" + points = list(self._trials_info.values()) + self._bo_duplicates + points = list(filter(lambda point: point[1] is not None, points)) + y = numpy.zeros(len(points)) + for i, (_point, result) in enumerate(points): + y[i] = result["objective"] + + return y + + def seed_rng(self, seed): + """Seed the state of the random number generator. + + Parameters + ---------- + seed: int + Integer seed for the random number generator. + + """ + self.rng = numpy.random.RandomState(seed) + + rand_nums = self.rng.randint(1, 10e8, 4) + + if self.robo: + self.robo.rng = numpy.random.RandomState(rand_nums[0]) + self.robo.maximize_func.rng.seed(rand_nums[1]) + + if self.model: + self.model.seed(rand_nums[2]) + + numpy.random.seed(rand_nums[3]) + + @property + def state_dict(self): + """Return a state dict that can be used to reset the state of the algorithm.""" + s_dict = super(RoBO, self).state_dict + + s_dict.update( + { + "rng_state": self.rng.get_state(), + "global_numpy_rng_state": numpy.random.get_state(), + "maximizer_rng_state": self.robo.maximize_func.rng.get_state(), + "bo_duplicates": self._bo_duplicates, + } + ) + + s_dict["model"] = self.model.state_dict() + + return s_dict + + def set_state(self, state_dict): + """Reset the state of the algorithm based on the given state_dict + + :param state_dict: Dictionary representing state of an algorithm + + """ + super(RoBO, self).set_state(state_dict) + + self.rng.set_state(state_dict["rng_state"]) + numpy.random.set_state(state_dict["global_numpy_rng_state"]) + self.robo.maximize_func.rng.set_state(state_dict["maximizer_rng_state"]) + self.model.set_state(state_dict["model"]) + self._bo_duplicates = state_dict["bo_duplicates"] + + def suggest(self, num=None): + """Suggest a `num`ber of new sets of parameters. + + Perform a step towards negative gradient and suggest that point. + + """ + num = min(num, max(self.n_init - self.n_suggested, 1)) + + samples = [] + candidates = [] + while len(samples) < num: + if candidates: + candidate = candidates.pop(0) + if candidate: + self.register(candidate) + samples.append(candidate) + elif self.n_observed < self.n_init: + candidates = self._suggest_random(num) + else: + candidates = self._suggest_bo(max(num - len(samples), 0)) + + if not candidates: + break + + return samples + + def _suggest(self, num, function): + points = [] + + attempts = 0 + max_attempts = 100 + while len(points) < num and attempts < max_attempts: + for candidate in function(num - len(points)): + if not self.has_suggested(candidate): + self.register(candidate) + points.append(candidate) + + if self.is_done: + return points + + attempts += 1 + print(attempts) + + return points + + def _suggest_random(self, num): + def sample(num): + return self.space.sample( + num, seed=tuple(self.rng.randint(0, 1000000, size=3)) + ) + + return self._suggest(num, sample) + + def _suggest_bo(self, num): + # pylint: disable = unused-argument + def suggest_bo(num): + # pylint: disable = protected-access + point = list(self.robo.choose_next(self.X, self.y)) + + # If already suggested, give corresponding result to BO to sample another point + if self.has_suggested(point): + result = self._trials_info[self.get_id(point)][1] + if result is None: + results = [] + for _, other_result in self._trials_info.values(): + if other_result is not None: + results.append(other_result["objective"]) + result = {"objective": numpy.array(results).mean()} + + self._bo_duplicates.append((point, result)) + + # self.optimizer.tell([point], [result]) + return [] + + return [point] + + return self._suggest(num, suggest_bo) + + @property + def is_done(self): + """Whether the algorithm is done and will not make further suggestions. + + Return True, if an algorithm holds that there can be no further improvement. + By default, the cardinality of the specified search space will be used to check + if all possible sets of parameters has been tried. + """ + if self.n_suggested >= self._original.cardinality: + return True + + if self.n_suggested >= getattr(self, "max_trials", float("inf")): + return True + + return False diff --git a/src/orion/algo/robo/bohamiann.py b/src/orion/algo/robo/bohamiann.py new file mode 100644 index 0000000..f523f0f --- /dev/null +++ b/src/orion/algo/robo/bohamiann.py @@ -0,0 +1,268 @@ +import numpy +import torch +from pybnn.bohamiann import Bohamiann +from robo.models.base_model import BaseModel +from robo.models.wrapper_bohamiann import get_default_network + +from orion.algo.robo.base import RoBO, build_bounds, build_kernel, infer_n_hypers + + +class RoBO_BOHAMIANN(RoBO): + """ + Wrapper for RoBO with BOHAMIANN + + For more information on the algorithm, see original paper at + https://papers.nips.cc/paper/2016/hash/a96d3afec184766bfeca7a9f989fc7e7-Abstract.html. + + Springenberg, Jost Tobias, et al. + "Bayesian optimization with robust Bayesian neural networks." + Advances in neural information processing systems 29 (2016): 4134-4142. + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Optimisation space with priors for each dimension. + seed: None, int or sequence of int + Seed to sample initial points and candidates points. + Default: 0. + n_initial_points: int + Number of initial points randomly sampled. If new points + are requested and less than `n_initial_points` are observed, + the next points will also be sampled randomly instead of being + sampled from the parzen estimators. + Default: ``20`` + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}``. + Defaults to 'random' + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + normalize_input: bool + Normalize the input based on the provided bounds (zero mean and unit standard deviation). + Defaults to ``True``. + normalize_output: bool + Normalize the output based on data (zero mean and unit standard deviation). + Defaults to ``False``. + burnin_steps: int or None. + The number of burnin steps before the sampling procedure starts. + If ``None``, ``burnin_steps = n_dims * 100`` where ``n_dims`` is the dimensionality + of the search space. Defaults to ``None``. + sampling_method: str + Can be one of ``['adaptive_sghmc', 'sgld', 'preconditioned_sgld', 'sghmc']``. + Defaults to ``"adaptive_sghmc"``. See PyBNN samplers' + `code `_ for more information. + use_double_precision: bool + Use double precision if using ``bohamiann``. Note that it can run faster on GPU + if using single precision. Defaults to ``True``. + num_steps: int or None + Number of sampling steps to perform after burn-in is finished. + In total, ``num_steps // keep_every`` network weights will be sampled. + If ``None``, ``num_steps = n_dims * 100 + 10000`` where ``n_dims`` is the + dimensionality of the search space. + keep_every: int + Number of sampling steps (after burn-in) to perform before keeping a sample. + In total, ``num_steps // keep_every`` network weights will be sampled. + learning_rate: float + Learning rate. Defaults to 1e-2. + batch_size: int + Batch size for training the neural network. Defaults to 20. + epsilon: float + epsilon for numerical stability. Defaults to 1e-10. + mdecay: float + momemtum decay. Defaults to 0.05. + verbose: bool + Write progress logs in stdout. Defaults to ``False``. + + """ + + def __init__( + self, + space, + seed=0, + n_initial_points=20, + maximizer="random", + acquisition_func="log_ei", + normalize_input=True, + normalize_output=False, + burnin_steps=None, + sampling_method="adaptive_sghmc", + use_double_precision=True, + num_steps=None, + keep_every=100, + learning_rate=1e-2, + batch_size=20, + epsilon=1e-10, + mdecay=0.05, + verbose=False, + ): + + super(RoBO_BOHAMIANN, self).__init__( + space, + maximizer=maximizer, + acquisition_func=acquisition_func, + normalize_input=normalize_input, + normalize_output=normalize_output, + burnin_steps=burnin_steps, + sampling_method=sampling_method, + use_double_precision=use_double_precision, + num_steps=num_steps, + keep_every=keep_every, + learning_rate=learning_rate, + batch_size=batch_size, + epsilon=epsilon, + mdecay=mdecay, + verbose=verbose, + n_initial_points=n_initial_points, + seed=seed, + ) + + def _initialize_model(self): + lower, upper = build_bounds(self.space) + self.model = OrionBohamiannWrapper( + normalize_input=self.normalize_input, + normalize_output=self.normalize_output, + burnin_steps=self.burnin_steps, + sampling_method=self.sampling_method, + use_double_precision=self.use_double_precision, + num_steps=self.num_steps, + keep_every=self.keep_every, + learning_rate=self.learning_rate, + batch_size=self.batch_size, + epsilon=self.epsilon, + mdecay=self.mdecay, + verbose=self.verbose, + lower=lower, + upper=upper, + ) + + +class OrionBohamiannWrapper(BaseModel): + """ + Wrapper for PyBNN's BOHAMIANN model + + Parameters + ---------- + normalize_input: bool + Normalize the input based on the provided bounds (zero mean and unit standard deviation). + Defaults to ``True``. + normalize_output: bool + Normalize the output based on data (zero mean and unit standard deviation). + Defaults to ``False``. + burnin_steps: int or None. + The number of burnin steps before the sampling procedure starts. + If ``None``, ``burnin_steps = n_dims * 100`` where ``n_dims`` is the dimensionality + of the search space. Defaults to ``None``. + sampling_method: str + Can be one of ``['adaptive_sghmc', 'sgld', 'preconditioned_sgld', 'sghmc']``. + Defaults to ``"adaptive_sghmc"``. See PyBNN samplers' + `code `_ for more information. + use_double_precision: bool + Use double precision if using ``bohamiann``. Note that it can run faster on GPU + if using single precision. Defaults to ``True``. + num_steps: int or None + Number of sampling steps to perform after burn-in is finished. + In total, ``num_steps // keep_every`` network weights will be sampled. + If ``None``, ``num_steps = n_dims * 100 + 10000`` where ``n_dims`` is the + dimensionality of the search space. + keep_every: int + Number of sampling steps (after burn-in) to perform before keeping a sample. + In total, ``num_steps // keep_every`` network weights will be sampled. + learning_rate: float + Learning rate. Defaults to 1e-2. + batch_size: int + Batch size for training the neural network. Defaults to 20. + epsilon: float + epsilon for numerical stability. Defaults to 1e-10. + mdecay: float + momemtum decay. Defaults to 0.05. + verbose: bool + Write progress logs in stdout. Defaults to ``False``. + + """ + + def __init__( + self, + lower, + upper, + sampling_method="adaptive_sghmc", + use_double_precision=True, + num_steps=None, + keep_every=100, + burnin_steps=None, + learning_rate=1e-2, + batch_size=20, + epsilon=1e-10, + mdecay=0.05, + verbose=False, + **kwargs + ): + + self.num_steps = num_steps + self.keep_every = keep_every + self.burnin_steps = burnin_steps + self.learning_rate = learning_rate # pylint:disable=invalid-name + self.batch_size = batch_size + self.epsilon = epsilon + self.mdecay = mdecay + self.verbose = verbose + + self.bnn = Bohamiann( + get_network=get_default_network, + sampling_method=sampling_method, + use_double_precision=use_double_precision, + **kwargs + ) + self.burnin_steps = burnin_steps + + self.lower = lower + self.upper = upper + + # pylint:disable=no-self-use + def set_state(self, state_dict): + """Restore the state of the optimizer""" + torch.random.set_rng_state(state_dict["torch"]) + + # pylint:disable=no-self-use + def state_dict(self): + """Return the current state of the optimizer so that it can be restored""" + return {"torch": torch.random.get_rng_state()} + + def seed(self, seed): + """Seed all internal RNGs""" + if torch.cuda.is_available(): + torch.backends.cudnn.benchmark = False + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + + torch.manual_seed(seed) + + def train(self, X, y, **kwargs): + self.X = X + self.y = y + + if self.num_steps: + num_steps = self.num_steps + else: + num_steps = X.shape[0] * 100 + 10000 + + if self.burnin_steps is None: + burnin_steps = X.shape[0] * 100 + else: + burnin_steps = self.burnin_steps + + self.bnn.train( + X, + y, + num_steps=num_steps, + keep_every=self.keep_every, + num_burn_in_steps=burnin_steps, + lr=self.learning_rate, + batch_size=self.batch_size, + epsilon=self.epsilon, + mdecay=self.mdecay, + continue_training=False, + verbose=self.verbose, + ) + + def predict(self, X_test): + return self.bnn.predict(X_test) diff --git a/src/orion/algo/robo/dngo.py b/src/orion/algo/robo/dngo.py new file mode 100644 index 0000000..147345e --- /dev/null +++ b/src/orion/algo/robo/dngo.py @@ -0,0 +1,200 @@ +import numpy +import torch + +from pybnn.dngo import DNGO + +from orion.algo.robo.base import RoBO, build_bounds, build_kernel, infer_n_hypers + + +class RoBO_DNGO(RoBO): + """ + Wrapper for RoBO with DNGO + + For more information on the algorithm, + see original paper at http://proceedings.mlr.press/v37/snoek15.html. + + J. Snoek, O. Rippel, K. Swersky, R. Kiros, N. Satish, + N. Sundaram, M.~M.~A. Patwary, Prabhat, R.~P. Adams + Scalable Bayesian Optimization Using Deep Neural Networks + Proc. of ICML'15 + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Optimisation space with priors for each dimension. + seed: None, int or sequence of int + Seed to sample initial points and candidates points. + Default: 0. + n_initial_points: int + Number of initial points randomly sampled. If new points + are requested and less than `n_initial_points` are observed, + the next points will also be sampled randomly instead of being + sampled from the parzen estimators. + Default: ``20`` + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}``. + Defaults to 'random' + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + normalize_input: bool + Normalize the input based on the provided bounds (zero mean and unit standard deviation). + Defaults to ``True``. + normalize_output: bool + Normalize the output based on data (zero mean and unit standard deviation). + Defaults to ``False``. + chain_length : int + The chain length of the MCMC sampler + burnin_steps: int + The number of burnin steps before the sampling procedure starts + batch_size: int + Batch size for training the neural network + num_epochs: int + Number of epochs for training + learning_rate: float + Initial learning rate for Adam + adapt_epoch: int + Defines after how many epochs the learning rate will be decayed by a factor 10 + + """ + + def __init__( + self, + space, + seed=0, + n_initial_points=20, + maximizer="random", + acquisition_func="log_ei", + normalize_input=True, + normalize_output=False, + chain_length=2000, + burnin_steps=2000, + batch_size=10, + num_epochs=500, + learning_rate=1e-2, + adapt_epoch=5000, + ): + + super(RoBO_DNGO, self).__init__( + space, + seed=seed, + n_initial_points=n_initial_points, + maximizer=maximizer, + acquisition_func=acquisition_func, + normalize_input=normalize_input, + normalize_output=normalize_output, + chain_length=chain_length, + burnin_steps=burnin_steps, + batch_size=batch_size, + num_epochs=num_epochs, + learning_rate=learning_rate, + adapt_epoch=adapt_epoch, + ) + + def _initialize_model(self): + lower, upper = build_bounds(self.space) + n_hypers = infer_n_hypers(build_kernel(lower, upper)) + self.model = OrionDNGOWrapper( + batch_size=self.batch_size, + num_epochs=self.num_epochs, + learning_rate=self.learning_rate, + adapt_epoch=self.adapt_epoch, + n_units_1=50, + n_units_2=50, + n_units_3=50, + alpha=1.0, + beta=1000, + prior=None, + do_mcmc=True, + n_hypers=n_hypers, + chain_length=self.chain_length, + burnin_steps=self.burnin_steps, + normalize_input=self.normalize_input, + normalize_output=self.normalize_output, + rng=None, + lower=lower, + upper=upper, + ) + + +class OrionDNGOWrapper(DNGO): + """ + Wrapper for PyBNN's DNGO model + + Parameters + ---------- + batch_size: int + Batch size for training the neural network + num_epochs: int + Number of epochs for training + learning_rate: float + Initial learning rate for Adam + adapt_epoch: int + Defines after how many epochs the learning rate will be decayed by a factor 10 + n_units_1: int + Number of units in layer 1 + n_units_2: int + Number of units in layer 2 + n_units_3: int + Number of units in layer 3 + alpha: float + Hyperparameter of the Bayesian linear regression + beta: float + Hyperparameter of the Bayesian linear regression + prior: Prior object + Prior for alpa and beta. If set to None the default prior is used + do_mcmc: bool + If set to true different values for alpha and beta are sampled via MCMC from the marginal log likelihood + Otherwise the marginal log likehood is optimized with scipy fmin function + n_hypers : int + Number of samples for alpha and beta + chain_length : int + The chain length of the MCMC sampler + burnin_steps: int + The number of burnin steps before the sampling procedure starts + normalize_output : bool + Zero mean unit variance normalization of the output values + normalize_input : bool + Zero mean unit variance normalization of the input values + rng: np.random.RandomState + Random number generator + + """ + + def __init__(self, lower, upper, **kwargs): + + super(OrionDNGOWrapper, self).__init__(**kwargs) + self.lower = lower + self.upper = upper + + # pylint:disable=no-self-use + def set_state(self, state_dict): + """Restore the state of the optimizer""" + torch.random.set_rng_state(state_dict["torch"]) + self.rng.set_state(state_dict["rng"]) + self.prior.rng.set_state(state_dict["prior_rng"]) + + # pylint:disable=no-self-use + def state_dict(self): + """Return the current state of the optimizer so that it can be restored""" + return { + "torch": torch.random.get_rng_state(), + "rng": self.rng.get_state(), + "prior_rng": self.prior.rng.get_state(), + } + + def seed(self, seed): + """Seed all internal RNGs""" + rng = numpy.random.RandomState(seed) + rand_nums = self.rng.randint(1, 10e8, 3) + pytorch_seed = rand_nums[0] + + if torch.cuda.is_available(): + torch.backends.cudnn.benchmark = False + torch.cuda.manual_seed_all(pytorch_seed) + torch.backends.cudnn.deterministic = True + + torch.manual_seed(pytorch_seed) + + self.rng.seed(rand_nums[1]) + self.prior.rng.seed(rand_nums[2]) diff --git a/src/orion/algo/robo/gp.py b/src/orion/algo/robo/gp.py new file mode 100644 index 0000000..a850ea3 --- /dev/null +++ b/src/orion/algo/robo/gp.py @@ -0,0 +1,289 @@ +import numpy + +from robo.acquisition_functions.marginalization import MarginalizationGPMCMC +from robo.models.gaussian_process import GaussianProcess +from robo.models.gaussian_process_mcmc import GaussianProcessMCMC + +from orion.algo.robo.base import ( + RoBO, + build_acquisition_func, + build_bounds, + build_kernel, + build_prior, + infer_n_hypers, +) + + +class RoBO_GP(RoBO): + """ + Wrapper for RoBO with Gaussian processes + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Optimisation space with priors for each dimension. + seed: None, int or sequence of int + Seed to sample initial points and candidates points. + Default: 0. + n_initial_points: int + Number of initial points randomly sampled. If new points + are requested and less than `n_initial_points` are observed, + the next points will also be sampled randomly instead of being + sampled from the parzen estimators. + Default: ``20`` + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}``. + Defaults to 'random' + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + normalize_input: bool + Normalize the input based on the provided bounds (zero mean and unit standard deviation). + Defaults to ``True``. + normalize_output: bool + Normalize the output based on data (zero mean and unit standard deviation). + Defaults to ``False``. + + """ + + def __init__( + self, + space, + seed=0, + n_initial_points=20, + maximizer="random", + acquisition_func="log_ei", + normalize_input=True, + normalize_output=False, + ): + + super(RoBO_GP, self).__init__( + space, + maximizer=maximizer, + acquisition_func=acquisition_func, + normalize_input=normalize_input, + normalize_output=normalize_output, + n_initial_points=n_initial_points, + seed=seed, + ) + + def _initialize_model(self): + lower, upper = build_bounds(self.space) + kernel = build_kernel(lower, upper) + prior = build_prior(kernel) + self.model = OrionGaussianProcessWrapper( + kernel, + prior=prior, + rng=None, + normalize_input=self.normalize_input, + normalize_output=self.normalize_output, + lower=lower, + upper=upper, + ) + + +class RoBO_GP_MCMC(RoBO): + """ + Wrapper for RoBO with Gaussian processes using Markov chain Monte Carlo + to marginalize out hyperparameters of the Bayesian Optimization. + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Optimisation space with priors for each dimension. + seed: None, int or sequence of int + Seed to sample initial points and candidates points. + Default: 0. + n_initial_points: int + Number of initial points randomly sampled. If new points + are requested and less than `n_initial_points` are observed, + the next points will also be sampled randomly instead of being + sampled from the parzen estimators. + Default: ``20`` + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}``. + Defaults to 'random' + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + normalize_input: bool + Normalize the input based on the provided bounds (zero mean and unit standard deviation). + Defaults to ``True``. + normalize_output: bool + Normalize the output based on data (zero mean and unit standard deviation). + Defaults to ``False``. + chain_length: int + The length of the MCMC chain. We start ``n_hypers`` walker for chain_length + steps and we use the last sample in the chain as a hyperparameter sample. + ``n_hypers`` is automatically infered based on dimensionality of the search space. + Defaults to 2000. + burnin_steps: int + The number of burnin steps before the actual MCMC sampling starts. + Defaults to 2000. + + """ + + def __init__( + self, + space, + seed=0, + n_initial_points=20, + maximizer="random", + acquisition_func="log_ei", + normalize_input=True, + normalize_output=False, + chain_length=2000, + burnin_steps=2000, + ): + + super(RoBO_GP_MCMC, self).__init__( + space, + seed=seed, + n_initial_points=n_initial_points, + maximizer=maximizer, + acquisition_func=acquisition_func, + normalize_input=normalize_input, + normalize_output=normalize_output, + chain_length=chain_length, + burnin_steps=burnin_steps, + ) + + def build_acquisition_func(self): + """Build a marginalized acquisition function with MCMC.""" + return MarginalizationGPMCMC(super(RoBO_GP_MCMC, self).build_acquisition_func()) + + def _initialize_model(self): + lower, upper = build_bounds(self.space) + kernel = build_kernel(lower, upper) + prior = build_prior(kernel) + n_hypers = infer_n_hypers(kernel) + self.model = OrionGaussianProcessMCMCWrapper( + kernel, + prior=prior, + n_hypers=n_hypers, + chain_length=self.chain_length, + burnin_steps=self.burnin_steps, + normalize_input=self.normalize_input, + normalize_output=self.normalize_output, + rng=None, + lower=lower, + upper=upper, + ) + + +class OrionGaussianProcessWrapper(GaussianProcess): + """ + Wrapper for RoBO's Gaussian processes model + + Parameters + ---------- + kernel : george kernel object + Specifies the kernel that is used for all Gaussian Process + prior : prior object + Defines a prior for the hyperparameters of the GP. Make sure that + it implements the Prior interface. + noise : float + Noise term that is added to the diagonal of the covariance matrix + for the Cholesky decomposition. + use_gradients : bool + Use gradient information to optimize the negative log likelihood + lower : numpy.array(D,) + Lower bound of the input space which is used for the input space normalization + upper : numpy.array(D,) + Upper bound of the input space which is used for the input space normalization + normalize_output : bool + Zero mean unit variance normalization of the output values + normalize_input : bool + Normalize all inputs to be in [0, 1]. This is important to define good priors for the + length scales. + rng: numpy.random.RandomState + Random number generator + + """ + + # pylint:disable=attribute-defined-outside-init + def set_state(self, state_dict): + """Restore the state of the optimizer""" + self.rng.set_state(state_dict["model_rng_state"]) + self.prior.rng.set_state(state_dict["prior_rng_state"]) + self.kernel.set_parameter_vector(state_dict["model_kernel_parameter_vector"]) + self.noise = state_dict["noise"] + + def state_dict(self): + """Return the current state of the optimizer so that it can be restored""" + return { + "prior_rng_state": self.prior.rng.get_state(), + "model_rng_state": self.rng.get_state(), + "model_kernel_parameter_vector": self.kernel.get_parameter_vector().tolist(), + "noise": self.noise, + } + + def seed(self, seed): + """Seed all internal RNGs""" + seeds = numpy.random.RandomState(seed).randint(1, 10e8, size=2) + self.rng.seed(seeds[0]) + self.prior.rng.seed(seeds[1]) + + +class OrionGaussianProcessMCMCWrapper(GaussianProcessMCMC): + """ + Wrapper for RoBO's Gaussian processes with MCMC model + + Parameters + ---------- + kernel : george kernel object + Specifies the kernel that is used for all Gaussian Process + prior : prior object + Defines a prior for the hyperparameters of the GP. Make sure that + it implements the Prior interface. During MCMC sampling the + lnlikelihood is multiplied with the prior. + n_hypers : int + The number of hyperparameter samples. This also determines the + number of walker for MCMC sampling as each walker will + return one hyperparameter sample. + chain_length : int + The length of the MCMC chain. We start n_hypers walker for + chain_length steps and we use the last sample + in the chain as a hyperparameter sample. + lower : np.array(D,) + Lower bound of the input space which is used for the input space normalization + upper : np.array(D,) + Upper bound of the input space which is used for the input space normalization + burnin_steps : int + The number of burnin steps before the actual MCMC sampling starts. + rng: np.random.RandomState + Random number generator + + """ + + # pylint:disable=attribute-defined-outside-init + def set_state(self, state_dict): + """Restore the state of the optimizer""" + self.rng.set_state(state_dict["model_rng_state"]) + self.prior.rng.set_state(state_dict["prior_rng_state"]) + + if state_dict.get("model_p0", None) is not None: + # pylint:disable=invalid-name + self.p0 = numpy.array(state_dict["model_p0"]) + self.burned = True + elif hasattr(self, "p0"): + delattr(self, "p0") + self.burned = False + + def state_dict(self): + """Return the current state of the optimizer so that it can be restored""" + s_dict = { + "prior_rng_state": self.prior.rng.get_state(), + "model_rng_state": self.rng.get_state(), + } + + if hasattr(self, "p0"): + s_dict["model_p0"] = self.p0.tolist() + + return s_dict + + def seed(self, seed): + """Seed all internal RNGs""" + seeds = numpy.random.RandomState(seed).randint(1, 10e8, size=2) + self.rng.seed(seeds[0]) + self.prior.rng.seed(seeds[1]) diff --git a/src/orion/algo/robo/randomforest.py b/src/orion/algo/robo/randomforest.py new file mode 100644 index 0000000..a6f5f50 --- /dev/null +++ b/src/orion/algo/robo/randomforest.py @@ -0,0 +1,160 @@ +import numpy +import pyrfr.regression as reg +from robo.models.random_forest import RandomForest + +from orion.algo.robo.base import RoBO, build_bounds + + +class RoBO_RandomForest(RoBO): + """ + Wrapper for RoBO with + + Parameters + ---------- + space: ``orion.algo.space.Space`` + Optimisation space with priors for each dimension. + seed: None, int or sequence of int + Seed to sample initial points and candidates points. + Default: 0. + n_initial_points: int + Number of initial points randomly sampled. If new points + are requested and less than `n_initial_points` are observed, + the next points will also be sampled randomly instead of being + sampled from the parzen estimators. + Default: ``20`` + maximizer: str + The optimizer for the acquisition function. + Can be one of ``{"random", "scipy", "differential_evolution"}``. + Defaults to 'random' + acquisition_func: str + Name of the acquisition function. Can be one of ``['ei', 'log_ei', 'pi', 'lcb']``. + num_trees: int + The number of trees in the random forest. Defaults to 30. + do_bootstrapping: bool + Turns on / off bootstrapping in the random forest. Defaults to ``True``. + n_points_per_tree: int + Number of data point per tree. If set to 0 then we will use all data points in each tree. + Defaults to 0. + compute_oob_error: bool + Turns on / off calculation of out-of-bag error. Defaults to ``False``. + return_total_variance: bool + Return law of total variance (mean of variances + variance of means, if True) + or explained variance (variance of means, if False). Defaults to ``True``. + + """ + + def __init__( + self, + space, + seed=0, + n_initial_points=20, + maximizer="random", + acquisition_func="log_ei", + num_trees=30, + do_bootstrapping=True, + n_points_per_tree=0, + compute_oob_error=False, + return_total_variance=True, + ): + + super(RoBO_RandomForest, self).__init__( + space, + maximizer=maximizer, + acquisition_func=acquisition_func, + num_trees=num_trees, + do_bootstrapping=do_bootstrapping, + n_points_per_tree=n_points_per_tree, + compute_oob_error=compute_oob_error, + return_total_variance=return_total_variance, + n_initial_points=n_initial_points, + seed=seed, + ) + + def _initialize_model(self): + lower, upper = build_bounds(self.space) + self.model = OrionRandomForestWrapper( + rng=None, + num_trees=self.num_trees, + do_bootstrapping=self.do_bootstrapping, + n_points_per_tree=self.n_points_per_tree, + compute_oob_error=self.compute_oob_error, + return_total_variance=self.return_total_variance, + lower=lower, + upper=upper, + ) + + +class OrionRandomForestWrapper(RandomForest): + """ + Wrapper for RoBO's RandomForest model + + Parameters + ---------- + lower : np.array(D,) + Lower bound of the input space which is used for the input space normalization + upper : np.array(D,) + Upper bound of the input space which is used for the input space normalization + num_trees: int + The number of trees in the random forest. + do_bootstrapping: bool + Turns on / off bootstrapping in the random forest. + n_points_per_tree: int + Number of data point per tree. If set to 0 then we will use all data points in each tree + compute_oob_error: bool + Turns on / off calculation of out-of-bag error. Default: False + return_total_variance: bool + Return law of total variance (mean of variances + variance of means, if True) + or explained variance (variance of means, if False). Default: True + rng: np.random.RandomState + Random number generator + """ + + def __init__( + self, + lower, + upper, + num_trees=30, + do_bootstrapping=True, + n_points_per_tree=0, + compute_oob_error=False, + return_total_variance=True, + rng=None, + ): + + super(OrionRandomForestWrapper, self).__init__( + num_trees=num_trees, + do_bootstrapping=do_bootstrapping, + n_points_per_tree=n_points_per_tree, + compute_oob_error=compute_oob_error, + return_total_variance=return_total_variance, + rng=rng, + ) + + self.lower = lower + self.upper = upper + + def train(self, X, y, **kwargs): + # NOTE: We cannot save `reg_rng` state so instead we control it + # with random integers sampled from `rng` and keep track of `rng` state. + self.reg_rng = reg.default_random_engine(int(self.rng.randint(10e8))) + super(OrionRandomForestWrapper, self).train(X, y, **kwargs) + + def predict(self, X_test, **kwargs): + # NOTE: We cannot save `reg_rng` state so instead we control it + # with random integers sampled from `rng` and keep track of `rng` state. + self.reg_rng = reg.default_random_engine(int(self.rng.randint(10e8))) + return super(OrionRandomForestWrapper, self).predict(X_test, **kwargs) + + def set_state(self, state_dict): + """Restore the state of the optimizer""" + self.rng.set_state(state_dict["model_rng_state"]) + + def state_dict(self): + """Return the current state of the optimizer so that it can be restored""" + return { + "model_rng_state": self.rng.get_state(), + } + + def seed(self, seed): + """Seed all internal RNGs""" + self.rng.seed(seed) diff --git a/tests/integration_test.py b/tests/integration_test.py deleted file mode 100644 index 02e27ef..0000000 --- a/tests/integration_test.py +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# pylint:disable=invalid-name -"""Perform integration tests for `orion.algo.robo`.""" -import os - -import numpy -import orion.core.cli -import pytest -from orion.algo.space import Integer, Real, Space -from orion.client import create_experiment -from orion.core.worker.primary_algo import PrimaryAlgo -from orion.testing.state import OrionState - - -# pylint:disable=unused-argument -def rosenbrock_function(x, y): - """Evaluate a n-D rosenbrock function.""" - z = x - 34.56789 - r = 4 * z ** 2 + 23.4 - return [dict(name="objective", type="objective", value=r)] - - -MODEL_TYPES = ["gp", "gp_mcmc", "bohamiann"] - - -@pytest.fixture() -def space(): - """Return an optimization space""" - space = Space() - dim1 = Integer("yolo1", "uniform", -3, 6) - space.register(dim1) - dim2 = Real("yolo2", "uniform", 0, 1) - space.register(dim2) - - return space - - -@pytest.mark.parametrize("model_type", MODEL_TYPES) -def test_seeding(space, model_type, mocker): - """Verify that seeding makes sampling deterministic""" - optimizer = PrimaryAlgo(space, {"robo": {"model_type": model_type}}) - - optimizer.seed_rng(1) - a = optimizer.suggest(1)[0] - assert not numpy.allclose(a, optimizer.suggest(1)[0]) - - optimizer.seed_rng(1) - assert numpy.allclose(a, optimizer.suggest(1)[0]) - - -@pytest.mark.parametrize("model_type", MODEL_TYPES) -def test_seeding_bo(space, model_type, mocker): - """Verify that seeding BO makes sampling deterministic""" - n_init = 3 - optimizer = PrimaryAlgo( - space, {"robo": {"model_type": model_type, "n_init": n_init}} - ) - optimizer.seed_rng(1) - - spy = mocker.spy(optimizer.algorithm.robo, "choose_next") - - samples = [] - for i in range(n_init + 2): - a = optimizer.suggest(1)[0] - optimizer.observe([a], [{"objective": i / n_init}]) - samples.append([a]) - - assert spy.call_count == 2 - - optimizer = PrimaryAlgo( - space, {"robo": {"model_type": model_type, "n_init": n_init}} - ) - optimizer.seed_rng(1) - - spy = mocker.spy(optimizer.algorithm.robo, "choose_next") - - for i in range(n_init + 2): - b = optimizer.suggest(1)[0] - optimizer.observe([b], [{"objective": i / n_init}]) - samples[i].append(b) - - assert spy.call_count == 2 - - for pair in samples: - assert numpy.allclose(*pair) - - -@pytest.mark.parametrize("model_type", MODEL_TYPES) -def test_set_state(space, model_type): - """Verify that resetting state makes sampling deterministic""" - optimizer = PrimaryAlgo(space, {"robo": {"model_type": model_type}}) - - optimizer.seed_rng(1) - state = optimizer.state_dict - a = optimizer.suggest(1)[0] - assert not numpy.allclose(a, optimizer.suggest(1)[0]) - - optimizer.set_state(state) - assert numpy.allclose(a, optimizer.suggest(1)[0]) - - -@pytest.mark.parametrize("model_type", MODEL_TYPES) -def test_set_state_bo(space, model_type, mocker): - """Verify that resetting state during BO makes sampling deterministic""" - n_init = 3 - optimizer = PrimaryAlgo( - space, {"robo": {"model_type": model_type, "n_init": n_init}} - ) - - spy = mocker.spy(optimizer.algorithm.robo, "choose_next") - - for i in range(n_init + 2): - a = optimizer.suggest(1)[0] - optimizer.observe([a], [{"objective": i / (n_init + 2)}]) - - assert spy.call_count == 2 - - state = optimizer.state_dict - a = optimizer.suggest(1)[0] - assert not numpy.allclose(a, optimizer.suggest(1)[0]) - - optimizer.set_state(state) - assert numpy.allclose(a, optimizer.suggest(1)[0]) - - -def test_optimizer(monkeypatch): - """Check functionality of BayesianOptimizer wrapper for single shaped dimension.""" - monkeypatch.chdir(os.path.dirname(os.path.abspath(__file__))) - - with OrionState(experiments=[], trials=[]): - - orion.core.cli.main( - [ - "hunt", - "--name", - "exp", - "--max-trials", - "5", - "--config", - "./benchmark/robo.yaml", - "./benchmark/rosenbrock.py", - "-x~uniform(-5, 5)", - ] - ) - - -def test_int(monkeypatch): - """Check support of integer values.""" - monkeypatch.chdir(os.path.dirname(os.path.abspath(__file__))) - - with OrionState(experiments=[], trials=[]): - - orion.core.cli.main( - [ - "hunt", - "--name", - "exp", - "--max-trials", - "5", - "--config", - "./benchmark/robo.yaml", - "./benchmark/rosenbrock.py", - "-x~uniform(-5, 5, discrete=True)", - ] - ) - - -def test_categorical(): - """Check support of categorical values.""" - with OrionState(experiments=[], trials=[]): - - exp = create_experiment( - name="exp", - space={"x": "choices([-5, -2, 0, 2, 5])", "y": "uniform(-50, 50, shape=2)"}, - algorithms={"robo": {"model_type": "gp", "n_init": 2}}, - debug=True, - ) - - for _ in range(10): - trial = exp.suggest() - assert trial.params["x"] in [-5, -2, 0, 2, 5] - exp.observe(trial, [dict(name="objective", type="objective", value=0)]) - - -def test_optimizer_two_inputs(monkeypatch): - """Check functionality of BayesianOptimizer wrapper for 2 dimensions.""" - monkeypatch.chdir(os.path.dirname(os.path.abspath(__file__))) - - with OrionState(experiments=[], trials=[]): - - orion.core.cli.main( - [ - "hunt", - "--name", - "exp", - "--max-trials", - "5", - "--config", - "./benchmark/robo.yaml", - "./benchmark/rosenbrock.py", - "-x~uniform(-5, 5)", - "-y~uniform(-10, 10)", - ] - ) - - -@pytest.mark.parametrize("model_type", MODEL_TYPES) -def test_optimizer_actually_optimize(model_type): - """Check if the optimizer has better optimization than random search.""" - if model_type == "bohamiann": - pytest.xfail("Bohamiann takes too long to train") - - best_random_search = 25.0 - - with OrionState(experiments=[], trials=[]): - - exp = create_experiment( - name="exp", - space={"x": "uniform(-50, 50, precision=10)"}, - max_trials=10, - algorithms={"robo": {"model_type": model_type, "n_init": 5}}, - debug=True, - ) - - exp.workon(rosenbrock_function, y=0) - - objective = exp.stats["best_evaluation"] - - assert best_random_search > objective diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..501604d --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Perform integration tests for `orion.algo.robo`.""" +import copy +import functools +import itertools +import os + +import numpy +import orion.core.cli +import pytest +from orion.testing.algo import BaseAlgoTests + +N_INIT = 10 + + +def modified_config(config, **kwargs): + modified = copy.deepcopy(config) + modified.update(kwargs) + return modified + + +class BaseRoBOTests(BaseAlgoTests): + def test_suggest_init(self, mocker): + algo = self.create_algo() + spy = self.spy_phase(mocker, 0, algo, "space.sample") + points = algo.suggest(1000) + assert len(points) == N_INIT + + def test_suggest_init_missing(self, mocker): + algo = self.create_algo() + missing = 3 + spy = self.spy_phase(mocker, N_INIT - missing, algo, "space.sample") + points = algo.suggest(1000) + assert len(points) == missing + + def test_suggest_init_overflow(self, mocker): + algo = self.create_algo() + spy = self.spy_phase(mocker, N_INIT - 1, algo, "space.sample") + # Now reaching N_INIT + points = algo.suggest(1000) + assert len(points) == 1 + # Verify point was sampled randomly, not using BO + assert spy.call_count == 1 + # Overflow above N_INIT + points = algo.suggest(1000) + assert len(points) == 1 + # Verify point was sampled randomly, not using BO + assert spy.call_count == 2 + + def test_suggest_n(self, mocker, num, attr): + algo = self.create_algo() + spy = self.spy_phase(mocker, num, algo, attr) + points = algo.suggest(5) + if num == 0: + assert len(points) == 5 + else: + assert len(points) == 1 + + def test_is_done_cardinality(self): + # TODO: Support correctly loguniform(discrete=True) + # See https://github.com/Epistimio/orion/issues/566 + space = self.update_space( + { + "x": "uniform(0, 4, discrete=True)", + "y": "choices(['a', 'b', 'c'])", + "z": "uniform(1, 6, discrete=True)", + } + ) + space = self.create_space(space) + assert space.cardinality == 5 * 3 * 6 + + algo = self.create_algo(space=space) + for i, (x, y, z) in enumerate(itertools.product(range(5), "abc", range(1, 7))): + assert not algo.is_done + n = len(algo.algorithm._trials_info) + algo.observe([[x, y, z]], [dict(objective=i)]) + assert len(algo.algorithm._trials_info) == n + 1 + + assert i + 1 == space.cardinality + + assert algo.is_done + + +class TestRoBO_GP(BaseRoBOTests): + + algo_name = "robo_gp" + config = { + "maximizer": "random", + "acquisition_func": "log_ei", + "n_init": N_INIT, + "normalize_input": False, + "normalize_output": True, + "seed": 1234, + } + + +class TestRoBO_GP_MCMC(BaseRoBOTests): + algo_name = "robo_gp_mcmc" + config = { + "maximizer": "random", + "acquisition_func": "log_ei", + "normalize_input": True, + "normalize_output": False, + "chain_length": 10, + "burnin_steps": 2, + "n_init": N_INIT, + "seed": 1234, + } + + +class TestRoBO_RandomForest(BaseRoBOTests): + algo_name = "robo_randomforest" + config = { + "maximizer": "random", + "acquisition_func": "log_ei", + "num_trees": 10, + "do_bootstrapping": False, + "n_points_per_tree": 5, + "compute_oob_error": True, + "return_total_variance": False, + "n_init": N_INIT, + "seed": 1234, + } + + +class TestRoBO_DNGO(TestRoBO_GP): + algo_name = "robo_dngo" + + config = { + "maximizer": "random", + "acquisition_func": "log_ei", + "normalize_input": True, + "normalize_output": False, + "chain_length": 10, + "burnin_steps": 2, + "learning_rate": 1e-2, + "batch_size": 10, + "num_epochs": 10, + "adapt_epoch": 20, + "n_init": N_INIT, + "seed": 1234, + } + + def test_configuration_to_model(self, mocker): + + train_config = dict( + chain_length=self.config["chain_length"] * 2, + burnin_steps=self.config["burnin_steps"] * 2, + num_epochs=self.config["num_epochs"] * 2, + adapt_epoch=self.config["adapt_epoch"] * 2, + learning_rate=self.config["learning_rate"] * 2, + batch_size=self.config["batch_size"] + 1, + ) + + tmp_config = modified_config(self.config, n_init=N_INIT + 1, **train_config) + + algo = self.create_algo(tmp_config) + + model = algo.algorithm.model + + assert model.chain_length == tmp_config["chain_length"] + assert model.burnin_steps == tmp_config["burnin_steps"] + assert model.num_epochs == tmp_config["num_epochs"] + assert model.adapt_epoch == tmp_config["adapt_epoch"] + assert model.init_learning_rate == tmp_config["learning_rate"] + assert model.batch_size == tmp_config["batch_size"] + + +class TestRoBO_BOHAMIANN(BaseRoBOTests): + algo_name = "robo_bohamiann" + config = { + "maximizer": "random", + "acquisition_func": "log_ei", + "normalize_input": True, + "normalize_output": False, + "burnin_steps": 2, + "sampling_method": "adaptive_sghmc", + "use_double_precision": True, + "num_steps": 100, + "keep_every": 10, + "learning_rate": 1e-2, + "batch_size": 10, + "epsilon": 1e-10, + "mdecay": 0.05, + "continue_training": False, + "verbose": False, + "n_init": N_INIT, + "seed": 1234, + } + + def test_configuration_to_model(self, mocker): + + train_config = dict( + burnin_steps=self.config["burnin_steps"] * 2, + num_steps=500, + keep_every=self.config["keep_every"] * 2, + learning_rate=self.config["learning_rate"] * 2, + batch_size=self.config["batch_size"] + 1, + epsilon=self.config["epsilon"] * 2, + mdecay=self.config["mdecay"] + 0.01, + continue_training=False, + verbose=True, + ) + + tmp_config = modified_config( + self.config, + sampling_method="sgld", + use_double_precision=False, + n_init=N_INIT + 1, + **train_config + ) + + # Adapt to bnn.train interface + train_config["num_burn_in_steps"] = train_config.pop("burnin_steps") + train_config["lr"] = train_config.pop("learning_rate") + + algo = self.create_algo(tmp_config) + + assert algo.algorithm.model.bnn.sampling_method == tmp_config["sampling_method"] + assert ( + algo.algorithm.model.bnn.use_double_precision + == tmp_config["use_double_precision"] + ) + + spy = self.spy_phase(mocker, tmp_config["n_init"] + 1, algo, "model.bnn.train") + algo.suggest(1) + assert spy.call_count > 0 + assert spy.call_args[1] == train_config + + +TestRoBO_GP.set_phases( + [("random", 0, "space.sample"), ("gp", N_INIT + 1, "robo.choose_next")] +) + +TestRoBO_GP_MCMC.set_phases([("gp_mcmc", N_INIT + 1, "robo.choose_next")]) + +TestRoBO_RandomForest.set_phases([("randomforest", N_INIT + 1, "robo.choose_next")]) + +TestRoBO_DNGO.set_phases([("dngo", N_INIT + 1, "robo.choose_next")]) + +TestRoBO_BOHAMIANN.set_phases([("bohamiann", N_INIT + 1, "robo.choose_next")]) From c395fd5877d8141b43f062224bbcf88cad09f627 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:20:43 -0400 Subject: [PATCH 03/30] isort --- src/orion/algo/robo/dngo.py | 1 - src/orion/algo/robo/gp.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/orion/algo/robo/dngo.py b/src/orion/algo/robo/dngo.py index 147345e..312ad7e 100644 --- a/src/orion/algo/robo/dngo.py +++ b/src/orion/algo/robo/dngo.py @@ -1,6 +1,5 @@ import numpy import torch - from pybnn.dngo import DNGO from orion.algo.robo.base import RoBO, build_bounds, build_kernel, infer_n_hypers diff --git a/src/orion/algo/robo/gp.py b/src/orion/algo/robo/gp.py index a850ea3..0dc62e7 100644 --- a/src/orion/algo/robo/gp.py +++ b/src/orion/algo/robo/gp.py @@ -1,5 +1,4 @@ import numpy - from robo.acquisition_functions.marginalization import MarginalizationGPMCMC from robo.models.gaussian_process import GaussianProcess from robo.models.gaussian_process_mcmc import GaussianProcessMCMC From d37a6e126a4d78315e9feca52fa78fd11e3ec952 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:21:17 -0400 Subject: [PATCH 04/30] Requirements are mostly in setup.py --- requirements.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2d3b406..cca2829 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1 @@ -Cython -PyYAML -Jinja2 -numpy -git+https://github.com/automl/george.git@development -git+https://github.com/automl/pybnn.git git+https://github.com/automl/RoBO.git From 33ca63596bbdb4180c5091436ba448fd199132a0 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:42:16 -0400 Subject: [PATCH 05/30] pylint --- .pylintrc | 2 +- src/orion/algo/robo/base.py | 18 +- src/orion/algo/robo/bohamiann.py | 13 +- src/orion/algo/robo/dngo.py | 16 +- src/orion/algo/robo/gp.py | 8 +- src/orion/algo/robo/randomforest.py | 10 +- src/orion/algo/robo/rbayes.py | 392 ---------------------------- src/orion/algo/robo/wrappers.py | 125 --------- tox.ini | 2 +- 9 files changed, 39 insertions(+), 547 deletions(-) delete mode 100644 src/orion/algo/robo/rbayes.py delete mode 100644 src/orion/algo/robo/wrappers.py diff --git a/.pylintrc b/.pylintrc index a8472e8..779c13b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -51,7 +51,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=ungrouped-imports,abstract-class-instantiated,useless-super-delegation,no-member,keyword-arg-before-vararg,unidiomatic-typecheck,redefined-outer-name,fixme,F0401,intern-builtin,wrong-import-position,wrong-import-order, C0415, F0010, R0205, R1705, R1711, R1720, W0106, W0107, W0127, W0706, C0330, C0326 +disable=ungrouped-imports,abstract-class-instantiated,useless-super-delegation,no-member,keyword-arg-before-vararg,unidiomatic-typecheck,redefined-outer-name,fixme,F0401,intern-builtin,wrong-import-position,wrong-import-order, C0415, F0010, R0205, R1705, R1711, R1720, W0106, W0107, W0127, W0706, C0330, C0326,invalid-name,too-many-arguments,attribute-defined-outside-init # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/src/orion/algo/robo/base.py b/src/orion/algo/robo/base.py index eb82ab5..f348122 100644 --- a/src/orion/algo/robo/base.py +++ b/src/orion/algo/robo/base.py @@ -1,10 +1,12 @@ +""" +Base class for RoBO algorithms. +""" import george import numpy from orion.algo.base import BaseAlgorithm from robo.acquisition_functions.ei import EI from robo.acquisition_functions.lcb import LCB from robo.acquisition_functions.log_ei import LogEI -from robo.acquisition_functions.marginalization import MarginalizationGPMCMC from robo.acquisition_functions.pi import PI from robo.initial_design import init_latin_hypercube_sampling from robo.maximizers.differential_evolution import DifferentialEvolution @@ -123,18 +125,13 @@ def build_optimizer(model, maximizer, acquisition_func): Optimizer """ - maximizer_rng = numpy.random.RandomState(maximizer_seed) if maximizer == "random": - max_func = RandomSampling( - acquisition_func, model.lower, model.upper, rng=maximizer_rng - ) + max_func = RandomSampling(acquisition_func, model.lower, model.upper, rng=None) elif maximizer == "scipy": - max_func = SciPyOptimizer( - acquisition_func, model.lower, model.upper, rng=maximizer_rng - ) + max_func = SciPyOptimizer(acquisition_func, model.lower, model.upper, rng=None) elif maximizer == "differential_evolution": max_func = DifferentialEvolution( - acquisition_func, model.lower, model.upper, rng=maximizer_rng + acquisition_func, model.lower, model.upper, rng=None ) else: raise ValueError( @@ -194,7 +191,6 @@ class RoBO(BaseAlgorithm): requires_dist = "linear" requires_shape = "flattened" - # pylint:disable=too-many-arguments def __init__( self, space, @@ -258,7 +254,6 @@ def _initialize(self): self.seed_rng(self.seed) - # pylint:disable=invalid-name @property def X(self): """Matrix containing trial points""" @@ -271,7 +266,6 @@ def X(self): return X - # pylint:disable=invalid-name @property def y(self): """Vector containing trial results""" diff --git a/src/orion/algo/robo/bohamiann.py b/src/orion/algo/robo/bohamiann.py index f523f0f..4c8e44b 100644 --- a/src/orion/algo/robo/bohamiann.py +++ b/src/orion/algo/robo/bohamiann.py @@ -1,10 +1,12 @@ -import numpy +""" +Wrapper for RoBO with BOHAMIANN +""" import torch from pybnn.bohamiann import Bohamiann from robo.models.base_model import BaseModel from robo.models.wrapper_bohamiann import get_default_network -from orion.algo.robo.base import RoBO, build_bounds, build_kernel, infer_n_hypers +from orion.algo.robo.base import RoBO, build_bounds class RoBO_BOHAMIANN(RoBO): @@ -200,7 +202,7 @@ def __init__( self.num_steps = num_steps self.keep_every = keep_every self.burnin_steps = burnin_steps - self.learning_rate = learning_rate # pylint:disable=invalid-name + self.learning_rate = learning_rate self.batch_size = batch_size self.epsilon = epsilon self.mdecay = mdecay @@ -237,6 +239,9 @@ def seed(self, seed): torch.manual_seed(seed) def train(self, X, y, **kwargs): + """ + Sets num_steps and burnin_steps before training with parent's train() + """ self.X = X self.y = y @@ -262,7 +267,9 @@ def train(self, X, y, **kwargs): mdecay=self.mdecay, continue_training=False, verbose=self.verbose, + **kwargs ) def predict(self, X_test): + """Predict using bnn.predict()""" return self.bnn.predict(X_test) diff --git a/src/orion/algo/robo/dngo.py b/src/orion/algo/robo/dngo.py index 312ad7e..da2e8f5 100644 --- a/src/orion/algo/robo/dngo.py +++ b/src/orion/algo/robo/dngo.py @@ -1,3 +1,6 @@ +""" +Wrapper for RoBO with DNGO +""" import numpy import torch from pybnn.dngo import DNGO @@ -143,8 +146,8 @@ class OrionDNGOWrapper(DNGO): prior: Prior object Prior for alpa and beta. If set to None the default prior is used do_mcmc: bool - If set to true different values for alpha and beta are sampled via MCMC from the marginal log likelihood - Otherwise the marginal log likehood is optimized with scipy fmin function + If set to true different values for alpha and beta are sampled via MCMC from the marginal + log likelihood. Otherwise the marginal log likehood is optimized with scipy fmin function. n_hypers : int Number of samples for alpha and beta chain_length : int @@ -166,14 +169,12 @@ def __init__(self, lower, upper, **kwargs): self.lower = lower self.upper = upper - # pylint:disable=no-self-use def set_state(self, state_dict): """Restore the state of the optimizer""" torch.random.set_rng_state(state_dict["torch"]) self.rng.set_state(state_dict["rng"]) self.prior.rng.set_state(state_dict["prior_rng"]) - # pylint:disable=no-self-use def state_dict(self): """Return the current state of the optimizer so that it can be restored""" return { @@ -184,8 +185,8 @@ def state_dict(self): def seed(self, seed): """Seed all internal RNGs""" - rng = numpy.random.RandomState(seed) - rand_nums = self.rng.randint(1, 10e8, 3) + self.rng = numpy.random.RandomState(seed) + rand_nums = self.rng.randint(1, 10e8, 2) pytorch_seed = rand_nums[0] if torch.cuda.is_available(): @@ -195,5 +196,4 @@ def seed(self, seed): torch.manual_seed(pytorch_seed) - self.rng.seed(rand_nums[1]) - self.prior.rng.seed(rand_nums[2]) + self.prior.rng.seed(rand_nums[1]) diff --git a/src/orion/algo/robo/gp.py b/src/orion/algo/robo/gp.py index 0dc62e7..daec98b 100644 --- a/src/orion/algo/robo/gp.py +++ b/src/orion/algo/robo/gp.py @@ -1,3 +1,7 @@ +""" +Wrapper for RoBO with GP (and MCMC) +""" + import numpy from robo.acquisition_functions.marginalization import MarginalizationGPMCMC from robo.models.gaussian_process import GaussianProcess @@ -5,7 +9,6 @@ from orion.algo.robo.base import ( RoBO, - build_acquisition_func, build_bounds, build_kernel, build_prior, @@ -200,7 +203,6 @@ class OrionGaussianProcessWrapper(GaussianProcess): """ - # pylint:disable=attribute-defined-outside-init def set_state(self, state_dict): """Restore the state of the optimizer""" self.rng.set_state(state_dict["model_rng_state"]) @@ -255,14 +257,12 @@ class OrionGaussianProcessMCMCWrapper(GaussianProcessMCMC): """ - # pylint:disable=attribute-defined-outside-init def set_state(self, state_dict): """Restore the state of the optimizer""" self.rng.set_state(state_dict["model_rng_state"]) self.prior.rng.set_state(state_dict["prior_rng_state"]) if state_dict.get("model_p0", None) is not None: - # pylint:disable=invalid-name self.p0 = numpy.array(state_dict["model_p0"]) self.burned = True elif hasattr(self, "p0"): diff --git a/src/orion/algo/robo/randomforest.py b/src/orion/algo/robo/randomforest.py index a6f5f50..20ec755 100644 --- a/src/orion/algo/robo/randomforest.py +++ b/src/orion/algo/robo/randomforest.py @@ -1,4 +1,6 @@ -import numpy +""" +Wrapper for RoBO with Random Forest +""" import pyrfr.regression as reg from robo.models.random_forest import RandomForest @@ -134,12 +136,18 @@ def __init__( self.upper = upper def train(self, X, y, **kwargs): + """ + Seeds the RNG of Random Forest before calling parent's train(). + """ # NOTE: We cannot save `reg_rng` state so instead we control it # with random integers sampled from `rng` and keep track of `rng` state. self.reg_rng = reg.default_random_engine(int(self.rng.randint(10e8))) super(OrionRandomForestWrapper, self).train(X, y, **kwargs) def predict(self, X_test, **kwargs): + """ + Seeds the RNG of Random Forest before calling parent's predict(). + """ # NOTE: We cannot save `reg_rng` state so instead we control it # with random integers sampled from `rng` and keep track of `rng` state. self.reg_rng = reg.default_random_engine(int(self.rng.randint(10e8))) diff --git a/src/orion/algo/robo/rbayes.py b/src/orion/algo/robo/rbayes.py deleted file mode 100644 index 802c891..0000000 --- a/src/orion/algo/robo/rbayes.py +++ /dev/null @@ -1,392 +0,0 @@ -# -*- coding: utf-8 -*- -""" -:mod:`orion.algo.robo.rbayes -- TODO -============================================ - -.. module:: robo - :platform: Unix - :synopsis: TODO - -TODO: Write long description - -""" -import george -import numpy -from orion.algo.base import BaseAlgorithm -from orion.algo.space import Space -from robo.acquisition_functions.ei import EI -from robo.acquisition_functions.lcb import LCB -from robo.acquisition_functions.log_ei import LogEI -from robo.acquisition_functions.marginalization import MarginalizationGPMCMC -from robo.acquisition_functions.pi import PI -from robo.initial_design import init_latin_hypercube_sampling -from robo.maximizers.differential_evolution import DifferentialEvolution -from robo.maximizers.random_sampling import RandomSampling -from robo.maximizers.scipy_optimizer import SciPyOptimizer -from robo.priors.default_priors import DefaultPrior -from robo.solver.bayesian_optimization import BayesianOptimization - -from orion.algo.robo.wrappers import ( - OrionBohamiannWrapper, - OrionGaussianProcessMCMCWrapper, - OrionGaussianProcessWrapper, -) - - -def build_bounds(space): - """ - Build bounds of optimization space - :param space: - - """ - lower = [] - upper = [] - for dim in space.values(): - low, high = dim.interval() - - shape = dim.shape - assert not shape or shape == [1] - - lower.append(low) - upper.append(high) - - return list(map(numpy.array, (lower, upper))) - - -def build_optimizer( - model, maximizer="random", acquisition_func="log_ei", maximizer_seed=1 -): - """ - General interface for Bayesian optimization for global black box - optimization problems. - Parameters - ---------- - maximizer: {"random", "scipy", "differential_evolution"} - The optimizer for the acquisition function. - acquisition_func: {"ei", "log_ei", "lcb", "pi"} - The acquisition function - maximizer_seed: int - Seed for random number generator of the acquisition function maximizer - Returns - ------- - Optimizer - :param maximizer_seed: - :param acquisition_func: - :param maximizer: - :param model: - - """ - if acquisition_func == "ei": - acquisition_func = EI(model) - elif acquisition_func == "log_ei": - acquisition_func = LogEI(model) - elif acquisition_func == "pi": - acquisition_func = PI(model) - elif acquisition_func == "lcb": - acquisition_func = LCB(model) - else: - raise ValueError( - "'{}' is not a valid acquisition function".format(acquisition_func) - ) - - if isinstance(model, OrionGaussianProcessMCMCWrapper): - acquisition_func = MarginalizationGPMCMC(acquisition_func) - else: - acquisition_func = acquisition_func - - maximizer_rng = numpy.random.RandomState(maximizer_seed) - if maximizer == "random": - max_func = RandomSampling( - acquisition_func, model.lower, model.upper, rng=maximizer_rng - ) - elif maximizer == "scipy": - max_func = SciPyOptimizer( - acquisition_func, model.lower, model.upper, rng=maximizer_rng - ) - elif maximizer == "differential_evolution": - max_func = DifferentialEvolution( - acquisition_func, model.lower, model.upper, rng=maximizer_rng - ) - else: - raise ValueError( - "'{}' is not a valid function to maximize the " - "acquisition function".format(maximizer) - ) - - # NOTE: Internal RNG of BO won't be used. - # NOTE: Nb of initial points won't be used within BO, but rather outside - bo = BayesianOptimization( - lambda: None, - model.lower, - model.upper, - acquisition_func, - model, - max_func, - initial_points=None, - rng=None, - initial_design=init_latin_hypercube_sampling, - output_path=None, - ) - - return bo - - -def build_model(lower, upper, model_type="gp_mcmc", model_seed=1, prior_seed=1): - """ - General interface for Bayesian optimization for global black box - optimization problems. - Parameters - ---------- - lower: numpy.ndarray (D,) - The lower bound of the search space - upper: numpy.ndarray (D,) - The upper bound of the search space - model_type: {"gp", "gp_mcmc", "rf", "bohamiann", "dngo"} - The model for the objective function. - model_seed: int - Seed for random number generator of the model - prior_seed: int - Seed for random number generator of the prior - Returns - ------- - Model - - """ - assert upper.shape[0] == lower.shape[0], "Dimension miss match" - assert numpy.all(lower < upper), "Lower bound >= upper bound" - - cov_amp = 2 - n_dims = lower.shape[0] - - initial_ls = numpy.ones([n_dims]) - exp_kernel = george.kernels.Matern52Kernel(initial_ls, ndim=n_dims) - kernel = cov_amp * exp_kernel - - prior = DefaultPrior(len(kernel) + 1, numpy.random.RandomState(prior_seed)) - - n_hypers = 3 * len(kernel) - if n_hypers % 2 == 1: - n_hypers += 1 - - # NOTE: Some models do not support RNG properly and rely on global RNG state - # so we need to seed here as well... - numpy.random.seed(model_seed) - model_rng = numpy.random.RandomState(model_seed) - if model_type == "gp": - model = OrionGaussianProcessWrapper( - kernel, - prior=prior, - rng=model_rng, - normalize_input=True, - normalize_output=False, - lower=lower, - upper=upper, - ) - - elif model_type == "gp_mcmc": - model = OrionGaussianProcessMCMCWrapper( - kernel, - prior=prior, - n_hypers=n_hypers, - chain_length=200, - burnin_steps=100, - normalize_input=True, - normalize_output=False, - rng=model_rng, - lower=lower, - upper=upper, - ) - - # TODO - # elif model_type == "rf": - # model = RandomForest(rng=model_rng) - - elif model_type == "bohamiann": - model = OrionBohamiannWrapper( - normalize_input=True, - normalize_output=False, - sampling_method="adaptive_sghmc", - use_double_precision=True, - print_every_n_steps=100, - lower=lower, - upper=upper, - ) - - # TODO - # elif model_type == "dngo": - # model = DNGO() - - else: - raise ValueError("'{}' is not a valid model".format(model_type)) - - return model - - -class RoBO(BaseAlgorithm): - """TODO: Class docstring""" - - requires_type = "real" - requires_dist = "linear" - requires_shape = "flattened" - - # pylint:disable=too-many-arguments - def __init__( - self, - space: Space, - model_type="gp", - maximizer="random", - acquisition_func="log_ei", - n_init=20, - model_seed=0, - prior_seed=0, - init_seed=0, - maximizer_seed=0, - ): - - super(RoBO, self).__init__( - space, - model_type=model_type, - acquisition_func=acquisition_func, - n_init=n_init, - model_seed=model_seed, - prior_seed=prior_seed, - init_seed=init_seed, - maximizer_seed=maximizer_seed, - ) - - self.maximizer = maximizer - self.suggest_count = 0 - self.model = None - self.robo = None - - @property - def space(self): - """Space of the optimizer""" - return self._space - - @space.setter - def space(self, space): - """Setter of optimizer's space. - - Side-effect: Will initialize optimizer. - """ - self._space = space - self._initialize() - - def _initialize(self): - """Initialize the optimizer once the space is transformed""" - lower, upper = build_bounds(self.space) - self.model = build_model( - lower, upper, self.model_type, self.model_seed, self.prior_seed - ) - self.robo = build_optimizer( - self.model, - maximizer=self.maximizer, - acquisition_func=self.acquisition_func, - maximizer_seed=self.maximizer_seed, - ) - - self.seed_rng(self.init_seed) - - # pylint:disable=invalid-name - @property - def X(self): - """Matrix containing trial points""" - ref_point = self.space.sample(1, seed=0)[0] - X = numpy.zeros((len(self._trials_info), len(ref_point))) - for i, (point, _result) in enumerate(self._trials_info.values()): - X[i] = point - - return X - - # pylint:disable=invalid-name - @property - def y(self): - """Vector containing trial results""" - y = numpy.zeros(len(self._trials_info)) - for i, (_point, result) in enumerate(self._trials_info.values()): - y[i] = result["objective"] - - return y - - def seed_rng(self, seed): - """Seed the state of the random number generator. - - :param seed: Integer seed for the random number generator. - - .. note:: This methods does nothing if the algorithm is deterministic. - - """ - self.rng = numpy.random.RandomState(seed) - - size = 3 - rand_nums = numpy.random.randint(1, 10e8, size) - - self.robo.rng = numpy.random.RandomState(rand_nums[0]) - self.robo.maximize_func.rng.seed(rand_nums[1]) - self.model.seed(rand_nums[2]) - - @property - def state_dict(self): - """Return a state dict that can be used to reset the state of the algorithm.""" - s_dict = super(RoBO, self).state_dict - - s_dict.update( - { - "rng_state": self.rng.get_state(), - "global_numpy_rng_state": numpy.random.get_state(), - "maximizer_rng_state": self.robo.maximize_func.rng.get_state(), - "suggest_count": self.suggest_count, - } - ) - - s_dict["model"] = self.model.state_dict() - - return s_dict - - def set_state(self, state_dict): - """Reset the state of the algorithm based on the given state_dict - - :param state_dict: Dictionary representing state of an algorithm - - """ - super(RoBO, self).set_state(state_dict) - - self.rng.set_state(state_dict["rng_state"]) - numpy.random.set_state(state_dict["global_numpy_rng_state"]) - self.robo.maximize_func.rng.set_state(state_dict["maximizer_rng_state"]) - self.model.set_state(state_dict["model"]) - self.suggest_count = state_dict["suggest_count"] - - def suggest(self, num=1): - """Suggest a `num`ber of new sets of parameters. - - TODO: document how suggest work for this algo - - Parameters - ---------- - num: int, optional - Number of points to suggest. Defaults to 1. - - Returns - ------- - list of points or None - A list of lists representing points suggested by the algorithm. The algorithm may opt - out if it cannot make a good suggestion at the moment (it may be waiting for other - trials to complete), in which case it will return None. - - Notes - ----- - New parameters must be compliant with the problem's domain `orion.algo.space.Space`. - - """ - if num > 1: - raise AttributeError("RoBO wrapper does not support num > 1.") - - self.suggest_count += 1 - if self.suggest_count > self.n_init: - return [self.robo.choose_next(self.X, self.y)] - else: - return self.space.sample( - num, seed=tuple(self.rng.randint(0, 1000000, size=3)) - ) diff --git a/src/orion/algo/robo/wrappers.py b/src/orion/algo/robo/wrappers.py deleted file mode 100644 index 5d2b551..0000000 --- a/src/orion/algo/robo/wrappers.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -:mod:`orion.algo.robo.wrappers -- Wrappers for RoBO Optimizers -============================================================== - -.. module:: robo - :platform: Unix - :synopsis: Wrappers for RoBO Optimizers - -Wraps RoBO optimizers to provide a uniform interface across them. Namely, -it adds the properties `lower` and `upper` and the methods `seed()`, `state_dict()` -and `set_state()`. -""" -import numpy -import torch -from pybnn.bohamiann import Bohamiann -from robo.models.gaussian_process import GaussianProcess -from robo.models.gaussian_process_mcmc import GaussianProcessMCMC -from robo.models.wrapper_bohamiann import WrapperBohamiann, get_default_network - - -class OrionGaussianProcessWrapper(GaussianProcess): - """Wrapper for GaussianProcess""" - - # pylint:disable=attribute-defined-outside-init - def set_state(self, state_dict): - """Restore the state of the optimizer""" - self.rng.set_state(state_dict["model_rng_state"]) - self.prior.rng.set_state(state_dict["prior_rng_state"]) - self.kernel.set_parameter_vector(state_dict["model_kernel_parameter_vector"]) - self.noise = state_dict["noise"] - - def state_dict(self): - """Return the current state of the optimizer so that it can be restored""" - return { - "prior_rng_state": self.prior.rng.get_state(), - "model_rng_state": self.rng.get_state(), - "model_kernel_parameter_vector": self.kernel.get_parameter_vector().tolist(), - "noise": self.noise, - } - - def seed(self, seed): - """Seed all internal RNGs""" - seeds = numpy.random.RandomState(seed).randint(1, 10e8, size=2) - self.rng.seed(seeds[0]) - self.prior.rng.seed(seeds[1]) - - -class OrionGaussianProcessMCMCWrapper(GaussianProcessMCMC): - """Wrapper for GaussianProcess with MCMC""" - - # pylint:disable=attribute-defined-outside-init - def set_state(self, state_dict): - """Restore the state of the optimizer""" - self.rng.set_state(state_dict["model_rng_state"]) - self.prior.rng.set_state(state_dict["prior_rng_state"]) - - if state_dict.get("model_p0", None) is not None: - # pylint:disable=invalid-name - self.p0 = numpy.array(state_dict["model_p0"]) - self.burned = True - elif hasattr(self, "p0"): - delattr(self, "p0") - self.burned = False - - def state_dict(self): - """Return the current state of the optimizer so that it can be restored""" - s_dict = { - "prior_rng_state": self.prior.rng.get_state(), - "model_rng_state": self.rng.get_state(), - } - - if hasattr(self, "p0"): - s_dict["model_p0"] = self.p0.tolist() - - return s_dict - - def seed(self, seed): - """Seed all internal RNGs""" - seeds = numpy.random.RandomState(seed).randint(1, 10e8, size=2) - self.rng.seed(seeds[0]) - self.prior.rng.seed(seeds[1]) - - -class OrionBohamiannWrapper(WrapperBohamiann): - """Wrapper for Bohamiann""" - - def __init__( - self, - lower, - upper, - learning_rate=1e-2, - verbose=False, - use_double_precision=True, - **kwargs - ): - - self.lr = learning_rate # pylint:disable=invalid-name - self.verbose = verbose - self.bnn = Bohamiann( - get_network=get_default_network, - use_double_precision=use_double_precision, - **kwargs - ) - - self.lower = lower - self.upper = upper - - # pylint:disable=no-self-use - def set_state(self, state_dict): - """Restore the state of the optimizer""" - torch.random.set_rng_state(state_dict["torch"]) - - # pylint:disable=no-self-use - def state_dict(self): - """Return the current state of the optimizer so that it can be restored""" - return {"torch": torch.random.get_rng_state()} - - def seed(self, seed): - """Seed all internal RNGs""" - if torch.cuda.is_available(): - torch.backends.cudnn.benchmark = False - torch.cuda.manual_seed_all(seed) - torch.backends.cudnn.deterministic = True - - torch.manual_seed(seed) diff --git a/tox.ini b/tox.ini index 1769a25..3f8b9bc 100644 --- a/tox.ini +++ b/tox.ini @@ -109,7 +109,7 @@ skip_install = false deps = pylint == 2.5.* commands = - pylint src tests --ignore src/orion/algo/robo/_version.py + pylint src --ignore src/orion/algo/robo/_version.py [testenv:packaging] description = Check whether README.rst is reST and missing from MANIFEST.in From b606e7f38d6f4532b8459feb3ee0ae3d821531aa Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:42:46 -0400 Subject: [PATCH 06/30] Add conftest --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..dd0ef7d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +"""Common fixtures and utils for unittests and functional tests.""" +import pytest + +pytest.register_assert_rewrite("orion.testing") From 55b95bf2c90f2eb2c10dc828712728c002150c8c Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:50:40 -0400 Subject: [PATCH 07/30] Add _static folder for doc --- docs/src/_static/stub | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/src/_static/stub diff --git a/docs/src/_static/stub b/docs/src/_static/stub new file mode 100644 index 0000000..e69de29 From 8273e22faf4280d6066bf3bec5f21c44d9a965ee Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 15:58:04 -0400 Subject: [PATCH 08/30] Tox setup must install --- tox.ini | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 3f8b9bc..1966e82 100644 --- a/tox.ini +++ b/tox.ini @@ -10,17 +10,13 @@ python = [testenv] description = Run tests with coverage with pytest under current Python env +usedevelop = true setenv = COVERAGE_FILE=.coverage.{envname} passenv = CI deps = -rtests/requirements.txt + -rrequirements.txt coverage -# usedevelop = True -skip_install = true -allowlist_externals = - bash - grep - xargs commands = bash -c 'grep -v "^#" requirements.txt | xargs -n 1 -L 1 pip install' pip install -e . From 3f60dd38b1434733fd03036f63d7029fc917c3ca Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 16:26:24 -0400 Subject: [PATCH 09/30] Fix import in test_bounds --- tests/test_bounds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bounds.py b/tests/test_bounds.py index 85be3ed..7eaeb0d 100644 --- a/tests/test_bounds.py +++ b/tests/test_bounds.py @@ -6,7 +6,7 @@ from orion.algo.space import Categorical, Integer, Real, Space from orion.core.worker.transformer import build_required_space -from orion.algo.robo.rbayes import build_bounds +from orion.algo.robo.base import build_bounds @pytest.fixture() From 9ff78d0e5212f635cd95f4fd24c6ef0ca2f049eb Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 17:19:48 -0400 Subject: [PATCH 10/30] n_init -> n_initial_points --- src/orion/algo/robo/base.py | 8 ++++---- tests/test_integration.py | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/orion/algo/robo/base.py b/src/orion/algo/robo/base.py index f348122..c016685 100644 --- a/src/orion/algo/robo/base.py +++ b/src/orion/algo/robo/base.py @@ -195,7 +195,7 @@ def __init__( self, space, seed=0, - n_init=20, + n_initial_points=20, maximizer="random", acquisition_func="log_ei", **kwargs, @@ -207,7 +207,7 @@ def __init__( super(RoBO, self).__init__( space, - n_init=n_init, + n_initial_points=n_initial_points, maximizer=maximizer, acquisition_func=acquisition_func, seed=seed, @@ -337,7 +337,7 @@ def suggest(self, num=None): Perform a step towards negative gradient and suggest that point. """ - num = min(num, max(self.n_init - self.n_suggested, 1)) + num = min(num, max(self.n_initial_points - self.n_suggested, 1)) samples = [] candidates = [] @@ -347,7 +347,7 @@ def suggest(self, num=None): if candidate: self.register(candidate) samples.append(candidate) - elif self.n_observed < self.n_init: + elif self.n_observed < self.n_initial_points: candidates = self._suggest_random(num) else: candidates = self._suggest_bo(max(num - len(samples), 0)) diff --git a/tests/test_integration.py b/tests/test_integration.py index 501604d..02a1955 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -88,7 +88,7 @@ class TestRoBO_GP(BaseRoBOTests): config = { "maximizer": "random", "acquisition_func": "log_ei", - "n_init": N_INIT, + "n_initial_points": N_INIT, "normalize_input": False, "normalize_output": True, "seed": 1234, @@ -104,7 +104,7 @@ class TestRoBO_GP_MCMC(BaseRoBOTests): "normalize_output": False, "chain_length": 10, "burnin_steps": 2, - "n_init": N_INIT, + "n_initial_points": N_INIT, "seed": 1234, } @@ -119,7 +119,7 @@ class TestRoBO_RandomForest(BaseRoBOTests): "n_points_per_tree": 5, "compute_oob_error": True, "return_total_variance": False, - "n_init": N_INIT, + "n_initial_points": N_INIT, "seed": 1234, } @@ -138,7 +138,7 @@ class TestRoBO_DNGO(TestRoBO_GP): "batch_size": 10, "num_epochs": 10, "adapt_epoch": 20, - "n_init": N_INIT, + "n_initial_points": N_INIT, "seed": 1234, } @@ -153,7 +153,9 @@ def test_configuration_to_model(self, mocker): batch_size=self.config["batch_size"] + 1, ) - tmp_config = modified_config(self.config, n_init=N_INIT + 1, **train_config) + tmp_config = modified_config( + self.config, n_initial_points=N_INIT + 1, **train_config + ) algo = self.create_algo(tmp_config) @@ -185,7 +187,7 @@ class TestRoBO_BOHAMIANN(BaseRoBOTests): "mdecay": 0.05, "continue_training": False, "verbose": False, - "n_init": N_INIT, + "n_initial_points": N_INIT, "seed": 1234, } @@ -207,7 +209,7 @@ def test_configuration_to_model(self, mocker): self.config, sampling_method="sgld", use_double_precision=False, - n_init=N_INIT + 1, + n_initial_points=N_INIT + 1, **train_config ) @@ -223,7 +225,9 @@ def test_configuration_to_model(self, mocker): == tmp_config["use_double_precision"] ) - spy = self.spy_phase(mocker, tmp_config["n_init"] + 1, algo, "model.bnn.train") + spy = self.spy_phase( + mocker, tmp_config["n_initial_points"] + 1, algo, "model.bnn.train" + ) algo.suggest(1) assert spy.call_count > 0 assert spy.call_args[1] == train_config From 261f2566ccb76294a9370210aeaf946db555df6f Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 17:19:58 -0400 Subject: [PATCH 11/30] Add python 3.9 to tox --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1966e82..f8959bf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,12 @@ [tox] -envlist = py{36,37,38}-{linux,macos} +envlist = py{36,37,38,39}-{linux,macos} [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38 + 3.9: py39 pypy3: pypy3 [testenv] From be38e3713190aff90a26675534598a40831427ee Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 18:26:11 -0400 Subject: [PATCH 12/30] Add back some requirements... --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index cca2829..38eeb07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ +git+https://github.com/automl/george.git@development +git+https://github.com/automl/pybnn.git git+https://github.com/automl/RoBO.git From 887295a57222ee843b3ddad9db64d0561ab496ea Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 19:39:32 -0400 Subject: [PATCH 13/30] Remove docs test to see if requirements are fine for py tests --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10cda30..5d11c3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toxenv: [black, isort, pylint, doc8, docs] + toxenv: [black, isort, pylint] steps: - uses: actions/checkout@v1 From e0affcf32815aa7f3631e801144b45dc92bf7412 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 19:43:28 -0400 Subject: [PATCH 14/30] Add missing requirements... --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index 38eeb07..2d3b406 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ +Cython +PyYAML +Jinja2 +numpy git+https://github.com/automl/george.git@development git+https://github.com/automl/pybnn.git git+https://github.com/automl/RoBO.git From 3238cbb5e32826846a9816cc0d8f105cba3f3255 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 19:56:43 -0400 Subject: [PATCH 15/30] Only rely on bash install of requirements.txt --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index f8959bf..f70605a 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,6 @@ setenv = COVERAGE_FILE=.coverage.{envname} passenv = CI deps = -rtests/requirements.txt - -rrequirements.txt coverage commands = bash -c 'grep -v "^#" requirements.txt | xargs -n 1 -L 1 pip install' @@ -141,9 +140,9 @@ commands = description = Invoke sphinx to build documentation and API reference basepython = python3 deps = - -rrequirements.txt -rdocs/requirements.txt commands = + bash -c 'grep -v "^#" requirements.txt | xargs -n 1 -L 1 pip install' sphinx-build -b html -d build/doctrees -nWT docs/src/ docs/build/html [testenv:serve-docs] From ff6d9a61017f8775ba94aaf67a1f9fee242f4623 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Fri, 4 Jun 2021 21:49:01 -0400 Subject: [PATCH 16/30] Remove continue_training from test --- tests/test_integration.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 02a1955..1b5a8fd 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -185,7 +185,6 @@ class TestRoBO_BOHAMIANN(BaseRoBOTests): "batch_size": 10, "epsilon": 1e-10, "mdecay": 0.05, - "continue_training": False, "verbose": False, "n_initial_points": N_INIT, "seed": 1234, @@ -201,7 +200,6 @@ def test_configuration_to_model(self, mocker): batch_size=self.config["batch_size"] + 1, epsilon=self.config["epsilon"] * 2, mdecay=self.config["mdecay"] + 0.01, - continue_training=False, verbose=True, ) From 0949c08429d8d13b4690ea376674dca9cacc23d1 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Mon, 7 Jun 2021 12:31:23 -0400 Subject: [PATCH 17/30] Add non-configurable options in BOHAMIANN test These options are not configurable because running with `continue_training` would not make sense inside the wrapper. We train fully at each suggest/observe. We need to include them in the test however because they are default arguments of the method. --- tests/test_integration.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1b5a8fd..1df05cd 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -215,6 +215,10 @@ def test_configuration_to_model(self, mocker): train_config["num_burn_in_steps"] = train_config.pop("burnin_steps") train_config["lr"] = train_config.pop("learning_rate") + # Add arguments that are not configurable + train_config["do_optimize"] = True + train_config["continue_training"] = False + algo = self.create_algo(tmp_config) assert algo.algorithm.model.bnn.sampling_method == tmp_config["sampling_method"] From 01fbea1fe5e489735fd9723708dd25d19bdd357f Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Mon, 7 Jun 2021 13:04:11 -0400 Subject: [PATCH 18/30] Use python3 for packaging --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f70605a..52570dc 100644 --- a/tox.ini +++ b/tox.ini @@ -109,7 +109,7 @@ commands = [testenv:packaging] description = Check whether README.rst is reST and missing from MANIFEST.in -basepython = python3.6 +basepython = python3 deps = check-manifest readme_renderer From e9554650d7e76ce04e4588355528bbab37e46e75 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Mon, 7 Jun 2021 13:04:32 -0400 Subject: [PATCH 19/30] Add back docs in tests --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d11c3a..10cda30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toxenv: [black, isort, pylint] + toxenv: [black, isort, pylint, doc8, docs] steps: - uses: actions/checkout@v1 From ce747b0808fd875f1769fa4bced73d67b1eef074 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Mon, 7 Jun 2021 13:46:29 -0400 Subject: [PATCH 20/30] Exclude stub in manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 6c5a693..d5a7c07 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,7 @@ exclude .pylintrc exclude codecov.yml exclude .mailmap exclude scripts/setup_gh.sh +exclude docs/src/_static/stub prune .github/ # Include src, tests, docs From 3a7f4f07c650c4033cbe8b140879b6c9f440a815 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:09:03 -0400 Subject: [PATCH 21/30] Add workflow to publish doc on github pages --- .github/workflows/doc.yaml | 32 +++++++++++++++++++++++++++ docs/deploy.sh | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 .github/workflows/doc.yaml create mode 100755 docs/deploy.sh diff --git a/.github/workflows/doc.yaml b/.github/workflows/doc.yaml new file mode 100644 index 0000000..c6f0bdf --- /dev/null +++ b/.github/workflows/doc.yaml @@ -0,0 +1,32 @@ +name: docs + +on: + push: + branches: + - master + - develop + release: + types: + [published] + workflow_dispatch: + +jobs: + + build_docs_job: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: bash ./scripts/setup_gh.sh + - name: Test with tox + run: tox -vv -e docs + - name: what? + - name: Execute script to build our documentation and update pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash docs/deploy.sh diff --git a/docs/deploy.sh b/docs/deploy.sh new file mode 100755 index 0000000..6bfa521 --- /dev/null +++ b/docs/deploy.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +################################################################################ +# Modified version of +# https://tech.michaelaltfield.net/2020/07/18/sphinx-rtd-github-pages-1/ +################################################################################ + +##################### +# DECLARE VARIABLES # +##################### + +pwd +ls -lah +export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) + +####################### +# Update GitHub Pages # +####################### + +git config --global user.name "${GITHUB_ACTOR}" +git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" + +docroot=`mktemp -d` +rsync -av "docs/_build/html/" "${docroot}/" + +pushd "${docroot}" + +# don't bother maintaining history; just generate fresh +git init +git remote add deploy "https://token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" +git checkout -b gh-pages + +# add .nojekyll to the root so that github won't 404 on content added to dirs +# that start with an underscore (_), such as our "_content" dir.. +touch .nojekyll + +# copy the resulting html pages built from sphinx above to our new git repo +git add . + +# commit all the new files +msg="Updating Docs for commit ${GITHUB_SHA} made on `date -d"@${SOURCE_DATE_EPOCH}" --iso-8601=seconds` from ${GITHUB_REF} by ${GITHUB_ACTOR}" +git commit -am "${msg}" + +# overwrite the contents of the gh-pages branch on our github.com repo +git push deploy gh-pages --force From e2714316e165b13dc9174aa68f96c1dfd6ebd8bd Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:10:55 -0400 Subject: [PATCH 22/30] fix doc gh-action --- .github/workflows/doc.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/doc.yaml b/.github/workflows/doc.yaml index c6f0bdf..ceea105 100644 --- a/.github/workflows/doc.yaml +++ b/.github/workflows/doc.yaml @@ -15,7 +15,6 @@ jobs: build_docs_job: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - name: Set up Python 3.7 uses: actions/setup-python@v2 @@ -25,8 +24,7 @@ jobs: run: bash ./scripts/setup_gh.sh - name: Test with tox run: tox -vv -e docs - - name: what? - - name: Execute script to build our documentation and update pages + - name: Deploy doc on gh-pages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bash docs/deploy.sh From e53a2f89a918f24dc0532a090f0cc2d939d33481 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:13:48 -0400 Subject: [PATCH 23/30] rename workflow... --- .github/workflows/{doc.yaml => docs.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{doc.yaml => docs.yaml} (100%) diff --git a/.github/workflows/doc.yaml b/.github/workflows/docs.yaml similarity index 100% rename from .github/workflows/doc.yaml rename to .github/workflows/docs.yaml From d8e88e7c0591eac04d0d08d4e43e622ce4f091f7 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:16:58 -0400 Subject: [PATCH 24/30] will this workflow run --- .github/workflows/docs_test.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/docs_test.yaml diff --git a/.github/workflows/docs_test.yaml b/.github/workflows/docs_test.yaml new file mode 100644 index 0000000..ceea105 --- /dev/null +++ b/.github/workflows/docs_test.yaml @@ -0,0 +1,30 @@ +name: docs + +on: + push: + branches: + - master + - develop + release: + types: + [published] + workflow_dispatch: + +jobs: + + build_docs_job: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: bash ./scripts/setup_gh.sh + - name: Test with tox + run: tox -vv -e docs + - name: Deploy doc on gh-pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash docs/deploy.sh From ca301525d043f57fbd9b9a449ebdafe95ae0be17 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:17:31 -0400 Subject: [PATCH 25/30] try rename workflow --- .github/workflows/docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ceea105..3c52db7 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,4 @@ -name: docs +name: docs-test on: push: From 4712ddf026de3612392ba3186b4304f443b76e47 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:18:48 -0400 Subject: [PATCH 26/30] Add yet another copy with a diff name... --- .github/workflows/docs2.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/docs2.yaml diff --git a/.github/workflows/docs2.yaml b/.github/workflows/docs2.yaml new file mode 100644 index 0000000..f79d268 --- /dev/null +++ b/.github/workflows/docs2.yaml @@ -0,0 +1,30 @@ +name: run-docs + +on: + push: + branches: + - master + - develop + release: + types: + [published] + workflow_dispatch: + +jobs: + + build_docs_job: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: bash ./scripts/setup_gh.sh + - name: Test with tox + run: tox -vv -e docs + - name: Deploy doc on gh-pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash docs/deploy.sh From dd7e53bea5fe24289ee9e63793c5e0c4d6fce1a5 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:20:45 -0400 Subject: [PATCH 27/30] ... --- .github/workflows/docs3.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/docs3.yaml diff --git a/.github/workflows/docs3.yaml b/.github/workflows/docs3.yaml new file mode 100644 index 0000000..3e0fa21 --- /dev/null +++ b/.github/workflows/docs3.yaml @@ -0,0 +1,30 @@ +name: docs-test-3 + +on: + push: + branches: + - master + - develop + release: + types: + [published] + workflow_dispatch: + +jobs: + + build_docs_job: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: bash ./scripts/setup_gh.sh + - name: Test with tox + run: tox -vv -e docs + - name: Deploy doc on gh-pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash docs/deploy.sh From a13ea09129ef8e4a1e0f944dd72247067991cf18 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:24:11 -0400 Subject: [PATCH 28/30] Run the workflow!!! --- .github/workflows/docs3.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs3.yaml b/.github/workflows/docs3.yaml index 3e0fa21..b8d6e20 100644 --- a/.github/workflows/docs3.yaml +++ b/.github/workflows/docs3.yaml @@ -2,6 +2,7 @@ name: docs-test-3 on: push: + pull_request: branches: - master - develop From 1e8be59237674e11ed45ce8e27cda38c8440b473 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:30:18 -0400 Subject: [PATCH 29/30] Bad build folder --- docs/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy.sh b/docs/deploy.sh index 6bfa521..ccf5e8c 100755 --- a/docs/deploy.sh +++ b/docs/deploy.sh @@ -21,7 +21,7 @@ git config --global user.name "${GITHUB_ACTOR}" git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" docroot=`mktemp -d` -rsync -av "docs/_build/html/" "${docroot}/" +rsync -av "docs/build/html/" "${docroot}/" pushd "${docroot}" From a6419b47b2c45fa82010ee601ae5d65783f729d6 Mon Sep 17 00:00:00 2001 From: Xavier Bouthillier Date: Wed, 9 Jun 2021 21:58:13 -0400 Subject: [PATCH 30/30] Clean up doc workflows --- .github/workflows/docs.yaml | 2 +- .github/workflows/docs2.yaml | 30 ------------------------------ .github/workflows/docs3.yaml | 31 ------------------------------- .github/workflows/docs_test.yaml | 30 ------------------------------ 4 files changed, 1 insertion(+), 92 deletions(-) delete mode 100644 .github/workflows/docs2.yaml delete mode 100644 .github/workflows/docs3.yaml delete mode 100644 .github/workflows/docs_test.yaml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 3c52db7..ceea105 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,4 @@ -name: docs-test +name: docs on: push: diff --git a/.github/workflows/docs2.yaml b/.github/workflows/docs2.yaml deleted file mode 100644 index f79d268..0000000 --- a/.github/workflows/docs2.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: run-docs - -on: - push: - branches: - - master - - develop - release: - types: - [published] - workflow_dispatch: - -jobs: - - build_docs_job: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install dependencies - run: bash ./scripts/setup_gh.sh - - name: Test with tox - run: tox -vv -e docs - - name: Deploy doc on gh-pages - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bash docs/deploy.sh diff --git a/.github/workflows/docs3.yaml b/.github/workflows/docs3.yaml deleted file mode 100644 index b8d6e20..0000000 --- a/.github/workflows/docs3.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: docs-test-3 - -on: - push: - pull_request: - branches: - - master - - develop - release: - types: - [published] - workflow_dispatch: - -jobs: - - build_docs_job: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install dependencies - run: bash ./scripts/setup_gh.sh - - name: Test with tox - run: tox -vv -e docs - - name: Deploy doc on gh-pages - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bash docs/deploy.sh diff --git a/.github/workflows/docs_test.yaml b/.github/workflows/docs_test.yaml deleted file mode 100644 index ceea105..0000000 --- a/.github/workflows/docs_test.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: docs - -on: - push: - branches: - - master - - develop - release: - types: - [published] - workflow_dispatch: - -jobs: - - build_docs_job: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - name: Install dependencies - run: bash ./scripts/setup_gh.sh - - name: Test with tox - run: tox -vv -e docs - - name: Deploy doc on gh-pages - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bash docs/deploy.sh