Repository: naszilla/bananas
Branch: main
Commit: e2c12ade9e29
Files: 51
Total size: 167.7 KB
Directory structure:
gitextract_n9lcj8a6/
├── .gitignore
├── LICENSE
├── README.md
├── acquisition_functions.py
├── bo/
│ ├── __init__.py
│ ├── acq/
│ │ ├── __init__.py
│ │ ├── acqmap.py
│ │ ├── acqopt.py
│ │ └── acquisition.py
│ ├── bo/
│ │ ├── __init__.py
│ │ └── probo.py
│ ├── dom/
│ │ ├── __init__.py
│ │ ├── list.py
│ │ └── real.py
│ ├── ds/
│ │ ├── __init__.py
│ │ └── makept.py
│ ├── fn/
│ │ ├── __init__.py
│ │ └── functionhandler.py
│ ├── pp/
│ │ ├── __init__.py
│ │ ├── gp/
│ │ │ ├── __init__.py
│ │ │ └── gp_utils.py
│ │ ├── pp_core.py
│ │ ├── pp_gp_george.py
│ │ ├── pp_gp_my_distmat.py
│ │ ├── pp_gp_stan.py
│ │ ├── pp_gp_stan_distmat.py
│ │ └── stan/
│ │ ├── __init__.py
│ │ ├── compile_stan.py
│ │ ├── gp_distmat.py
│ │ ├── gp_distmat_fixedsig.py
│ │ ├── gp_hier2.py
│ │ ├── gp_hier2_matern.py
│ │ └── gp_hier3.py
│ └── util/
│ ├── __init__.py
│ ├── datatransform.py
│ └── print_utils.py
├── darts/
│ ├── __init__.py
│ └── arch.py
├── data.py
├── meta_neural_net.py
├── meta_neuralnet.ipynb
├── metann_runner.py
├── nas_algorithms.py
├── nas_bench/
│ ├── __init__.py
│ └── cell.py
├── nas_bench_201/
│ ├── __init__.py
│ └── cell.py
├── params.py
├── run_experiments_parallel.sh
├── run_experiments_sequential.py
└── train_arch_runner.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.DS_Store
*.sw*
*.pyc
*.Rapp.history
*.*~
*.out
*hide
*hide_*
*save
*save_*
*saved
*saved_*
*pylintrc*
nasbench_only108.tfrecord
.ipynb_checkpoints/*
*aux.pkl
*config.pkl
*nextpt.pkl
*data.pkl
*log.txt
================================================
FILE: LICENSE
================================================
Copyright (c) 2019, naszilla.
All rights reserved.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# BANANAS
**Note: our naszilla/bananas repo has been extended and renamed to [naszilla/naszilla](https://github.com/naszilla/naszilla), and this repo is deprecated and not maintained. Please use [naszilla/naszilla](https://github.com/naszilla/naszilla), which has more functionality.**
[BANANAS: Bayesian Optimization with Neural Architectures for Neural Architecture Search](https://arxiv.org/abs/1910.11858)\
Colin White, Willie Neiswanger, and Yash Savani.\
_arXiv:1910.11858_.
## A new method for neural architecture search
BANANAS is a neural architecture search (NAS) algorithm which uses Bayesian optimization with a meta neural network to predict the validation accuracy of neural architectures. We use a path-based encoding scheme to featurize the neural architectures that are used to train the neural network model. After training on just 200 architectures, we are able to predict the validation accuracy of new architectures to within one percent on average. The full NAS algorithm beats the state of the art on the NASBench and the DARTS search spaces. On the NASBench search space, BANANAS is over 100x more efficient than random search, and 3.8x more efficent than the next-best algorithm we tried. On the DARTS search space, BANANAS finds an architecture with a test error of 2.57%.
## Requirements
- jupyter
- tensorflow == 1.14.0 (used for all experiments)
- nasbench (follow the installation instructions [here](https://github.com/google-research/nasbench))
- nas-bench-201 (follow the installation instructions [here](https://github.com/D-X-Y/NAS-Bench-201))
- pytorch == 1.2.0, torchvision == 0.4.0 (used for experiments on the DARTS search space)
- pybnn (used only for the DNGO baselien algorithm. Installation instructions [here](https://github.com/automl/pybnn))
If you run experiments on DARTS, you will need our fork of the darts repo:
- Download the repo: https://github.com/naszilla/darts
- If the repo is not in your home directory, i.e., `~/darts`, then update line 5 of `bananas/darts/arch.py` and line 8 of `bananas/train_arch_runner.py` with the correct path to this repo
## Train a meta neural network with a notebook on the NASBench dataset
- Download the nasbench_only108 tfrecord file (size 499MB) [here](https://storage.googleapis.com/nasbench/nasbench_only108.tfrecord)
- Place `nasbench_only108.tfrecord` in the top level folder of this repo
- Open and run `meta_neuralnet.ipynb` to reproduce Table 1 and Figure A.1 of our paper
## Evaluate pretrained BANANAS architecture
The best architecture found by BANANAS on the DARTS search space achieved 2.57% test error. To evaluate our pretrained neural architecture, download the weights [bananas.pt](https://drive.google.com/file/d/1d8jnI0R9fvXBjkIY7CRogyxynEh6TWu_/view?usp=sharing) and put it inside the folder `/cnn`
```bash
cd /cnn; python test.py --model_path bananas.pt
```
The error on the test set should be 2.57%. This can be run on a CPU or GPU, but it will be faster on a GPU.
The best neural architecture found by BANANAS on CIFAR-10. Convolutional cell (left), and reduction cell (right).
## Train BANANAS architecture
Train the best architecture found by BANANAS.
```bash
cd /cnn; python train.py --auxiliary --cutout
```
This will train the architecture from scratch, which takes about 34 hours on an NVIDIA V100 GPU.
The final test error should be 2.59%.
Setting the random seed to 4 by adding `--seed 4` will result in a test error of 2.57%.
We report the random seeds and hardware used in Table 2 of our paper [here](https://docs.google.com/spreadsheets/d/1z6bHUgX8r0y9Bh9Zxot_B9nT_9qLWJoD0Um0fTYdpus/edit?usp=sharing).
## Run BANANAS on the NASBench search space
To run BANANAS on NASBench, download `nasbench_only108.tfrecord` and place it in the top level folder of this repo.
```bash
python run_experiments_sequential.py
```
This will test the nasbench algorithm against several other NAS algorithms on the NASBench search space.
To customize your experiment, open `params.py`. Here, you can change the hyperparameters and the algorithms to run.
To run experiments with NAS-Bench-201, download `NAS-Bench-201-v1_0-e61699.pth` and place it in the top level folder of this repo.
Choose between cifar10, cifar100, and imagenet. For example,
```bash
python run_experiments_sequential.py --search_space nasbench_201_cifar10
```
## Run BANANAS on the DARTS search space
We highly recommend using multiple GPUs to run BANANAS on the DARTS search space. You can run BANANAS in parallel on GCP using the shell script:
```bash
run_experiments_parallel.sh
```
## Contributions
We welcome community contributions to this repo!
## Citation
Please cite [our paper](https://arxiv.org/abs/1910.11858) if you use code from this repo:
```bibtex
@inproceedings{white2019bananas,
title={BANANAS: Bayesian Optimization with Neural Architectures for Neural Architecture Search},
author={White, Colin and Neiswanger, Willie and Savani, Yash},
booktitle={Proceedings of the AAAI Conference on Artificial Intelligence},
year={2021}
}
```
================================================
FILE: acquisition_functions.py
================================================
import numpy as np
import sys
# Different acquisition functions that can be used by BANANAS
def acq_fn(predictions, explore_type='its'):
predictions = np.array(predictions)
# Upper confidence bound (UCB) acquisition function
if explore_type == 'ucb':
explore_factor = 0.5
mean = np.mean(predictions, axis=0)
std = np.sqrt(np.var(predictions, axis=0))
ucb = mean - explore_factor * std
sorted_indices = np.argsort(ucb)
# Expected improvement (EI) acquisition function
elif explore_type == 'ei':
ei_calibration_factor = 5.
mean = list(np.mean(predictions, axis=0))
std = list(np.sqrt(np.var(predictions, axis=0)) /
ei_calibration_factor)
min_y = ytrain.min()
gam = [(min_y - mean[i]) / std[i] for i in range(len(mean))]
ei = [-1 * std[i] * (gam[i] * norm.cdf(gam[i]) + norm.pdf(gam[i]))
for i in range(len(mean))]
sorted_indices = np.argsort(ei)
# Probability of improvement (PI) acquisition function
elif explore_type == 'pi':
mean = list(np.mean(predictions, axis=0))
std = list(np.sqrt(np.var(predictions, axis=0)))
min_y = ytrain.min()
pi = [-1 * norm.cdf(min_y, loc=mean[i], scale=std[i]) for i in range(len(mean))]
sorted_indices = np.argsort(pi)
# Thompson sampling (TS) acquisition function
elif explore_type == 'ts':
rand_ind = np.random.randint(predictions.shape[0])
ts = predictions[rand_ind,:]
sorted_indices = np.argsort(ts)
# Top exploitation
elif explore_type == 'percentile':
min_prediction = np.min(predictions, axis=0)
sorted_indices = np.argsort(min_prediction)
# Top mean
elif explore_type == 'mean':
mean = np.mean(predictions, axis=0)
sorted_indices = np.argsort(mean)
elif explore_type == 'confidence':
confidence_factor = 2
mean = np.mean(predictions, axis=0)
std = np.sqrt(np.var(predictions, axis=0))
conf = mean + confidence_factor * std
sorted_indices = np.argsort(conf)
# Independent Thompson sampling (ITS) acquisition function
elif explore_type == 'its':
mean = np.mean(predictions, axis=0)
std = np.sqrt(np.var(predictions, axis=0))
samples = np.random.normal(mean, std)
sorted_indices = np.argsort(samples)
else:
print('Invalid exploration type in meta neuralnet search', explore_type)
sys.exit()
return sorted_indices
================================================
FILE: bo/__init__.py
================================================
"""
Code for running Bayesian Optimization (BO) in NASzilla.
"""
================================================
FILE: bo/acq/__init__.py
================================================
"""
Code for acquisition strategies.
"""
================================================
FILE: bo/acq/acqmap.py
================================================
"""
Classes to manage acqmap (acquisition maps from xin to acquisition value).
"""
from argparse import Namespace
import numpy as np
import copy
from bo.acq.acquisition import Acquisitioner
from bo.util.datatransform import DataTransformer
#from bo.pp.pp_gp_george import GeorgeGpPP
#from bo.pp.pp_gp_stan import StanGpPP
from bo.pp.pp_gp_my_distmat import MyGpDistmatPP
class AcqMapper(object):
""" Class to manage acqmap (acquisition map). """
def __init__(self, data, amp, print_flag=True):
""" Constructor
Parameters:
amp - Namespace of acqmap params
print_flag - True or False
"""
self.data = data
self.set_am_params(amp)
#self.setup_acqmap()
if print_flag: self.print_str()
def set_am_params(self, amp):
""" Set the acqmap params.
Inputs:
amp - Namespace of acqmap parameters """
self.amp = amp
def get_acqmap(self, xin_is_list=True):
""" Return acqmap.
Inputs: xin_is_list True if input to acqmap is a list of xin """
# Potentially do acqmap setup here. Could include inference,
# cachining/computing quantities, instantiating objects used in acqmap
# definition. This becomes important when we do sequential opt of acqmaps.
return self.acqmap_list if xin_is_list else self.acqmap_single
def acqmap_list(self, xin_list):
""" Acqmap defined on a list of xin. """
def get_trans_data():
""" Returns transformed data. """
dt = DataTransformer(self.data.y, False)
return Namespace(X=self.data.X, y=dt.transform_data(self.data.y))
def apply_acq_to_pmlist(pmlist, acq_str, trans_data):
""" Apply acquisition to pmlist. """
acqp = Namespace(acq_str=acq_str, pmout_str='sample')
acq = Acquisitioner(trans_data, acqp, False)
acqfn = acq.acq_method
return [acqfn(p) for p in pmlist]
def georgegp_acqmap(acq_str):
""" Acqmaps for GeorgeGpPP """
trans_data = get_trans_data()
pp = GeorgeGpPP(trans_data, self.amp.modelp, False)
pmlist = pp.sample_pp_pred(self.amp.nppred, xin_list) if acq_str=='ts' \
else pp.sample_pp_post_pred(self.amp.nppred, xin_list)
return apply_acq_to_pmlist(pmlist, acq_str, trans_data)
def stangp_acqmap(acq_str):
""" Acqmaps for StanGpPP """
trans_data = get_trans_data()
pp = StanGpPP(trans_data, self.amp.modelp, False)
pp.infer_post_and_update_samples(print_result=True)
pmlist, _ = pp.sample_pp_pred(self.amp.nppred, xin_list) if acq_str=='ts' \
else pp.sample_pp_post_pred(self.amp.nppred, xin_list, full_cov=True, \
nloop=np.min([50,self.amp.nppred]))
return apply_acq_to_pmlist(pmlist, acq_str, trans_data)
def mygpdistmat_acqmap(acq_str):
""" Acqmaps for MyGpDistmatPP """
trans_data = get_trans_data()
pp = MyGpDistmatPP(trans_data, self.amp.modelp, False)
pp.infer_post_and_update_samples(print_result=True)
pmlist, _ = pp.sample_pp_pred(self.amp.nppred, xin_list) if acq_str=='ts' \
else pp.sample_pp_post_pred(self.amp.nppred, xin_list, full_cov=True)
return apply_acq_to_pmlist(pmlist, acq_str, trans_data)
# Mapping of am_str to acqmap
if self.amp.am_str=='georgegp_ei':
return georgegp_acqmap('ei')
elif self.amp.am_str=='georgegp_pi':
return georgegp_acqmap('pi')
elif self.amp.am_str=='georgegp_ucb':
return georgegp_acqmap('ucb')
elif self.amp.am_str=='georgegp_ts':
return georgegp_acqmap('ts')
elif self.amp.am_str=='stangp_ei':
return stangp_acqmap('ei')
elif self.amp.am_str=='stangp_pi':
return stangp_acqmap('pi')
elif self.amp.am_str=='stangp_ucb':
return stangp_acqmap('ucb')
elif self.amp.am_str=='stangp_ts':
return stangp_acqmap('ts')
elif self.amp.am_str=='mygpdistmat_ei':
return mygpdistmat_acqmap('ei')
elif self.amp.am_str=='mygpdistmat_pi':
return mygpdistmat_acqmap('pi')
elif self.amp.am_str=='mygpdistmat_ucb':
return mygpdistmat_acqmap('ucb')
elif self.amp.am_str=='mygpdistmat_ts':
return mygpdistmat_acqmap('ts')
elif self.amp.am_str=='null':
return [0. for xin in xin_list]
def acqmap_single(self, xin):
""" Acqmap defined on a single xin. Returns acqmap(xin) value, not list. """
return self.acqmap_list([xin])[0]
def print_str(self):
""" Print a description string """
print('*AcqMapper with amp='+str(self.amp)
+'.\n-----')
================================================
FILE: bo/acq/acqopt.py
================================================
"""
Classes to perform acquisition function optimization.
"""
from argparse import Namespace
import numpy as np
class AcqOptimizer(object):
""" Class to perform acquisition function optimization """
def __init__(self, optp=None, print_flag=True):
""" Constructor
Inputs:
optp - Namespace of opt parameters
print_flag - True or False
"""
self.set_opt_params(optp)
if print_flag: self.print_str()
def set_opt_params(self, optp):
""" Set the optimizer params.
Inputs:
acqp - Namespace of acquisition parameters """
if optp is None:
optp = Namespace(opt_str='rand', max_iter=1000)
self.optp = optp
def optimize(self, dom, am):
""" Optimize acqfn(probmap(x)) over x in domain """
if self.optp.opt_str=='rand':
return self.optimize_rand(dom, am)
def optimize_rand(self, dom, am):
""" Optimize acqmap(x) over domain via random search """
xin_list = dom.unif_rand_sample(self.optp.max_iter)
amlist = am.acqmap_list(xin_list)
return xin_list[np.argmin(amlist)]
# Utilities
def print_str(self):
""" print a description string """
print('*AcqOptimizer with optp='+str(self.optp)
+'.\n-----')
================================================
FILE: bo/acq/acquisition.py
================================================
"""
Classes to manage acquisition functions.
"""
from argparse import Namespace
import numpy as np
from scipy.stats import norm
class Acquisitioner(object):
""" Class to manage acquisition functions """
def __init__(self, data, acqp=None, print_flag=True):
""" Constructor
Parameters:
acqp - Namespace of acquisition parameters
print_flag - True or False
"""
self.data = data
self.set_acq_params(acqp)
self.set_acq_method()
if print_flag: self.print_str()
def set_acq_params(self, acqp):
""" Set the acquisition params.
Parameters:
acqp - Namespace of acquisition parameters """
if acqp is None:
acqp = Namespace(acq_str='ei', pmout_str='sample')
self.acqp = acqp
def set_acq_method(self):
""" Set the acquisition method """
if self.acqp.acq_str=='ei': self.acq_method = self.ei
if self.acqp.acq_str=='pi': self.acq_method = self.pi
if self.acqp.acq_str=='ts': self.acq_method = self.ts
if self.acqp.acq_str=='ucb': self.acq_method = self.ucb
if self.acqp.acq_str=='rand': self.acq_method = self.rand
if self.acqp.acq_str=='null': self.acq_method = self.null
#if self.acqp.acqStr=='map': return self.map
def ei(self, pmout):
""" Expected improvement (EI) """
if self.acqp.pmout_str=='sample':
return self.bbacq_ei(pmout)
def pi(self, pmout):
""" Probability of improvement (PI) """
if self.acqp.pmout_str=='sample':
return self.bbacq_pi(pmout)
def ucb(self, pmout):
""" Upper (lower) confidence bound (UCB) """
if self.acqp.pmout_str=='sample':
return self.bbacq_ucb(pmout)
def ts(self, pmout):
""" Thompson sampling (TS) """
if self.acqp.pmout_str=='sample':
return self.bbacq_ts(pmout)
def rand(self, pmout):
""" Uniform random sampling """
return np.random.random()
def null(self, pmout):
""" Return constant 0. """
return 0.
# Black Box Acquisition Functions
def bbacq_ei(self, pmout_samp, normal=False):
""" Black box acquisition: BB-EI
Input: pmout_samp: post-pred samples - np array (shape=(nsamp,1))
Returns: EI acq value """
youts = np.array(pmout_samp).flatten()
nsamp = youts.shape[0]
if normal:
mu = np.mean(youts)
sig = np.std(youts)
gam = (self.data.y.min() - mu) / sig
eiVal = -1*sig*(gam*norm.cdf(gam) + norm.pdf(gam))
else:
diffs = self.data.y.min() - youts
ind_below_min = np.argwhere(diffs>0)
eiVal = -1*np.sum(diffs[ind_below_min])/float(nsamp) if \
len(ind_below_min)>0 else 0
return eiVal
def bbacq_pi(self, pmout_samp, normal=False):
""" Black box acquisition: BB-PI
Input: pmout_samp: post-pred samples - np array (shape=(nsamp,1))
Returns: PI acq value """
youts = np.array(pmout_samp).flatten()
nsamp = youts.shape[0]
if normal:
mu = np.mean(youts)
sig = np.sqrt(np.var(youts))
piVal = -1*norm.cdf(self.data.y.min(),loc=mu,scale=sig)
else:
piVal = -1*len(np.argwhere(youts=self.domp.min_max[i][0] and
pt[i]<=self.domp.min_max[i][1] for i in range(self.ndimx)]
ret=False if False in bool_list else True
return ret
def unif_rand_sample(self, n=1):
""" Draws a sample uniformly at random from domain """
li = [np.random.uniform(mm[0], mm[1], n) for mm in self.domp.min_max]
return np.array(li).T
def print_str(self):
""" Print a description string """
print('*RealDomain with domp = ' + str(self.domp) + '.')
print('-----')
================================================
FILE: bo/ds/__init__.py
================================================
"""
Code for makept (serializing and subprocesses) strategy.
"""
================================================
FILE: bo/ds/makept.py
================================================
"""
Make a point in a domain, and serialize it.
"""
import sys
import os
sys.path.append(os.path.expanduser('./'))
from argparse import Namespace, ArgumentParser
import pickle
import time
import numpy as np
from bo.dom.real import RealDomain
from bo.dom.list import ListDomain
from bo.acq.acqmap import AcqMapper
from bo.acq.acqopt import AcqOptimizer
def main(args, search_space, printinfo=False):
starttime = time.time()
# Load config and data
makerp = pickle.load(open(args.configpkl, 'rb'))
data = pickle.load(open(args.datapkl, 'rb'))
if hasattr(args, 'mode') and args.mode == 'single_process':
makerp.domp.mode = args.mode
makerp.domp.iteridx = args.iteridx
makerp.amp.modelp.mode = args.mode
else:
np.random.seed(args.seed)
# Instantiate Domain, AcqMapper, AcqOptimizer
dom = get_domain(makerp.domp, search_space)
am = AcqMapper(data, makerp.amp, False)
ao = AcqOptimizer(makerp.optp, False)
# Optimize over domain to get nextpt
nextpt = ao.optimize(dom, am)
# Serialize nextpt
with open(args.nextptpkl, 'wb') as f:
pickle.dump(nextpt, f)
# Print
itertime = time.time()-starttime
if printinfo: print_info(nextpt, itertime, args.nextptpkl)
def get_domain(domp, search_space):
""" Return Domain object. """
if not hasattr(domp, 'dom_str'):
domp.dom_str = 'real'
if domp.dom_str=='real':
return RealDomain(domp, False)
elif domp.dom_str=='list':
return ListDomain(search_space, domp, False)
def print_info(nextpt, itertime, nextptpkl):
print('*Found nextpt = ' + str(nextpt) + '.')
print('*Saved nextpt as ' + nextptpkl + '.')
print('*Timing: makept took ' + str(itertime) + ' seconds.')
print('-----')
if __name__ == "__main__":
parser = ArgumentParser(description='Args for a single instance of acquisition optimization.')
parser.add_argument('--seed', dest='seed', type=int, default=1111)
parser.add_argument('--configpkl', dest='configpkl', type=str, default='config.pkl')
parser.add_argument('--datapkl', dest='datapkl', type=str, default='data.pkl')
parser.add_argument('--nextptpkl', dest='nextptpkl', type=str, default='nextpt.pkl')
args = parser.parse_args()
main(args, printinfo=False)
================================================
FILE: bo/fn/__init__.py
================================================
"""
Code for synthetic functions to query (perform experiment on).
"""
================================================
FILE: bo/fn/functionhandler.py
================================================
"""
Classes to handle functions.
"""
from argparse import Namespace
import numpy as np
def get_fh(fn, data=None, fhp=None, print_flag=True):
""" Returns a function handler object """
if fhp is None:
fhp=Namespace(fhstr='basic', namestr='noname')
# Return FH object
if fhp.fhstr=='basic':
return BasicFH(fn, data, fhp, print_flag)
elif fhp.fhstr=='extrainfo':
return ExtraInfoFH(fn, data, fhp, print_flag)
elif fhp.fhstr=='nannn':
return NanNNFH(fn, data, fhp, print_flag)
elif fhp.fhstr=='replacenannn':
return ReplaceNanNNFH(fn, data, fhp, print_flag)
elif fhp.fhstr=='object':
return ObjectFH(fn, data, fhp, print_flag)
class BasicFH(object):
""" Class to handle basic functions, which map from an array xin to a real
value yout. """
def __init__(self, fn, data=None, fhp=None, print_flag=True):
""" Constructor.
Inputs:
pmp - Namespace of probmap params
print_flag - True or False
"""
self.fn = fn
self.data = data
self.fhp = fhp
if print_flag: self.print_str()
def call_fn_and_add_data(self, xin):
""" Call self.fn(xin), and update self.data """
yout = self.fn(xin)
print('new datapoint score', yout)
self.add_data_single(xin, yout)
def add_data_single(self, xin, yout):
""" Update self.data with a single xin yout pair.
Inputs:
xin: np.array size=(1, -1)
yout: np.array size=(1, 1) """
xin = np.array(xin).reshape(1, -1)
yout = np.array(yout).reshape(1, 1)
newdata = Namespace(X=xin, y=yout)
self.add_data(newdata)
def add_data(self, newdata):
""" Update self.data with newdata Namespace.
Inputs:
newdata: Namespace with fields X and y """
if self.data is None:
self.data = newdata
else:
self.data.X = np.concatenate((self.data.X, newdata.X), 0)
self.data.y = np.concatenate((self.data.y, newdata.y), 0)
def print_str(self):
""" Print a description string. """
print('*BasicFH with fhp='+str(self.fhp)
+'.\n-----')
class ExtraInfoFH(BasicFH):
""" Class to handle functions that map from an array xin to a real
value yout, but also return extra info """
def __init__(self, fn, data=None, fhp=None, print_flag=True):
""" Constructor.
Inputs:
pmp - Namespace of probmap params
print_flag - True or False
"""
super(ExtraInfoFH, self).__init__(fn, data, fhp, False)
self.extrainfo = []
if print_flag: self.print_str()
def call_fn_and_add_data(self, xin):
""" Call self.fn(xin), and update self.data """
yout, exinf = self.fn(xin)
self.add_data_single(xin, yout)
self.extrainfo.append(exinf)
def print_str(self):
""" Print a description string. """
print('*ExtraInfoFH with fhp='+str(self.fhp)
+'.\n-----')
class NanNNFH(BasicFH):
""" Class to handle NN functions that map from an array xin to either
a real value yout or np.NaN, but also return extra info """
def __init__(self, fn, data=None, fhp=None, print_flag=True):
""" Constructor.
Inputs:
pmp - Namespace of probmap params
print_flag - True or False
"""
super(NanNNFH, self).__init__(fn, data, fhp, False)
self.extrainfo = []
if print_flag: self.print_str()
def call_fn_and_add_data(self, xin):
""" Call self.fn(xin), and update self.data """
timethresh = 60.
yout, walltime = self.fn(xin)
if walltime > timethresh:
self.add_data_single_nan(xin)
else:
self.add_data_single(xin, yout)
self.possibly_init_xnan()
exinf = Namespace(xin=xin, yout=yout, walltime=walltime)
self.extrainfo.append(exinf)
def add_data_single_nan(self, xin):
""" Update self.data.X_nan with a single xin.
Inputs:
xin: np.array size=(1, -1) """
xin = xin.reshape(1,-1)
newdata = Namespace(X = np.ones((0, xin.shape[1])),
y = np.ones((0, 1)),
X_nan = xin)
self.add_data_nan(newdata)
def add_data_nan(self, newdata):
""" Update self.data with newdata Namespace.
Inputs:
newdata: Namespace with fields X, y, X_nan """
if self.data is None:
self.data = newdata
else:
self.data.X_nan = np.concatenate((self.data.X_nan, newdata.X_nan), 0)
def possibly_init_xnan(self):
""" If self.data doesn't have X_nan, then create it. """
if not hasattr(self.data, 'X_nan'):
self.data.X_nan = np.ones((0, self.data.X.shape[1]))
def print_str(self):
""" Print a description string. """
print('*NanNNFH with fhp='+str(self.fhp)
+'.\n-----')
class ReplaceNanNNFH(BasicFH):
""" Class to handle NN functions that map from an array xin to either
a real value yout or np.NaN. If np.NaN, we replace it with a large
positive value. We also return extra info """
def __init__(self, fn, data=None, fhp=None, print_flag=True):
""" Constructor.
Inputs:
pmp - Namespace of probmap params
print_flag - True or False
"""
super(ReplaceNanNNFH, self).__init__(fn, data, fhp, False)
self.extrainfo = []
if print_flag: self.print_str()
def call_fn_and_add_data(self, xin):
""" Call self.fn(xin), and update self.data """
timethresh = 60.
replace_nan_val = 5.
yout, walltime = self.fn(xin)
if walltime > timethresh:
yout = replace_nan_val
self.add_data_single(xin, yout)
exinf = Namespace(xin=xin, yout=yout, walltime=walltime)
self.extrainfo.append(exinf)
def print_str(self):
""" Print a description string. """
print('*ReplaceNanNNFH with fhp='+str(self.fhp)
+'.\n-----')
class ObjectFH(object):
""" Class to handle basic functions, which map from some object xin to a real
value yout. """
def __init__(self, fn, data=None, fhp=None, print_flag=True):
""" Constructor.
Inputs:
pmp - Namespace of probmap params
print_flag - True or False
"""
self.fn = fn
self.data = data
self.fhp = fhp
if print_flag: self.print_str()
def call_fn_and_add_data(self, xin):
""" Call self.fn(xin), and update self.data """
yout = self.fn(xin)
self.add_data_single(xin, yout)
def add_data_single(self, xin, yout):
""" Update self.data with a single xin yout pair. """
newdata = Namespace(X=[xin], y=np.array(yout).reshape(1, 1))
self.add_data(newdata)
def add_data(self, newdata):
""" Update self.data with newdata Namespace.
Inputs:
newdata: Namespace with fields X and y """
if self.data is None:
self.data = newdata
else:
self.data.X.extend(newdata.X)
self.data.y = np.concatenate((self.data.y, newdata.y), 0)
def print_str(self):
""" Print a description string. """
print('*ObjectFH with fhp='+str(self.fhp)
+'.\n-----')
================================================
FILE: bo/pp/__init__.py
================================================
"""
Code for defining and running probabilistic programs.
"""
================================================
FILE: bo/pp/gp/__init__.py
================================================
"""
Code for Gaussian process (GP) utilities and functions.
"""
================================================
FILE: bo/pp/gp/gp_utils.py
================================================
"""
Utilities for Gaussian process (GP) inference
"""
import numpy as np
from scipy.linalg import solve_triangular
from scipy.spatial.distance import cdist
#import GPy as gpy
def kern_gibbscontext(xmatcon1, xmatcon2, xmatact1, xmatact2, theta, alpha,
lscon, whichlsfn=1):
""" Gibbs kernel (ls_fn of context only) """
actdim = xmatact1.shape[1]
lsarr1 = ls_fn(xmatcon1, theta, whichlsfn).flatten()
lsarr2 = ls_fn(xmatcon2, theta, whichlsfn).flatten()
sum_sq_ls = np.add.outer(lsarr1, lsarr2)
inexp = -1. * np.divide(cdist(xmatact1, xmatact2, 'sqeuclidean'), sum_sq_ls)
prod_ls = np.outer(lsarr1, lsarr2)
#coef = np.power(np.divide(2*prod_ls, sum_sq_ls), actdim/2.) # Correct
coef = 1.
kern_gibbscontext_only_ns = np.multiply(coef, np.exp(inexp))
kern_expquad_ns = kern_exp_quad_noscale(xmatcon1, xmatcon2, lscon)
return alpha**2 * np.multiply(kern_gibbscontext_only_ns, kern_expquad_ns)
def kern_gibbs1d(xmat1, xmat2, theta, alpha):
""" Gibbs kernel in 1d """
lsarr1 = ls_fn(xmat1, theta).flatten()
lsarr2 = ls_fn(xmat2, theta).flatten()
sum_sq_ls = np.add.outer(lsarr1, lsarr2)
prod_ls = np.outer(lsarr1, lsarr2) #TODO product of this for each dim
coef = np.sqrt(np.divide(2*prod_ls, sum_sq_ls))
inexp = cdist(xmat1, xmat2, 'sqeuclidean') / sum_sq_ls #TODO sum of this for each dim
return alpha**2 * coef * np.exp(-1 * inexp)
def ls_fn(xmat, theta, whichlsfn=1):
theta = np.array(theta).reshape(-1,1)
if theta.shape[0]==2:
if whichlsfn==1 or whichlsfn==2:
return np.log(1 + np.exp(theta[0][0] + np.matmul(xmat,theta[1]))) # softplus transform
elif whichlsfn==3:
return np.exp(theta[0][0] + np.matmul(xmat,theta[1])) # exp transform
elif theta.shape[0]==3:
if whichlsfn==1:
return np.log(1 + np.exp(theta[0][0] + np.matmul(xmat,theta[1]) +
np.matmul(np.power(xmat,2),theta[2]))) # softplus transform
elif whichlsfn==2:
return np.log(1 + np.exp(theta[0][0] + np.matmul(xmat,theta[1]) +
np.matmul(np.abs(xmat),theta[2]))) # softplus on abs transform
elif whichlsfn==3:
return np.exp(theta[0][0] + np.matmul(xmat,theta[1]) +
np.matmul(np.power(xmat,2),theta[2])) # exp transform
else:
print('ERROR: theta parameter is incorrect.')
def kern_matern32(xmat1, xmat2, ls, alpha):
""" Matern 3/2 kernel, currently using GPy """
kern = gpy.kern.Matern32(input_dim=xmat1.shape[1], variance=alpha**2,
lengthscale=ls)
return kern.K(xmat1,xmat2)
def kern_exp_quad(xmat1, xmat2, ls, alpha):
""" Exponentiated quadratic kernel function aka squared exponential kernel
aka RBF kernel """
return alpha**2 * kern_exp_quad_noscale(xmat1, xmat2, ls)
def kern_exp_quad_noscale(xmat1, xmat2, ls):
""" Exponentiated quadratic kernel function aka squared exponential kernel
aka RBF kernel, without scale parameter. """
sq_norm = (-1/(2 * ls**2)) * cdist(xmat1, xmat2, 'sqeuclidean')
return np.exp(sq_norm)
def squared_euc_distmat(xmat1, xmat2, coef=1.):
""" Distance matrix of squared euclidean distance (multiplied by coef)
between points in xmat1 and xmat2. """
return coef * cdist(xmat1, xmat2, 'sqeuclidean')
def kern_distmat(xmat1, xmat2, ls, alpha, distfn):
""" Kernel for a given distmat, via passed-in distfn (which is assumed to be
fn of xmat1 and xmat2 only) """
distmat = distfn(xmat1, xmat2)
sq_norm = -distmat / ls**2
return alpha**2 * np.exp(sq_norm)
def get_cholesky_decomp(k11_nonoise, sigma, psd_str):
""" Returns cholesky decomposition """
if psd_str == 'try_first':
k11 = k11_nonoise + sigma**2 * np.eye(k11_nonoise.shape[0])
try:
return stable_cholesky(k11, False)
except np.linalg.linalg.LinAlgError:
return get_cholesky_decomp(k11_nonoise, sigma, 'project_first')
elif psd_str == 'project_first':
k11_nonoise = project_symmetric_to_psd_cone(k11_nonoise)
return get_cholesky_decomp(k11_nonoise, sigma, 'is_psd')
elif psd_str == 'is_psd':
k11 = k11_nonoise + sigma**2 * np.eye(k11_nonoise.shape[0])
return stable_cholesky(k11)
def stable_cholesky(mmat, make_psd=True):
""" Returns a 'stable' cholesky decomposition of mmat """
if mmat.size == 0:
return mmat
try:
lmat = np.linalg.cholesky(mmat)
except np.linalg.linalg.LinAlgError as e:
if not make_psd:
raise e
diag_noise_power = -11
max_mmat = np.diag(mmat).max()
diag_noise = np.diag(mmat).max() * 1e-11
break_loop = False
while not break_loop:
try:
lmat = np.linalg.cholesky(mmat + ((10**diag_noise_power) * max_mmat) *
np.eye(mmat.shape[0]))
break_loop = True
except np.linalg.linalg.LinAlgError:
if diag_noise_power > -9:
print('stable_cholesky failed with diag_noise_power=%d.'%(diag_noise_power))
diag_noise_power += 1
if diag_noise_power >= 5:
print('***** stable_cholesky failed: added diag noise = %e'%(diag_noise))
return lmat
def project_symmetric_to_psd_cone(mmat, is_symmetric=True, epsilon=0):
""" Project symmetric matrix mmat to the PSD cone """
if is_symmetric:
try:
eigvals, eigvecs = np.linalg.eigh(mmat)
except np.linalg.LinAlgError:
print('LinAlgError encountered with np.eigh. Defaulting to eig.')
eigvals, eigvecs = np.linalg.eig(mmat)
eigvals = np.real(eigvals)
eigvecs = np.real(eigvecs)
else:
eigvals, eigvecs = np.linalg.eig(mmat)
clipped_eigvals = np.clip(eigvals, epsilon, np.inf)
return (eigvecs * clipped_eigvals).dot(eigvecs.T)
def solve_lower_triangular(amat, b):
""" Solves amat*x=b when amat is lower triangular """
return solve_triangular_base(amat, b, lower=True)
def solve_upper_triangular(amat, b):
""" Solves amat*x=b when amat is upper triangular """
return solve_triangular_base(amat, b, lower=False)
def solve_triangular_base(amat, b, lower):
""" Solves amat*x=b when amat is a triangular matrix. """
if amat.size == 0 and b.shape[0] == 0:
return np.zeros((b.shape))
else:
return solve_triangular(amat, b, lower=lower)
def sample_mvn(mu, covmat, nsamp):
""" Sample from multivariate normal distribution with mean mu and covariance
matrix covmat """
mu = mu.reshape(-1,)
ndim = len(mu)
lmat = stable_cholesky(covmat)
umat = np.random.normal(size=(ndim, nsamp))
return lmat.dot(umat).T + mu
================================================
FILE: bo/pp/pp_core.py
================================================
"""
Base classes for probabilistic programs.
"""
import pickle
class DiscPP(object):
""" Parent class for discriminative probabilistic programs """
def __init__(self):
""" Constructor """
self.sample_list = []
if not hasattr(self,'data'):
raise NotImplementedError('Implement var data in a child class')
#if not hasattr(self,'ndimx'):
#raise NotImplementedError('Implement var ndimx in a child class')
#if not hasattr(self,'ndataInit'):
#raise NotImplementedError('Implement var ndataInit in a child class')
def infer_post_and_update_samples(self,nsamp):
""" Run an inference algorithm (given self.data), draw samples from the
posterior, and store in self.sample_list. """
raise NotImplementedError('Implement method in a child class')
def sample_pp_post_pred(self,nsamp,input_list):
""" Sample nsamp times from PP posterior predictive, for each x-input in
input_list """
raise NotImplementedError('Implement method in a child class')
def sample_pp_pred(self,nsamp,input_list,lv_list=None):
""" Sample nsamp times from PP predictive for parameter lv, for each
x-input in input_list. If lv is None, draw it uniformly at random
from self.sample_list. """
raise NotImplementedError('Implement method in a child class')
def add_new_data(self,newData):
""" Add data (newData) to self.data """
raise NotImplementedError('Implement method in a child class')
def get_namespace_to_save(self):
""" Return namespace containing object info (to save to file) """
raise NotImplementedError('Implement method in a child class')
def save_namespace_to_file(self,fileStr,printFlag):
""" Saves results from get_namespace_to_save in fileStr """
ppNamespaceToSave = self.get_namespace_to_save()
ff = open(fileStr,'wb')
pickle.dump(ppNamespaceToSave,ff)
ff.close()
if printFlag:
print('*Saved DiscPP Namespace in pickle file: ' +fileStr+'\n-----')
================================================
FILE: bo/pp/pp_gp_george.py
================================================
"""
Classes for hierarchical GP models with George PP
"""
from argparse import Namespace
import numpy as np
import scipy.optimize as spo
import george
import emcee
from bo.pp.pp_core import DiscPP
class GeorgeGpPP(DiscPP):
""" Hierarchical GPs implemented with George """
def __init__(self,data=None,modelp=None,printFlag=True):
""" Constructor """
self.set_data(data)
self.set_model_params(modelp)
self.ndimx = self.modelp.ndimx
self.set_kernel()
self.set_model()
super(GeorgeGpPP,self).__init__()
if printFlag:
self.print_str()
def set_data(self,data):
if data is None:
pass #TODO: handle case where there's no data
self.data = data
def set_model_params(self,modelp):
if modelp is None:
modelp = Namespace(ndimx=1, noiseVar=1e-3, kernLs=1.5, kernStr='mat',
fitType='mle')
self.modelp = modelp
def set_kernel(self):
""" Set kernel for GP """
if self.modelp.kernStr=='mat':
self.kernel = self.data.y.var() * \
george.kernels.Matern52Kernel(self.modelp.kernLs, ndim=self.ndimx)
if self.modelp.kernStr=='rbf': # NOTE: periodically produces errors
self.kernel = self.data.y.var() * \
george.kernels.ExpSquaredKernel(self.modelp.kernLs, ndim=self.ndimx)
def set_model(self):
""" Set GP regression model """
self.model = self.get_model()
self.model.compute(self.data.X)
self.fit_hyperparams(printOut=False)
def get_model(self):
""" Returns GPRegression model """
return george.GP(kernel=self.kernel,fit_mean=True)
def fit_hyperparams(self,printOut=False):
if self.modelp.fitType=='mle':
spo.minimize(self.neg_log_like, self.model.get_parameter_vector(),
jac=True)
elif self.modelp.fitType=='bayes':
self.nburnin = 200
nsamp = 200
nwalkers = 36
gpdim = len(self.model)
self.sampler = emcee.EnsembleSampler(nwalkers, gpdim, self.log_post)
p0 = self.model.get_parameter_vector() + 1e-4*np.random.randn(nwalkers,
gpdim)
print 'Running burn-in.'
p0, _, _ = self.sampler.run_mcmc(p0, self.nburnin)
print 'Running main chain.'
self.sampler.run_mcmc(p0, nsamp)
if printOut:
print 'Final GP hyperparam (in opt or MCMC chain):'
print self.model.get_parameter_dict()
def infer_post_and_update_samples(self):
""" Update self.sample_list """
self.sample_list = [None] #TODO: need to not-break ts fn in maker_bayesopt.py
def sample_pp_post_pred(self,nsamp,input_list):
""" Sample from posterior predictive of PP.
Inputs:
input_list - list of np arrays size=(-1,)
Returns:
list (len input_list) of np arrays (size=(nsamp,1))."""
inputArray = np.array(input_list)
if self.modelp.fitType=='mle':
inputArray = np.array(input_list)
ppredArray = self.model.sample_conditional(self.data.y.flatten(),
inputArray, nsamp).T
elif self.modelp.fitType=='bayes':
ppredArray = np.zeros(shape=[len(input_list),nsamp])
for s in range(nsamp):
walkidx = np.random.randint(self.sampler.chain.shape[0])
sampidx = np.random.randint(self.nburnin, self.sampler.chain.shape[1])
hparamSamp = self.sampler.chain[walkidx, sampidx]
print 'hparamSamp = ' + str(hparamSamp) # TODO: remove print statement
self.model.set_parameter_vector(hparamSamp)
ppredArray[:,s] = self.model.sample_conditional(self.data.y.flatten(),
inputArray, 1).flatten()
return list(ppredArray) # each element is row in ppredArray matrix
def sample_pp_pred(self,nsamp,input_list,lv=None):
""" Sample from predictive of PP for parameter lv.
Returns: list (len input_list) of np arrays (size (nsamp,1))."""
if self.modelp.fitType=='bayes':
print('*WARNING: fitType=bayes not implemented for sample_pp_pred. \
Reverting to fitType=mle')
# TODO: Equivalent algo for fitType=='bayes':
# - draw posterior sample path over all xin in input_list
# - draw pred samples around sample path pt, based on noise model
inputArray = np.array(input_list)
samplePath = self.model.sample_conditional(self.data.y.flatten(),
inputArray).reshape(-1,)
return [np.random.normal(s,np.sqrt(self.modelp.noiseVar),nsamp).reshape(-1,)
for s in samplePath]
def neg_log_like(self,hparams):
""" Compute and return the negative log likelihood for model
hyperparameters hparams, as well as its gradient. """
self.model.set_parameter_vector(hparams)
g = self.model.grad_log_likelihood(self.data.y.flatten(), quiet=True)
return -self.model.log_likelihood(self.data.y.flatten(), quiet=True), -g
def log_post(self,hparams):
""" Compute and return the log posterior density (up to constant of
proportionality) for the model hyperparameters hparams. """
# Uniform prior between -100 and 100, for each hyperparam
if np.any((-100 > hparams[1:]) + (hparams[1:] > 100)):
return -np.inf
self.model.set_parameter_vector(hparams)
return self.model.log_likelihood(self.data.y.flatten(), quiet=True)
# Utilities
def print_str(self):
""" Print a description string """
print '*GeorgeGpPP with modelp='+str(self.modelp)+'.'
print '-----'
================================================
FILE: bo/pp/pp_gp_my_distmat.py
================================================
"""
Classes for GP models without any PP backend, using a given distance matrix.
"""
from argparse import Namespace
import time
import copy
import numpy as np
from scipy.spatial.distance import cdist
from bo.pp.pp_core import DiscPP
from bo.pp.gp.gp_utils import kern_exp_quad, kern_matern32, \
get_cholesky_decomp, solve_upper_triangular, solve_lower_triangular, \
sample_mvn, squared_euc_distmat, kern_distmat
from bo.util.print_utils import suppress_stdout_stderr
class MyGpDistmatPP(DiscPP):
""" GPs using a kernel specified by a given distance matrix, without any PP
backend """
def __init__(self, data=None, modelp=None, printFlag=True):
""" Constructor """
self.set_model_params(modelp)
self.set_data(data)
self.set_model()
super(MyGpDistmatPP,self).__init__()
if printFlag:
self.print_str()
def set_model_params(self, modelp):
""" Set self.modelp """
if modelp is None:
pass #TODO
self.modelp = modelp
def set_data(self, data):
""" Set self.data """
if data is None:
pass #TODO
self.data_init = copy.deepcopy(data)
self.data = copy.deepcopy(self.data_init)
def set_model(self):
""" Set GP regression model """
self.model = self.get_model()
def get_model(self):
""" Returns model object """
return None
def infer_post_and_update_samples(self, print_result=False):
""" Update self.sample_list """
self.sample_list = [Namespace(ls=self.modelp.kernp.ls,
alpha=self.modelp.kernp.alpha,
sigma=self.modelp.kernp.sigma)]
if print_result: self.print_inference_result()
def get_distmat(self, xmat1, xmat2):
""" Get distance matrix """
#return squared_euc_distmat(xmat1, xmat2, .5)
from data import Data
self.distmat = Data.generate_distance_matrix
#print('distmat')
#print(self.distmat(xmat1, xmat2, self.modelp.distance))
return self.distmat(xmat1, xmat2, self.modelp.distance)
def print_inference_result(self):
""" Print results of stan inference """
print('*ls pt est = '+str(self.sample_list[0].ls)+'.')
print('*alpha pt est = '+str(self.sample_list[0].alpha)+'.')
print('*sigma pt est = '+str(self.sample_list[0].sigma)+'.')
print('-----')
def sample_pp_post_pred(self, nsamp, input_list, full_cov=False):
""" Sample from posterior predictive of PP.
Inputs:
input_list - list of np arrays size=(-1,)
Returns:
list (len input_list) of np arrays (size=(nsamp,1))."""
samp = self.sample_list[0]
postmu, postcov = self.gp_post(self.data.X, self.data.y, input_list,
samp.ls, samp.alpha, samp.sigma, full_cov)
if full_cov:
ppred_list = list(sample_mvn(postmu, postcov, nsamp))
else:
ppred_list = list(np.random.normal(postmu.reshape(-1,),
postcov.reshape(-1,),
size=(nsamp, len(input_list))))
return list(np.stack(ppred_list).T), ppred_list
def sample_pp_pred(self, nsamp, input_list, lv=None):
""" Sample from predictive of PP for parameter lv.
Returns: list (len input_list) of np arrays (size (nsamp,1))."""
if lv is None:
lv = self.sample_list[0]
postmu, postcov = self.gp_post(self.data.X, self.data.y, input_list, lv.ls,
lv.alpha, lv.sigma)
pred_list = list(sample_mvn(postmu, postcov, 1)) ###TODO: sample from this mean nsamp times
return list(np.stack(pred_list).T), pred_list
def gp_post(self, x_train_list, y_train_arr, x_pred_list, ls, alpha, sigma,
full_cov=True):
""" Compute parameters of GP posterior """
kernel = lambda a, b, c, d: kern_distmat(a, b, c, d, self.get_distmat)
k11_nonoise = kernel(x_train_list, x_train_list, ls, alpha)
lmat = get_cholesky_decomp(k11_nonoise, sigma, 'try_first')
smat = solve_upper_triangular(lmat.T, solve_lower_triangular(lmat,
y_train_arr))
k21 = kernel(x_pred_list, x_train_list, ls, alpha)
mu2 = k21.dot(smat)
k22 = kernel(x_pred_list, x_pred_list, ls, alpha)
vmat = solve_lower_triangular(lmat, k21.T)
k2 = k22 - vmat.T.dot(vmat)
if full_cov is False:
k2 = np.sqrt(np.diag(k2))
return mu2, k2
# Utilities
def print_str(self):
""" Print a description string """
print('*MyGpDistmatPP with modelp='+str(self.modelp)+'.')
print('-----')
================================================
FILE: bo/pp/pp_gp_stan.py
================================================
"""
Classes for GP models with Stan
"""
from argparse import Namespace
import time
import numpy as np
import copy
from bo.pp.pp_core import DiscPP
import bo.pp.stan.gp_hier2 as gpstan2
import bo.pp.stan.gp_hier3 as gpstan3
import bo.pp.stan.gp_hier2_matern as gpstan2_matern
from bo.pp.gp.gp_utils import kern_exp_quad, kern_matern32, \
get_cholesky_decomp, solve_upper_triangular, solve_lower_triangular, \
sample_mvn
from bo.util.print_utils import suppress_stdout_stderr
class StanGpPP(DiscPP):
""" Hierarchical GPs implemented with Stan """
def __init__(self, data=None, modelp=None, printFlag=True):
""" Constructor """
self.set_model_params(modelp)
self.set_data(data)
self.ndimx = self.modelp.ndimx
self.set_model()
super(StanGpPP,self).__init__()
if printFlag:
self.print_str()
def set_model_params(self,modelp):
if modelp is None:
modelp = Namespace(ndimx=1, model_str='optfixedsig',
gp_mean_transf_str='constant')
if modelp.model_str=='optfixedsig':
modelp.kernp = Namespace(u1=.1, u2=5., n1=10., n2=10., sigma=1e-5)
modelp.infp = Namespace(niter=1000)
elif modelp.model_str=='opt' or modelp.model_str=='optmatern32':
modelp.kernp = Namespace(ig1=1., ig2=5., n1=10., n2=20., n3=.01,
n4=.01)
modelp.infp = Namespace(niter=1000)
elif modelp.model_str=='samp' or modelp.model_str=='sampmatern32':
modelp.kernp = Namespace(ig1=1., ig2=5., n1=10., n2=20., n3=.01,
n4=.01)
modelp.infp = Namespace(niter=1500, nwarmup=500)
self.modelp = modelp
def set_data(self, data):
""" Set self.data """
if data is None:
pass #TODO: handle case where there's no data
self.data_init = copy.deepcopy(data)
self.data = self.get_transformed_data(self.data_init,
self.modelp.gp_mean_transf_str)
def get_transformed_data(self, data, transf_str='linear'):
""" Transform data, for non-zero-mean GP """
newdata = Namespace(X=data.X)
if transf_str=='linear':
mmat,_,_,_ = np.linalg.lstsq(np.concatenate([data.X,
np.ones((data.X.shape[0],1))],1), data.y.flatten(), rcond=None)
self.gp_mean_vec = lambda x: np.matmul(np.concatenate([x,
np.ones((x.shape[0],1))],1), mmat)
newdata.y = data.y - self.gp_mean_vec(data.X).reshape(-1,1)
if transf_str=='constant':
yconstant = data.y.mean()
#yconstant = 0.
self.gp_mean_vec = lambda x: np.array([yconstant for xcomp in x])
newdata.y = data.y - self.gp_mean_vec(data.X).reshape(-1,1)
return newdata
def set_model(self):
""" Set GP regression model """
self.model = self.get_model()
def get_model(self):
""" Returns GPRegression model """
if self.modelp.model_str=='optfixedsig':
return gpstan3.get_model(print_status=False)
elif self.modelp.model_str=='opt' or self.modelp.model_str=='samp':
return gpstan2.get_model(print_status=False)
elif self.modelp.model_str=='optmatern32' or \
self.modelp.model_str=='sampmatern32':
return gpstan2_matern.get_model(print_status=False)
def infer_post_and_update_samples(self, seed=5000012, print_result=False):
""" Update self.sample_list """
data_dict = self.get_stan_data_dict()
with suppress_stdout_stderr():
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' \
or self.modelp.model_str=='optmatern32':
stanout = self.model.optimizing(data_dict, iter=self.modelp.infp.niter,
#seed=seed, as_vector=True, algorithm='Newton')
seed=seed, as_vector=True, algorithm='LBFGS')
elif self.modelp.model_str=='samp' or self.modelp.model_str=='sampmatern32':
stanout = self.model.sampling(data_dict, iter=self.modelp.infp.niter +
self.modelp.infp.nwarmup, warmup=self.modelp.infp.nwarmup, chains=1,
seed=seed, refresh=1000)
print('-----')
self.sample_list = self.get_sample_list_from_stan_out(stanout)
if print_result: self.print_inference_result()
def get_stan_data_dict(self):
""" Return data dict for stan sampling method """
if self.modelp.model_str=='optfixedsig':
return {'u1':self.modelp.kernp.u1, 'u2':self.modelp.kernp.u2,
'n1':self.modelp.kernp.n1, 'n2':self.modelp.kernp.n2,
'sigma':self.modelp.kernp.sigma, 'D':self.ndimx,
'N':len(self.data.X), 'x':self.data.X, 'y':self.data.y.flatten()}
elif self.modelp.model_str=='opt' or self.modelp.model_str=='samp':
return {'ig1':self.modelp.kernp.ig1, 'ig2':self.modelp.kernp.ig2,
'n1':self.modelp.kernp.n1, 'n2':self.modelp.kernp.n2,
'n3':self.modelp.kernp.n3, 'n4':self.modelp.kernp.n4,
'D':self.ndimx, 'N':len(self.data.X), 'x':self.data.X,
'y':self.data.y.flatten()}
elif self.modelp.model_str=='optmatern32' or \
self.modelp.model_str=='sampmatern32':
return {'ig1':self.modelp.kernp.ig1, 'ig2':self.modelp.kernp.ig2,
'n1':self.modelp.kernp.n1, 'n2':self.modelp.kernp.n2,
'n3':self.modelp.kernp.n3, 'n4':self.modelp.kernp.n4,
'D':self.ndimx, 'N':len(self.data.X), 'x':self.data.X,
'y':self.data.y.flatten(), 'covid':2}
def get_sample_list_from_stan_out(self, stanout):
""" Convert stan output to sample_list """
if self.modelp.model_str=='optfixedsig':
return [Namespace(ls=stanout['rho'], alpha=stanout['alpha'],
sigma=self.modelp.kernp.sigma)]
elif self.modelp.model_str=='opt' or self.modelp.model_str=='optmatern32':
return [Namespace(ls=stanout['rho'], alpha=stanout['alpha'],
sigma=stanout['sigma'])]
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampmatern32':
sdict = stanout.extract(['rho','alpha','sigma'])
return [Namespace(ls=sdict['rho'][i], alpha=sdict['alpha'][i],
sigma=sdict['sigma'][i]) for i in range(sdict['rho'].shape[0])]
def print_inference_result(self):
""" Print results of stan inference """
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' or \
self.modelp.model_str=='optmatern32':
print('*ls pt est = '+str(self.sample_list[0].ls)+'.')
print('*alpha pt est = '+str(self.sample_list[0].alpha)+'.')
print('*sigma pt est = '+str(self.sample_list[0].sigma)+'.')
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampmatern32':
ls_arr = np.array([ns.ls for ns in self.sample_list])
alpha_arr = np.array([ns.alpha for ns in self.sample_list])
sigma_arr = np.array([ns.sigma for ns in self.sample_list])
print('*ls mean = '+str(ls_arr.mean())+'.')
print('*ls std = '+str(ls_arr.std())+'.')
print('*alpha mean = '+str(alpha_arr.mean())+'.')
print('*alpha std = '+str(alpha_arr.std())+'.')
print('*sigma mean = '+str(sigma_arr.mean())+'.')
print('*sigma std = '+str(sigma_arr.std())+'.')
print('-----')
def sample_pp_post_pred(self, nsamp, input_list, full_cov=False, nloop=None):
""" Sample from posterior predictive of PP.
Inputs:
input_list - list of np arrays size=(-1,)
Returns:
list (len input_list) of np arrays (size=(nsamp,1))."""
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' or \
self.modelp.model_str=='optmatern32':
nloop = 1
sampids = [0]
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampmatern32':
if nloop is None: nloop=nsamp
nsamp = int(nsamp/nloop)
sampids = np.random.randint(len(self.sample_list), size=(nloop,))
ppred_list = []
for i in range(nloop):
samp = self.sample_list[sampids[i]]
postmu, postcov = self.gp_post(self.data.X, self.data.y,
np.stack(input_list), samp.ls, samp.alpha, samp.sigma, full_cov)
if full_cov:
ppred_list.extend(list(sample_mvn(postmu, postcov, nsamp)))
else:
ppred_list.extend(list(np.random.normal(postmu.reshape(-1,),
postcov.reshape(-1,), size=(nsamp, len(input_list)))))
return self.get_reverse_transform(list(np.stack(ppred_list).T), ppred_list,
input_list)
def sample_pp_pred(self, nsamp, input_list, lv=None):
""" Sample from predictive of PP for parameter lv.
Returns: list (len input_list) of np arrays (size (nsamp,1))."""
x_pred = np.stack(input_list)
if lv is None:
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' \
or self.modelp.model_str=='optmatern32':
lv = self.sample_list[0]
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampmatern32':
lv = self.sample_list[np.random.randint(len(self.sample_list))]
postmu, postcov = self.gp_post(self.data.X, self.data.y, x_pred, lv.ls,
lv.alpha, lv.sigma)
pred_list = list(sample_mvn(postmu, postcov, 1)) ###TODO: sample from this mean nsamp times
return self.get_reverse_transform(list(np.stack(pred_list).T), pred_list,
input_list)
def get_reverse_transform(self, pp1, pp2, input_list):
""" Apply reverse of data transform to ppred or pred """
pp1 = [pp1[i] + self.gp_mean_vec(input_list[i].reshape(1,-1)) for i in
range(len(input_list))]
pp2 = [psamp + self.gp_mean_vec(np.array(input_list)) for psamp in pp2]
return pp1, pp2
def gp_post(self, x_train, y_train, x_pred, ls, alpha, sigma, full_cov=True):
""" Compute parameters of GP posterior """
if self.modelp.model_str=='optmatern32' or \
self.modelp.model_str=='sampmatern32':
kernel = kern_matern32
else:
kernel = kern_exp_quad
k11_nonoise = kernel(x_train, x_train, ls, alpha)
lmat = get_cholesky_decomp(k11_nonoise, sigma, 'try_first')
smat = solve_upper_triangular(lmat.T, solve_lower_triangular(lmat, y_train))
k21 = kernel(x_pred, x_train, ls, alpha)
mu2 = k21.dot(smat)
k22 = kernel(x_pred, x_pred, ls, alpha)
vmat = solve_lower_triangular(lmat, k21.T)
k2 = k22 - vmat.T.dot(vmat)
if full_cov is False:
k2 = np.sqrt(np.diag(k2))
return mu2, k2
# Utilities
def print_str(self):
""" Print a description string """
print('*StanGpPP with modelp='+str(self.modelp)+'.')
print('-----')
================================================
FILE: bo/pp/pp_gp_stan_distmat.py
================================================
"""
Classes for GP models with Stan, using a given distance matrix.
"""
from argparse import Namespace
import time
import copy
import numpy as np
from scipy.spatial.distance import cdist
from bo.pp.pp_core import DiscPP
import bo.pp.stan.gp_distmat as gpstan
import bo.pp.stan.gp_distmat_fixedsig as gpstan_fixedsig
from bo.pp.gp.gp_utils import kern_exp_quad, kern_matern32, \
get_cholesky_decomp, solve_upper_triangular, solve_lower_triangular, \
sample_mvn, squared_euc_distmat, kern_distmat
from bo.util.print_utils import suppress_stdout_stderr
class StanGpDistmatPP(DiscPP):
""" Hierarchical GPs using a given distance matrix, implemented with Stan """
def __init__(self, data=None, modelp=None, printFlag=True):
""" Constructor """
self.set_model_params(modelp)
self.set_data(data)
self.ndimx = self.modelp.ndimx
self.set_model()
super(StanGpDistmatPP,self).__init__()
if printFlag:
self.print_str()
def set_model_params(self, modelp):
""" Set self.modelp """
if modelp is None:
pass #TODO
self.modelp = modelp
def set_data(self, data):
""" Set self.data """
if data is None:
pass #TODO
self.data_init = copy.deepcopy(data)
self.data = copy.deepcopy(self.data_init)
def set_model(self):
""" Set GP regression model """
self.model = self.get_model()
def get_model(self):
""" Returns GPRegression model """
if self.modelp.model_str=='optfixedsig' or \
self.modelp.model_str=='sampfixedsig':
return gpstan_fixedsig.get_model(print_status=True)
elif self.modelp.model_str=='opt' or self.modelp.model_str=='samp':
return gpstan.get_model(print_status=True)
elif self.modelp.model_str=='fixedparam':
return None
def infer_post_and_update_samples(self, seed=543210, print_result=False):
""" Update self.sample_list """
data_dict = self.get_stan_data_dict()
with suppress_stdout_stderr():
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt':
stanout = self.model.optimizing(data_dict, iter=self.modelp.infp.niter,
#seed=seed, as_vector=True, algorithm='Newton')
seed=seed, as_vector=True, algorithm='LBFGS')
elif self.modelp.model_str=='samp' or self.modelp.model_str=='sampfixedsig':
stanout = self.model.sampling(data_dict, iter=self.modelp.infp.niter +
self.modelp.infp.nwarmup, warmup=self.modelp.infp.nwarmup, chains=1,
seed=seed, refresh=1000)
elif self.modelp.model_str=='fixedparam':
stanout = None
print('-----')
self.sample_list = self.get_sample_list_from_stan_out(stanout)
if print_result: self.print_inference_result()
def get_stan_data_dict(self):
""" Return data dict for stan sampling method """
if self.modelp.model_str=='optfixedsig' or \
self.modelp.model_str=='sampfixedsig':
return {'ig1':self.modelp.kernp.ig1, 'ig2':self.modelp.kernp.ig2,
'n1':self.modelp.kernp.n1, 'n2':self.modelp.kernp.n2,
'sigma':self.modelp.kernp.sigma, 'D':self.ndimx,
'N':len(self.data.X), 'y':self.data.y.flatten(),
'distmat':self.get_distmat(self.data.X, self.data.X)}
elif self.modelp.model_str=='opt' or self.modelp.model_str=='samp':
return {'ig1':self.modelp.kernp.ig1, 'ig2':self.modelp.kernp.ig2,
'n1':self.modelp.kernp.n1, 'n2':self.modelp.kernp.n2,
'n3':self.modelp.kernp.n3, 'n4':self.modelp.kernp.n4,
'D':self.ndimx, 'N':len(self.data.X), 'y':self.data.y.flatten(),
'distmat':self.get_distmat(self.data.X, self.data.X)}
def get_distmat(self, xmat1, xmat2):
""" Get distance matrix """
# For now, will compute squared euc distance * .5, on self.data.X
return squared_euc_distmat(xmat1, xmat2, .5)
def get_sample_list_from_stan_out(self, stanout):
""" Convert stan output to sample_list """
if self.modelp.model_str=='optfixedsig':
return [Namespace(ls=stanout['rho'], alpha=stanout['alpha'],
sigma=self.modelp.kernp.sigma)]
elif self.modelp.model_str=='opt':
return [Namespace(ls=stanout['rho'], alpha=stanout['alpha'],
sigma=stanout['sigma'])]
elif self.modelp.model_str=='sampfixedsig':
sdict = stanout.extract(['rho','alpha'])
return [Namespace(ls=sdict['rho'][i], alpha=sdict['alpha'][i],
sigma=self.modelp.kernp.sigma) for i in range(sdict['rho'].shape[0])]
elif self.modelp.model_str=='samp':
sdict = stanout.extract(['rho','alpha','sigma'])
return [Namespace(ls=sdict['rho'][i], alpha=sdict['alpha'][i],
sigma=sdict['sigma'][i]) for i in range(sdict['rho'].shape[0])]
elif self.modelp.model_str=='fixedparam':
return [Namespace(ls=self.modelp.kernp.ls, alpha=self.modelp.kernp.alpha,
sigma=self.modelp.kernp.sigma)]
def print_inference_result(self):
""" Print results of stan inference """
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' or \
self.modelp.model_str=='fixedparam':
print('*ls pt est = '+str(self.sample_list[0].ls)+'.')
print('*alpha pt est = '+str(self.sample_list[0].alpha)+'.')
print('*sigma pt est = '+str(self.sample_list[0].sigma)+'.')
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampfixedsig':
ls_arr = np.array([ns.ls for ns in self.sample_list])
alpha_arr = np.array([ns.alpha for ns in self.sample_list])
sigma_arr = np.array([ns.sigma for ns in self.sample_list])
print('*ls mean = '+str(ls_arr.mean())+'.')
print('*ls std = '+str(ls_arr.std())+'.')
print('*alpha mean = '+str(alpha_arr.mean())+'.')
print('*alpha std = '+str(alpha_arr.std())+'.')
print('*sigma mean = '+str(sigma_arr.mean())+'.')
print('*sigma std = '+str(sigma_arr.std())+'.')
print('-----')
def sample_pp_post_pred(self, nsamp, input_list, full_cov=False, nloop=None):
""" Sample from posterior predictive of PP.
Inputs:
input_list - list of np arrays size=(-1,)
Returns:
list (len input_list) of np arrays (size=(nsamp,1))."""
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' or \
self.modelp.model_str=='fixedparam':
nloop = 1
sampids = [0]
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampfixedsig':
if nloop is None: nloop=nsamp
nsamp = int(nsamp/nloop)
sampids = np.random.randint(len(self.sample_list), size=(nloop,))
ppred_list = []
for i in range(nloop):
samp = self.sample_list[sampids[i]]
postmu, postcov = self.gp_post(self.data.X, self.data.y,
np.stack(input_list), samp.ls, samp.alpha, samp.sigma, full_cov)
if full_cov:
ppred_list.extend(list(sample_mvn(postmu, postcov, nsamp)))
else:
ppred_list.extend(list(np.random.normal(postmu.reshape(-1,),
postcov.reshape(-1,), size=(nsamp, len(input_list)))))
return list(np.stack(ppred_list).T), ppred_list
def sample_pp_pred(self, nsamp, input_list, lv=None):
""" Sample from predictive of PP for parameter lv.
Returns: list (len input_list) of np arrays (size (nsamp,1))."""
x_pred = np.stack(input_list)
if lv is None:
if self.modelp.model_str=='optfixedsig' or self.modelp.model_str=='opt' \
or self.modelp.model_str=='fixedparam':
lv = self.sample_list[0]
elif self.modelp.model_str=='samp' or \
self.modelp.model_str=='sampfixedsig':
lv = self.sample_list[np.random.randint(len(self.sample_list))]
postmu, postcov = self.gp_post(self.data.X, self.data.y, x_pred, lv.ls,
lv.alpha, lv.sigma)
pred_list = list(sample_mvn(postmu, postcov, 1)) ###TODO: sample from this mean nsamp times
return list(np.stack(pred_list).T), pred_list
def gp_post(self, x_train, y_train, x_pred, ls, alpha, sigma, full_cov=True):
""" Compute parameters of GP posterior """
kernel = lambda a, b, c, d: kern_distmat(a, b, c, d, self.get_distmat)
k11_nonoise = kernel(x_train, x_train, ls, alpha)
lmat = get_cholesky_decomp(k11_nonoise, sigma, 'try_first')
smat = solve_upper_triangular(lmat.T, solve_lower_triangular(lmat, y_train))
k21 = kernel(x_pred, x_train, ls, alpha)
mu2 = k21.dot(smat)
k22 = kernel(x_pred, x_pred, ls, alpha)
vmat = solve_lower_triangular(lmat, k21.T)
k2 = k22 - vmat.T.dot(vmat)
if full_cov is False:
k2 = np.sqrt(np.diag(k2))
return mu2, k2
# Utilities
def print_str(self):
""" Print a description string """
print('*StanGpDistmatPP with modelp='+str(self.modelp)+'.')
print('-----')
================================================
FILE: bo/pp/stan/__init__.py
================================================
"""
Code for defining and compiling models in Stan.
"""
================================================
FILE: bo/pp/stan/compile_stan.py
================================================
"""
Script to compile stan models
"""
#import pp_new.stan.gp_hier2 as gpstan
#import pp_new.stan.gp_hier3 as gpstan
#import pp_new.stan.gp_hier2_matern as gpstan
import pp_new.stan.gp_distmat as gpstan
#import pp_new.stan.gp_distmat_fixedsig as gpstan
# Recompile model and return it
model = gpstan.get_model(recompile=True)
================================================
FILE: bo/pp/stan/gp_distmat.py
================================================
"""
Functions to define and compile PPs in Stan, for model:
hierarchical GP (prior on rho, alpha, sigma) using a given distance matrix.
"""
import time
import pickle
import pystan
def get_model(recompile=False, print_status=True):
model_file_str = 'bo/pp/stan/hide_model/gp_distmat.pkl'
if recompile:
starttime = time.time()
model = pystan.StanModel(model_code=get_model_code())
buildtime = time.time()-starttime
with open(model_file_str,'wb') as f:
pickle.dump(model, f)
if print_status:
print('*Time taken to compile = '+ str(buildtime) +' seconds.\n-----')
print('*Model saved in file ' + model_file_str + '.\n-----')
else:
model = pickle.load(open(model_file_str,'rb'))
if print_status:
print('*Model loaded from file ' + model_file_str + '.\n-----')
return model
def get_model_code():
""" Parse modelp and return stan model code """
return """
data {
int N;
matrix[N, N] distmat;
vector[N] y;
real ig1;
real ig2;
real n1;
real n2;
real n3;
real n4;
}
parameters {
real rho;
real alpha;
real sigma;
}
model {
matrix[N, N] cov = square(alpha) * exp(-distmat / square(rho))
+ diag_matrix(rep_vector(square(sigma), N));
matrix[N, N] L_cov = cholesky_decompose(cov);
rho ~ inv_gamma(ig1, ig2);
alpha ~ normal(n1, n2);
sigma ~ normal(n3, n4);
y ~ multi_normal_cholesky(rep_vector(0, N), L_cov);
}
"""
if __name__ == '__main__':
get_model()
================================================
FILE: bo/pp/stan/gp_distmat_fixedsig.py
================================================
"""
Functions to define and compile PPs in Stan, for model:
hierarchical GP (prior on rho, alpha) and fixed sigma, using a given
distance matrix.
"""
import time
import pickle
import pystan
def get_model(recompile=False, print_status=True):
model_file_str = 'bo/pp/stan/hide_model/gp_distmat_fixedsig.pkl'
if recompile:
starttime = time.time()
model = pystan.StanModel(model_code=get_model_code())
buildtime = time.time()-starttime
with open(model_file_str,'wb') as f:
pickle.dump(model, f)
if print_status:
print('*Time taken to compile = '+ str(buildtime) +' seconds.\n-----')
print('*Model saved in file ' + model_file_str + '.\n-----')
else:
model = pickle.load(open(model_file_str,'rb'))
if print_status:
print('*Model loaded from file ' + model_file_str + '.\n-----')
return model
def get_model_code():
""" Parse modelp and return stan model code """
return """
data {
int N;
matrix[N, N] distmat;
vector[N] y;
real ig1;
real ig2;
real n1;
real n2;
real sigma;
}
parameters {
real rho;
real alpha;
}
model {
matrix[N, N] cov = square(alpha) * exp(-distmat / square(rho))
+ diag_matrix(rep_vector(square(sigma), N));
matrix[N, N] L_cov = cholesky_decompose(cov);
rho ~ inv_gamma(ig1, ig2);
alpha ~ normal(n1, n2);
y ~ multi_normal_cholesky(rep_vector(0, N), L_cov);
}
"""
if __name__ == '__main__':
get_model()
================================================
FILE: bo/pp/stan/gp_hier2.py
================================================
"""
Functions to define and compile PPs in Stan, for model:
hierarchical GP (prior on rho, alpha, sigma)
"""
import time
import pickle
import pystan
def get_model(recompile=False, print_status=True):
model_file_str = 'bo/pp/stan/hide_model/gp_hier2.pkl'
if recompile:
starttime = time.time()
model = pystan.StanModel(model_code=get_model_code())
buildtime = time.time()-starttime
with open(model_file_str,'wb') as f:
pickle.dump(model, f)
if print_status:
print('*Time taken to compile = '+ str(buildtime) +' seconds.\n-----')
print('*Model saved in file ' + model_file_str + '.\n-----')
else:
model = pickle.load(open(model_file_str,'rb'))
if print_status:
print('*Model loaded from file ' + model_file_str + '.\n-----')
return model
def get_model_code():
""" Parse modelp and return stan model code """
return """
data {
int D;
int N;
vector[D] x[N];
vector[N] y;
real ig1;
real ig2;
real n1;
real n2;
real n3;
real n4;
}
parameters {
real rho;
real alpha;
real sigma;
}
model {
matrix[N, N] cov = cov_exp_quad(x, alpha, rho)
+ diag_matrix(rep_vector(square(sigma), N));
matrix[N, N] L_cov = cholesky_decompose(cov);
rho ~ inv_gamma(ig1, ig2);
alpha ~ normal(n1, n2);
sigma ~ normal(n3, n4);
y ~ multi_normal_cholesky(rep_vector(0, N), L_cov);
}
"""
if __name__ == '__main__':
get_model()
================================================
FILE: bo/pp/stan/gp_hier2_matern.py
================================================
"""
Functions to define and compile PPs in Stan, for model: hierarchical GP (prior
on rho, alpha, sigma), with matern kernel
"""
import time
import pickle
import pystan
def get_model(recompile=False, print_status=True):
model_file_str = 'bo/pp/stan/hide_model/gp_hier2_matern.pkl'
if recompile:
starttime = time.time()
model = pystan.StanModel(model_code=get_model_code())
buildtime = time.time()-starttime
with open(model_file_str,'wb') as f:
pickle.dump(model, f)
if print_status:
print('*Time taken to compile = '+ str(buildtime) +' seconds.\n-----')
print('*Model saved in file ' + model_file_str + '.\n-----')
else:
model = pickle.load(open(model_file_str,'rb'))
if print_status:
print('*Model loaded from file ' + model_file_str + '.\n-----')
return model
def get_model_code():
""" Parse modelp and return stan model code """
return """
functions {
matrix distance_matrix_single(int N, vector[] x) {
matrix[N, N] distmat;
for(i in 1:(N-1)) {
for(j in (i+1):N) {
distmat[i, j] = distance(x[i], x[j]);
}
}
return distmat;
}
matrix matern_covariance(int N, matrix dist, real ls, real alpha_sq, int COVFN) {
matrix[N,N] S;
real dist_ls;
real sqrt3;
real sqrt5;
sqrt3=sqrt(3.0);
sqrt5=sqrt(5.0);
// exponential == Matern nu=1/2 , (p=0; nu=p+1/2)
if (COVFN==1) {
for(i in 1:(N-1)) {
for(j in (i+1):N) {
dist_ls = fabs(dist[i,j])/ls;
S[i,j] = alpha_sq * exp(- dist_ls );
}
}
}
// Matern nu= 3/2 covariance
else if (COVFN==2) {
for(i in 1:(N-1)) {
for(j in (i+1):N) {
dist_ls = fabs(dist[i,j])/ls;
S[i,j] = alpha_sq * (1 + sqrt3 * dist_ls) * exp(-sqrt3 * dist_ls);
}
}
}
// Matern nu=5/2 covariance
else if (COVFN==3) {
for(i in 1:(N-1)) {
for(j in (i+1):N) {
dist_ls = fabs(dist[i,j])/ls;
S[i,j] = alpha_sq * (1 + sqrt5 *dist_ls + 5* pow(dist_ls,2)/3) * exp(-sqrt5 *dist_ls);
}
}
}
// Matern as nu->Inf become Gaussian (aka squared exponential cov)
else if (COVFN==4) {
for(i in 1:(N-1)) {
for(j in (i+1):N) {
dist_ls = fabs(dist[i,j])/ls;
S[i,j] = alpha_sq * exp( -pow(dist_ls,2)/2 ) ;
}
}
}
// fill upper triangle
for(i in 1:(N-1)) {
for(j in (i+1):N) {
S[j,i] = S[i,j];
}
}
// create diagonal: nugget(nonspatial) + spatial variance + eps ensures positive definiteness
for(i in 1:N) {
S[i,i] = alpha_sq;
}
return S;
}
}
data {
int D;
int N;
vector[D] x[N];
vector[N] y;
real ig1;
real ig2;
real n1;
real n2;
real n3;
real n4;
int covid;
}
parameters {
real rho;
real alpha;
real sigma;
}
model {
matrix[N, N] distmat = distance_matrix_single(N, x);
matrix[N, N] cov = matern_covariance(N, distmat, rho, square(alpha), covid);
matrix[N, N] L_cov = cholesky_decompose(cov);
rho ~ inv_gamma(ig1, ig2);
alpha ~ normal(n1, n2);
sigma ~ normal(n3, n4);
y ~ multi_normal_cholesky(rep_vector(0, N), L_cov);
}
"""
if __name__ == '__main__':
get_model()
================================================
FILE: bo/pp/stan/gp_hier3.py
================================================
"""
Functions to define and compile PPs in Stan, for model:
hierarchical GP with uniform prior on rho, normal prior on alpha,
and fixed sigma
"""
import time
import pickle
import pystan
def get_model(recompile=False, print_status=True):
model_file_str = 'bo/pp/stan/hide_model/gp_hier3.pkl'
if recompile:
starttime = time.time()
model = pystan.StanModel(model_code=get_model_code())
buildtime = time.time()-starttime
with open(model_file_str,'wb') as f:
pickle.dump(model, f)
if print_status:
print('*Time taken to compile = '+ str(buildtime) +' seconds.\n-----')
print('*Model saved in file ' + model_file_str + '.\n-----')
else:
model = pickle.load(open(model_file_str,'rb'))
if print_status:
print('*Model loaded from file ' + model_file_str + '.\n-----')
return model
def get_model_code():
""" Parse modelp and return stan model code """
return """
data {
int D;
int N;
vector[D] x[N];
vector[N] y;
real u1;
real u2;
real n1;
real n2;
real sigma;
}
parameters {
real rho;
real alpha;
}
model {
matrix[N, N] cov = cov_exp_quad(x, alpha, rho)
+ diag_matrix(rep_vector(square(sigma), N));
matrix[N, N] L_cov = cholesky_decompose(cov);
rho ~ uniform(u1, u2);
alpha ~ normal(n1, n2);
y ~ multi_normal_cholesky(rep_vector(0, N), L_cov);
}
"""
if __name__ == '__main__':
get_model()
================================================
FILE: bo/util/__init__.py
================================================
"""
Miscellaneous utilities.
"""
================================================
FILE: bo/util/datatransform.py
================================================
"""
Classes for transforming data.
"""
from argparse import Namespace
import numpy as np
from sklearn.preprocessing import StandardScaler
#import sklearn.preprocessing as sklp
class DataTransformer(object):
""" Class for transforming data """
def __init__(self, datamat, printflag=True):
""" Constructor
Parameters:
datamat - numpy array (n x d) of data to be transformed
"""
self.datamat = datamat
self.set_transformers()
if printflag:
self.print_str()
def set_transformers(self):
""" Set transformers using self.datamat """
self.ss = StandardScaler()
self.ss.fit(self.datamat)
def transform_data(self, datamat=None):
""" Return transformed datamat (default self.datamat) """
if datamat is None:
datamat = self.datamat
return self.ss.transform(datamat)
def inv_transform_data(self, datamat):
""" Return inverse transform of datamat """
return self.ss.inverse_transform(datamat)
def print_str(self):
""" Print a description string """
print('*DataTransformer with self.datamat.shape = ' +
str(self.datamat.shape) + '.')
print('-----')
================================================
FILE: bo/util/print_utils.py
================================================
"""
Utilities for printing and output
"""
import os
class suppress_stdout_stderr(object):
''' A context manager for doing a "deep suppression" of stdout and stderr in
Python, i.e. will suppress all print, even if the print originates in a
compiled C/Fortran sub-function.
This will not suppress raised exceptions, since exceptions are printed
to stderr just before a script exits, and after the context manager has
exited (at least, I think that is why it lets exceptions through). '''
def __init__(self):
# Open a pair of null files
self.null_fds = [os.open(os.devnull, os.O_RDWR) for x in range(2)]
# Save the actual stdout (1) and stderr (2) file descriptors.
self.save_fds = [os.dup(1), os.dup(2)]
def __enter__(self):
# Assign the null pointers to stdout and stderr.
os.dup2(self.null_fds[0], 1)
os.dup2(self.null_fds[1], 2)
def __exit__(self, *_):
# Re-assign the real stdout/stderr back to (1) and (2)
os.dup2(self.save_fds[0], 1)
os.dup2(self.save_fds[1], 2)
# Close the null files
for fd in self.null_fds + self.save_fds:
os.close(fd)
================================================
FILE: darts/__init__.py
================================================
================================================
FILE: darts/arch.py
================================================
import numpy as np
import sys
import os
import copy
import random
sys.path.append(os.path.expanduser('~/darts/cnn'))
from train_class import Train
OPS = ['none',
'max_pool_3x3',
'avg_pool_3x3',
'skip_connect',
'sep_conv_3x3',
'sep_conv_5x5',
'dil_conv_3x3',
'dil_conv_5x5'
]
NUM_VERTICES = 4
INPUT_1 = 'c_k-2'
INPUT_2 = 'c_k-1'
class Arch:
def __init__(self, arch):
self.arch = arch
def serialize(self):
return self.arch
def query(self, epochs=50):
trainer = Train()
val_losses, test_losses = trainer.main(self.arch, epochs=epochs)
val_loss = 100 - np.mean(val_losses)
test_loss = 100 - test_losses[-1]
return val_loss, test_loss
@classmethod
def random_arch(cls):
# output a uniformly random architecture spec
# from the DARTS repository
# https://github.com/quark0/darts
normal = []
reduction = []
for i in range(NUM_VERTICES):
ops = np.random.choice(range(len(OPS)), NUM_VERTICES)
#input nodes for conv
nodes_in_normal = np.random.choice(range(i+2), 2, replace=False)
#input nodes for reduce
nodes_in_reduce = np.random.choice(range(i+2), 2, replace=False)
normal.extend([(nodes_in_normal[0], ops[0]), (nodes_in_normal[1], ops[1])])
reduction.extend([(nodes_in_reduce[0], ops[2]), (nodes_in_reduce[1], ops[3])])
return (normal, reduction)
def get_arch_list(self):
# convert tuple to list so that it is mutable
arch_list = []
for cell in self.arch:
arch_list.append([])
for pair in cell:
arch_list[-1].append([])
for num in pair:
arch_list[-1][-1].append(num)
return arch_list
def mutate(self, edits):
""" mutate a single arch """
# first convert tuple to array so that it is mutable
mutation = self.get_arch_list()
#make mutations
for _ in range(edits):
cell = np.random.choice(2)
pair = np.random.choice(len(OPS))
num = np.random.choice(2)
if num == 1:
mutation[cell][pair][num] = np.random.choice(len(OPS))
else:
inputs = pair // 2 + 2
choice = np.random.choice(inputs)
if pair % 2 == 0 and mutation[cell][pair+1][num] != choice:
mutation[cell][pair][num] = choice
elif pair % 2 != 0 and mutation[cell][pair-1][num] != choice:
mutation[cell][pair][num] = choice
return mutation
def get_paths(self):
""" return all paths from input to output """
path_builder = [[[], [], [], []], [[], [], [], []]]
paths = [[], []]
for i, cell in enumerate(self.arch):
for j in range(len(OPS)):
if cell[j][0] == 0:
path = [INPUT_1, OPS[cell[j][1]]]
path_builder[i][j//2].append(path)
paths[i].append(path)
elif cell[j][0] == 1:
path = [INPUT_2, OPS[cell[j][1]]]
path_builder[i][j//2].append(path)
paths[i].append(path)
else:
for path in path_builder[i][cell[j][0] - 2]:
path = [*path, OPS[cell[j][1]]]
path_builder[i][j//2].append(path)
paths[i].append(path)
# check if there are paths of length >=5
contains_long_path = [False, False]
if max([len(path) for path in paths[0]]) >= 5:
contains_long_path[0] = True
if max([len(path) for path in paths[1]]) >= 5:
contains_long_path[1] = True
return paths, contains_long_path
def get_path_indices(self, long_paths=True):
"""
compute the index of each path
There are 4 * (8^0 + ... + 8^4) paths total
If long_paths = False, we give a single boolean to all paths of
size 4, so there are only 4 * (1 + 8^0 + ... + 8^3) paths
"""
paths, contains_long_path = self.get_paths()
normal_paths, reduce_paths = paths
num_ops = len(OPS)
"""
Compute the max number of paths per input per cell.
Since there are two cells and two inputs per cell,
total paths = 4 * max_paths
"""
if not long_paths:
max_paths = 1 + sum([num_ops ** i for i in range(NUM_VERTICES)])
else:
max_paths = sum([num_ops ** i for i in range(NUM_VERTICES + 1)])
path_indices = []
# set the base index based on the cell and the input
for i, paths in enumerate((normal_paths, reduce_paths)):
for path in paths:
index = i * 2 * max_paths
if path[0] == INPUT_2:
index += max_paths
# recursively compute the index of the path
for j in range(NUM_VERTICES + 1):
if j == len(path) - 1:
path_indices.append(index)
break
elif j == (NUM_VERTICES - 1) and not long_paths:
path_indices.append(2 * (i + 1) * max_paths - 1)
break
else:
index += num_ops ** j * (OPS.index(path[j + 1]) + 1)
return (tuple(path_indices), contains_long_path)
def encode_paths(self, long_paths=True):
# output one-hot encoding of paths
path_indices, _ = self.get_path_indices(long_paths=long_paths)
num_ops = len(OPS)
if not long_paths:
max_paths = 1 + sum([num_ops ** i for i in range(NUM_VERTICES)])
else:
max_paths = sum([num_ops ** i for i in range(NUM_VERTICES + 1)])
path_encoding = np.zeros(4 * max_paths)
for index in path_indices:
path_encoding[index] = 1
return path_encoding
def path_distance(self, other):
# compute the distance between two architectures
# by comparing their path encodings
return np.sum(np.array(self.encode_paths() != np.array(other.encode_paths())))
================================================
FILE: data.py
================================================
import numpy as np
import pickle
import sys
import os
if 'search_space' not in os.environ or os.environ['search_space'] == 'nasbench':
from nasbench import api
from nas_bench.cell import Cell
elif os.environ['search_space'] == 'darts':
from darts.arch import Arch
elif os.environ['search_space'][:12] == 'nasbench_201':
from nas_201_api import NASBench201API as API
from nas_bench_201.cell import Cell
else:
print('Invalid search space environ in data.py')
sys.exit()
class Data:
def __init__(self,
search_space,
dataset='cifar10',
nasbench_folder='./',
loaded_nasbench=None):
self.search_space = search_space
self.dataset = dataset
if loaded_nasbench:
self.nasbench = loaded_nasbench
elif search_space == 'nasbench':
self.nasbench = api.NASBench(nasbench_folder + 'nasbench_only108.tfrecord')
elif search_space == 'nasbench_201':
self.nasbench = API(os.path.expanduser('~/nas-bench-201/NAS-Bench-201-v1_0-e61699.pth'))
elif search_space != 'darts':
print(search_space, 'is not a valid search space')
sys.exit()
def get_type(self):
return self.search_space
def query_arch(self,
arch=None,
train=True,
encoding_type='path',
cutoff=-1,
deterministic=True,
epochs=0):
arch_dict = {}
arch_dict['epochs'] = epochs
if self.search_space in ['nasbench', 'nasbench_201']:
if arch is None:
arch = Cell.random_cell(self.nasbench)
arch_dict['spec'] = arch
if encoding_type == 'adj':
encoding = Cell(**arch).encode_standard()
elif encoding_type == 'path':
encoding = Cell(**arch).encode_paths()
elif encoding_type == 'trunc_path':
encoding = Cell(**arch).encode_paths()[:cutoff]
else:
print('invalid encoding type')
arch_dict['encoding'] = encoding
if train:
arch_dict['val_loss'] = Cell(**arch).get_val_loss(self.nasbench,
deterministic=deterministic,
dataset=self.dataset)
arch_dict['test_loss'] = Cell(**arch).get_test_loss(self.nasbench,
dataset=self.dataset)
arch_dict['num_params'] = Cell(**arch).get_num_params(self.nasbench)
arch_dict['val_per_param'] = (arch_dict['val_loss'] - 4.8) * (arch_dict['num_params'] ** 0.5) / 100
else:
if arch is None:
arch = Arch.random_arch()
arch_dict['spec'] = arch
if encoding_type == 'path':
encoding = Arch(arch).encode_paths()
elif encoding_type == 'trunc_path':
encoding = Arch(arch).encode_paths()[:cutoff]
else:
encoding = arch
arch_dict['encoding'] = encoding
if train:
if epochs == 0:
epochs = 50
arch_dict['val_loss'], arch_dict['test_loss'] = Arch(arch).query(epochs=epochs)
return arch_dict
def mutate_arch(self,
arch,
mutation_rate=1.0):
if self.search_space in ['nasbench', 'nasbench_201']:
return Cell(**arch).mutate(self.nasbench,
mutation_rate=mutation_rate)
else:
return Arch(arch).mutate(int(mutation_rate))
def get_hash(self, arch):
# return the path indices of the architecture, used as a hash
if self.search_space == 'nasbench':
return Cell(**arch).get_path_indices()
elif self.search_space == 'darts':
return Arch(arch).get_path_indices()[0]
else:
return Cell(**arch).get_string()
def generate_random_dataset(self,
num=10,
train=True,
encoding_type='path',
cutoff=-1,
random='standard',
allow_isomorphisms=False,
deterministic_loss=True,
patience_factor=5):
"""
create a dataset of randomly sampled architectues
test for isomorphisms using a hash map of path indices
use patience_factor to avoid infinite loops
"""
data = []
dic = {}
tries_left = num * patience_factor
while len(data) < num:
tries_left -= 1
if tries_left <= 0:
break
arch_dict = self.query_arch(train=train,
encoding_type=encoding_type,
cutoff=cutoff,
deterministic=deterministic_loss)
h = self.get_hash(arch_dict['spec'])
if allow_isomorphisms or h not in dic:
dic[h] = 1
data.append(arch_dict)
return data
def get_candidates(self,
data,
num=100,
acq_opt_type='mutation',
encoding_type='path',
cutoff=-1,
loss='val_loss',
patience_factor=5,
deterministic_loss=True,
num_arches_to_mutate=1,
max_mutation_rate=1,
allow_isomorphisms=False):
"""
Creates a set of candidate architectures with mutated and/or random architectures
"""
candidates = []
# set up hash map
dic = {}
for d in data:
arch = d['spec']
h = self.get_hash(arch)
dic[h] = 1
if acq_opt_type in ['mutation', 'mutation_random']:
# mutate architectures with the lowest loss
best_arches = [arch['spec'] for arch in sorted(data, key=lambda i:i[loss])[:num_arches_to_mutate * patience_factor]]
# stop when candidates is size num
# use patience_factor instead of a while loop to avoid long or infinite runtime
for arch in best_arches:
if len(candidates) >= num:
break
for i in range(num // num_arches_to_mutate // max_mutation_rate):
for rate in range(1, max_mutation_rate + 1):
mutated = self.mutate_arch(arch, mutation_rate=rate)
arch_dict = self.query_arch(mutated,
train=False,
encoding_type=encoding_type,
cutoff=cutoff)
h = self.get_hash(mutated)
if allow_isomorphisms or h not in dic:
dic[h] = 1
candidates.append(arch_dict)
if acq_opt_type in ['random', 'mutation_random']:
# add randomly sampled architectures to the set of candidates
for _ in range(num * patience_factor):
if len(candidates) >= 2 * num:
break
arch_dict = self.query_arch(train=False,
encoding_type=encoding_type,
cutoff=cutoff)
h = self.get_hash(arch_dict['spec'])
if allow_isomorphisms or h not in dic:
dic[h] = 1
candidates.append(arch_dict)
return candidates
def remove_duplicates(self, candidates, data):
# input: two sets of architectues: candidates and data
# output: candidates with arches from data removed
dic = {}
for d in data:
dic[self.get_hash(d['spec'])] = 1
unduplicated = []
for candidate in candidates:
if self.get_hash(candidate['spec']) not in dic:
dic[self.get_hash(candidate['spec'])] = 1
unduplicated.append(candidate)
return unduplicated
def encode_data(self, dicts):
"""
method used by metann_runner.py (for Arch)
input: list of arch dictionary objects
output: xtrain (encoded architectures), ytrain (val loss)
"""
data = []
for dic in dicts:
arch = dic['spec']
encoding = Arch(arch).encode_paths()
data.append((arch, encoding, dic['val_loss_avg'], None))
return data
def get_arch_list(self,
aux_file_path,
iteridx=0,
num_top_arches=5,
max_edits=20,
num_repeats=5,
verbose=1):
# Method used for gp_bayesopt
if self.search_space == 'darts':
print('get_arch_list only supported for nasbench and nasbench_201')
sys.exit()
# load the list of architectures chosen by bayesopt so far
base_arch_list = pickle.load(open(aux_file_path, 'rb'))
top_arches = [archtuple[0] for archtuple in base_arch_list[:num_top_arches]]
if verbose:
top_5_loss = [archtuple[1][0] for archtuple in base_arch_list[:min(5, len(base_arch_list))]]
print('top 5 val losses {}'.format(top_5_loss))
# perturb the best k architectures
dic = {}
for archtuple in base_arch_list:
path_indices = Cell(**archtuple[0]).get_path_indices()
dic[path_indices] = 1
new_arch_list = []
for arch in top_arches:
for edits in range(1, max_edits):
for _ in range(num_repeats):
perturbation = Cell(**arch).perturb(self.nasbench, edits)
path_indices = Cell(**perturbation).get_path_indices()
if path_indices not in dic:
dic[path_indices] = 1
new_arch_list.append(perturbation)
# make sure new_arch_list is not empty
while len(new_arch_list) == 0:
for _ in range(100):
arch = Cell.random_cell(self.nasbench)
path_indices = Cell(**arch).get_path_indices()
if path_indices not in dic:
dic[path_indices] = 1
new_arch_list.append(arch)
return new_arch_list
@classmethod
def generate_distance_matrix(cls, arches_1, arches_2, distance):
# Method used for gp_bayesopt for nasbench
matrix = np.zeros([len(arches_1), len(arches_2)])
for i, arch_1 in enumerate(arches_1):
for j, arch_2 in enumerate(arches_2):
if distance == 'edit_distance':
matrix[i][j] = Cell(**arch_1).edit_distance(Cell(**arch_2))
elif distance == 'path_distance':
matrix[i][j] = Cell(**arch_1).path_distance(Cell(**arch_2))
elif distance == 'trunc_path_distance':
matrix[i][j] = Cell(**arch_1).path_distance(Cell(**arch_2))
elif distance == 'nasbot_distance':
matrix[i][j] = Cell(**arch_1).nasbot_distance(Cell(**arch_2))
else:
print('{} is an invalid distance'.format(distance))
sys.exit()
return matrix
================================================
FILE: meta_neural_net.py
================================================
import argparse
import itertools
import os
import random
import sys
import numpy as np
from matplotlib import pyplot as plt
from tensorflow import keras
import tensorflow as tf
from tensorflow.keras import backend as K
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
def mle_loss(y_true, y_pred):
# Minimum likelihood estimate loss function
mean = tf.slice(y_pred, [0, 0], [-1, 1])
var = tf.slice(y_pred, [0, 1], [-1, 1])
return 0.5 * tf.log(2*np.pi*var) + tf.square(y_true - mean) / (2*var)
def mape_loss(y_true, y_pred):
# Minimum absolute percentage error loss function
lower_bound = 4.5
fraction = tf.math.divide(tf.subtract(y_pred, lower_bound), \
tf.subtract(y_true, lower_bound))
return tf.abs(tf.subtract(fraction, 1))
class MetaNeuralnet:
def get_dense_model(self,
input_dims,
num_layers,
layer_width,
loss,
regularization):
input_layer = keras.layers.Input(input_dims)
model = keras.models.Sequential()
for _ in range(num_layers):
model.add(keras.layers.Dense(layer_width, activation='relu'))
model = model(input_layer)
if loss == 'mle':
mean = keras.layers.Dense(1)(model)
var = keras.layers.Dense(1)(model)
var = keras.layers.Activation(tf.math.softplus)(var)
output = keras.layers.concatenate([mean, var])
else:
if regularization == 0:
output = keras.layers.Dense(1)(model)
else:
reg = keras.regularizers.l1(regularization)
output = keras.layers.Dense(1, kernel_regularizer=reg)(model)
dense_net = keras.models.Model(inputs=input_layer, outputs=output)
return dense_net
def fit(self, xtrain, ytrain,
num_layers=10,
layer_width=20,
loss='mae',
epochs=200,
batch_size=32,
lr=.01,
verbose=0,
regularization=0,
**kwargs):
if loss == 'mle':
loss_fn = mle_loss
elif loss == 'mape':
loss_fn = mape_loss
else:
loss_fn = 'mae'
self.model = self.get_dense_model((xtrain.shape[1],),
loss=loss_fn,
num_layers=num_layers,
layer_width=layer_width,
regularization=regularization)
optimizer = keras.optimizers.Adam(lr=lr, beta_1=.9, beta_2=.99)
self.model.compile(optimizer=optimizer, loss=loss_fn)
#print(self.model.summary())
self.model.fit(xtrain, ytrain,
batch_size=batch_size,
epochs=epochs,
verbose=verbose)
train_pred = np.squeeze(self.model.predict(xtrain))
train_error = np.mean(abs(train_pred-ytrain))
return train_error
def predict(self, xtest):
return self.model.predict(xtest)
================================================
FILE: meta_neuralnet.ipynb
================================================
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Train a Meta Neural Network on NASBench\n",
"## Predict the accuracy of neural networks to within one percent!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%load_ext autoreload\n",
"%autoreload 2\n",
"%matplotlib inline"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"from matplotlib import pyplot as plt\n",
"from nasbench import api\n",
"\n",
"from data import Data\n",
"from meta_neural_net import MetaNeuralnet"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# define a function to plot the meta neural networks\n",
"\n",
"def plot_meta_neuralnet(ytrain, train_pred, ytest, test_pred, max_disp=500, title=None):\n",
" \n",
" plt.scatter(ytrain[:max_disp], train_pred[:max_disp], label='training data', alpha=0.7, s=64)\n",
" plt.scatter(ytest[:max_disp], test_pred[:max_disp], label = 'test data', alpha=0.7, marker='^')\n",
"\n",
" # axis limits\n",
" plt.xlim((5, 15))\n",
" plt.ylim((5, 15))\n",
" ax_lim = np.array([np.min([plt.xlim()[0], plt.ylim()[0]]),\n",
" np.max([plt.xlim()[1], plt.ylim()[1]])])\n",
" plt.xlim(ax_lim)\n",
" plt.ylim(ax_lim)\n",
" \n",
" # 45-degree line\n",
" plt.plot(ax_lim, ax_lim, 'k:') \n",
" \n",
" plt.gca().set_aspect('equal', adjustable='box')\n",
" plt.title(title)\n",
" plt.legend(loc='best')\n",
" plt.xlabel('true percent error')\n",
" plt.ylabel('predicted percent error')\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"# load the NASBench dataset\n",
"# takes about 1 minute to load the nasbench dataset\n",
"search_space = Data('nasbench')\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# method which runs a meta neural network experiment\n",
"def meta_neuralnet_experiment(params, \n",
" ns=[100, 500], \n",
" num_ensemble=3, \n",
" test_size=500,\n",
" cutoff=40,\n",
" plot=True):\n",
" \n",
" for n in ns:\n",
" for encoding_type in ['adj', 'path']:\n",
"\n",
" train_data = search_space.generate_random_dataset(num=n, \n",
" encoding_type=encoding_type,\n",
" cutoff=cutoff)\n",
" \n",
" test_data = search_space.generate_random_dataset(num=test_size, \n",
" encoding_type=encoding_type,\n",
" cutoff=cutoff)\n",
" \n",
" print(len(test_data))\n",
" test_data = search_space.remove_duplicates(test_data, train_data)\n",
" print(len(test_data))\n",
" \n",
" xtrain = np.array([d['encoding'] for d in train_data])\n",
" ytrain = np.array([d['val_loss'] for d in train_data])\n",
"\n",
" xtest = np.array([d['encoding'] for d in test_data])\n",
" ytest = np.array([d['val_loss'] for d in test_data])\n",
"\n",
" train_errors = []\n",
" test_errors = []\n",
" meta_neuralnet = MetaNeuralnet()\n",
" for _ in range(num_ensemble): \n",
" meta_neuralnet.fit(xtrain, ytrain, **params)\n",
" train_pred = np.squeeze(meta_neuralnet.predict(xtrain))\n",
" train_error = np.mean(abs(train_pred-ytrain))\n",
" train_errors.append(train_error)\n",
" test_pred = np.squeeze(meta_neuralnet.predict(xtest)) \n",
" test_error = np.mean(abs(test_pred-ytest))\n",
" test_errors.append(test_error)\n",
"\n",
" train_error = np.round(np.mean(train_errors, axis=0), 3)\n",
" test_error = np.round(np.mean(test_errors, axis=0), 3)\n",
" print('Meta neuralnet training size: {}, encoding type: {}'.format(n, encoding_type))\n",
" print('Train error: {}, test error: {}'.format(train_error, test_error))\n",
"\n",
" if plot:\n",
" if encoding_type == 'path':\n",
" title = 'Path encoding, training set size {}'.format(n)\n",
" else:\n",
" title = 'Adjacency list encoding, training set size {}'.format(n) \n",
"\n",
" plot_meta_neuralnet(ytrain, train_pred, ytest, test_pred, title=title)\n",
" plt.show() \n",
" print('correlation', np.corrcoef(ytest, test_pred)[1,0])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"meta_neuralnet_params = {'loss':'mae', 'num_layers':10, 'layer_width':20, 'epochs':200, \\\n",
" 'batch_size':32, 'lr':.01, 'regularization':0, 'verbose':0}\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"meta_neuralnet_experiment(meta_neuralnet_params)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
================================================
FILE: metann_runner.py
================================================
import argparse
import time
import logging
import sys
import os
import pickle
import numpy as np
from acquisition_functions import acq_fn
from data import Data
from meta_neural_net import MetaNeuralnet
"""
meta neural net runner is used in run_experiments_parallel
- loads data by opening k*i pickle files from previous iterations
- trains a meta neural network and predicts accuracy of all candidates
- outputs k pickle files of the architecture to be trained next
"""
def run_meta_neuralnet(search_space, dicts,
k=10,
verbose=1,
num_ensemble=5,
epochs=10000,
lr=0.00001,
loss='scaled',
explore_type='its',
explore_factor=0.5):
# data: list of arch dictionary objects
# trains a meta neural network
# returns list of k arch dictionary objects - the k best predicted
results = []
meta_neuralnet = MetaNeuralnet()
data = search_space.encode_data(dicts)
xtrain = np.array([d[1] for d in data])
ytrain = np.array([d[2] for d in data])
candidates = search_space.get_candidates(data,
acq_opt_type='mutation_random',
encode_paths=True,
allow_isomorphisms=True,
deterministic_loss=None)
xcandidates = np.array([c[1] for c in candidates])
candidates_specs = [c[0] for c in candidates]
predictions = []
# train an ensemble of neural networks
train_error = 0
for _ in range(num_ensemble):
meta_neuralnet = MetaNeuralnet()
train_error += meta_neuralnet.fit(xtrain, ytrain,
loss=loss,
epochs=epochs,
lr=lr)
predictions.append(np.squeeze(meta_neuralnet.predict(xcandidates)))
train_error /= num_ensemble
if verbose:
print('Meta neural net train error: {}'.format(train_error))
sorted_indices = acq_fn(predictions, explore_type)
top_k_candidates = [candidates_specs[i] for i in sorted_indices[:k]]
candidates_dict = []
for candidate in top_k_candidates:
d = {}
d['spec'] = candidate
candidates_dict.append(d)
return candidates_dict
def run(args):
save_dir = '{}/'.format(args.experiment_name)
if not os.path.exists(save_dir):
os.mkdir(save_dir)
query = args.query
k = args.k
trained_prefix = args.trained_filename
untrained_prefix = args.untrained_filename
threshold = args.threshold
search_space = Data('darts')
# if it's the first iteration, choose k arches at random to train
if query == 0:
print('about to generate {} random'.format(k))
data = search_space.generate_random_dataset(num=k, train=False)
arches = [d['spec'] for d in data]
next_arches = []
for arch in arches:
d = {}
d['spec'] = arch
next_arches.append(d)
else:
# get the data from prior iterations from pickle files
data = []
for i in range(query):
filepath = '{}{}_{}.pkl'.format(save_dir, trained_prefix, i)
with open(filepath, 'rb') as f:
arch = pickle.load(f)
data.append(arch)
print('Iteration {}'.format(query))
print('Data from last round')
print(data)
# run the meta neural net to output the next arches
next_arches = run_meta_neuralnet(search_space, data, k=k)
print('next batch')
print(next_arches)
# output the new arches to pickle files
for i in range(k):
index = query + i
filepath = '{}{}_{}.pkl'.format(save_dir, untrained_prefix, index)
next_arches[i]['index'] = index
next_arches[i]['filepath'] = filepath
with open(filepath, 'wb') as f:
pickle.dump(next_arches[i], f)
def main(args):
#set up save dir
save_dir = './'
#set up logging
log_format = '%(asctime)s %(message)s'
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format=log_format, datefmt='%m/%d %I:%M:%S %p')
fh = logging.FileHandler(os.path.join(save_dir, 'log.txt'))
fh.setFormatter(logging.Formatter(log_format))
logging.getLogger().addHandler(fh)
logging.info(args)
run(args)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Args for meta neural net')
parser.add_argument('--experiment_name', type=str, default='darts_test', help='Folder for input/output files')
parser.add_argument('--params', type=str, default='test', help='Which set of params to use')
parser.add_argument('--query', type=int, default=0, help='Which query is Neural BayesOpt on')
parser.add_argument('--trained_filename', type=str, default='trained_spec', help='name of input files')
parser.add_argument('--untrained_filename', type=str, default='untrained_spec', help='name of output files')
parser.add_argument('--k', type=int, default=10, help='number of arches to train per iteration')
parser.add_argument('--threshold', type=int, default=20, help='throw out arches with val loss above threshold')
args = parser.parse_args()
main(args)
================================================
FILE: nas_algorithms.py
================================================
import itertools
import os
import pickle
import sys
import copy
import numpy as np
import tensorflow as tf
from argparse import Namespace
from data import Data
def run_nas_algorithm(algo_params, search_space, mp):
# run nas algorithm
ps = copy.deepcopy(algo_params)
algo_name = ps.pop('algo_name')
if algo_name == 'random':
data = random_search(search_space, **ps)
elif algo_name == 'evolution':
data = evolution_search(search_space, **ps)
elif algo_name == 'bananas':
data = bananas(search_space, mp, **ps)
elif algo_name == 'gp_bayesopt':
data = gp_bayesopt_search(search_space, **ps)
elif algo_name == 'dngo':
data = dngo_search(search_space, **ps)
else:
print('invalid algorithm name')
sys.exit()
k = 10
if 'k' in ps:
k = ps['k']
total_queries = 150
if 'total_queries' in ps:
total_queries = ps['total_queries']
loss = 'val_loss'
if 'loss' in ps:
loss = ps['loss']
return compute_best_test_losses(data, k, total_queries, loss), data
def compute_best_test_losses(data, k, total_queries, loss):
"""
Given full data from a completed nas algorithm,
output the test error of the arch with the best val error
after every multiple of k
"""
results = []
for query in range(k, total_queries + k, k):
best_arch = sorted(data[:query], key=lambda i:i[loss])[0]
test_error = best_arch['test_loss']
results.append((query, test_error))
return results
def random_search(search_space,
total_queries=150,
loss='val_loss',
deterministic=True,
verbose=1):
"""
random search
"""
data = search_space.generate_random_dataset(num=total_queries,
encoding_type='adj',
deterministic_loss=deterministic)
if verbose:
top_5_loss = sorted([d[loss] for d in data])[:min(5, len(data))]
print('random, query {}, top 5 losses {}'.format(total_queries, top_5_loss))
return data
def evolution_search(search_space,
total_queries=150,
num_init=10,
k=10,
loss='val_loss',
population_size=30,
tournament_size=10,
mutation_rate=1.0,
deterministic=True,
regularize=True,
verbose=1):
"""
regularized evolution
"""
data = search_space.generate_random_dataset(num=num_init,
deterministic_loss=deterministic)
losses = [d[loss] for d in data]
query = num_init
population = [i for i in range(min(num_init, population_size))]
while query <= total_queries:
# evolve the population by mutating the best architecture
# from a random subset of the population
sample = np.random.choice(population, tournament_size)
best_index = sorted([(i, losses[i]) for i in sample], key=lambda i:i[1])[0][0]
mutated = search_space.mutate_arch(data[best_index]['spec'],
mutation_rate=mutation_rate)
arch_dict = search_space.query_arch(mutated, deterministic=deterministic)
data.append(arch_dict)
losses.append(arch_dict[loss])
population.append(len(data) - 1)
# kill the oldest (or worst) from the population
if len(population) >= population_size:
if regularize:
oldest_index = sorted([i for i in population])[0]
population.remove(oldest_index)
else:
worst_index = sorted([(i, losses[i]) for i in population], key=lambda i:i[1])[-1][0]
population.remove(worst_index)
if verbose and (query % k == 0):
top_5_loss = sorted([d[loss] for d in data])[:min(5, len(data))]
print('evolution, query {}, top 5 losses {}'.format(query, top_5_loss))
query += 1
return data
def bananas(search_space,
metann_params,
num_init=10,
k=10,
loss='val_loss',
total_queries=150,
num_ensemble=5,
acq_opt_type='mutation',
num_arches_to_mutate=1,
explore_type='its',
encoding_type='trunc_path',
cutoff=40,
deterministic=True,
verbose=1):
"""
Bayesian optimization with a neural network model
"""
from acquisition_functions import acq_fn
from meta_neural_net import MetaNeuralnet
data = search_space.generate_random_dataset(num=num_init,
encoding_type=encoding_type,
cutoff=cutoff,
deterministic_loss=deterministic)
query = num_init + k
while query <= total_queries:
xtrain = np.array([d['encoding'] for d in data])
ytrain = np.array([d[loss] for d in data])
if (query == num_init + k) and verbose:
print('bananas xtrain shape', xtrain.shape)
print('bananas ytrain shape', ytrain.shape)
# get a set of candidate architectures
candidates = search_space.get_candidates(data,
acq_opt_type=acq_opt_type,
encoding_type=encoding_type,
cutoff=cutoff,
num_arches_to_mutate=num_arches_to_mutate,
loss=loss,
deterministic_loss=deterministic)
xcandidates = np.array([c['encoding'] for c in candidates])
candidate_predictions = []
# train an ensemble of neural networks
train_error = 0
for _ in range(num_ensemble):
meta_neuralnet = MetaNeuralnet()
train_error += meta_neuralnet.fit(xtrain, ytrain, **metann_params)
# predict the validation loss of the candidate architectures
candidate_predictions.append(np.squeeze(meta_neuralnet.predict(xcandidates)))
# clear the tensorflow graph
tf.reset_default_graph()
tf.keras.backend.clear_session()
train_error /= num_ensemble
if verbose:
print('query {}, Meta neural net train error: {}'.format(query, train_error))
# compute the acquisition function for all the candidate architectures
candidate_indices = acq_fn(candidate_predictions, explore_type)
# add the k arches with the minimum acquisition function values
for i in candidate_indices[:k]:
arch_dict = search_space.query_arch(candidates[i]['spec'],
encoding_type=encoding_type,
cutoff=cutoff,
deterministic=deterministic)
data.append(arch_dict)
if verbose:
top_5_loss = sorted([(d[loss], d['epochs']) for d in data], key=lambda d: d[0])[:min(5, len(data))]
print('bananas, query {}, top 5 losses (loss, test, epoch): {}'.format(query, top_5_loss))
recent_10_loss = [(d[loss], d['epochs']) for d in data[-10:]]
print('bananas, query {}, most recent 10 (loss, test, epoch): {}'.format(query, recent_10_loss))
query += k
return data
def gp_bayesopt_search(search_space,
num_init=10,
k=10,
total_queries=150,
distance='edit_distance',
deterministic=True,
tmpdir='./temp',
max_iter=200,
mode='single_process',
nppred=1000):
"""
Bayesian optimization with a GP prior
"""
from bo.bo.probo import ProBO
# set up the path for auxiliary pickle files
if not os.path.exists(tmpdir):
os.mkdir(tmpdir)
aux_file_path = os.path.join(tmpdir, 'aux.pkl')
num_iterations = total_queries - num_init
# black-box function that bayesopt will optimize
def fn(arch):
return search_space.query_arch(arch, deterministic=deterministic)['val_loss']
# set all the parameters for the various BayesOpt classes
fhp = Namespace(fhstr='object', namestr='train')
domp = Namespace(dom_str='list', set_domain_list_auto=True,
aux_file_path=aux_file_path,
distance=distance)
modelp = Namespace(kernp=Namespace(ls=3., alpha=1.5, sigma=1e-5),
infp=Namespace(niter=num_iterations, nwarmup=500),
distance=distance, search_space=search_space.get_type())
amp = Namespace(am_str='mygpdistmat_ucb', nppred=nppred, modelp=modelp)
optp = Namespace(opt_str='rand', max_iter=max_iter)
makerp = Namespace(domp=domp, amp=amp, optp=optp)
probop = Namespace(niter=num_iterations, fhp=fhp,
makerp=makerp, tmpdir=tmpdir, mode=mode)
data = Namespace()
# Set up initial data
init_data = search_space.generate_random_dataset(num=num_init,
deterministic_loss=deterministic)
data.X = [d['spec'] for d in init_data]
data.y = np.array([[d['val_loss']] for d in init_data])
# initialize aux file
pairs = [(data.X[i], data.y[i]) for i in range(len(data.y))]
pairs.sort(key=lambda x: x[1])
with open(aux_file_path, 'wb') as f:
pickle.dump(pairs, f)
# run Bayesian Optimization
bo = ProBO(fn, search_space, aux_file_path, data, probop, True)
bo.run_bo()
# get the validation and test loss for all architectures chosen by BayesOpt
results = []
for arch in data.X:
archtuple = search_space.query_arch(arch)
results.append(archtuple)
return results
def dngo_search(search_space,
num_init=10,
k=10,
loss='val_loss',
total_queries=150,
encoding_type='path',
cutoff=40,
acq_opt_type='mutation',
explore_type='ucb',
deterministic=True,
verbose=True):
import torch
from pybnn import DNGO
from pybnn.util.normalization import zero_mean_unit_var_normalization, zero_mean_unit_var_denormalization
from acquisition_functions import acq_fn
def fn(arch):
return search_space.query_arch(arch, deterministic=deterministic)[loss]
# set up initial data
data = search_space.generate_random_dataset(num=num_init,
encoding_type=encoding_type,
cutoff=cutoff,
deterministic_loss=deterministic)
query = num_init + k
while query <= total_queries:
# set up data
x = np.array([d['encoding'] for d in data])
y = np.array([d[loss] for d in data])
# get a set of candidate architectures
candidates = search_space.get_candidates(data,
acq_opt_type=acq_opt_type,
encoding_type=encoding_type,
cutoff=cutoff,
deterministic_loss=deterministic)
xcandidates = np.array([d['encoding'] for d in candidates])
# train the model
model = DNGO(do_mcmc=False)
model.train(x, y, do_optimize=True)
predictions = model.predict(xcandidates)
candidate_indices = acq_fn(np.array(predictions), explore_type)
# add the k arches with the minimum acquisition function values
for i in candidate_indices[:k]:
arch_dict = search_space.query_arch(candidates[i]['spec'],
encoding_type=encoding_type,
cutoff=cutoff,
deterministic=deterministic)
data.append(arch_dict)
if verbose:
top_5_loss = sorted([(d[loss], d['epochs']) for d in data], key=lambda d: d[0])[:min(5, len(data))]
print('dngo, query {}, top 5 val losses (val, test, epoch): {}'.format(query, top_5_loss))
recent_10_loss = [(d[loss], d['epochs']) for d in data[-10:]]
print('dngo, query {}, most recent 10 (val, test, epoch): {}'.format(query, recent_10_loss))
query += k
return data
================================================
FILE: nas_bench/__init__.py
================================================
================================================
FILE: nas_bench/cell.py
================================================
import numpy as np
import copy
import itertools
import random
import sys
import os
import pickle
from nasbench import api
INPUT = 'input'
OUTPUT = 'output'
CONV3X3 = 'conv3x3-bn-relu'
CONV1X1 = 'conv1x1-bn-relu'
MAXPOOL3X3 = 'maxpool3x3'
OPS = [CONV3X3, CONV1X1, MAXPOOL3X3]
NUM_VERTICES = 7
OP_SPOTS = NUM_VERTICES - 2
MAX_EDGES = 9
class Cell:
def __init__(self, matrix, ops):
self.matrix = matrix
self.ops = ops
def serialize(self):
return {
'matrix': self.matrix,
'ops': self.ops
}
def modelspec(self):
return api.ModelSpec(matrix=self.matrix, ops=self.ops)
@classmethod
def random_cell(cls, nasbench):
"""
From the NASBench repository
one-hot adjacency matrix
draw [0,1] for each slot in the adjacency matrix
"""
while True:
matrix = np.random.choice(
[0, 1], size=(NUM_VERTICES, NUM_VERTICES))
matrix = np.triu(matrix, 1)
ops = np.random.choice(OPS, size=NUM_VERTICES).tolist()
ops[0] = INPUT
ops[-1] = OUTPUT
spec = api.ModelSpec(matrix=matrix, ops=ops)
if nasbench.is_valid(spec):
return {
'matrix': matrix,
'ops': ops
}
def get_val_loss(self, nasbench, deterministic=1, patience=50, epochs=None, dataset=None):
if not deterministic:
# output one of the three validation accuracies at random
if epochs:
return (100*(1 - nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops), epochs=epochs)['validation_accuracy']))
else:
return (100*(1 - nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops))['validation_accuracy']))
else:
# query the api until we see all three accuracies, then average them
# a few architectures only have two accuracies, so we use patience to avoid an infinite loop
accs = []
while len(accs) < 3 and patience > 0:
patience -= 1
if epochs:
acc = nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops), epochs=epochs)['validation_accuracy']
else:
acc = nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops))['validation_accuracy']
if acc not in accs:
accs.append(acc)
return round(100*(1-np.mean(accs)), 4)
def get_test_loss(self, nasbench, patience=50, epochs=None, dataset=None):
"""
query the api until we see all three accuracies, then average them
a few architectures only have two accuracies, so we use patience to avoid an infinite loop
"""
accs = []
while len(accs) < 3 and patience > 0:
patience -= 1
if epochs:
acc = nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops), epochs=epochs)['test_accuracy']
else:
acc = nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops))['test_accuracy']
if acc not in accs:
accs.append(acc)
return round(100*(1-np.mean(accs)), 4)
def get_num_params(self, nasbench):
return nasbench.query(api.ModelSpec(matrix=self.matrix, ops=self.ops))['trainable_parameters']
def perturb(self, nasbench, edits=1):
"""
create new perturbed cell
inspird by https://github.com/google-research/nasbench
"""
new_matrix = copy.deepcopy(self.matrix)
new_ops = copy.deepcopy(self.ops)
for _ in range(edits):
while True:
if np.random.random() < 0.5:
for src in range(0, NUM_VERTICES - 1):
for dst in range(src+1, NUM_VERTICES):
new_matrix[src][dst] = 1 - new_matrix[src][dst]
else:
for ind in range(1, NUM_VERTICES - 1):
available = [op for op in OPS if op != new_ops[ind]]
new_ops[ind] = np.random.choice(available)
new_spec = api.ModelSpec(new_matrix, new_ops)
if nasbench.is_valid(new_spec):
break
return {
'matrix': new_matrix,
'ops': new_ops
}
def mutate(self,
nasbench,
mutation_rate=1.0,
patience=5000):
"""
A stochastic approach to perturbing the cell
inspird by https://github.com/google-research/nasbench
"""
p = 0
while p < patience:
p += 1
new_matrix = copy.deepcopy(self.matrix)
new_ops = copy.deepcopy(self.ops)
edge_mutation_prob = mutation_rate / (NUM_VERTICES * (NUM_VERTICES - 1) / 2)
# flip each edge w.p. so expected flips is 1. same for ops
for src in range(0, NUM_VERTICES - 1):
for dst in range(src + 1, NUM_VERTICES):
if random.random() < edge_mutation_prob:
new_matrix[src, dst] = 1 - new_matrix[src, dst]
op_mutation_prob = mutation_rate / OP_SPOTS
for ind in range(1, OP_SPOTS + 1):
if random.random() < op_mutation_prob:
available = [o for o in OPS if o != new_ops[ind]]
new_ops[ind] = random.choice(available)
new_spec = api.ModelSpec(new_matrix, new_ops)
if nasbench.is_valid(new_spec):
return {
'matrix': new_matrix,
'ops': new_ops
}
return self.mutate(nasbench, mutation_rate+1)
def encode_standard(self):
"""
compute the "standard" encoding,
i.e. adjacency matrix + op list encoding
"""
encoding_length = (NUM_VERTICES ** 2 - NUM_VERTICES) // 2 + OP_SPOTS
encoding = np.zeros((encoding_length))
dic = {CONV1X1: 0., CONV3X3: 0.5, MAXPOOL3X3: 1.0}
n = 0
for i in range(NUM_VERTICES - 1):
for j in range(i+1, NUM_VERTICES):
encoding[n] = self.matrix[i][j]
n += 1
for i in range(1, NUM_VERTICES - 1):
encoding[-i] = dic[self.ops[i]]
return tuple(encoding)
def get_paths(self):
"""
return all paths from input to output
"""
paths = []
for j in range(0, NUM_VERTICES):
paths.append([[]]) if self.matrix[0][j] else paths.append([])
# create paths sequentially
for i in range(1, NUM_VERTICES - 1):
for j in range(1, NUM_VERTICES):
if self.matrix[i][j]:
for path in paths[i]:
paths[j].append([*path, self.ops[i]])
return paths[-1]
def get_path_indices(self):
"""
compute the index of each path
There are 3^0 + ... + 3^5 paths total.
(Paths can be length 0 to 5, and for each path, for each node, there
are three choices for the operation.)
"""
paths = self.get_paths()
mapping = {CONV3X3: 0, CONV1X1: 1, MAXPOOL3X3: 2}
path_indices = []
for path in paths:
index = 0
for i in range(NUM_VERTICES - 1):
if i == len(path):
path_indices.append(index)
break
else:
index += len(OPS) ** i * (mapping[path[i]] + 1)
path_indices.sort()
return tuple(path_indices)
def encode_paths(self):
""" output one-hot encoding of paths """
num_paths = sum([len(OPS) ** i for i in range(OP_SPOTS + 1)])
path_indices = self.get_path_indices()
encoding = np.zeros(num_paths)
for index in path_indices:
encoding[index] = 1
return encoding
def path_distance(self, other):
"""
compute the distance between two architectures
by comparing their path encodings
"""
return np.sum(np.array(self.encode_paths() != np.array(other.encode_paths())))
def trunc_path_distance(self, other, cutoff=40):
"""
compute the distance between two architectures
by comparing their path encodings
"""
encoding = self.encode_paths()[:cutoff]
other_encoding = other.encode_paths()[:cutoff]
return np.sum(np.array(encoding) != np.array(other_encoding))
def edit_distance(self, other):
"""
compute the distance between two architectures
by comparing their adjacency matrices and op lists
"""
graph_dist = np.sum(np.array(self.matrix) != np.array(other.matrix))
ops_dist = np.sum(np.array(self.ops) != np.array(other.ops))
return (graph_dist + ops_dist)
def nasbot_distance(self, other):
# distance based on optimal transport between row sums, column sums, and ops
row_sums = sorted(np.array(self.matrix).sum(axis=0))
col_sums = sorted(np.array(self.matrix).sum(axis=1))
other_row_sums = sorted(np.array(other.matrix).sum(axis=0))
other_col_sums = sorted(np.array(other.matrix).sum(axis=1))
row_dist = np.sum(np.abs(np.subtract(row_sums, other_row_sums)))
col_dist = np.sum(np.abs(np.subtract(col_sums, other_col_sums)))
counts = [self.ops.count(op) for op in OPS]
other_counts = [other.ops.count(op) for op in OPS]
ops_dist = np.sum(np.abs(np.subtract(counts, other_counts)))
return (row_dist + col_dist + ops_dist)
================================================
FILE: nas_bench_201/__init__.py
================================================
================================================
FILE: nas_bench_201/cell.py
================================================
import numpy as np
import copy
import itertools
import random
import sys
import os
import pickle
OPS = ['avg_pool_3x3', 'nor_conv_1x1', 'nor_conv_3x3', 'none', 'skip_connect']
NUM_OPS = len(OPS)
OP_SPOTS = 6
LONGEST_PATH_LENGTH = 3
class Cell:
def __init__(self, string):
self.string = string
def get_string(self):
return self.string
def serialize(self):
return {
'string':self.string
}
@classmethod
def random_cell(cls, nasbench, max_nodes=4):
"""
From the AutoDL-Projects repository
"""
ops = []
for i in range(OP_SPOTS):
op = random.choice(OPS)
ops.append(op)
return {'string':cls.get_string_from_ops(ops)}
def get_runtime(self, nasbench, dataset='cifar100'):
return nasbench.query_by_index(index, dataset).get_eval('x-valid')['time']
def get_val_loss(self, nasbench, deterministic=1, dataset='cifar100'):
index = nasbench.query_index_by_arch(self.string)
if dataset == 'cifar10':
results = nasbench.query_by_index(index, 'cifar10-valid')
else:
results = nasbench.query_by_index(index, dataset)
accs = []
for key in results.keys():
accs.append(results[key].get_eval('x-valid')['accuracy'])
if deterministic:
return round(100-np.mean(accs), 10)
else:
return round(100-np.random.choice(accs), 10)
def get_test_loss(self, nasbench, dataset='cifar100', deterministic=1):
index = nasbench.query_index_by_arch(self.string)
results = nasbench.query_by_index(index, dataset)
accs = []
for key in results.keys():
accs.append(results[key].get_eval('ori-test')['accuracy'])
if deterministic:
return round(100-np.mean(accs), 4)
else:
return round(100-np.random.choice(accs), 4)
def get_op_list(self):
# given a string, get the list of operations
tokens = self.string.split('|')
ops = [t.split('~')[0] for i,t in enumerate(tokens) if i not in [0,2,5,9]]
return ops
def get_num(self):
# compute the unique number of the architecture, in [0, 15624]
ops = self.get_op_list()
index = 0
for i, op in enumerate(ops):
index += OPS.index(op) * NUM_OPS ** i
return index
@classmethod
def get_string_from_ops(cls, ops):
# given a list of operations, get the string
strings = ['|']
nodes = [0, 0, 1, 0, 1, 2]
for i, op in enumerate(ops):
strings.append(op+'~{}|'.format(nodes[i]))
if i < len(nodes) - 1 and nodes[i+1] == 0:
strings.append('+|')
return ''.join(strings)
def perturb(self, nasbench,
mutation_rate=1):
# more deterministic version of mutate
ops = self.get_op_list()
new_ops = []
num = np.random.choice(len(ops))
for i, op in enumerate(ops):
if i == num:
available = [o for o in OPS if o != op]
new_ops.append(np.random.choice(available))
else:
new_ops.append(op)
return {'string':self.get_string_from_ops(new_ops)}
def mutate(self,
nasbench,
mutation_rate=1.0,
patience=5000):
p = 0
ops = self.get_op_list()
new_ops = []
# keeping mutation_prob consistent with nasbench_101
mutation_prob = mutation_rate / (OP_SPOTS - 2)
for i, op in enumerate(ops):
if random.random() < mutation_prob:
available = [o for o in OPS if o != op]
new_ops.append(random.choice(available))
else:
new_ops.append(op)
return {'string':self.get_string_from_ops(new_ops)}
def encode_standard(self):
"""
compute the standard encoding
"""
ops = self.get_op_list()
encoding = []
for op in ops:
encoding.append(OPS.index(op))
return encoding
def get_num_params(self, nasbench):
# todo update to the newer nasbench-201 dataset
return 100
def get_paths(self):
"""
return all paths from input to output
"""
path_blueprints = [[3], [0,4], [1,5], [0,2,5]]
ops = self.get_op_list()
paths = []
for blueprint in path_blueprints:
paths.append([ops[node] for node in blueprint])
return paths
def get_path_indices(self):
"""
compute the index of each path
"""
paths = self.get_paths()
path_indices = []
for i, path in enumerate(paths):
if i == 0:
index = 0
elif i in [1, 2]:
index = NUM_OPS
else:
index = NUM_OPS + NUM_OPS ** 2
for j, op in enumerate(path):
index += OPS.index(op) * NUM_OPS ** j
path_indices.append(index)
return tuple(path_indices)
def encode_paths(self):
""" output one-hot encoding of paths """
num_paths = sum([NUM_OPS ** i for i in range(1, LONGEST_PATH_LENGTH + 1)])
path_indices = self.get_path_indices()
encoding = np.zeros(num_paths)
for index in path_indices:
encoding[index] = 1
return encoding
def path_distance(self, other):
"""
compute the distance between two architectures
by comparing their path encodings
"""
return np.sum(np.array(self.encode_paths() != np.array(other.encode_paths())))
def trunc_path_distance(self, other, cutoff=30):
"""
compute the distance between two architectures
by comparing their truncated path encodings
"""
paths = np.array(self.encode_paths()[cutoff])
other_paths = np.array(other.encode_paths()[cutoff])
return np.sum(paths != other_paths)
def edit_distance(self, other):
ops = self.get_op_list()
other_ops = other.get_op_list()
return np.sum([1 for i in range(len(ops)) if ops[i] != other_ops[i]])
def nasbot_distance(self, other):
# distance based on optimal transport between row sums, column sums, and ops
ops = self.get_op_list()
other_ops = other.get_op_list()
counts = [ops.count(op) for op in OPS]
other_counts = [other_ops.count(op) for op in OPS]
ops_dist = np.sum(np.abs(np.subtract(counts, other_counts)))
return ops_dist + self.edit_distance(other)
================================================
FILE: params.py
================================================
import sys
def algo_params(param_str):
"""
Return params list based on param_str.
These are the parameters used to produce the figures in the paper
For AlphaX and Reinforcement Learning, we used the corresponding github repos:
https://github.com/linnanwang/AlphaX-NASBench101
https://github.com/automl/nas_benchmarks
"""
params = []
if param_str == 'test':
params.append({'algo_name':'random', 'total_queries':30})
params.append({'algo_name':'evolution', 'total_queries':30})
params.append({'algo_name':'bananas', 'total_queries':30})
params.append({'algo_name':'gp_bayesopt', 'total_queries':30})
params.append({'algo_name':'dngo', 'total_queries':30})
elif param_str == 'test_simple':
params.append({'algo_name':'random', 'total_queries':30})
params.append({'algo_name':'evolution', 'total_queries':30})
elif param_str == 'random':
params.append({'algo_name':'random', 'total_queries':10})
elif param_str == 'bananas':
params.append({'algo_name':'bananas', 'total_queries':150, 'verbose':0})
elif param_str == 'main_experiments':
params.append({'algo_name':'random', 'total_queries':150})
params.append({'algo_name':'evolution', 'total_queries':150})
params.append({'algo_name':'bananas', 'total_queries':150})
params.append({'algo_name':'gp_bayesopt', 'total_queries':150})
params.append({'algo_name':'dngo', 'total_queries':150})
elif param_str == 'ablation':
params.append({'algo_name':'bananas', 'total_queries':150})
params.append({'algo_name':'bananas', 'total_queries':150, 'encoding_type':'adjacency'})
params.append({'algo_name':'gp_bayesopt', 'total_queries':150, 'distance':'path_distance'})
params.append({'algo_name':'gp_bayesopt', 'total_queries':150, 'distance':'edit_distance'})
params.append({'algo_name':'bananas', 'total_queries':150, 'acq_opt_type':'random'})
else:
print('invalid algorithm params: {}'.format(param_str))
sys.exit()
print('\n* Running experiment: ' + param_str)
return params
def meta_neuralnet_params(param_str):
if param_str == 'nasbench':
params = {'search_space':'nasbench', 'dataset':'cifar10', 'loss':'mae', 'num_layers':10, 'layer_width':20, \
'epochs':150, 'batch_size':32, 'lr':.01, 'regularization':0, 'verbose':0}
elif param_str == 'darts':
params = {'search_space':'darts', 'dataset':'cifar10', 'loss':'mape', 'num_layers':10, 'layer_width':20, \
'epochs':10000, 'batch_size':32, 'lr':.00001, 'regularization':0, 'verbose':0}
elif param_str == 'nasbench_201_cifar10':
params = {'search_space':'nasbench_201', 'dataset':'cifar10', 'loss':'mae', 'num_layers':10, 'layer_width':20, \
'epochs':150, 'batch_size':32, 'lr':.01, 'regularization':0, 'verbose':0}
elif param_str == 'nasbench_201_cifar100':
params = {'search_space':'nasbench_201', 'dataset':'cifar100', 'loss':'mae', 'num_layers':10, 'layer_width':20, \
'epochs':150, 'batch_size':32, 'lr':.01, 'regularization':0, 'verbose':0}
elif param_str == 'nasbench_201_imagenet':
params = {'search_space':'nasbench_201', 'dataset':'ImageNet16-120', 'loss':'mae', 'num_layers':10, 'layer_width':20, \
'epochs':150, 'batch_size':32, 'lr':.01, 'regularization':0, 'verbose':0}
else:
print('invalid meta neural net params: {}'.format(param_str))
sys.exit()
return params
================================================
FILE: run_experiments_parallel.sh
================================================
param_str=fifty_epochs
experiment_name=bananas
# set all instance names and zones
instances=(bananas-t4-1-vm bananas-t4-2-vm bananas-t4-3-vm bananas-t4-4-vm \
bananas-t4-5-vm bananas-t4-6-vm bananas-t4-7-vm bananas-t4-8-vm \
bananas-t4-9-vm bananas-t4-10-vm)
zones=(us-west1-b us-west1-b us-west1-b us-west1-b us-west1-b us-west1-b \
us-west1-b us-west1-b us-west1-b us-west1-b)
# set parameters based on the param string
if [ $param_str = test ]; then
start_iteration=0
end_iteration=1
k=10
untrained_filename=untrained_spec
trained_filename=trained_spec
epochs=1
fi
if [ $param_str = fifty_epochs ]; then
start_iteration=0
end_iteration=9
k=10
untrained_filename=untrained_spec
trained_filename=trained_spec
epochs=50
fi
# start bananas
for i in $(seq $start_iteration $end_iteration)
do
let start=$i*$k
let end=($i+1)*$k-1
# train the neural net
# input: all pickle files with index from 0 to i*k-1
# output: k pickle files for the architectures to train next (indices i*k to (i+1)*k-1)
echo about to run meta neural network in iteration $i
python3 metann_runner.py --experiment_name $experiment_name --params $nas_params --k $k \
--untrained_filename $untrained_filename --trained_filename $trained_filename --query $start
echo outputted architectures to train in iteration $i
# train the k architectures
let max_j=$k-1
for j in $(seq 0 $max_j )
do
let query=$i*$k+$j
instance=${instances[$j]}
zone=${zones[$j]}
untrained_filepath=$experiment_name/$untrained_filename\_$query.pkl
trained_filepath=$experiment_name/$trained_filename\_$query.pkl
echo about to copy file $untrained_filepath to instance $instance
gcloud compute scp $untrained_filepath $instance:~/naszilla/$experiment_name/ --zone $zone
echo about to ssh into instance $instance
gcloud compute ssh $instance --zone $zone --command="cd naszilla; \
python3 train_arch_runner.py --untrained_filepath $untrained_filepath \
--trained_filepath $trained_filepath --epochs $epochs" &
done
wait
echo all architectures trained in iteration $i
# copy results of trained architectures to the master CPU
let max_j=$k-1
for j in $(seq 0 $max_j )
do
let query=$i*$k+$j
instance=${instances[$j]}
zone=${zones[$j]}
trained_filepath=$experiment_name/$trained_filename\_$query.pkl
gcloud compute scp $instance:~/naszilla/$trained_filepath $experiment_name --zone $zone
done
echo finished iteration $i
done
================================================
FILE: run_experiments_sequential.py
================================================
import argparse
import time
import logging
import sys
import os
import pickle
import numpy as np
import copy
from params import *
def run_experiments(args, save_dir):
os.environ['search_space'] = args.search_space
from nas_algorithms import run_nas_algorithm
from data import Data
trials = args.trials
out_file = args.output_filename
save_specs = args.save_specs
metann_params = meta_neuralnet_params(args.search_space)
algorithm_params = algo_params(args.algo_params)
num_algos = len(algorithm_params)
logging.info(algorithm_params)
# set up search space
mp = copy.deepcopy(metann_params)
ss = mp.pop('search_space')
dataset = mp.pop('dataset')
search_space = Data(ss, dataset=dataset)
for i in range(trials):
results = []
walltimes = []
run_data = []
for j in range(num_algos):
# run NAS algorithm
print('\n* Running algorithm: {}'.format(algorithm_params[j]))
starttime = time.time()
algo_result, run_datum = run_nas_algorithm(algorithm_params[j], search_space, mp)
algo_result = np.round(algo_result, 5)
# remove unnecessary dict entries that take up space
for d in run_datum:
if not save_specs:
d.pop('spec')
for key in ['encoding', 'adjacency', 'path', 'dist_to_min']:
if key in d:
d.pop(key)
# add walltime, results, run_data
walltimes.append(time.time()-starttime)
results.append(algo_result)
run_data.append(run_datum)
# print and pickle results
filename = os.path.join(save_dir, '{}_{}.pkl'.format(out_file, i))
print('\n* Trial summary: (params, results, walltimes)')
print(algorithm_params)
print(metann_params)
print(results)
print(walltimes)
print('\n* Saving to file {}'.format(filename))
with open(filename, 'wb') as f:
pickle.dump([algorithm_params, metann_params, results, walltimes, run_data], f)
f.close()
def main(args):
# make save directory
save_dir = args.save_dir
if not os.path.exists(save_dir):
os.mkdir(save_dir)
algo_params = args.algo_params
save_path = save_dir + '/' + algo_params + '/'
if not os.path.exists(save_path):
os.mkdir(save_path)
# set up logging
log_format = '%(asctime)s %(message)s'
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format=log_format, datefmt='%m/%d %I:%M:%S %p')
fh = logging.FileHandler(os.path.join(save_dir, 'log.txt'))
fh.setFormatter(logging.Formatter(log_format))
logging.getLogger().addHandler(fh)
logging.info(args)
run_experiments(args, save_path)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Args for BANANAS experiments')
parser.add_argument('--trials', type=int, default=500, help='Number of trials')
parser.add_argument('--search_space', type=str, default='nasbench', \
help='nasbench or darts')
parser.add_argument('--algo_params', type=str, default='main_experiments', help='which parameters to use')
parser.add_argument('--output_filename', type=str, default='round', help='name of output files')
parser.add_argument('--save_dir', type=str, default='results_output', help='name of save directory')
parser.add_argument('--save_specs', type=bool, default=False, help='save the architecture specs')
args = parser.parse_args()
main(args)
================================================
FILE: train_arch_runner.py
================================================
import argparse
import time
import logging
import sys
import os
import pickle
sys.path.append(os.path.expanduser('~/darts/cnn'))
from train_class import Train
"""
train arch runner is used in run_experiments_parallel
- loads data by opening a pickle file containing an architecture spec
- trains that architecture for e epochs
- outputs a new pickle file with the architecture spec and its validation loss
"""
def run(args):
untrained_filepath = os.path.expanduser(args.untrained_filepath)
trained_filepath = os.path.expanduser(args.trained_filepath)
epochs = args.epochs
gpu = args.gpu
train_portion = args.train_portion
seed = args.seed
save = args.save
# load the arch spec that will be trained
dic = pickle.load(open(untrained_filepath, 'rb'))
arch = dic['spec']
print('loaded arch', arch)
# train the arch
trainer = Train()
val_accs, test_accs = trainer.main(arch,
epochs=epochs,
gpu=gpu,
train_portion=train_portion,
seed=seed,
save=save)
val_sum = 0
for epoch, val_acc in val_accs:
key = 'val_loss_' + str(epoch)
dic[key] = 100 - val_acc
val_sum += dic[key]
for epoch, test_acc in test_accs:
key = 'test_loss_' + str(epoch)
dic[key] = 100 - test_acc
val_loss_avg = val_sum / len(val_accs)
dic['val_loss_avg'] = val_loss_avg
dic['val_loss'] = 100 - val_accs[-1][-1]
dic['test_loss'] = 100 - test_accs[-1][-1]
dic['filepath'] = args.trained_filepath
print('arch {}'.format(arch))
print('val loss: {}'.format(dic['val_loss']))
print('test loss: {}'.format(dic['test_loss']))
print('val loss avg: {}'.format(dic['val_loss_avg']))
with open(trained_filepath, 'wb') as f:
pickle.dump(dic, f)
def main(args):
#set up save dir
save_dir = './'
#set up logging
log_format = '%(asctime)s %(message)s'
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format=log_format, datefmt='%m/%d %I:%M:%S %p')
fh = logging.FileHandler(os.path.join(save_dir, 'log.txt'))
fh.setFormatter(logging.Formatter(log_format))
logging.getLogger().addHandler(fh)
logging.info(args)
run(args)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Args for training a darts arch')
parser.add_argument('--untrained_filepath', type=str, default='darts_test/untrained_spec_0.pkl', help='name of input files')
parser.add_argument('--trained_filepath', type=str, default='darts_test/trained_spec_0.pkl', help='name of output files')
parser.add_argument('--epochs', type=int, default=50, help='number of training epochs')
parser.add_argument('--gpu', type=int, default=0, help='which gpu to use')
parser.add_argument('--train_portion', type=float, default=0.7, help='portion of training data used for training')
parser.add_argument('--seed', type=float, default=0, help='random seed to use')
parser.add_argument('--save', type=str, default='EXP', help='directory to save to')
args = parser.parse_args()
main(args)