diff --git a/mloop/controllers.py b/mloop/controllers.py index 5e6e303..0db4a2a 100644 --- a/mloop/controllers.py +++ b/mloop/controllers.py @@ -539,6 +539,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, @@ -548,9 +549,10 @@ def __init__(self, interface, 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.machine_learner_type = machine_learner_type + self.last_training_cost = None self.last_training_bad = None self.last_training_run_flag = False @@ -678,13 +680,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)) @@ -708,13 +711,14 @@ 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=self.ml_learner_name) + 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: self.new_params_event.set() - + ml_count = 0 + + self.save_archive() self._get_cost_and_in_dict() @@ -789,6 +793,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, @@ -797,7 +802,6 @@ 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, @@ -829,17 +833,17 @@ def __init__(self, interface, 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) - super(NeuralNetController,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_name = 'neural_net' 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 12f6ed5..ae56fa9 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 @@ -127,9 +128,9 @@ def __init__(self, '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. @@ -918,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_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 +936,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.bad_run_indexs = mlu.safe_cast_to_list(self.training_dict['bad_run_indexs']) - + 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 @@ -970,7 +971,6 @@ def __init__(self, except KeyError: self.has_local_minima = False - super(GaussianProcessLearner,self).__init__(num_params=num_params, min_boundary=min_boundary, max_boundary=max_boundary, @@ -1024,10 +1024,7 @@ 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 self.parameter_searches = max(10,self.num_params) @@ -1189,7 +1186,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) @@ -1266,7 +1263,6 @@ def update_archive(self): 'noise_level':self.noise_level}) - def fit_gaussian_process(self): ''' Fit the Gaussian process to the current data @@ -1521,42 +1517,107 @@ 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) - 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 - + + 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 - 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) @@ -1574,7 +1635,14 @@ 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) #Search bounds @@ -1587,17 +1655,7 @@ def __init__(self, self.cost_has_noise = True self.noise_level = 1 - # 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', + 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, @@ -1630,6 +1688,18 @@ def predict_cost(self,params): ''' return self.neural_net_impl.predict_cost(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 + return [] + + 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. @@ -1769,7 +1839,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): ''' 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