diff --git a/psyneulink/core/components/component.py b/psyneulink/core/components/component.py index 5c2f4175016..69f1bc96e69 100644 --- a/psyneulink/core/components/component.py +++ b/psyneulink/core/components/component.py @@ -1243,9 +1243,6 @@ def __init__(self, # - assign function's output to self.defaults.value (based on call of self.execute) self._instantiate_function(function=function, function_params=function_params, context=context) - # FIX TIME 3/18/21 - if '(RESULT) to (OUTPUT_CIM_TransferMechanism-1_RESULT)' in self.name: - assert True self._instantiate_value(context=context) # INSTANTIATE ATTRIBUTES AFTER FUNCTION diff --git a/psyneulink/core/components/functions/nonstateful/learningfunctions.py b/psyneulink/core/components/functions/nonstateful/learningfunctions.py index 7a4586cd2f7..c42e089856e 100644 --- a/psyneulink/core/components/functions/nonstateful/learningfunctions.py +++ b/psyneulink/core/components/functions/nonstateful/learningfunctions.py @@ -181,7 +181,9 @@ class EMStorage(LearningFunction): EMStorage( \ default_variable=None, \ axis=0, \ + storage_location=None \ storage_prob=1.0, \ + decay_rate=0.0, \ params=None, \ name=None, \ prefs=None) @@ -207,10 +209,19 @@ class EMStorage(LearningFunction): axis : int : default 0 specifies the axis of `memory_matrix ` to which `entry ` is assigned. + storage_location : int : default None + specifies the location (row or col determined by `axis `) of `memory_matrix + ` at which the new entry is stored (replacing the existing one); + if None, the weeakest entry (one with the lowest norm) along `axis ` of + `memory_matrix ` is used. + storage_prob : float : default default_learning_rate specifies the probability with which `entry ` is assigned to `memory_matrix `. + decay_rate : float : default 0.0 + specifies the rate at which pre-existing entries in `memory_matrix ` are decayed. + params : Dict[param keyword: param value] : default None a `parameter dictionary ` that specifies the parameters for the function. Values specified for parameters in the dictionary override any assigned to those @@ -240,10 +251,17 @@ class EMStorage(LearningFunction): axis : int determines axis of `memory_matrix ` to which `entry ` is assigned. + storage_location : int + specifies the location (row or col determined by `axis `) of `memory_matrix + ` at which the new entry is stored. + storage_prob : float determines the probability with which `entry ` is stored in `memory_matrix `. + decay_rate : float + determines the rate at which pre-existing entries in `memory_matrix ` are decayed. + random_state : numpy.RandomState private pseudorandom number generator @@ -275,6 +293,12 @@ class Parameters(LearningFunction.Parameters): :type: int :read only: True + decay_rate + see `decay_rate ` + + :default value: 0.0 + :type: float + entry see `entry ` @@ -295,6 +319,12 @@ class Parameters(LearningFunction.Parameters): :default value: None :type: ``numpy.random.RandomState`` + storage_location + see `storage_location ` + + :default value: None + :type: int + storage_prob see `storage_prob ` @@ -306,12 +336,14 @@ class Parameters(LearningFunction.Parameters): read_only=True, pnl_internal=True, constructor_argument='default_variable') + entry = Parameter([0], read_only=True) + memory_matrix = Parameter([[0],[0]], read_only=True) axis = Parameter(0, read_only=True, structural=True) + storage_location = Parameter(None, read_only=True) + storage_prob = Parameter(1.0, modulable=True) + decay_rate = Parameter(0.0, modulable=True) random_state = Parameter(None, loggable=False, getter=_random_state_getter, dependencies='seed') seed = Parameter(DEFAULT_SEED, modulable=True, fallback_default=True, setter=_seed_setter) - storage_prob = Parameter(1.0, modulable=True) - entry = Parameter([0], read_only=True) - memory_matrix = Parameter([[0],[0]], read_only=True) default_learning_rate = 1.0 @@ -326,7 +358,9 @@ def _validate_storage_prob(self, storage_prob): def __init__(self, default_variable=None, axis=0, + storage_location=None, storage_prob=1.0, + decay_rate=0.0, seed=None, params=None, owner=None, @@ -335,7 +369,9 @@ def __init__(self, super().__init__( default_variable=default_variable, axis=axis, + storage_location=storage_location, storage_prob=storage_prob, + decay_rate=decay_rate, seed=seed, params=params, owner=owner, @@ -401,16 +437,22 @@ def _function(self, entry = variable axis = self.parameters.axis._get(context) + storage_location = self.parameters.storage_location._get(context) storage_prob = self.parameters.storage_prob._get(context) + decay_rate = self.parameters.decay_rate._get(context) random_state = self.parameters.random_state._get(context) + # FIX: IMPLEMENT decay_rate CALCUALTION + # IMPLEMENTATION NOTE: if memory_matrix is an arg, it must in params (put there by Component.function() # Manage memory_matrix param memory_matrix = None if params: memory_matrix = params.pop(MEMORY_MATRIX, None) axis = params.pop('axis', axis) + storage_location = params.pop('storage_location', storage_location) storage_prob = params.pop('storage_prob', storage_prob) + decay_rate = params.pop('decay_rate', decay_rate) # During init, function is called directly from Component (i.e., not from LearningMechanism execute() method), # so need "placemarker" error_matrix for validation if memory_matrix is None: @@ -430,8 +472,13 @@ def _function(self, # Don't store entry during initialization to avoid contaminating memory_matrix pass elif random_state.uniform(0, 1) < storage_prob: - # Store entry in slot with weakest memory (one with lowest norm) along specified axis - idx_of_min = np.argmin(np.linalg.norm(memory_matrix, axis=axis)) + if decay_rate: + memory_matrix *= decay_rate + if storage_location is not None: + idx_of_min = storage_location + else: + # Find weakest entry (i.e., with lowest norm) along specified axis of matrix + idx_of_min = np.argmin(np.linalg.norm(memory_matrix, axis=axis)) if axis == 0: memory_matrix[:,idx_of_min] = np.array(entry) elif axis == 1: diff --git a/psyneulink/core/components/mechanisms/mechanism.py b/psyneulink/core/components/mechanisms/mechanism.py index 267c4043917..27300c81407 100644 --- a/psyneulink/core/components/mechanisms/mechanism.py +++ b/psyneulink/core/components/mechanisms/mechanism.py @@ -1865,9 +1865,9 @@ def _handle_arg_input_ports(self, input_ports): try: parsed_input_port_spec = _parse_port_spec(owner=self, - port_type=InputPort, - port_spec=s, - ) + port_type=InputPort, + port_spec=s, + context=Context(string='handle_arg_input_ports')) except AttributeError as e: if DEFER_VARIABLE_SPEC_TO_MECH_MSG in e.args[0]: default_variable_from_input_ports.append(InputPort.defaults.variable) @@ -1980,9 +1980,11 @@ def _validate_params(self, request_set, target_set=None, context=None): try: try: for port_spec in params[INPUT_PORTS]: - _parse_port_spec(owner=self, port_type=InputPort, port_spec=port_spec) + _parse_port_spec(owner=self, port_type=InputPort, port_spec=port_spec, + context=Context(string='mechanism.validate_params')) except TypeError: - _parse_port_spec(owner=self, port_type=InputPort, port_spec=params[INPUT_PORTS]) + _parse_port_spec(owner=self, port_type=InputPort, port_spec=params[INPUT_PORTS], + context=Context(string='mechanism.validate_params')) except AttributeError as e: if DEFER_VARIABLE_SPEC_TO_MECH_MSG in e.args[0]: pass diff --git a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py index 086a1a41e5a..e3c4c98ec79 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py @@ -632,6 +632,7 @@ OBJECTIVE_MECHANISM, OUTCOME, OWNER_VALUE, PARAMS, PORT_TYPE, PRODUCT, PROJECTION_TYPE, PROJECTIONS, \ SEPARATE, SIZE from psyneulink.core.globals.parameters import Parameter, check_user_specified +from psyneulink.core.globals.context import Context from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel from psyneulink.core.globals.utilities import ContentAddressableList, convert_all_elements_to_np_array, convert_to_list, convert_to_np_array @@ -672,24 +673,17 @@ def __init__(self, message, data=None): def validate_monitored_port_spec(owner, spec_list): + context = Context(string='ControlMechanism.validate_monitored_port_spec') for spec in spec_list: if isinstance(spec, MonitoredOutputPortTuple): spec = spec.output_port elif isinstance(spec, tuple): - spec = _parse_port_spec( - owner=owner, - port_type=InputPort, - port_spec=spec, - ) + spec = _parse_port_spec(owner=owner, port_type=InputPort, port_spec=spec, context=context) spec = spec['params'][PROJECTIONS][0][0] elif isinstance(spec, dict): # If it is a dict, parse to validate that it is an InputPort specification dict # (for InputPort of ObjectiveMechanism to be assigned to the monitored_output_port) - spec = _parse_port_spec( - owner=owner, - port_type=InputPort, - port_spec=spec, - ) + spec = _parse_port_spec(owner=owner, port_type=InputPort, port_spec=spec, context=context) # Get the OutputPort, to validate that it is in the ControlMechanism's Composition (below); # presumes that the monitored_output_port is the first in the list of projection_specs # in the InputPort port specification dictionary returned from the parse, @@ -1263,15 +1257,10 @@ def _validate_output_ports(self, control): port_types = self._owner.outputPortTypes for ctl_spec in control: - ctl_spec = _parse_port_spec( - port_type=port_types, owner=self._owner, port_spec=ctl_spec - ) - if not ( - isinstance(ctl_spec, port_types) - or ( - isinstance(ctl_spec, dict) and ctl_spec[PORT_TYPE] == port_types - ) - ): + ctl_spec = _parse_port_spec(port_type=port_types, owner=self._owner, port_spec=ctl_spec, + context=Context(string='ControlMechanism._validate_input_ports')) + if not (isinstance(ctl_spec, port_types) + or (isinstance(ctl_spec, dict) and ctl_spec[PORT_TYPE] == port_types)): return 'invalid port specification' # FIX 5/28/20: diff --git a/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py index 1a2095fbc63..ef50bd9f8e9 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py @@ -2581,7 +2581,8 @@ def _validate_entries(spec=None, source=None): self.state_feature_specs[i] = spec # Get InputPort specification dictionary for state_input_port and update its entries - parsed_spec = _parse_port_spec(owner=self, port_type=InputPort, port_spec=spec) + parsed_spec = _parse_port_spec(owner=self, port_type=InputPort, port_spec=spec, + context=Context(string='OptimizationControlMechanism._parse_specs')) parsed_spec[NAME] = state_input_port_names[i] if parsed_spec[PARAMS] and SHADOW_INPUTS in parsed_spec[PARAMS]: # Composition._update_shadow_projections will take care of PROJECTIONS specification diff --git a/psyneulink/core/components/mechanisms/modulatory/learning/learningmechanism.py b/psyneulink/core/components/mechanisms/modulatory/learning/learningmechanism.py index ce7e6f93d91..3dd64d9112f 100644 --- a/psyneulink/core/components/mechanisms/modulatory/learning/learningmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/learning/learningmechanism.py @@ -544,7 +544,7 @@ from psyneulink.core.components.ports.modulatorysignals.learningsignal import LearningSignal from psyneulink.core.components.ports.parameterport import ParameterPort from psyneulink.core.components.shellclasses import Mechanism -from psyneulink.core.globals.context import ContextFlags, handle_external_context +from psyneulink.core.globals.context import Context, ContextFlags, handle_external_context from psyneulink.core.globals.keywords import \ ADDITIVE, ASSERT, ENABLED, INPUT_PORTS, \ LEARNING, LEARNING_MECHANISM, LEARNING_PROJECTION, LEARNING_SIGNAL, LEARNING_SIGNALS, MATRIX, \ @@ -1161,7 +1161,8 @@ def _validate_params(self, request_set, target_set=None, context=None): format(LEARNING_SIGNAL, self.name)) for spec in target_set[LEARNING_SIGNALS]: - learning_signal = _parse_port_spec(port_type=LearningSignal, owner=self, port_spec=spec) + learning_signal = _parse_port_spec(port_type=LearningSignal, owner=self, port_spec=spec, + context=Context(string='LearningMechanism.validate_params')) # Validate that the receiver of the LearningProjection (if specified) # is a MappingProjection and in the same Composition as self (if specified) diff --git a/psyneulink/core/components/mechanisms/processing/objectivemechanism.py b/psyneulink/core/components/mechanisms/processing/objectivemechanism.py index 3c15dc7b12e..7ee1c7e1f3a 100644 --- a/psyneulink/core/components/mechanisms/processing/objectivemechanism.py +++ b/psyneulink/core/components/mechanisms/processing/objectivemechanism.py @@ -377,7 +377,7 @@ from psyneulink.core.components.ports.inputport import InputPort, INPUT_PORT from psyneulink.core.components.ports.outputport import OutputPort from psyneulink.core.components.ports.port import _parse_port_spec -from psyneulink.core.globals.context import ContextFlags, handle_external_context +from psyneulink.core.globals.context import Context, ContextFlags, handle_external_context from psyneulink.core.globals.keywords import \ CONTROL, EXPONENT, EXPONENTS, LEARNING, MATRIX, NAME, OBJECTIVE_MECHANISM, OUTCOME, OWNER_VALUE, \ PARAMS, PREFERENCE_SET_NAME, PROJECTION, PROJECTIONS, PORT_TYPE, VARIABLE, WEIGHT, WEIGHTS @@ -714,7 +714,8 @@ def add_to_monitor(self, monitor_specs, context=None): monitor_specs[i] = spec # Parse spec to get value of OutputPort and (possibly) the Projection from it - input_port = _parse_port_spec(owner=self, port_type = InputPort, port_spec=spec) + input_port = _parse_port_spec(owner=self, port_type = InputPort, port_spec=spec, + context=Context(string='objective_mechanism.add_to_monitor')) # There should be only one ProjectionTuple specified, # that designates the OutputPort and (possibly) a Projection from it diff --git a/psyneulink/core/components/ports/inputport.py b/psyneulink/core/components/ports/inputport.py index 2d4eaaa8a93..c4064233415 100644 --- a/psyneulink/core/components/ports/inputport.py +++ b/psyneulink/core/components/ports/inputport.py @@ -519,14 +519,14 @@ .. _InputPort_Function: -* `function ` -- combines the `value ` of all of the - `Projections ` received by the InputPort, and assigns the result to the InputPort's `value - ` attribute. The default function is `LinearCombination` that performs an elementwise (Hadamard) - sums the values. However, the parameters of the `function ` -- and thus the `value - ` of the InputPort -- can be modified by any `GatingProjections ` received by - the InputPort (listed in its `mod_afferents ` attribute. A custom function can also be - specified, so long as it generates a result that is compatible with the item of the Mechanism's `variable - ` to which the `InputPort is assigned `. +* `function ` -- combines the `value ` of all of the `path_afferent + ` `Projections ` received by the InputPort, and assigns the result to the + InputPort's `value ` attribute. The default function is `LinearCombination` that performs an + elementwise (Hadamard) sum of the afferent values. However, the parameters of the `function ` + -- and thus the `value ` of the InputPort -- can be modified by any `GatingProjections + ` received by the InputPort (listed in its `mod_afferents ` attribute. + A custom function can also be specified, so long as it generates a result that is compatible with the item of the + Mechanism's `variable ` to which the `InputPort is assigned `. .. _InputPort_Value: @@ -551,7 +551,7 @@ An InputPort cannot be executed directly. It is executed when the Mechanism to which it belongs is executed. When this occurs, the InputPort executes any `Projections ` it receives, calls its `function -` to combines the values received from any `MappingProjections ` it receives +` to combine the values received from any `MappingProjections ` it receives (listed in its its `path_afferents ` attribute) and modulate them in response to any `GatingProjections ` (listed in its `mod_afferents ` attribute), and then assigns the result to the InputPort's `value ` attribute. This, in turn, is assigned to @@ -739,7 +739,7 @@ class InputPort(Port_Base): applied and it will generate a value that is the same length as the Projection's `value `. However, if the InputPort receives more than one Projection and uses a function other than a CombinationFunction, a warning is generated and only the `value - ` of the first Projection list in `path_afferents ` + ` of the first Projection listed in `path_afferents ` is used by the function, which may generate unexpected results when executing the Mechanism or Composition to which it belongs. @@ -1113,18 +1113,24 @@ def _get_all_projections(self): return self._get_all_afferents() @beartype - def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): - """Get weights, exponents and/or any connections specified in an InputPort specification tuple - + def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec, context=None): + """Parse any InputPort-specific specifications, including SIZE, COMBINE, WEIGHTS and EXPONENTS + Get SIZE and/or COMBINE specification in if port_specific_spec is a dict + Get weights, exponents and/or any connections specified if port_specific_spec is a tuple Tuple specification can be: (port_spec, connections) (port_spec, weights, exponents, connections) - See Port._parse_port_specific_spec for additional info. + See Port._parse_port_specific_specs for additional info. Returns: - - port_spec: 1st item of tuple if it is a numeric value; otherwise None - - params dict with WEIGHT, EXPONENT and/or PROJECTIONS entries if any of these was specified. + - port_spec: + - updated with SIZE and/or COMBINE specifications for dict; + - 1st item for tuple if it is a numeric value; + - otherwise None + - params dict: + - with WEIGHT, EXPONENT and/or PROJECTIONS entries if any of these was specified. + - purged of SIZE and/or COMBINE entries if they were specified in port_specific_spec """ # FIX: ADD FACILITY TO SPECIFY WEIGHTS AND/OR EXPONENTS FOR INDIVIDUAL OutputPort SPECS @@ -1145,16 +1151,49 @@ def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): # FIX: USE ObjectiveMechanism EXAMPLES # if MECHANISM in port_specific_spec: # if OUTPUT_PORTS in port_specific_spec - if SIZE in port_specific_spec: - if (VARIABLE in port_specific_spec or - any(key in port_dict and port_dict[key] is not None for key in {VARIABLE, SIZE})): - raise InputPortError(f"PROGRAM ERROR: SIZE specification found in port_specific_spec dict " - f"for {self.__name__} specification of {owner.name} when SIZE or VARIABLE " - f"is already present in its port_specific_spec dict or port_dict.") - port_dict.update({VARIABLE:np.zeros(port_specific_spec[SIZE])}) - del port_specific_spec[SIZE] + + if any(spec in port_specific_spec for spec in {SIZE, COMBINE}): + + if SIZE in port_specific_spec: + if (VARIABLE in port_specific_spec or + any(key in port_dict and port_dict[key] is not None for key in {VARIABLE, SIZE})): + raise InputPortError(f"PROGRAM ERROR: SIZE specification found in port_specific_spec dict " + f"for {self.__name__} specification of {owner.name} when SIZE or VARIABLE " + f"is already present in its port_specific_spec dict or port_dict.") + port_dict.update({VARIABLE:np.zeros(port_specific_spec[SIZE])}) + del port_specific_spec[SIZE] + + if COMBINE in port_specific_spec: + fct_err = None + if (FUNCTION in port_specific_spec and port_specific_spec[FUNCTION] is not None): + fct_str = port_specific_spec[FUNCTION].componentName + fct_err = port_specific_spec[FUNCTION].operation != port_specific_spec[COMBINE] + del port_specific_spec[FUNCTION] + elif (FUNCTION in port_dict and port_dict[FUNCTION] is not None): + fct_str = port_dict[FUNCTION].componentName + fct_err = port_dict[FUNCTION].operation != port_specific_spec[COMBINE] + del port_dict[FUNCTION] + if fct_err is True: + raise InputPortError(f"COMBINE entry (='{port_specific_spec[COMBINE]}') of InputPort " + f"specification dictionary for '{self.__name__}' of '{owner.name}' " + f"conflicts with FUNCTION entry ({fct_str}); remove one or the other.") + if fct_err is False and any(source in context.string + for source in {'validate_params', + '_instantiate_input_ports', + '_instantiate_output_ports'}): # Suppress warning in earlier calls + warnings.warn(f"Both COMBINE ('{port_specific_spec[COMBINE]}') and FUNCTION ({fct_str}) " + f"specifications found in InputPort specification dictionary for '{self.__name__}' " + f"of '{owner.name}'; no need to specify both.") + # FIX: THE NEXT LINE, WHICH SHOULD JUST PASS THE COMBINE SPECIFICATION ON TO THE CONSTRUCTOR + # (AND HANDLE FUNCTION ASSIGNMENT THERE) CAUSES A CRASH (APPEARS TO BE A RECURSION ERROR); + # THEREFORE, NEED TO SET FUNCTION HERE + # port_dict.update({COMBINE: port_specific_spec[COMBINE]}) + port_specific_spec[FUNCTION] = LinearCombination(operation=port_specific_spec[COMBINE]) + del port_specific_spec[COMBINE] + return port_dict, port_specific_spec - return None, port_specific_spec + else: + return None, port_specific_spec elif isinstance(port_specific_spec, tuple): @@ -1520,6 +1559,7 @@ def _instantiate_input_ports(owner, input_ports=None, reference_value=None, cont if input_ports is not None: input_ports = _parse_shadow_inputs(owner, input_ports) + context.string = context.string or '_instantiate_input_ports' port_list = _instantiate_port_list(owner=owner, port_list=input_ports, port_types=InputPort, diff --git a/psyneulink/core/components/ports/modulatorysignals/controlsignal.py b/psyneulink/core/components/ports/modulatorysignals/controlsignal.py index 95fcf458c5a..fe8f069bae6 100644 --- a/psyneulink/core/components/ports/modulatorysignals/controlsignal.py +++ b/psyneulink/core/components/ports/modulatorysignals/controlsignal.py @@ -1025,7 +1025,7 @@ def _instantiate_cost_attributes(self, context=None): self.duration_cost = 0 self.cost = self.defaults.cost = self.intensity_cost - def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): + def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec, context=None): """Get ControlSignal specified for a parameter or in a 'control_signals' argument Tuple specification can be: diff --git a/psyneulink/core/components/ports/outputport.py b/psyneulink/core/components/ports/outputport.py index 8c22809f9df..7c60b5bffa5 100644 --- a/psyneulink/core/components/ports/outputport.py +++ b/psyneulink/core/components/ports/outputport.py @@ -634,6 +634,7 @@ VALUE, VARIABLE, \ output_port_spec_to_parameter_name, INPUT_PORT_VARIABLES from psyneulink.core.globals.parameters import Parameter, check_user_specified +from psyneulink.core.globals.context import Context from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel from psyneulink.core.globals.utilities import \ @@ -1063,14 +1064,14 @@ def _parse_function_variable(self, variable, context=None): return _parse_output_port_variable(variable, self.owner) @beartype - def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): + def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec, context=None): """Get variable spec and/or connections specified in an OutputPort specification tuple Tuple specification can be: (port_spec, connections) (port_spec, variable spec, connections) - See Port._parse_port_specific_spec for additional info. + See Port._parse_port_specific_specs for additional info. Returns: - port_spec: 1st item of tuple @@ -1404,7 +1405,8 @@ def _instantiate_output_ports(owner, output_ports=None, context=None): else: # parse output_port from psyneulink.core.components.ports.port import _parse_port_spec - output_port = _parse_port_spec(port_type=OutputPort, owner=owner, port_spec=output_port) + output_port = _parse_port_spec(port_type=OutputPort, owner=owner, port_spec=output_port, + context=Context(string='OutputPort._instantiate_output_ports')) _maintain_backward_compatibility(output_port, output_port[NAME], owner) @@ -1456,6 +1458,7 @@ def _instantiate_output_ports(owner, output_ports=None, context=None): # Use OutputPort as default port_types = OutputPort + context.string = context.string or '_instantiate_output_ports' port_list = _instantiate_port_list(owner=owner, port_list=output_ports, port_types=port_types, diff --git a/psyneulink/core/components/ports/parameterport.py b/psyneulink/core/components/ports/parameterport.py index 0eac8e38716..3b7d0010cc9 100644 --- a/psyneulink/core/components/ports/parameterport.py +++ b/psyneulink/core/components/ports/parameterport.py @@ -802,7 +802,7 @@ def _get_all_projections(self): return self.mod_afferents @beartype - def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): + def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec, context=None): """Get connections specified in a ParameterPort specification tuple Tuple specification can be: diff --git a/psyneulink/core/components/ports/port.py b/psyneulink/core/components/ports/port.py index 14be8e546be..8c1ce7d97b9 100644 --- a/psyneulink/core/components/ports/port.py +++ b/psyneulink/core/components/ports/port.py @@ -1842,7 +1842,7 @@ def _get_all_projections(self): def _get_all_afferents(self): assert False, f"Subclass of Port ({self.__class__.__name__}) must implement '_get_all_afferents()' method." - def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): + def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec, context=None): """Parse parameters in Port specification tuple specific to each subclass Called by _parse_port_spec() @@ -2026,7 +2026,9 @@ def set_projection_value(projection, value, context): # Handle LearningProjection # - update LearningSignals only if context == LEARNING; otherwise, assign zero for projection_value # IMPLEMENTATION NOTE: done here rather than in its own method in order to exploit parsing of params above - elif (isinstance(projection, LearningProjection) and ContextFlags.LEARNING not in context.execution_phase): + elif (isinstance(projection, LearningProjection) + and (ContextFlags.LEARNING not in context.execution_phase + or not projection.receiver.owner.learnable)): projection_value = projection.defaults.value * 0.0 elif ( # learning projections add extra behavior in _execute that invalidates identity function @@ -2652,14 +2654,14 @@ def _instantiate_port(port_type: Type[Port], # Port's type reference_value = reference_value_dict[VARIABLE] parsed_port_spec = _parse_port_spec(port_type=port_type, - owner=owner, - reference_value=reference_value, - name=name, - variable=variable, - params=params, - prefs=prefs, - context=context, - **port_spec) + owner=owner, + reference_value=reference_value, + name=name, + variable=variable, + params=params, + prefs=prefs, + context=context, + **port_spec) # PORT SPECIFICATION IS A Port OBJECT *************************************** # Validate and return @@ -3061,8 +3063,9 @@ def _parse_port_spec(port_type=None, port_specification, context) port_specification = _parse_port_spec(port_type=port_type, - owner=owner, - port_spec=new_port_specification) + owner=owner, + port_spec=new_port_specification, + context=context) assert True except AttributeError: raise PortError("Attempt to assign a {} ({}) to {} that belongs to another {} ({})". @@ -3210,9 +3213,10 @@ def _parse_port_spec(port_type=None, if port_specific_specs: port_spec, params = port_type._parse_port_specific_specs(port_type, - owner=owner, - port_dict=port_dict, - port_specific_spec = port_specific_specs) + owner=owner, + port_dict=port_dict, + port_specific_spec = port_specific_specs, + context=context) # Port subclass returned a port_spec, so call _parse_port_spec to parse it if port_spec is not None: port_dict = _parse_port_spec(context=context, port_spec=port_spec, **standard_args) diff --git a/psyneulink/core/compositions/composition.py b/psyneulink/core/compositions/composition.py index 299aa1968ca..5cdc41efb8c 100644 --- a/psyneulink/core/compositions/composition.py +++ b/psyneulink/core/compositions/composition.py @@ -2812,7 +2812,7 @@ def input_function(env, result): from psyneulink.core.components.mechanisms.modulatory.control.optimizationcontrolmechanism import AGENT_REP, \ OptimizationControlMechanism from psyneulink.core.components.mechanisms.modulatory.learning.learningmechanism import \ - LearningMechanism, ACTIVATION_INPUT_INDEX, ACTIVATION_OUTPUT_INDEX, ERROR_SIGNAL, ERROR_SIGNAL_INDEX + LearningMechanism, LearningTiming, ACTIVATION_INPUT_INDEX, ACTIVATION_OUTPUT_INDEX, ERROR_SIGNAL, ERROR_SIGNAL_INDEX from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base from psyneulink.core.components.mechanisms.processing.compositioninterfacemechanism import CompositionInterfaceMechanism from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism @@ -4950,7 +4950,6 @@ def _get_invalid_aux_components(self, node): receiver_node = component.receiver.owner.composition else: receiver_node = component.receiver.owner - # Defer instantiation of all shadow Projections until call to _update_shadow_projections() if (not all([sender_node in valid_nodes, receiver_node in valid_nodes]) or (hasattr(component.receiver, SHADOW_INPUTS) and component.receiver.shadow_inputs)): invalid_components.append(component) @@ -6470,6 +6469,12 @@ def _check_for_unused_projections(self, context): node._check_for_unused_projections(context) if isinstance(node, Mechanism): for proj in [p for p in node.projections if p not in self.projections]: + # LearningProjections not listed in self.projections but executed during EXECUTION_PHASE are OK + # (e.g., EMComposition.storage_node) + if (isinstance(proj, LearningProjection) + and proj.sender.owner.learning_timing is LearningTiming.EXECUTION_PHASE + and proj.receiver.owner in self.projections): + continue proj_deferred = proj._initialization_status & ContextFlags.DEFERRED_INIT proj_name = proj._name if proj_deferred else proj.name if proj in node.afferents: @@ -8919,12 +8924,16 @@ def _check_nodes_initialization_status(self, context=None): for node in self._partially_added_nodes: for proj in self._get_invalid_aux_components(node): receiver = proj.receiver.owner - warnings.warn( - f"{node.name} has been specified to project to {receiver.name}, " - f"but {receiver.name} is not in {self.name} or any of its nested Compositions. " - f"This projection will be deactivated until {receiver.name} is added to {self.name} " - f"or a composition nested within it." - ) + # LearningProjections not listed in self.projections but executed during EXECUTION_PHASE are OK + # (e.g., EMComposition.storage_node) + if not (isinstance(proj, LearningProjection) + and proj.sender.owner.learning_timing is LearningTiming.EXECUTION_PHASE + and receiver in self.projections): + warnings.warn( + f"'{node.name}' has been specified to project to '{receiver.name}', " + f"but the latter is not in '{self.name}' or any of its nested Compositions. " + f"This projection will be deactivated until '{receiver.name}' is added to '{self.name}' " + f"or a composition nested within it.") def _get_total_cost_of_control_allocation(self, control_allocation, context, runtime_params): total_cost = 0. @@ -11334,7 +11343,7 @@ def execute( # Set to LEARNING if Mechanism receives any PathwayProjections that are being learned # for which learning_enabled == True or ONLINE (i.e., not False or AFTER) - # Implementation Note: RecurrentTransferMechanisms are special cased as the + # Implementation Note: RecurrentTransferMechanisms are special cases as the # AutoAssociativeMechanism should be handling learning - not the RTM itself. if self._is_learning(context) and not isinstance(node, RecurrentTransferMechanism): projections = set(self.projections).intersection(set(node.path_afferents)) diff --git a/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py b/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py index 40576561ec0..e2b21bf92fb 100644 --- a/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py +++ b/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py @@ -79,7 +79,7 @@ parameters to which they are assigned. It must also have at least one, and usually several `fields ` specifications that identify the `OutputPort`\\s of the `ProcessingMechanism`\\s from which it receives its `fields `, and a `field_types ` -specification that inidicates whether each `field is a key or a value field `. +specification that indicates whether each `field is a key or a value field `. .. _EMStorageMechanism_Structure: @@ -103,9 +103,17 @@ * it has a `field_types ` attribute that specifies whether each `field ` is a `key or a value field `. + * it has a `field_weights ` attribute that specifies whether each `field + ` each norms for each field are weighted before deteterming the weakest `entry + ` in `memory_matrix `. + * it has a `memory_matrix ` attribute that represents the full memory that the EMStorageMechanism is used to update. + * it has a `concatenation_node ` attribute used to access the concatenated + inputs to the `key ` fields of the `entry ` to be stored in its + `memory_matrix ` attribute. + * it has a several *LEARNING_SIGNAL* `OutputPorts ` that each send a `LearningProjection` to the `matrix ` parameter of a 'MappingProjection` that constitutes a `field ` of the `memory_matrix ` attribute. @@ -115,6 +123,11 @@ and it returns a `learning_signal ` (a weight matrix assigned to one of the Mechanism's *LEARNING_SIGNAL* OutputPorts), but no `error_signal `. + * the default form of `modulation ` for its `learning_signals + ` is *OVERRIDE*, so that the `matrix ` parameter of + the `MappingProjection` to which the `LearningProjection` projects is replaced by the `value + ` of the `learning_signal `. + * its `decay_rate `, a float in the interval [0,1] that is used to decay `memory_matrix ` before an `entry ` is stored. @@ -181,6 +194,45 @@ FIELDS = 'fields' FIELD_TYPES = 'field_types' +# def _memory_matrix_getter(owning_component=None, context=None): +# if context.composition: +# return context.composition.parameters.memory._get(context) +# else: +# return None + +def _memory_matrix_getter(owning_component=None, context=None)->list: + """Return list of memories in which rows (outer dimension) are memories for each field. + These are derived from `matrix ` parameter of the `afferent + ` MappingProjections to each of the `retrieved_nodes `. + """ + if owning_component.is_initializing: + if owning_component.learning_signals is None or owning_component.fields is None: + return None + + num_fields = len(owning_component.fields) + + # Get learning_signals that project to retrieved_nodes + num_learning_signals = len(owning_component.learning_signals) + learning_signals_for_retrieved = owning_component.learning_signals[num_learning_signals - num_fields:] + + # Get memory from learning_signals that project to retrieved_nodes + if owning_component.is_initializing: + # If initializing, learning_signals are still MappingProjections used to specify them, so get from them + memory = [retrieved_learning_signal.parameters.matrix.get(context) + for retrieved_learning_signal in learning_signals_for_retrieved] + else: + # Otherwise, get directly from the learning_signals + memory = [retrieved_learning_signal.efferents[0].receiver.owner.parameters.matrix.get(context) + for retrieved_learning_signal in learning_signals_for_retrieved] + + # Get memory capacity from first length of first matrix (can use full set since might be ragged array) + memory_capacity = len(memory[0]) + + # Reorganize memory so that each row is an entry and each column is a field + return [[memory[j][i] for j in range(num_fields)] + for i in range(memory_capacity)] + + class EMStorageMechanismError(LearningMechanismError): pass @@ -193,8 +245,8 @@ class EMStorageMechanism(LearningMechanism): field_types, \ memory_matrix, \ function=EMStorage, \ - decay_rate=0.0, \ storage_prob=1.0, \ + decay_rate=0.0, \ learning_signals, \ modulation=OVERRIDE, \ params=None, \ @@ -216,7 +268,7 @@ class EMStorageMechanism(LearningMechanism): fields : List[OutputPort, Mechanism, Projection, tuple[str, Mechanism, Projection] or dict] : default None specifies the `OutputPort`\\(s), the `value `\\s of which are used as the corresponding `fields ` of the `memory_matrix `; - used to construct the Mechanism's `InputPorts `; must be the same lenghtt as `variable + used to construct the Mechanism's `InputPorts `; must be the same length as `variable `. field_types : List[int] : default None @@ -225,6 +277,18 @@ class EMStorageMechanism(LearningMechanism): must contain only 1's (for keys) and 0's (for values), with the same number of these as there are items in the `variable ` and `fields ` arguments. + field_weights : List[float] : default None + specifies whether norms for each field are weighted before determining the weakest `entry + ` in `memory_matrix `. If None (the default), + the norm of each `entry ` is calculated across all fields at once; if specified, + it must contain only floats from 0 to 1, and be the same length as the `fields ` + argument (see `field_weights ` for additional details). + + concatenation_node : OutputPort or Mechanism : default None + specifies the `OutputPort` or `Mechanism` in which the `value ` of the `key fields + ` are concatenated (see `concatenate keys ` + for additional details). + memory_matrix : List or 2d np.array : default None specifies the shape of the `memory ` used to store an `entry ` (see `memory_matrix ` for additional details). @@ -233,30 +297,43 @@ class EMStorageMechanism(LearningMechanism): specifies the function used to assign each item of the `variable ` to the corresponding `field ` of the `memory_matrix `. It must take as its `variable argument a list or 1d array of numeric values - (the "activity vector") and return a list, 2d np.array or np.matrix for the corresponding `field - ` of the `memory_matrix ` (see `function - ` for additional details). + (the "activity vector"), as well as a ``memory_matrix`` argument that is a 2d array or matrix to which + the `variable ` is assigned, ``axis`` and ``storage_location`` arguments that + determine where in ``memory_matrix`` the `variable ` is entered, and optional + ``storage_prob`` and ``decay_rate`` arguments that determine the probability with which storage occurs and + the rate at which the `memory_matrix ` decays, respectively. The function + must return a list, 2d np.array or np.matrix for the corresponding `field ` of the + `memory_matrix ` that is updated. learning_signals : List[ParameterPort, Projection, tuple[str, Projection] or dict] : default None - specifies the `ParameterPort`\\(s) of the `MappingProjections ` that implement the `memory - ` in which the `entry ` is stored; there must the same - number of these as `fields `, and they must be specified in the sqme order. - - decay_rate : float : default 0.0 - specifies the rate at which `entries ` in the `memory_matrix - ` decays (see `decay_rate ` for additional + specifies the `ParameterPort`\\(s) for the `matrix ` parameter of the + `MappingProjection>`\\s that implement the `memory ` in which the `entry + ` is stored; there must the same number of these as `fields + `, and they must be specified in the sqme order. + + modulation : str : default OVERRIDE + specifies form of `modulation ` that `learning_signals + ` use to modify the `matrix ` parameter of the + `MappingProjections ` that implement the `memory ` in which + `entries ` is stored (see `modulation ` for additional details). storage_prob : float : default None specifies the probability with which the current entry is stored in the EMSorageMechanism's `memory_matrix ` (see `storage_prob ` for details). + decay_rate : float : default 0.0 + specifies the rate at which `entries ` in the `memory_matrix + ` decays (see `decay_rate ` for additional + details). + Attributes ---------- # FIX: FINISH EDITING: variable : 2d np.array + each item of the 2d array is used as a template for the shape of each the `fields ` that comprise and `entry ` in the `memory_matrix `, and that must be compatible (in number and type) with the `value @@ -273,6 +350,13 @@ class EMStorageMechanism(LearningMechanism): and the corresponding `fields ` are key (1) or value (0) fields. (see `fields ` for additional details). + field_weights : List[float] or None + determines whether norms for each field are weighted before identifying the weakest `entry + ` in `memory_matrix `. If is None (the default), + the norm of each `entry ` is calculated across all fields at once; if specified, + it must contain only floats from 0 to 1, and be the same length as the `fields ` + argument (see `field_weights ` for additional details). + learned_projections : List[MappingProjection] list of the `MappingProjections `, the `matrix ` Parameters of which are modified by the EMStorageMechanism. @@ -284,15 +368,15 @@ class EMStorageMechanism(LearningMechanism): np.matrix assigned to the corresponding `field ` of the `memory_matrix `. + storage_prob : float + specifies the probability with which the current entry is stored in the EMSorageMechanism's `memory_matrix + `. + decay_rate : float : default 0.0 determines the rate at which `entries ` in the `memory_matrix ` decay; the decay rate is applied to `memory_matrix ` before it is updated with the new `entry `. - storage_prob : float - specifies the probability with which the current entry is stored in the EMSorageMechanism's `memory_matrix - `. - learning_signals : List[LearningSignal] list of all of the `LearningSignals ` for the EMStorageMechanism, each of which sends a `LearningProjection` to the `ParameterPort`\\(s) for the `MappingProjections @@ -306,6 +390,15 @@ class EMStorageMechanism(LearningMechanism): in the order of the `LearningSignals ` to which they belong (that is, in the order they are listed in the `learning_signals ` attribute). + modulation : str + determines form of `modulation ` that `learning_signals + ` use to modify the `matrix ` parameter of the + `MappingProjections ` that implement the `memory ` in which + `entries ` is stored. *OVERRIDE* (the default) insure that entries are stored + exactly as specified by the `value ` of the `fields ` of the + `entry `; other values can have unpredictable consequences + (see `ModulatorySignal_Types for additional details) + output_ports : ContentAddressableList[OutputPort] list of the EMStorageMechanism's `OutputPorts `, beginning with its `learning_signals `, and followed by any additional @@ -327,30 +420,45 @@ class Parameters(LearningMechanism.Parameters): Attributes ---------- + concatenation_node + see `concatenation_node ` + + :default value: None + :type: ``Mechanism or OutputPort`` + :read only: True + decay_rate see `decay_rate ` :default value: 0.0 :type: ``float`` - fields see `fields ` :default value: None :type: ``list`` + :read only: True field_types see `field_types ` :default value: None :type: ``list`` + :read only: True + + field_weights + see `field_weights ` + + :default value: None + :type: ``list or np.ndarray`` memory_matrix see `memory_matrix ` :default value: None :type: ``np.ndarray`` + :read only: True function see `function ` @@ -365,6 +473,20 @@ class Parameters(LearningMechanism.Parameters): :type: ``list`` :read only: True + learning_signals + see `learning_signals ` + + :default value: [] + :type: ``List[MappingProjection or ParameterPort]`` + :read only: True + + modulation + see `modulation ` + + :default value: OVERRIDE + :type: ModulationParam + :read only: True + output_ports see `learning_signals ` @@ -379,7 +501,7 @@ class Parameters(LearningMechanism.Parameters): :type: ``float`` """ - # input_ports = Parameter([], + # input_ports = Parameter([], # FIX: SHOULD BE ABLE TO UE THIS WITH 'fields' AS CONSTRUCTOR ARGUMENT # stateful=False, # loggable=False, # read_only=True, @@ -388,22 +510,32 @@ class Parameters(LearningMechanism.Parameters): # # constructor_argument='fields', # ) fields = Parameter([], - stateful=False, + stateful=False, + loggable=False, + read_only=True, + structural=True, + parse_spec=True, + ) + field_types = Parameter([],stateful=False, loggable=False, read_only=True, structural=True, parse_spec=True, - ) - field_types = Parameter([], - stateful=False, - loggable=False, - read_only=True, - structural=True, - parse_spec=True, - dependiencies='fields') + dependiencies='fields') + field_weights = Parameter(None, + modulable=True, + stateful=True, + loggable=True, + dependiencies='fields') + concatenation_node = Parameter(None, + stateful=False, + loggable=False, + read_only=True, + structural=True) function = Parameter(EMStorage, stateful=False, loggable=False) - storage_prob = Parameter(1.0, modulable=True) - decay_rate = Parameter(0.0, modulable=True) + storage_prob = Parameter(1.0, modulable=True, stateful=True) + decay_rate = Parameter(0.0, modulable=True, stateful=True) + memory_matrix = Parameter(None, getter=_memory_matrix_getter, read_only=True, structural=True) modulation = OVERRIDE output_ports = Parameter([], stateful=False, @@ -422,7 +554,6 @@ class Parameters(LearningMechanism.Parameters): # learning_timing = LearningTiming.LEARNING_PHASE learning_timing = LearningTiming.EXECUTION_PHASE - # FIX: WRITE VALIDATION AND PARSE METHODS FOR THESE def _validate_field_types(self, field_types): if not len(field_types) or len(field_types) != len(self.fields): return f"must be specified with a number of items equal to " \ @@ -430,6 +561,12 @@ def _validate_field_types(self, field_types): if not all(item in {1,0} for item in field_types): return f"must be a list of 1s (for keys) and 0s (for values)." + def _validate_field_weights(self, field_weights): + if not field_weights or len(field_weights) != len(self.fields): + return f"must be specified with a number of items equal to " \ + f"the number of fields specified {len(self.fields)}" + if not all(isinstance(item, (int, float)) and (0 <= item <= 1) for item in field_weights): + return f"must be a list floats from 0 to 1." def _validate_storage_prob(self, storage_prob): storage_prob = float(storage_prob) @@ -450,6 +587,8 @@ def __init__(self, default_variable: Union[list, np.ndarray], fields: Union[list, tuple, dict, OutputPort, Mechanism, Projection] = None, field_types: list = None, + field_weights: Optional[Union[list, np.ndarray]] = None, + concatenation_node: Optional[Union[OutputPort, Mechanism]] = None, memory_matrix: Union[list, np.ndarray] = None, function: Optional[Callable] = EMStorage, learning_signals: Union[list, dict, ParameterPort, Projection, tuple] = None, @@ -462,33 +601,21 @@ def __init__(self, **kwargs ): - # # USE FOR IMPLEMENTATION OF deferred_init() - # # Store args for deferred initialization - # self._init_args = locals().copy() - # self._init_args['context'] = self - # self._init_args['name'] = name - - # # Flag for deferred initialization - # self.initialization_status = ContextFlags.DEFERRED_INIT - # self.initialization_status = ContextFlags.DEFERRED_INIT - - # self._storage_prob = storage_prob - # self.num_key_fields = len([i for i in field_types if i==0]) - super().__init__(default_variable=default_variable, fields=fields, field_types=field_types, + concatenation_node=concatenation_node, memory_matrix=memory_matrix, function=function, + learning_signals=learning_signals, modulation=modulation, decay_rate=decay_rate, storage_prob=storage_prob, - learning_signals=learning_signals, + field_weights=field_weights, params=params, name=name, prefs=prefs, - **kwargs - ) + **kwargs) def _validate_variable(self, variable, context=None): """Validate that variable has only one item: activation_input. @@ -511,7 +638,7 @@ def _validate_params(self, request_set, target_set=None, context=None): memory_matrix = request_set[MEMORY_MATRIX] # Items in variable should have the same shape as memory_matrix if memory_matrix[0].shape != np.array(self.variable).shape: - raise EMStorageMechanismError(f"The 'variable' arg for {self.name} ({variable}) must be " + raise EMStorageMechanismError(f"The 'variable' arg for {self.name} ({self.variable}) must be " f"a list or 2d np.array containing entries that have the same shape " f"({memory_matrix.shape}) as an entry (row) in 'memory_matrix' arg.") @@ -530,16 +657,34 @@ def _validate_params(self, request_set, target_set=None, context=None): f"the same number of items as its 'fields' arg ({len(fields)}).") num_keys = len([i for i in field_types if i==1]) + concatenate_keys = 'concatenation_node' in request_set and request_set['concatenation_node'] is not None + # Ensure the number of learning_signals is equal to the number of fields + number of keys if LEARNING_SIGNALS in request_set: learning_signals = request_set[LEARNING_SIGNALS] - if len(learning_signals) != len(fields) + num_keys: - raise EMStorageMechanismError(f"The number ({len(learning_signals)}) of 'learning_signals' specified " + if concatenate_keys: + num_match_fields = 1 + else: + num_match_fields = num_keys + if len(learning_signals) != num_match_fields + len(fields): + raise EMStorageMechanismError(f"The number of 'learning_signals' ({len(learning_signals)}) specified " f"for {self.name} must be the same as the number of items " f"in its variable ({len(self.variable)}).") - # Ensure shape of memory_matrix for each field is same as learning_signal for corresponding retrieval_node - for i, learning_signal in enumerate(learning_signals[num_keys:]): + # Ensure shape of learning_signals matches shapes of matrices for match nodes (i.e., either keys or concatenate) + for i, learning_signal in enumerate(learning_signals[:num_match_fields]): + learning_signal_shape = learning_signal.parameters.matrix._get(context).shape + if concatenate_keys: + memory_matrix_field_shape = np.array([np.concatenate(row, dtype=object).flatten() + for row in memory_matrix[:,0:num_keys]]).T.shape + else: + memory_matrix_field_shape = np.array(memory_matrix[:,i].tolist()).T.shape + assert learning_signal_shape == memory_matrix_field_shape, \ + f"The shape ({learning_signal_shape}) of the matrix for the Projection {learning_signal.name} " \ + f"used to specify learning signal {i} of {self.name} does not match the shape " \ + f"of the corresponding field {i} of its 'memory_matrix' {memory_matrix_field_shape})." + # Ensure shape of learning_signals matches shapes of matrices for retrieval nodes (i.e., all input fields) + for i, learning_signal in enumerate(learning_signals[num_match_fields:]): learning_signal_shape = learning_signal.parameters.matrix._get(context).shape memory_matrix_field_shape = np.array(memory_matrix[:,i].tolist()).shape assert learning_signal_shape == memory_matrix_field_shape, \ @@ -584,40 +729,81 @@ def _execute(self, runtime_params=None): """Execute EMStorageMechanism. function and return learning_signals + For each node in key_input_nodes and value_input_nodes, + assign its value to afferent weights of corresponding retrieved_node. + - memory = matrix of entries made up vectors for each field in each entry (row) + - memory_full_vectors = matrix of entries made up vectors concatentated across all fields (used for norm) + - entry_to_store = key_input or value_input to store + - field_memories = weights of Projections for each field + + DIVISION OF LABOR BETWEEN MECHANISM AND FUNCTION: + EMStorageMechanism._execute: + - compute norms to find weakest entry in memory + - compute storage_prob to determine whether to store current entry in memory + - call function for each LearningSignal to decay existing memory and assign input to weakest entry + EMStore function: + - decay existing memories + - assign input to weakest entry (given index for passed from EMStorageMechanism) + :return: List[2d np.array] self.learning_signal """ # FIX: SET LEARNING MODE HERE FOR SHOW_GRAPH - decay_rate = self.parameters.decay_rate._get(context) - storage_prob = self.parameters.storage_prob._get(context) + decay_rate = self.parameters.decay_rate._get(context) # modulable, so use getter + storage_prob = self.parameters.storage_prob._get(context) # modulable, so use getter + field_weights = self.parameters.field_weights.get(context) # modulable, so use getter + concatenation_node = self.concatenation_node + num_match_fields = 1 if concatenation_node else len([i for i in self.field_types if i==1]) - num_key_fields = len([i for i in self.field_types if i==1]) - num_fields = len(self.fields) - # learning_signals are afferents to match_nodes (key fields) then retrieval_nodes (all fields) - entries = [i for i in range(num_key_fields)] + [i for i in range(num_fields)] - value = [] - for j, item in enumerate(zip(entries, self.learning_signals)): - i = item[0] - learning_signal = item[1] - # During initialization, learning_signal is still a reciever Projection specification so get its matrix + memory = self.parameters.memory_matrix._get(context) + if memory is None or self.is_initializing: if self.is_initializing: - matrix = learning_signal.parameters.matrix._get(context) - # During execution, learning_signal is the sending OutputPort, so get the matrix of the receiver + # Return existing matrices for field_memories # FIX: THE FOLLOWING DOESN'T TEST FUNCTION: + return [learning_signal.receiver.path_afferents[0].parameters.matrix.get() + for learning_signal in self.learning_signals] + # Raise exception if not initializing and memory is not specified else: - matrix = learning_signal.efferents[0].receiver.owner.parameters.matrix._get(context) - if decay_rate: - matrix *= decay_rate - axis = 0 if j < num_key_fields else 1 - entry = variable[i] - value.append(super(LearningMechanism, self)._execute(variable=entry, - memory_matrix=matrix, + owner_string = "" + if self.owner: + owner_string = " of " + self.owner.name + raise EMStorageMechanismError(f"Call to {self.__class__.__name__} function {owner_string} " + f"must include '{MEMORY_MATRIX}' in params arg.") + + # Get least used slot (i.e., weakest memory = row of matrix with lowest weights) computed across all fields + field_norms = np.array([np.linalg.norm(field, axis=1) for field in [row for row in memory]]) + if field_weights is not None: + field_norms *= field_weights + row_norms = np.sum(field_norms, axis=1) + idx_of_weakest_memory = np.argmin(row_norms) + + value = [] + for i, field_projection in enumerate([learning_signal.efferents[0].receiver.owner + for learning_signal in self.learning_signals]): + if i < num_match_fields: + # For match matrices, + # get entry to store from variable of Projection matrix (memory_field) + # to match_node in which memory will be store (this is to accomodate concatenation_node) + axis = 0 + entry_to_store = field_projection.variable + if concatenation_node is None: + assert np.all(entry_to_store == variable[i]),\ + f"PROGRAM ERROR: misalignment between inputs and fields for storing them" + else: + # For retrieval matrices, + # get entry to store from variable (which has inputs to all fields) + axis = 1 + entry_to_store = variable[i - num_match_fields] + # Get matrix containing memories for the field from the Projection + field_memory_matrix = field_projection.parameters.matrix.get(context) + + value.append(super(LearningMechanism, self)._execute(variable=entry_to_store, + memory_matrix=field_memory_matrix, axis=axis, + storage_location=idx_of_weakest_memory, storage_prob=storage_prob, + decay_rate=decay_rate, context=context, runtime_params=runtime_params)) - - self.parameters.value._set(value, context) - return value diff --git a/psyneulink/library/compositions/emcomposition.py b/psyneulink/library/compositions/emcomposition.py index 199b588d3c1..2ab70465c2b 100644 --- a/psyneulink/library/compositions/emcomposition.py +++ b/psyneulink/library/compositions/emcomposition.py @@ -9,15 +9,19 @@ # ********************************************* EMComposition ************************************************* # TODO: +# - FIX: TRY +# - refactoring node_constructors to go after super().__init__() of EMComposition +# - FIX: SHOULD MEMORY DECAY OCCUR IF STORAGE DOES NOT? CURRENTLY IT DOES NOT (SEE EMStorage Function) +# - FIX: DOCUMENT USE OF STORAGE_LOCATION (NONE => LOCAL, SPECIFIED => GLOBAL) # - FIX: IMPLEMENT LearningMechanism FOR RETRIEVAL WEIGHTS (WHAT IS THE ERROR SIGNAL AND DERIVATIVE IT SHOULD USE?) # - FIX: GENERATE ANIMATION w/ STORAGE (uses Learning but not in usual way) -# - FIX: DEAL WITH INDEXING IN NAMES FOR NON-CONTIGOUS KEYS AND VALUES (reorder to keep all keys together?) +# - FIX: DEAL WITH INDEXING IN NAMES FOR NON-CONTIGUOUS KEYS AND VALUES (reorder to keep all keys together?) # - FIX: WRITE MORE TESTS FOR EXECUTION, WARNINGS, AND ERROR MESSAGES # - 3d tuple with first entry != memory_capacity if specified # - list with number of entries > memory_capacity if specified -# - test that input is added to the correct row of the matrix for each key and value for +# - input is added to the correct row of the matrix for each key and value for # for non-contiguous keys (e.g, field_weights = [1,0,1])) -# - test explicitly that storage occurs after retrieval +# - explicitly that storage occurs after retrieval # - FIX: _import_composition: # - MOVE LearningProjections # - MOVE Condition? (e.g., AllHaveRun) (OR PUT ON MECHANISM?) @@ -35,7 +39,7 @@ # - FIX: ?ADD add_memory() METHOD FOR STORING W/O RETRIEVAL, OR JUST ADD retrieval_prob AS modulable Parameter # - FIX: LEARNING: # - ADD LEARNING MECHANISM TO ADJUST FIELD_WEIGHTS (THAT MULTIPLICATIVELY MODULATES MAPPING PROJECTION) -# - DEAL WITH ERROR SIGNALS to retrieval_weighting_node OR AS PASS-THROUGH +# - DEAL WITH ERROR SIGNALS to softmax_weighting_node OR AS PASS-THROUGH # - FIX: CONFIDENCE COMPUTATION (USING SIGMOID ON DOT PRODUCTS) AND REPORT THAT (EVEN ON FIRST CALL) # - FIX: ALLOW SOFTMAX SPEC TO BE A DICT WITH PARAMETERS FOR _get_softmax_gain() FUNCTION # - FIX: PSYNEULINK: @@ -243,8 +247,8 @@ of its first dimension (axis 0)). All non-zero entries must be positive, and designate *keys* -- fields that are used to match items in memory for retrieval (see `Match memories by field `). Entries of 0 designate *values* -- fields that are ignored during the matching process, but the values of which - are retrieved and assigned as the `value ` of the corresponding `retrieval_node - `. This distinction between keys and value implements a standard "dictionary; however, + are retrieved and assigned as the `value ` of the corresponding `retrieved_node + `. This distinction between keys and value implements a standard "dictionary; however, if all entries are non-zero, then all fields are treated as keys, implemented a full form of content-addressable memory. If ``learn_weights`` is True, the field_weights can be modified during training; otherwise they remain fixed. The following options can be used to specify ``field_weights``: @@ -285,7 +289,7 @@ then combining the results. .. note:: - All `key_input_nodes ` and `retrieval_nodes ` + All `key_input_nodes ` and `retrieved_nodes ` are always preserved, even when `concatenate_keys ` is True, so that separate inputs can be provided for each key, and the value of each key can be retrieved separately. @@ -347,16 +351,16 @@ .. _EMComposition_Memory_Storage: .. technical_note:: The memories are actually stored in the `matrix ` parameters of the `MappingProjections` - from the `retrieval_weighting_node ` to each of the `retrieval_nodes - `. Memories associated with each key are also stored in the `matrix + from the `softmax_weighting_node ` to each of the `retrieved_nodes + `. Memories associated with each key are also stored in the `matrix ` parameters of the `MappingProjections` from the `key_input_nodes ` to each of the corresponding `match_nodes `. This is done so that the match of each key to the memories for the corresponding field can be computed simply by passing the input for each key through the Projection (which computes the dot product of the input with the Projection's `matrix ` parameter) to the corresponding match_node; and, similarly, retrieivals can be computed by passing the softmax disintributions and weighting for each field computed - in the `retrieval_weighting_node ` through its Projection to each - `retrieval_node ` (which computes the dot product of the weighted softmax over + in the `softmax_weighting_node ` through its Projection to each + `retrieved_node ` (which computes the dot product of the weighted softmax over entries with the corresponding field of each entry) to get the retreieved value for each field. .. _EMComposition_Output: @@ -365,7 +369,7 @@ ~~~~~~~ The outputs corresponding to retrieved value for each field are represented as `OUTPUT ` `Nodes -` of the EMComposition, listed in its `retrieval_nodes ` attribute. +` of the EMComposition, listed in its `retrieved_nodes ` attribute. .. _EMComposition_Execution: @@ -407,21 +411,21 @@ ` for details). * **Weight fields**. The softmax normalized dot products of keys and memories for each field are passed to the - `retrieval_weighting_node `, which applies the corresponding `field_weight + `softmax_weighting_node `, which applies the corresponding `field_weight ` to the softmaxed dot products of memories for each field, and then haddamard sums those weighted dot products to produce a single weighting for each memory. -* **Retrieve values by field**. The vector of weights for each memory generated by the `retrieval_weighting_node - ` is passed through the Projections to the each of the `retrieval_nodes - ` to compute the retrieved value for each field. +* **Retrieve values by field**. The vector of weights for each memory generated by the `softmax_weighting_node + ` is passed through the Projections to the each of the `retrieved_nodes + ` to compute the retrieved value for each field. * **Decay memories**. If `memory_decay ` is True, then each of the memories is decayed by the amount specified in `memory_decay `. .. technical_note:: This is done by multiplying the `matrix ` parameter of the `MappingProjection` from - the `retrieval_weighting_node ` to each of the `retrieval_nodes - `, as well as the `matrix ` parameter of the + the `softmax_weighting_node ` to each of the `retrieved_nodes + `, as well as the `matrix ` parameter of the `MappingProjection` from each `key_input_node ` to the corresponding `match_node ` by `memory_decay `, by 1 - `memory_decay `. @@ -435,8 +439,8 @@ .. technical_note:: This is done by adding the input vectors to the the corresponding rows of the `matrix ` - of the `MappingProjection` from the `retreival_weighting_node ` to each - of the `retrieval_nodes `, as well as the `matrix ` + of the `MappingProjection` from the `retreival_weighting_node ` to each + of the `retrieved_nodes `, as well as the `matrix ` parameter of the `MappingProjection` from each `key_input_node ` to the corresponding `match_node ` (see note `above ` for additional details). If `memory_capacity ` has been reached, then the weakest @@ -446,9 +450,9 @@ FROM CodePilot: (OF HISTORICAL INTEREST?) inputs to its `key_input_nodes ` and `value_input_nodes ` are assigned the values of the corresponding items in the -`input ` argument. The `retrieval_weighting_node ` +`input ` argument. The `softmax_weighting_node ` computes the dot product of each key with each memory, and then applies a softmax function to each row of the -resulting matrix. The `retrieval_nodes ` then compute the dot product of the +resulting matrix. The `retrieved_nodes ` then compute the dot product of the softmaxed values for each memory with the corresponding value for each memory, and the result is assigned to the corresponding `output ` item. COMMENT @@ -460,7 +464,7 @@ If `learn ` is called and the `learn_weights ` attribute is True, then the `field_weights ` are modified to minimize the error passed to the EMComposition -retrieval nodes, using the learning_rate specified in the `learning_rate ` attribute. If +retrieved nodes, using the learning_rate specified in the `learning_rate ` attribute. If `learn_weights ` is False (or `run ` is called, then the `field_weights ` are not modified and the EMComposition is simply executed without any modification, and the error signal is passed to the nodes that project to its `INPUT ` `Nodes @@ -647,7 +651,7 @@ **Use of field_weights to specify keys and values.** -Note that the figure now shows `RETRIEVAL WEIGHTING ` `nodes `, +Note that the figure now shows `RETRIEVAL WEIGHTING ` `nodes `, that are used to implement the relative contribution that each key field makes to the matching process specifed in `field_weights ` argument. By default, these are equal (all assigned a value of 1), but different values can be used to weight the relative contribution of each key field. The values are normalized so @@ -666,7 +670,7 @@ **Use of field_weights to specify relative contribution of fields to matching process.** Note that in this case, the `concatenate_keys_node ` has been replaced by a -pair of `retreival_weighting_nodes `, one for each key field. This is because +pair of `retreival_weighting_nodes `, one for each key field. This is because the keys were assigned different weights; when they are assigned equal weights, or if no weights are specified, and `normalize_memories ` is `True`, then the keys are concatenated and are concatenated for efficiency of processing. This can be suppressed by specifying `concatenate_keys` as `False` @@ -679,12 +683,13 @@ --------------- """ import numpy as np +import graph_scheduler as gs import warnings from psyneulink._typing import Optional, Union from psyneulink.core.components.functions.nonstateful.transferfunctions import SoftMax, LinearMatrix -from psyneulink.core.components.functions.nonstateful.combinationfunctions import Concatenate +from psyneulink.core.components.functions.nonstateful.combinationfunctions import Concatenate, LinearCombination from psyneulink.core.components.functions.function import \ DEFAULT_SEED, _random_state_getter, _seed_setter from psyneulink.core.compositions.composition import CompositionError, NodeRole @@ -694,13 +699,11 @@ from psyneulink.core.components.mechanisms.processing.transfermechanism import TransferMechanism from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import ControlMechanism from psyneulink.core.components.mechanisms.modulatory.control.gating.gatingmechanism import GatingMechanism -from psyneulink.core.components.mechanisms.modulatory.learning.learningmechanism import LearningMechanism from psyneulink.core.components.projections.pathway.mappingprojection import MappingProjection from psyneulink.core.globals.parameters import Parameter, check_user_specified from psyneulink.core.globals.keywords import \ - AUTO, CONTROL, DEFAULT_INPUT, DEFAULT_VARIABLE, EM_COMPOSITION, GAIN, IDENTITY_MATRIX, \ - MULTIPLICATIVE_PARAM, NAME, PARAMS, PROJECTIONS, RANDOM, SIZE, VARIABLE -from psyneulink.core.scheduling.condition import AllHaveRun + AUTO, COMBINE, CONTROL, DEFAULT_INPUT, DEFAULT_VARIABLE, EM_COMPOSITION, FUNCTION, GAIN, IDENTITY_MATRIX, \ + MULTIPLICATIVE_PARAM, NAME, PARAMS, PRODUCT, PROJECTIONS, RANDOM, SIZE, VARIABLE from psyneulink.core.globals.utilities import all_within_range __all__ = [ @@ -712,11 +715,16 @@ def _memory_getter(owning_component=None, context=None)->list: """Return list of memories in which rows (outer dimension) are memories for each field. These are derived from `matrix ` parameter of the `afferent - ` MappingProjections to each of the `retrieval_nodes `. + ` MappingProjections to each of the `retrieved_nodes `. """ - # Get memory from Projection(s) to each retrieval_node - memory = [retrieval_node.path_afferents[0].parameters.matrix.get(context) - for retrieval_node in owning_component.retrieval_nodes] + + # If storage_node is implemented, get memory from that + if owning_component.use_storage_node: + return owning_component.storage_node.parameters.memory_matrix.get(context) + + # Otherwise, get memory from Projection(s) to each retrieved_node + memory = [retrieved_node.path_afferents[0].parameters.matrix.get(context) + for retrieved_node in owning_component.retrieved_nodes] # Reorganize memory so that each row is an entry and each column is a field memory_capacity = owning_component.memory_capacity or owning_component.defaults.memory_capacity return [[memory[j][i] for j in range(owning_component.num_fields)] @@ -740,17 +748,22 @@ class EMCompositionError(CompositionError): class EMComposition(AutodiffComposition): """ - EMComposition( \ - memory_template=[[0],[0]], \ - field_weights=None, \ - field_names=None, \ - concatenate_keys=False, \ - learn_weights=True, \ - learning_rate=True, \ - memory_capacity=None, \ - memory_decay_rate=AUTO, \ - storage_prob=1.0, \ - name="EM_Composition" \ + EMComposition( \ + memory_template=[[0],[0]], \ + memory_fill=0, \ + memory_capacity=None, \ + field_weights=None, \ + field_names=None, \ + concatenate_keys=False, \ + normalize_memories=True, \ + softmax_gain=CONTROL, \ + storage_prob=1.0, \ + memory_decay_rate=AUTO, \ + learn_weights=True, \ + learning_rate=True, \ + use_storage_node=True, \ + use_gating_for_weighting=False, \ + name="EM_Composition" \ ) Subclass of `AutodiffComposition` that implements the functions of an `EpisodicMemoryMechanism` in a @@ -769,6 +782,10 @@ class EMComposition(AutodiffComposition): specifies the value used to fill the memory when it is initialized; see `memory_fill ` for details. + memory_capacity : int : default None + specifies the number of items that can be stored in the EMComposition's memory; + see `memory_capacity ` for details. + field_weights : tuple : default (1,0) specifies the relative weight assigned to each key when matching an item in memory' see `field weights ` for details. @@ -794,6 +811,10 @@ class EMComposition(AutodiffComposition): when the EMComposition is executed (see `Retrieval and Storage ` for additional details). + memory_decay_rate : float : AUTO + specifies the rate at which items in the EMComposition's memory decay; + see `memory_decay_rate ` for details. + learn_weights : bool : default False specifies whether `field_weights ` are learnable during training; see `Learning ` for additional details. @@ -801,18 +822,21 @@ class EMComposition(AutodiffComposition): learning_rate : float : default .01 specifies rate at which `field_weights ` are learned if ``learn_weights`` is True. - memory_capacity : int : default None - specifies the number of items that can be stored in the EMComposition's memory; - see `memory_capacity ` for details. - - memory_decay : bool : default True - specifies whether memories decay with each execution of the EMComposition; - see `memory_decay ` for details. - - memory_decay_rate : float : AUTO - specifies the rate at which items in the EMComposition's memory decay; - see `memory_decay_rate ` for details. - + use_storage_node : bool : default True + specifies whether to use a `LearningMechanism` to store entries in `memory `. + If False, a method on EMComposition is used, which precludes use of `import_composition + ` to integrate the EMComposition into another Composition; to do so, + use_storage_node must be set to True. + + use_gating_for_weighting : bool : default False + specifies whether to use a `GatingMechanism` to modulate the `softmax_weighting_node + ` instead of a standard ProcessingMechanism. If True, then + a GatingMechanism is constructed and used to gate the `OutputPort` of each `retrieval_weighting_node + EMComposition.retrieval_weighting_nodes`; otherwise, the output of each `retrieval_weighting_node + EMComposition.retrieval_weighting_nodes` projects to the `InputPort` of the `softmax_weighting_node + EMComposition.softmax_weighting_node` that receives a Projection from the corresponding + `retrieval_weighting_node `, and multiplies its `value + `. Attributes ---------- @@ -830,6 +854,10 @@ class EMComposition(AutodiffComposition): .. _EMComposition_Parameters: + memory_capacity : int + determines the number of items that can be stored in `memory `; see `memory_capacity + ` for additional details. + field_weights : list[float] determines which fields of the input are treated as "keys" (non-zero values), used to match entries in `memory ` for retrieval, and which are used as "values" (zero values), that are stored and @@ -841,14 +869,6 @@ class EMComposition(AutodiffComposition): determines which names that can be used to label fields in `memory `; see `field_names ` for additional details. - learn_weights : bool - determines whether `field_weights ` are learnable during training; see - `Learning ` for additional details. - - learning_rate : float - determines whether the rate at which `field_weights ` are learned if - `learn_weights` is True; see `EMComposition_Learning>` for additional details. - concatenate_keys : bool determines whether keys are concatenated into a single field before matching them to items in `memory `; see `concatenate keys ` for additional details. @@ -867,14 +887,18 @@ class EMComposition(AutodiffComposition): when the EMComposition is executed (see `Retrieval and Storage ` for additional details). - memory_capacity : int - determines the number of items that can be stored in `memory `; see `memory_capacity - ` for additional details. - memory_decay_rate : float determines the rate at which items in the EMComposition's memory decay (see `memory_decay_rate ` for details). + learn_weights : bool + determines whether `field_weights ` are learnable during training; see + `Learning ` for additional details. + + learning_rate : float + determines whether the rate at which `field_weights ` are learned if + `learn_weights` is True; see `EMComposition_Learning>` for additional details. + .. _EMComposition_Nodes: key_input_nodes : list[TransferMechanism] @@ -916,23 +940,31 @@ class EMComposition(AutodiffComposition): retrieval_gating_nodes : list[GatingMechanism] `GatingMechanisms ` that uses the `field weight ` for each - field to modulate the output of the corresponding `retrieval_node ` before it - is passed to the `retrieval_weighting_node `. These are implemented - only if more than one `key field ` is specified (see `Fields ` - for additional details). - - retrieval_weighting_node : TransferMechanism + field to modulate the output of the corresponding `retrieved_node ` before it + is passed to the `softmax_weighting_node `. These are implemented + only if `use_gating_for_weighting ` is True and more than one + `key field ` is specified (see `Fields ` for additional details). + + retrieval_weighting_nodes : list[ProcessingMechanism] + `ProcessingMechanisms ` that use the `field weight ` for + each field as their (fixed) input to multiply the input to the corresponding `input_port ` of the + `softmax_weighting_node `. These are implemented only if more than one + `key field ` is specified (see `Fields ` for additional details), + and are replaced with `retrieval_gating_nodes ` if + `use_gating_for_weighting ` is True. + + softmax_weighting_node : TransferMechanism `TransferMechanism` that receives the softmax normalized dot products of the keys and memories from the `softmax_nodes `, weights these using `field_weights `, and haddamard sums those weighted dot products to produce a single weighting for each memory. - retrieval_nodes : list[TransferMechanism] + retrieved_nodes : list[TransferMechanism] `TransferMechanisms ` that receive the vector retrieved for each field in `memory ` (see `Retrieve values by field ` for additional details); these are assigned the same names as the `key_input_nodes ` and `value_input_nodes ` to which they correspond appended with the suffix - *_RETRIEVAL*. + *_RETRIEVED*. storage_node : EMStorageMechanism `EMStorageMechanism` that receives inputs from the `key_input_nodes ` and @@ -941,8 +973,8 @@ class EMComposition(AutodiffComposition): has been made (see `Retrieval and Storage ` for additional details). .. technical_note:: - The `storage_node ` is assigned a Condition to execute after the `retrieval_nodes - ` have executed, to ensure that storage occurs after retrieval, but before + The `storage_node ` is assigned a Condition to execute after the `retrieved_nodes + ` have executed, to ensure that storage occurs after retrieval, but before any subequent processing is done (i.e., in a composition in which the EMComposition may be embededded. """ @@ -958,23 +990,17 @@ class Parameters(AutodiffComposition.Parameters): :default value: False :type: ``bool`` - memory_decay - see `memory_decay ` - - :default value: False - :type: ``bool`` - - memory_decay_rate - see `memory_decay_rate ` + field_names + see `field_names ` - :default value: 0.001 - :type: ``float`` + :default value: None + :type: ``list`` - learn_weights - see `learn_weights ` + field_weights + see `field_weights ` - :default value: False # False UNTIL IMPLEMENTED - :type: ``bool`` + :default value: None + :type: ``numpy.ndarray`` learning_rate see `learning_results ` @@ -982,6 +1008,12 @@ class Parameters(AutodiffComposition.Parameters): :default value: [] :type: ``list`` + learn_weights + see `learn_weights ` + + :default value: False # False UNTIL IMPLEMENTED + :type: ``bool`` + memory see `memory ` @@ -994,24 +1026,18 @@ class Parameters(AutodiffComposition.Parameters): :default value: 1000 :type: ``int`` + memory_decay_rate + see `memory_decay_rate ` + + :default value: 0.001 + :type: ``float`` + memory_template see `memory_template ` :default value: np.array([[0],[0]]) :type: ``np.ndarray`` - field_names - see `field_names ` - - :default value: None - :type: ``list`` - - field_weights - see `field_weights ` - - :default value: None - :type: ``numpy.ndarray`` - normalize_memories see `normalize_memories ` @@ -1041,13 +1067,12 @@ class Parameters(AutodiffComposition.Parameters): field_weights = Parameter(None, structural=True) field_names = Parameter(None, structural=True) concatenate_keys = Parameter(False, structural=True) - memory_decay_rate = Parameter(AUTO, loggable=True, modulable=True, fallback_default=True, - dependencies={'memory_capacity'}) normalize_memories = Parameter(True, loggable=False, fallback_default=True) + softmax_gain = Parameter(CONTROL, modulable=True, fallback_default=True) + storage_prob = Parameter(1.0, modulable=True, aliases=[MULTIPLICATIVE_PARAM]) + memory_decay_rate = Parameter(AUTO, modulable=True) learn_weights = Parameter(False, fallback_default=True) # FIX: False until learning is implemented learning_rate = Parameter(.001, fallback_default=True) - storage_prob = Parameter(1.0, modulable=True, aliases=[MULTIPLICATIVE_PARAM]) - softmax_gain = Parameter(CONTROL, modulable=True, fallback_default=True) random_state = Parameter(None, loggable=False, getter=_random_state_getter, dependencies='seed') seed = Parameter(DEFAULT_SEED, modulable=True, fallback_default=True, setter=_seed_setter) @@ -1094,12 +1119,6 @@ def _validate_storage_prob(self, storage_prob): if not all_within_range(storage_prob, 0, 1): return f"must be a float in the interval [0,1]." - # def _parse_memory_decay_rate(self, memory_decay_rate): - # if not memory_decay_rate: - # return 0.0 - # if memory_decay_rate is AUTO: - # return 1 / self.memory_capacity.get() - @check_user_specified def __init__(self, memory_template:Union[tuple, list, np.ndarray]=[[0],[0]], @@ -1108,12 +1127,14 @@ def __init__(self, field_names:Optional[list]=None, field_weights:tuple=None, concatenate_keys:bool=False, - learn_weights:bool=False, # FIX: False FOR NOW, UNTIL IMPLEMENTED - learning_rate:float=None, - memory_decay_rate:Union[float,AUTO]=AUTO, normalize_memories:bool=True, softmax_gain:Union[float, CONTROL]=CONTROL, storage_prob:float=1.0, + memory_decay_rate:Union[float,AUTO]=AUTO, + learn_weights:bool=False, # FIX: False FOR NOW, UNTIL IMPLEMENTED + learning_rate:float=None, + use_storage_node:bool=True, + use_gating_for_weighting:bool=False, random_state=None, seed=None, name="EM_Composition"): @@ -1136,30 +1157,35 @@ def __init__(self, if memory_decay_rate is AUTO: memory_decay_rate = 1 / memory_capacity - # Instantiate Composition ------------------------------------------------------------------------- + self.use_storage_node = use_storage_node + - pathway = self._construct_pathway(memory_template, - memory_capacity, - memory_decay_rate, - field_weights, - concatenate_keys, - normalize_memories, - softmax_gain, - storage_prob) + # Instantiate Composition ------------------------------------------------------------------------- - super().__init__(pathway, + nodes = self._construct_pathway(memory_template, + memory_capacity, + field_weights, + concatenate_keys, + normalize_memories, + softmax_gain, + storage_prob, + memory_decay_rate, + use_storage_node, + use_gating_for_weighting) + + super().__init__(nodes, name=name, memory_template = memory_template, memory_capacity = memory_capacity, field_weights = field_weights, field_names = field_names, concatenate_keys = concatenate_keys, + softmax_gain = softmax_gain, + storage_prob = storage_prob, memory_decay_rate = memory_decay_rate, normalize_memories = normalize_memories, learn_weights = learn_weights, learning_rate = learning_rate, - storage_prob = storage_prob, - softmax_gain = softmax_gain, random_state = random_state, seed = seed ) @@ -1170,20 +1196,21 @@ def __init__(self, self._set_learning_attributes() # Set condition on storage_node - # for node in self.retrieval_nodes: + # for node in self.retrieved_nodes: # self.scheduler.add_condition(self.storage_node, WhenFinished(node)) - # self.scheduler.add_condition(self.storage_node, WhenFinished(self.retrieval_nodes[1])) - self.scheduler.add_condition(self.storage_node, AllHaveRun(*self.retrieval_nodes)) + # self.scheduler.add_condition(self.storage_node, WhenFinished(self.retrieved_nodes[1])) + if self.use_storage_node: + self.scheduler.add_condition(self.storage_node, gs.AllHaveRun(*self.retrieved_nodes)) # Suppress warnings for no efferent Projections for node in self.value_input_nodes: node.output_ports['RESULT'].parameters.require_projection_in_composition.set(False, override=True) - for port in self.retrieval_weighting_node.output_ports: + for port in self.softmax_weighting_node.output_ports: if 'RESULT' in port.name: port.parameters.require_projection_in_composition.set(False, override=True) - # Suppress retrieval_gating_nodes as INPUT nodes of the Composition - for node in self.retrieval_gating_nodes: + # Suppress retrieval_weighting_nodes as INPUT nodes of the Composition + for node in self.retrieval_weighting_nodes: self.exclude_node_roles(node, NodeRole.INPUT) # Suppress value_input_nodes as OUTPUT nodes of the Composition @@ -1431,12 +1458,15 @@ def _parse_memory_shape(self, memory_template): def _construct_pathway(self, memory_template, memory_capacity, - memory_decay_rate, field_weights, concatenate_keys, normalize_memories, softmax_gain, - storage_prob)->set: + storage_prob, + memory_decay_rate, + use_storage_node, + use_gating_for_weighting, + )->set: """Construct pathway for EMComposition""" # Construct nodes of Composition @@ -1446,21 +1476,29 @@ def _construct_pathway(self, self.concatenate_keys_node = self._construct_concatenate_keys_node(concatenate_keys) self.match_nodes = self._construct_match_nodes(memory_template, memory_capacity, concatenate_keys,normalize_memories) + if not use_gating_for_weighting: + self.retrieval_weighting_nodes = self._construct_retrieval_weighting_nodes(field_weights, + concatenate_keys, + use_gating_for_weighting) self.softmax_nodes = self._construct_softmax_nodes(memory_capacity, field_weights, softmax_gain) + if use_gating_for_weighting: + self.retrieval_weighting_nodes = self._construct_retrieval_weighting_nodes(field_weights, + concatenate_keys, + use_gating_for_weighting) self.softmax_control_nodes = self._construct_softmax_control_nodes(softmax_gain) - self.retrieval_gating_nodes = self._construct_retrieval_gating_nodes(field_weights, concatenate_keys) - self.retrieval_weighting_node = self._construct_retrieval_weighting_node(memory_capacity) - self.retrieval_nodes = self._construct_retrieval_nodes(memory_template) - self.storage_node = self._construct_storage_node(memory_template, field_weights, concatenate_keys, - memory_decay_rate, storage_prob) + self.softmax_weighting_node = self._construct_softmax_weighting_node(memory_capacity, use_gating_for_weighting) + self.retrieved_nodes = self._construct_retrieved_nodes(memory_template) + if use_storage_node: + self.storage_node = self._construct_storage_node(memory_template, field_weights, self.concatenate_keys_node, + memory_decay_rate, storage_prob) # Construct pathway as a set of nodes, since Projections are specified in the construction of each node # (and specifying INPUT or OUTPUT Nodes in a list would cause them to be interpreted as linear pathways) pathway = set(self.key_input_nodes + self.value_input_nodes - + self.match_nodes + self.softmax_control_nodes + self.softmax_nodes - + [self.retrieval_weighting_node] + self.retrieval_gating_nodes + self.retrieval_nodes - + [self.storage_node] - ) + + self.match_nodes + self.retrieval_weighting_nodes + self.softmax_control_nodes + + self.softmax_nodes + [self.softmax_weighting_node] + self.retrieved_nodes) + if use_storage_node: + pathway.add(self.storage_node) if self.concatenate_keys_node is not None: pathway.add(self.concatenate_keys_node) @@ -1487,7 +1525,7 @@ def _construct_key_input_nodes(self, field_weights)->list: def _construct_value_input_nodes(self, field_weights)->list: """Create one input node for each value to be stored in memory. - Used to assign new set of weights for Projection for retrieval_weighting_node -> retrieval_node[i] + Used to assign new set of weights for Projection for softmax_weighting_node -> retrieved_node[i] where i is selected randomly without replacement from (0->memory_capacity) """ @@ -1545,7 +1583,8 @@ def _construct_match_nodes(self, memory_template, memory_capacity, concatenate_k PROJECTIONS: MappingProjection(sender=self.concatenate_keys_node, matrix=matrix, function=LinearMatrix( - normalize=normalize_memories))}, + normalize=normalize_memories), + name=f'MEMORY')}, name='MATCH')] # One node for each key @@ -1557,7 +1596,8 @@ def _construct_match_nodes(self, memory_template, memory_capacity, concatenate_k PROJECTIONS: MappingProjection(sender=self.key_input_nodes[i].output_port, matrix = np.array(memory_template[:,i].tolist() ).transpose().astype(float), - function=LinearMatrix(normalize=normalize_memories))}, + function=LinearMatrix(normalize=normalize_memories), + name=f'MEMORY for {self.key_names[i]}')}, name=f'MATCH {self.key_names[i]}') for i in range(self.num_keys) ] @@ -1585,7 +1625,8 @@ def _construct_softmax_nodes(self, memory_capacity, field_weights, softmax_gain) softmax_nodes = [TransferMechanism(input_ports={SIZE:memory_capacity, PROJECTIONS: match_node.output_port}, function=SoftMax(gain=softmax_gain), - name='SOFTMAX' if len(self.match_nodes) == 1 else f'SOFTMAX {i}') + name='SOFTMAX' if len(self.match_nodes) == 1 + else f'SOFTMAX for {self.key_names[i]}') for i, match_node in enumerate(self.match_nodes)] return softmax_nodes @@ -1604,75 +1645,103 @@ def _construct_softmax_control_nodes(self, softmax_gain)->list: return softmax_control_nodes - def _construct_retrieval_gating_nodes(self, field_weights, concatenate_keys)->list: - """Create GatingMechanisms that weight each key's contribution to the retrieved values. + def _construct_retrieval_weighting_nodes(self, field_weights, concatenate_keys, use_gating_for_weighting)->list: + """Create ProcessingMechanisms that weight each key's softmax contribution to the retrieved values. """ - # FIX: CONSIDER USING THIS FOR INPUT GATING OF MATCH NODE(S)? - retrieval_gating_nodes = [] - if not concatenate_keys and self.num_keys > 1: - retrieval_gating_nodes = [GatingMechanism(input_ports={VARIABLE: field_weights[i], - PARAMS:{DEFAULT_INPUT: DEFAULT_VARIABLE}, - NAME: 'OUTCOME'}, - gate=[key_match_pair[1].output_ports[0]], - name= 'RETRIEVAL WEIGHTING' if self.num_keys == 1 - else f'RETRIEVAL WEIGHTING {i}') - for i, key_match_pair in enumerate(zip(self.key_input_nodes, self.softmax_nodes))] - - return retrieval_gating_nodes + retrieval_weighting_nodes = [] - def _construct_retrieval_weighting_node(self, memory_capacity)->ProcessingMechanism: + if not concatenate_keys and self.num_keys > 1: + if use_gating_for_weighting: + retrieval_weighting_nodes = [GatingMechanism(input_ports={VARIABLE: field_weights[i], + PARAMS:{DEFAULT_INPUT: DEFAULT_VARIABLE}, + NAME: 'OUTCOME'}, + gate=[key_match_pair[1].output_ports[0]], + name= 'RETRIEVAL WEIGHTING' if self.num_keys == 1 + else f'RETRIEVAL WEIGHTING {i}') + for i, key_match_pair in enumerate(zip(self.key_input_nodes, + self.softmax_nodes))] + else: + retrieval_weighting_nodes = [ProcessingMechanism(input_ports={VARIABLE: field_weights[i], + PARAMS:{DEFAULT_INPUT: DEFAULT_VARIABLE}, + NAME: 'FIELD_WEIGHT'}, + name= 'WEIGHT' if self.num_keys == 1 + else f'WEIGHT FOR {self.key_names[i]}') + for i in range(self.num_keys)] + return retrieval_weighting_nodes + + def _construct_softmax_weighting_node(self, memory_capacity, use_gating_for_weighting)->ProcessingMechanism: """Create nodes that compute the weighting of each item in memory. """ - retrieval_weighting_node = ProcessingMechanism(input_ports=[{SIZE:memory_capacity, - PROJECTIONS:[m.output_port for m in - self.softmax_nodes]}], - name='RETRIEVAL') - assert len(retrieval_weighting_node.output_port.value) == memory_capacity, \ - 'PROGRAM ERROR: number of items in retrieval_weighting_node ' \ - '({len(retrieval_weighting_node.output_port)}) does not match memory_capacity ({self.memory_capacity})' + if use_gating_for_weighting: + softmax_weighting_node = ProcessingMechanism(input_ports=[{SIZE:memory_capacity, + PROJECTIONS:[m.output_port for m in + self.softmax_nodes]}], + name='RETRIEVAL') + else: + if self.retrieval_weighting_nodes: + input_ports = [{SIZE: memory_capacity, + FUNCTION: LinearCombination(operation=PRODUCT), + PROJECTIONS: [sm_rw_pair[0].output_port, sm_rw_pair[1].output_port], + NAME: f"WEIGHT FOR {self.key_names[i]}"} + for i, sm_rw_pair in enumerate(zip(self.softmax_nodes, + self.retrieval_weighting_nodes))] + else: + input_ports = [{SIZE: memory_capacity, + PROJECTIONS: [sm_node.output_port], + NAME: f"WEIGHT FOR {self.key_names[i]}"} + for i, sm_node in enumerate(self.softmax_nodes)] + + softmax_weighting_node = ProcessingMechanism(input_ports=input_ports, + function=LinearCombination, + name='RETRIEVAL') - return retrieval_weighting_node + assert len(softmax_weighting_node.output_port.value) == memory_capacity, \ + 'PROGRAM ERROR: number of items in softmax_weighting_node ' \ + '({len(softmax_weighting_node.output_port)}) does not match memory_capacity ({self.memory_capacity})' - def _construct_retrieval_nodes(self, memory_template)->list: + return softmax_weighting_node + + def _construct_retrieved_nodes(self, memory_template)->list: """Create nodes that report the value field(s) for the item(s) matched in memory. """ - self.retrieved_key_nodes = [TransferMechanism(input_ports={SIZE: len(self.key_input_nodes[i].variable[0]), - PROJECTIONS: - MappingProjection( - sender=self.retrieval_weighting_node, - # matrix=ZEROS_MATRIX) - matrix=memory_template[:,i]) - }, - name= f'{self.key_names[i]} RETRIEVED') - for i in range(self.num_keys)] - - self.retrieved_value_nodes = [TransferMechanism(input_ports={SIZE: len(self.value_input_nodes[i].variable[0]), - PROJECTIONS: - MappingProjection( - sender=self.retrieval_weighting_node, - # matrix=ZEROS_MATRIX) - matrix=memory_template[:, - i + self.num_keys]) - }, - name= f'{self.value_names[i]} RETRIEVED') - for i in range(self.num_values)] + self.retrieved_key_nodes = \ + [TransferMechanism(input_ports={SIZE: len(self.key_input_nodes[i].variable[0]), + PROJECTIONS: + MappingProjection( + sender=self.softmax_weighting_node, + matrix=memory_template[:,i], + name=f'MEMORY FOR {self.key_names[i]}') + }, + name= f'{self.key_names[i]} RETRIEVED') + for i in range(self.num_keys)] + + self.retrieved_value_nodes = \ + [TransferMechanism(input_ports={SIZE: len(self.value_input_nodes[i].variable[0]), + PROJECTIONS: + MappingProjection( + sender=self.softmax_weighting_node, + matrix=memory_template[:, + i + self.num_keys], + name=f'MEMORY FOR {self.value_names[i]}')}, + name= f'{self.value_names[i]} RETRIEVED') + for i in range(self.num_values)] return self.retrieved_key_nodes + self.retrieved_value_nodes def _construct_storage_node(self, memory_template, field_weights, - concatenate_keys, + concatenate_keys_node, memory_decay_rate, storage_prob)->list: """Create EMStorageMechanism that stores the key and value inputs in memory. Memories are stored by adding the current input to each field to the corresponding row of the matrix for - the Projection from the key_input_node to the matching_node and retrieval_node for keys, and from the - value_input_node to the retrieval_node for values. The `function ` of the - `EMSorageMechanism` that takes the following arguments: + the Projection from the key_input_node (or concatenate_node) to the matching_node and retrieved_node for keys, + and from the value_input_node to the retrieved_node for values. The `function ` + of the `EMSorageMechanism` that takes the following arguments: - **variable* -- template for an `entry ` in `memory`; @@ -1682,7 +1751,10 @@ def _construct_storage_node(self, - **field_types* -- a list of the same length as ``fields``, containing 1's for key fields and 0's for value fields; - - **memory_matrix* -- `memory_template `), + - **concatenate_keys_node* -- node used to concatenate keys (if `concatenate_keys + ` is `True`) or None; + + - **memory_matrix* -- `memory_template `); - **learning_signals* -- list of ` `MappingProjection`\\s (or their ParameterPort`\\s) that store each `field ` of `memory `; @@ -1692,23 +1764,31 @@ def _construct_storage_node(self, - **storage_prob** -- probability for storing an entry in `memory `. """ - field_types = [0 if weight == 0 else 1 for weight in field_weights] - - if concatenate_keys: - projections = [self.concatenate_keys_node.input_ports[i].path_afferents[0] for i in range(self.num_keys)] + \ - [self.retrieval_nodes[i].input_port.path_afferents[0] for i in range(len(self.input_nodes))] - else: - projections = [self.match_nodes[i].input_port.path_afferents[0] for i in range(self.num_keys)] + \ - [self.retrieval_nodes[i].input_port.path_afferents[0] for i in range(len(self.input_nodes))] + # if concatenate_keys: + # projections = [self.concatenate_keys_node.input_ports[i].path_afferents[0] for i in range(self.num_keys)] + \ + # [self.retrieved_nodes[i].input_port.path_afferents[0] for i in range(len(self.input_nodes))] + # variable = [self.input_nodes[i].value[0] for i in range(self.num_fields)] + # fields = [self.input_nodes[i] for i in range(self.num_fields)] + # else: + # variable = [self.input_nodes[i].value[0] for i in range(self.num_fields)] + # fields = [self.input_nodes[i] for i in range(self.num_fields)] + # projections = [self.match_nodes[i].input_port.path_afferents[0] for i in range(self.num_keys)] + \ + # [self.retrieved_nodes[i].input_port.path_afferents[0] for i in range(len(self.input_nodes))] + + learning_signals = [match_node.input_port.path_afferents[0] + for match_node in self.match_nodes] + \ + [retrieved_node.input_port.path_afferents[0] + for retrieved_node in self.retrieved_nodes] storage_node = EMStorageMechanism(default_variable=[self.input_nodes[i].value[0] for i in range(self.num_fields)], fields=[self.input_nodes[i] for i in range(self.num_fields)], - field_types=field_types, + field_types=[0 if weight == 0 else 1 for weight in field_weights], + concatenation_node=self.concatenate_keys_node, memory_matrix=memory_template, - learning_signals=projections, - decay_rate = memory_decay_rate, + learning_signals=learning_signals, storage_prob=storage_prob, + decay_rate = memory_decay_rate, name='STORAGE MECHANISM') return storage_node @@ -1717,14 +1797,13 @@ def _set_learning_attributes(self): """ # self.require_node_roles(self.storage_node, NodeRole.LEARNING) - # Turn off learning for all Projections except afferents to retrieval_gating_nodes. - for node in self.nodes: - for input_port in node.input_ports: - for proj in input_port.path_afferents: - if node in self.retrieval_gating_nodes: - proj.learnable = self.learn_weights - else: - proj.learnable = False + for projection in self.projections: + if projection.sender.owner in self.retrieval_weighting_nodes: + # projection.learnable = self.learn_weights + projection.learnable = True # FIX: FOR NOW, UNTIL LEARNING OF RETRIEVAL WEIGHTING IS IMPLEMENTED + else: + projection.learnable = False + # ***************************************************************************************************************** # *********************************** Execution Methods ********************************************************** @@ -1733,11 +1812,12 @@ def _set_learning_attributes(self): def execute(self, inputs, context, **kwargs): """Set input to weights of Projection to match_node.""" results = super().execute(inputs, **kwargs) - # self._store_memory(inputs, context) + if not self.use_storage_node: + self._store_memory(inputs, context) return results def _store_memory(self, inputs, context): - """Store inputs in memory as weights of Projections to softmax_nodes (keys) and retrieval_nodes (values). + """Store inputs in memory as weights of Projections to softmax_nodes (keys) and retrieved_nodes (values). """ storage_prob = np.array(self._get_current_parameter_value(STORAGE_PROB, context)).astype(float) random_state = self._get_current_parameter_value('random_state', context) @@ -1750,43 +1830,73 @@ def _store_memory(self, inputs, context): def _encode_memory(self, context=None): """Encode inputs as memories For each node in key_input_nodes and value_input_nodes, - assign its value to afferent weights of corresponding retrieval_node. - - memory = key_input or value_input - - memories = weights of Projections for each field + assign its value to afferent weights of corresponding retrieved_node. + - memory = matrix of entries made up vectors for each field in each entry (row) + - memory_full_vectors = matrix of entries made up vectors concatentated across all fields (used for norm) + - entry_to_store = key_input or value_input to store + - field_memories = weights of Projections for each field """ - for i, input_node in enumerate(self.key_input_nodes + self.value_input_nodes): - memory = input_node.value[0] - # Store key_input vector in projections from input_key_nodes to match_nodes - if input_node in self.key_input_nodes: - # For key_input: - # assign as weights for first empty row of Projection.matrix from key_input_node to match_node - memories = input_node.efferents[0].parameters.matrix.get(context) + # Get least used slot (i.e., weakest memory = row of matrix with lowest weights) computed across all fields + purge_by_field_weights = False + field_norms = np.array([np.linalg.norm(field, axis=1) + for field in [row for row in self.parameters.memory.get(context)]]) + if purge_by_field_weights: + field_norms *= self.field_weights + row_norms = np.sum(field_norms, axis=1) + idx_of_min = np.argmin(row_norms) + + # If concatenate_keys is True, assign entry to col of matrix for Projection from concatenate_node to match_node + if self.concatenate_keys_node: + # Get entry to store from concatenate_keys_node + entry_to_store = self.concatenate_keys_node.value[0] + # Get matrix of weights for Projection from concatenate_node to match_node + field_memories = self.concatenate_keys_node.efferents[0].parameters.matrix.get(context) + # Decay existing memories before storage if memory_decay_rate is specified + if self.memory_decay_rate: + field_memories *= self.parameters.memory_decay_rate._get(context) + # Assign input vector to col of matrix that has lowest norm (i.e., weakest memory) + field_memories[:,idx_of_min] = np.array(entry_to_store) + # Assign updated matrix to Projection + self.concatenate_keys_node.efferents[0].parameters.matrix.set(field_memories, context) + + # Otherwise, assign input for each key field to col of matrix for Projection from key_input_node to match_node + else: + for i, input_node in enumerate(self.key_input_nodes): + # Get entry to store from key_input_node + entry_to_store = input_node.value[0] + # Get matrix of weights for Projection from key_input_node to match_node + field_memories = input_node.efferents[0].parameters.matrix.get(context) + # Decay existing memories before storage if memory_decay_rate is specified if self.memory_decay_rate: - memories *= self.parameters.memory_decay_rate._get(context) - # Get least used slot (i.e., weakest memory = row of matrix with lowest weights) - # idx_of_min = np.argmin(memories.sum(axis=0)) - idx_of_min = np.argmin(np.linalg.norm(memories, axis=0)) - memories[:,idx_of_min] = np.array(memory) - input_node.efferents[0].parameters.matrix.set(memories, context) - - # For all inputs, assign input vector to afferent weights of corresponding retrieval_node - memories = self.retrieval_nodes[i].path_afferents[0].parameters.matrix.get(context) + field_memories *= self.parameters.memory_decay_rate._get(context) + # Assign key_input vector to col of matrix that has lowest norm (i.e., weakest memory) + field_memories[:,idx_of_min] = np.array(entry_to_store) + # Assign updated matrix to Projection + input_node.efferents[0].parameters.matrix.set(field_memories, context) + + # For each key and value field, assign input to row of matrix for Projection to retrieved_nodes + for i, input_node in enumerate(self.key_input_nodes + self.value_input_nodes): + # Get entry to store from key_input_node or value_input_node + entry_to_store = input_node.value[0] + # Get matrix of weights for Projection from input_node to match_node + field_memories = self.retrieved_nodes[i].path_afferents[0].parameters.matrix.get(context) + # Decay existing memories before storage if memory_decay_rate is specified if self.memory_decay_rate: - memories *= self.memory_decay_rate - # Get least used slot (i.e., weakest memory = row of matrix with lowest weights) - idx_of_min = np.argmin(memories.sum(axis=1)) - memories[idx_of_min] = np.array(memory) - self.retrieval_nodes[i].path_afferents[0].parameters.matrix.set(memories, context) + field_memories *= self.memory_decay_rate + # Assign input vector to col of matrix that has lowest norm (i.e., weakest memory) + field_memories[idx_of_min] = np.array(entry_to_store) + # Assign updated matrix to Projection + self.retrieved_nodes[i].path_afferents[0].parameters.matrix.set(field_memories, context) def learn(self): raise EMCompositionError(f"EMComposition can be constructed, but 'learn' method not yet working") def get_output_values(self, context=None): - """Override to provide ordering of retrieval_nodes that matches order of inputs. + """Override to provide ordering of retrieved_nodes that matches order of inputs. This is needed since nodes were constructed as sets """ - return [retrieval_node.output_port.parameters.value.get(context) - for retrieval_node in self.retrieval_nodes - if (not self.output_CIM._sender_is_probe(self.output_CIM.port_map[retrieval_node.output_port][1]) + return [retrieved_node.output_port.parameters.value.get(context) + for retrieved_node in self.retrieved_nodes + if (not self.output_CIM._sender_is_probe(self.output_CIM.port_map[retrieved_node.output_port][1]) or self.include_probes_in_output)] diff --git a/psyneulink/library/compositions/regressioncfa.py b/psyneulink/library/compositions/regressioncfa.py index a03f6030f6a..ef878c4056c 100644 --- a/psyneulink/library/compositions/regressioncfa.py +++ b/psyneulink/library/compositions/regressioncfa.py @@ -88,6 +88,7 @@ from psyneulink.core.compositions.compositionfunctionapproximator import CompositionFunctionApproximator, CompositionFunctionApproximatorError from psyneulink.core.globals.keywords import ALL, CONTROL_SIGNALS, DEFAULT_VARIABLE, VARIABLE from psyneulink.core.globals.parameters import Parameter, check_user_specified +from psyneulink.core.globals.context import Context from psyneulink.core.globals.utilities import get_deepcopy_with_shared, powerset, tensor_power __all__ = ['PREDICTION_TERMS', 'PV', 'RegressionCFA'] @@ -458,7 +459,8 @@ def __init__(self, feature_values, control_signals, specified_terms): assert False, "PROGRAM ERROR: unrecognized specification for {} arg of {}: {}".\ format(repr(CONTROL_SIGNALS), self.name, c) else: - port_spec_dict = _parse_port_spec(port_type=ControlSignal, owner=self, port_spec=c) + port_spec_dict = _parse_port_spec(port_type=ControlSignal, owner=self, port_spec=c, + context=Context(string='RegressionCFA.__init__')) v = port_spec_dict[VARIABLE] v = v or ControlSignal.defaults.variable control_allocation.append(v) diff --git a/requirements.txt b/requirements.txt index d75bb1d1d23..2e851cc7649 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ autograd<1.7 -beartype<0.15.0 -dill<0.3.7 +beartype<0.16.0 +dill<0.3.8 fastkde>=1.0.24, <1.0.27 graph-scheduler>=0.2.0, <1.1.3 graphviz<0.21.0 diff --git a/tests/composition/test_composition.py b/tests/composition/test_composition.py index 441b4bcb0c5..ac24a054bbc 100644 --- a/tests/composition/test_composition.py +++ b/tests/composition/test_composition.py @@ -5196,14 +5196,14 @@ def test_import_composition(self, comp_mode): c.import_composition(em, get_input_from={i1:em.key_input_nodes[0], i2:em.value_input_nodes[0]}, - send_output_to={em.retrieval_nodes[0]:o1, - em.retrieval_nodes[1]:o2}) + send_output_to={em.retrieved_nodes[0]:o1, + em.retrieved_nodes[1]:o2}) assert all(node in c.nodes for node in em.nodes) assert i1 in [proj.sender.owner for proj in em.key_input_nodes[0].path_afferents] assert i2 in [proj.sender.owner for proj in em.value_input_nodes[0].path_afferents] - assert o1 in [proj.receiver.owner for proj in em.retrieval_nodes[0].efferents] - assert o2 in [proj.receiver.owner for proj in em.retrieval_nodes[1].efferents] + assert o1 in [proj.receiver.owner for proj in em.retrieved_nodes[0].efferents] + assert o2 in [proj.receiver.owner for proj in em.retrieved_nodes[1].efferents] assert all(entry in c.excluded_node_roles for entry in em.excluded_node_roles) assert all(entry in c.required_node_roles for entry in em.required_node_roles) diff --git a/tests/composition/test_emcomposition.py b/tests/composition/test_emcomposition.py index eca65924b81..d40237657b3 100644 --- a/tests/composition/test_emcomposition.py +++ b/tests/composition/test_emcomposition.py @@ -98,23 +98,23 @@ def test_report_prefs(self): # NOTE: None => use default value (i.e., don't specify in constructor, rather than forcing None as value of arg) # ------------------ SPECS --------------------------------------------- ------- EXPECTED ------------------- # memory_template memory_fill field_wts cncat_ky nmlze sm_gain repeat #fields #keys #vals concat - (0, (2,3), None, None, None, None, None, False, 2, 1, 1, False,), - (0.1, (2,3), .1, None, None, None, None, False, 2, 1, 1, False,), - (0.2, (2,3), (0,.1), None, None, None, None, False, 2, 1, 1, False,), - (0.3, (4,2,3), .1, None, None, None, None, False, 2, 1, 1, False,), - (1, [[0,0],[0,0]], None, None, None, None, None, False, 2, 1, 1, False,), - (1.1, [[0,0],[0,0]], None, [1,1], None, None, None, False, 2, 2, 0, False,), - (2, [[0,0],[0,0],[0,0]], None, None, None, None, None, False, 3, 2, 1, False,), - (2.1, [[0,0],[0,0],[0,0]], None, None, None, None, 1.5, False, 3, 2, 1, False,), - (2.2, [[0,0],[0,0],[0,0]], None, None, None, None, CONTROL, False, 3, 2, 1, False,), - (3, [[0,0,0],[0,0]], None, None, None, None, None, False, 2, 1, 1, False,), - (4, [[0,0,0],[0],[0,0]], None, None, None, None, None, False, 3, 2, 1, False,), - (5, [[0,0],[0,0],[0,0]], None, 1, None, None, None, False, 3, 3, 0, False,), - (5.1, [[0,0],[0,0],[0,0]], None, 1, None, None, 0.1, False, 3, 3, 0, False,), - (5.2, [[0,0],[0,0],[0,0]], None, 1, None, None, CONTROL, False, 3, 3, 0, False,), - (6, [[0,0,0],[0],[0,0]], None, [1,1,1], False, None, None, False, 3, 3, 0, False,), - (7, [[0,0,0],[0],[0,0]], None, [1,1,1], True, None, None, False, 3, 3, 0, True,), - (7.1, [[0,0,0],[0],[0,0]], None, [1,1,1], True , False, None, False, 3, 3, 0, False,), + # (0, (2,3), None, None, None, None, None, False, 2, 1, 1, False,), + # (0.1, (2,3), .1, None, None, None, None, False, 2, 1, 1, False,), + # (0.2, (2,3), (0,.1), None, None, None, None, False, 2, 1, 1, False,), + # (0.3, (4,2,3), .1, None, None, None, None, False, 2, 1, 1, False,), + # (1, [[0,0],[0,0]], None, None, None, None, None, False, 2, 1, 1, False,), + # (1.1, [[0,0],[0,0]], None, [1,1], None, None, None, False, 2, 2, 0, False,), + # (2, [[0,0],[0,0],[0,0]], None, None, None, None, None, False, 3, 2, 1, False,), + # (2.1, [[0,0],[0,0],[0,0]], None, None, None, None, 1.5, False, 3, 2, 1, False,), + # (2.2, [[0,0],[0,0],[0,0]], None, None, None, None, CONTROL, False, 3, 2, 1, False,), + # (3, [[0,0,0],[0,0]], None, None, None, None, None, False, 2, 1, 1, False,), + # (4, [[0,0,0],[0],[0,0]], None, None, None, None, None, False, 3, 2, 1, False,), + # (5, [[0,0],[0,0],[0,0]], None, 1, None, None, None, False, 3, 3, 0, False,), + # (5.1, [[0,0],[0,0],[0,0]], None, 1, None, None, 0.1, False, 3, 3, 0, False,), + # (5.2, [[0,0],[0,0],[0,0]], None, 1, None, None, CONTROL, False, 3, 3, 0, False,), + # (6, [[0,0,0],[0],[0,0]], None, [1,1,1], False, None, None, False, 3, 3, 0, False,), + # (7, [[0,0,0],[0],[0,0]], None, [1,1,1], True, None, None, False, 3, 3, 0, True,), + # (7.1, [[0,0,0],[0],[0,0]], None, [1,1,1], True , False, None, False, 3, 3, 0, False,), (8, [[0,0],[0,0],[0,0]], None, [1,2,0], None, None, None, False, 3, 2, 1, False,), (8.1, [[0,0],[0,0],[0,0]], None, [1,2,0], True, None, None, False, 3, 2, 1, False,), (9, [[0,1],[0,0],[0,0]], None, [1,2,0], None, None, None, [0,1], 3, 2, 1, False,), @@ -127,9 +127,9 @@ def test_report_prefs(self): [[0,0],[0,0,0],[0,0]]], .1, [1,1,0], None, None, None, 2, 3, 2, 1, False,), (12.2, [[[0,0],[0,0,0],[0,0]], # two entries specified, first has 0's [[0,2],[0,0,0],[0,0]]], .1, [1,1,0], None, None, None, 2, 3, 2, 1, False,), - (12.3, [[[0,1],[0,0,0],[0,0]], # two entries specified, fields have same weights + (12.3, [[[0,1],[0,0,0],[0,0]], # two entries specified, fields have same weights, but concatenate is False [[0,2],[0,0,0],[0,0]]], .1, [1,1,0], None, None, None, 2, 3, 2, 1, False), - (13, [[[0,1],[0,0,0],[0,0]], # two entries specified, fields have same weights, but conccatenate_keys is False + (13, [[[0,1],[0,0,0],[0,0]], # two entries specified, fields have same weights, and concatenate_keys is True [[0,2],[0,0,0],[0,0]]], .1, [1,1,0], True, None, None, 2, 3, 2, 1, True), (14, [[[0,1],[0,0,0],[0,0]], # two entries specified, all fields are keys [[0,2],[0,0,0],[0,0]]], .1, [1,1,1], None, None, None, 2, 3, 3, 0, False), @@ -230,18 +230,18 @@ def test_structure(self, assert len(em.value_input_nodes) == num_values assert isinstance(em.concatenate_keys_node, Mechanism) == concatenate_node if em.concatenate_keys: - assert em.retrieval_gating_nodes == [] + assert em.retrieval_weighting_nodes == [] assert bool(softmax_gain in {None, CONTROL}) == bool(len(em.softmax_control_nodes)) else: if num_keys > 1: - assert len(em.retrieval_gating_nodes) == num_keys + assert len(em.retrieval_weighting_nodes) == num_keys else: - assert em.retrieval_gating_nodes == [] + assert em.retrieval_weighting_nodes == [] if softmax_gain in {None, CONTROL}: assert len(em.softmax_control_nodes) == num_keys else: assert em.softmax_control_nodes == [] - assert len(em.retrieval_nodes) == num_fields + assert len(em.retrieved_nodes) == num_fields def test_memory_fill(start, memory_fill): memory_fill = memory_fill or 0 @@ -273,19 +273,6 @@ def test_memory_fill(start, memory_fill): @pytest.mark.pytorch class TestExecution: - # TEST: - # 0: 3 entries that fill memory; no decay, one key, high softmax gain, no storage, inputs has only key (no value) - # 1: 3 entries that fill memory; no decay, one key, high softmax gain, no storage, inputs has key & value - # 2: same as 1 but different value (that should be ignored) - # 3: same as 2 but has extra entry filled with random values (which changes retrieval) - # 4: same as 3 but uses both fields as keys (no values) - # 5: same as 4 but no concatenation of keys (confirms that results are similar w/ and w/o concatenation) - # 6: same as 5, but different field_weights - # 7: store + no decay - # 8: store + default decay (should be AUTO - # 9: store + explicit AUTO decay - # 10: store + numerical decay - test_execution_data = [ # NOTE: None => use default value (i.e., don't specify in constructor, rather than forcing None as value of arg) # ---------------------------------------- SPECS ----------------------------------- ----- EXPECTED --------- @@ -397,20 +384,20 @@ class TestExecution: ids=[x[0] for x in test_execution_data]) @pytest.mark.composition @pytest.mark.benchmark - def test_execution(self, - comp_mode, - test_num, - memory_template, - memory_capacity, - memory_fill, - memory_decay_rate, - field_weights, - concatenate_keys, - normalize_memories, - softmax_gain, - storage_prob, - inputs, - expected_retrieval): + def test_simple_execution(self, + comp_mode, + test_num, + memory_template, + memory_capacity, + memory_fill, + memory_decay_rate, + field_weights, + concatenate_keys, + normalize_memories, + softmax_gain, + storage_prob, + inputs, + expected_retrieval): if comp_mode != pnl.ExecutionMode.Python: pytest.skip('Compilation not yet support for Composition.import.') @@ -449,10 +436,10 @@ def test_execution(self, np.testing.assert_allclose(retrieved, expected_retrieval) # Validate that sum of weighted softmax distributions in retrieval_weighting_node itself sums to 1 - np.testing.assert_allclose(np.sum(em.retrieval_weighting_node.value), 1.0, atol=1e-15) + np.testing.assert_allclose(np.sum(em.softmax_weighting_node.value), 1.0, atol=1e-15) # Validate that sum of its output ports also sums to 1 - np.testing.assert_allclose(np.sum([port.value for port in em.retrieval_weighting_node.output_ports]), + np.testing.assert_allclose(np.sum([port.value for port in em.softmax_weighting_node.output_ports]), 1.0, atol=1e-15) # Validate storage @@ -475,6 +462,45 @@ def test_execution(self, assert all(elem == memory_fill for elem in em.memory[-1]) + @pytest.mark.composition + @pytest.mark.benchmark + @pytest.mark.parametrize('concatenate', [True, False]) + @pytest.mark.parametrize('use_storage_node', [True, False]) + def test_multiple_trials_concatenation_and_storage_node(self,comp_mode, concatenate, use_storage_node): + + if comp_mode != pnl.ExecutionMode.Python: + pytest.skip('Compilation not yet support for Composition.import.') + + def temp(context): + memory = context.composition.parameters.memory.get(context) + assert True + + em = EMComposition(memory_template=(2,3), + field_weights=[1,1], + memory_capacity=4, + softmax_gain=100, + memory_fill=(0,.001), + concatenate_keys=concatenate, + use_storage_node=use_storage_node) + + inputs = [[[[1,2,3]],[[4,5,6]],[[10,20,30]],[[40,50,60]],[[100,200,300]],[[400,500,600]]], + [[[1,2,5]],[[4,5,8]],[[11,21,31]],[[41,51,61]],[[111,222,333]],[[444,555,666]]], + [[[1,2,10]],[[4,5,10]]],[[[51,52,53]],[[81,82,83]],[[777,888,999]],[[1111,2222,3333]]]] + + expected_memory = [[[0.15625, 0.3125, 0.46875], [0.171875, 0.328125, 0.484375]], + [[400., 500., 600.], [444., 555., 666.]], + [[2.5, 3.125, 3.75 ], [2.5625, 3.1875, 3.8125]], + [[25., 50., 75.], [27.75, 55.5, 83.25]]] + + input_nodes = em.key_input_nodes + em.value_input_nodes + inputs = {input_nodes[i]:inputs[i] for + i in range(len(input_nodes))} + em.run(inputs=inputs, + # call_after_trial=temp + ) + np.testing.assert_equal(em.memory, expected_memory) + + # ***************************************************************************************************************** # ************************************* FROM AutodiffComposition ************************************************ # ***************************************************************************************************************** diff --git a/tests/composition/test_parameterestimationcomposition.py b/tests/composition/test_parameterestimationcomposition.py index 167ce2de56c..0dcde7a5562 100644 --- a/tests/composition/test_parameterestimationcomposition.py +++ b/tests/composition/test_parameterestimationcomposition.py @@ -125,15 +125,15 @@ def test_pec_run_input_formats(inputs_dict, error_msg): @pytest.mark.parametrize( - "opt_method", + "opt_method, result", [ - "differential_evolution", - optuna.samplers.RandomSampler(), - optuna.samplers.CmaEsSampler(), + ("differential_evolution", [0.010363518438648106]), + (optuna.samplers.RandomSampler(), [0.01]), + (optuna.samplers.CmaEsSampler(), [0.01]), ], ids=["differential_evolultion", "optuna_random_sampler", "optuna_cmaes_sampler"], ) -def test_parameter_optimization_ddm(func_mode, opt_method): +def test_parameter_optimization_ddm(func_mode, opt_method, result): """Test parameter optimization of a DDM in integrator mode""" if func_mode == "Python": @@ -212,9 +212,7 @@ def reward_rate(sim_data): ret = pec.run(inputs={comp: trial_inputs}) - np.testing.assert_allclose( - pec.optimized_parameter_values, [0.010363518438648106], atol=1e-2 - ) + np.testing.assert_allclose(pec.optimized_parameter_values, result) # func_mode is a hacky wa to get properly marked; Python, LLVM, and CUDA @@ -229,7 +227,7 @@ def test_parameter_estimation_ddm_mle(func_mode): # High-level parameters the impact performance of the test num_trials = 50 time_step_size = 0.01 - num_estimates = 40000 + num_estimates = 1000 ddm_params = dict( starting_value=0.0, diff --git a/tests/ports/test_input_ports.py b/tests/ports/test_input_ports.py index f2d12d6b52e..897053c2f60 100644 --- a/tests/ports/test_input_ports.py +++ b/tests/ports/test_input_ports.py @@ -64,6 +64,32 @@ def test_combine_param_conflicting_fct_class_spec(self): assert "Specification of 'combine' argument (PRODUCT) conflicts with Function specified " \ "in 'function' argument (Linear) for InputPort" in str(error_text.value) + def test_combine_dict_spec(self): + t = pnl.TransferMechanism(input_ports={pnl.COMBINE: pnl.PRODUCT}) + assert t.input_port.function.operation == pnl.PRODUCT + + def test_equivalent_function_dict_spec(self): + t = pnl.TransferMechanism(input_ports={pnl.FUNCTION:pnl.LinearCombination(operation=pnl.PRODUCT)}) + assert t.input_port.function.operation == pnl.PRODUCT + + def test_combine_dict_spec_redundant_with_function(self): + with pytest.warns(UserWarning) as warnings: # Warn, since default_input is NOT set + t = pnl.TransferMechanism(input_ports={pnl.COMBINE:pnl.PRODUCT, + pnl.FUNCTION:pnl.LinearCombination(operation=pnl.PRODUCT)}) + assert any(w.message.args[0] == "Both COMBINE ('product') and FUNCTION (LinearCombination Function) " + "specifications found in InputPort specification dictionary for " + "'InputPort' of 'TransferMechanism-0'; no need to specify both." + for w in warnings) + assert t.input_port.function.operation == pnl.PRODUCT + + def test_combine_dict_spec_conflicts_with_function(self): + with pytest.raises(pnl.InputPortError) as error_text: + t = pnl.TransferMechanism(input_ports={pnl.COMBINE:pnl.PRODUCT, + pnl.FUNCTION:pnl.LinearCombination}) + assert "COMBINE entry (='product') of InputPort specification dictionary for 'InputPort' of " \ + "'TransferMechanism-0' conflicts with FUNCTION entry (LinearCombination Function); " \ + "remove one or the other." in str(error_text.value) + def test_single_projection_variable(self): a = pnl.TransferMechanism() b = pnl.TransferMechanism()