From ea14b2f1f9989d1e32f7a709a77d07d8adca12fc Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 21 Oct 2016 16:02:40 +1100 Subject: [PATCH 01/33] Tweaks to tutorials documentation --- docs/tutorials.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 9c113f3..5c2469e 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -11,7 +11,7 @@ There are two different approaches to using M-LOOP: 1. You can execute M-LOOP from a command line (or shell) and configure it using a text file. 2. You can use M-LOOP as a :ref:`python API `. -If you have a standard experiment, that is operated by LabVIEW, Simulink or some other method, then your should use option 1 and follow the :ref:` first tutorial `. If your experiment is operated using python, you should consider using option 2 as it will give you more flexibility and control, in which case, look at the :ref:`second tutorial `. +If you have a standard experiment, that is operated by LabVIEW, Simulink or some other method, then you should use option 1 and follow the :ref:`first tutorial `. If your experiment is operated using python, you should consider using option 2 as it will give you more flexibility and control, in which case, look at the :ref:`second tutorial `. .. _sec-standard-experiment: @@ -178,7 +178,7 @@ When writing the file *exp_output.txt* there are three keywords and values you c cost refers to the cost calculated from the experimental data. uncer, is optional, and refers to the uncertainty in the cost measurement made. Note, M-LOOP by default assumes there is some noise corrupting costs, which is fitted and compensated for. Hence, if there is some noise in your costs which you are unable to predict from a single measurement, do not worry, you do not have to estimate uncer, you can just leave it out. Lastly bad can be used to indicate an experiment failed and was not able to produce a cost. If the experiment worked set bad = false and if it failed set bad = true. -Note you do not have to include all of the keywords, you must provide at least a cost or the bad keyword set to false. For example a successful run can simply be:: +Note you do not have to include all of the keywords, you must provide at least a cost or the bad keyword set to true. For example a successful run can simply be:: cost = 0.3 @@ -219,7 +219,7 @@ M-LOOP, by default, will produce a set of visualizations. These plots show the o Python controlled experiment ============================ -If you have an experiment that is already under python control you can use M-LOOP as an API. Below we go over the example python script *python_controlled_experiment.py* you should also read over the :ref:` first tutorial ` to get a general idea of how M-LOOP works. +If you have an experiment that is already under python control you can use M-LOOP as an API. Below we go over the example python script *python_controlled_experiment.py* you should also read over the :ref:`first tutorial ` to get a general idea of how M-LOOP works. When integrating M-LOOP into your laboratory remember that it will be controlling you experiment, not vice versa. Hence, at the top level of your python script you will execute M-LOOP which will then call on your experiment when needed. Your experiment will not be making calls of M-LOOP. From 3c97b8f4c1a23cd66d080648ff0b4df88d5e3317 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Sat, 22 Oct 2016 22:20:39 +1100 Subject: [PATCH 02/33] Fix setup syntax error --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ef248c5..01f5b48 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def main(): }, setup_requires=['pytest-runner'], - install_requires = ['pip>=7.0' + install_requires = ['pip>=7.0', 'docutils>=0.3', 'numpy>=1.11', 'scipy>=0.17', From 1897106d00a2913e1737be28c7f7bcd2efc7696f Mon Sep 17 00:00:00 2001 From: mhush Date: Thu, 3 Nov 2016 17:27:56 +1100 Subject: [PATCH 03/33] Added additional tests for halting conditions. Fixed bug with GP fitting data with bad runs. --- examples/gaussian_process_complete_config.txt | 2 +- mloop/controllers.py | 42 ++++++-------- mloop/learners.py | 26 +++++---- tests/test_units.py | 80 +++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 tests/test_units.py diff --git a/examples/gaussian_process_complete_config.txt b/examples/gaussian_process_complete_config.txt index 2e50dbe..55a1420 100644 --- a/examples/gaussian_process_complete_config.txt +++ b/examples/gaussian_process_complete_config.txt @@ -2,7 +2,7 @@ #--------------------------------- #General options -max_num_runs = 100 #number of planned runs +max_num_runs = 500 #number of planned runs target_cost = 0.1 #cost to beat #Gaussian process options diff --git a/mloop/controllers.py b/mloop/controllers.py index e4b6964..03bfc23 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -282,7 +282,7 @@ def _get_cost_and_in_dict(self): except ValueError: self.log.error('One of the values you provided in the cost dict could not be converted into the right type.') raise - if self.curr_bad and 'cost' in dict: + if self.curr_bad and ('cost' in in_dict): self.log.warning('The cost provided with the bad run will be saved, but not used by the learners.') self.in_costs.append(self.curr_cost) @@ -677,26 +677,23 @@ def _optimization_routine(self): Overrides _optimization_routine. Uses the parent routine for the training runs. Implements a customized _optimization_rountine when running the Gaussian Process learner. ''' #Run the training runs using the standard optimization routine. Adjust the number of max_runs - save_max_num_runs = self.max_num_runs - self.max_num_runs = self.num_training_runs - 1 self.log.debug('Starting training optimization.') super(GaussianProcessController,self)._optimization_routine() - #Start last training run - self.log.info('Run:' + str(self.num_in_costs +1)) - next_params = self._next_params() - self._put_params_and_out_dict(next_params) - - #Begin GP optimization routine - self.max_num_runs = save_max_num_runs - - self.log.debug('Starting GP optimization.') - self.new_params_event.set() - self.save_archive() - self._get_cost_and_in_dict() - - gp_consec = 0 - gp_count = 0 + if self.check_end_conditions(): + #Start last training run + self.log.info('Run:' + str(self.num_in_costs +1)) + next_params = self._next_params() + self._put_params_and_out_dict(next_params) + + self.log.debug('Starting GP optimization.') + self.new_params_event.set() + self.save_archive() + self._get_cost_and_in_dict() + + gp_consec = 0 + gp_count = 0 + while self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) if gp_consec==self.generation_num or (self.no_delay and self.gp_learner_params_queue.empty()): @@ -723,12 +720,7 @@ def _shut_down(self): self.log.debug('GP learner end set.') self.end_gp_learner.set() self.gp_learner.join() - #self.gp_learner.join(self.gp_learner.learner_wait*3) - ''' - if self.gp_learner.is_alive(): - self.log.warning('GP Learner did not join in time had to terminate.') - self.gp_learner.terminate() - ''' + self.log.debug('GP learner joined') last_dict = None while not self.gp_learner_params_queue.empty(): @@ -750,7 +742,7 @@ def _shut_down(self): self.archive_dict.update(last_dict) else: if self.gp_learner.predict_global_minima_at_end or self.gp_learner.predict_local_minima_at_end: - self.log.warning('GP Learner may not have closed properly unable to get best and/or all minima.') + self.log.info('GP Learner did not provide best and/or all minima.') super(GaussianProcessController,self)._shut_down() def print_results(self): diff --git a/mloop/learners.py b/mloop/learners.py index 14556f2..771f093 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1073,9 +1073,9 @@ def __init__(self, if self.default_bad_uncertainty < 0: self.log.error('Default bad uncertainty must be positive.') raise ValueError - if (self.default_bad_cost is None) and (self.default_bad_cost is None): + if (self.default_bad_cost is None) and (self.default_bad_uncertainty is None): self.bad_defaults_set = False - elif (self.default_bad_cost is not None) and (self.default_bad_cost is not None): + elif (self.default_bad_cost is not None) and (self.default_bad_uncertainty is not None): self.bad_defaults_set = True else: self.log.error('Both the default cost and uncertainty must be set for a bad run or they must both be set to None.') @@ -1156,13 +1156,14 @@ def get_params_and_costs(self): new_costs = [] new_uncers = [] new_bads = [] - new_costs_count = 0 update_bads_flag = False while not self.costs_in_queue.empty(): (param, cost, uncer, bad) = self.costs_in_queue.get_nowait() + self.costs_count +=1 + if bad: - new_bads.append(self.data_count) + new_bads.append(self.costs_count-1) if self.bad_defaults_set: cost = self.default_bad_cost uncer = self.default_bad_uncertainty @@ -1181,18 +1182,15 @@ def get_params_and_costs(self): self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) uncer = max(float(uncer), self.minimum_uncertainty) - new_costs_count += 1 - self.costs_count +=1 - cost_change_flag = False if cost > self.worst_cost: self.worst_cost = cost - self.worst_index = self.costs_count + self.worst_index = self.costs_count-1 cost_change_flag = True if cost < self.best_cost: self.best_cost = cost self.best_params = param - self.best_index = self.costs_count + self.best_index = self.costs_count-1 cost_change_flag = True if cost_change_flag: self.cost_range = self.worst_cost - self.best_cost @@ -1202,7 +1200,8 @@ def get_params_and_costs(self): new_params.append(param) new_costs.append(cost) new_uncers.append(uncer) - + + if self.all_params.size==0: self.all_params = np.array(new_params, dtype=float) self.all_costs = np.array(new_costs, dtype=float) @@ -1212,13 +1211,15 @@ def get_params_and_costs(self): self.all_costs = np.concatenate((self.all_costs, np.array(new_costs, dtype=float))) self.all_uncers = np.concatenate((self.all_uncers, np.array(new_uncers, dtype=float))) + self.bad_run_indexs.append(new_bads) + if self.all_params.shape != (self.costs_count,self.num_params): self.log('Saved GP params are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_params)) if self.all_costs.shape != (self.costs_count,): self.log('Saved GP costs are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_costs)) if self.all_uncers.shape != (self.costs_count,): self.log('Saved GP uncertainties are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_uncers)) - + if update_bads_flag: self.update_bads() @@ -1366,6 +1367,9 @@ def run(self): raise LearnerInterrupt() except LearnerInterrupt: pass + if self.predict_global_minima_at_end or self.predict_local_minima_at_end: + self.get_params_and_costs() + self.fit_gaussian_process() end_dict = {} if self.predict_global_minima_at_end: self.find_global_minima() diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..a45e831 --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,80 @@ +''' +Unit test for all of the example scripts provided in the examples folder. +''' +from __future__ import absolute_import, division, print_function + +import os +import unittest +import math +import mloop.interfaces as mli +import mloop.controllers as mlc +import numpy as np +import multiprocessing as mp + +class CostListInterface(mli.Interface): + def __init__(self, cost_list): + super(CostListInterface,self).__init__() + self.call_count = 0 + self.cost_list = cost_list + def get_next_cost_dict(self,params_dict): + if np.isfinite(self.cost_list[self.call_count]): + cost_dict = {'cost': self.cost_list[self.call_count]} + else: + cost_dict = {'bad': True} + self.call_count += 1 + return cost_dict + +class TestUnits(unittest.TestCase): + + def test_max_num_runs(self): + cost_list = [5.,4.,3.,2.,1.] + interface = CostListInterface(cost_list) + controller = mlc.create_controller(interface, + max_num_runs = 5, + target_cost = -1, + max_num_runs_without_better_params = 2) + controller.optimize() + self.assertTrue(controller.best_cost == 1.) + self.assertTrue(np.array_equiv(np.array(controller.in_costs), + np.array(cost_list))) + + + def test_max_num_runs_without_better_params(self): + cost_list = [1.,2.,3.,4.,5.] + interface = CostListInterface(cost_list) + controller = mlc.create_controller(interface, + max_num_runs = 10, + target_cost = -1, + max_num_runs_without_better_params = 4) + controller.optimize() + self.assertTrue(controller.best_cost == 1.) + self.assertTrue(np.array_equiv(np.array(controller.in_costs), + np.array(cost_list))) + + def test_target_cost(self): + cost_list = [1.,2.,-1.] + interface = CostListInterface(cost_list) + controller = mlc.create_controller(interface, + max_num_runs = 10, + target_cost = -1, + max_num_runs_without_better_params = 4) + controller.optimize() + self.assertTrue(controller.best_cost == -1.) + self.assertTrue(np.array_equiv(np.array(controller.in_costs), + np.array(cost_list))) + + def test_bad(self): + cost_list = [1., float('nan'),2.,float('nan'),-1.] + interface = CostListInterface(cost_list) + controller = mlc.create_controller(interface, + max_num_runs = 10, + target_cost = -1, + max_num_runs_without_better_params = 4) + controller.optimize() + self.assertTrue(controller.best_cost == -1.) + for x,y in zip(controller.in_costs,cost_list): + self.assertTrue(x==y or (math.isnan(x) and math.isnan(y))) + +if __name__ == "__main__": + mp.freeze_support() + unittest.main() \ No newline at end of file From cfa5748ebb82d1ac7d23eaab5042e4b056881984 Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Fri, 4 Nov 2016 15:55:14 +1100 Subject: [PATCH 04/33] Fixed halting conditions Previously the training runs had to be completed before M-LOOP would halt. This lead to unintuitive behavior when the halting conditions were early on in the optimization process. M-LOOP now halts immediately when any of the halting conditions are met. --- examples/gaussian_process_complete_config.txt | 2 +- mloop/controllers.py | 46 ++++++++++++++++----------- mloop/launchers.py | 1 - mloop/learners.py | 11 ++----- mloop/utilities.py | 24 ++++++++++++++ 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/examples/gaussian_process_complete_config.txt b/examples/gaussian_process_complete_config.txt index 55a1420..2e50dbe 100644 --- a/examples/gaussian_process_complete_config.txt +++ b/examples/gaussian_process_complete_config.txt @@ -2,7 +2,7 @@ #--------------------------------- #General options -max_num_runs = 500 #number of planned runs +max_num_runs = 100 #number of planned runs target_cost = 0.1 #cost to beat #Gaussian process options diff --git a/mloop/controllers.py b/mloop/controllers.py index 03bfc23..691e51b 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -334,6 +334,8 @@ def optimize(self): self._start_up() self._optimization_routine() log.info('Controller finished. Closing down M-LOOP. Please wait a moment...') + except ControllerInterrupt: + self.log.warning('Controller ended by interruption.') except (KeyboardInterrupt,SystemExit): log.warning('!!! Do not give the interrupt signal again !!! \n M-LOOP stopped with keyboard interupt or system exit. Please wait at least 1 minute for the threads to safely shut down. \n ') log.warning('Closing down controller.') @@ -392,22 +394,19 @@ def _optimization_routine(self): Runs controller main loop. Gives parameters to experiment and saves costs returned. ''' self.log.debug('Start controller loop.') - try: + self.log.info('Run:' + str(self.num_in_costs +1)) + next_params = self._first_params() + self._put_params_and_out_dict(next_params) + self.save_archive() + self._get_cost_and_in_dict() + while self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) - next_params = self._first_params() + next_params = self._next_params() self._put_params_and_out_dict(next_params) self.save_archive() self._get_cost_and_in_dict() - while self.check_end_conditions(): - self.log.info('Run:' + str(self.num_in_costs +1)) - next_params = self._next_params() - self._put_params_and_out_dict(next_params) - self.save_archive() - self._get_cost_and_in_dict() - self.log.debug('End controller loop.') - except ControllerInterrupt: - self.log.warning('Controller ended by interruption.') - + self.log.debug('End controller loop.') + def _first_params(self): ''' Checks queue to get first parameters. @@ -619,7 +618,7 @@ def __init__(self, interface, self.new_params_event = self.gp_learner.new_params_event self.remaining_kwargs = self.gp_learner.remaining_kwargs self.generation_num = self.gp_learner.generation_num - + def _put_params_and_out_dict(self, params): ''' Override _put_params_and_out_dict function, used when the training learner creates parameters. Makes the defualt param_type the training type and sets last_training_run_flag. @@ -678,8 +677,18 @@ def _optimization_routine(self): ''' #Run the training runs using the standard optimization routine. Adjust the number of max_runs self.log.debug('Starting training optimization.') - super(GaussianProcessController,self)._optimization_routine() - + self.log.info('Run:' + str(self.num_in_costs +1)) + next_params = self._first_params() + self._put_params_and_out_dict(next_params) + self.save_archive() + self._get_cost_and_in_dict() + while (self.num_in_costs < self.num_training_runs) and self.check_end_conditions(): + self.log.info('Run:' + str(self.num_in_costs +1)) + next_params = self._next_params() + self._put_params_and_out_dict(next_params) + self.save_archive() + self._get_cost_and_in_dict() + if self.check_end_conditions(): #Start last training run self.log.info('Run:' + str(self.num_in_costs +1)) @@ -690,10 +699,11 @@ def _optimization_routine(self): self.new_params_event.set() self.save_archive() self._get_cost_and_in_dict() - + self.log.debug('End training runs.') + gp_consec = 0 - gp_count = 0 - + gp_count = 0 + while self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) if gp_consec==self.generation_num or (self.no_delay and self.gp_learner_params_queue.empty()): diff --git a/mloop/launchers.py b/mloop/launchers.py index 5cafb09..a41e378 100644 --- a/mloop/launchers.py +++ b/mloop/launchers.py @@ -27,7 +27,6 @@ def launch_from_file(config_filename, except (IOError, OSError): print('Unable to open M-LOOP configuration file:' + repr(config_filename)) raise - file_kwargs.update(kwargs) #Main run sequence #Create interface and extract unused keywords diff --git a/mloop/learners.py b/mloop/learners.py index 771f093..9f7cea7 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -927,10 +927,7 @@ def __init__(self, self.length_scale = np.squeeze(np.array(self.training_dict['length_scale'])) self.length_scale_history = list(self.training_dict['length_scale_history']) self.noise_level = float(self.training_dict['noise_level']) - if isinstance(self.training_dict['noise_level_history'], np.ndarray): - self.noise_level_history = list(np.squeeze(self.training_dict['noise_level_history'])) - else: - self.noise_level_history = list( self.training_dict['noise_level_history']) + self.noise_level_history = mlu.safe_cast_to_list(self.training_dict['noise_level_history']) #Counters self.costs_count = int(self.training_dict['costs_count']) @@ -942,11 +939,7 @@ def __init__(self, self.all_costs = np.squeeze(np.array(self.training_dict['all_costs'], dtype=float)) self.all_uncers = np.squeeze(np.array(self.training_dict['all_uncers'], dtype=float)) - if isinstance(self.training_dict['bad_run_indexs'], np.ndarray): - self.bad_run_indexs = list(np.squeeze(self.training_dict['bad_run_indexs'])) - else: - self.bad_run_indexs = list(self.training_dict['bad_run_indexs']) - + self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) #Derived properties self.best_cost = float(self.training_dict['best_cost']) diff --git a/mloop/utilities.py b/mloop/utilities.py index 87c90e3..1d5a1cf 100644 --- a/mloop/utilities.py +++ b/mloop/utilities.py @@ -173,6 +173,30 @@ def check_file_type_supported(file_type): ''' return file_type == 'mat' or 'txt' or 'pkl' +def safe_cast_to_list(in_array): + ''' + Attempts to safely cast a numpy array to a list, if not a numpy array just casts to list on the object. + + Args: + in_array (array or equivalent): The array (or otherwise) to be converted to a list. + + Returns: + list : List of elements from in_array + + ''' + + if isinstance(in_array, np.ndarray): + t_array = np.squeeze(in_array) + if t_array.shape == (): + out_list = [t_array[()]] + else: + out_list = list(t_array) + else: + out_list = list(in_array) + + return out_list + + class NullQueueListener(): ''' Shell class with start and stop functions that do nothing. Queue listener is not implemented in python 2. Current fix is to simply use the multiprocessing class to pipe straight to the cmd line if running on python 2. This is class is just a placeholder. From 58577fd25a7185ca26bb1a5d2ba09f20694cfba2 Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Fri, 4 Nov 2016 17:04:38 +1100 Subject: [PATCH 05/33] v2.1.1 Candidate Updated the documentation. Candidate for new version to be released on PyPI --- docs/api/controllers.rst | 2 +- docs/api/index.rst | 1 + docs/api/interfaces.rst | 2 +- docs/api/launchers.rst | 2 +- docs/api/learners.rst | 2 +- docs/api/mloop.rst | 2 +- docs/api/t_esting.rst | 2 +- docs/api/utilities.rst | 2 +- docs/api/visualizations.rst | 2 +- docs/tutorials.rst | 38 ++++++++++++++++++++++++++------------ examples/tutorial_config.txt | 10 +++++++--- mloop/__init__.py | 2 +- setup.py | 2 +- 13 files changed, 44 insertions(+), 25 deletions(-) diff --git a/docs/api/controllers.rst b/docs/api/controllers.rst index 9af1001..4679486 100644 --- a/docs/api/controllers.rst +++ b/docs/api/controllers.rst @@ -1,7 +1,7 @@ .. _api-controllers: controllers ------------ +=========== .. automodule:: mloop.controllers :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index 3d2ff16..b8d6915 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,5 +1,6 @@ .. _sec-api: +========== M-LOOP API ========== diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 80eb1e9..9d443c8 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -1,5 +1,5 @@ interfaces ----------- +========== .. automodule:: mloop.interfaces :members: diff --git a/docs/api/launchers.rst b/docs/api/launchers.rst index 7d3c105..3e9454c 100644 --- a/docs/api/launchers.rst +++ b/docs/api/launchers.rst @@ -1,5 +1,5 @@ launchers ---------- +========= .. automodule:: mloop.launchers :members: diff --git a/docs/api/learners.rst b/docs/api/learners.rst index 642105a..7385be9 100644 --- a/docs/api/learners.rst +++ b/docs/api/learners.rst @@ -1,7 +1,7 @@ .. _api-learners: learners ---------- +======== .. automodule:: mloop.learners :members: diff --git a/docs/api/mloop.rst b/docs/api/mloop.rst index a0127dd..affcb8f 100644 --- a/docs/api/mloop.rst +++ b/docs/api/mloop.rst @@ -1,4 +1,4 @@ mloop ------ +===== .. automodule:: mloop diff --git a/docs/api/t_esting.rst b/docs/api/t_esting.rst index 9bb25ae..1209b5a 100644 --- a/docs/api/t_esting.rst +++ b/docs/api/t_esting.rst @@ -1,5 +1,5 @@ testing -------- +======= .. automodule:: mloop.testing :members: diff --git a/docs/api/utilities.rst b/docs/api/utilities.rst index 1f22fb5..8e63990 100644 --- a/docs/api/utilities.rst +++ b/docs/api/utilities.rst @@ -1,5 +1,5 @@ utilities ---------- +========= .. automodule:: mloop.utilities :members: diff --git a/docs/api/visualizations.rst b/docs/api/visualizations.rst index f602372..91d7209 100644 --- a/docs/api/visualizations.rst +++ b/docs/api/visualizations.rst @@ -1,5 +1,5 @@ visualizations --------------- +============== .. automodule:: mloop.visualizations :members: diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 5c2469e..4fdefb8 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -68,15 +68,19 @@ You can add comments to your file using #, everything past # will be ignored. Ex num_params = 2 #number of parameters min_boundary = [-1,-1] #minimum boundary max_boundary = [1,1] #maximum boundary + first_params = [0.5,0.5] #first parameters to try + trust_region = 0.4 #maximum % move distance from best params #Halting conditions max_num_runs = 1000 #maximum number of runs max_num_runs_without_better_params = 50 #maximum number of runs without finding better parameters target_cost = 0.01 #optimization halts when a cost below this target is found + + #Learner options + cost_has_noise = True #whether the cost are corrupted by noise or not - #Learner specific options - first_params = [0.5,0.5] #first parameters to try - trust_region = 0.4 #maximum % move distance from best params + #Timing options + no_delay = True #wait for learner to make generate new parameters or use training algorithms #File format options interface_file_type = 'txt' #file types of *exp_input.mat* and *exp_output.mat* @@ -86,7 +90,7 @@ You can add comments to your file using #, everything past # will be ignored. Ex #Visualizations visualizations = True -We will now explain the options in each of their groups. In almost all cases you will only need to the parameters settings and halting conditions, but we have also describe a few of the most commonly used extra options. +We will now explain the options in each of their groups. In almost all cases you will only need to the parameters settings and halting conditions, but we have also described a few of the most commonly used extra options. Parameter settings ~~~~~~~~~~~~~~~~~~ @@ -99,6 +103,10 @@ The number of parameters and their limits is defined with three keywords:: num_params defines the number of parameters, min_boundary defines the minimum value each of the parameters can take and max_boundary defines the maximum value each parameter can take. Here there are two value which each must be between -1 and 1. +first_parameters defines the first parameters the learner will try. You only need to set this if you have a safe set of parameters you want the experiment to start with. Just delete this keyword if any set of parameters in the boundaries will work. + +trust_region defines the maximum change allowed in the parameters from the best parameters found so far. In the current example the region size is 2 by 2, with a trust region of 40% thus the maximum allowed change for the second run will be [0 +/- 0.8, 0 +/- 0.8]. This is only needed if your experiment produces bad results when the parameters are changes significantly between runs. Simply delete this keyword if your experiment works with any set of parameters within the boundaries. + Halting conditions ~~~~~~~~~~~~~~~~~~ @@ -107,6 +115,8 @@ The halting conditions define when the simulation will stop. We present three op max_num_runs = 100 max_num_runs_without_better_params = 10 target_cost = 0.1 + first_params = [0.5,0.5] + trust_region = 0.4 max_num_runs is the maximum number of runs that the optimization algorithm is allowed to run. max_num_runs_without_better_params is the maximum number of runs allowed before a lower cost and better parameters is found. Finally, when target_cost is set, if a run produces a cost that is less than this value the optimization process will stop. @@ -119,19 +129,23 @@ If you do not want one of the halting conditions, simply delete it from your fil max_num_runs_without_better_params = 10 -Learner specific options -~~~~~~~~~~~~~~~~~~~~~~~~ +Learner Options +~~~~~~~~~~~~~~~ -There are many learner specific options (and different learner algorithms) described in :ref:`sec-examples`. Here we consider just a couple of the most commonly used ones. M-LOOP has been designed to find an optimum quickly with no custom configuration as long as the experiment is able to provide a cost for every parameter it provides. +There are many learner specific options (and different learner algorithms) described in :ref:`sec-examples`. Here we just present a common one:: -However if your experiment will fail to work if there are sudden and significant changes to your parameters you may need to set the following options:: + cost_has_noise = True + +If the cost you provide has noise in it, meaning your the cost you calculate would fluctuate if you did multiple experiments with the same parameters, then set this flag to True. If the costs your provide have no noise then set this flag to False. M-LOOP will automatically determine if the costs have noise in them or not, so if you are unsure, just delete this keyword and it will use the default value of True. - first_parameters = [0.5,0.5] - trust_region = 0.4 +Timing options +~~~~~~~~~~~~~~ -first_parameters defines the first parameters the learner will try. trust_region defines the maximum change allowed in the parameters from the best parameters found so far. In the current example the region size is 2 by 2, with a trust region of 40% thus the maximum allowed change for the second run will be [0 +/- 0.8, 0 +/- 0.8]. +M-LOOP learns how the experiment works by fitting the parameters and costs using a gaussian process. This learning process can take some time. If M-LOOP is asked for new parameters before it has time to generate a new prediction, it will use the training algorithm to provide a new set of parameters to test. This allows for an experiment to be run while the learner is still thinking. The training algorithm by default is differential evolution, this algorithm is also used to do the first initial set of experiments which are then used to train M-LOOP. If you would prefer M-LOOP waits for the learner to come up with its best prediction before running another experiment you can change this behavior with the option:: -If you experiment reliably produces costs for any parameter set you will not need these settings and you can just delete them. + no_delay = True + +Set no_delay to true to ensure there is no pauses between experiments and set it to false if you to give M-LOOP to have the time to come up with its most informed choice. Sometimes doing fewer more intelligent experiments will lead to an optimal quicker than many quick unintelligent experiments. You can delete the keyword if you are unsure and it will default to True. File format options ~~~~~~~~~~~~~~~~~~~ diff --git a/examples/tutorial_config.txt b/examples/tutorial_config.txt index cc8216a..cd07d29 100644 --- a/examples/tutorial_config.txt +++ b/examples/tutorial_config.txt @@ -8,15 +8,19 @@ interface_type = 'file' num_params = 2 #number of parameters min_boundary = [-1,-1] #minimum boundary max_boundary = [1,1] #maximum boundary +first_params = [0.5,0.5] #first parameters to try +trust_region = 0.4 #maximum % move distance from best params #Halting conditions max_num_runs = 1000 #maximum number of runs max_num_runs_without_better_params = 50 #maximum number of runs without finding better parameters target_cost = 0.01 #optimization halts when a cost below this target is found -#Learner specific options -first_params = [0.5,0.5] #first parameters to try -trust_region = 0.4 #maximum % move distance from best params +#Learner options +cost_has_noise = True #whether the cost are corrupted by noise or not + +#Timing options +no_delay = True #wait for learner to make generate new parameters or use training algorithms #File format options interface_file_type = 'txt' #file types of *exp_input.mat* and *exp_output.mat* diff --git a/mloop/__init__.py b/mloop/__init__.py index 06df418..9e53155 100644 --- a/mloop/__init__.py +++ b/mloop/__init__.py @@ -12,5 +12,5 @@ import os -__version__= "2.1.0" +__version__= "2.1.1" __all__ = ['controllers','interfaces','launchers','learners','testing','utilities','visualizations','cmd'] \ No newline at end of file diff --git a/setup.py b/setup.py index 01f5b48..c6b6017 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ def main(): license = 'MIT', keywords = 'automated machine learning optimization optimisation science experiment quantum', url = 'https://github.com/michaelhush/M-LOOP/', - download_url = 'https://github.com/michaelhush/M-LOOP/tarball/v2.1.0', + download_url = 'https://github.com/michaelhush/M-LOOP/tarball/v2.1.1', classifiers = ['Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Science/Research', From baa5074f237b87ca55257f72601c93ddbfe7465d Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Thu, 24 Nov 2016 14:46:47 +1100 Subject: [PATCH 06/33] Update to test and utilities Added some updates to docstrings and test unit parameters. --- mloop/controllers.py | 2 +- mloop/utilities.py | 2 ++ tests/test_units.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index 691e51b..d018367 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -675,7 +675,7 @@ def _optimization_routine(self): ''' Overrides _optimization_routine. Uses the parent routine for the training runs. Implements a customized _optimization_rountine when running the Gaussian Process learner. ''' - #Run the training runs using the standard optimization routine. Adjust the number of max_runs + #Run the training runs using the standard optimization routine. self.log.debug('Starting training optimization.') self.log.info('Run:' + str(self.num_in_costs +1)) next_params = self._first_params() diff --git a/mloop/utilities.py b/mloop/utilities.py index 1d5a1cf..cd35aee 100644 --- a/mloop/utilities.py +++ b/mloop/utilities.py @@ -57,6 +57,8 @@ def _config_logger(log_filename = default_log_filename, file_log_level (Optional[int]) : Level of log output for file, default is logging.DEBUG = 10 console_log_level (Optional[int]) :Level of log output for console, defalut is logging.INFO = 20 + Returns: + dictionary: Dict with extra keywords not used by the logging configuration. ''' if not os.path.exists(log_foldername): os.makedirs(log_foldername) diff --git a/tests/test_units.py b/tests/test_units.py index a45e831..905143a 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -32,7 +32,7 @@ def test_max_num_runs(self): controller = mlc.create_controller(interface, max_num_runs = 5, target_cost = -1, - max_num_runs_without_better_params = 2) + max_num_runs_without_better_params = 10) controller.optimize() self.assertTrue(controller.best_cost == 1.) self.assertTrue(np.array_equiv(np.array(controller.in_costs), From ecffda81ea86abdb28fb03d67e851ea164a4f35a Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Thu, 24 Nov 2016 22:51:03 +1100 Subject: [PATCH 07/33] Added a shell for the nerual net Added a controller and learner for the neural net. Also added a new class MachineLearnerController which GaussianProcess and NeuralNet both inherit from. I broke the visualizations for GPs in this update. But all the tests work. --- mloop/controllers.py | 160 ++++++++++++---- mloop/learners.py | 494 +++++++++++++++++++++++++++++++++++++++++++++++- mloop/visualizations.py | 10 +- 3 files changed, 616 insertions(+), 48 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index d018367..ca2035a 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -47,6 +47,8 @@ def create_controller(interface, controller_type = str(controller_type) if controller_type=='gaussian_process': controller = GaussianProcessController(interface, **controller_config_dict) + elif controller_type=='neural_net': + controller = NeuralNetController(interface, **controller_config_dict) elif controller_type=='differential_evolution': controller = DifferentialEvolutionController(interface, **controller_config_dict) elif controller_type=='nelder_mead': @@ -521,11 +523,9 @@ def _next_params(self): return self.learner_params_queue.get() - - -class GaussianProcessController(Controller): +class MachineLearnerController(Controller): ''' - Controller for the Gaussian Process solver. Primarily suggests new points from the Gaussian Process learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. + Abstract Controller class for the machine learning based solvers. Args: interface (Interface): The interface to the experiment under optimization. @@ -548,7 +548,8 @@ def __init__(self, interface, learner_archive_filename = mll.default_learner_archive_filename, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): - super(GaussianProcessController,self).__init__(interface, **kwargs) + + super(MachineLearnerController,self).__init__(interface, **kwargs) self.last_training_cost = None self.last_training_bad = None @@ -603,27 +604,22 @@ def __init__(self, interface, self.archive_dict.update({'training_type':self.training_type}) self._update_controller_with_learner_attributes() - self.gp_learner = mll.GaussianProcessLearner(start_datetime=self.start_datetime, - num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - trust_region=trust_region, - learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **self.remaining_kwargs) - - self.gp_learner_params_queue = self.gp_learner.params_out_queue - self.gp_learner_costs_queue = self.gp_learner.costs_in_queue - self.end_gp_learner = self.gp_learner.end_event - self.new_params_event = self.gp_learner.new_params_event - self.remaining_kwargs = self.gp_learner.remaining_kwargs - self.generation_num = self.gp_learner.generation_num + + def _update_controller_with_machine_learner_attributes(self): + + self.ml_learner_params_queue = self.ml_learner.params_out_queue + self.ml_learner_costs_queue = self.ml_learner.costs_in_queue + self.end_ml_learner = self.ml_learner.end_event + self.new_params_event = self.ml_learner.new_params_event + self.remaining_kwargs = self.ml_learner.remaining_kwargs + self.generation_num = self.ml_learner.generation_num + def _put_params_and_out_dict(self, params): ''' Override _put_params_and_out_dict function, used when the training learner creates parameters. Makes the defualt param_type the training type and sets last_training_run_flag. ''' - super(GaussianProcessController,self)._put_params_and_out_dict(params, param_type=self.training_type) + super(MachineLearnerController,self)._put_params_and_out_dict(params, param_type=self.training_type) self.last_training_run_flag = True def _get_cost_and_in_dict(self): @@ -631,12 +627,12 @@ def _get_cost_and_in_dict(self): Call _get_cost_and_in_dict() of parent Controller class. But also sends cost to Gaussian process learner and saves the cost if the parameters came from a trainer. ''' - super(GaussianProcessController,self)._get_cost_and_in_dict() + super(MachineLearnerController,self)._get_cost_and_in_dict() if self.last_training_run_flag: self.last_training_cost = self.curr_cost self.last_training_bad = self.curr_bad self.last_training_run_flag = False - self.gp_learner_costs_queue.put((self.curr_params, + self.ml_learner_costs_queue.put((self.curr_params, self.curr_cost, self.curr_uncer, self.curr_bad)) @@ -667,9 +663,9 @@ def _start_up(self): ''' Runs pararent method and also starts training_learner. ''' - super(GaussianProcessController,self)._start_up() + super(MachineLearnerController,self)._start_up() self.log.debug('GP learner started.') - self.gp_learner.start() + self.ml_learner.start() def _optimization_routine(self): ''' @@ -701,22 +697,22 @@ def _optimization_routine(self): self._get_cost_and_in_dict() self.log.debug('End training runs.') - gp_consec = 0 - gp_count = 0 + ml_consec = 0 + ml_count = 0 while self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) - if gp_consec==self.generation_num or (self.no_delay and self.gp_learner_params_queue.empty()): + if ml_consec==self.generation_num or (self.no_delay and self.ml_learner_params_queue.empty()): next_params = self._next_params() self._put_params_and_out_dict(next_params) - gp_consec = 0 + ml_consec = 0 else: - next_params = self.gp_learner_params_queue.get() - super(GaussianProcessController,self)._put_params_and_out_dict(next_params, param_type='gaussian_process') - gp_consec += 1 - gp_count += 1 + next_params = self.ml_learner_params_queue.get() + super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type='gaussian_process') + ml_consec += 1 + ml_count += 1 - if gp_count%self.generation_num == 2: + if ml_count%self.generation_num == 2: self.new_params_event.set() self.save_archive() @@ -728,13 +724,13 @@ def _shut_down(self): Shutdown and clean up resources of the Gaussian process controller. ''' self.log.debug('GP learner end set.') - self.end_gp_learner.set() - self.gp_learner.join() + self.end_ml_learner.set() + self.ml_learner.join() self.log.debug('GP learner joined') last_dict = None - while not self.gp_learner_params_queue.empty(): - last_dict = self.gp_learner_params_queue.get_nowait() + while not self.ml_learner_params_queue.empty(): + last_dict = self.ml_learner_params_queue.get_nowait() if isinstance(last_dict, dict): try: self.predicted_best_parameters = last_dict['predicted_best_parameters'] @@ -751,15 +747,15 @@ def _shut_down(self): pass self.archive_dict.update(last_dict) else: - if self.gp_learner.predict_global_minima_at_end or self.gp_learner.predict_local_minima_at_end: - self.log.info('GP Learner did not provide best and/or all minima.') - super(GaussianProcessController,self)._shut_down() + if self.ml_learner.predict_global_minima_at_end or self.ml_learner.predict_local_minima_at_end: + self.log.info('Machine Learner did not provide best and/or all minima.') + super(MachineLearnerController,self)._shut_down() def print_results(self): ''' Adds some additional output to the results specific to controller. ''' - super(GaussianProcessController,self).print_results() + super(MachineLearnerController,self).print_results() try: self.log.info('Predicted best parameters:' + str(self.predicted_best_parameters)) self.log.info('Predicted best cost:' + str(self.predicted_best_cost) + ' +/- ' + str(self.predicted_best_uncertainty)) @@ -771,7 +767,87 @@ def print_results(self): except AttributeError: pass +class GaussianProcessController(MachineLearnerController): + ''' + Controller for the Gaussian Process solver. Primarily suggests new points from the Gaussian Process learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. + + Args: + interface (Interface): The interface to the experiment under optimization. + **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. + + Keyword Args: + + ''' + + def __init__(self, interface, + num_params=None, + min_boundary=None, + max_boundary=None, + trust_region=None, + learner_archive_filename = mll.default_learner_archive_filename, + learner_archive_file_type = mll.default_learner_archive_file_type, + **kwargs): + + super(GaussianProcessController,self).__init__(interface, + num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + trust_region=trust_region, + learner_archive_filename=learner_archive_filename, + learner_archive_file_type=learner_archive_file_type, + **kwargs) + + self.ml_learner = mll.GaussianProcessLearner(start_datetime=self.start_datetime, + num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + trust_region=trust_region, + learner_archive_filename=learner_archive_filename, + learner_archive_file_type=learner_archive_file_type, + **self.remaining_kwargs) + + self._update_controller_with_machine_learner_attributes() +class NeuralNetController(MachineLearnerController): + ''' + Controller for the Neural Net solver. Primarily suggests new points from the Neural Net learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. + + Args: + interface (Interface): The interface to the experiment under optimization. + **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. + Keyword Args: + + ''' + + def __init__(self, interface, + num_params=None, + min_boundary=None, + max_boundary=None, + trust_region=None, + learner_archive_filename = mll.default_learner_archive_filename, + learner_archive_file_type = mll.default_learner_archive_file_type, + **kwargs): + + super(GaussianProcessController,self).__init__(interface, + num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + trust_region=trust_region, + learner_archive_filename=learner_archive_filename, + learner_archive_file_type=learner_archive_file_type, + **kwargs) + + self.ml_learner = mll.NeuralNetLearner(start_datetime=self.start_datetime, + num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + trust_region=trust_region, + learner_archive_filename=learner_archive_filename, + learner_archive_file_type=learner_archive_file_type, + **self.remaining_kwargs) + + self._update_controller_with_machine_learner_attributes() + \ No newline at end of file diff --git a/mloop/learners.py b/mloop/learners.py index 9f7cea7..ed15093 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -848,7 +848,6 @@ def update_archive(self): 'generation_count':self.generation_count}) - class GaussianProcessLearner(Learner, mp.Process): ''' Gaussian process learner. Generats new parameters based on a gaussian process fitted to all previous data. @@ -1468,7 +1467,500 @@ def find_local_minima(self): self.has_local_minima = True self.log.info('Search completed') + +class NeuralNetLearner(Learner, mp.Process): + ''' + Shell of Neural Net Learner. + + Args: + params_out_queue (queue): Queue for parameters sent to controller. + costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple + end_event (event): Event to trigger end of learner. + + Keyword Args: + length_scale (Optional [array]): The initial guess for length scale(s) of the gaussian process. The array can either of size one or the number of parameters or None. If it is size one, it is assumed all the correlation lengths are the same. If it is the number of the parameters then all the parameters have their own independent length scale. If it is None, it is assumed all the length scales should be independent and they are all given an initial value of 1. Default None. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + default_bad_cost (Optional [float]): If a run is reported as bad and default_bad_cost is provided, the cost for the bad run is set to this default value. If default_bad_cost is None, then the worst cost received is set to all the bad runs. Default None. + default_bad_uncertainty (Optional [float]): If a run is reported as bad and default_bad_uncertainty is provided, the uncertainty for the bad run is set to this default value. If default_bad_uncertainty is None, then the uncertainty is set to a tenth of the best to worst cost range. Default None. + minimum_uncertainty (Optional [float]): The minimum uncertainty associated with provided costs. Must be above zero to avoid fitting errors. Default 1e-8. + predict_global_minima_at_end (Optional [bool]): If True finds the global minima when the learner is ended. Does not if False. Default True. + predict_local_minima_at_end (Optional [bool]): If True finds the all minima when the learner is ended. Does not if False. Default False. + + Attributes: + all_params (array): Array containing all parameters sent to learner. + all_costs (array): Array containing all costs sent to learner. + all_uncers (array): Array containing all uncertainties sent to learner. + scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. + bad_run_indexs (list): list of indexes to all runs that were marked as bad. + best_cost (float): Minimum received cost, updated during execution. + best_params (array): Parameters of best run. (reference to element in params array). + best_index (int): index of the best cost and params. + worst_cost (float): Maximum received cost, updated during execution. + worst_index (int): index to run with worst cost. + cost_range (float): Difference between worst_cost and best_cost + generation_num (int): Number of sets of parameters to generate each generation. Set to 5. + length_scale_history (list): List of length scales found after each fit. + noise_level_history (list): List of noise levels found after each fit. + fit_count (int): Counter for the number of times the gaussian process has been fit. + cost_count (int): Counter for the number of costs, parameters and uncertainties added to learner. + params_count (int): Counter for the number of parameters asked to be evaluated by the learner. + gaussian_process (GaussianProcessRegressor): Gaussian process that is fitted to data and used to make predictions + cost_scaler (StandardScaler): Scaler used to normalize the provided costs. + has_trust_region (bool): Whether the learner has a trust region. + ''' + + def __init__(self, + update_hyperparameters = True, + trust_region=None, + default_bad_cost = None, + default_bad_uncertainty = None, + nn_training_filename =None, + nn_training_file_type ='txt', + predict_global_minima_at_end = True, + predict_local_minima_at_end = False, + **kwargs): + + + + super(NeuralNetLearner,self).__init__(**kwargs) + + #Storage variables, archived + self.all_params = np.array([], dtype=float) + self.all_costs = np.array([], dtype=float) + self.all_uncers = np.array([], dtype=float) + self.bad_run_indexs = [] + self.best_cost = float('inf') + self.best_params = float('nan') + self.best_index = 0 + self.worst_cost = float('-inf') + self.worst_index = 0 + self.cost_range = float('inf') + self.length_scale_history = [] + self.noise_level_history = [] + + self.costs_count = 0 + self.fit_count = 0 + self.params_count = 0 + + self.has_local_minima = False + self.has_global_minima = False + + #Multiprocessor controls + self.new_params_event = mp.Event() + + #Storage variables and counters + self.search_params = [] + self.scaled_costs = None + self.cost_bias = None + self.uncer_bias = None + + #Constants, limits and tolerances + self.search_precision = 1.0e-6 + self.parameter_searches = max(10,self.num_params) + self.hyperparameter_searches = max(10,self.num_params) + self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty + + #Optional user set variables + self.update_hyperparameters = bool(update_hyperparameters) + self.predict_global_minima_at_end = bool(predict_global_minima_at_end) + self.predict_local_minima_at_end = bool(predict_local_minima_at_end) + if default_bad_cost is not None: + self.default_bad_cost = float(default_bad_cost) + else: + self.default_bad_cost = None + if default_bad_uncertainty is not None: + self.default_bad_uncertainty = float(default_bad_uncertainty) + else: + self.default_bad_uncertainty = None + + self._set_trust_region(trust_region) + + #Search bounds + self.search_min = self.min_boundary + self.search_max = self.max_boundary + self.search_diff = self.search_max - self.search_min + self.search_region = list(zip(self.search_min, self.search_max)) + + self.cost_scaler = skp.StandardScaler() + + + #--- FAKE NN CONSTRUCTOR START ---# + + self.length_scale = 1 + self.cost_has_noise = True + self.noise_level = 1 + + self.create_nerual_net() + + + + #--- FAKE NN CONSTRUCTOR END ---# + + self.archive_dict.update({'archive_type':'nerual_net_learner', + 'bad_run_indexs':self.bad_run_indexs, + 'generation_num':self.generation_num, + 'search_precision':self.search_precision, + 'parameter_searches':self.parameter_searches, + 'hyperparameter_searches':self.hyperparameter_searches, + 'bad_uncer_frac':self.bad_uncer_frac, + 'trust_region':self.trust_region, + 'has_trust_region':self.has_trust_region, + 'predict_global_minima_at_end':self.predict_global_minima_at_end, + 'predict_local_minima_at_end':self.predict_local_minima_at_end}) + + #Remove logger so gaussian process can be safely picked for multiprocessing on Windows + self.log = None + + + #--- FAKE NN METHODS START ---# + + + def create_neural_net(self): + ''' + TO DO: Implement correctly + + Create the nerual net. + + ''' + gp_kernel = skk.RBF(length_scale=self.length_scale) + skk.WhiteKernel(noise_level=self.noise_level) + + if self.update_hyperparameters: + self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,n_restarts_optimizer=self.hyperparameter_searches) + else: + self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,optimizer=None) + + def fit_neural_net(self): + ''' + TO DO: Implement correctly + + Determine the appropriate number of layers for the NN given the data. + + Fit the Neural Net with the appropriate topology to the data + + ''' + self.log.debug('Fitting Gaussian process.') + if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: + self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') + raise ValueError + + self.scaled_costs = self.cost_scaler.fit_transform(self.all_costs[:,np.newaxis])[:,0] + self.scaled_uncers = self.all_uncers * self.cost_scaler.scale_ + self.gaussian_process.alpha_ = self.scaled_uncers + self.gaussian_process.fit(self.all_params,self.scaled_costs) + + if self.update_hyperparameters: + + self.fit_count += 1 + self.gaussian_process.kernel = self.gaussian_process.kernel_ + + last_hyperparameters = self.gaussian_process.kernel.get_params() + + if self.cost_has_noise: + self.length_scale = last_hyperparameters['k1__length_scale'] + if isinstance(self.length_scale, float): + self.length_scale = np.array([self.length_scale]) + self.length_scale_history.append(self.length_scale) + self.noise_level = last_hyperparameters['k2__noise_level'] + self.noise_level_history.append(self.noise_level) + else: + self.length_scale = last_hyperparameters['length_scale'] + self.length_scale_history.append(self.length_scale) + + def predict_cost(self,params): + ''' + Produces a prediction of cost from the gaussian process at params. + + Returns: + float : Predicted cost at paramters + ''' + return self.gaussian_process.predict(params[np.newaxis,:]) + + #--- FAKE NN CONSTRUCTOR END ---# + + + def wait_for_new_params_event(self): + ''' + Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. + ''' + while not self.end_event.is_set(): + if self.new_params_event.wait(timeout=self.learner_wait): + self.new_params_event.clear() + break + else: + continue + else: + self.log.debug('GaussianProcessLearner end signal received. Ending') + raise LearnerInterrupt + + + def get_params_and_costs(self): + ''' + Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. + ''' + if self.costs_in_queue.empty(): + self.log.error('Gaussian process asked for new parameters but no new costs were provided.') + raise ValueError + + new_params = [] + new_costs = [] + new_uncers = [] + new_bads = [] + update_bads_flag = False + + while not self.costs_in_queue.empty(): + (param, cost, uncer, bad) = self.costs_in_queue.get_nowait() + self.costs_count +=1 + + if bad: + new_bads.append(self.costs_count-1) + if self.bad_defaults_set: + cost = self.default_bad_cost + uncer = self.default_bad_uncertainty + else: + cost = self.worst_cost + uncer = self.cost_range*self.bad_uncer_frac + + param = np.array(param, dtype=float) + if not self.check_num_params(param): + self.log.error('Incorrect number of parameters provided to Gaussian process learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) + raise ValueError + if not self.check_in_boundary(param): + self.log.warning('Parameters provided to Gaussian process learner not in boundaries:' + repr(param)) + cost = float(cost) + if uncer < 0: + self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) + uncer = max(float(uncer), self.minimum_uncertainty) + + cost_change_flag = False + if cost > self.worst_cost: + self.worst_cost = cost + self.worst_index = self.costs_count-1 + cost_change_flag = True + if cost < self.best_cost: + self.best_cost = cost + self.best_params = param + self.best_index = self.costs_count-1 + cost_change_flag = True + if cost_change_flag: + self.cost_range = self.worst_cost - self.best_cost + if not self.bad_defaults_set: + update_bads_flag = True + + new_params.append(param) + new_costs.append(cost) + new_uncers.append(uncer) + + + if self.all_params.size==0: + self.all_params = np.array(new_params, dtype=float) + self.all_costs = np.array(new_costs, dtype=float) + self.all_uncers = np.array(new_uncers, dtype=float) + else: + self.all_params = np.concatenate((self.all_params, np.array(new_params, dtype=float))) + self.all_costs = np.concatenate((self.all_costs, np.array(new_costs, dtype=float))) + self.all_uncers = np.concatenate((self.all_uncers, np.array(new_uncers, dtype=float))) + + self.bad_run_indexs.append(new_bads) + + if self.all_params.shape != (self.costs_count,self.num_params): + self.log('Saved NN params are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_params)) + if self.all_costs.shape != (self.costs_count,): + self.log('Saved NN costs are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_costs)) + if self.all_uncers.shape != (self.costs_count,): + self.log('Saved NN uncertainties are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_uncers)) + + if update_bads_flag: + self.update_bads() + + self.update_search_region() + + def update_bads(self): + ''' + Best and/or worst costs have changed, update the values associated with bad runs accordingly. + ''' + for index in self.bad_run_indexs: + self.all_costs[index] = self.worst_cost + self.all_uncers[index] = self.cost_range*self.bad_uncer_frac + + def update_search_region(self): + ''' + If trust boundaries is not none, updates the search boundaries based on the defined trust region. + ''' + if self.has_trust_region: + self.search_min = np.maximum(self.best_params - self.trust_region, self.min_boundary) + self.search_max = np.minimum(self.best_params + self.trust_region, self.max_boundary) + self.search_diff = self.search_max - self.search_min + self.search_region = list(zip(self.search_min, self.search_max)) + + def update_search_params(self): + ''' + Update the list of parameters to use for the next search. + ''' + self.search_params = [] + self.search_params.append(self.best_params) + for _ in range(self.parameter_searches): + self.search_params.append(self.search_min + nr.uniform(size=self.num_params) * self.search_diff) + + def update_archive(self): + ''' + Update the archive. + ''' + self.archive_dict.update({'all_params':self.all_params, + 'all_costs':self.all_costs, + 'all_uncers':self.all_uncers, + 'best_cost':self.best_cost, + 'best_params':self.best_params, + 'best_index':self.best_index, + 'worst_cost':self.worst_cost, + 'worst_index':self.worst_index, + 'cost_range':self.cost_range, + 'fit_count':self.fit_count, + 'costs_count':self.costs_count, + 'params_count':self.params_count, + 'update_hyperparameters':self.update_hyperparameters}) + + def find_next_parameters(self): + ''' + Returns next parameters to find. Increments counters and bias function appropriately. + + Return: + next_params (array): Returns next parameters from biased cost search. + ''' + self.params_count += 1 + self.update_bias_function() + self.update_search_params() + next_params = None + next_cost = float('inf') + for start_params in self.search_params: + result = so.minimize(self.predict_biased_cost, start_params, bounds = self.search_region, tol=self.search_precision) + if result.fun < next_cost: + next_params = result.x + next_cost = result.fun + return next_params + def run(self): + ''' + Starts running the Gaussian process learner. When the new parameters event is triggered, reads the cost information provided and updates the Gaussian process with the information. Then searches the Gaussian process for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. + ''' + #logging to the main log file from a process (as apposed to a thread) in cpython is currently buggy on windows and/or python 2.7 + #current solution is to only log to the console for warning and above from a process + self.log = mp.log_to_stderr(logging.WARNING) + + try: + while not self.end_event.is_set(): + #self.log.debug('Learner waiting for new params event') + self.save_archive() + self.wait_for_new_params_event() + #self.log.debug('Gaussian process learner reading costs') + self.get_params_and_costs() + self.fit_gaussian_process() + for _ in range(self.generation_num): + self.log.debug('Gaussian process learner generating parameter:'+ str(self.params_count+1)) + next_params = self.find_next_parameters() + self.params_out_queue.put(next_params) + if self.end_event.is_set(): + raise LearnerInterrupt() + except LearnerInterrupt: + pass + if self.predict_global_minima_at_end or self.predict_local_minima_at_end: + self.get_params_and_costs() + self.fit_gaussian_process() + end_dict = {} + if self.predict_global_minima_at_end: + self.find_global_minima() + end_dict.update({'predicted_best_parameters':self.predicted_best_parameters, + 'predicted_best_cost':self.predicted_best_cost, + 'predicted_best_uncertainty':self.predicted_best_uncertainty}) + if self.predict_local_minima_at_end: + self.find_local_minima() + end_dict.update({'local_minima_parameters':self.local_minima_parameters, + 'local_minima_costs':self.local_minima_costs, + 'local_minima_uncers':self.local_minima_uncers}) + self.params_out_queue.put(end_dict) + self._shut_down() + self.log.debug('Ended Gaussian Process Learner') + + def find_global_minima(self): + ''' + Performs a quick search for the predicted global minima from the learner. Does not return any values, but creates the following attributes. + + Attributes: + predicted_best_parameters (array): the parameters for the predicted global minima + predicted_best_cost (float): the cost at the predicted global minima + predicted_best_uncertainty (float): the uncertainty of the predicted global minima + ''' + self.log.debug('Started search for predicted global minima.') + + self.predicted_best_parameters = None + self.predicted_best_scaled_cost = float('inf') + self.predicted_best_scaled_uncertainty = None + + search_params = [] + search_params.append(self.best_params) + for _ in range(self.parameter_searches): + search_params.append(self.min_boundary + nr.uniform(size=self.num_params) * self.diff_boundary) + + search_bounds = list(zip(self.min_boundary, self.max_boundary)) + for start_params in search_params: + result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) + curr_best_params = result.x + (curr_best_cost,curr_best_uncer) = self.gaussian_process.predict(curr_best_params[np.newaxis,:],return_std=True) + if curr_best_cost Date: Fri, 25 Nov 2016 17:09:50 +1100 Subject: [PATCH 08/33] Fix some whitespace errors Git complains to me about them when I touch nearby lines, so I figured it was easier just to fix them. --- mloop/controllers.py | 290 ++++++++++---------- mloop/learners.py | 746 +++++++++++++++++++++++++-------------------------- 2 files changed, 518 insertions(+), 518 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index ca2035a..3777c4d 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -18,32 +18,32 @@ class ControllerInterrupt(Exception): ''' - Exception that is raised when the controlled is ended with the end flag or event. + Exception that is raised when the controlled is ended with the end flag or event. ''' def __init__(self): super(ControllerInterrupt,self).__init__() - + def create_controller(interface, - controller_type='gaussian_process', + controller_type='gaussian_process', **controller_config_dict): ''' Start the controller with the options provided. - + Args: interface (interface): Interface with queues and events to be passed to controller - + Keyword Args: controller_type (Optional [str]): Defines the type of controller can be 'random', 'nelder' or 'gaussian_process'. Defaults to 'gaussian_process'. **controller_config_dict : Options to be passed to controller. - + Returns: Controller : threadible object which must be started with start() to get the controller running. - + Raises: ValueError : if controller_type is an unrecognized string ''' log = logging.getLogger(__name__) - + controller_type = str(controller_type) if controller_type=='gaussian_process': controller = GaussianProcessController(interface, **controller_config_dict) @@ -58,33 +58,33 @@ def create_controller(interface, else: log.error('Unknown controller type:' + repr(controller_type)) raise ValueError - + return controller class Controller(): ''' Abstract class for controllers. The controller controls the entire M-LOOP process. The controller for each algorithm all inherit from this class. The class stores a variety of data which all algorithms use and also all of the achiving and saving features. - + In order to implement your own controller class the minimum requirement is to add a learner to the learner variable. And implement the next_parameters method, where you provide the appropriate information to the learner and get the next parameters. - + See the RandomController for a simple implementation of a controller. - + Note the first three keywords are all possible halting conditions for the controller. If any of them are satisfied the controller will halt (meaning an and condition is used). - + Also creates an empty variable learner. The simplest way to make a working controller is to assign a learner of some kind to this variable, and add appropriate queues and events from it. - + Args: interface (interface): The interface process. Is run by learner. - + Keyword Args: max_num_runs (Optional [float]): The number of runs before the controller stops. If set to float('+inf') the controller will run forever. Default float('inf'), meaning the controller will run until another condition is met. target_cost (Optional [float]): The target cost for the run. If a run achieves a cost lower than the target, the controller is stopped. Default float('-inf'), meaning the controller will run until another condition is met. - max_num_runs_without_better_params (Otional [float]): Puts a limit on the number of runs are allowed before a new better set of parameters is found. Default float('inf'), meaning the controller will run until another condition is met. + max_num_runs_without_better_params (Otional [float]): Puts a limit on the number of runs are allowed before a new better set of parameters is found. Default float('inf'), meaning the controller will run until another condition is met. controller_archive_filename (Optional [string]): Filename for archive. Contains costs, parameter history and other details depending on the controller type. Default 'ControllerArchive.mat' controller_archive_file_type (Optional [string]): File type for archive. Can be either 'txt' a human readable text file, 'pkl' a python dill file, 'mat' a matlab file or None if there is no archive. Default 'mat'. archive_extra_dict (Optional [dict]): A dictionary with any extra variables that are to be saved to the archive. If None, nothing is added. Default None. start_datetime (Optional datetime): Datetime for when controller was started. - + Attributes: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. @@ -94,7 +94,7 @@ class Controller(): learner_costs_queue (queue): The costs queue for the learner end_learner (event): Event used to trigger the end of the learner num_in_costs (int): Counter for the number of costs received. - num_out_params (int): Counter for the number of parameters received. + num_out_params (int): Counter for the number of parameters received. out_params (list): List of all parameters sent out by controller. out_extras (list): Any extras associated with the output parameters. in_costs (list): List of costs received by controller. @@ -102,10 +102,10 @@ class Controller(): best_cost (float): The lowest, and best, cost received by the learner. best_uncer (float): The uncertainty associated with the best cost. best_params (array): The best parameters recieved by the learner. - best_index (float): The run number that produced the best cost. - + best_index (float): The run number that produced the best cost. + ''' - + def __init__(self, interface, max_num_runs = float('+inf'), target_cost = float('-inf'), @@ -115,11 +115,11 @@ def __init__(self, interface, archive_extra_dict = None, start_datetime = None, **kwargs): - + #Make logger self.remaining_kwargs = mlu._config_logger(**kwargs) self.log = logging.getLogger(__name__) - + #Variable that are included in archive self.num_in_costs = 0 self.num_out_params = 0 @@ -135,7 +135,7 @@ def __init__(self, interface, self.best_uncer = float('nan') self.best_index = float('nan') self.best_params = float('nan') - + #Variables that used internally self.last_out_params = None self.curr_params = None @@ -143,29 +143,29 @@ def __init__(self, interface, self.curr_uncer = None self.curr_bad = None self.curr_extras = None - + #Constants self.controller_wait = float(1) - + #Learner related variables self.learner_params_queue = None self.learner_costs_queue = None self.end_learner = None self.learner = None - + #Variables set by user - + #save interface and extract important variables if isinstance(interface, mli.Interface): self.interface = interface else: self.log.error('interface is not a Interface as defined in the MLOOP package.') raise TypeError - + self.params_out_queue = interface.params_out_queue self.costs_in_queue = interface.costs_in_queue self.end_interface = interface.end_event - + #Other options if start_datetime is None: self.start_datetime = datetime.datetime.now() @@ -180,7 +180,7 @@ def __init__(self, interface, if self.max_num_runs_without_better_params<=0: self.log.error('Max number of repeats must be greater than zero. max_num_runs:'+repr(max_num_runs_without_better_params)) raise ValueError - + if mlu.check_file_type_supported(controller_archive_file_type): self.controller_archive_file_type = controller_archive_file_type else: @@ -193,7 +193,7 @@ def __init__(self, interface, os.makedirs(mlu.archive_foldername) self.controller_archive_filename =str(controller_archive_filename) self.total_archive_filename = mlu.archive_foldername + self.controller_archive_filename + '_' + mlu.datetime_to_string(self.start_datetime) + '.' + self.controller_archive_file_type - + self.archive_dict = {'archive_type':'controller', 'num_out_params':self.num_out_params, 'out_params':self.out_params, @@ -205,21 +205,21 @@ def __init__(self, interface, 'in_extras':self.in_extras, 'max_num_runs':self.max_num_runs, 'start_datetime':mlu.datetime_to_string(self.start_datetime)} - + if archive_extra_dict is not None: self.archive_dict.update(archive_extra_dict) - + self.log.debug('Controller init completed.') - + def check_end_conditions(self): ''' Check whether either of the three end contions have been met: number_of_runs, target_cost or max_num_runs_without_better_params. - + Returns: - bool : True, if the controlled should continue, False if the controller should end. + bool : True, if the controlled should continue, False if the controller should end. ''' return (self.num_in_costs < self.max_num_runs) and (self.best_cost > self.target_cost) and (self.num_last_best_cost < self.max_num_runs_without_better_params) - + def _update_controller_with_learner_attributes(self): ''' Update the controller with properties from the learner. @@ -228,19 +228,19 @@ def _update_controller_with_learner_attributes(self): self.learner_costs_queue = self.learner.costs_in_queue self.end_learner = self.learner.end_event self.remaining_kwargs = self.learner.remaining_kwargs - + self.archive_dict.update({'num_params':self.learner.num_params, 'min_boundary':self.learner.min_boundary, 'max_boundary':self.learner.max_boundary}) - - + + def _put_params_and_out_dict(self, params, param_type=None, **kwargs): ''' - Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. - + Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. + Args: params (array) : array of values to be sent to file - + Keyword Args: **kwargs: any additional data to be attached to file sent to experiment ''' @@ -255,11 +255,11 @@ def _put_params_and_out_dict(self, params, param_type=None, **kwargs): self.out_type.append(param_type) self.log.info('params ' + str(params)) #self.log.debug('Put params num:' + repr(self.num_out_params )) - + def _get_cost_and_in_dict(self): ''' Get cost, uncertainty, parameters, bad and extra data from experiment. Stores in a list of history and also puts variables in their appropriate 'current' variables - + Note returns nothing, stores everything in the internal storage arrays and the curr_variables ''' while True: @@ -269,16 +269,16 @@ def _get_cost_and_in_dict(self): continue else: break - + self.num_in_costs += 1 self.num_last_best_cost += 1 - + if not ('cost' in in_dict) and (not ('bad' in in_dict) or not in_dict['bad']): self.log.error('You must provide at least the key cost or the key bad with True.') raise ValueError try: - self.curr_cost = float(in_dict.pop('cost',float('nan'))) - self.curr_uncer = float(in_dict.pop('uncer',0)) + self.curr_cost = float(in_dict.pop('cost',float('nan'))) + self.curr_uncer = float(in_dict.pop('uncer',0)) self.curr_bad = bool(in_dict.pop('bad',False)) self.curr_extras = in_dict except ValueError: @@ -286,7 +286,7 @@ def _get_cost_and_in_dict(self): raise if self.curr_bad and ('cost' in in_dict): self.log.warning('The cost provided with the bad run will be saved, but not used by the learners.') - + self.in_costs.append(self.curr_cost) self.in_uncers.append(self.curr_uncer) self.in_bads.append(self.curr_bad) @@ -303,7 +303,7 @@ def _get_cost_and_in_dict(self): else: self.log.info('cost ' + str(self.curr_cost) + ' +/- ' + str(self.curr_uncer)) #self.log.debug('Got cost num:' + repr(self.num_in_costs)) - + def save_archive(self): ''' Save the archive associated with the controller class. Only occurs if the filename for the archive is not None. Saves with the format previously set. @@ -322,15 +322,15 @@ def save_archive(self): raise else: self.log.debug('Did not save controller archive file.') - + def optimize(self): ''' Optimize the experiment. This code learner and interface processes/threads are launched and appropriately ended. - + Starts both threads and catches kill signals and shuts down appropriately. ''' log = logging.getLogger(__name__) - + try: log.info('Optimization started.') self._start_up() @@ -350,14 +350,14 @@ def optimize(self): self._shut_down() self.print_results() self.log.info('M-LOOP Done.') - + def _start_up(self): ''' Start the learner and interface threads/processes. ''' self.learner.start() self.interface.start() - + def _shut_down(self): ''' Shutdown and clean up resources of the controller. end the learners, queue_listener and make one last save of archive. @@ -373,8 +373,8 @@ def _shut_down(self): self.log.debug('Learner joined.') self.interface.join() self.log.debug('Interface joined.') - self.save_archive() - + self.save_archive() + def print_results(self): ''' Print results from optimization run to the logs @@ -393,7 +393,7 @@ def print_results(self): def _optimization_routine(self): ''' - Runs controller main loop. Gives parameters to experiment and saves costs returned. + Runs controller main loop. Gives parameters to experiment and saves costs returned. ''' self.log.debug('Start controller loop.') self.log.info('Run:' + str(self.num_in_costs +1)) @@ -411,61 +411,61 @@ def _optimization_routine(self): def _first_params(self): ''' - Checks queue to get first parameters. - + Checks queue to get first parameters. + Returns: Parameters for first experiment ''' return self.learner_params_queue.get() - + def _next_params(self): ''' Abstract method. - + When implemented should send appropriate information to learner and get next parameters. - + Returns: Parameters for next experiment. ''' pass - + class RandomController(Controller): ''' Controller that simply returns random variables for the next parameters. Costs are stored but do not influence future points picked. - + Args: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. **kwargs (Optional [dict]): Dictionary of options to be passed to Controller and Random Learner. - + ''' def __init__(self, interface,**kwargs): - + super(RandomController,self).__init__(interface, **kwargs) self.learner = mll.RandomLearner(start_datetime = self.start_datetime, learner_archive_filename=None, **self.remaining_kwargs) - + self._update_controller_with_learner_attributes() self.out_type.append('random') - - self.log.debug('Random controller init completed.') - + + self.log.debug('Random controller init completed.') + def _next_params(self): ''' Sends cost uncer and bad tuple to learner then gets next parameters. - + Returns: Parameters for next experiment. ''' self.learner_costs_queue.put(self.best_params) - return self.learner_params_queue.get() - + return self.learner_params_queue.get() + class NelderMeadController(Controller): ''' Controller for the Nelder-Mead solver. Suggests new parameters based on the Nelder-Mead algorithm. Can take no boundaries or hard boundaries. More details for the Nelder-Mead options are in the learners section. - + Args: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. @@ -473,14 +473,14 @@ class NelderMeadController(Controller): ''' def __init__(self, interface, **kwargs): - super(NelderMeadController,self).__init__(interface, **kwargs) - + super(NelderMeadController,self).__init__(interface, **kwargs) + self.learner = mll.NelderMeadLearner(start_datetime = self.start_datetime, **self.remaining_kwargs) - + self._update_controller_with_learner_attributes() self.out_type.append('nelder_mead') - + def _next_params(self): ''' Gets next parameters from Nelder-Mead learner. @@ -488,14 +488,14 @@ def _next_params(self): if self.curr_bad: cost = float('inf') else: - cost = self.curr_cost + cost = self.curr_cost self.learner_costs_queue.put(cost) return self.learner_params_queue.get() class DifferentialEvolutionController(Controller): ''' - Controller for the differential evolution learner. - + Controller for the differential evolution learner. + Args: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. @@ -503,14 +503,14 @@ class DifferentialEvolutionController(Controller): ''' def __init__(self, interface, **kwargs): - super(DifferentialEvolutionController,self).__init__(interface, **kwargs) - + super(DifferentialEvolutionController,self).__init__(interface, **kwargs) + self.learner = mll.DifferentialEvolutionLearner(start_datetime = self.start_datetime, **self.remaining_kwargs) - + self._update_controller_with_learner_attributes() self.out_type.append('differential_evolution') - + def _next_params(self): ''' Gets next parameters from differential evolution learner. @@ -518,15 +518,15 @@ def _next_params(self): if self.curr_bad: cost = float('inf') else: - cost = self.curr_cost + cost = self.curr_cost self.learner_costs_queue.put(cost) return self.learner_params_queue.get() class MachineLearnerController(Controller): ''' - Abstract Controller class for the machine learning based solvers. - + Abstract Controller class for the machine learning based solvers. + Args: interface (Interface): The interface to the experiment under optimization. **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. @@ -536,37 +536,37 @@ class MachineLearnerController(Controller): num_training_runs (Optional [int]): The number of training runs to before starting the learner. If None, will by ten or double the number of parameters, whatever is larger. no_delay (Optional [bool]): If True, there is never any delay between a returned cost and the next parameters to run for the experiment. In practice, this means if the gaussian process has not prepared the next parameters in time the learner defined by the initial training source is used instead. If false, the controller will wait for the gaussian process to predict the next parameters and there may be a delay between runs. ''' - - def __init__(self, interface, + + def __init__(self, interface, training_type='differential_evolution', num_training_runs=None, no_delay=True, num_params=None, min_boundary=None, max_boundary=None, - trust_region=None, + trust_region=None, learner_archive_filename = mll.default_learner_archive_filename, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): - - super(MachineLearnerController,self).__init__(interface, **kwargs) - + + super(MachineLearnerController,self).__init__(interface, **kwargs) + self.last_training_cost = None self.last_training_bad = None self.last_training_run_flag = False - + if num_training_runs is None: if num_params is None: self.num_training_runs = 10 else: self.num_training_runs = max(10, 2*int(num_params)) else: - self.num_training_runs = int(num_training_runs) + self.num_training_runs = int(num_training_runs) if self.num_training_runs<=0: self.log.error('Number of training runs must be larger than zero:'+repr(self.num_training_runs)) raise ValueError self.no_delay = bool(no_delay) - + self.training_type = str(training_type) if self.training_type == 'random': self.learner = mll.RandomLearner(start_datetime=self.start_datetime, @@ -586,7 +586,7 @@ def __init__(self, interface, learner_archive_filename=None, learner_archive_file_type=learner_archive_file_type, **self.remaining_kwargs) - + elif self.training_type == 'differential_evolution': self.learner = mll.DifferentialEvolutionLearner(start_datetime=self.start_datetime, num_params=num_params, @@ -596,32 +596,32 @@ def __init__(self, interface, evolution_strategy='rand2', learner_archive_filename=None, learner_archive_file_type=learner_archive_file_type, - **self.remaining_kwargs) - + **self.remaining_kwargs) + else: self.log.error('Unknown training type provided to Gaussian process controller:' + repr(training_type)) self.archive_dict.update({'training_type':self.training_type}) self._update_controller_with_learner_attributes() - - + + def _update_controller_with_machine_learner_attributes(self): - + self.ml_learner_params_queue = self.ml_learner.params_out_queue self.ml_learner_costs_queue = self.ml_learner.costs_in_queue self.end_ml_learner = self.ml_learner.end_event self.new_params_event = self.ml_learner.new_params_event self.remaining_kwargs = self.ml_learner.remaining_kwargs self.generation_num = self.ml_learner.generation_num - - + + def _put_params_and_out_dict(self, params): ''' Override _put_params_and_out_dict function, used when the training learner creates parameters. Makes the defualt param_type the training type and sets last_training_run_flag. ''' super(MachineLearnerController,self)._put_params_and_out_dict(params, param_type=self.training_type) - self.last_training_run_flag = True - + self.last_training_run_flag = True + def _get_cost_and_in_dict(self): ''' Call _get_cost_and_in_dict() of parent Controller class. But also sends cost to Gaussian process learner and saves the cost if the parameters came from a trainer. @@ -636,7 +636,7 @@ def _get_cost_and_in_dict(self): self.curr_cost, self.curr_uncer, self.curr_bad)) - + def _next_params(self): ''' Gets next parameters from training learner. @@ -646,19 +646,19 @@ def _next_params(self): if self.last_training_bad: cost = float('inf') else: - cost = self.last_training_cost + cost = self.last_training_cost self.learner_costs_queue.put(cost) temp = self.learner_params_queue.get() - + elif self.training_type == 'random': #Copied from RandomController self.learner_costs_queue.put(self.best_params) - temp = self.learner_params_queue.get() - + temp = self.learner_params_queue.get() + else: self.log.error('Unknown training type called. THIS SHOULD NOT HAPPEN') return temp - + def _start_up(self): ''' Runs pararent method and also starts training_learner. @@ -684,7 +684,7 @@ def _optimization_routine(self): self._put_params_and_out_dict(next_params) self.save_archive() self._get_cost_and_in_dict() - + if self.check_end_conditions(): #Start last training run self.log.info('Run:' + str(self.num_in_costs +1)) @@ -696,10 +696,10 @@ def _optimization_routine(self): self.save_archive() self._get_cost_and_in_dict() self.log.debug('End training runs.') - + ml_consec = 0 - ml_count = 0 - + ml_count = 0 + while self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) if ml_consec==self.generation_num or (self.no_delay and self.ml_learner_params_queue.empty()): @@ -750,16 +750,16 @@ def _shut_down(self): if self.ml_learner.predict_global_minima_at_end or self.ml_learner.predict_local_minima_at_end: self.log.info('Machine Learner did not provide best and/or all minima.') super(MachineLearnerController,self)._shut_down() - + def print_results(self): ''' - Adds some additional output to the results specific to controller. + Adds some additional output to the results specific to controller. ''' super(MachineLearnerController,self).print_results() try: self.log.info('Predicted best parameters:' + str(self.predicted_best_parameters)) self.log.info('Predicted best cost:' + str(self.predicted_best_cost) + ' +/- ' + str(self.predicted_best_uncertainty)) - + except AttributeError: pass try: @@ -769,34 +769,34 @@ def print_results(self): class GaussianProcessController(MachineLearnerController): ''' - Controller for the Gaussian Process solver. Primarily suggests new points from the Gaussian Process learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. - + Controller for the Gaussian Process solver. Primarily suggests new points from the Gaussian Process learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. + Args: interface (Interface): The interface to the experiment under optimization. **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. Keyword Args: - + ''' - - def __init__(self, interface, + + def __init__(self, interface, num_params=None, min_boundary=None, max_boundary=None, - trust_region=None, + trust_region=None, learner_archive_filename = mll.default_learner_archive_filename, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): - + super(GaussianProcessController,self).__init__(interface, num_params=num_params, min_boundary=min_boundary, max_boundary=max_boundary, trust_region=trust_region, learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **kwargs) - + learner_archive_file_type=learner_archive_file_type, + **kwargs) + self.ml_learner = mll.GaussianProcessLearner(start_datetime=self.start_datetime, num_params=num_params, min_boundary=min_boundary, @@ -805,26 +805,26 @@ def __init__(self, interface, learner_archive_filename=learner_archive_filename, learner_archive_file_type=learner_archive_file_type, **self.remaining_kwargs) - + self._update_controller_with_machine_learner_attributes() class NeuralNetController(MachineLearnerController): ''' - Controller for the Neural Net solver. Primarily suggests new points from the Neural Net learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. - + Controller for the Neural Net solver. Primarily suggests new points from the Neural Net learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. + Args: interface (Interface): The interface to the experiment under optimization. **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. Keyword Args: - + ''' - - def __init__(self, interface, + + def __init__(self, interface, num_params=None, min_boundary=None, max_boundary=None, - trust_region=None, + trust_region=None, learner_archive_filename = mll.default_learner_archive_filename, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): @@ -835,9 +835,9 @@ def __init__(self, interface, max_boundary=max_boundary, trust_region=trust_region, learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **kwargs) - + learner_archive_file_type=learner_archive_file_type, + **kwargs) + self.ml_learner = mll.NeuralNetLearner(start_datetime=self.start_datetime, num_params=num_params, min_boundary=min_boundary, @@ -846,8 +846,8 @@ def __init__(self, interface, learner_archive_filename=learner_archive_filename, learner_archive_file_type=learner_archive_file_type, **self.remaining_kwargs) - + self._update_controller_with_machine_learner_attributes() - \ No newline at end of file + diff --git a/mloop/learners.py b/mloop/learners.py index ed15093..a0f3572 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1,5 +1,5 @@ ''' -Module of learners used to determine what parameters to try next given previous cost evaluations. +Module of learners used to determine what parameters to try next given previous cost evaluations. Each learner is created and controlled by a controller. ''' @@ -21,26 +21,26 @@ import multiprocessing as mp learner_thread_count = 0 -default_learner_archive_filename = 'learner_archive' +default_learner_archive_filename = 'learner_archive' default_learner_archive_file_type = 'txt' class LearnerInterrupt(Exception): ''' - Exception that is raised when the learner is ended with the end flag or event. + Exception that is raised when the learner is ended with the end flag or event. ''' def __init__(self): ''' Create LearnerInterrupt. ''' super(LearnerInterrupt,self).__init__() - + class Learner(): ''' Base class for all learners. Contains default boundaries and some useful functions that all learners use. - - The class that inherits from this class should also inherit from threading.Thread or multiprocessing.Process, depending if you need the learner to be a genuine parallel process or not. - + + The class that inherits from this class should also inherit from threading.Thread or multiprocessing.Process, depending if you need the learner to be a genuine parallel process or not. + Keyword Args: num_params (Optional [int]): The number of parameters to be optimized. If None defaults to 1. Default None. min_boundary (Optional [array]): Array with minimimum values allowed for each parameter. Note if certain values have no minimum value you can set them to -inf for example [-1, 2, float('-inf')] is a valid min_boundary. If None sets all the boundaries to '-1'. Default None. @@ -49,36 +49,36 @@ class Learner(): learner_archive_file_type (Optional [string]): File type for archive. Can be either 'txt' a human readable text file, 'pkl' a python dill file, 'mat' a matlab file or None if there is no archive. Default 'mat'. log_level (Optional [int]): Level for the learners logger. If None, set to warning. Default None. start_datetime (Optional [datetime]): Start date time, if None, is automatically generated. - + Attributes: params_out_queue (queue): Queue for parameters created by learner. costs_in_queue (queue): Queue for costs to be used by learner. end_event (event): Event to trigger end of learner. ''' - - def __init__(self, + + def __init__(self, num_params=None, - min_boundary=None, - max_boundary=None, + min_boundary=None, + max_boundary=None, learner_archive_filename=default_learner_archive_filename, learner_archive_file_type=default_learner_archive_file_type, start_datetime=None, **kwargs): super(Learner,self).__init__() - + global learner_thread_count - learner_thread_count += 1 + learner_thread_count += 1 self.log = logging.getLogger(__name__ + '.' + str(learner_thread_count)) - + self.learner_wait=float(1) - + self.remaining_kwargs = kwargs - + self.params_out_queue = mp.Queue() self.costs_in_queue = mp.Queue() self.end_event = mp.Event() - + if num_params is None: self.log.warning('num_params not provided, setting to default value of 1.') self.num_params = 1 @@ -121,42 +121,42 @@ def __init__(self, os.makedirs(mlu.archive_foldername) self.learner_archive_filename =str(learner_archive_filename) self.total_archive_filename = mlu.archive_foldername + self.learner_archive_filename + '_' + mlu.datetime_to_string(self.start_datetime) + '.' + self.learner_archive_file_type - + self.archive_dict = {'archive_type':'learner', 'num_params':self.num_params, 'min_boundary':self.min_boundary, 'max_boundary':self.max_boundary, 'start_datetime':mlu.datetime_to_string(self.start_datetime)} - - self.log.debug('Learner init completed.') - + + self.log.debug('Learner init completed.') + def check_num_params(self,param): ''' Check the number of parameters is right. ''' return param.shape == (self.num_params,) - + def check_in_boundary(self,param): ''' Check give parameters are within stored boundaries - + Args: param (array): array of parameters - + Returns: bool : True if the parameters are within boundaries, False otherwise. ''' param = np.array(param) testbool = np.all(param >= self.min_boundary) and np.all(param <= self.max_boundary) return testbool - + def check_in_diff_boundary(self,param): ''' Check given distances are less than the boundaries - + Args: param (array): array of distances - + Returns: bool : True if the distances are smaller or equal to boundaries, False otherwise. ''' @@ -166,11 +166,11 @@ def check_in_diff_boundary(self,param): def put_params_and_get_cost(self, params, **kwargs): ''' - Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. - + Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. + Args: params (array) : array of values to be sent to file - + Returns: cost from the cost queue ''' @@ -196,7 +196,7 @@ def put_params_and_get_cost(self, params, **kwargs): raise LearnerInterrupt #self.log.debug('Learner cost='+repr(cost)) return cost - + def save_archive(self): ''' Save the archive associated with the learner class. Only occurs if the filename for the archive is not None. Saves with the format previously set. @@ -204,19 +204,19 @@ def save_archive(self): self.update_archive() if self.learner_archive_filename is not None: mlu.save_dict_to_file(self.archive_dict, self.total_archive_filename, self.learner_archive_file_type) - + def update_archive(self): ''' Abstract method for update to the archive. To be implemented by child class. ''' pass - + def _set_trust_region(self,trust_region): ''' - Sets trust region properties for learner that have this. Common function for learners with trust regions. - + Sets trust region properties for learner that have this. Common function for learners with trust regions. + Args: - trust_region (float or array): Property defines the trust region. + trust_region (float or array): Property defines the trust region. ''' if trust_region is None: self.trust_region = float('nan') @@ -231,7 +231,7 @@ def _set_trust_region(self,trust_region): raise ValueError else: self.trust_region = np.array(trust_region, dtype=float) - + if self.has_trust_region: if not self.check_num_params(self.trust_region): self.log.error('Shape of the trust_region does not match the number of parameters:' + repr(self.trust_region)) @@ -242,7 +242,7 @@ def _set_trust_region(self,trust_region): if not self.check_in_diff_boundary(self.trust_region): self.log.error('The trust_region must be smaller than the range of the boundaries:' + repr(self.trust_region)) raise ValueError - + def _shut_down(self): ''' Shut down and perform one final save of learner. @@ -253,24 +253,24 @@ def _shut_down(self): class RandomLearner(Learner, threading.Thread): ''' Random learner. Simply generates new parameters randomly with a uniform distribution over the boundaries. Learner is perhaps a misnomer for this class. - + Args: - **kwargs (Optional dict): Other values to be passed to Learner. - + **kwargs (Optional dict): Other values to be passed to Learner. + Keyword Args: min_boundary (Optional [array]): If set to None, overrides default learner values and sets it to a set of value 0. Default None. max_boundary (Optional [array]): If set to None overides default learner values and sets it to an array of value 1. Default None. first_params (Optional [array]): The first parameters to test. If None will just randomly sample the initial condition. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. ''' - - def __init__(self, + + def __init__(self, trust_region=None, first_params=None, **kwargs): - + super(RandomLearner,self).__init__(**kwargs) - + if not np.all(self.diff_boundary>0.0): self.log.error('All elements of max_boundary are not larger than min_boundary') raise ValueError @@ -287,13 +287,13 @@ def __init__(self, if not self.check_in_boundary(self.first_params): self.log.error('first_params is not in the boundary:' + repr(self.first_params)) raise ValueError - + self._set_trust_region(trust_region) - + self.archive_dict.update({'archive_type':'random_learner'}) - + self.log.debug('Random learner init completed.') - + def run(self): ''' Puts the next parameters on the queue which are randomly picked from a uniform distribution between the minimum and maximum boundaries when a cost is added to the cost queue. @@ -303,7 +303,7 @@ def run(self): next_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary else: next_params = self.first_params - while not self.end_event.is_set(): + while not self.end_event.is_set(): try: centre_params = self.put_params_and_get_cost(next_params) except LearnerInterrupt: @@ -315,46 +315,46 @@ def run(self): next_params = temp_min + nr.rand(self.num_params) * (temp_max - temp_min) else: next_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary - + self._shut_down() self.log.debug('Ended Random Learner') - + class NelderMeadLearner(Learner, threading.Thread): ''' Nelder-Mead learner. Executes the Nelder-Mead learner algorithm and stores the needed simplex to estimate the next points. - + Args: params_out_queue (queue): Queue for parameters from controller. costs_in_queue (queue): Queue for costs for nelder learner. The queue should be populated with cost (float) corresponding to the last parameter sent from the Nelder-Mead Learner. Can be a float('inf') if it was a bad run. end_event (event): Event to trigger end of learner. - + Keyword Args: initial_simplex_corner (Optional [array]): Array for the initial set of parameters, which is the lowest corner of the initial simplex. If None the initial parameters are randomly sampled if the boundary conditions are provided, or all are set to 0 if boundary conditions are not provided. initial_simplex_displacements (Optional [array]): Array used to construct the initial simplex. Each array is the positive displacement of the parameters above the init_params. If None and there are no boundary conditions, all are set to 1. If None and there are boundary conditions assumes the initial conditions are scaled. Default None. - initial_simplex_scale (Optional [float]): Creates a simplex using a the boundary conditions and the scaling factor provided. If None uses the init_simplex if provided. If None and init_simplex is not provided, but boundary conditions are is set to 0.5. Default None. - + initial_simplex_scale (Optional [float]): Creates a simplex using a the boundary conditions and the scaling factor provided. If None uses the init_simplex if provided. If None and init_simplex is not provided, but boundary conditions are is set to 0.5. Default None. + Attributes: init_simplex_corner (array): Parameters for the corner of the initial simple used. - init_simplex_disp (array): Parameters for the displacements about the simplex corner used to create the initial simple. + init_simplex_disp (array): Parameters for the displacements about the simplex corner used to create the initial simple. simplex_params (array): Parameters of the current simplex simplex_costs (array): Costs associated with the parameters of the current simplex - + ''' - def __init__(self, - initial_simplex_corner=None, - initial_simplex_displacements=None, + def __init__(self, + initial_simplex_corner=None, + initial_simplex_displacements=None, initial_simplex_scale=None, **kwargs): - + super(NelderMeadLearner,self).__init__(**kwargs) - + self.num_boundary_hits = 0 self.rho = 1 self.chi = 2 self.psi = 0.5 self.sigma = 0.5 - + if initial_simplex_displacements is None and initial_simplex_scale is None: self.init_simplex_disp = self.diff_boundary * 0.6 self.init_simplex_disp[self.init_simplex_disp==float('inf')] = 1 @@ -368,7 +368,7 @@ def __init__(self, self.init_simplex_disp = np.array(initial_simplex_displacements, dtype=float) else: self.log.error('initial_simplex_displacements and initial_simplex_scale can not both be provided simultaneous.') - + if not self.check_num_params(self.init_simplex_disp): self.log.error('There is the wrong number of elements in the initial simplex displacement:' + repr(self.init_simplex_disp)) raise ValueError @@ -378,7 +378,7 @@ def __init__(self, if not self.check_in_diff_boundary(self.init_simplex_disp): self.log.error('Initial simplex displacements must be within boundaries. init_simplex_disp:'+ repr(self.init_simplex_disp) + '. diff_boundary:' +repr(self.diff_boundary)) raise ValueError - + if initial_simplex_corner is None: diff_roll = (self.diff_boundary - self.init_simplex_disp) * nr.rand(self.num_params) diff_roll[diff_roll==float('+inf')]= 0 @@ -387,42 +387,42 @@ def __init__(self, self.init_simplex_corner += diff_roll else: self.init_simplex_corner = np.array(initial_simplex_corner, dtype=float) - + if not self.check_num_params(self.init_simplex_corner): - self.log.error('There is the wrong number of elements in the initial simplex corner:' + repr(self.init_simplex_corner)) + self.log.error('There is the wrong number of elements in the initial simplex corner:' + repr(self.init_simplex_corner)) if not self.check_in_boundary(self.init_simplex_corner): self.log.error('Initial simplex corner outside of boundaries:' + repr(self.init_simplex_corner)) raise ValueError - + if not np.all(np.isfinite(self.init_simplex_corner + self.init_simplex_disp)): self.log.error('Initial simplex corner and simplex are not finite numbers. init_simplex_corner:'+ repr(self.init_simplex_corner) + '. init_simplex_disp:' +repr(self.init_simplex_disp)) raise ValueError if not self.check_in_boundary(self.init_simplex_corner + self.init_simplex_disp): self.log.error('Largest boundary of simplex not inside the boundaries:' + repr(self.init_simplex_corner + self.init_simplex_disp)) raise ValueError - + self.simplex_params = np.zeros((self.num_params + 1, self.num_params), dtype=float) self.simplex_costs = np.zeros((self.num_params + 1,), dtype=float) - + self.archive_dict.update({'archive_type':'nelder_mead_learner', 'initial_simplex_corner':self.init_simplex_corner, 'initial_simplex_displacements':self.init_simplex_disp}) - + self.log.debug('Nelder-Mead learner init completed.') - + def run(self): ''' - Runs Nelder-Mead algorithm to produce new parameters given costs, until end signal is given. + Runs Nelder-Mead algorithm to produce new parameters given costs, until end signal is given. ''' - + self.log.info('Starting Nelder Mead Learner') - + N = int(self.num_params) - + one2np1 = list(range(1, N + 1)) - + self.simplex_params[0] = self.init_simplex_corner - + try: self.simplex_costs[0] = self.put_params_and_get_cost(self.init_simplex_corner) except ValueError: @@ -431,7 +431,7 @@ def run(self): except LearnerInterrupt: self.log.info('Ended Nelder-Mead before end of simplex') return - + for k in range(0, N): y = np.array(self.init_simplex_corner, copy=True) y[k] = y[k] + self.init_simplex_disp[k] @@ -444,22 +444,22 @@ def run(self): except LearnerInterrupt: self.log.info('Ended Nelder-Mead before end of simplex') return - + self.simplex_costs[k + 1] = f - + ind = np.argsort(self.simplex_costs) self.simplex_costs = np.take(self.simplex_costs, ind, 0) # sort so sim[0,:] has the lowest function value self.simplex_params = np.take(self.simplex_params, ind, 0) - + while not self.end_event.is_set(): - + xbar = np.add.reduce(self.simplex_params[:-1], 0) / N xr = (1 +self.rho) * xbar -self.rho * self.simplex_params[-1] - + if self.check_in_boundary(xr): try: - fxr = self.put_params_and_get_cost(xr) + fxr = self.put_params_and_get_cost(xr) except ValueError: self.log.error('Outside of boundary on first reduce. THIS SHOULD NOT HAPPEN') raise @@ -470,12 +470,12 @@ def run(self): fxr = float('inf') self.num_boundary_hits+=1 self.log.debug('Hit boundary (reflect): '+str(self.num_boundary_hits)+' times.') - + doshrink = 0 - + if fxr < self.simplex_costs[0]: xe = (1 +self.rho *self.chi) * xbar -self.rho *self.chi * self.simplex_params[-1] - + if self.check_in_boundary(xe): try: fxe = self.put_params_and_get_cost(xe) @@ -486,10 +486,10 @@ def run(self): break else: #Hit boundary so set the cost above maximum this ensures the algorithm does a contracting reflection - fxe = fxr+1.0 + fxe = fxr+1.0 self.num_boundary_hits+=1 self.log.debug('Hit boundary (expand): '+str(self.num_boundary_hits)+' times.') - + if fxe < fxr: self.simplex_params[-1] = xe self.simplex_costs[-1] = fxe @@ -541,11 +541,11 @@ def run(self): raise except LearnerInterrupt: break - + ind = np.argsort(self.simplex_costs) self.simplex_params = np.take(self.simplex_params, ind, 0) self.simplex_costs = np.take(self.simplex_costs, ind, 0) - + self._shut_down() self.log.info('Ended Nelder-Mead') @@ -558,43 +558,43 @@ def update_archive(self): class DifferentialEvolutionLearner(Learner, threading.Thread): ''' - Adaption of the differential evolution algorithm in scipy. - + Adaption of the differential evolution algorithm in scipy. + Args: params_out_queue (queue): Queue for parameters sent to controller. costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple end_event (event): Event to trigger end of learner. - + Keyword Args: first_params (Optional [array]): The first parameters to test. If None will just randomly sample the initial condition. Default None. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. evolution_strategy (Optional [string]): the differential evolution strategy to use, options are 'best1', 'best1', 'rand1' and 'rand2'. The default is 'best2'. population_size (Optional [int]): multiplier proportional to the number of parameters in a generation. The generation population is set to population_size * parameter_num. Default 15. mutation_scale (Optional [tuple]): The mutation scale when picking new points. Otherwise known as differential weight. When provided as a tuple (min,max) a mutation constant is picked randomly in the interval. Default (0.5,1.0). cross_over_probability (Optional [float]): The recombination constand or crossover probability, the probability a new points will be added to the population. restart_tolerance (Optional [float]): when the current population have a spread less than the initial tolerance, namely stdev(curr_pop) < restart_tolerance stdev(init_pop), it is likely the population is now in a minima, and so the search is started again. - + Attributes: has_trust_region (bool): Whether the learner has a trust region. - num_population_members (int): The number of parameters in a generation. + num_population_members (int): The number of parameters in a generation. params_generations (list): History of the parameters generations. A list of all the parameters in the population, for each generation created. costs_generations (list): History of the costs generations. A list of all the costs in the population, for each generation created. init_std (float): The initial standard deviation in costs of the population. Calucalted after sampling (or resampling) the initial population. curr_std (float): The current standard devation in costs of the population. Calculated after sampling each generation. ''' - def __init__(self, + def __init__(self, first_params = None, trust_region = None, - evolution_strategy='best1', + evolution_strategy='best1', population_size=15, - mutation_scale=(0.5, 1), - cross_over_probability=0.7, - restart_tolerance=0.01, + mutation_scale=(0.5, 1), + cross_over_probability=0.7, + restart_tolerance=0.01, **kwargs): - + super(DifferentialEvolutionLearner,self).__init__(**kwargs) - + if first_params is None: self.first_params = float('nan') else: @@ -605,9 +605,9 @@ def __init__(self, if not self.check_in_boundary(self.first_params): self.log.error('first_params is not in the boundary:' + repr(self.first_params)) raise ValueError - + self._set_trust_region(trust_region) - + if evolution_strategy == 'best1': self.mutation_func = self._best1 elif evolution_strategy == 'best2': @@ -619,7 +619,7 @@ def __init__(self, else: self.log.error('Please select a valid mutation strategy') raise ValueError - + self.evolution_strategy = evolution_strategy self.restart_tolerance = restart_tolerance @@ -628,29 +628,29 @@ def __init__(self, else: self.log.error('Mutation scale must be a tuple with (min,max) between 0 and 2. mutation_scale:' + repr(mutation_scale)) raise ValueError - + if cross_over_probability <= 1 and cross_over_probability >= 0: self.cross_over_probability = cross_over_probability else: self.log.error('Cross over probability must be between 0 and 1. cross_over_probability:' + repr(cross_over_probability)) - + if population_size >= 5: self.population_size = population_size else: self.log.error('Population size must be greater or equal to 5:' + repr(population_size)) - + self.num_population_members = self.population_size * self.num_params - + self.first_sample = True - + self.params_generations = [] self.costs_generations = [] self.generation_count = 0 - + self.min_index = 0 self.init_std = 0 self.curr_std = 0 - + self.archive_dict.update({'archive_type':'differential_evolution', 'evolution_strategy':self.evolution_strategy, 'mutation_scale':self.mutation_scale, @@ -661,26 +661,26 @@ def __init__(self, 'first_params':self.first_params, 'has_trust_region':self.has_trust_region, 'trust_region':self.trust_region}) - - + + def run(self): ''' Runs the Differential Evolution Learner. ''' try: - + self.generate_population() - + while not self.end_event.is_set(): - + self.next_generation() - + if self.curr_std < self.restart_tolerance * self.init_std: self.generate_population() - + except LearnerInterrupt: return - + def save_generation(self): ''' Save history of generations. @@ -688,29 +688,29 @@ def save_generation(self): self.params_generations.append(np.copy(self.population)) self.costs_generations.append(np.copy(self.population_costs)) self.generation_count += 1 - + def generate_population(self): ''' Sample a new random set of variables ''' - + self.population = [] self.population_costs = [] self.min_index = 0 - + if np.all(np.isfinite(self.first_params)) and self.first_sample: curr_params = self.first_params self.first_sample = False else: curr_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary - + curr_cost = self.put_params_and_get_cost(curr_params) - + self.population.append(curr_params) self.population_costs.append(curr_cost) - + for index in range(1, self.num_population_members): - + if self.has_trust_region: temp_min = np.maximum(self.min_boundary,self.population[self.min_index] - self.trust_region) temp_max = np.minimum(self.max_boundary,self.population[self.min_index] + self.trust_region) @@ -719,76 +719,76 @@ def generate_population(self): curr_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary curr_cost = self.put_params_and_get_cost(curr_params) - + self.population.append(curr_params) self.population_costs.append(curr_cost) - + if curr_cost < self.population_costs[self.min_index]: self.min_index = index - + self.population = np.array(self.population) self.population_costs = np.array(self.population_costs) - + self.init_std = np.std(self.population_costs) self.curr_std = self.init_std - + self.save_generation() - + def next_generation(self): ''' Evolve the population by a single generation ''' - + self.curr_scale = nr.uniform(self.mutation_scale[0], self.mutation_scale[1]) - + for index in range(self.num_population_members): - + curr_params = self.mutate(index) curr_cost = self.put_params_and_get_cost(curr_params) - + if curr_cost < self.population_costs[index]: self.population[index] = curr_params self.population_costs[index] = curr_cost - + if curr_cost < self.population_costs[self.min_index]: self.min_index = index - + self.curr_std = np.std(self.population_costs) - + self.save_generation() def mutate(self, index): ''' Mutate the parameters at index. - + Args: index (int): Index of the point to be mutated. ''' - + fill_point = nr.randint(0, self.num_params) candidate_params = self.mutation_func(index) crossovers = nr.rand(self.num_params) < self.cross_over_probability crossovers[fill_point] = True mutated_params = np.where(crossovers, candidate_params, self.population[index]) - + if self.has_trust_region: temp_min = np.maximum(self.min_boundary,self.population[self.min_index] - self.trust_region) temp_max = np.minimum(self.max_boundary,self.population[self.min_index] + self.trust_region) rand_params = temp_min + nr.rand(self.num_params) * (temp_max - temp_min) else: rand_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary - + projected_params = np.where(np.logical_or(mutated_params < self.min_boundary, mutated_params > self.max_boundary), rand_params, mutated_params) - + return projected_params def _best1(self, index): ''' Use best parameters and two others to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1 = self.random_index_sample(index, 2) return (self.population[self.min_index] + self.curr_scale *(self.population[r0] - self.population[r1])) @@ -796,9 +796,9 @@ def _best1(self, index): def _rand1(self, index): ''' Use three random parameters to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1, r2 = self.random_index_sample(index, 3) return (self.population[r0] + self.curr_scale * (self.population[r1] - self.population[r2])) @@ -806,9 +806,9 @@ def _rand1(self, index): def _best2(self, index): ''' Use best parameters and four others to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1, r2, r3 = self.random_index_sample(index, 4) return self.population[self.min_index] + self.curr_scale * (self.population[r0] + self.population[r1] - self.population[r2] - self.population[r3]) @@ -816,9 +816,9 @@ def _best2(self, index): def _rand2(self, index): ''' Use five random parameters to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1, r2, r3, r4 = self.random_index_sample(index, 5) return self.population[r0] + self.curr_scale * (self.population[r1] + self.population[r2] - self.population[r3] - self.population[r4]) @@ -826,7 +826,7 @@ def _rand2(self, index): def random_index_sample(self, index, num_picks): ''' Randomly select a num_picks of indexes, without index. - + Args: index(int): The index that is not included num_picks(int): The number of picks. @@ -834,7 +834,7 @@ def random_index_sample(self, index, num_picks): rand_indexes = list(range(self.num_population_members)) rand_indexes.remove(index) return random.sample(rand_indexes, num_picks) - + def update_archive(self): ''' Update the archive. @@ -850,49 +850,49 @@ def update_archive(self): class GaussianProcessLearner(Learner, mp.Process): ''' - Gaussian process learner. Generats new parameters based on a gaussian process fitted to all previous data. - + Gaussian process learner. Generats new parameters based on a gaussian process fitted to all previous data. + Args: params_out_queue (queue): Queue for parameters sent to controller. costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple end_event (event): Event to trigger end of learner. - + Keyword Args: length_scale (Optional [array]): The initial guess for length scale(s) of the gaussian process. The array can either of size one or the number of parameters or None. If it is size one, it is assumed all the correlation lengths are the same. If it is the number of the parameters then all the parameters have their own independent length scale. If it is None, it is assumed all the length scales should be independent and they are all given an initial value of 1. Default None. - cost_has_noise (Optional [bool]): If true the learner assumes there is common additive white noise that corrupts the costs provided. This noise is assumed to be on top of the uncertainty in the costs (if it is provided). If false, it is assumed that there is no noise in the cost (or if uncertainties are provided no extra noise beyond the uncertainty). Default True. + cost_has_noise (Optional [bool]): If true the learner assumes there is common additive white noise that corrupts the costs provided. This noise is assumed to be on top of the uncertainty in the costs (if it is provided). If false, it is assumed that there is no noise in the cost (or if uncertainties are provided no extra noise beyond the uncertainty). Default True. noise_level (Optional [float]): The initial guess for the noise level in the costs, is only used if cost_has_noise is true. Default 1.0. update_hyperparameters (Optional [bool]): Whether the length scales and noise estimate should be updated when new data is provided. Is set to true by default. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. default_bad_cost (Optional [float]): If a run is reported as bad and default_bad_cost is provided, the cost for the bad run is set to this default value. If default_bad_cost is None, then the worst cost received is set to all the bad runs. Default None. default_bad_uncertainty (Optional [float]): If a run is reported as bad and default_bad_uncertainty is provided, the uncertainty for the bad run is set to this default value. If default_bad_uncertainty is None, then the uncertainty is set to a tenth of the best to worst cost range. Default None. minimum_uncertainty (Optional [float]): The minimum uncertainty associated with provided costs. Must be above zero to avoid fitting errors. Default 1e-8. predict_global_minima_at_end (Optional [bool]): If True finds the global minima when the learner is ended. Does not if False. Default True. predict_local_minima_at_end (Optional [bool]): If True finds the all minima when the learner is ended. Does not if False. Default False. - + Attributes: all_params (array): Array containing all parameters sent to learner. all_costs (array): Array containing all costs sent to learner. all_uncers (array): Array containing all uncertainties sent to learner. - scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. + scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. bad_run_indexs (list): list of indexes to all runs that were marked as bad. best_cost (float): Minimum received cost, updated during execution. best_params (array): Parameters of best run. (reference to element in params array). - best_index (int): index of the best cost and params. + best_index (int): index of the best cost and params. worst_cost (float): Maximum received cost, updated during execution. worst_index (int): index to run with worst cost. cost_range (float): Difference between worst_cost and best_cost generation_num (int): Number of sets of parameters to generate each generation. Set to 5. length_scale_history (list): List of length scales found after each fit. - noise_level_history (list): List of noise levels found after each fit. + noise_level_history (list): List of noise levels found after each fit. fit_count (int): Counter for the number of times the gaussian process has been fit. cost_count (int): Counter for the number of costs, parameters and uncertainties added to learner. - params_count (int): Counter for the number of parameters asked to be evaluated by the learner. + params_count (int): Counter for the number of parameters asked to be evaluated by the learner. gaussian_process (GaussianProcessRegressor): Gaussian process that is fitted to data and used to make predictions - cost_scaler (StandardScaler): Scaler used to normalize the provided costs. - has_trust_region (bool): Whether the learner has a trust region. - ''' - - def __init__(self, + cost_scaler (StandardScaler): Scaler used to normalize the provided costs. + has_trust_region (bool): Whether the learner has a trust region. + ''' + + def __init__(self, length_scale = None, update_hyperparameters = True, cost_has_noise=True, @@ -906,40 +906,40 @@ def __init__(self, predict_global_minima_at_end = True, predict_local_minima_at_end = False, **kwargs): - + if gp_training_filename is not None: - + gp_training_filename = str(gp_training_filename) gp_training_file_type = str(gp_training_file_type) if not mlu.check_file_type_supported(gp_training_file_type): self.log.error('GP training file type not supported' + repr(gp_training_file_type)) - + self.training_dict = mlu.get_dict_from_file(gp_training_filename, gp_training_file_type) - + #Basic optimization settings num_params = int(self.training_dict['num_params']) min_boundary = np.squeeze(np.array(self.training_dict['min_boundary'], dtype=float)) max_boundary = np.squeeze(np.array(self.training_dict['max_boundary'], dtype=float)) - + #Configuration of the learner self.cost_has_noise = bool(self.training_dict['cost_has_noise']) self.length_scale = np.squeeze(np.array(self.training_dict['length_scale'])) self.length_scale_history = list(self.training_dict['length_scale_history']) self.noise_level = float(self.training_dict['noise_level']) self.noise_level_history = mlu.safe_cast_to_list(self.training_dict['noise_level_history']) - + #Counters self.costs_count = int(self.training_dict['costs_count']) self.fit_count = int(self.training_dict['fit_count']) self.params_count = int(self.training_dict['params_count']) - + #Data from previous experiment self.all_params = np.array(self.training_dict['all_params'], dtype=float) self.all_costs = np.squeeze(np.array(self.training_dict['all_costs'], dtype=float)) self.all_uncers = np.squeeze(np.array(self.training_dict['all_uncers'], dtype=float)) - - self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) - + + self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) + #Derived properties self.best_cost = float(self.training_dict['best_cost']) self.best_params = np.squeeze(np.array(self.training_dict['best_params'], dtype=float)) @@ -956,7 +956,7 @@ def __init__(self, self.has_global_minima = False try: self.local_minima_parameters = list(self.training_dict['local_minima_parameters']) - + if isinstance(self.training_dict['local_minima_costs'], np.ndarray): self.local_minima_costs = list(np.squeeze(self.training_dict['local_minima_costs'])) else: @@ -965,21 +965,21 @@ def __init__(self, self.local_minima_uncers = list(np.squeeze(self.training_dict['local_minima_uncers'])) else: self.local_minima_uncers = list(self.training_dict['local_minima_uncers']) - + self.has_local_minima = True except KeyError: self.has_local_minima = False - - + + super(GaussianProcessLearner,self).__init__(num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, + min_boundary=min_boundary, + max_boundary=max_boundary, **kwargs) - + else: - + super(GaussianProcessLearner,self).__init__(**kwargs) - + #Storage variables, archived self.all_params = np.array([], dtype=float) self.all_costs = np.array([], dtype=float) @@ -993,14 +993,14 @@ def __init__(self, self.cost_range = float('inf') self.length_scale_history = [] self.noise_level_history = [] - + self.costs_count = 0 self.fit_count = 0 self.params_count = 0 - + self.has_local_minima = False self.has_global_minima = False - + #Optional user set variables if length_scale is None: self.length_scale = np.ones((self.num_params,)) @@ -1008,32 +1008,32 @@ def __init__(self, self.length_scale = np.array(length_scale, dtype=float) self.noise_level = float(noise_level) self.cost_has_noise = bool(cost_has_noise) - - + + #Multiprocessor controls self.new_params_event = mp.Event() - + #Storage variables and counters self.search_params = [] self.scaled_costs = None self.cost_bias = None self.uncer_bias = None - + #Internal variable for bias function self.bias_func_cycle = 4 - self.bias_func_cost_factor = [1.0,1.0,1.0,1.0] + self.bias_func_cost_factor = [1.0,1.0,1.0,1.0] self.bias_func_uncer_factor =[0.0,1.0,2.0,3.0] self.generation_num = self.bias_func_cycle if self.generation_num < 3: self.log.error('Number in generation must be larger than 2.') raise ValueError - + #Constants, limits and tolerances self.search_precision = 1.0e-6 self.parameter_searches = max(10,self.num_params) self.hyperparameter_searches = max(10,self.num_params) - self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty - + self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty + #Optional user set variables self.update_hyperparameters = bool(update_hyperparameters) self.predict_global_minima_at_end = bool(predict_global_minima_at_end) @@ -1048,7 +1048,7 @@ def __init__(self, self.default_bad_uncertainty = None self.minimum_uncertainty = float(minimum_uncertainty) self._set_trust_region(trust_region) - + #Checks of variables if self.length_scale.size == 1: self.length_scale = float(self.length_scale) @@ -1075,17 +1075,17 @@ def __init__(self, if self.minimum_uncertainty <= 0: self.log.error('Minimum uncertainty must be larger than zero for the learner.') raise ValueError - + self.create_gaussian_process() - + #Search bounds self.search_min = self.min_boundary self.search_max = self.max_boundary self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - + self.cost_scaler = skp.StandardScaler() - + self.archive_dict.update({'archive_type':'gaussian_process_learner', 'cost_has_noise':self.cost_has_noise, 'length_scale_history':self.length_scale_history, @@ -1103,11 +1103,11 @@ def __init__(self, 'has_trust_region':self.has_trust_region, 'predict_global_minima_at_end':self.predict_global_minima_at_end, 'predict_local_minima_at_end':self.predict_local_minima_at_end}) - + #Remove logger so gaussian process can be safely picked for multiprocessing on Windows self.log = None - - + + def create_gaussian_process(self): ''' Create the initial Gaussian process. @@ -1120,10 +1120,10 @@ def create_gaussian_process(self): self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,n_restarts_optimizer=self.hyperparameter_searches) else: self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,optimizer=None) - + def wait_for_new_params_event(self): ''' - Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. + Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. ''' while not self.end_event.is_set(): if self.new_params_event.wait(timeout=self.learner_wait): @@ -1134,26 +1134,26 @@ def wait_for_new_params_event(self): else: self.log.debug('GaussianProcessLearner end signal received. Ending') raise LearnerInterrupt - - + + def get_params_and_costs(self): ''' - Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. + Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. ''' if self.costs_in_queue.empty(): self.log.error('Gaussian process asked for new parameters but no new costs were provided.') raise ValueError - + new_params = [] new_costs = [] new_uncers = [] new_bads = [] update_bads_flag = False - + while not self.costs_in_queue.empty(): (param, cost, uncer, bad) = self.costs_in_queue.get_nowait() self.costs_count +=1 - + if bad: new_bads.append(self.costs_count-1) if self.bad_defaults_set: @@ -1162,7 +1162,7 @@ def get_params_and_costs(self): else: cost = self.worst_cost uncer = self.cost_range*self.bad_uncer_frac - + param = np.array(param, dtype=float) if not self.check_num_params(param): self.log.error('Incorrect number of parameters provided to Gaussian process learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) @@ -1173,7 +1173,7 @@ def get_params_and_costs(self): if uncer < 0: self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) uncer = max(float(uncer), self.minimum_uncertainty) - + cost_change_flag = False if cost > self.worst_cost: self.worst_cost = cost @@ -1188,12 +1188,12 @@ def get_params_and_costs(self): self.cost_range = self.worst_cost - self.best_cost if not self.bad_defaults_set: update_bads_flag = True - + new_params.append(param) new_costs.append(cost) new_uncers.append(uncer) - - + + if self.all_params.size==0: self.all_params = np.array(new_params, dtype=float) self.all_costs = np.array(new_costs, dtype=float) @@ -1202,21 +1202,21 @@ def get_params_and_costs(self): self.all_params = np.concatenate((self.all_params, np.array(new_params, dtype=float))) self.all_costs = np.concatenate((self.all_costs, np.array(new_costs, dtype=float))) self.all_uncers = np.concatenate((self.all_uncers, np.array(new_uncers, dtype=float))) - + self.bad_run_indexs.append(new_bads) - + if self.all_params.shape != (self.costs_count,self.num_params): self.log('Saved GP params are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_params)) if self.all_costs.shape != (self.costs_count,): self.log('Saved GP costs are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_costs)) if self.all_uncers.shape != (self.costs_count,): self.log('Saved GP uncertainties are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_uncers)) - + if update_bads_flag: self.update_bads() - + self.update_search_region() - + def update_bads(self): ''' Best and/or worst costs have changed, update the values associated with bad runs accordingly. @@ -1224,7 +1224,7 @@ def update_bads(self): for index in self.bad_run_indexs: self.all_costs[index] = self.worst_cost self.all_uncers[index] = self.cost_range*self.bad_uncer_frac - + def update_search_region(self): ''' If trust boundaries is not none, updates the search boundaries based on the defined trust region. @@ -1234,7 +1234,7 @@ def update_search_region(self): self.search_max = np.minimum(self.best_params + self.trust_region, self.max_boundary) self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - + def update_search_params(self): ''' Update the list of parameters to use for the next search. @@ -1243,7 +1243,7 @@ def update_search_params(self): self.search_params.append(self.best_params) for _ in range(self.parameter_searches): self.search_params.append(self.search_min + nr.uniform(size=self.num_params) * self.search_diff) - + def update_archive(self): ''' Update the archive. @@ -1262,10 +1262,10 @@ def update_archive(self): 'params_count':self.params_count, 'update_hyperparameters':self.update_hyperparameters, 'length_scale':self.length_scale, - 'noise_level':self.noise_level}) - + 'noise_level':self.noise_level}) + + - def fit_gaussian_process(self): ''' Fit the Gaussian process to the current data @@ -1278,14 +1278,14 @@ def fit_gaussian_process(self): self.scaled_uncers = self.all_uncers * self.cost_scaler.scale_ self.gaussian_process.alpha_ = self.scaled_uncers self.gaussian_process.fit(self.all_params,self.scaled_costs) - + if self.update_hyperparameters: - + self.fit_count += 1 self.gaussian_process.kernel = self.gaussian_process.kernel_ - + last_hyperparameters = self.gaussian_process.kernel.get_params() - + if self.cost_has_noise: self.length_scale = last_hyperparameters['k1__length_scale'] if isinstance(self.length_scale, float): @@ -1296,30 +1296,30 @@ def fit_gaussian_process(self): else: self.length_scale = last_hyperparameters['length_scale'] self.length_scale_history.append(self.length_scale) - - + + def update_bias_function(self): ''' Set the constants for the cost bias function. ''' self.cost_bias = self.bias_func_cost_factor[self.params_count%self.bias_func_cycle] self.uncer_bias = self.bias_func_uncer_factor[self.params_count%self.bias_func_cycle] - + def predict_biased_cost(self,params): ''' Predicts the biased cost at the given parameters. The bias function is: biased_cost = cost_bias*pred_cost - uncer_bias*pred_uncer - + Returns: pred_bias_cost (float): Biased cost predicted at the given parameters ''' (pred_cost, pred_uncer) = self.gaussian_process.predict(params[np.newaxis,:], return_std=True) return self.cost_bias*pred_cost - self.uncer_bias*pred_uncer - + def find_next_parameters(self): ''' Returns next parameters to find. Increments counters and bias function appropriately. - + Return: next_params (array): Returns next parameters from biased cost search. ''' @@ -1334,7 +1334,7 @@ def find_next_parameters(self): next_params = result.x next_cost = result.fun return next_params - + def run(self): ''' Starts running the Gaussian process learner. When the new parameters event is triggered, reads the cost information provided and updates the Gaussian process with the information. Then searches the Gaussian process for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. @@ -1342,7 +1342,7 @@ def run(self): #logging to the main log file from a process (as apposed to a thread) in cpython is currently buggy on windows and/or python 2.7 #current solution is to only log to the console for warning and above from a process self.log = mp.log_to_stderr(logging.WARNING) - + try: while not self.end_event.is_set(): #self.log.debug('Learner waiting for new params event') @@ -1376,36 +1376,36 @@ def run(self): self.params_out_queue.put(end_dict) self._shut_down() self.log.debug('Ended Gaussian Process Learner') - + def predict_cost(self,params): ''' - Produces a prediction of cost from the gaussian process at params. - + Produces a prediction of cost from the gaussian process at params. + Returns: float : Predicted cost at paramters ''' return self.gaussian_process.predict(params[np.newaxis,:]) - + def find_global_minima(self): ''' Performs a quick search for the predicted global minima from the learner. Does not return any values, but creates the following attributes. - + Attributes: predicted_best_parameters (array): the parameters for the predicted global minima predicted_best_cost (float): the cost at the predicted global minima predicted_best_uncertainty (float): the uncertainty of the predicted global minima ''' self.log.debug('Started search for predicted global minima.') - + self.predicted_best_parameters = None self.predicted_best_scaled_cost = float('inf') self.predicted_best_scaled_uncertainty = None - + search_params = [] search_params.append(self.best_params) for _ in range(self.parameter_searches): search_params.append(self.min_boundary + nr.uniform(size=self.num_params) * self.diff_boundary) - + search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in search_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) @@ -1415,55 +1415,55 @@ def find_global_minima(self): self.predicted_best_parameters = curr_best_params self.predicted_best_scaled_cost = curr_best_cost self.predicted_best_scaled_uncertainty = curr_best_uncer - + self.predicted_best_cost = self.cost_scaler.inverse_transform(self.predicted_best_scaled_cost) self.predicted_best_uncertainty = self.predicted_best_scaled_uncertainty / self.cost_scaler.scale_ - + self.archive_dict.update({'predicted_best_parameters':self.predicted_best_parameters, 'predicted_best_scaled_cost':self.predicted_best_scaled_cost, 'predicted_best_scaled_uncertainty':self.predicted_best_scaled_uncertainty, 'predicted_best_cost':self.predicted_best_cost, 'predicted_best_uncertainty':self.predicted_best_uncertainty}) - + self.has_global_minima = True - self.log.debug('Predicted global minima found.') - + self.log.debug('Predicted global minima found.') + def find_local_minima(self): ''' - Performs a comprehensive search of the learner for all predicted local minima (and hence the global as well) in the landscape. Note, this can take a very long time. - + Performs a comprehensive search of the learner for all predicted local minima (and hence the global as well) in the landscape. Note, this can take a very long time. + Attributes: local_minima_parameters (list): list of all the parameters for local minima. local_minima_costs (list): list of all the costs at local minima. local_minima_uncers (list): list of all the uncertainties at local minima. - + ''' self.log.info('Searching for all minima.') - + self.minima_tolerance = 10*self.search_precision - + self.number_of_local_minima = 0 self.local_minima_parameters = [] self.local_minima_costs = [] self.local_minima_uncers = [] - + search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in self.all_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) curr_minima_params = result.x (curr_minima_cost,curr_minima_uncer) = self.gaussian_process.predict(curr_minima_params[np.newaxis,:],return_std=True) - if all( not np.all( np.abs(params - curr_minima_params) < self.minima_tolerance ) for params in self.local_minima_parameters): + if all( not np.all( np.abs(params - curr_minima_params) < self.minima_tolerance ) for params in self.local_minima_parameters): #Non duplicate point so add to the list self.number_of_local_minima += 1 self.local_minima_parameters.append(curr_minima_params) self.local_minima_costs.append(curr_minima_cost) self.local_minima_uncers.append(curr_minima_uncer) - + self.archive_dict.update({'number_of_local_minima':self.number_of_local_minima, 'local_minima_parameters':self.local_minima_parameters, 'local_minima_costs':self.local_minima_costs, 'local_minima_uncers':self.local_minima_uncers}) - + self.has_local_minima = True self.log.info('Search completed') @@ -1471,45 +1471,45 @@ def find_local_minima(self): class NeuralNetLearner(Learner, mp.Process): ''' Shell of Neural Net Learner. - + Args: params_out_queue (queue): Queue for parameters sent to controller. costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple end_event (event): Event to trigger end of learner. - + Keyword Args: length_scale (Optional [array]): The initial guess for length scale(s) of the gaussian process. The array can either of size one or the number of parameters or None. If it is size one, it is assumed all the correlation lengths are the same. If it is the number of the parameters then all the parameters have their own independent length scale. If it is None, it is assumed all the length scales should be independent and they are all given an initial value of 1. Default None. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. default_bad_cost (Optional [float]): If a run is reported as bad and default_bad_cost is provided, the cost for the bad run is set to this default value. If default_bad_cost is None, then the worst cost received is set to all the bad runs. Default None. default_bad_uncertainty (Optional [float]): If a run is reported as bad and default_bad_uncertainty is provided, the uncertainty for the bad run is set to this default value. If default_bad_uncertainty is None, then the uncertainty is set to a tenth of the best to worst cost range. Default None. minimum_uncertainty (Optional [float]): The minimum uncertainty associated with provided costs. Must be above zero to avoid fitting errors. Default 1e-8. predict_global_minima_at_end (Optional [bool]): If True finds the global minima when the learner is ended. Does not if False. Default True. predict_local_minima_at_end (Optional [bool]): If True finds the all minima when the learner is ended. Does not if False. Default False. - + Attributes: all_params (array): Array containing all parameters sent to learner. all_costs (array): Array containing all costs sent to learner. all_uncers (array): Array containing all uncertainties sent to learner. - scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. + scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. bad_run_indexs (list): list of indexes to all runs that were marked as bad. best_cost (float): Minimum received cost, updated during execution. best_params (array): Parameters of best run. (reference to element in params array). - best_index (int): index of the best cost and params. + best_index (int): index of the best cost and params. worst_cost (float): Maximum received cost, updated during execution. worst_index (int): index to run with worst cost. cost_range (float): Difference between worst_cost and best_cost generation_num (int): Number of sets of parameters to generate each generation. Set to 5. length_scale_history (list): List of length scales found after each fit. - noise_level_history (list): List of noise levels found after each fit. + noise_level_history (list): List of noise levels found after each fit. fit_count (int): Counter for the number of times the gaussian process has been fit. cost_count (int): Counter for the number of costs, parameters and uncertainties added to learner. - params_count (int): Counter for the number of parameters asked to be evaluated by the learner. + params_count (int): Counter for the number of parameters asked to be evaluated by the learner. gaussian_process (GaussianProcessRegressor): Gaussian process that is fitted to data and used to make predictions - cost_scaler (StandardScaler): Scaler used to normalize the provided costs. - has_trust_region (bool): Whether the learner has a trust region. - ''' - - def __init__(self, + cost_scaler (StandardScaler): Scaler used to normalize the provided costs. + has_trust_region (bool): Whether the learner has a trust region. + ''' + + def __init__(self, update_hyperparameters = True, trust_region=None, default_bad_cost = None, @@ -1519,11 +1519,11 @@ def __init__(self, predict_global_minima_at_end = True, predict_local_minima_at_end = False, **kwargs): - - - + + + super(NeuralNetLearner,self).__init__(**kwargs) - + #Storage variables, archived self.all_params = np.array([], dtype=float) self.all_costs = np.array([], dtype=float) @@ -1537,29 +1537,29 @@ def __init__(self, self.cost_range = float('inf') self.length_scale_history = [] self.noise_level_history = [] - + self.costs_count = 0 self.fit_count = 0 self.params_count = 0 - + self.has_local_minima = False self.has_global_minima = False - + #Multiprocessor controls self.new_params_event = mp.Event() - + #Storage variables and counters self.search_params = [] self.scaled_costs = None self.cost_bias = None self.uncer_bias = None - + #Constants, limits and tolerances self.search_precision = 1.0e-6 self.parameter_searches = max(10,self.num_params) self.hyperparameter_searches = max(10,self.num_params) - self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty - + self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty + #Optional user set variables self.update_hyperparameters = bool(update_hyperparameters) self.predict_global_minima_at_end = bool(predict_global_minima_at_end) @@ -1572,30 +1572,30 @@ def __init__(self, self.default_bad_uncertainty = float(default_bad_uncertainty) else: self.default_bad_uncertainty = None - + self._set_trust_region(trust_region) - + #Search bounds self.search_min = self.min_boundary self.search_max = self.max_boundary self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - + self.cost_scaler = skp.StandardScaler() - - + + #--- FAKE NN CONSTRUCTOR START ---# - + self.length_scale = 1 self.cost_has_noise = True self.noise_level = 1 - + self.create_nerual_net() - - - + + + #--- FAKE NN CONSTRUCTOR END ---# - + self.archive_dict.update({'archive_type':'nerual_net_learner', 'bad_run_indexs':self.bad_run_indexs, 'generation_num':self.generation_num, @@ -1607,54 +1607,54 @@ def __init__(self, 'has_trust_region':self.has_trust_region, 'predict_global_minima_at_end':self.predict_global_minima_at_end, 'predict_local_minima_at_end':self.predict_local_minima_at_end}) - + #Remove logger so gaussian process can be safely picked for multiprocessing on Windows self.log = None - - + + #--- FAKE NN METHODS START ---# - - + + def create_neural_net(self): ''' TO DO: Implement correctly - + Create the nerual net. - + ''' gp_kernel = skk.RBF(length_scale=self.length_scale) + skk.WhiteKernel(noise_level=self.noise_level) - + if self.update_hyperparameters: self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,n_restarts_optimizer=self.hyperparameter_searches) else: self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,optimizer=None) - + def fit_neural_net(self): ''' TO DO: Implement correctly - - Determine the appropriate number of layers for the NN given the data. - + + Determine the appropriate number of layers for the NN given the data. + Fit the Neural Net with the appropriate topology to the data - + ''' self.log.debug('Fitting Gaussian process.') if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') raise ValueError - + self.scaled_costs = self.cost_scaler.fit_transform(self.all_costs[:,np.newaxis])[:,0] self.scaled_uncers = self.all_uncers * self.cost_scaler.scale_ self.gaussian_process.alpha_ = self.scaled_uncers self.gaussian_process.fit(self.all_params,self.scaled_costs) - + if self.update_hyperparameters: - + self.fit_count += 1 self.gaussian_process.kernel = self.gaussian_process.kernel_ - + last_hyperparameters = self.gaussian_process.kernel.get_params() - + if self.cost_has_noise: self.length_scale = last_hyperparameters['k1__length_scale'] if isinstance(self.length_scale, float): @@ -1665,22 +1665,22 @@ def fit_neural_net(self): else: self.length_scale = last_hyperparameters['length_scale'] self.length_scale_history.append(self.length_scale) - + def predict_cost(self,params): ''' - Produces a prediction of cost from the gaussian process at params. - + Produces a prediction of cost from the gaussian process at params. + Returns: float : Predicted cost at paramters ''' return self.gaussian_process.predict(params[np.newaxis,:]) - + #--- FAKE NN CONSTRUCTOR END ---# - + def wait_for_new_params_event(self): ''' - Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. + Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. ''' while not self.end_event.is_set(): if self.new_params_event.wait(timeout=self.learner_wait): @@ -1691,26 +1691,26 @@ def wait_for_new_params_event(self): else: self.log.debug('GaussianProcessLearner end signal received. Ending') raise LearnerInterrupt - - + + def get_params_and_costs(self): ''' - Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. + Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. ''' if self.costs_in_queue.empty(): self.log.error('Gaussian process asked for new parameters but no new costs were provided.') raise ValueError - + new_params = [] new_costs = [] new_uncers = [] new_bads = [] update_bads_flag = False - + while not self.costs_in_queue.empty(): (param, cost, uncer, bad) = self.costs_in_queue.get_nowait() self.costs_count +=1 - + if bad: new_bads.append(self.costs_count-1) if self.bad_defaults_set: @@ -1719,7 +1719,7 @@ def get_params_and_costs(self): else: cost = self.worst_cost uncer = self.cost_range*self.bad_uncer_frac - + param = np.array(param, dtype=float) if not self.check_num_params(param): self.log.error('Incorrect number of parameters provided to Gaussian process learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) @@ -1730,7 +1730,7 @@ def get_params_and_costs(self): if uncer < 0: self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) uncer = max(float(uncer), self.minimum_uncertainty) - + cost_change_flag = False if cost > self.worst_cost: self.worst_cost = cost @@ -1745,12 +1745,12 @@ def get_params_and_costs(self): self.cost_range = self.worst_cost - self.best_cost if not self.bad_defaults_set: update_bads_flag = True - + new_params.append(param) new_costs.append(cost) new_uncers.append(uncer) - - + + if self.all_params.size==0: self.all_params = np.array(new_params, dtype=float) self.all_costs = np.array(new_costs, dtype=float) @@ -1759,21 +1759,21 @@ def get_params_and_costs(self): self.all_params = np.concatenate((self.all_params, np.array(new_params, dtype=float))) self.all_costs = np.concatenate((self.all_costs, np.array(new_costs, dtype=float))) self.all_uncers = np.concatenate((self.all_uncers, np.array(new_uncers, dtype=float))) - + self.bad_run_indexs.append(new_bads) - + if self.all_params.shape != (self.costs_count,self.num_params): self.log('Saved NN params are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_params)) if self.all_costs.shape != (self.costs_count,): self.log('Saved NN costs are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_costs)) if self.all_uncers.shape != (self.costs_count,): self.log('Saved NN uncertainties are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_uncers)) - + if update_bads_flag: self.update_bads() - + self.update_search_region() - + def update_bads(self): ''' Best and/or worst costs have changed, update the values associated with bad runs accordingly. @@ -1781,7 +1781,7 @@ def update_bads(self): for index in self.bad_run_indexs: self.all_costs[index] = self.worst_cost self.all_uncers[index] = self.cost_range*self.bad_uncer_frac - + def update_search_region(self): ''' If trust boundaries is not none, updates the search boundaries based on the defined trust region. @@ -1791,7 +1791,7 @@ def update_search_region(self): self.search_max = np.minimum(self.best_params + self.trust_region, self.max_boundary) self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - + def update_search_params(self): ''' Update the list of parameters to use for the next search. @@ -1800,7 +1800,7 @@ def update_search_params(self): self.search_params.append(self.best_params) for _ in range(self.parameter_searches): self.search_params.append(self.search_min + nr.uniform(size=self.num_params) * self.search_diff) - + def update_archive(self): ''' Update the archive. @@ -1817,12 +1817,12 @@ def update_archive(self): 'fit_count':self.fit_count, 'costs_count':self.costs_count, 'params_count':self.params_count, - 'update_hyperparameters':self.update_hyperparameters}) + 'update_hyperparameters':self.update_hyperparameters}) def find_next_parameters(self): ''' Returns next parameters to find. Increments counters and bias function appropriately. - + Return: next_params (array): Returns next parameters from biased cost search. ''' @@ -1837,7 +1837,7 @@ def find_next_parameters(self): next_params = result.x next_cost = result.fun return next_params - + def run(self): ''' Starts running the Gaussian process learner. When the new parameters event is triggered, reads the cost information provided and updates the Gaussian process with the information. Then searches the Gaussian process for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. @@ -1845,7 +1845,7 @@ def run(self): #logging to the main log file from a process (as apposed to a thread) in cpython is currently buggy on windows and/or python 2.7 #current solution is to only log to the console for warning and above from a process self.log = mp.log_to_stderr(logging.WARNING) - + try: while not self.end_event.is_set(): #self.log.debug('Learner waiting for new params event') @@ -1879,27 +1879,27 @@ def run(self): self.params_out_queue.put(end_dict) self._shut_down() self.log.debug('Ended Gaussian Process Learner') - + def find_global_minima(self): ''' Performs a quick search for the predicted global minima from the learner. Does not return any values, but creates the following attributes. - + Attributes: predicted_best_parameters (array): the parameters for the predicted global minima predicted_best_cost (float): the cost at the predicted global minima predicted_best_uncertainty (float): the uncertainty of the predicted global minima ''' self.log.debug('Started search for predicted global minima.') - + self.predicted_best_parameters = None self.predicted_best_scaled_cost = float('inf') self.predicted_best_scaled_uncertainty = None - + search_params = [] search_params.append(self.best_params) for _ in range(self.parameter_searches): search_params.append(self.min_boundary + nr.uniform(size=self.num_params) * self.diff_boundary) - + search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in search_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) @@ -1909,55 +1909,55 @@ def find_global_minima(self): self.predicted_best_parameters = curr_best_params self.predicted_best_scaled_cost = curr_best_cost self.predicted_best_scaled_uncertainty = curr_best_uncer - + self.predicted_best_cost = self.cost_scaler.inverse_transform(self.predicted_best_scaled_cost) self.predicted_best_uncertainty = self.predicted_best_scaled_uncertainty / self.cost_scaler.scale_ - + self.archive_dict.update({'predicted_best_parameters':self.predicted_best_parameters, 'predicted_best_scaled_cost':self.predicted_best_scaled_cost, 'predicted_best_scaled_uncertainty':self.predicted_best_scaled_uncertainty, 'predicted_best_cost':self.predicted_best_cost, 'predicted_best_uncertainty':self.predicted_best_uncertainty}) - + self.has_global_minima = True - self.log.debug('Predicted global minima found.') - + self.log.debug('Predicted global minima found.') + def find_local_minima(self): ''' - Performs a comprehensive search of the learner for all predicted local minima (and hence the global as well) in the landscape. Note, this can take a very long time. - + Performs a comprehensive search of the learner for all predicted local minima (and hence the global as well) in the landscape. Note, this can take a very long time. + Attributes: local_minima_parameters (list): list of all the parameters for local minima. local_minima_costs (list): list of all the costs at local minima. local_minima_uncers (list): list of all the uncertainties at local minima. - + ''' self.log.info('Searching for all minima.') - + self.minima_tolerance = 10*self.search_precision - + self.number_of_local_minima = 0 self.local_minima_parameters = [] self.local_minima_costs = [] self.local_minima_uncers = [] - + search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in self.all_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) curr_minima_params = result.x (curr_minima_cost,curr_minima_uncer) = self.gaussian_process.predict(curr_minima_params[np.newaxis,:],return_std=True) - if all( not np.all( np.abs(params - curr_minima_params) < self.minima_tolerance ) for params in self.local_minima_parameters): + if all( not np.all( np.abs(params - curr_minima_params) < self.minima_tolerance ) for params in self.local_minima_parameters): #Non duplicate point so add to the list self.number_of_local_minima += 1 self.local_minima_parameters.append(curr_minima_params) self.local_minima_costs.append(curr_minima_cost) self.local_minima_uncers.append(curr_minima_uncer) - + self.archive_dict.update({'number_of_local_minima':self.number_of_local_minima, 'local_minima_parameters':self.local_minima_parameters, 'local_minima_costs':self.local_minima_costs, 'local_minima_uncers':self.local_minima_uncers}) - + self.has_local_minima = True self.log.info('Search completed') From 326f98bc4af455385f9dcf21f48c2fcf84dab19a Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 25 Nov 2016 17:13:35 +1100 Subject: [PATCH 09/33] Fix some minor controller documentation errors --- mloop/controllers.py | 62 +++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index 3777c4d..3ab7d0b 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -11,7 +11,7 @@ import logging import os -controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4} +controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4,'neural_net':5} number_of_controllers = 4 default_controller_archive_filename = 'controller_archive' default_controller_archive_file_type = 'txt' @@ -33,7 +33,7 @@ def create_controller(interface, interface (interface): Interface with queues and events to be passed to controller Keyword Args: - controller_type (Optional [str]): Defines the type of controller can be 'random', 'nelder' or 'gaussian_process'. Defaults to 'gaussian_process'. + controller_type (Optional [str]): Defines the type of controller can be 'random', 'nelder', 'gaussian_process' or 'neural_net'. Defaults to 'gaussian_process'. **controller_config_dict : Options to be passed to controller. Returns: @@ -529,12 +529,12 @@ class MachineLearnerController(Controller): Args: interface (Interface): The interface to the experiment under optimization. - **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. - + **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class and initial training learner. + Keyword Args: - initial_training_source (Optional [string]): The type for the initial training source can be 'random' for the random learner or 'nelder_mead' for the Nelder-Mead learner. This leaner is also called if the Gaussian process learner is too slow and a new point is needed. Default 'random'. - num_training_runs (Optional [int]): The number of training runs to before starting the learner. If None, will by ten or double the number of parameters, whatever is larger. - no_delay (Optional [bool]): If True, there is never any delay between a returned cost and the next parameters to run for the experiment. In practice, this means if the gaussian process has not prepared the next parameters in time the learner defined by the initial training source is used instead. If false, the controller will wait for the gaussian process to predict the next parameters and there may be a delay between runs. + training_type (Optional [string]): The type for the initial training source can be 'random' for the random learner, 'nelder_mead' for the Nelder-Mead learner or 'differential_evolution' for the Differential Evolution learner. This learner is also called if the machine learning learner is too slow and a new point is needed. Default 'differential_evolution'. + num_training_runs (Optional [int]): The number of training runs to before starting the learner. If None, will be ten or double the number of parameters, whatever is larger. + no_delay (Optional [bool]): If True, there is never any delay between a returned cost and the next parameters to run for the experiment. In practice, this means if the machine learning learner has not prepared the next parameters in time the learner defined by the initial training source is used instead. If false, the controller will wait for the machine learning learner to predict the next parameters and there may be a delay between runs. ''' def __init__(self, interface, @@ -599,8 +599,8 @@ def __init__(self, interface, **self.remaining_kwargs) else: - self.log.error('Unknown training type provided to Gaussian process controller:' + repr(training_type)) - + self.log.error('Unknown training type provided to machine learning controller:' + repr(training_type)) + self.archive_dict.update({'training_type':self.training_type}) self._update_controller_with_learner_attributes() @@ -624,8 +624,8 @@ def _put_params_and_out_dict(self, params): def _get_cost_and_in_dict(self): ''' - Call _get_cost_and_in_dict() of parent Controller class. But also sends cost to Gaussian process learner and saves the cost if the parameters came from a trainer. - + Call _get_cost_and_in_dict() of parent Controller class. But also sends cost to machine learning learner and saves the cost if the parameters came from a trainer. + ''' super(MachineLearnerController,self)._get_cost_and_in_dict() if self.last_training_run_flag: @@ -664,12 +664,12 @@ def _start_up(self): Runs pararent method and also starts training_learner. ''' super(MachineLearnerController,self)._start_up() - self.log.debug('GP learner started.') + self.log.debug('ML learner started.') self.ml_learner.start() def _optimization_routine(self): ''' - Overrides _optimization_routine. Uses the parent routine for the training runs. Implements a customized _optimization_rountine when running the Gaussian Process learner. + Overrides _optimization_routine. Uses the parent routine for the training runs. Implements a customized _optimization_routine when running the machine learning learner. ''' #Run the training runs using the standard optimization routine. self.log.debug('Starting training optimization.') @@ -690,8 +690,8 @@ def _optimization_routine(self): self.log.info('Run:' + str(self.num_in_costs +1)) next_params = self._next_params() self._put_params_and_out_dict(next_params) - - self.log.debug('Starting GP optimization.') + + self.log.debug('Starting ML optimization.') self.new_params_event.set() self.save_archive() self._get_cost_and_in_dict() @@ -708,26 +708,26 @@ def _optimization_routine(self): ml_consec = 0 else: next_params = self.ml_learner_params_queue.get() - super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type='gaussian_process') + super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type=self.ml_learner_name) ml_consec += 1 ml_count += 1 - + if ml_count%self.generation_num == 2: self.new_params_event.set() - + self.save_archive() self._get_cost_and_in_dict() - + def _shut_down(self): ''' - Shutdown and clean up resources of the Gaussian process controller. + Shutdown and clean up resources of the machine learning controller. ''' - self.log.debug('GP learner end set.') + self.log.debug('ML learner end set.') self.end_ml_learner.set() self.ml_learner.join() - - self.log.debug('GP learner joined') + + self.log.debug('ML learner joined') last_dict = None while not self.ml_learner_params_queue.empty(): last_dict = self.ml_learner_params_queue.get_nowait() @@ -748,7 +748,7 @@ def _shut_down(self): self.archive_dict.update(last_dict) else: if self.ml_learner.predict_global_minima_at_end or self.ml_learner.predict_local_minima_at_end: - self.log.info('Machine Learner did not provide best and/or all minima.') + self.log.info('Machine learning learner did not provide best and/or all minima.') super(MachineLearnerController,self)._shut_down() def print_results(self): @@ -773,8 +773,8 @@ class GaussianProcessController(MachineLearnerController): Args: interface (Interface): The interface to the experiment under optimization. - **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. - + **kwargs (Optional [dict]): Dictionary of options to be passed to MachineLearnerController parent class and Gaussian Process learner. + Keyword Args: ''' @@ -797,6 +797,7 @@ def __init__(self, interface, learner_archive_file_type=learner_archive_file_type, **kwargs) + self.ml_learner_name = 'gaussian_process' self.ml_learner = mll.GaussianProcessLearner(start_datetime=self.start_datetime, num_params=num_params, min_boundary=min_boundary, @@ -814,8 +815,8 @@ class NeuralNetController(MachineLearnerController): Args: interface (Interface): The interface to the experiment under optimization. - **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. - + **kwargs (Optional [dict]): Dictionary of options to be passed to MachineLearnerController parent class and Neural Net learner. + Keyword Args: ''' @@ -828,8 +829,8 @@ def __init__(self, interface, learner_archive_filename = mll.default_learner_archive_filename, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): - - super(GaussianProcessController,self).__init__(interface, + + super(NeuralNetController,self).__init__(interface, num_params=num_params, min_boundary=min_boundary, max_boundary=max_boundary, @@ -838,6 +839,7 @@ def __init__(self, interface, learner_archive_file_type=learner_archive_file_type, **kwargs) + self.ml_learner_name = 'neural_net' self.ml_learner = mll.NeuralNetLearner(start_datetime=self.start_datetime, num_params=num_params, min_boundary=min_boundary, From 635a5f787f4abb815a375035291cbc9c324669b8 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 25 Nov 2016 18:43:21 +1100 Subject: [PATCH 10/33] Tweaks to NN learner shell --- mloop/learners.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index a0f3572..8810f76 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1675,7 +1675,7 @@ def predict_cost(self,params): ''' return self.gaussian_process.predict(params[np.newaxis,:]) - #--- FAKE NN CONSTRUCTOR END ---# + #--- FAKE NN METHODS END ---# def wait_for_new_params_event(self): @@ -1853,7 +1853,7 @@ def run(self): self.wait_for_new_params_event() #self.log.debug('Gaussian process learner reading costs') self.get_params_and_costs() - self.fit_gaussian_process() + self.fit_neural_net() for _ in range(self.generation_num): self.log.debug('Gaussian process learner generating parameter:'+ str(self.params_count+1)) next_params = self.find_next_parameters() @@ -1864,7 +1864,7 @@ def run(self): pass if self.predict_global_minima_at_end or self.predict_local_minima_at_end: self.get_params_and_costs() - self.fit_gaussian_process() + self.fit_neural_net() end_dict = {} if self.predict_global_minima_at_end: self.find_global_minima() @@ -1904,6 +1904,7 @@ def find_global_minima(self): for start_params in search_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) curr_best_params = result.x + # TODO: Doesn't apply to NN (curr_best_cost,curr_best_uncer) = self.gaussian_process.predict(curr_best_params[np.newaxis,:],return_std=True) if curr_best_cost Date: Fri, 25 Nov 2016 19:12:32 +1100 Subject: [PATCH 11/33] Remove unnecessary uncertainty stuff from NNL --- mloop/learners.py | 57 +++++++++++++++++++++---------------------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 8810f76..5f92b85 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1617,11 +1617,10 @@ def __init__(self, def create_neural_net(self): ''' - TO DO: Implement correctly - Create the nerual net. ''' + #TODO: Do. gp_kernel = skk.RBF(length_scale=self.length_scale) + skk.WhiteKernel(noise_level=self.noise_level) if self.update_hyperparameters: @@ -1631,13 +1630,12 @@ def create_neural_net(self): def fit_neural_net(self): ''' - TO DO: Implement correctly - Determine the appropriate number of layers for the NN given the data. Fit the Neural Net with the appropriate topology to the data ''' + #TODO: Do. self.log.debug('Fitting Gaussian process.') if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') @@ -1673,6 +1671,7 @@ def predict_cost(self,params): Returns: float : Predicted cost at paramters ''' + #TODO: Do. return self.gaussian_process.predict(params[np.newaxis,:]) #--- FAKE NN METHODS END ---# @@ -1689,7 +1688,7 @@ def wait_for_new_params_event(self): else: continue else: - self.log.debug('GaussianProcessLearner end signal received. Ending') + self.log.debug('NeuralNetLearner end signal received. Ending') raise LearnerInterrupt @@ -1698,7 +1697,7 @@ def get_params_and_costs(self): Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. ''' if self.costs_in_queue.empty(): - self.log.error('Gaussian process asked for new parameters but no new costs were provided.') + self.log.error('Neural network asked for new parameters but no new costs were provided.') raise ValueError new_params = [] @@ -1722,10 +1721,10 @@ def get_params_and_costs(self): param = np.array(param, dtype=float) if not self.check_num_params(param): - self.log.error('Incorrect number of parameters provided to Gaussian process learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) + self.log.error('Incorrect number of parameters provided to neural network learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) raise ValueError if not self.check_in_boundary(param): - self.log.warning('Parameters provided to Gaussian process learner not in boundaries:' + repr(param)) + self.log.warning('Parameters provided to neural network learner not in boundaries:' + repr(param)) cost = float(cost) if uncer < 0: self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) @@ -1821,18 +1820,18 @@ def update_archive(self): def find_next_parameters(self): ''' - Returns next parameters to find. Increments counters and bias function appropriately. + Returns next parameters to find. Increments counters appropriately. Return: - next_params (array): Returns next parameters from biased cost search. + next_params (array): Returns next parameters from cost search. ''' + # TODO: We could implement some other type of biasing. self.params_count += 1 - self.update_bias_function() self.update_search_params() next_params = None next_cost = float('inf') for start_params in self.search_params: - result = so.minimize(self.predict_biased_cost, start_params, bounds = self.search_region, tol=self.search_precision) + result = so.minimize(self.predict_cost, start_params, bounds = self.search_region, tol=self.search_precision) if result.fun < next_cost: next_params = result.x next_cost = result.fun @@ -1840,7 +1839,7 @@ def find_next_parameters(self): def run(self): ''' - Starts running the Gaussian process learner. When the new parameters event is triggered, reads the cost information provided and updates the Gaussian process with the information. Then searches the Gaussian process for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. + Starts running the neural network learner. When the new parameters event is triggered, reads the cost information provided and updates the neural network with the information. Then searches the neural network for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. ''' #logging to the main log file from a process (as apposed to a thread) in cpython is currently buggy on windows and/or python 2.7 #current solution is to only log to the console for warning and above from a process @@ -1855,7 +1854,7 @@ def run(self): self.get_params_and_costs() self.fit_neural_net() for _ in range(self.generation_num): - self.log.debug('Gaussian process learner generating parameter:'+ str(self.params_count+1)) + self.log.debug('Neural network learner generating parameter:'+ str(self.params_count+1)) next_params = self.find_next_parameters() self.params_out_queue.put(next_params) if self.end_event.is_set(): @@ -1869,16 +1868,14 @@ def run(self): if self.predict_global_minima_at_end: self.find_global_minima() end_dict.update({'predicted_best_parameters':self.predicted_best_parameters, - 'predicted_best_cost':self.predicted_best_cost, - 'predicted_best_uncertainty':self.predicted_best_uncertainty}) + 'predicted_best_cost':self.predicted_best_cost}) if self.predict_local_minima_at_end: self.find_local_minima() end_dict.update({'local_minima_parameters':self.local_minima_parameters, - 'local_minima_costs':self.local_minima_costs, - 'local_minima_uncers':self.local_minima_uncers}) + 'local_minima_costs':self.local_minima_costs}) self.params_out_queue.put(end_dict) self._shut_down() - self.log.debug('Ended Gaussian Process Learner') + self.log.debug('Ended neural network learner') def find_global_minima(self): ''' @@ -1887,13 +1884,11 @@ def find_global_minima(self): Attributes: predicted_best_parameters (array): the parameters for the predicted global minima predicted_best_cost (float): the cost at the predicted global minima - predicted_best_uncertainty (float): the uncertainty of the predicted global minima ''' self.log.debug('Started search for predicted global minima.') self.predicted_best_parameters = None self.predicted_best_scaled_cost = float('inf') - self.predicted_best_scaled_uncertainty = None search_params = [] search_params.append(self.best_params) @@ -1902,23 +1897,19 @@ def find_global_minima(self): search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in search_params: + # TODO: Take advantage of the fact that we get the gradient for free, so can use that to speed up the minimizer. result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) curr_best_params = result.x - # TODO: Doesn't apply to NN - (curr_best_cost,curr_best_uncer) = self.gaussian_process.predict(curr_best_params[np.newaxis,:],return_std=True) + curr_best_cost = result.fun if curr_best_cost Date: Thu, 1 Dec 2016 10:50:26 +1100 Subject: [PATCH 12/33] Added visualization introduced bug Visualizations now work for NN and GP learners. Mysterious bug has appeared in GP. The scikit-learn stops providing uncertainty predictions after being fit for a certain number of times. Commiting so I can change branch and investigate. --- mloop/controllers.py | 45 +++++++---- mloop/learners.py | 193 ++++++++++++++++++++++++++++++++++++------------ mloop/utilities.py | 16 ++++ mloop/visualizations.py | 144 +++++++++++++++++++++++++++++++++++- 4 files changed, 335 insertions(+), 63 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index ca2035a..4352a40 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -11,8 +11,8 @@ import logging import os -controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4} -number_of_controllers = 4 +controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4,'neural_net':5} +number_of_controllers = 5 default_controller_archive_filename = 'controller_archive' default_controller_archive_file_type = 'txt' @@ -338,6 +338,7 @@ def optimize(self): log.info('Controller finished. Closing down M-LOOP. Please wait a moment...') except ControllerInterrupt: self.log.warning('Controller ended by interruption.') + ''' except (KeyboardInterrupt,SystemExit): log.warning('!!! Do not give the interrupt signal again !!! \n M-LOOP stopped with keyboard interupt or system exit. Please wait at least 1 minute for the threads to safely shut down. \n ') log.warning('Closing down controller.') @@ -347,6 +348,7 @@ def optimize(self): self.log.warning('Safely shut down. Below are results found before exception.') self.print_results() raise + ''' self._shut_down() self.print_results() self.log.info('M-LOOP Done.') @@ -539,6 +541,7 @@ class MachineLearnerController(Controller): def __init__(self, interface, training_type='differential_evolution', + machine_learner_type='machine_learner', num_training_runs=None, no_delay=True, num_params=None, @@ -551,6 +554,8 @@ def __init__(self, interface, super(MachineLearnerController,self).__init__(interface, **kwargs) + self.machine_learner_type = machine_learner_type + self.last_training_cost = None self.last_training_bad = None self.last_training_run_flag = False @@ -678,13 +683,14 @@ def _optimization_routine(self): self._put_params_and_out_dict(next_params) self.save_archive() self._get_cost_and_in_dict() + while (self.num_in_costs < self.num_training_runs) and self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) next_params = self._next_params() self._put_params_and_out_dict(next_params) self.save_archive() self._get_cost_and_in_dict() - + if self.check_end_conditions(): #Start last training run self.log.info('Run:' + str(self.num_in_costs +1)) @@ -701,19 +707,26 @@ def _optimization_routine(self): ml_count = 0 while self.check_end_conditions(): + print('1-1.') self.log.info('Run:' + str(self.num_in_costs +1)) if ml_consec==self.generation_num or (self.no_delay and self.ml_learner_params_queue.empty()): + print('1-2.') next_params = self._next_params() + print('1-3.') self._put_params_and_out_dict(next_params) ml_consec = 0 else: + print('1-4.') next_params = self.ml_learner_params_queue.get() - super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type='gaussian_process') + print('1-5.') + super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type=self.machine_learner_type) ml_consec += 1 ml_count += 1 - - if ml_count%self.generation_num == 2: + if ml_count==self.generation_num: + print('1-6.') self.new_params_event.set() + ml_count = 0 + self.save_archive() self._get_cost_and_in_dict() @@ -789,6 +802,7 @@ def __init__(self, interface, **kwargs): super(GaussianProcessController,self).__init__(interface, + machine_learner_type='gaussian_process', num_params=num_params, min_boundary=min_boundary, max_boundary=max_boundary, @@ -829,15 +843,16 @@ def __init__(self, interface, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): - super(GaussianProcessController,self).__init__(interface, - num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - trust_region=trust_region, - learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **kwargs) - + super(NeuralNetController,self).__init__(interface, + machine_learner_type='neural_net', + num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + trust_region=trust_region, + learner_archive_filename=learner_archive_filename, + learner_archive_file_type=learner_archive_file_type, + **kwargs) + self.ml_learner = mll.NeuralNetLearner(start_datetime=self.start_datetime, num_params=num_params, min_boundary=min_boundary, diff --git a/mloop/learners.py b/mloop/learners.py index ed15093..9aefa99 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -91,6 +91,7 @@ def __init__(self, self.min_boundary = np.full((self.num_params,), -1.0) else: self.min_boundary = np.array(min_boundary, dtype=np.float) + if self.min_boundary.shape != (self.num_params,): self.log.error('min_boundary array the wrong shape:' + repr(self.min_boundary.shape)) raise ValueError @@ -130,6 +131,9 @@ def __init__(self, self.log.debug('Learner init completed.') + + + def check_num_params(self,param): ''' Check the number of parameters is right. @@ -918,12 +922,12 @@ def __init__(self, #Basic optimization settings num_params = int(self.training_dict['num_params']) - min_boundary = np.squeeze(np.array(self.training_dict['min_boundary'], dtype=float)) - max_boundary = np.squeeze(np.array(self.training_dict['max_boundary'], dtype=float)) + min_boundary = mlu.safe_cast_to_list(self.training_dict['min_boundary']) + max_boundary = mlu.safe_cast_to_list(self.training_dict['max_boundary']) #Configuration of the learner self.cost_has_noise = bool(self.training_dict['cost_has_noise']) - self.length_scale = np.squeeze(np.array(self.training_dict['length_scale'])) + self.length_scale = mlu.safe_squeeze(self.training_dict['length_scale']) self.length_scale_history = list(self.training_dict['length_scale_history']) self.noise_level = float(self.training_dict['noise_level']) self.noise_level_history = mlu.safe_cast_to_list(self.training_dict['noise_level_history']) @@ -935,20 +939,20 @@ def __init__(self, #Data from previous experiment self.all_params = np.array(self.training_dict['all_params'], dtype=float) - self.all_costs = np.squeeze(np.array(self.training_dict['all_costs'], dtype=float)) - self.all_uncers = np.squeeze(np.array(self.training_dict['all_uncers'], dtype=float)) + self.all_costs = mlu.safe_squeeze(self.training_dict['all_costs']) + self.all_uncers = mlu.safe_squeeze(self.training_dict['all_uncers']) self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) #Derived properties self.best_cost = float(self.training_dict['best_cost']) - self.best_params = np.squeeze(np.array(self.training_dict['best_params'], dtype=float)) + self.best_params = mlu.safe_squeeze(self.training_dict['best_params']) self.best_index = int(self.training_dict['best_index']) self.worst_cost = float(self.training_dict['worst_cost']) self.worst_index = int(self.training_dict['worst_index']) self.cost_range = float(self.training_dict['cost_range']) try: - self.predicted_best_parameters = np.squeeze(np.array(self.training_dict['predicted_best_parameters'])) + self.predicted_best_parameters = mlu.safe_squeeze(self.training_dict['predicted_best_parameters']) self.predicted_best_cost = float(self.training_dict['predicted_best_cost']) self.predicted_best_uncertainty = float(self.training_dict['predicted_best_uncertainty']) self.has_global_minima = True @@ -969,7 +973,6 @@ def __init__(self, self.has_local_minima = True except KeyError: self.has_local_minima = False - super(GaussianProcessLearner,self).__init__(num_params=num_params, min_boundary=min_boundary, @@ -1024,9 +1027,6 @@ def __init__(self, self.bias_func_cost_factor = [1.0,1.0,1.0,1.0] self.bias_func_uncer_factor =[0.0,1.0,2.0,3.0] self.generation_num = self.bias_func_cycle - if self.generation_num < 3: - self.log.error('Number in generation must be larger than 2.') - raise ValueError #Constants, limits and tolerances self.search_precision = 1.0e-6 @@ -1188,7 +1188,7 @@ def get_params_and_costs(self): self.cost_range = self.worst_cost - self.best_cost if not self.bad_defaults_set: update_bads_flag = True - + new_params.append(param) new_costs.append(cost) new_uncers.append(uncer) @@ -1263,13 +1263,13 @@ def update_archive(self): 'update_hyperparameters':self.update_hyperparameters, 'length_scale':self.length_scale, 'noise_level':self.noise_level}) - - + def fit_gaussian_process(self): ''' Fit the Gaussian process to the current data ''' + print('3-1') self.log.debug('Fitting Gaussian process.') if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') @@ -1279,6 +1279,7 @@ def fit_gaussian_process(self): self.gaussian_process.alpha_ = self.scaled_uncers self.gaussian_process.fit(self.all_params,self.scaled_costs) + print('3-2') if self.update_hyperparameters: self.fit_count += 1 @@ -1296,6 +1297,9 @@ def fit_gaussian_process(self): else: self.length_scale = last_hyperparameters['length_scale'] self.length_scale_history.append(self.length_scale) + print('3-3') + print(repr(self.length_scale)) + print(repr(self.noise_level)) def update_bias_function(self): @@ -1313,7 +1317,10 @@ def predict_biased_cost(self,params): Returns: pred_bias_cost (float): Biased cost predicted at the given parameters ''' + #print('2-8-1-1.') + #(pred_cost, pred_uncer) = (self.gaussian_process.predict(params[np.newaxis,:]), 0.1) (pred_cost, pred_uncer) = self.gaussian_process.predict(params[np.newaxis,:], return_std=True) + #print('2-8-1-2.') return self.cost_bias*pred_cost - self.uncer_bias*pred_uncer def find_next_parameters(self): @@ -1324,15 +1331,20 @@ def find_next_parameters(self): next_params (array): Returns next parameters from biased cost search. ''' self.params_count += 1 + print('2-6.') self.update_bias_function() self.update_search_params() next_params = None next_cost = float('inf') + print('2-7.') for start_params in self.search_params: + print('2-8-1.') result = so.minimize(self.predict_biased_cost, start_params, bounds = self.search_region, tol=self.search_precision) + print('2-8-2.') if result.fun < next_cost: next_params = result.x next_cost = result.fun + print('2-9.') return next_params def run(self): @@ -1347,13 +1359,18 @@ def run(self): while not self.end_event.is_set(): #self.log.debug('Learner waiting for new params event') self.save_archive() + print('2-1.') self.wait_for_new_params_event() #self.log.debug('Gaussian process learner reading costs') + print('2-2.') self.get_params_and_costs() + print('2-4.') self.fit_gaussian_process() for _ in range(self.generation_num): + print('2-5.') self.log.debug('Gaussian process learner generating parameter:'+ str(self.params_count+1)) next_params = self.find_next_parameters() + print('2-10.') self.params_out_queue.put(next_params) if self.end_event.is_set(): raise LearnerInterrupt() @@ -1520,41 +1537,106 @@ def __init__(self, predict_local_minima_at_end = False, **kwargs): - + if nn_training_filename is not None: + + nn_training_filename = str(nn_training_filename) + nn_training_file_type = str(nn_training_file_type) + if not mlu.check_file_type_supported(nn_training_file_type): + self.log.error('GP training file type not supported' + repr(nn_training_file_type)) + + self.training_dict = mlu.get_dict_from_file(nn_training_filename, nn_training_file_type) + + #Basic optimization settings + num_params = int(self.training_dict['num_params']) + min_boundary = mlu.safe_cast_to_list(self.training_dict['min_boundary']) + max_boundary = mlu.safe_cast_to_list(self.training_dict['max_boundary']) + + #Counters + self.costs_count = int(self.training_dict['costs_count']) + self.fit_count = int(self.training_dict['fit_count']) + self.params_count = int(self.training_dict['params_count']) - super(NeuralNetLearner,self).__init__(**kwargs) - - #Storage variables, archived - self.all_params = np.array([], dtype=float) - self.all_costs = np.array([], dtype=float) - self.all_uncers = np.array([], dtype=float) - self.bad_run_indexs = [] - self.best_cost = float('inf') - self.best_params = float('nan') - self.best_index = 0 - self.worst_cost = float('-inf') - self.worst_index = 0 - self.cost_range = float('inf') - self.length_scale_history = [] - self.noise_level_history = [] - - self.costs_count = 0 - self.fit_count = 0 - self.params_count = 0 - - self.has_local_minima = False - self.has_global_minima = False + #Data from previous experiment + self.all_params = np.array(self.training_dict['all_params'], dtype=float) + self.all_costs = mlu.safe_squeeze(self.training_dict['all_costs']) + self.all_uncers = mlu.safe_squeeze(self.training_dict['all_uncers']) + + self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) + #Derived properties + self.best_cost = float(self.training_dict['best_cost']) + self.best_params = mlu.safe_squeeze(self.training_dict['best_params']) + self.best_index = int(self.training_dict['best_index']) + self.worst_cost = float(self.training_dict['worst_cost']) + self.worst_index = int(self.training_dict['worst_index']) + self.cost_range = float(self.training_dict['cost_range']) + + #Configuration of the fake neural net learner + self.length_scale = mlu.safe_squeeze(self.training_dict['length_scale']) + self.noise_level = float(self.training_dict['noise_level']) + + + try: + self.predicted_best_parameters = mlu.safe_squeeze(self.training_dict['predicted_best_parameters']) + self.predicted_best_cost = float(self.training_dict['predicted_best_cost']) + self.predicted_best_uncertainty = float(self.training_dict['predicted_best_uncertainty']) + self.has_global_minima = True + except KeyError: + self.has_global_minima = False + try: + self.local_minima_parameters = list(self.training_dict['local_minima_parameters']) + + if isinstance(self.training_dict['local_minima_costs'], np.ndarray): + self.local_minima_costs = list(np.squeeze(self.training_dict['local_minima_costs'])) + else: + self.local_minima_costs = list(self.training_dict['local_minima_costs']) + if isinstance(self.training_dict['local_minima_uncers'], np.ndarray): + self.local_minima_uncers = list(np.squeeze(self.training_dict['local_minima_uncers'])) + else: + self.local_minima_uncers = list(self.training_dict['local_minima_uncers']) + + self.has_local_minima = True + except KeyError: + self.has_local_minima = False + + super(NeuralNetLearner,self).__init__(num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + **kwargs) + else: + + super(NeuralNetLearner,self).__init__(**kwargs) + + #Storage variables, archived + self.all_params = np.array([], dtype=float) + self.all_costs = np.array([], dtype=float) + self.all_uncers = np.array([], dtype=float) + self.bad_run_indexs = [] + self.best_cost = float('inf') + self.best_params = float('nan') + self.best_index = 0 + self.worst_cost = float('-inf') + self.worst_index = 0 + self.cost_range = float('inf') + self.length_scale_history = [] + self.noise_level_history = [] + + self.costs_count = 0 + self.fit_count = 0 + self.params_count = 0 + + self.has_local_minima = False + self.has_global_minima = False + #Multiprocessor controls self.new_params_event = mp.Event() #Storage variables and counters self.search_params = [] self.scaled_costs = None - self.cost_bias = None - self.uncer_bias = None #Constants, limits and tolerances + self.generation_num = 1 self.search_precision = 1.0e-6 self.parameter_searches = max(10,self.num_params) self.hyperparameter_searches = max(10,self.num_params) @@ -1572,6 +1654,13 @@ def __init__(self, self.default_bad_uncertainty = float(default_bad_uncertainty) else: self.default_bad_uncertainty = None + if (self.default_bad_cost is None) and (self.default_bad_uncertainty is None): + self.bad_defaults_set = False + elif (self.default_bad_cost is not None) and (self.default_bad_uncertainty is not None): + self.bad_defaults_set = True + else: + self.log.error('Both the default cost and uncertainty must be set for a bad run or they must both be set to None.') + raise ValueError self._set_trust_region(trust_region) @@ -1590,7 +1679,7 @@ def __init__(self, self.cost_has_noise = True self.noise_level = 1 - self.create_nerual_net() + self.create_neural_net() @@ -1638,6 +1727,7 @@ def fit_neural_net(self): Fit the Neural Net with the appropriate topology to the data ''' + self.log.debug('Fitting Gaussian process.') if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') @@ -1674,7 +1764,17 @@ def predict_cost(self,params): float : Predicted cost at paramters ''' return self.gaussian_process.predict(params[np.newaxis,:]) - + + + def predict_costs_from_param_array(self,params): + ''' + Produces a prediction of costs from an array of params. + + Returns: + float : Predicted cost at paramters + ''' + return self.gaussian_process.predict(params) + #--- FAKE NN CONSTRUCTOR END ---# @@ -1817,7 +1917,9 @@ def update_archive(self): 'fit_count':self.fit_count, 'costs_count':self.costs_count, 'params_count':self.params_count, - 'update_hyperparameters':self.update_hyperparameters}) + 'update_hyperparameters':self.update_hyperparameters, + 'length_scale':self.length_scale, + 'noise_level':self.noise_level}) def find_next_parameters(self): ''' @@ -1827,12 +1929,11 @@ def find_next_parameters(self): next_params (array): Returns next parameters from biased cost search. ''' self.params_count += 1 - self.update_bias_function() self.update_search_params() next_params = None next_cost = float('inf') for start_params in self.search_params: - result = so.minimize(self.predict_biased_cost, start_params, bounds = self.search_region, tol=self.search_precision) + result = so.minimize(self.predict_cost, start_params, bounds = self.search_region, tol=self.search_precision) if result.fun < next_cost: next_params = result.x next_cost = result.fun @@ -1853,7 +1954,7 @@ def run(self): self.wait_for_new_params_event() #self.log.debug('Gaussian process learner reading costs') self.get_params_and_costs() - self.fit_gaussian_process() + self.fit_neural_net() for _ in range(self.generation_num): self.log.debug('Gaussian process learner generating parameter:'+ str(self.params_count+1)) next_params = self.find_next_parameters() @@ -1864,7 +1965,7 @@ def run(self): pass if self.predict_global_minima_at_end or self.predict_local_minima_at_end: self.get_params_and_costs() - self.fit_gaussian_process() + self.fit_neural_net() end_dict = {} if self.predict_global_minima_at_end: self.find_global_minima() diff --git a/mloop/utilities.py b/mloop/utilities.py index cd35aee..7efdd08 100644 --- a/mloop/utilities.py +++ b/mloop/utilities.py @@ -197,7 +197,23 @@ def safe_cast_to_list(in_array): out_list = list(in_array) return out_list + +def safe_squeeze(in_array, set_dtype = float): + ''' + Attempts to squeeze an array, but has a different behavior for arrays with only a single value. + + Args: + in_array (array): The array to be squeezed + + Returns: + array: Array. + ''' + + out_array = np.squeeze(np.array(in_array, dtype=set_dtype)) + if out_array.shape == (): + out_array = np.array([out_array[()]]) + return out_array class NullQueueListener(): ''' diff --git a/mloop/visualizations.py b/mloop/visualizations.py index 6505c38..be8c5f3 100644 --- a/mloop/visualizations.py +++ b/mloop/visualizations.py @@ -43,6 +43,12 @@ def show_all_default_visualizations(controller, show_plots=True): log.debug('Creating differential evolution visualizations.') create_differential_evolution_learner_visualizations(controller.learner.total_archive_filename, file_type=controller.learner.learner_archive_file_type) + + if isinstance(controller, mlc.NeuralNetController): + log.debug('Creating neural net visualizations.') + create_neural_net_learner_visualizations(controller.ml_learner.total_archive_filename, + file_type=controller.learner.learner_archive_file_type) + if isinstance(controller, mlc.GaussianProcessController): log.debug('Creating gaussian process visualizations.') @@ -51,6 +57,7 @@ def show_all_default_visualizations(controller, show_plots=True): file_type=controller.ml_learner.learner_archive_file_type, plot_all_minima_vs_cost=plot_all_minima_vs_cost_flag) + log.info('Showing visualizations, close all to end MLOOP.') if show_plots: plt.show() @@ -375,8 +382,8 @@ class GaussianProcessVisualizer(mll.GaussianProcessLearner): def __init__(self, filename, file_type = 'pkl', **kwargs): - super(GaussianProcessVisualizer, self).__init__(ml_training_filename = filename, - ml_training_file_type = file_type, + super(GaussianProcessVisualizer, self).__init__(gp_training_filename = filename, + gp_training_file_type = file_type, update_hyperparameters = False, **kwargs) @@ -545,4 +552,137 @@ def plot_hyperparameters_vs_run(self): plt.xlabel(run_label) plt.ylabel(noise_label) plt.title('GP Learner: Noise level vs fit number.') + +def create_neural_net_learner_visualizations(filename, + file_type='pkl', + plot_cross_sections=True): + ''' + Creates plots from a neural nets learner file. + + Args: + filename (Optional [string]): Filename for the neural net archive. Must provide datetime or filename. Default None. + + Keyword Args: + file_type (Optional [string]): File type 'pkl' pickle, 'mat' matlab or 'txt' text. + plot_cross_sections (Optional [bool]): If True plot predict landscape cross sections, else do not. Default True. + plot_all_minima_vs_cost (Optional [bool]): If True plot all minima parameters versus cost number, False does not. If None it will only make the plots if all minima were previously calculated. Default None. + ''' + visualization = NeuralNetVisualizer(filename, file_type=file_type) + if plot_cross_sections: + visualization.plot_cross_sections() + + +class NeuralNetVisualizer(mll.NeuralNetLearner): + ''' + NeuralNetVisualizer extends of NeuralNetLearner, designed not to be used as a learner, but to instead post process a NeuralNetLearner archive file and produce useful data for visualization of the state of the learner. + + Args: + filename (String): Filename of the GaussianProcessLearner archive. + + Keyword Args: + file_type (String): Can be 'mat' for matlab, 'pkl' for pickle or 'txt' for text. Default 'pkl'. + + ''' + + def __init__(self, filename, file_type = 'pkl', **kwargs): + + super(NeuralNetVisualizer, self).__init__(nn_training_filename = filename, + nn_training_file_type = file_type, + update_hyperparameters = False, + **kwargs) + + self.log = logging.getLogger(__name__) + + #Trust region + self.has_trust_region = bool(np.array(self.training_dict['has_trust_region'])) + self.trust_region = np.squeeze(np.array(self.training_dict['trust_region'], dtype=float)) + + self.create_neural_net() + self.fit_neural_net() + + if np.all(np.isfinite(self.min_boundary)) and np.all(np.isfinite(self.min_boundary)): + self.finite_flag = True + self.param_scaler = lambda p: (p-self.min_boundary)/self.diff_boundary + else: + self.finite_flag = False + + self.param_colors = _color_list_from_num_of_params(self.num_params) + if self.has_trust_region: + self.scaled_trust_min = self.param_scaler(np.maximum(self.best_params - self.trust_region, self.min_boundary)) + self.scaled_trust_max = self.param_scaler(np.minimum(self.best_params + self.trust_region, self.max_boundary)) + + def run(self): + ''' + Overides the GaussianProcessLearner multiprocessor run routine. Does nothing but makes a warning. + ''' + self.log.warning('You should not have executed start() from the GaussianProcessVisualizer. It is not intended to be used as a independent process. Ending.') + + + def return_cross_sections(self, points=100, cross_section_center=None): + ''' + Finds the predicted global minima, then returns a list of vectors of parameters values, costs and uncertainties, corresponding to the 1D cross sections along each parameter axis through the predicted global minima. + + Keyword Args: + points (int): the number of points to sample along each cross section. Default value is 100. + cross_section_center (array): parameter array where the centre of the cross section should be taken. If None, the parameters for the best returned cost are used. + + Returns: + a tuple (cross_arrays, cost_arrays, uncer_arrays) + cross_parameter_arrays (list): a list of arrays for each cross section, with the values of the varied parameter going from the minimum to maximum value. + cost_arrays (list): a list of arrays for the costs evaluated along each cross section about the minimum. + uncertainty_arrays (list): a list of uncertainties + + ''' + points = int(points) + if points <= 0: + self.log.error('Points provided must be larger than zero:' + repr(points)) + raise ValueError + + if cross_section_center is None: + cross_section_center = self.best_params + else: + cross_section_center = np.array(cross_section_center) + if not self.check_in_boundary(cross_section_center): + self.log.error('cross_section_center not in boundaries:' + repr(cross_section_center)) + raise ValueError + + cross_parameter_arrays = [ np.linspace(min_p, max_p, points) for (min_p,max_p) in zip(self.min_boundary,self.max_boundary)] + cost_arrays = [] + for ind in range(self.num_params): + sample_parameters = np.array([cross_section_center for _ in range(points)]) + sample_parameters[:, ind] = cross_parameter_arrays[ind] + costs = self.predict_costs_from_param_array(sample_parameters) + cost_arrays.append(costs) + cross_parameter_arrays = np.array(cross_parameter_arrays)/self.cost_scaler.scale_ + cost_arrays = self.cost_scaler.inverse_transform(np.array(cost_arrays)) + return (cross_parameter_arrays,cost_arrays) + + def plot_cross_sections(self): + ''' + Produce a figure of the cross section about best cost and parameters + ''' + global figure_counter, legend_loc + figure_counter += 1 + plt.figure(figure_counter) + points = 100 + (_,cost_arrays) = self.return_cross_sections(points=points) + rel_params = np.linspace(0,1,points) + for ind in range(self.num_params): + plt.plot(rel_params,cost_arrays[ind,:],'-',color=self.param_colors[ind]) + if self.has_trust_region: + axes = plt.gca() + ymin, ymax = axes.get_ylim() + ytrust = ymin + 0.1*(ymax - ymin) + for ind in range(self.num_params): + plt.plot([self.scaled_trust_min[ind],self.scaled_trust_max[ind]],[ytrust,ytrust],'s', color=self.param_colors[ind]) + plt.xlabel(scale_param_label) + plt.xlim((0,1)) + plt.ylabel(cost_label) + plt.title('NN Learner: Predicted landscape' + ('with trust regions.' if self.has_trust_region else '.')) + artists = [] + for ind in range(self.num_params): + artists.append(plt.Line2D((0,1),(0,0), color=self.param_colors[ind], linestyle='-')) + plt.legend(artists,[str(x) for x in range(1,self.num_params+1)],loc=legend_loc) + + \ No newline at end of file From e8a87155d61406e8f0d06c986883e085a6c93eff Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Thu, 1 Dec 2016 11:44:39 +1100 Subject: [PATCH 13/33] NerualNet ready for actually net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There appears to be some issues with multiprocessing and gaussian process but only on MacOS, and possibly just my machine. So I’ve removed all the testing statements I had in the previous commit. Branch should be ready now to integrate in a genuine NN. --- mloop/controllers.py | 8 -------- mloop/learners.py | 20 +------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index 4352a40..486bc3e 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -338,7 +338,6 @@ def optimize(self): log.info('Controller finished. Closing down M-LOOP. Please wait a moment...') except ControllerInterrupt: self.log.warning('Controller ended by interruption.') - ''' except (KeyboardInterrupt,SystemExit): log.warning('!!! Do not give the interrupt signal again !!! \n M-LOOP stopped with keyboard interupt or system exit. Please wait at least 1 minute for the threads to safely shut down. \n ') log.warning('Closing down controller.') @@ -348,7 +347,6 @@ def optimize(self): self.log.warning('Safely shut down. Below are results found before exception.') self.print_results() raise - ''' self._shut_down() self.print_results() self.log.info('M-LOOP Done.') @@ -707,23 +705,17 @@ def _optimization_routine(self): ml_count = 0 while self.check_end_conditions(): - print('1-1.') self.log.info('Run:' + str(self.num_in_costs +1)) if ml_consec==self.generation_num or (self.no_delay and self.ml_learner_params_queue.empty()): - print('1-2.') next_params = self._next_params() - print('1-3.') self._put_params_and_out_dict(next_params) ml_consec = 0 else: - print('1-4.') next_params = self.ml_learner_params_queue.get() - print('1-5.') super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type=self.machine_learner_type) ml_consec += 1 ml_count += 1 if ml_count==self.generation_num: - print('1-6.') self.new_params_event.set() ml_count = 0 diff --git a/mloop/learners.py b/mloop/learners.py index 9aefa99..a931c93 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1269,7 +1269,6 @@ def fit_gaussian_process(self): ''' Fit the Gaussian process to the current data ''' - print('3-1') self.log.debug('Fitting Gaussian process.') if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') @@ -1279,7 +1278,6 @@ def fit_gaussian_process(self): self.gaussian_process.alpha_ = self.scaled_uncers self.gaussian_process.fit(self.all_params,self.scaled_costs) - print('3-2') if self.update_hyperparameters: self.fit_count += 1 @@ -1297,10 +1295,7 @@ def fit_gaussian_process(self): else: self.length_scale = last_hyperparameters['length_scale'] self.length_scale_history.append(self.length_scale) - print('3-3') - print(repr(self.length_scale)) - print(repr(self.noise_level)) - + def update_bias_function(self): ''' @@ -1317,10 +1312,7 @@ def predict_biased_cost(self,params): Returns: pred_bias_cost (float): Biased cost predicted at the given parameters ''' - #print('2-8-1-1.') - #(pred_cost, pred_uncer) = (self.gaussian_process.predict(params[np.newaxis,:]), 0.1) (pred_cost, pred_uncer) = self.gaussian_process.predict(params[np.newaxis,:], return_std=True) - #print('2-8-1-2.') return self.cost_bias*pred_cost - self.uncer_bias*pred_uncer def find_next_parameters(self): @@ -1331,20 +1323,15 @@ def find_next_parameters(self): next_params (array): Returns next parameters from biased cost search. ''' self.params_count += 1 - print('2-6.') self.update_bias_function() self.update_search_params() next_params = None next_cost = float('inf') - print('2-7.') for start_params in self.search_params: - print('2-8-1.') result = so.minimize(self.predict_biased_cost, start_params, bounds = self.search_region, tol=self.search_precision) - print('2-8-2.') if result.fun < next_cost: next_params = result.x next_cost = result.fun - print('2-9.') return next_params def run(self): @@ -1359,18 +1346,13 @@ def run(self): while not self.end_event.is_set(): #self.log.debug('Learner waiting for new params event') self.save_archive() - print('2-1.') self.wait_for_new_params_event() #self.log.debug('Gaussian process learner reading costs') - print('2-2.') self.get_params_and_costs() - print('2-4.') self.fit_gaussian_process() for _ in range(self.generation_num): - print('2-5.') self.log.debug('Gaussian process learner generating parameter:'+ str(self.params_count+1)) next_params = self.find_next_parameters() - print('2-10.') self.params_out_queue.put(next_params) if self.end_event.is_set(): raise LearnerInterrupt() From 2efd3176e8f0e01af81b06ed3d48800464d5c21c Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 10:22:41 +1100 Subject: [PATCH 14/33] Fix some NN typos --- mloop/learners.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 5f92b85..714c3fa 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1484,7 +1484,7 @@ class NeuralNetLearner(Learner, mp.Process): default_bad_uncertainty (Optional [float]): If a run is reported as bad and default_bad_uncertainty is provided, the uncertainty for the bad run is set to this default value. If default_bad_uncertainty is None, then the uncertainty is set to a tenth of the best to worst cost range. Default None. minimum_uncertainty (Optional [float]): The minimum uncertainty associated with provided costs. Must be above zero to avoid fitting errors. Default 1e-8. predict_global_minima_at_end (Optional [bool]): If True finds the global minima when the learner is ended. Does not if False. Default True. - predict_local_minima_at_end (Optional [bool]): If True finds the all minima when the learner is ended. Does not if False. Default False. + predict_local_minima_at_end (Optional [bool]): If True finds all minima when the learner is ended. Does not if False. Default False. Attributes: all_params (array): Array containing all parameters sent to learner. @@ -1590,13 +1590,13 @@ def __init__(self, self.cost_has_noise = True self.noise_level = 1 - self.create_nerual_net() + self.create_neural_net() #--- FAKE NN CONSTRUCTOR END ---# - self.archive_dict.update({'archive_type':'nerual_net_learner', + self.archive_dict.update({'archive_type':'neural_net_learner', 'bad_run_indexs':self.bad_run_indexs, 'generation_num':self.generation_num, 'search_precision':self.search_precision, @@ -1617,7 +1617,7 @@ def __init__(self, def create_neural_net(self): ''' - Create the nerual net. + Create the neural net. ''' #TODO: Do. From d5c57497b3c1b6b8f8f7fc93c3f9cb22d35b1e48 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 15:02:12 +1100 Subject: [PATCH 15/33] Basic NN learner implementation I've pulled the actual network logic out into a new class, to keep the TF stuff separate from everything else and to keep a clear separation between what's modelling the landscape and what's doing prediction. --- mloop/learners.py | 61 ++-------------------------- mloop/nnlearner.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 58 deletions(-) create mode 100644 mloop/nnlearner.py diff --git a/mloop/learners.py b/mloop/learners.py index 714c3fa..92b5e44 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1583,18 +1583,11 @@ def __init__(self, self.cost_scaler = skp.StandardScaler() - - #--- FAKE NN CONSTRUCTOR START ---# - self.length_scale = 1 self.cost_has_noise = True self.noise_level = 1 - self.create_neural_net() - - - - #--- FAKE NN CONSTRUCTOR END ---# + self.neural_net_impl = NeuralNetImpl(self.num_params) self.archive_dict.update({'archive_type':'neural_net_learner', 'bad_run_indexs':self.bad_run_indexs, @@ -1611,23 +1604,6 @@ def __init__(self, #Remove logger so gaussian process can be safely picked for multiprocessing on Windows self.log = None - - #--- FAKE NN METHODS START ---# - - - def create_neural_net(self): - ''' - Create the neural net. - - ''' - #TODO: Do. - gp_kernel = skk.RBF(length_scale=self.length_scale) + skk.WhiteKernel(noise_level=self.noise_level) - - if self.update_hyperparameters: - self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,n_restarts_optimizer=self.hyperparameter_searches) - else: - self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,optimizer=None) - def fit_neural_net(self): ''' Determine the appropriate number of layers for the NN given the data. @@ -1635,34 +1611,7 @@ def fit_neural_net(self): Fit the Neural Net with the appropriate topology to the data ''' - #TODO: Do. - self.log.debug('Fitting Gaussian process.') - if self.all_params.size==0 or self.all_costs.size==0 or self.all_uncers.size==0: - self.log.error('Asked to fit GP but no data is in all_costs, all_params or all_uncers.') - raise ValueError - - self.scaled_costs = self.cost_scaler.fit_transform(self.all_costs[:,np.newaxis])[:,0] - self.scaled_uncers = self.all_uncers * self.cost_scaler.scale_ - self.gaussian_process.alpha_ = self.scaled_uncers - self.gaussian_process.fit(self.all_params,self.scaled_costs) - - if self.update_hyperparameters: - - self.fit_count += 1 - self.gaussian_process.kernel = self.gaussian_process.kernel_ - - last_hyperparameters = self.gaussian_process.kernel.get_params() - - if self.cost_has_noise: - self.length_scale = last_hyperparameters['k1__length_scale'] - if isinstance(self.length_scale, float): - self.length_scale = np.array([self.length_scale]) - self.length_scale_history.append(self.length_scale) - self.noise_level = last_hyperparameters['k2__noise_level'] - self.noise_level_history.append(self.noise_level) - else: - self.length_scale = last_hyperparameters['length_scale'] - self.length_scale_history.append(self.length_scale) + self.neural_net_impl.fit_neural_net(self.all_params, self.all_costs) def predict_cost(self,params): ''' @@ -1671,11 +1620,7 @@ def predict_cost(self,params): Returns: float : Predicted cost at paramters ''' - #TODO: Do. - return self.gaussian_process.predict(params[np.newaxis,:]) - - #--- FAKE NN METHODS END ---# - + return self.neural_net_impl.predict_cost(params) def wait_for_new_params_event(self): ''' diff --git a/mloop/nnlearner.py b/mloop/nnlearner.py new file mode 100644 index 0000000..bbf5a76 --- /dev/null +++ b/mloop/nnlearner.py @@ -0,0 +1,116 @@ +import logging +import math +import tensorflow as tf +import numpy as np + +class NeuralNetImpl(): + ''' + Neural network implementation. + + Args: + num_params (int): The number of params. + + + Attributes: + TODO + ''' + + def __init__(self, + num_params = None): + + self.log = logging.getLogger(__name__) + if num_params is None: + self.log.error("num_params must be provided") + raise ValueError + self.num_params = num_params + + self.tf_session = tf.InteractiveSession() + + # Initial hyperparameters + self.num_layers = 1 + self.layer_dim = 128 + self.train_epochs = 300 + self.batch_size = 64 + + # Inputs + self.input_placeholder = tf.placeholder(tf.float32, shape=[None, self.num_params]) + self.output_placeholder = tf.placeholder(tf.float32, shape=[None, 1]) + self.keep_prob = tf.placeholder_with_default(1., shape=[]) + self.regularisation_coefficient = tf.placeholder_with_default(0., shape=[]) + + self._create_neural_net() + + def _create_neural_net(self): + ''' + Creates the neural net with topology specified by the current hyperparameters. + + ''' + # Forget about any old weights/biases + self.weights = [] + self.biases = [] + + # Input + internal nodes + # TODO: Use length scale for setting initial weights? + prev_layer_dim = self.num_params + prev_h = self.input_placeholder + for dim in [self.layer_dim] * self.num_layers: + self.weights.append(tf.Variable(tf.random_normal([prev_layer_dim, dim], stddev=0.1))) + self.biases.append(tf.Variable(tf.random_normal([dim]))) + prev_layer_dim = dim + prev_h = tf.nn.dropout( + tf.nn.sigmoid(tf.matmul(prev_h, self.weights[-1]) + self.biases[-1]), + keep_prob=self.keep_prob) + + # Output node + self.weights.append(tf.Variable(tf.random_normal([prev_layer_dim, 1]))) + self.biases.append(tf.Variable(tf.random_normal([1]))) + self.output_var = tf.matmul(prev_h, self.weights[-1]) + self.biases[-1] + + # Loss function and training + loss_func = ( + tf.reduce_mean(tf.reduce_sum(tf.square(self.output_var - self.output_placeholder), + reduction_indices=[1])) + + self.regularisation_coefficient * sum([tf.nn.l2_loss(W) for W in self.weights])) + self.train_step = tf.train.AdamOptimizer().minimize(loss_func) + + self.tf_session.run(tf.initialize_all_variables()) + + def fit_neural_net(self, all_params, all_costs): + ''' + Determine the appropriate number of layers for the NN given the data. + + Fit the Neural Net with the appropriate topology to the data + + Args: + all_params (array): array of all parameter arrays + all_costs (array): array of costs (associated with the corresponding parameters) + ''' + self.log.debug('Fitting neural network') + if len(all_params) == 0: + self.log.error('No data provided.') + raise ValueError + if not len(all_params) == len(all_costs): + self.log.error("Params and costs must have the same length") + raise ValueError + + # TODO: Fit hyperparameters. + + for i in range(self.train_epochs): + # Split the data into random batches, and train on each batch + all_indices = np.random.permutation(len(all_params)) + for j in range(math.ceil(len(all_params) / self.batch_size)): + batch_indices = all_indices[j * self.batch_size : (j + 1) * self.batch_size] + batch_input = [all_params[index] for index in batch_indices] + batch_output = [[all_costs[index]] for index in batch_indices] + self.tf_session.run(self.train_step, + feed_dict={self.input_placeholder: batch_input, + self.output_placeholder: batch_output}) + + def predict_cost(self,params): + ''' + Produces a prediction of cost from the neural net at params. + + Returns: + float : Predicted cost at parameters + ''' + return self.tf_session.run(self.output_var, feed_dict={self.input_placeholder: [params]})[0][0] From d7b1fcad79a40211944583142f56ad07826f62d5 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 16:17:48 +1100 Subject: [PATCH 16/33] Fix number_of_controllers definition --- mloop/controllers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index 3ab7d0b..5e6e303 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -12,7 +12,7 @@ import os controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4,'neural_net':5} -number_of_controllers = 4 +number_of_controllers = len(controller_dict) default_controller_archive_filename = 'controller_archive' default_controller_archive_file_type = 'txt' From 2126150da18b3e8e2322a344014dfcf4a4b4ac50 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 16:23:32 +1100 Subject: [PATCH 17/33] More NNController tidying/tweaking --- mloop/learners.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 92b5e44..40f983d 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1125,6 +1125,7 @@ def wait_for_new_params_event(self): ''' Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. ''' + self.log.debug("Waiting for new params") while not self.end_event.is_set(): if self.new_params_event.wait(timeout=self.learner_wait): self.new_params_event.clear() @@ -1487,6 +1488,7 @@ class NeuralNetLearner(Learner, mp.Process): predict_local_minima_at_end (Optional [bool]): If True finds all minima when the learner is ended. Does not if False. Default False. Attributes: + TODO: Update these. all_params (array): Array containing all parameters sent to learner. all_costs (array): Array containing all costs sent to learner. all_uncers (array): Array containing all uncertainties sent to learner. @@ -1588,6 +1590,15 @@ def __init__(self, self.noise_level = 1 self.neural_net_impl = NeuralNetImpl(self.num_params) + # TODO: What are these? + self.generation_num = 4 + if (self.default_bad_cost is None) and (self.default_bad_uncertainty is None): + self.bad_defaults_set = False + elif (self.default_bad_cost is not None) and (self.default_bad_uncertainty is not None): + self.bad_defaults_set = True + else: + self.log.error('Both the default cost and uncertainty must be set for a bad run or they must both be set to None.') + raise ValueError self.archive_dict.update({'archive_type':'neural_net_learner', 'bad_run_indexs':self.bad_run_indexs, @@ -1792,10 +1803,10 @@ def run(self): try: while not self.end_event.is_set(): - #self.log.debug('Learner waiting for new params event') + self.log.debug('Learner waiting for new params event') self.save_archive() self.wait_for_new_params_event() - #self.log.debug('Gaussian process learner reading costs') + self.log.debug('NN learner reading costs') self.get_params_and_costs() self.fit_neural_net() for _ in range(self.generation_num): From d78a6617ebfd935d7caa4d2d73ebc384f9c8e12d Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 16:23:55 +1100 Subject: [PATCH 18/33] Remove scaler from NNController --- mloop/learners.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 40f983d..8f8038d 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1583,8 +1583,6 @@ def __init__(self, self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - self.cost_scaler = skp.StandardScaler() - self.length_scale = 1 self.cost_has_noise = True self.noise_level = 1 @@ -1861,7 +1859,7 @@ def find_global_minima(self): self.predicted_best_parameters = curr_best_params self.predicted_best_scaled_cost = curr_best_cost - self.predicted_best_cost = self.cost_scaler.inverse_transform(self.predicted_best_scaled_cost) + self.predicted_best_cost = self.predicted_best_scaled_cost self.archive_dict.update({'predicted_best_parameters':self.predicted_best_parameters, 'predicted_best_scaled_cost':self.predicted_best_scaled_cost, From 34b504b0cdbb542cc3a28ea41f6132e504699995 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 16:25:44 +1100 Subject: [PATCH 19/33] Tidying/logging for NN impl --- mloop/nnlearner.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mloop/nnlearner.py b/mloop/nnlearner.py index bbf5a76..56debd2 100644 --- a/mloop/nnlearner.py +++ b/mloop/nnlearner.py @@ -9,16 +9,13 @@ class NeuralNetImpl(): Args: num_params (int): The number of params. - - - Attributes: - TODO ''' def __init__(self, num_params = None): self.log = logging.getLogger(__name__) + self.log.debug('Initialising neural network impl') if num_params is None: self.log.error("num_params must be provided") raise ValueError @@ -45,6 +42,7 @@ def _create_neural_net(self): Creates the neural net with topology specified by the current hyperparameters. ''' + self.log.debug('Creating neural network') # Forget about any old weights/biases self.weights = [] self.biases = [] From 9224be50eee3cef679c4776d4268dff7f748a278 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 2 Dec 2016 16:25:54 +1100 Subject: [PATCH 20/33] Fix importing/creation of NN impl We need to specify nnlearner as a package. More subtly, because of TF we can only run NNI in the same process in which it's created. This means we need to wait until the run() method of the learner is called before constructing the impl. --- mloop/__init__.py | 2 +- mloop/learners.py | 5 ++++- mloop/nnlearner.py | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mloop/__init__.py b/mloop/__init__.py index 9e53155..dd553f6 100644 --- a/mloop/__init__.py +++ b/mloop/__init__.py @@ -13,4 +13,4 @@ import os __version__= "2.1.1" -__all__ = ['controllers','interfaces','launchers','learners','testing','utilities','visualizations','cmd'] \ No newline at end of file +__all__ = ['controllers','interfaces','launchers','learners','nnlearner','testing','utilities','visualizations','cmd'] diff --git a/mloop/learners.py b/mloop/learners.py index 8f8038d..12f6ed5 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1587,7 +1587,6 @@ def __init__(self, self.cost_has_noise = True self.noise_level = 1 - self.neural_net_impl = NeuralNetImpl(self.num_params) # TODO: What are these? self.generation_num = 4 if (self.default_bad_cost is None) and (self.default_bad_uncertainty is None): @@ -1799,6 +1798,10 @@ def run(self): #current solution is to only log to the console for warning and above from a process self.log = mp.log_to_stderr(logging.WARNING) + # The network needs to be created in the same process in which it runs + import mloop.nnlearner as mlnn + self.neural_net_impl = mlnn.NeuralNetImpl(self.num_params) + try: while not self.end_event.is_set(): self.log.debug('Learner waiting for new params event') diff --git a/mloop/nnlearner.py b/mloop/nnlearner.py index 56debd2..14b612e 100644 --- a/mloop/nnlearner.py +++ b/mloop/nnlearner.py @@ -7,6 +7,8 @@ class NeuralNetImpl(): ''' Neural network implementation. + This must run in the same process in which it's created. + Args: num_params (int): The number of params. ''' From be3c8a51d08bf85825096fc98d495ed0201bbd2d Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Sun, 4 Dec 2016 10:03:01 +1100 Subject: [PATCH 21/33] Pull NNI construction into create_neural_net --- mloop/learners.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index ae56fa9..f5169a4 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1670,6 +1670,13 @@ def __init__(self, #Remove logger so gaussian process can be safely picked for multiprocessing on Windows self.log = None + def create_neural_net(self): + ''' + Creates the neural net. Must be called from the same process as fit_neural_net, predict_cost and predict_costs_from_param_array. + ''' + import mloop.nnlearner as mlnn + self.neural_net_impl = mlnn.NeuralNetImpl(self.num_params) + def fit_neural_net(self): ''' Determine the appropriate number of layers for the NN given the data. @@ -1871,8 +1878,7 @@ def run(self): self.log = mp.log_to_stderr(logging.WARNING) # The network needs to be created in the same process in which it runs - import mloop.nnlearner as mlnn - self.neural_net_impl = mlnn.NeuralNetImpl(self.num_params) + self.create_neural_net() try: while not self.end_event.is_set(): From 3a46a17c7c0305089d4b5c6cf8c091ea304cc9de Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Sun, 4 Dec 2016 10:03:36 +1100 Subject: [PATCH 22/33] Dumb implementation of predict_costs array version --- mloop/learners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index f5169a4..01c8ea1 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1703,8 +1703,8 @@ def predict_costs_from_param_array(self,params): Returns: float : Predicted cost at paramters ''' -# TODO - return [] + # TODO: Can do this more efficiently. + return [self.predict_cost(param) for param in params] def wait_for_new_params_event(self): From 89f1e1afb9d1c637eadda31e430ea9ae078b0b2d Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Sun, 4 Dec 2016 11:04:03 +1100 Subject: [PATCH 23/33] Set new_params_event in MLC after getting the cost When generation_num=1, if the new_params_event is set first then the learner will try to get the cost when the queue is empty, causing an exception. --- mloop/controllers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index 0db4a2a..8cf527e 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -714,13 +714,14 @@ def _optimization_routine(self): super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type=self.machine_learner_type) ml_consec += 1 ml_count += 1 + + self.save_archive() + self._get_cost_and_in_dict() + if ml_count==self.generation_num: self.new_params_event.set() ml_count = 0 - - self.save_archive() - self._get_cost_and_in_dict() def _shut_down(self): From 3e4b3df1efdc22b9b3055c55dbb49b5dd77c7e96 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Sun, 4 Dec 2016 11:08:24 +1100 Subject: [PATCH 24/33] Add (trivial) scaler back to NNL --- mloop/learners.py | 13 ++++++++++--- mloop/visualizations.py | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 01c8ea1..b759889 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1655,6 +1655,12 @@ def __init__(self, self.cost_has_noise = True self.noise_level = 1 + # Set up the scaler to do nothing. + # TODO: Figure out how to use scaling for the NN (it's a bit difficult because we don't + # completely re-train each time, and don't want the scaling changing without doing a complete + # re-train). + self.cost_scaler = skp.StandardScaler(with_mean=False, with_std=False) + self.archive_dict.update({'archive_type':'nerual_net_learner', 'bad_run_indexs':self.bad_run_indexs, 'generation_num':self.generation_num, @@ -1684,7 +1690,9 @@ def fit_neural_net(self): Fit the Neural Net with the appropriate topology to the data ''' - self.neural_net_impl.fit_neural_net(self.all_params, self.all_costs) + self.scaled_costs = self.cost_scaler.fit_transform(self.all_costs[:,np.newaxis])[:,0] + + self.neural_net_impl.fit_neural_net(self.all_params, self.scaled_costs) def predict_cost(self,params): ''' @@ -1940,8 +1948,7 @@ def find_global_minima(self): self.predicted_best_parameters = curr_best_params self.predicted_best_scaled_cost = curr_best_cost - self.predicted_best_cost = self.predicted_best_scaled_cost - + self.predicted_best_cost = float(self.cost_scaler.inverse_transform(self.predicted_best_scaled_cost)) self.archive_dict.update({'predicted_best_parameters':self.predicted_best_parameters, 'predicted_best_scaled_cost':self.predicted_best_scaled_cost, 'predicted_best_cost':self.predicted_best_cost}) diff --git a/mloop/visualizations.py b/mloop/visualizations.py index be8c5f3..494a458 100644 --- a/mloop/visualizations.py +++ b/mloop/visualizations.py @@ -653,7 +653,10 @@ def return_cross_sections(self, points=100, cross_section_center=None): sample_parameters[:, ind] = cross_parameter_arrays[ind] costs = self.predict_costs_from_param_array(sample_parameters) cost_arrays.append(costs) - cross_parameter_arrays = np.array(cross_parameter_arrays)/self.cost_scaler.scale_ + if self.cost_scaler.scale_: + cross_parameter_arrays = np.array(cross_parameter_arrays)/self.cost_scaler.scale_ + else: + cross_parameter_arrays = np.array(cross_parameter_arrays) cost_arrays = self.cost_scaler.inverse_transform(np.array(cost_arrays)) return (cross_parameter_arrays,cost_arrays) @@ -683,6 +686,3 @@ def plot_cross_sections(self): for ind in range(self.num_params): artists.append(plt.Line2D((0,1),(0,0), color=self.param_colors[ind], linestyle='-')) plt.legend(artists,[str(x) for x in range(1,self.num_params+1)],loc=legend_loc) - - - \ No newline at end of file From f22c97975cced17febeadb453c6e1bf53b708af2 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Sun, 4 Dec 2016 11:09:49 +1100 Subject: [PATCH 25/33] Don't do one last train in order to predict minima at the end. This was causing an exception to be thrown when trying to get costs from the queue. --- mloop/learners.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index b759889..8d80d77 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1904,9 +1904,11 @@ def run(self): raise LearnerInterrupt() except LearnerInterrupt: pass - if self.predict_global_minima_at_end or self.predict_local_minima_at_end: - self.get_params_and_costs() - self.fit_neural_net() + # TODO: Fix this. We can't just do what's here because the costs queue might be empty, but + # we should get anything left in it and do one last train. + #if self.predict_global_minima_at_end or self.predict_local_minima_at_end: + # self.get_params_and_costs() + # self.fit_neural_net() end_dict = {} if self.predict_global_minima_at_end: self.find_global_minima() From e30906a4223f1c977991b063fa4878f31b94a892 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 9 Dec 2016 16:43:12 +1100 Subject: [PATCH 26/33] Tweak some NNI params to perform better on the test --- mloop/nnlearner.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mloop/nnlearner.py b/mloop/nnlearner.py index 14b612e..10dad2d 100644 --- a/mloop/nnlearner.py +++ b/mloop/nnlearner.py @@ -71,7 +71,7 @@ def _create_neural_net(self): tf.reduce_mean(tf.reduce_sum(tf.square(self.output_var - self.output_placeholder), reduction_indices=[1])) + self.regularisation_coefficient * sum([tf.nn.l2_loss(W) for W in self.weights])) - self.train_step = tf.train.AdamOptimizer().minimize(loss_func) + self.train_step = tf.train.AdamOptimizer(1.0).minimize(loss_func) self.tf_session.run(tf.initialize_all_variables()) @@ -104,7 +104,9 @@ def fit_neural_net(self, all_params, all_costs): batch_output = [[all_costs[index]] for index in batch_indices] self.tf_session.run(self.train_step, feed_dict={self.input_placeholder: batch_input, - self.output_placeholder: batch_output}) + self.output_placeholder: batch_output, + self.regularisation_coefficient: 0.01, + }) def predict_cost(self,params): ''' From e6d371aabb5da883c46a0654debbd0bf2696a039 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 9 Dec 2016 16:45:07 +1100 Subject: [PATCH 27/33] Still print predicted_best_cost even when predicted_best_uncertainty isn't set --- mloop/controllers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mloop/controllers.py b/mloop/controllers.py index 8cf527e..ea53553 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -763,8 +763,11 @@ def print_results(self): super(MachineLearnerController,self).print_results() try: self.log.info('Predicted best parameters:' + str(self.predicted_best_parameters)) - self.log.info('Predicted best cost:' + str(self.predicted_best_cost) + ' +/- ' + str(self.predicted_best_uncertainty)) - + try: + errorstring = ' +/- ' + str(self.predicted_best_uncertainty) + except AttributeError: + errorstring = '' + self.log.info('Predicted best cost:' + str(self.predicted_best_cost) + errorstring) except AttributeError: pass try: From e6e83e815ace6ed5543e5bde92a9b5b5531b0508 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 9 Dec 2016 16:54:03 +1100 Subject: [PATCH 28/33] Use TF gradient when minimizing NN cost function estimate --- mloop/learners.py | 30 +++++++++++++++++++++++++----- mloop/nnlearner.py | 12 ++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 8d80d77..ad6e5b1 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1703,6 +1703,16 @@ def predict_cost(self,params): ''' return self.neural_net_impl.predict_cost(params) + def predict_cost_gradient(self,params): + ''' + Produces a prediction of the gradient of the cost function at params. + + Returns: + float : Predicted gradient at paramters + ''' + # scipy.optimize.minimize doesn't seem to like a 32-bit Jacobian, so we convert to 64 + return np.float64(self.neural_net_impl.predict_cost_gradient(params)) + def predict_costs_from_param_array(self,params): ''' @@ -1871,7 +1881,11 @@ def find_next_parameters(self): next_params = None next_cost = float('inf') for start_params in self.search_params: - result = so.minimize(self.predict_cost, start_params, bounds = self.search_region, tol=self.search_precision) + result = so.minimize(fun = self.predict_cost, + x0 = start_params, + jac = self.predict_cost_gradient, + bounds = self.search_region, + tol = self.search_precision) if result.fun < next_cost: next_params = result.x next_cost = result.fun @@ -1942,8 +1956,11 @@ def find_global_minima(self): search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in search_params: - # TODO: Take advantage of the fact that we get the gradient for free, so can use that to speed up the minimizer. - result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) + result = so.minimize(fun = self.predict_cost, + x0 = start_params, + jac = self.predict_cost_gradient, + bounds = search_bounds, + tol = self.search_precision) curr_best_params = result.x curr_best_cost = result.fun if curr_best_cost Date: Fri, 9 Dec 2016 17:05:38 +1100 Subject: [PATCH 29/33] Plot NN surface when there are 2 params --- mloop/visualizations.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/mloop/visualizations.py b/mloop/visualizations.py index 494a458..8219fbc 100644 --- a/mloop/visualizations.py +++ b/mloop/visualizations.py @@ -12,6 +12,8 @@ import matplotlib.pyplot as plt import matplotlib as mpl +from mpl_toolkits.mplot3d import Axes3D + figure_counter = 0 cmap = plt.get_cmap('hsv') run_label = 'Run number' @@ -570,6 +572,7 @@ def create_neural_net_learner_visualizations(filename, visualization = NeuralNetVisualizer(filename, file_type=file_type) if plot_cross_sections: visualization.plot_cross_sections() + visualization.plot_surface() class NeuralNetVisualizer(mll.NeuralNetLearner): @@ -659,7 +662,7 @@ def return_cross_sections(self, points=100, cross_section_center=None): cross_parameter_arrays = np.array(cross_parameter_arrays) cost_arrays = self.cost_scaler.inverse_transform(np.array(cost_arrays)) return (cross_parameter_arrays,cost_arrays) - + def plot_cross_sections(self): ''' Produce a figure of the cross section about best cost and parameters @@ -686,3 +689,26 @@ def plot_cross_sections(self): for ind in range(self.num_params): artists.append(plt.Line2D((0,1),(0,0), color=self.param_colors[ind], linestyle='-')) plt.legend(artists,[str(x) for x in range(1,self.num_params+1)],loc=legend_loc) + + def plot_surface(self): + ''' + Produce a figure of the cost surface (only works when there are 2 parameters) + ''' + if self.num_params != 2: + return + global figure_counter + figure_counter += 1 + fig = plt.figure(figure_counter) + ax = fig.add_subplot(111, projection='3d') + + points = 50 + param_set = [ np.linspace(min_p, max_p, points) for (min_p,max_p) in zip(self.min_boundary,self.max_boundary)] + params = [(x,y) for x in param_set[0] for y in param_set[1]] + costs = self.predict_costs_from_param_array(params) + ax.scatter([param[0] for param in params], [param[1] for param in params], costs) + ax.set_zlim(top=100) + ax.set_xlabel('x') + ax.set_ylabel('y') + ax.set_zlabel('cost') + + ax.scatter(self.all_params[:,0], self.all_params[:,1], self.all_costs, c='r') From 9835e3f7ef66762c7c4138d434cf7a1f44e71b65 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 9 Dec 2016 17:36:38 +1100 Subject: [PATCH 30/33] Revert "Get NN working a bit better on the tests" --- mloop/__init__.py | 2 +- mloop/controllers.py | 454 ++++++++------------ mloop/learners.py | 1095 ++++++++++++----------------------------------- mloop/nnlearner.py | 130 ------ mloop/utilities.py | 16 - mloop/visualizations.py | 174 +------- 6 files changed, 464 insertions(+), 1407 deletions(-) delete mode 100644 mloop/nnlearner.py diff --git a/mloop/__init__.py b/mloop/__init__.py index dd553f6..9e53155 100644 --- a/mloop/__init__.py +++ b/mloop/__init__.py @@ -13,4 +13,4 @@ import os __version__= "2.1.1" -__all__ = ['controllers','interfaces','launchers','learners','nnlearner','testing','utilities','visualizations','cmd'] +__all__ = ['controllers','interfaces','launchers','learners','testing','utilities','visualizations','cmd'] \ No newline at end of file diff --git a/mloop/controllers.py b/mloop/controllers.py index ea53553..d018367 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -11,44 +11,42 @@ import logging import os -controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4,'neural_net':5} -number_of_controllers = len(controller_dict) +controller_dict = {'random':1,'nelder_mead':2,'gaussian_process':3,'differential_evolution':4} +number_of_controllers = 4 default_controller_archive_filename = 'controller_archive' default_controller_archive_file_type = 'txt' class ControllerInterrupt(Exception): ''' - Exception that is raised when the controlled is ended with the end flag or event. + Exception that is raised when the controlled is ended with the end flag or event. ''' def __init__(self): super(ControllerInterrupt,self).__init__() - + def create_controller(interface, - controller_type='gaussian_process', + controller_type='gaussian_process', **controller_config_dict): ''' Start the controller with the options provided. - + Args: interface (interface): Interface with queues and events to be passed to controller - + Keyword Args: - controller_type (Optional [str]): Defines the type of controller can be 'random', 'nelder', 'gaussian_process' or 'neural_net'. Defaults to 'gaussian_process'. + controller_type (Optional [str]): Defines the type of controller can be 'random', 'nelder' or 'gaussian_process'. Defaults to 'gaussian_process'. **controller_config_dict : Options to be passed to controller. - + Returns: Controller : threadible object which must be started with start() to get the controller running. - + Raises: ValueError : if controller_type is an unrecognized string ''' log = logging.getLogger(__name__) - + controller_type = str(controller_type) if controller_type=='gaussian_process': controller = GaussianProcessController(interface, **controller_config_dict) - elif controller_type=='neural_net': - controller = NeuralNetController(interface, **controller_config_dict) elif controller_type=='differential_evolution': controller = DifferentialEvolutionController(interface, **controller_config_dict) elif controller_type=='nelder_mead': @@ -58,33 +56,33 @@ def create_controller(interface, else: log.error('Unknown controller type:' + repr(controller_type)) raise ValueError - + return controller class Controller(): ''' Abstract class for controllers. The controller controls the entire M-LOOP process. The controller for each algorithm all inherit from this class. The class stores a variety of data which all algorithms use and also all of the achiving and saving features. - + In order to implement your own controller class the minimum requirement is to add a learner to the learner variable. And implement the next_parameters method, where you provide the appropriate information to the learner and get the next parameters. - + See the RandomController for a simple implementation of a controller. - + Note the first three keywords are all possible halting conditions for the controller. If any of them are satisfied the controller will halt (meaning an and condition is used). - + Also creates an empty variable learner. The simplest way to make a working controller is to assign a learner of some kind to this variable, and add appropriate queues and events from it. - + Args: interface (interface): The interface process. Is run by learner. - + Keyword Args: max_num_runs (Optional [float]): The number of runs before the controller stops. If set to float('+inf') the controller will run forever. Default float('inf'), meaning the controller will run until another condition is met. target_cost (Optional [float]): The target cost for the run. If a run achieves a cost lower than the target, the controller is stopped. Default float('-inf'), meaning the controller will run until another condition is met. - max_num_runs_without_better_params (Otional [float]): Puts a limit on the number of runs are allowed before a new better set of parameters is found. Default float('inf'), meaning the controller will run until another condition is met. + max_num_runs_without_better_params (Otional [float]): Puts a limit on the number of runs are allowed before a new better set of parameters is found. Default float('inf'), meaning the controller will run until another condition is met. controller_archive_filename (Optional [string]): Filename for archive. Contains costs, parameter history and other details depending on the controller type. Default 'ControllerArchive.mat' controller_archive_file_type (Optional [string]): File type for archive. Can be either 'txt' a human readable text file, 'pkl' a python dill file, 'mat' a matlab file or None if there is no archive. Default 'mat'. archive_extra_dict (Optional [dict]): A dictionary with any extra variables that are to be saved to the archive. If None, nothing is added. Default None. start_datetime (Optional datetime): Datetime for when controller was started. - + Attributes: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. @@ -94,7 +92,7 @@ class Controller(): learner_costs_queue (queue): The costs queue for the learner end_learner (event): Event used to trigger the end of the learner num_in_costs (int): Counter for the number of costs received. - num_out_params (int): Counter for the number of parameters received. + num_out_params (int): Counter for the number of parameters received. out_params (list): List of all parameters sent out by controller. out_extras (list): Any extras associated with the output parameters. in_costs (list): List of costs received by controller. @@ -102,10 +100,10 @@ class Controller(): best_cost (float): The lowest, and best, cost received by the learner. best_uncer (float): The uncertainty associated with the best cost. best_params (array): The best parameters recieved by the learner. - best_index (float): The run number that produced the best cost. - + best_index (float): The run number that produced the best cost. + ''' - + def __init__(self, interface, max_num_runs = float('+inf'), target_cost = float('-inf'), @@ -115,11 +113,11 @@ def __init__(self, interface, archive_extra_dict = None, start_datetime = None, **kwargs): - + #Make logger self.remaining_kwargs = mlu._config_logger(**kwargs) self.log = logging.getLogger(__name__) - + #Variable that are included in archive self.num_in_costs = 0 self.num_out_params = 0 @@ -135,7 +133,7 @@ def __init__(self, interface, self.best_uncer = float('nan') self.best_index = float('nan') self.best_params = float('nan') - + #Variables that used internally self.last_out_params = None self.curr_params = None @@ -143,29 +141,29 @@ def __init__(self, interface, self.curr_uncer = None self.curr_bad = None self.curr_extras = None - + #Constants self.controller_wait = float(1) - + #Learner related variables self.learner_params_queue = None self.learner_costs_queue = None self.end_learner = None self.learner = None - + #Variables set by user - + #save interface and extract important variables if isinstance(interface, mli.Interface): self.interface = interface else: self.log.error('interface is not a Interface as defined in the MLOOP package.') raise TypeError - + self.params_out_queue = interface.params_out_queue self.costs_in_queue = interface.costs_in_queue self.end_interface = interface.end_event - + #Other options if start_datetime is None: self.start_datetime = datetime.datetime.now() @@ -180,7 +178,7 @@ def __init__(self, interface, if self.max_num_runs_without_better_params<=0: self.log.error('Max number of repeats must be greater than zero. max_num_runs:'+repr(max_num_runs_without_better_params)) raise ValueError - + if mlu.check_file_type_supported(controller_archive_file_type): self.controller_archive_file_type = controller_archive_file_type else: @@ -193,7 +191,7 @@ def __init__(self, interface, os.makedirs(mlu.archive_foldername) self.controller_archive_filename =str(controller_archive_filename) self.total_archive_filename = mlu.archive_foldername + self.controller_archive_filename + '_' + mlu.datetime_to_string(self.start_datetime) + '.' + self.controller_archive_file_type - + self.archive_dict = {'archive_type':'controller', 'num_out_params':self.num_out_params, 'out_params':self.out_params, @@ -205,21 +203,21 @@ def __init__(self, interface, 'in_extras':self.in_extras, 'max_num_runs':self.max_num_runs, 'start_datetime':mlu.datetime_to_string(self.start_datetime)} - + if archive_extra_dict is not None: self.archive_dict.update(archive_extra_dict) - + self.log.debug('Controller init completed.') - + def check_end_conditions(self): ''' Check whether either of the three end contions have been met: number_of_runs, target_cost or max_num_runs_without_better_params. - + Returns: - bool : True, if the controlled should continue, False if the controller should end. + bool : True, if the controlled should continue, False if the controller should end. ''' return (self.num_in_costs < self.max_num_runs) and (self.best_cost > self.target_cost) and (self.num_last_best_cost < self.max_num_runs_without_better_params) - + def _update_controller_with_learner_attributes(self): ''' Update the controller with properties from the learner. @@ -228,19 +226,19 @@ def _update_controller_with_learner_attributes(self): self.learner_costs_queue = self.learner.costs_in_queue self.end_learner = self.learner.end_event self.remaining_kwargs = self.learner.remaining_kwargs - + self.archive_dict.update({'num_params':self.learner.num_params, 'min_boundary':self.learner.min_boundary, 'max_boundary':self.learner.max_boundary}) - - + + def _put_params_and_out_dict(self, params, param_type=None, **kwargs): ''' - Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. - + Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. + Args: params (array) : array of values to be sent to file - + Keyword Args: **kwargs: any additional data to be attached to file sent to experiment ''' @@ -255,11 +253,11 @@ def _put_params_and_out_dict(self, params, param_type=None, **kwargs): self.out_type.append(param_type) self.log.info('params ' + str(params)) #self.log.debug('Put params num:' + repr(self.num_out_params )) - + def _get_cost_and_in_dict(self): ''' Get cost, uncertainty, parameters, bad and extra data from experiment. Stores in a list of history and also puts variables in their appropriate 'current' variables - + Note returns nothing, stores everything in the internal storage arrays and the curr_variables ''' while True: @@ -269,16 +267,16 @@ def _get_cost_and_in_dict(self): continue else: break - + self.num_in_costs += 1 self.num_last_best_cost += 1 - + if not ('cost' in in_dict) and (not ('bad' in in_dict) or not in_dict['bad']): self.log.error('You must provide at least the key cost or the key bad with True.') raise ValueError try: - self.curr_cost = float(in_dict.pop('cost',float('nan'))) - self.curr_uncer = float(in_dict.pop('uncer',0)) + self.curr_cost = float(in_dict.pop('cost',float('nan'))) + self.curr_uncer = float(in_dict.pop('uncer',0)) self.curr_bad = bool(in_dict.pop('bad',False)) self.curr_extras = in_dict except ValueError: @@ -286,7 +284,7 @@ def _get_cost_and_in_dict(self): raise if self.curr_bad and ('cost' in in_dict): self.log.warning('The cost provided with the bad run will be saved, but not used by the learners.') - + self.in_costs.append(self.curr_cost) self.in_uncers.append(self.curr_uncer) self.in_bads.append(self.curr_bad) @@ -303,7 +301,7 @@ def _get_cost_and_in_dict(self): else: self.log.info('cost ' + str(self.curr_cost) + ' +/- ' + str(self.curr_uncer)) #self.log.debug('Got cost num:' + repr(self.num_in_costs)) - + def save_archive(self): ''' Save the archive associated with the controller class. Only occurs if the filename for the archive is not None. Saves with the format previously set. @@ -322,15 +320,15 @@ def save_archive(self): raise else: self.log.debug('Did not save controller archive file.') - + def optimize(self): ''' Optimize the experiment. This code learner and interface processes/threads are launched and appropriately ended. - + Starts both threads and catches kill signals and shuts down appropriately. ''' log = logging.getLogger(__name__) - + try: log.info('Optimization started.') self._start_up() @@ -350,14 +348,14 @@ def optimize(self): self._shut_down() self.print_results() self.log.info('M-LOOP Done.') - + def _start_up(self): ''' Start the learner and interface threads/processes. ''' self.learner.start() self.interface.start() - + def _shut_down(self): ''' Shutdown and clean up resources of the controller. end the learners, queue_listener and make one last save of archive. @@ -373,8 +371,8 @@ def _shut_down(self): self.log.debug('Learner joined.') self.interface.join() self.log.debug('Interface joined.') - self.save_archive() - + self.save_archive() + def print_results(self): ''' Print results from optimization run to the logs @@ -393,7 +391,7 @@ def print_results(self): def _optimization_routine(self): ''' - Runs controller main loop. Gives parameters to experiment and saves costs returned. + Runs controller main loop. Gives parameters to experiment and saves costs returned. ''' self.log.debug('Start controller loop.') self.log.info('Run:' + str(self.num_in_costs +1)) @@ -411,61 +409,61 @@ def _optimization_routine(self): def _first_params(self): ''' - Checks queue to get first parameters. - + Checks queue to get first parameters. + Returns: Parameters for first experiment ''' return self.learner_params_queue.get() - + def _next_params(self): ''' Abstract method. - + When implemented should send appropriate information to learner and get next parameters. - + Returns: Parameters for next experiment. ''' pass - + class RandomController(Controller): ''' Controller that simply returns random variables for the next parameters. Costs are stored but do not influence future points picked. - + Args: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. **kwargs (Optional [dict]): Dictionary of options to be passed to Controller and Random Learner. - + ''' def __init__(self, interface,**kwargs): - + super(RandomController,self).__init__(interface, **kwargs) self.learner = mll.RandomLearner(start_datetime = self.start_datetime, learner_archive_filename=None, **self.remaining_kwargs) - + self._update_controller_with_learner_attributes() self.out_type.append('random') - - self.log.debug('Random controller init completed.') - + + self.log.debug('Random controller init completed.') + def _next_params(self): ''' Sends cost uncer and bad tuple to learner then gets next parameters. - + Returns: Parameters for next experiment. ''' self.learner_costs_queue.put(self.best_params) - return self.learner_params_queue.get() - + return self.learner_params_queue.get() + class NelderMeadController(Controller): ''' Controller for the Nelder-Mead solver. Suggests new parameters based on the Nelder-Mead algorithm. Can take no boundaries or hard boundaries. More details for the Nelder-Mead options are in the learners section. - + Args: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. @@ -473,14 +471,14 @@ class NelderMeadController(Controller): ''' def __init__(self, interface, **kwargs): - super(NelderMeadController,self).__init__(interface, **kwargs) - + super(NelderMeadController,self).__init__(interface, **kwargs) + self.learner = mll.NelderMeadLearner(start_datetime = self.start_datetime, **self.remaining_kwargs) - + self._update_controller_with_learner_attributes() self.out_type.append('nelder_mead') - + def _next_params(self): ''' Gets next parameters from Nelder-Mead learner. @@ -488,14 +486,14 @@ def _next_params(self): if self.curr_bad: cost = float('inf') else: - cost = self.curr_cost + cost = self.curr_cost self.learner_costs_queue.put(cost) return self.learner_params_queue.get() class DifferentialEvolutionController(Controller): ''' - Controller for the differential evolution learner. - + Controller for the differential evolution learner. + Args: params_out_queue (queue): Queue for parameters to next be run by experiment. costs_in_queue (queue): Queue for costs (and other details) that have been returned by experiment. @@ -503,14 +501,14 @@ class DifferentialEvolutionController(Controller): ''' def __init__(self, interface, **kwargs): - super(DifferentialEvolutionController,self).__init__(interface, **kwargs) - + super(DifferentialEvolutionController,self).__init__(interface, **kwargs) + self.learner = mll.DifferentialEvolutionLearner(start_datetime = self.start_datetime, **self.remaining_kwargs) - + self._update_controller_with_learner_attributes() self.out_type.append('differential_evolution') - + def _next_params(self): ''' Gets next parameters from differential evolution learner. @@ -518,57 +516,56 @@ def _next_params(self): if self.curr_bad: cost = float('inf') else: - cost = self.curr_cost + cost = self.curr_cost self.learner_costs_queue.put(cost) return self.learner_params_queue.get() -class MachineLearnerController(Controller): - ''' - Abstract Controller class for the machine learning based solvers. + +class GaussianProcessController(Controller): + ''' + Controller for the Gaussian Process solver. Primarily suggests new points from the Gaussian Process learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. + Args: interface (Interface): The interface to the experiment under optimization. - **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class and initial training learner. - + **kwargs (Optional [dict]): Dictionary of options to be passed to Controller parent class, initial training learner and Gaussian Process learner. + Keyword Args: - training_type (Optional [string]): The type for the initial training source can be 'random' for the random learner, 'nelder_mead' for the Nelder-Mead learner or 'differential_evolution' for the Differential Evolution learner. This learner is also called if the machine learning learner is too slow and a new point is needed. Default 'differential_evolution'. - num_training_runs (Optional [int]): The number of training runs to before starting the learner. If None, will be ten or double the number of parameters, whatever is larger. - no_delay (Optional [bool]): If True, there is never any delay between a returned cost and the next parameters to run for the experiment. In practice, this means if the machine learning learner has not prepared the next parameters in time the learner defined by the initial training source is used instead. If false, the controller will wait for the machine learning learner to predict the next parameters and there may be a delay between runs. + initial_training_source (Optional [string]): The type for the initial training source can be 'random' for the random learner or 'nelder_mead' for the Nelder-Mead learner. This leaner is also called if the Gaussian process learner is too slow and a new point is needed. Default 'random'. + num_training_runs (Optional [int]): The number of training runs to before starting the learner. If None, will by ten or double the number of parameters, whatever is larger. + no_delay (Optional [bool]): If True, there is never any delay between a returned cost and the next parameters to run for the experiment. In practice, this means if the gaussian process has not prepared the next parameters in time the learner defined by the initial training source is used instead. If false, the controller will wait for the gaussian process to predict the next parameters and there may be a delay between runs. ''' - - def __init__(self, interface, + + def __init__(self, interface, training_type='differential_evolution', - machine_learner_type='machine_learner', num_training_runs=None, no_delay=True, num_params=None, min_boundary=None, max_boundary=None, - trust_region=None, + trust_region=None, learner_archive_filename = mll.default_learner_archive_filename, learner_archive_file_type = mll.default_learner_archive_file_type, **kwargs): - - super(MachineLearnerController,self).__init__(interface, **kwargs) - self.machine_learner_type = machine_learner_type + super(GaussianProcessController,self).__init__(interface, **kwargs) self.last_training_cost = None self.last_training_bad = None self.last_training_run_flag = False - + if num_training_runs is None: if num_params is None: self.num_training_runs = 10 else: self.num_training_runs = max(10, 2*int(num_params)) else: - self.num_training_runs = int(num_training_runs) + self.num_training_runs = int(num_training_runs) if self.num_training_runs<=0: self.log.error('Number of training runs must be larger than zero:'+repr(self.num_training_runs)) raise ValueError self.no_delay = bool(no_delay) - + self.training_type = str(training_type) if self.training_type == 'random': self.learner = mll.RandomLearner(start_datetime=self.start_datetime, @@ -588,7 +585,7 @@ def __init__(self, interface, learner_archive_filename=None, learner_archive_file_type=learner_archive_file_type, **self.remaining_kwargs) - + elif self.training_type == 'differential_evolution': self.learner = mll.DifferentialEvolutionLearner(start_datetime=self.start_datetime, num_params=num_params, @@ -598,47 +595,52 @@ def __init__(self, interface, evolution_strategy='rand2', learner_archive_filename=None, learner_archive_file_type=learner_archive_file_type, - **self.remaining_kwargs) - + **self.remaining_kwargs) + else: - self.log.error('Unknown training type provided to machine learning controller:' + repr(training_type)) - + self.log.error('Unknown training type provided to Gaussian process controller:' + repr(training_type)) + self.archive_dict.update({'training_type':self.training_type}) self._update_controller_with_learner_attributes() - - - def _update_controller_with_machine_learner_attributes(self): - - self.ml_learner_params_queue = self.ml_learner.params_out_queue - self.ml_learner_costs_queue = self.ml_learner.costs_in_queue - self.end_ml_learner = self.ml_learner.end_event - self.new_params_event = self.ml_learner.new_params_event - self.remaining_kwargs = self.ml_learner.remaining_kwargs - self.generation_num = self.ml_learner.generation_num - - + + self.gp_learner = mll.GaussianProcessLearner(start_datetime=self.start_datetime, + num_params=num_params, + min_boundary=min_boundary, + max_boundary=max_boundary, + trust_region=trust_region, + learner_archive_filename=learner_archive_filename, + learner_archive_file_type=learner_archive_file_type, + **self.remaining_kwargs) + + self.gp_learner_params_queue = self.gp_learner.params_out_queue + self.gp_learner_costs_queue = self.gp_learner.costs_in_queue + self.end_gp_learner = self.gp_learner.end_event + self.new_params_event = self.gp_learner.new_params_event + self.remaining_kwargs = self.gp_learner.remaining_kwargs + self.generation_num = self.gp_learner.generation_num + def _put_params_and_out_dict(self, params): ''' Override _put_params_and_out_dict function, used when the training learner creates parameters. Makes the defualt param_type the training type and sets last_training_run_flag. ''' - super(MachineLearnerController,self)._put_params_and_out_dict(params, param_type=self.training_type) - self.last_training_run_flag = True - + super(GaussianProcessController,self)._put_params_and_out_dict(params, param_type=self.training_type) + self.last_training_run_flag = True + def _get_cost_and_in_dict(self): ''' - Call _get_cost_and_in_dict() of parent Controller class. But also sends cost to machine learning learner and saves the cost if the parameters came from a trainer. - + Call _get_cost_and_in_dict() of parent Controller class. But also sends cost to Gaussian process learner and saves the cost if the parameters came from a trainer. + ''' - super(MachineLearnerController,self)._get_cost_and_in_dict() + super(GaussianProcessController,self)._get_cost_and_in_dict() if self.last_training_run_flag: self.last_training_cost = self.curr_cost self.last_training_bad = self.curr_bad self.last_training_run_flag = False - self.ml_learner_costs_queue.put((self.curr_params, + self.gp_learner_costs_queue.put((self.curr_params, self.curr_cost, self.curr_uncer, self.curr_bad)) - + def _next_params(self): ''' Gets next parameters from training learner. @@ -648,30 +650,30 @@ def _next_params(self): if self.last_training_bad: cost = float('inf') else: - cost = self.last_training_cost + cost = self.last_training_cost self.learner_costs_queue.put(cost) temp = self.learner_params_queue.get() - + elif self.training_type == 'random': #Copied from RandomController self.learner_costs_queue.put(self.best_params) - temp = self.learner_params_queue.get() - + temp = self.learner_params_queue.get() + else: self.log.error('Unknown training type called. THIS SHOULD NOT HAPPEN') return temp - + def _start_up(self): ''' Runs pararent method and also starts training_learner. ''' - super(MachineLearnerController,self)._start_up() - self.log.debug('ML learner started.') - self.ml_learner.start() + super(GaussianProcessController,self)._start_up() + self.log.debug('GP learner started.') + self.gp_learner.start() def _optimization_routine(self): ''' - Overrides _optimization_routine. Uses the parent routine for the training runs. Implements a customized _optimization_routine when running the machine learning learner. + Overrides _optimization_routine. Uses the parent routine for the training runs. Implements a customized _optimization_rountine when running the Gaussian Process learner. ''' #Run the training runs using the standard optimization routine. self.log.debug('Starting training optimization.') @@ -680,62 +682,59 @@ def _optimization_routine(self): self._put_params_and_out_dict(next_params) self.save_archive() self._get_cost_and_in_dict() - while (self.num_in_costs < self.num_training_runs) and self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) next_params = self._next_params() self._put_params_and_out_dict(next_params) self.save_archive() self._get_cost_and_in_dict() - + if self.check_end_conditions(): #Start last training run self.log.info('Run:' + str(self.num_in_costs +1)) next_params = self._next_params() self._put_params_and_out_dict(next_params) - - self.log.debug('Starting ML optimization.') + + self.log.debug('Starting GP optimization.') self.new_params_event.set() self.save_archive() self._get_cost_and_in_dict() self.log.debug('End training runs.') - - ml_consec = 0 - ml_count = 0 - + + gp_consec = 0 + gp_count = 0 + while self.check_end_conditions(): self.log.info('Run:' + str(self.num_in_costs +1)) - if ml_consec==self.generation_num or (self.no_delay and self.ml_learner_params_queue.empty()): + if gp_consec==self.generation_num or (self.no_delay and self.gp_learner_params_queue.empty()): next_params = self._next_params() self._put_params_and_out_dict(next_params) - ml_consec = 0 + gp_consec = 0 else: - next_params = self.ml_learner_params_queue.get() - super(MachineLearnerController,self)._put_params_and_out_dict(next_params, param_type=self.machine_learner_type) - ml_consec += 1 - ml_count += 1 - + next_params = self.gp_learner_params_queue.get() + super(GaussianProcessController,self)._put_params_and_out_dict(next_params, param_type='gaussian_process') + gp_consec += 1 + gp_count += 1 + + if gp_count%self.generation_num == 2: + self.new_params_event.set() + self.save_archive() self._get_cost_and_in_dict() - - if ml_count==self.generation_num: - self.new_params_event.set() - ml_count = 0 - - + def _shut_down(self): ''' - Shutdown and clean up resources of the machine learning controller. + Shutdown and clean up resources of the Gaussian process controller. ''' - self.log.debug('ML learner end set.') - self.end_ml_learner.set() - self.ml_learner.join() - - self.log.debug('ML learner joined') + self.log.debug('GP learner end set.') + self.end_gp_learner.set() + self.gp_learner.join() + + self.log.debug('GP learner joined') last_dict = None - while not self.ml_learner_params_queue.empty(): - last_dict = self.ml_learner_params_queue.get_nowait() + while not self.gp_learner_params_queue.empty(): + last_dict = self.gp_learner_params_queue.get_nowait() if isinstance(last_dict, dict): try: self.predicted_best_parameters = last_dict['predicted_best_parameters'] @@ -752,22 +751,19 @@ def _shut_down(self): pass self.archive_dict.update(last_dict) else: - if self.ml_learner.predict_global_minima_at_end or self.ml_learner.predict_local_minima_at_end: - self.log.info('Machine learning learner did not provide best and/or all minima.') - super(MachineLearnerController,self)._shut_down() - + if self.gp_learner.predict_global_minima_at_end or self.gp_learner.predict_local_minima_at_end: + self.log.info('GP Learner did not provide best and/or all minima.') + super(GaussianProcessController,self)._shut_down() + def print_results(self): ''' - Adds some additional output to the results specific to controller. + Adds some additional output to the results specific to controller. ''' - super(MachineLearnerController,self).print_results() + super(GaussianProcessController,self).print_results() try: self.log.info('Predicted best parameters:' + str(self.predicted_best_parameters)) - try: - errorstring = ' +/- ' + str(self.predicted_best_uncertainty) - except AttributeError: - errorstring = '' - self.log.info('Predicted best cost:' + str(self.predicted_best_cost) + errorstring) + self.log.info('Predicted best cost:' + str(self.predicted_best_cost) + ' +/- ' + str(self.predicted_best_uncertainty)) + except AttributeError: pass try: @@ -775,89 +771,7 @@ def print_results(self): except AttributeError: pass -class GaussianProcessController(MachineLearnerController): - ''' - Controller for the Gaussian Process solver. Primarily suggests new points from the Gaussian Process learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. - - Args: - interface (Interface): The interface to the experiment under optimization. - **kwargs (Optional [dict]): Dictionary of options to be passed to MachineLearnerController parent class and Gaussian Process learner. - - Keyword Args: - - ''' - - def __init__(self, interface, - num_params=None, - min_boundary=None, - max_boundary=None, - trust_region=None, - learner_archive_filename = mll.default_learner_archive_filename, - learner_archive_file_type = mll.default_learner_archive_file_type, - **kwargs): - - super(GaussianProcessController,self).__init__(interface, - machine_learner_type='gaussian_process', - num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - trust_region=trust_region, - learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **kwargs) - - self.ml_learner = mll.GaussianProcessLearner(start_datetime=self.start_datetime, - num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - trust_region=trust_region, - learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **self.remaining_kwargs) - - self._update_controller_with_machine_learner_attributes() - -class NeuralNetController(MachineLearnerController): - ''' - Controller for the Neural Net solver. Primarily suggests new points from the Neural Net learner. However, during the initial few runs it must rely on a different optimization algorithm to get some points to seed the learner. - - Args: - interface (Interface): The interface to the experiment under optimization. - **kwargs (Optional [dict]): Dictionary of options to be passed to MachineLearnerController parent class and Neural Net learner. - - Keyword Args: - - ''' - - def __init__(self, interface, - num_params=None, - min_boundary=None, - max_boundary=None, - trust_region=None, - learner_archive_filename = mll.default_learner_archive_filename, - learner_archive_file_type = mll.default_learner_archive_file_type, - **kwargs): - - super(NeuralNetController,self).__init__(interface, - machine_learner_type='neural_net', - num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - trust_region=trust_region, - learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **kwargs) - - self.ml_learner = mll.NeuralNetLearner(start_datetime=self.start_datetime, - num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - trust_region=trust_region, - learner_archive_filename=learner_archive_filename, - learner_archive_file_type=learner_archive_file_type, - **self.remaining_kwargs) - - self._update_controller_with_machine_learner_attributes() - + + \ No newline at end of file diff --git a/mloop/learners.py b/mloop/learners.py index ad6e5b1..9f7cea7 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -1,5 +1,5 @@ ''' -Module of learners used to determine what parameters to try next given previous cost evaluations. +Module of learners used to determine what parameters to try next given previous cost evaluations. Each learner is created and controlled by a controller. ''' @@ -21,26 +21,26 @@ import multiprocessing as mp learner_thread_count = 0 -default_learner_archive_filename = 'learner_archive' +default_learner_archive_filename = 'learner_archive' default_learner_archive_file_type = 'txt' class LearnerInterrupt(Exception): ''' - Exception that is raised when the learner is ended with the end flag or event. + Exception that is raised when the learner is ended with the end flag or event. ''' def __init__(self): ''' Create LearnerInterrupt. ''' super(LearnerInterrupt,self).__init__() - + class Learner(): ''' Base class for all learners. Contains default boundaries and some useful functions that all learners use. - - The class that inherits from this class should also inherit from threading.Thread or multiprocessing.Process, depending if you need the learner to be a genuine parallel process or not. - + + The class that inherits from this class should also inherit from threading.Thread or multiprocessing.Process, depending if you need the learner to be a genuine parallel process or not. + Keyword Args: num_params (Optional [int]): The number of parameters to be optimized. If None defaults to 1. Default None. min_boundary (Optional [array]): Array with minimimum values allowed for each parameter. Note if certain values have no minimum value you can set them to -inf for example [-1, 2, float('-inf')] is a valid min_boundary. If None sets all the boundaries to '-1'. Default None. @@ -49,36 +49,36 @@ class Learner(): learner_archive_file_type (Optional [string]): File type for archive. Can be either 'txt' a human readable text file, 'pkl' a python dill file, 'mat' a matlab file or None if there is no archive. Default 'mat'. log_level (Optional [int]): Level for the learners logger. If None, set to warning. Default None. start_datetime (Optional [datetime]): Start date time, if None, is automatically generated. - + Attributes: params_out_queue (queue): Queue for parameters created by learner. costs_in_queue (queue): Queue for costs to be used by learner. end_event (event): Event to trigger end of learner. ''' - - def __init__(self, + + def __init__(self, num_params=None, - min_boundary=None, - max_boundary=None, + min_boundary=None, + max_boundary=None, learner_archive_filename=default_learner_archive_filename, learner_archive_file_type=default_learner_archive_file_type, start_datetime=None, **kwargs): super(Learner,self).__init__() - + global learner_thread_count - learner_thread_count += 1 + learner_thread_count += 1 self.log = logging.getLogger(__name__ + '.' + str(learner_thread_count)) - + self.learner_wait=float(1) - + self.remaining_kwargs = kwargs - + self.params_out_queue = mp.Queue() self.costs_in_queue = mp.Queue() self.end_event = mp.Event() - + if num_params is None: self.log.warning('num_params not provided, setting to default value of 1.') self.num_params = 1 @@ -91,7 +91,6 @@ def __init__(self, self.min_boundary = np.full((self.num_params,), -1.0) else: self.min_boundary = np.array(min_boundary, dtype=np.float) - if self.min_boundary.shape != (self.num_params,): self.log.error('min_boundary array the wrong shape:' + repr(self.min_boundary.shape)) raise ValueError @@ -122,7 +121,7 @@ def __init__(self, os.makedirs(mlu.archive_foldername) self.learner_archive_filename =str(learner_archive_filename) self.total_archive_filename = mlu.archive_foldername + self.learner_archive_filename + '_' + mlu.datetime_to_string(self.start_datetime) + '.' + self.learner_archive_file_type - + self.archive_dict = {'archive_type':'learner', 'num_params':self.num_params, 'min_boundary':self.min_boundary, @@ -136,28 +135,28 @@ def check_num_params(self,param): Check the number of parameters is right. ''' return param.shape == (self.num_params,) - + def check_in_boundary(self,param): ''' Check give parameters are within stored boundaries - + Args: param (array): array of parameters - + Returns: bool : True if the parameters are within boundaries, False otherwise. ''' param = np.array(param) testbool = np.all(param >= self.min_boundary) and np.all(param <= self.max_boundary) return testbool - + def check_in_diff_boundary(self,param): ''' Check given distances are less than the boundaries - + Args: param (array): array of distances - + Returns: bool : True if the distances are smaller or equal to boundaries, False otherwise. ''' @@ -167,11 +166,11 @@ def check_in_diff_boundary(self,param): def put_params_and_get_cost(self, params, **kwargs): ''' - Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. - + Send parameters to queue and whatever additional keywords. Saves sent variables in appropriate storage arrays. + Args: params (array) : array of values to be sent to file - + Returns: cost from the cost queue ''' @@ -197,7 +196,7 @@ def put_params_and_get_cost(self, params, **kwargs): raise LearnerInterrupt #self.log.debug('Learner cost='+repr(cost)) return cost - + def save_archive(self): ''' Save the archive associated with the learner class. Only occurs if the filename for the archive is not None. Saves with the format previously set. @@ -205,19 +204,19 @@ def save_archive(self): self.update_archive() if self.learner_archive_filename is not None: mlu.save_dict_to_file(self.archive_dict, self.total_archive_filename, self.learner_archive_file_type) - + def update_archive(self): ''' Abstract method for update to the archive. To be implemented by child class. ''' pass - + def _set_trust_region(self,trust_region): ''' - Sets trust region properties for learner that have this. Common function for learners with trust regions. - + Sets trust region properties for learner that have this. Common function for learners with trust regions. + Args: - trust_region (float or array): Property defines the trust region. + trust_region (float or array): Property defines the trust region. ''' if trust_region is None: self.trust_region = float('nan') @@ -232,7 +231,7 @@ def _set_trust_region(self,trust_region): raise ValueError else: self.trust_region = np.array(trust_region, dtype=float) - + if self.has_trust_region: if not self.check_num_params(self.trust_region): self.log.error('Shape of the trust_region does not match the number of parameters:' + repr(self.trust_region)) @@ -243,7 +242,7 @@ def _set_trust_region(self,trust_region): if not self.check_in_diff_boundary(self.trust_region): self.log.error('The trust_region must be smaller than the range of the boundaries:' + repr(self.trust_region)) raise ValueError - + def _shut_down(self): ''' Shut down and perform one final save of learner. @@ -254,24 +253,24 @@ def _shut_down(self): class RandomLearner(Learner, threading.Thread): ''' Random learner. Simply generates new parameters randomly with a uniform distribution over the boundaries. Learner is perhaps a misnomer for this class. - + Args: - **kwargs (Optional dict): Other values to be passed to Learner. - + **kwargs (Optional dict): Other values to be passed to Learner. + Keyword Args: min_boundary (Optional [array]): If set to None, overrides default learner values and sets it to a set of value 0. Default None. max_boundary (Optional [array]): If set to None overides default learner values and sets it to an array of value 1. Default None. first_params (Optional [array]): The first parameters to test. If None will just randomly sample the initial condition. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. ''' - - def __init__(self, + + def __init__(self, trust_region=None, first_params=None, **kwargs): - + super(RandomLearner,self).__init__(**kwargs) - + if not np.all(self.diff_boundary>0.0): self.log.error('All elements of max_boundary are not larger than min_boundary') raise ValueError @@ -288,13 +287,13 @@ def __init__(self, if not self.check_in_boundary(self.first_params): self.log.error('first_params is not in the boundary:' + repr(self.first_params)) raise ValueError - + self._set_trust_region(trust_region) - + self.archive_dict.update({'archive_type':'random_learner'}) - + self.log.debug('Random learner init completed.') - + def run(self): ''' Puts the next parameters on the queue which are randomly picked from a uniform distribution between the minimum and maximum boundaries when a cost is added to the cost queue. @@ -304,7 +303,7 @@ def run(self): next_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary else: next_params = self.first_params - while not self.end_event.is_set(): + while not self.end_event.is_set(): try: centre_params = self.put_params_and_get_cost(next_params) except LearnerInterrupt: @@ -316,46 +315,46 @@ def run(self): next_params = temp_min + nr.rand(self.num_params) * (temp_max - temp_min) else: next_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary - + self._shut_down() self.log.debug('Ended Random Learner') - + class NelderMeadLearner(Learner, threading.Thread): ''' Nelder-Mead learner. Executes the Nelder-Mead learner algorithm and stores the needed simplex to estimate the next points. - + Args: params_out_queue (queue): Queue for parameters from controller. costs_in_queue (queue): Queue for costs for nelder learner. The queue should be populated with cost (float) corresponding to the last parameter sent from the Nelder-Mead Learner. Can be a float('inf') if it was a bad run. end_event (event): Event to trigger end of learner. - + Keyword Args: initial_simplex_corner (Optional [array]): Array for the initial set of parameters, which is the lowest corner of the initial simplex. If None the initial parameters are randomly sampled if the boundary conditions are provided, or all are set to 0 if boundary conditions are not provided. initial_simplex_displacements (Optional [array]): Array used to construct the initial simplex. Each array is the positive displacement of the parameters above the init_params. If None and there are no boundary conditions, all are set to 1. If None and there are boundary conditions assumes the initial conditions are scaled. Default None. - initial_simplex_scale (Optional [float]): Creates a simplex using a the boundary conditions and the scaling factor provided. If None uses the init_simplex if provided. If None and init_simplex is not provided, but boundary conditions are is set to 0.5. Default None. - + initial_simplex_scale (Optional [float]): Creates a simplex using a the boundary conditions and the scaling factor provided. If None uses the init_simplex if provided. If None and init_simplex is not provided, but boundary conditions are is set to 0.5. Default None. + Attributes: init_simplex_corner (array): Parameters for the corner of the initial simple used. - init_simplex_disp (array): Parameters for the displacements about the simplex corner used to create the initial simple. + init_simplex_disp (array): Parameters for the displacements about the simplex corner used to create the initial simple. simplex_params (array): Parameters of the current simplex simplex_costs (array): Costs associated with the parameters of the current simplex - + ''' - def __init__(self, - initial_simplex_corner=None, - initial_simplex_displacements=None, + def __init__(self, + initial_simplex_corner=None, + initial_simplex_displacements=None, initial_simplex_scale=None, **kwargs): - + super(NelderMeadLearner,self).__init__(**kwargs) - + self.num_boundary_hits = 0 self.rho = 1 self.chi = 2 self.psi = 0.5 self.sigma = 0.5 - + if initial_simplex_displacements is None and initial_simplex_scale is None: self.init_simplex_disp = self.diff_boundary * 0.6 self.init_simplex_disp[self.init_simplex_disp==float('inf')] = 1 @@ -369,7 +368,7 @@ def __init__(self, self.init_simplex_disp = np.array(initial_simplex_displacements, dtype=float) else: self.log.error('initial_simplex_displacements and initial_simplex_scale can not both be provided simultaneous.') - + if not self.check_num_params(self.init_simplex_disp): self.log.error('There is the wrong number of elements in the initial simplex displacement:' + repr(self.init_simplex_disp)) raise ValueError @@ -379,7 +378,7 @@ def __init__(self, if not self.check_in_diff_boundary(self.init_simplex_disp): self.log.error('Initial simplex displacements must be within boundaries. init_simplex_disp:'+ repr(self.init_simplex_disp) + '. diff_boundary:' +repr(self.diff_boundary)) raise ValueError - + if initial_simplex_corner is None: diff_roll = (self.diff_boundary - self.init_simplex_disp) * nr.rand(self.num_params) diff_roll[diff_roll==float('+inf')]= 0 @@ -388,42 +387,42 @@ def __init__(self, self.init_simplex_corner += diff_roll else: self.init_simplex_corner = np.array(initial_simplex_corner, dtype=float) - + if not self.check_num_params(self.init_simplex_corner): - self.log.error('There is the wrong number of elements in the initial simplex corner:' + repr(self.init_simplex_corner)) + self.log.error('There is the wrong number of elements in the initial simplex corner:' + repr(self.init_simplex_corner)) if not self.check_in_boundary(self.init_simplex_corner): self.log.error('Initial simplex corner outside of boundaries:' + repr(self.init_simplex_corner)) raise ValueError - + if not np.all(np.isfinite(self.init_simplex_corner + self.init_simplex_disp)): self.log.error('Initial simplex corner and simplex are not finite numbers. init_simplex_corner:'+ repr(self.init_simplex_corner) + '. init_simplex_disp:' +repr(self.init_simplex_disp)) raise ValueError if not self.check_in_boundary(self.init_simplex_corner + self.init_simplex_disp): self.log.error('Largest boundary of simplex not inside the boundaries:' + repr(self.init_simplex_corner + self.init_simplex_disp)) raise ValueError - + self.simplex_params = np.zeros((self.num_params + 1, self.num_params), dtype=float) self.simplex_costs = np.zeros((self.num_params + 1,), dtype=float) - + self.archive_dict.update({'archive_type':'nelder_mead_learner', 'initial_simplex_corner':self.init_simplex_corner, 'initial_simplex_displacements':self.init_simplex_disp}) - + self.log.debug('Nelder-Mead learner init completed.') - + def run(self): ''' - Runs Nelder-Mead algorithm to produce new parameters given costs, until end signal is given. + Runs Nelder-Mead algorithm to produce new parameters given costs, until end signal is given. ''' - + self.log.info('Starting Nelder Mead Learner') - + N = int(self.num_params) - + one2np1 = list(range(1, N + 1)) - + self.simplex_params[0] = self.init_simplex_corner - + try: self.simplex_costs[0] = self.put_params_and_get_cost(self.init_simplex_corner) except ValueError: @@ -432,7 +431,7 @@ def run(self): except LearnerInterrupt: self.log.info('Ended Nelder-Mead before end of simplex') return - + for k in range(0, N): y = np.array(self.init_simplex_corner, copy=True) y[k] = y[k] + self.init_simplex_disp[k] @@ -445,22 +444,22 @@ def run(self): except LearnerInterrupt: self.log.info('Ended Nelder-Mead before end of simplex') return - + self.simplex_costs[k + 1] = f - + ind = np.argsort(self.simplex_costs) self.simplex_costs = np.take(self.simplex_costs, ind, 0) # sort so sim[0,:] has the lowest function value self.simplex_params = np.take(self.simplex_params, ind, 0) - + while not self.end_event.is_set(): - + xbar = np.add.reduce(self.simplex_params[:-1], 0) / N xr = (1 +self.rho) * xbar -self.rho * self.simplex_params[-1] - + if self.check_in_boundary(xr): try: - fxr = self.put_params_and_get_cost(xr) + fxr = self.put_params_and_get_cost(xr) except ValueError: self.log.error('Outside of boundary on first reduce. THIS SHOULD NOT HAPPEN') raise @@ -471,12 +470,12 @@ def run(self): fxr = float('inf') self.num_boundary_hits+=1 self.log.debug('Hit boundary (reflect): '+str(self.num_boundary_hits)+' times.') - + doshrink = 0 - + if fxr < self.simplex_costs[0]: xe = (1 +self.rho *self.chi) * xbar -self.rho *self.chi * self.simplex_params[-1] - + if self.check_in_boundary(xe): try: fxe = self.put_params_and_get_cost(xe) @@ -487,10 +486,10 @@ def run(self): break else: #Hit boundary so set the cost above maximum this ensures the algorithm does a contracting reflection - fxe = fxr+1.0 + fxe = fxr+1.0 self.num_boundary_hits+=1 self.log.debug('Hit boundary (expand): '+str(self.num_boundary_hits)+' times.') - + if fxe < fxr: self.simplex_params[-1] = xe self.simplex_costs[-1] = fxe @@ -542,11 +541,11 @@ def run(self): raise except LearnerInterrupt: break - + ind = np.argsort(self.simplex_costs) self.simplex_params = np.take(self.simplex_params, ind, 0) self.simplex_costs = np.take(self.simplex_costs, ind, 0) - + self._shut_down() self.log.info('Ended Nelder-Mead') @@ -559,43 +558,43 @@ def update_archive(self): class DifferentialEvolutionLearner(Learner, threading.Thread): ''' - Adaption of the differential evolution algorithm in scipy. - + Adaption of the differential evolution algorithm in scipy. + Args: params_out_queue (queue): Queue for parameters sent to controller. costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple end_event (event): Event to trigger end of learner. - + Keyword Args: first_params (Optional [array]): The first parameters to test. If None will just randomly sample the initial condition. Default None. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. evolution_strategy (Optional [string]): the differential evolution strategy to use, options are 'best1', 'best1', 'rand1' and 'rand2'. The default is 'best2'. population_size (Optional [int]): multiplier proportional to the number of parameters in a generation. The generation population is set to population_size * parameter_num. Default 15. mutation_scale (Optional [tuple]): The mutation scale when picking new points. Otherwise known as differential weight. When provided as a tuple (min,max) a mutation constant is picked randomly in the interval. Default (0.5,1.0). cross_over_probability (Optional [float]): The recombination constand or crossover probability, the probability a new points will be added to the population. restart_tolerance (Optional [float]): when the current population have a spread less than the initial tolerance, namely stdev(curr_pop) < restart_tolerance stdev(init_pop), it is likely the population is now in a minima, and so the search is started again. - + Attributes: has_trust_region (bool): Whether the learner has a trust region. - num_population_members (int): The number of parameters in a generation. + num_population_members (int): The number of parameters in a generation. params_generations (list): History of the parameters generations. A list of all the parameters in the population, for each generation created. costs_generations (list): History of the costs generations. A list of all the costs in the population, for each generation created. init_std (float): The initial standard deviation in costs of the population. Calucalted after sampling (or resampling) the initial population. curr_std (float): The current standard devation in costs of the population. Calculated after sampling each generation. ''' - def __init__(self, + def __init__(self, first_params = None, trust_region = None, - evolution_strategy='best1', + evolution_strategy='best1', population_size=15, - mutation_scale=(0.5, 1), - cross_over_probability=0.7, - restart_tolerance=0.01, + mutation_scale=(0.5, 1), + cross_over_probability=0.7, + restart_tolerance=0.01, **kwargs): - + super(DifferentialEvolutionLearner,self).__init__(**kwargs) - + if first_params is None: self.first_params = float('nan') else: @@ -606,9 +605,9 @@ def __init__(self, if not self.check_in_boundary(self.first_params): self.log.error('first_params is not in the boundary:' + repr(self.first_params)) raise ValueError - + self._set_trust_region(trust_region) - + if evolution_strategy == 'best1': self.mutation_func = self._best1 elif evolution_strategy == 'best2': @@ -620,7 +619,7 @@ def __init__(self, else: self.log.error('Please select a valid mutation strategy') raise ValueError - + self.evolution_strategy = evolution_strategy self.restart_tolerance = restart_tolerance @@ -629,29 +628,29 @@ def __init__(self, else: self.log.error('Mutation scale must be a tuple with (min,max) between 0 and 2. mutation_scale:' + repr(mutation_scale)) raise ValueError - + if cross_over_probability <= 1 and cross_over_probability >= 0: self.cross_over_probability = cross_over_probability else: self.log.error('Cross over probability must be between 0 and 1. cross_over_probability:' + repr(cross_over_probability)) - + if population_size >= 5: self.population_size = population_size else: self.log.error('Population size must be greater or equal to 5:' + repr(population_size)) - + self.num_population_members = self.population_size * self.num_params - + self.first_sample = True - + self.params_generations = [] self.costs_generations = [] self.generation_count = 0 - + self.min_index = 0 self.init_std = 0 self.curr_std = 0 - + self.archive_dict.update({'archive_type':'differential_evolution', 'evolution_strategy':self.evolution_strategy, 'mutation_scale':self.mutation_scale, @@ -662,26 +661,26 @@ def __init__(self, 'first_params':self.first_params, 'has_trust_region':self.has_trust_region, 'trust_region':self.trust_region}) - - + + def run(self): ''' Runs the Differential Evolution Learner. ''' try: - + self.generate_population() - + while not self.end_event.is_set(): - + self.next_generation() - + if self.curr_std < self.restart_tolerance * self.init_std: self.generate_population() - + except LearnerInterrupt: return - + def save_generation(self): ''' Save history of generations. @@ -689,29 +688,29 @@ def save_generation(self): self.params_generations.append(np.copy(self.population)) self.costs_generations.append(np.copy(self.population_costs)) self.generation_count += 1 - + def generate_population(self): ''' Sample a new random set of variables ''' - + self.population = [] self.population_costs = [] self.min_index = 0 - + if np.all(np.isfinite(self.first_params)) and self.first_sample: curr_params = self.first_params self.first_sample = False else: curr_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary - + curr_cost = self.put_params_and_get_cost(curr_params) - + self.population.append(curr_params) self.population_costs.append(curr_cost) - + for index in range(1, self.num_population_members): - + if self.has_trust_region: temp_min = np.maximum(self.min_boundary,self.population[self.min_index] - self.trust_region) temp_max = np.minimum(self.max_boundary,self.population[self.min_index] + self.trust_region) @@ -720,76 +719,76 @@ def generate_population(self): curr_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary curr_cost = self.put_params_and_get_cost(curr_params) - + self.population.append(curr_params) self.population_costs.append(curr_cost) - + if curr_cost < self.population_costs[self.min_index]: self.min_index = index - + self.population = np.array(self.population) self.population_costs = np.array(self.population_costs) - + self.init_std = np.std(self.population_costs) self.curr_std = self.init_std - + self.save_generation() - + def next_generation(self): ''' Evolve the population by a single generation ''' - + self.curr_scale = nr.uniform(self.mutation_scale[0], self.mutation_scale[1]) - + for index in range(self.num_population_members): - + curr_params = self.mutate(index) curr_cost = self.put_params_and_get_cost(curr_params) - + if curr_cost < self.population_costs[index]: self.population[index] = curr_params self.population_costs[index] = curr_cost - + if curr_cost < self.population_costs[self.min_index]: self.min_index = index - + self.curr_std = np.std(self.population_costs) - + self.save_generation() def mutate(self, index): ''' Mutate the parameters at index. - + Args: index (int): Index of the point to be mutated. ''' - + fill_point = nr.randint(0, self.num_params) candidate_params = self.mutation_func(index) crossovers = nr.rand(self.num_params) < self.cross_over_probability crossovers[fill_point] = True mutated_params = np.where(crossovers, candidate_params, self.population[index]) - + if self.has_trust_region: temp_min = np.maximum(self.min_boundary,self.population[self.min_index] - self.trust_region) temp_max = np.minimum(self.max_boundary,self.population[self.min_index] + self.trust_region) rand_params = temp_min + nr.rand(self.num_params) * (temp_max - temp_min) else: rand_params = self.min_boundary + nr.rand(self.num_params) * self.diff_boundary - + projected_params = np.where(np.logical_or(mutated_params < self.min_boundary, mutated_params > self.max_boundary), rand_params, mutated_params) - + return projected_params def _best1(self, index): ''' Use best parameters and two others to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1 = self.random_index_sample(index, 2) return (self.population[self.min_index] + self.curr_scale *(self.population[r0] - self.population[r1])) @@ -797,9 +796,9 @@ def _best1(self, index): def _rand1(self, index): ''' Use three random parameters to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1, r2 = self.random_index_sample(index, 3) return (self.population[r0] + self.curr_scale * (self.population[r1] - self.population[r2])) @@ -807,9 +806,9 @@ def _rand1(self, index): def _best2(self, index): ''' Use best parameters and four others to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1, r2, r3 = self.random_index_sample(index, 4) return self.population[self.min_index] + self.curr_scale * (self.population[r0] + self.population[r1] - self.population[r2] - self.population[r3]) @@ -817,9 +816,9 @@ def _best2(self, index): def _rand2(self, index): ''' Use five random parameters to generate mutation. - + Args: - index (int): Index of member to mutate. + index (int): Index of member to mutate. ''' r0, r1, r2, r3, r4 = self.random_index_sample(index, 5) return self.population[r0] + self.curr_scale * (self.population[r1] + self.population[r2] - self.population[r3] - self.population[r4]) @@ -827,7 +826,7 @@ def _rand2(self, index): def random_index_sample(self, index, num_picks): ''' Randomly select a num_picks of indexes, without index. - + Args: index(int): The index that is not included num_picks(int): The number of picks. @@ -835,7 +834,7 @@ def random_index_sample(self, index, num_picks): rand_indexes = list(range(self.num_population_members)) rand_indexes.remove(index) return random.sample(rand_indexes, num_picks) - + def update_archive(self): ''' Update the archive. @@ -849,51 +848,52 @@ def update_archive(self): 'generation_count':self.generation_count}) + class GaussianProcessLearner(Learner, mp.Process): ''' - Gaussian process learner. Generats new parameters based on a gaussian process fitted to all previous data. - + Gaussian process learner. Generats new parameters based on a gaussian process fitted to all previous data. + Args: params_out_queue (queue): Queue for parameters sent to controller. costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple end_event (event): Event to trigger end of learner. - + Keyword Args: length_scale (Optional [array]): The initial guess for length scale(s) of the gaussian process. The array can either of size one or the number of parameters or None. If it is size one, it is assumed all the correlation lengths are the same. If it is the number of the parameters then all the parameters have their own independent length scale. If it is None, it is assumed all the length scales should be independent and they are all given an initial value of 1. Default None. - cost_has_noise (Optional [bool]): If true the learner assumes there is common additive white noise that corrupts the costs provided. This noise is assumed to be on top of the uncertainty in the costs (if it is provided). If false, it is assumed that there is no noise in the cost (or if uncertainties are provided no extra noise beyond the uncertainty). Default True. + cost_has_noise (Optional [bool]): If true the learner assumes there is common additive white noise that corrupts the costs provided. This noise is assumed to be on top of the uncertainty in the costs (if it is provided). If false, it is assumed that there is no noise in the cost (or if uncertainties are provided no extra noise beyond the uncertainty). Default True. noise_level (Optional [float]): The initial guess for the noise level in the costs, is only used if cost_has_noise is true. Default 1.0. update_hyperparameters (Optional [bool]): Whether the length scales and noise estimate should be updated when new data is provided. Is set to true by default. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. + trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. default_bad_cost (Optional [float]): If a run is reported as bad and default_bad_cost is provided, the cost for the bad run is set to this default value. If default_bad_cost is None, then the worst cost received is set to all the bad runs. Default None. default_bad_uncertainty (Optional [float]): If a run is reported as bad and default_bad_uncertainty is provided, the uncertainty for the bad run is set to this default value. If default_bad_uncertainty is None, then the uncertainty is set to a tenth of the best to worst cost range. Default None. minimum_uncertainty (Optional [float]): The minimum uncertainty associated with provided costs. Must be above zero to avoid fitting errors. Default 1e-8. predict_global_minima_at_end (Optional [bool]): If True finds the global minima when the learner is ended. Does not if False. Default True. predict_local_minima_at_end (Optional [bool]): If True finds the all minima when the learner is ended. Does not if False. Default False. - + Attributes: all_params (array): Array containing all parameters sent to learner. all_costs (array): Array containing all costs sent to learner. all_uncers (array): Array containing all uncertainties sent to learner. - scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. + scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. bad_run_indexs (list): list of indexes to all runs that were marked as bad. best_cost (float): Minimum received cost, updated during execution. best_params (array): Parameters of best run. (reference to element in params array). - best_index (int): index of the best cost and params. + best_index (int): index of the best cost and params. worst_cost (float): Maximum received cost, updated during execution. worst_index (int): index to run with worst cost. cost_range (float): Difference between worst_cost and best_cost generation_num (int): Number of sets of parameters to generate each generation. Set to 5. length_scale_history (list): List of length scales found after each fit. - noise_level_history (list): List of noise levels found after each fit. + noise_level_history (list): List of noise levels found after each fit. fit_count (int): Counter for the number of times the gaussian process has been fit. cost_count (int): Counter for the number of costs, parameters and uncertainties added to learner. - params_count (int): Counter for the number of parameters asked to be evaluated by the learner. + params_count (int): Counter for the number of parameters asked to be evaluated by the learner. gaussian_process (GaussianProcessRegressor): Gaussian process that is fitted to data and used to make predictions - cost_scaler (StandardScaler): Scaler used to normalize the provided costs. - has_trust_region (bool): Whether the learner has a trust region. - ''' - - def __init__(self, + cost_scaler (StandardScaler): Scaler used to normalize the provided costs. + has_trust_region (bool): Whether the learner has a trust region. + ''' + + def __init__(self, length_scale = None, update_hyperparameters = True, cost_has_noise=True, @@ -907,49 +907,49 @@ def __init__(self, predict_global_minima_at_end = True, predict_local_minima_at_end = False, **kwargs): - + if gp_training_filename is not None: - + gp_training_filename = str(gp_training_filename) gp_training_file_type = str(gp_training_file_type) if not mlu.check_file_type_supported(gp_training_file_type): self.log.error('GP training file type not supported' + repr(gp_training_file_type)) - + self.training_dict = mlu.get_dict_from_file(gp_training_filename, gp_training_file_type) - + #Basic optimization settings num_params = int(self.training_dict['num_params']) - min_boundary = mlu.safe_cast_to_list(self.training_dict['min_boundary']) - max_boundary = mlu.safe_cast_to_list(self.training_dict['max_boundary']) + min_boundary = np.squeeze(np.array(self.training_dict['min_boundary'], dtype=float)) + max_boundary = np.squeeze(np.array(self.training_dict['max_boundary'], dtype=float)) #Configuration of the learner self.cost_has_noise = bool(self.training_dict['cost_has_noise']) - self.length_scale = mlu.safe_squeeze(self.training_dict['length_scale']) + self.length_scale = np.squeeze(np.array(self.training_dict['length_scale'])) self.length_scale_history = list(self.training_dict['length_scale_history']) self.noise_level = float(self.training_dict['noise_level']) self.noise_level_history = mlu.safe_cast_to_list(self.training_dict['noise_level_history']) - + #Counters self.costs_count = int(self.training_dict['costs_count']) self.fit_count = int(self.training_dict['fit_count']) self.params_count = int(self.training_dict['params_count']) - + #Data from previous experiment self.all_params = np.array(self.training_dict['all_params'], dtype=float) - self.all_costs = mlu.safe_squeeze(self.training_dict['all_costs']) - self.all_uncers = mlu.safe_squeeze(self.training_dict['all_uncers']) + self.all_costs = np.squeeze(np.array(self.training_dict['all_costs'], dtype=float)) + self.all_uncers = np.squeeze(np.array(self.training_dict['all_uncers'], dtype=float)) self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) #Derived properties self.best_cost = float(self.training_dict['best_cost']) - self.best_params = mlu.safe_squeeze(self.training_dict['best_params']) + self.best_params = np.squeeze(np.array(self.training_dict['best_params'], dtype=float)) self.best_index = int(self.training_dict['best_index']) self.worst_cost = float(self.training_dict['worst_cost']) self.worst_index = int(self.training_dict['worst_index']) self.cost_range = float(self.training_dict['cost_range']) try: - self.predicted_best_parameters = mlu.safe_squeeze(self.training_dict['predicted_best_parameters']) + self.predicted_best_parameters = np.squeeze(np.array(self.training_dict['predicted_best_parameters'])) self.predicted_best_cost = float(self.training_dict['predicted_best_cost']) self.predicted_best_uncertainty = float(self.training_dict['predicted_best_uncertainty']) self.has_global_minima = True @@ -957,7 +957,7 @@ def __init__(self, self.has_global_minima = False try: self.local_minima_parameters = list(self.training_dict['local_minima_parameters']) - + if isinstance(self.training_dict['local_minima_costs'], np.ndarray): self.local_minima_costs = list(np.squeeze(self.training_dict['local_minima_costs'])) else: @@ -966,20 +966,21 @@ def __init__(self, self.local_minima_uncers = list(np.squeeze(self.training_dict['local_minima_uncers'])) else: self.local_minima_uncers = list(self.training_dict['local_minima_uncers']) - + self.has_local_minima = True except KeyError: self.has_local_minima = False - + + super(GaussianProcessLearner,self).__init__(num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, + min_boundary=min_boundary, + max_boundary=max_boundary, **kwargs) - + else: - + super(GaussianProcessLearner,self).__init__(**kwargs) - + #Storage variables, archived self.all_params = np.array([], dtype=float) self.all_costs = np.array([], dtype=float) @@ -993,14 +994,14 @@ def __init__(self, self.cost_range = float('inf') self.length_scale_history = [] self.noise_level_history = [] - + self.costs_count = 0 self.fit_count = 0 self.params_count = 0 - + self.has_local_minima = False self.has_global_minima = False - + #Optional user set variables if length_scale is None: self.length_scale = np.ones((self.num_params,)) @@ -1008,29 +1009,32 @@ def __init__(self, self.length_scale = np.array(length_scale, dtype=float) self.noise_level = float(noise_level) self.cost_has_noise = bool(cost_has_noise) - - + + #Multiprocessor controls self.new_params_event = mp.Event() - + #Storage variables and counters self.search_params = [] self.scaled_costs = None self.cost_bias = None self.uncer_bias = None - + #Internal variable for bias function self.bias_func_cycle = 4 - self.bias_func_cost_factor = [1.0,1.0,1.0,1.0] + self.bias_func_cost_factor = [1.0,1.0,1.0,1.0] self.bias_func_uncer_factor =[0.0,1.0,2.0,3.0] self.generation_num = self.bias_func_cycle + if self.generation_num < 3: + self.log.error('Number in generation must be larger than 2.') + raise ValueError #Constants, limits and tolerances self.search_precision = 1.0e-6 self.parameter_searches = max(10,self.num_params) self.hyperparameter_searches = max(10,self.num_params) - self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty - + self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty + #Optional user set variables self.update_hyperparameters = bool(update_hyperparameters) self.predict_global_minima_at_end = bool(predict_global_minima_at_end) @@ -1045,7 +1049,7 @@ def __init__(self, self.default_bad_uncertainty = None self.minimum_uncertainty = float(minimum_uncertainty) self._set_trust_region(trust_region) - + #Checks of variables if self.length_scale.size == 1: self.length_scale = float(self.length_scale) @@ -1072,17 +1076,17 @@ def __init__(self, if self.minimum_uncertainty <= 0: self.log.error('Minimum uncertainty must be larger than zero for the learner.') raise ValueError - + self.create_gaussian_process() - + #Search bounds self.search_min = self.min_boundary self.search_max = self.max_boundary self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - + self.cost_scaler = skp.StandardScaler() - + self.archive_dict.update({'archive_type':'gaussian_process_learner', 'cost_has_noise':self.cost_has_noise, 'length_scale_history':self.length_scale_history, @@ -1100,11 +1104,11 @@ def __init__(self, 'has_trust_region':self.has_trust_region, 'predict_global_minima_at_end':self.predict_global_minima_at_end, 'predict_local_minima_at_end':self.predict_local_minima_at_end}) - + #Remove logger so gaussian process can be safely picked for multiprocessing on Windows self.log = None - - + + def create_gaussian_process(self): ''' Create the initial Gaussian process. @@ -1117,12 +1121,11 @@ def create_gaussian_process(self): self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,n_restarts_optimizer=self.hyperparameter_searches) else: self.gaussian_process = skg.GaussianProcessRegressor(kernel=gp_kernel,optimizer=None) - + def wait_for_new_params_event(self): ''' - Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. + Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. ''' - self.log.debug("Waiting for new params") while not self.end_event.is_set(): if self.new_params_event.wait(timeout=self.learner_wait): self.new_params_event.clear() @@ -1132,26 +1135,26 @@ def wait_for_new_params_event(self): else: self.log.debug('GaussianProcessLearner end signal received. Ending') raise LearnerInterrupt - - + + def get_params_and_costs(self): ''' - Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. + Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. ''' if self.costs_in_queue.empty(): self.log.error('Gaussian process asked for new parameters but no new costs were provided.') raise ValueError - + new_params = [] new_costs = [] new_uncers = [] new_bads = [] update_bads_flag = False - + while not self.costs_in_queue.empty(): (param, cost, uncer, bad) = self.costs_in_queue.get_nowait() self.costs_count +=1 - + if bad: new_bads.append(self.costs_count-1) if self.bad_defaults_set: @@ -1160,7 +1163,7 @@ def get_params_and_costs(self): else: cost = self.worst_cost uncer = self.cost_range*self.bad_uncer_frac - + param = np.array(param, dtype=float) if not self.check_num_params(param): self.log.error('Incorrect number of parameters provided to Gaussian process learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) @@ -1171,7 +1174,7 @@ def get_params_and_costs(self): if uncer < 0: self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) uncer = max(float(uncer), self.minimum_uncertainty) - + cost_change_flag = False if cost > self.worst_cost: self.worst_cost = cost @@ -1186,12 +1189,12 @@ def get_params_and_costs(self): self.cost_range = self.worst_cost - self.best_cost if not self.bad_defaults_set: update_bads_flag = True - + new_params.append(param) new_costs.append(cost) new_uncers.append(uncer) - - + + if self.all_params.size==0: self.all_params = np.array(new_params, dtype=float) self.all_costs = np.array(new_costs, dtype=float) @@ -1200,21 +1203,21 @@ def get_params_and_costs(self): self.all_params = np.concatenate((self.all_params, np.array(new_params, dtype=float))) self.all_costs = np.concatenate((self.all_costs, np.array(new_costs, dtype=float))) self.all_uncers = np.concatenate((self.all_uncers, np.array(new_uncers, dtype=float))) - + self.bad_run_indexs.append(new_bads) - + if self.all_params.shape != (self.costs_count,self.num_params): self.log('Saved GP params are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_params)) if self.all_costs.shape != (self.costs_count,): self.log('Saved GP costs are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_costs)) if self.all_uncers.shape != (self.costs_count,): self.log('Saved GP uncertainties are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_uncers)) - + if update_bads_flag: self.update_bads() - + self.update_search_region() - + def update_bads(self): ''' Best and/or worst costs have changed, update the values associated with bad runs accordingly. @@ -1222,7 +1225,7 @@ def update_bads(self): for index in self.bad_run_indexs: self.all_costs[index] = self.worst_cost self.all_uncers[index] = self.cost_range*self.bad_uncer_frac - + def update_search_region(self): ''' If trust boundaries is not none, updates the search boundaries based on the defined trust region. @@ -1232,7 +1235,7 @@ def update_search_region(self): self.search_max = np.minimum(self.best_params + self.trust_region, self.max_boundary) self.search_diff = self.search_max - self.search_min self.search_region = list(zip(self.search_min, self.search_max)) - + def update_search_params(self): ''' Update the list of parameters to use for the next search. @@ -1241,7 +1244,7 @@ def update_search_params(self): self.search_params.append(self.best_params) for _ in range(self.parameter_searches): self.search_params.append(self.search_min + nr.uniform(size=self.num_params) * self.search_diff) - + def update_archive(self): ''' Update the archive. @@ -1260,9 +1263,10 @@ def update_archive(self): 'params_count':self.params_count, 'update_hyperparameters':self.update_hyperparameters, 'length_scale':self.length_scale, - 'noise_level':self.noise_level}) - + 'noise_level':self.noise_level}) + + def fit_gaussian_process(self): ''' Fit the Gaussian process to the current data @@ -1275,14 +1279,14 @@ def fit_gaussian_process(self): self.scaled_uncers = self.all_uncers * self.cost_scaler.scale_ self.gaussian_process.alpha_ = self.scaled_uncers self.gaussian_process.fit(self.all_params,self.scaled_costs) - + if self.update_hyperparameters: - + self.fit_count += 1 self.gaussian_process.kernel = self.gaussian_process.kernel_ - + last_hyperparameters = self.gaussian_process.kernel.get_params() - + if self.cost_has_noise: self.length_scale = last_hyperparameters['k1__length_scale'] if isinstance(self.length_scale, float): @@ -1293,30 +1297,30 @@ def fit_gaussian_process(self): else: self.length_scale = last_hyperparameters['length_scale'] self.length_scale_history.append(self.length_scale) - - + + def update_bias_function(self): ''' Set the constants for the cost bias function. ''' self.cost_bias = self.bias_func_cost_factor[self.params_count%self.bias_func_cycle] self.uncer_bias = self.bias_func_uncer_factor[self.params_count%self.bias_func_cycle] - + def predict_biased_cost(self,params): ''' Predicts the biased cost at the given parameters. The bias function is: biased_cost = cost_bias*pred_cost - uncer_bias*pred_uncer - + Returns: pred_bias_cost (float): Biased cost predicted at the given parameters ''' (pred_cost, pred_uncer) = self.gaussian_process.predict(params[np.newaxis,:], return_std=True) return self.cost_bias*pred_cost - self.uncer_bias*pred_uncer - + def find_next_parameters(self): ''' Returns next parameters to find. Increments counters and bias function appropriately. - + Return: next_params (array): Returns next parameters from biased cost search. ''' @@ -1331,7 +1335,7 @@ def find_next_parameters(self): next_params = result.x next_cost = result.fun return next_params - + def run(self): ''' Starts running the Gaussian process learner. When the new parameters event is triggered, reads the cost information provided and updates the Gaussian process with the information. Then searches the Gaussian process for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. @@ -1339,7 +1343,7 @@ def run(self): #logging to the main log file from a process (as apposed to a thread) in cpython is currently buggy on windows and/or python 2.7 #current solution is to only log to the console for warning and above from a process self.log = mp.log_to_stderr(logging.WARNING) - + try: while not self.end_event.is_set(): #self.log.debug('Learner waiting for new params event') @@ -1373,36 +1377,36 @@ def run(self): self.params_out_queue.put(end_dict) self._shut_down() self.log.debug('Ended Gaussian Process Learner') - + def predict_cost(self,params): ''' - Produces a prediction of cost from the gaussian process at params. - + Produces a prediction of cost from the gaussian process at params. + Returns: float : Predicted cost at paramters ''' return self.gaussian_process.predict(params[np.newaxis,:]) - + def find_global_minima(self): ''' Performs a quick search for the predicted global minima from the learner. Does not return any values, but creates the following attributes. - + Attributes: predicted_best_parameters (array): the parameters for the predicted global minima predicted_best_cost (float): the cost at the predicted global minima predicted_best_uncertainty (float): the uncertainty of the predicted global minima ''' self.log.debug('Started search for predicted global minima.') - + self.predicted_best_parameters = None self.predicted_best_scaled_cost = float('inf') self.predicted_best_scaled_uncertainty = None - + search_params = [] search_params.append(self.best_params) for _ in range(self.parameter_searches): search_params.append(self.min_boundary + nr.uniform(size=self.num_params) * self.diff_boundary) - + search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in search_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) @@ -1412,608 +1416,59 @@ def find_global_minima(self): self.predicted_best_parameters = curr_best_params self.predicted_best_scaled_cost = curr_best_cost self.predicted_best_scaled_uncertainty = curr_best_uncer - + self.predicted_best_cost = self.cost_scaler.inverse_transform(self.predicted_best_scaled_cost) self.predicted_best_uncertainty = self.predicted_best_scaled_uncertainty / self.cost_scaler.scale_ - + self.archive_dict.update({'predicted_best_parameters':self.predicted_best_parameters, 'predicted_best_scaled_cost':self.predicted_best_scaled_cost, 'predicted_best_scaled_uncertainty':self.predicted_best_scaled_uncertainty, 'predicted_best_cost':self.predicted_best_cost, 'predicted_best_uncertainty':self.predicted_best_uncertainty}) - + self.has_global_minima = True - self.log.debug('Predicted global minima found.') - + self.log.debug('Predicted global minima found.') + def find_local_minima(self): ''' - Performs a comprehensive search of the learner for all predicted local minima (and hence the global as well) in the landscape. Note, this can take a very long time. - + Performs a comprehensive search of the learner for all predicted local minima (and hence the global as well) in the landscape. Note, this can take a very long time. + Attributes: local_minima_parameters (list): list of all the parameters for local minima. local_minima_costs (list): list of all the costs at local minima. local_minima_uncers (list): list of all the uncertainties at local minima. - + ''' self.log.info('Searching for all minima.') - + self.minima_tolerance = 10*self.search_precision - + self.number_of_local_minima = 0 self.local_minima_parameters = [] self.local_minima_costs = [] self.local_minima_uncers = [] - + search_bounds = list(zip(self.min_boundary, self.max_boundary)) for start_params in self.all_params: result = so.minimize(self.predict_cost, start_params, bounds = search_bounds, tol=self.search_precision) curr_minima_params = result.x (curr_minima_cost,curr_minima_uncer) = self.gaussian_process.predict(curr_minima_params[np.newaxis,:],return_std=True) - if all( not np.all( np.abs(params - curr_minima_params) < self.minima_tolerance ) for params in self.local_minima_parameters): + if all( not np.all( np.abs(params - curr_minima_params) < self.minima_tolerance ) for params in self.local_minima_parameters): #Non duplicate point so add to the list self.number_of_local_minima += 1 self.local_minima_parameters.append(curr_minima_params) self.local_minima_costs.append(curr_minima_cost) self.local_minima_uncers.append(curr_minima_uncer) - + self.archive_dict.update({'number_of_local_minima':self.number_of_local_minima, 'local_minima_parameters':self.local_minima_parameters, 'local_minima_costs':self.local_minima_costs, 'local_minima_uncers':self.local_minima_uncers}) - - self.has_local_minima = True - self.log.info('Search completed') - - -class NeuralNetLearner(Learner, mp.Process): - ''' - Shell of Neural Net Learner. - - Args: - params_out_queue (queue): Queue for parameters sent to controller. - costs_in_queue (queue): Queue for costs for gaussian process. This must be tuple - end_event (event): Event to trigger end of learner. - - Keyword Args: - length_scale (Optional [array]): The initial guess for length scale(s) of the gaussian process. The array can either of size one or the number of parameters or None. If it is size one, it is assumed all the correlation lengths are the same. If it is the number of the parameters then all the parameters have their own independent length scale. If it is None, it is assumed all the length scales should be independent and they are all given an initial value of 1. Default None. - trust_region (Optional [float or array]): The trust region defines the maximum distance the learner will travel from the current best set of parameters. If None, the learner will search everywhere. If a float, this number must be between 0 and 1 and defines maximum distance the learner will venture as a percentage of the boundaries. If it is an array, it must have the same size as the number of parameters and the numbers define the maximum absolute distance that can be moved along each direction. - default_bad_cost (Optional [float]): If a run is reported as bad and default_bad_cost is provided, the cost for the bad run is set to this default value. If default_bad_cost is None, then the worst cost received is set to all the bad runs. Default None. - default_bad_uncertainty (Optional [float]): If a run is reported as bad and default_bad_uncertainty is provided, the uncertainty for the bad run is set to this default value. If default_bad_uncertainty is None, then the uncertainty is set to a tenth of the best to worst cost range. Default None. - minimum_uncertainty (Optional [float]): The minimum uncertainty associated with provided costs. Must be above zero to avoid fitting errors. Default 1e-8. - predict_global_minima_at_end (Optional [bool]): If True finds the global minima when the learner is ended. Does not if False. Default True. - predict_local_minima_at_end (Optional [bool]): If True finds all minima when the learner is ended. Does not if False. Default False. - - Attributes: - TODO: Update these. - all_params (array): Array containing all parameters sent to learner. - all_costs (array): Array containing all costs sent to learner. - all_uncers (array): Array containing all uncertainties sent to learner. - scaled_costs (array): Array contaning all the costs scaled to have zero mean and a standard deviation of 1. Needed for training the gaussian process. - bad_run_indexs (list): list of indexes to all runs that were marked as bad. - best_cost (float): Minimum received cost, updated during execution. - best_params (array): Parameters of best run. (reference to element in params array). - best_index (int): index of the best cost and params. - worst_cost (float): Maximum received cost, updated during execution. - worst_index (int): index to run with worst cost. - cost_range (float): Difference between worst_cost and best_cost - generation_num (int): Number of sets of parameters to generate each generation. Set to 5. - length_scale_history (list): List of length scales found after each fit. - noise_level_history (list): List of noise levels found after each fit. - fit_count (int): Counter for the number of times the gaussian process has been fit. - cost_count (int): Counter for the number of costs, parameters and uncertainties added to learner. - params_count (int): Counter for the number of parameters asked to be evaluated by the learner. - gaussian_process (GaussianProcessRegressor): Gaussian process that is fitted to data and used to make predictions - cost_scaler (StandardScaler): Scaler used to normalize the provided costs. - has_trust_region (bool): Whether the learner has a trust region. - ''' - - def __init__(self, - update_hyperparameters = True, - trust_region=None, - default_bad_cost = None, - default_bad_uncertainty = None, - nn_training_filename =None, - nn_training_file_type ='txt', - predict_global_minima_at_end = True, - predict_local_minima_at_end = False, - **kwargs): - - if nn_training_filename is not None: - - nn_training_filename = str(nn_training_filename) - nn_training_file_type = str(nn_training_file_type) - if not mlu.check_file_type_supported(nn_training_file_type): - self.log.error('GP training file type not supported' + repr(nn_training_file_type)) - - self.training_dict = mlu.get_dict_from_file(nn_training_filename, nn_training_file_type) - - #Basic optimization settings - num_params = int(self.training_dict['num_params']) - min_boundary = mlu.safe_cast_to_list(self.training_dict['min_boundary']) - max_boundary = mlu.safe_cast_to_list(self.training_dict['max_boundary']) - - #Counters - self.costs_count = int(self.training_dict['costs_count']) - self.fit_count = int(self.training_dict['fit_count']) - self.params_count = int(self.training_dict['params_count']) - - #Data from previous experiment - self.all_params = np.array(self.training_dict['all_params'], dtype=float) - self.all_costs = mlu.safe_squeeze(self.training_dict['all_costs']) - self.all_uncers = mlu.safe_squeeze(self.training_dict['all_uncers']) - - self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) - - #Derived properties - self.best_cost = float(self.training_dict['best_cost']) - self.best_params = mlu.safe_squeeze(self.training_dict['best_params']) - self.best_index = int(self.training_dict['best_index']) - self.worst_cost = float(self.training_dict['worst_cost']) - self.worst_index = int(self.training_dict['worst_index']) - self.cost_range = float(self.training_dict['cost_range']) - - #Configuration of the fake neural net learner - self.length_scale = mlu.safe_squeeze(self.training_dict['length_scale']) - self.noise_level = float(self.training_dict['noise_level']) - - - try: - self.predicted_best_parameters = mlu.safe_squeeze(self.training_dict['predicted_best_parameters']) - self.predicted_best_cost = float(self.training_dict['predicted_best_cost']) - self.predicted_best_uncertainty = float(self.training_dict['predicted_best_uncertainty']) - self.has_global_minima = True - except KeyError: - self.has_global_minima = False - try: - self.local_minima_parameters = list(self.training_dict['local_minima_parameters']) - - if isinstance(self.training_dict['local_minima_costs'], np.ndarray): - self.local_minima_costs = list(np.squeeze(self.training_dict['local_minima_costs'])) - else: - self.local_minima_costs = list(self.training_dict['local_minima_costs']) - if isinstance(self.training_dict['local_minima_uncers'], np.ndarray): - self.local_minima_uncers = list(np.squeeze(self.training_dict['local_minima_uncers'])) - else: - self.local_minima_uncers = list(self.training_dict['local_minima_uncers']) - - self.has_local_minima = True - except KeyError: - self.has_local_minima = False - - super(NeuralNetLearner,self).__init__(num_params=num_params, - min_boundary=min_boundary, - max_boundary=max_boundary, - **kwargs) - else: - - super(NeuralNetLearner,self).__init__(**kwargs) - - #Storage variables, archived - self.all_params = np.array([], dtype=float) - self.all_costs = np.array([], dtype=float) - self.all_uncers = np.array([], dtype=float) - self.bad_run_indexs = [] - self.best_cost = float('inf') - self.best_params = float('nan') - self.best_index = 0 - self.worst_cost = float('-inf') - self.worst_index = 0 - self.cost_range = float('inf') - self.length_scale_history = [] - self.noise_level_history = [] - self.costs_count = 0 - self.fit_count = 0 - self.params_count = 0 - - self.has_local_minima = False - self.has_global_minima = False - - #Multiprocessor controls - self.new_params_event = mp.Event() - - #Storage variables and counters - self.search_params = [] - self.scaled_costs = None - - #Constants, limits and tolerances - self.generation_num = 1 - self.search_precision = 1.0e-6 - self.parameter_searches = max(10,self.num_params) - self.hyperparameter_searches = max(10,self.num_params) - self.bad_uncer_frac = 0.1 #Fraction of cost range to set a bad run uncertainty - - #Optional user set variables - self.update_hyperparameters = bool(update_hyperparameters) - self.predict_global_minima_at_end = bool(predict_global_minima_at_end) - self.predict_local_minima_at_end = bool(predict_local_minima_at_end) - if default_bad_cost is not None: - self.default_bad_cost = float(default_bad_cost) - else: - self.default_bad_cost = None - if default_bad_uncertainty is not None: - self.default_bad_uncertainty = float(default_bad_uncertainty) - else: - self.default_bad_uncertainty = None - if (self.default_bad_cost is None) and (self.default_bad_uncertainty is None): - self.bad_defaults_set = False - elif (self.default_bad_cost is not None) and (self.default_bad_uncertainty is not None): - self.bad_defaults_set = True - else: - self.log.error('Both the default cost and uncertainty must be set for a bad run or they must both be set to None.') - raise ValueError - - self._set_trust_region(trust_region) - - #Search bounds - self.search_min = self.min_boundary - self.search_max = self.max_boundary - self.search_diff = self.search_max - self.search_min - self.search_region = list(zip(self.search_min, self.search_max)) - - self.length_scale = 1 - self.cost_has_noise = True - self.noise_level = 1 - - # Set up the scaler to do nothing. - # TODO: Figure out how to use scaling for the NN (it's a bit difficult because we don't - # completely re-train each time, and don't want the scaling changing without doing a complete - # re-train). - self.cost_scaler = skp.StandardScaler(with_mean=False, with_std=False) - - self.archive_dict.update({'archive_type':'nerual_net_learner', - 'bad_run_indexs':self.bad_run_indexs, - 'generation_num':self.generation_num, - 'search_precision':self.search_precision, - 'parameter_searches':self.parameter_searches, - 'hyperparameter_searches':self.hyperparameter_searches, - 'bad_uncer_frac':self.bad_uncer_frac, - 'trust_region':self.trust_region, - 'has_trust_region':self.has_trust_region, - 'predict_global_minima_at_end':self.predict_global_minima_at_end, - 'predict_local_minima_at_end':self.predict_local_minima_at_end}) - - #Remove logger so gaussian process can be safely picked for multiprocessing on Windows - self.log = None - - def create_neural_net(self): - ''' - Creates the neural net. Must be called from the same process as fit_neural_net, predict_cost and predict_costs_from_param_array. - ''' - import mloop.nnlearner as mlnn - self.neural_net_impl = mlnn.NeuralNetImpl(self.num_params) - - def fit_neural_net(self): - ''' - Determine the appropriate number of layers for the NN given the data. - - Fit the Neural Net with the appropriate topology to the data - - ''' - self.scaled_costs = self.cost_scaler.fit_transform(self.all_costs[:,np.newaxis])[:,0] - - self.neural_net_impl.fit_neural_net(self.all_params, self.scaled_costs) - - def predict_cost(self,params): - ''' - Produces a prediction of cost from the gaussian process at params. - - Returns: - float : Predicted cost at paramters - ''' - return self.neural_net_impl.predict_cost(params) - - def predict_cost_gradient(self,params): - ''' - Produces a prediction of the gradient of the cost function at params. - - Returns: - float : Predicted gradient at paramters - ''' - # scipy.optimize.minimize doesn't seem to like a 32-bit Jacobian, so we convert to 64 - return np.float64(self.neural_net_impl.predict_cost_gradient(params)) - - - def predict_costs_from_param_array(self,params): - ''' - Produces a prediction of costs from an array of params. - - Returns: - float : Predicted cost at paramters - ''' - # TODO: Can do this more efficiently. - return [self.predict_cost(param) for param in params] - - - def wait_for_new_params_event(self): - ''' - Waits for a new parameters event and starts a new parameter generation cycle. Also checks end event and will break if it is triggered. - ''' - while not self.end_event.is_set(): - if self.new_params_event.wait(timeout=self.learner_wait): - self.new_params_event.clear() - break - else: - continue - else: - self.log.debug('NeuralNetLearner end signal received. Ending') - raise LearnerInterrupt - - - def get_params_and_costs(self): - ''' - Get the parameters and costs from the queue and place in their appropriate all_[type] arrays. Also updates bad costs, best parameters, and search boundaries given trust region. - ''' - if self.costs_in_queue.empty(): - self.log.error('Neural network asked for new parameters but no new costs were provided.') - raise ValueError - - new_params = [] - new_costs = [] - new_uncers = [] - new_bads = [] - update_bads_flag = False - - while not self.costs_in_queue.empty(): - (param, cost, uncer, bad) = self.costs_in_queue.get_nowait() - self.costs_count +=1 - - if bad: - new_bads.append(self.costs_count-1) - if self.bad_defaults_set: - cost = self.default_bad_cost - uncer = self.default_bad_uncertainty - else: - cost = self.worst_cost - uncer = self.cost_range*self.bad_uncer_frac - - param = np.array(param, dtype=float) - if not self.check_num_params(param): - self.log.error('Incorrect number of parameters provided to neural network learner:' + repr(param) + '. Number of parameters:' + str(self.num_params)) - raise ValueError - if not self.check_in_boundary(param): - self.log.warning('Parameters provided to neural network learner not in boundaries:' + repr(param)) - cost = float(cost) - if uncer < 0: - self.log.error('Provided uncertainty must be larger or equal to zero:' + repr(uncer)) - uncer = max(float(uncer), self.minimum_uncertainty) - - cost_change_flag = False - if cost > self.worst_cost: - self.worst_cost = cost - self.worst_index = self.costs_count-1 - cost_change_flag = True - if cost < self.best_cost: - self.best_cost = cost - self.best_params = param - self.best_index = self.costs_count-1 - cost_change_flag = True - if cost_change_flag: - self.cost_range = self.worst_cost - self.best_cost - if not self.bad_defaults_set: - update_bads_flag = True - - new_params.append(param) - new_costs.append(cost) - new_uncers.append(uncer) - - - if self.all_params.size==0: - self.all_params = np.array(new_params, dtype=float) - self.all_costs = np.array(new_costs, dtype=float) - self.all_uncers = np.array(new_uncers, dtype=float) - else: - self.all_params = np.concatenate((self.all_params, np.array(new_params, dtype=float))) - self.all_costs = np.concatenate((self.all_costs, np.array(new_costs, dtype=float))) - self.all_uncers = np.concatenate((self.all_uncers, np.array(new_uncers, dtype=float))) - - self.bad_run_indexs.append(new_bads) - - if self.all_params.shape != (self.costs_count,self.num_params): - self.log('Saved NN params are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_params)) - if self.all_costs.shape != (self.costs_count,): - self.log('Saved NN costs are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_costs)) - if self.all_uncers.shape != (self.costs_count,): - self.log('Saved NN uncertainties are the wrong size. THIS SHOULD NOT HAPPEN:' + repr(self.all_uncers)) - - if update_bads_flag: - self.update_bads() - - self.update_search_region() - - def update_bads(self): - ''' - Best and/or worst costs have changed, update the values associated with bad runs accordingly. - ''' - for index in self.bad_run_indexs: - self.all_costs[index] = self.worst_cost - self.all_uncers[index] = self.cost_range*self.bad_uncer_frac - - def update_search_region(self): - ''' - If trust boundaries is not none, updates the search boundaries based on the defined trust region. - ''' - if self.has_trust_region: - self.search_min = np.maximum(self.best_params - self.trust_region, self.min_boundary) - self.search_max = np.minimum(self.best_params + self.trust_region, self.max_boundary) - self.search_diff = self.search_max - self.search_min - self.search_region = list(zip(self.search_min, self.search_max)) - - def update_search_params(self): - ''' - Update the list of parameters to use for the next search. - ''' - self.search_params = [] - self.search_params.append(self.best_params) - for _ in range(self.parameter_searches): - self.search_params.append(self.search_min + nr.uniform(size=self.num_params) * self.search_diff) - - def update_archive(self): - ''' - Update the archive. - ''' - self.archive_dict.update({'all_params':self.all_params, - 'all_costs':self.all_costs, - 'all_uncers':self.all_uncers, - 'best_cost':self.best_cost, - 'best_params':self.best_params, - 'best_index':self.best_index, - 'worst_cost':self.worst_cost, - 'worst_index':self.worst_index, - 'cost_range':self.cost_range, - 'fit_count':self.fit_count, - 'costs_count':self.costs_count, - 'params_count':self.params_count, - 'update_hyperparameters':self.update_hyperparameters, - 'length_scale':self.length_scale, - 'noise_level':self.noise_level}) - - def find_next_parameters(self): - ''' - Returns next parameters to find. Increments counters appropriately. - - Return: - next_params (array): Returns next parameters from cost search. - ''' - # TODO: We could implement some other type of biasing. - self.params_count += 1 - self.update_search_params() - next_params = None - next_cost = float('inf') - for start_params in self.search_params: - result = so.minimize(fun = self.predict_cost, - x0 = start_params, - jac = self.predict_cost_gradient, - bounds = self.search_region, - tol = self.search_precision) - if result.fun < next_cost: - next_params = result.x - next_cost = result.fun - return next_params - - def run(self): - ''' - Starts running the neural network learner. When the new parameters event is triggered, reads the cost information provided and updates the neural network with the information. Then searches the neural network for new optimal parameters to test based on the biased cost. Parameters to test next are put on the output parameters queue. - ''' - #logging to the main log file from a process (as apposed to a thread) in cpython is currently buggy on windows and/or python 2.7 - #current solution is to only log to the console for warning and above from a process - self.log = mp.log_to_stderr(logging.WARNING) - - # The network needs to be created in the same process in which it runs - self.create_neural_net() - - try: - while not self.end_event.is_set(): - self.log.debug('Learner waiting for new params event') - self.save_archive() - self.wait_for_new_params_event() - self.log.debug('NN learner reading costs') - self.get_params_and_costs() - self.fit_neural_net() - for _ in range(self.generation_num): - self.log.debug('Neural network learner generating parameter:'+ str(self.params_count+1)) - next_params = self.find_next_parameters() - self.params_out_queue.put(next_params) - if self.end_event.is_set(): - raise LearnerInterrupt() - except LearnerInterrupt: - pass - # TODO: Fix this. We can't just do what's here because the costs queue might be empty, but - # we should get anything left in it and do one last train. - #if self.predict_global_minima_at_end or self.predict_local_minima_at_end: - # self.get_params_and_costs() - # self.fit_neural_net() - end_dict = {} - if self.predict_global_minima_at_end: - self.find_global_minima() - end_dict.update({'predicted_best_parameters':self.predicted_best_parameters, - 'predicted_best_cost':self.predicted_best_cost}) - if self.predict_local_minima_at_end: - self.find_local_minima() - end_dict.update({'local_minima_parameters':self.local_minima_parameters, - 'local_minima_costs':self.local_minima_costs}) - self.params_out_queue.put(end_dict) - self._shut_down() - self.log.debug('Ended neural network learner') - - def find_global_minima(self): - ''' - Performs a quick search for the predicted global minima from the learner. Does not return any values, but creates the following attributes. - - Attributes: - predicted_best_parameters (array): the parameters for the predicted global minima - predicted_best_cost (float): the cost at the predicted global minima - ''' - self.log.debug('Started search for predicted global minima.') - - self.predicted_best_parameters = None - self.predicted_best_scaled_cost = float('inf') - - search_params = [] - search_params.append(self.best_params) - for _ in range(self.parameter_searches): - search_params.append(self.min_boundary + nr.uniform(size=self.num_params) * self.diff_boundary) - - search_bounds = list(zip(self.min_boundary, self.max_boundary)) - for start_params in search_params: - result = so.minimize(fun = self.predict_cost, - x0 = start_params, - jac = self.predict_cost_gradient, - bounds = search_bounds, - tol = self.search_precision) - curr_best_params = result.x - curr_best_cost = result.fun - if curr_best_cost Date: Fri, 3 Mar 2017 09:44:58 +1100 Subject: [PATCH 31/33] Previous data files can now be imported Added support for previous data files to be imported into a gaussian process learner. --- mloop/learners.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mloop/learners.py b/mloop/learners.py index 9f7cea7..f283411 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -971,8 +971,10 @@ def __init__(self, except KeyError: self.has_local_minima = False - - super(GaussianProcessLearner,self).__init__(num_params=num_params, + if 'num_params' in kwargs: + super(GaussianProcessLearner,self).__init__(**kwargs) + else: + super(GaussianProcessLearner,self).__init__(num_params=num_params, min_boundary=min_boundary, max_boundary=max_boundary, **kwargs) From 47c16bfa06d773b7f38372917f190a51f363959d Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Fri, 24 Mar 2017 15:42:47 +1100 Subject: [PATCH 32/33] Updated bug in visualizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed a bug where an attribute wasn’t present in the learner class. Was a problem when attempting to plot the visualizations from a file. --- mloop/visualizations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mloop/visualizations.py b/mloop/visualizations.py index 9f47743..763b649 100644 --- a/mloop/visualizations.py +++ b/mloop/visualizations.py @@ -340,7 +340,7 @@ def plot_params_vs_generations(self): def create_gaussian_process_learner_visualizations(filename, file_type='pkl', plot_cross_sections=True, - plot_all_minima_vs_cost=True, + plot_all_minima_vs_cost=False, plot_hyperparameters_vs_run=True): ''' Runs the plots from a gaussian process learner file. @@ -351,7 +351,7 @@ def create_gaussian_process_learner_visualizations(filename, Keyword Args: file_type (Optional [string]): File type 'pkl' pickle, 'mat' matlab or 'txt' text. plot_cross_sections (Optional [bool]): If True plot predict landscape cross sections, else do not. Default True. - plot_all_minima_vs_cost (Optional [bool]): If True plot all minima parameters versus cost number, False does not. If None it will only make the plots if all minima were previously calculated. Default None. + plot_all_minima_vs_cost (Optional [bool]): If True plot all minima parameters versus cost number, False does not. If None it will only make the plots if all minima were previously calculated. Default False. ''' visualization = GaussianProcessVisualizer(filename, file_type=file_type) if plot_cross_sections: @@ -486,8 +486,7 @@ def plot_all_minima_vs_cost(self): ''' Produce figure of the all the local minima versus cost. ''' - if not self.has_all_minima: - self.find_all_minima() + self.find_all_minima() global figure_counter, legend_loc figure_counter += 1 plt.figure(figure_counter) From 3bc037458862118d13703c13271cea3204c7377d Mon Sep 17 00:00:00 2001 From: Michael Hush Date: Wed, 29 Mar 2017 14:40:29 +1100 Subject: [PATCH 33/33] Fixed one param visualization bug and typos in documentation When optimizing one parameter, there were some issues reimporting the saved files for the visualizations to work. This was due to the problematic corner case of zero D or one D with one element arrays in numpy. This has now been sanitized. Also fixed some critical typos in the documentation. --- docs/interfaces.rst | 2 +- docs/tutorials.rst | 2 +- examples/shell_interface_config.txt | 2 +- mloop/learners.py | 29 +++++++++++------------------ mloop/utilities.py | 19 +++++++++++++++++++ 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/docs/interfaces.rst b/docs/interfaces.rst index 497348e..926fb9d 100644 --- a/docs/interfaces.rst +++ b/docs/interfaces.rst @@ -54,7 +54,7 @@ Shell interface The shell interface is used when experiments can be run from a command in a shell. M-LOOP will still need to be configured and executed in the same manner described for a file interface as describe in :ref:`tutorial `. The only difference is how M-LOOP starts the experiment and reads data. To use this interface you must include the following options:: - interface='shell' + interface_type='shell' command='./run_exp' params_args_type='direct' diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 4fdefb8..a7d0dd9 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -31,7 +31,7 @@ There are three stages: M-LOOP - M-LOOP first looks for the configuration file *exp_input.txt*, which contains options like the number of parameters and their limits, in the folder it is executed, then starts the optimization process. + M-LOOP first looks for the configuration file *exp_config.txt*, which contains options like the number of parameters and their limits, in the folder it is executed, then starts the optimization process. 2. M-LOOP controls and optimizes the experiment by exchanging files written to disk. M-LOOP produces a file called *exp_input.txt* which contains a variable params with the next parameters to be run by the experiment. The experiment is expected to run an experiment with these parameters and measure the resultant cost. The experiment should then write the file *exp_output.txt* which contains at least the variable cost which quantifies the performance of that experimental run, and optionally, the variables uncer (for uncertainty) and bad (if the run failed). This process is repeated many times until the halting condition is met. diff --git a/examples/shell_interface_config.txt b/examples/shell_interface_config.txt index e988077..7fa786e 100644 --- a/examples/shell_interface_config.txt +++ b/examples/shell_interface_config.txt @@ -3,4 +3,4 @@ interface_type = 'shell' #The type of interface command = 'python shell_script.py' #The command for the command line to run the experiment to get a cost from the parameters -params_args_type = 'direct' #The format of the parameters when providing them on the command line. 'direct' simply appends them, e.g. python CLIscript.py 7 2 1, 'named' names each parameter, e.g. python CLIscript.py --param1 7 --param2 2 --param3 1 \ No newline at end of file +params_args_type = 'direct' #The format of the parameters when providing them on the command line. 'direct' simply appends them, e.g. python shell_script.py 7 2 1, 'named' names each parameter, e.g. python shell_script.py --param1 7 --param2 2 --param3 1 \ No newline at end of file diff --git a/mloop/learners.py b/mloop/learners.py index f283411..b4e8b76 100644 --- a/mloop/learners.py +++ b/mloop/learners.py @@ -919,12 +919,12 @@ def __init__(self, #Basic optimization settings num_params = int(self.training_dict['num_params']) - min_boundary = np.squeeze(np.array(self.training_dict['min_boundary'], dtype=float)) - max_boundary = np.squeeze(np.array(self.training_dict['max_boundary'], dtype=float)) + min_boundary = mlu.safe_cast_to_array(self.training_dict['min_boundary']) + max_boundary = mlu.safe_cast_to_array(self.training_dict['max_boundary']) #Configuration of the learner self.cost_has_noise = bool(self.training_dict['cost_has_noise']) - self.length_scale = np.squeeze(np.array(self.training_dict['length_scale'])) + self.length_scale = mlu.safe_cast_to_array(self.training_dict['length_scale']) self.length_scale_history = list(self.training_dict['length_scale_history']) self.noise_level = float(self.training_dict['noise_level']) self.noise_level_history = mlu.safe_cast_to_list(self.training_dict['noise_level_history']) @@ -935,37 +935,30 @@ def __init__(self, self.params_count = int(self.training_dict['params_count']) #Data from previous experiment - self.all_params = np.array(self.training_dict['all_params'], dtype=float) - self.all_costs = np.squeeze(np.array(self.training_dict['all_costs'], dtype=float)) - self.all_uncers = np.squeeze(np.array(self.training_dict['all_uncers'], dtype=float)) + self.all_params = np.array(self.training_dict['all_params']) + self.all_costs = mlu.safe_cast_to_array(self.training_dict['all_costs']) + self.all_uncers = mlu.safe_cast_to_array(self.training_dict['all_uncers']) self.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) #Derived properties self.best_cost = float(self.training_dict['best_cost']) - self.best_params = np.squeeze(np.array(self.training_dict['best_params'], dtype=float)) + self.best_params = mlu.safe_cast_to_array(self.training_dict['best_params']) self.best_index = int(self.training_dict['best_index']) self.worst_cost = float(self.training_dict['worst_cost']) self.worst_index = int(self.training_dict['worst_index']) self.cost_range = float(self.training_dict['cost_range']) try: - self.predicted_best_parameters = np.squeeze(np.array(self.training_dict['predicted_best_parameters'])) + self.predicted_best_parameters = mlu.safe_cast_to_array(self.training_dict['predicted_best_parameters']) self.predicted_best_cost = float(self.training_dict['predicted_best_cost']) self.predicted_best_uncertainty = float(self.training_dict['predicted_best_uncertainty']) self.has_global_minima = True except KeyError: self.has_global_minima = False try: - self.local_minima_parameters = list(self.training_dict['local_minima_parameters']) - - if isinstance(self.training_dict['local_minima_costs'], np.ndarray): - self.local_minima_costs = list(np.squeeze(self.training_dict['local_minima_costs'])) - else: - self.local_minima_costs = list(self.training_dict['local_minima_costs']) - if isinstance(self.training_dict['local_minima_uncers'], np.ndarray): - self.local_minima_uncers = list(np.squeeze(self.training_dict['local_minima_uncers'])) - else: - self.local_minima_uncers = list(self.training_dict['local_minima_uncers']) + self.local_minima_parameters = mlu.safe_cast_to_list(self.training_dict['local_minima_parameters']) + self.local_minima_costs = mlu.safe_cast_to_list(self.training_dict['local_minima_costs']) + self.local_minima_uncers = mlu.safe_cast_to_list(self.training_dict['local_minima_uncers']) self.has_local_minima = True except KeyError: diff --git a/mloop/utilities.py b/mloop/utilities.py index cd35aee..2ec4b26 100644 --- a/mloop/utilities.py +++ b/mloop/utilities.py @@ -175,6 +175,25 @@ def check_file_type_supported(file_type): ''' return file_type == 'mat' or 'txt' or 'pkl' +def safe_cast_to_array(in_array): + ''' + Attempts to safely cast the input to an array. Takes care of border cases + + Args: + in_array (array or equivalent): The array (or otherwise) to be converted to a list. + + Returns: + array : array that has been squeezed and 0-D cases change to 1-D cases + + ''' + + out_array = np.squeeze(np.array(in_array)) + + if out_array.shape == (): + out_array = np.array([out_array[()]]) + + return out_array + def safe_cast_to_list(in_array): ''' Attempts to safely cast a numpy array to a list, if not a numpy array just casts to list on the object.