From 3d3d56c0e696d2a91dc2b79afa2ac66949a0ba8a Mon Sep 17 00:00:00 2001 From: Brandon Rohrer Date: Mon, 17 Sep 2018 10:28:27 -0400 Subject: [PATCH] Fixed bug in featurizer mapping from candidates to model features. --- becca/brain.py | 18 ++++--- becca/featurizer.py | 112 +++++++++++++++++++++++------------------- becca/input_filter.py | 22 +++++---- becca/model.py | 4 +- becca/model_numba.py | 6 ++- becca/ziptie.py | 43 +++++++++------- 6 files changed, 114 insertions(+), 91 deletions(-) diff --git a/becca/brain.py b/becca/brain.py index cca4e1a..b505cb0 100755 --- a/becca/brain.py +++ b/becca/brain.py @@ -74,7 +74,7 @@ def __init__(self, world, config=None): "name": None, "reporting_interval": 1e3, "restore": True, - "visualize_interval": 1e3, + "visualize_interval": 1e4, } if config is None: config = {} @@ -192,7 +192,6 @@ def __init__(self, world, config=None): self.featurizer = Featurizer( debug=self.debug, n_inputs=self.n_features, - threshold=1e3, ) # The model builds sequences of features and goals and reward # for making predictions about its world. @@ -273,18 +272,21 @@ def sense_act_learn(self, sensors, reward): feature_activities = self.featurizer.featurize( np.concatenate((self.postprocessor.consolidated_commands, input_activities))) + (model_feature_activities, conditional_predictions, conditional_rewards, - conditional_curiosities) = self.model.step( - feature_activities, reward) + conditional_curiosities + ) = self.model.step(feature_activities, reward) + feature_goals, i_goal = self.actor.choose( feature_activities=model_feature_activities, conditional_predictions=conditional_predictions, conditional_rewards=conditional_rewards, conditional_curiosities=conditional_curiosities, ) - feature_pool_goals = self.model.update_goals(feature_goals, i_goal) + feature_pool_goals = self.model.update_goals( + feature_goals, i_goal) debug_local = False if debug_local: @@ -299,7 +301,7 @@ def sense_act_learn(self, sensors, reward): # Isolate the actions from the rest of the goals. self.actions = (self.postprocessor.convert_to_actions( - input_goals[:self.n_commands])) + input_goals[:self.n_commands])) # Update the inputs in a pair of top-down/bottom-up passes. # Top-down @@ -391,8 +393,8 @@ def backup(self): print('Pickling error: {0} encountered while saving brain data'. format(perr)) except Exception as err: - print('Unknown error: {0} encountered while saving brain data'. - format(err)) + print('Unknown error: {0} encountered while saving brain data' + .format(err)) else: success = True return success diff --git a/becca/featurizer.py b/becca/featurizer.py index 698ef3f..e988e27 100755 --- a/becca/featurizer.py +++ b/becca/featurizer.py @@ -14,7 +14,6 @@ def __init__( self, debug=False, n_inputs=None, - threshold=None, ): """ Configure the featurizer. @@ -25,8 +24,6 @@ def __init__( n_inputs : int The number of inputs (cables) that each Ziptie will be equipped to handle. - threshold : float - See Ziptie.nucleation_threshold """ self.debug = debug @@ -66,16 +63,19 @@ def __init__( # features, which sparsity helps keep Becca fast. self.ziptie = Ziptie( n_cables=self.n_inputs, - threshold=threshold, debug=self.debug) # n_features: int # The total number of features that have been colllected so far. # This includes the cable candidate pools from each ziptie. - self.n_features = [0, 0] + self.n_features_by_level = [0, 0] # mapping: 2D array of ints - # The transformation from + # The transformation from candidates (List or arrays of values) + # to the feature pool. + # If there is a one at [row_i, col_j] then + # candidate row_i maps to feature index col_j . + # feature_pool = np.matmul(candidates, self.mapping) self.mapping = np.zeros((0, 0), dtype=np.int) def featurize(self, new_candidates): @@ -168,7 +168,7 @@ def update_inputs(self): resets = [] for i_level, level_resets in enumerate(all_resets): - i_start = np.sum(np.array(self.n_features[:i_level])) + i_start = np.sum(np.array(self.n_features_by_level[:i_level])) for i_reset in level_resets: resets.append(np.where( self.mapping[i_start + i_reset, :])[0]) @@ -188,10 +188,9 @@ def map_from_feature_pool(self, feature_values): ------- candidate_values: list of array of floats """ - # feature_pool_values = np.matmul(feature_values, self.mapping.T) candidate_values = [] i_last = 0 - for n_feat in self.n_features: + for n_feat in self.n_features_by_level: candidate_values.append( feature_values[i_last: i_last + n_feat]) i_last += n_feat @@ -210,51 +209,62 @@ def map_to_feature_pool(self, candidate_values): ------- feature_values: array of floats """ - # Check whether the number of candidates has expanded at any level - # and adapt. - n_candidates_by_level = [values.size for values in candidate_values] - total_n_candidates = np.sum(np.array(n_candidates_by_level)) - total_n_features = np.sum(np.array(self.n_features)) - if (total_n_features < total_n_candidates): - # Update_needed - delta = [] - for i_level, candidate_pool in enumerate(candidate_values): - delta.append(candidate_pool.size - self.n_features[i_level]) - # Create a larger map - new_mapping = np.zeros( - (total_n_candidates, total_n_candidates), dtype=np.int) - new_mapping[:total_n_features, :total_n_features] = self.mapping - new_mapping[total_n_features:, total_n_features:] = ( - np.eye(total_n_candidates - total_n_features)) - - # Shift new rows upward to sit with their own levels. - last_row = 0 - for i_level in range(len(candidate_values) - 1): - last_row += self.n_features[i_level] - start = total_n_features - stop = start + delta[i_level] - if delta[i_level] > 0: - move_rows = new_mapping[start:stop, :] - new_mapping[ - last_row + delta[i_level]: - last_row + delta[i_level] - + self.n_features[i_level + 1], : - ] = new_mapping[ - last_row: - last_row - + self.n_features[i_level + 1], : - ] - new_mapping[ - last_row: last_row + delta[i_level], :] = move_rows - self.n_features[i_level] += delta[i_level] - start += delta[i_level] - last_row += delta[i_level] - self.n_features[-1] += delta[-1] - self.mapping = new_mapping - + self.grow_map(candidate_values) all_candidate_values = [] for level_candidate_values in candidate_values: all_candidate_values += list(level_candidate_values) feature_values = np.matmul( np.array(all_candidate_values), self.mapping) return feature_values + + def grow_map(self, candidate_values): + """ + Check whether we need to add more candidates to the feature pool. + + New candidates will come in appended to the end of their + respective input pools. However, feature indices need to + stay consistent throughout the life of each feature. + these new candidates need to be given indices at the + end of the currently used set of feature pool indices. + + Parameters + ---------- + candidate_values: list of arrays of floats + """ + # Check whether the number of candidates has expanded + # at any level and adapt. + n_candidates_by_level = [ + values.size for values in candidate_values] + total_n_candidates = np.sum(np.array(n_candidates_by_level)) + total_n_features = np.sum(np.array(self.n_features_by_level)) + + if (total_n_features < total_n_candidates): + # Create a larger map + new_mapping = [] + i_last_old = 0 # Track last candidate handled. + j_last_new = total_n_features # Track last feature assigned. + + for i_level in range(len(self.n_features_by_level)): + n_cand = n_candidates_by_level[i_level] + n_feat = self.n_features_by_level[i_level] + delta = n_cand - n_feat + + level_old_map = np.zeros((n_feat, total_n_candidates)) + level_old_map[:, :total_n_features] = self.mapping[ + i_last_old:i_last_old + n_feat, :] + new_mapping.append(level_old_map) + + if delta > 0: + level_new_map = np.zeros((delta, total_n_candidates)) + level_new_map[ + :, + j_last_new: j_last_new + delta + ] = np.eye(delta) + new_mapping.append(level_new_map) + + j_last_new += delta + self.n_features_by_level[i_level] += delta + + i_last_old += n_feat + self.mapping = np.concatenate(new_mapping) + return diff --git a/becca/input_filter.py b/becca/input_filter.py index c2bd196..4993dc9 100644 --- a/becca/input_filter.py +++ b/becca/input_filter.py @@ -45,8 +45,8 @@ def __init__(self, debug=False, n_inputs=None, name='filter'): # as the number of candidates. # A 1 in position i, j indicates that candidate i maps to # input j. - self.mapping = np.zeros((self.n_inputs * 2, self.n_inputs), - dtype=np.int) + self.mapping = np.zeros( + (self.n_inputs * 2, self.n_inputs), dtype=np.int) # Position in this array shows which candidate is being assigned. # The value at that position shows the input index it is @@ -99,8 +99,8 @@ def update_activities(self, candidate_activities): self.n_candidates = self.candidate_activities.size capacity = self.mapping.shape[0] if self.n_candidates >= capacity: - new_mapping = np.zeros((self.n_candidates * 2, self.n_inputs), - dtype=np.int) + new_mapping = np.zeros( + (self.n_candidates * 2, self.n_inputs), dtype=np.int) new_mapping[:capacity, :] = self.mapping self.mapping = new_mapping @@ -130,8 +130,8 @@ def update_activities(self, candidate_activities): self.mapping[:self.n_candidates, :], axis=1) == 0)[0] self.i_in_use = np.where(self.mapping)[0] self.bench_pressure[self.i_benched] += ( - self.candidate_activities[self.i_benched] / - (tools.epsilon + self.candidate_activities[self.i_benched] / ( + tools.epsilon + self.cumulative_activities[self.i_benched] * self.pressure_time)) @@ -212,7 +212,9 @@ def update_inputs(self, upstream_resets=None): resets = [] candidate_score = ( - self.candidate_fitness + self.bench_pressure[self.n_candidates]) + self.candidate_fitness + + self.bench_pressure[self.n_candidates] + ) # Find lowest scoring candidates in use. i_lowest_scoring_in_use = np.argsort( candidate_score[self.i_in_use])[::-1] @@ -225,8 +227,10 @@ def update_inputs(self, upstream_resets=None): # n_inputs_unassigned = self.n_inputs - n_inputs_used n_inputs_unassigned = self.n_inputs - self.i_in_use.size i_fill = 0 - while(n_inputs_unassigned > 0 and - i_highest_scoring_benched.size > i_fill): + while( + n_inputs_unassigned > 0 + and i_highest_scoring_benched.size > i_fill + ): i_in = self.i_benched[i_highest_scoring_benched[i_fill]] self.mapping[i_in, self.n_inputs - n_inputs_unassigned] = 1 # self.inverse_mapping[self.n_inputs - n_inputs_unassigned] = i_in diff --git a/becca/model.py b/becca/model.py index c237aa3..192baa4 100755 --- a/becca/model.py +++ b/becca/model.py @@ -136,7 +136,7 @@ def __init__( # curiosity_update_rate : float # One of the factors that determines he rate at which # a prefix increases its curiosity. - self.curiosity_update_rate = 3e-3 + self.curiosity_update_rate = 1e-2 def step(self, candidate_activities, reward): """ @@ -223,7 +223,6 @@ def update_activities(self, candidate_activities): feature_activities: array of floats previous_feature_activities: array of floats """ - # TODO: incorporate _update_activities() into this feature_activities = self.filter.update_activities( candidate_activities) @@ -249,7 +248,6 @@ def calculate_fitness(self): The fitness of each of the feature candidate inputs to the model. """ - nb.update_fitness( self.feature_fitness, self.prefix_occurrences, diff --git a/becca/model_numba.py b/becca/model_numba.py index 6cdc468..20c8f3d 100755 --- a/becca/model_numba.py +++ b/becca/model_numba.py @@ -31,8 +31,10 @@ def update_prefixes( for i_goal in range(n_goals): prefix_activities[i_feature, i_goal] *= 1 - prefix_decay_rate - new_prefix_activity = (previous_feature_activities[i_feature] * - goal_activities[i_goal]) + new_prefix_activity = ( + previous_feature_activities[i_feature] * + goal_activities[i_goal] + ) prefix_activities[i_feature, i_goal] += new_prefix_activity prefix_activities[i_feature, i_goal] = min( prefix_activities[i_feature, i_goal], 1) diff --git a/becca/ziptie.py b/becca/ziptie.py index 0a5c924..5a86194 100644 --- a/becca/ziptie.py +++ b/becca/ziptie.py @@ -31,7 +31,7 @@ def __init__( debug=False, n_cables=16, name=None, - threshold=1e4, + threshold=1e3, ): """ Initialize the ziptie, pre-allocating data structures. @@ -82,26 +82,27 @@ def __init__( # mapping: 2D array of ints # The mapping between cables and bundles. # If element i,j is 1, then cable i is a member of bundle j - self.mapping = np.zeros((self.n_cables, self.n_cables), dtype=np.int) + self.mapping = np.zeros( + (self.n_cables, self.n_cables), dtype=np.int) # agglomeration_energy: 2D array of floats # The accumulated agglomeration energy for each # bundle-cable pair. Bundles are represented in rows, # cables are in columns. - self.agglomeration_energy = np.zeros((self.n_cables, - self.n_cables)) + self.agglomeration_energy = np.zeros( + (self.n_cables, self.n_cables)) # agglomeration_mask: 2D array of floats # A binary array indicating which cable-bundle # pairs are allowed to accumulate # energy and which are not. Some combinations are # disallowed because they result in redundant bundles. - self.agglomeration_mask = np.ones((self.n_cables, - self.n_cables)) + self.agglomeration_mask = np.ones( + (self.n_cables, self.n_cables)) # nucleation_energy: 2D array of floats # The accumualted nucleation energy associated # with each cable-cable pair. - self.nucleation_energy = np.zeros((self.n_cables, - self.n_cables)) + self.nucleation_energy = np.zeros( + (self.n_cables, self.n_cables)) # nucleation_mask: 2D array of floats # A binary array indicating which cable-cable # pairs are allowed to accumulate @@ -142,10 +143,13 @@ def create_new_bundles(self): If the right conditions have been reached, create a new bundle. """ # Incrementally accumulate nucleation energy. - nb.nucleation_energy_gather(self.cable_activities, - self.nucleation_energy, - self.nucleation_mask) - max_energy, i_cable_a, i_cable_b = nb.max_2d(self.nucleation_energy) + nb.nucleation_energy_gather( + self.cable_activities, + self.nucleation_energy, + self.nucleation_mask, + ) + max_energy, i_cable_a, i_cable_b = nb.max_2d( + self.nucleation_energy) # Add a new bundle if appropriate if max_energy > self.nucleation_threshold: @@ -200,12 +204,15 @@ def grow_bundles(self): Update an estimate of co-activity between all cables. """ # Incrementally accumulate agglomeration energy. - nb.agglomeration_energy_gather(self.bundle_activities, - self.cable_activities, - self.n_bundles, - self.agglomeration_energy, - self.agglomeration_mask) - max_energy, i_bundle, i_cable = nb.max_2d(self.agglomeration_energy) + nb.agglomeration_energy_gather( + self.bundle_activities, + self.cable_activities, + self.n_bundles, + self.agglomeration_energy, + self.agglomeration_mask, + ) + max_energy, i_bundle, i_cable = nb.max_2d( + self.agglomeration_energy) # Add a new bundle if appropriate if max_energy > self.agglomeration_threshold: