Skip to content

Commit

Permalink
-
Browse files Browse the repository at this point in the history
  • Loading branch information
jdcpni committed Aug 5, 2023
1 parent a47c10f commit 6b620f7
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,7 @@ def _function(self,
return self.convert_output_type(result)

@handle_external_context()
def derivative(self, input=None, output=None, context=None):
def derivative(self, input=None, output=None, covariates=None, context=None):
"""
derivative(input)
Expand All @@ -1435,22 +1435,29 @@ def derivative(self, input=None, output=None, context=None):
Arguments
---------
input : 1d or 2d np.array : default class_defaults.variable
output : 1d np.array : default class_defaults.variable[0]
value of the input to the Linear transform at which derivative is to be taken.
a single numeric array or multiple arrays being combined, and at which derivative is to be taken.
.. technical_note::
output arg is used for consistency with other derivatives used by BackPropagation, and is ignored.
covariates : 2d np.array : default class_defaults.variable[1:]
the input(s) to the LinearCombination function other than the one for which the derivative is being
computed; these are used to calculate the Jacobian of the LinearCombination function.
Returns
-------
Scale : number (if input is 1d) or array (if input is 2d)
"""
if covariates is None or self.operation == SUM:
jacobian = self._get_current_parameter_value(SCALE, context)
else:
jacobian = np.prod(np.vstack(covariates), axis=0) * self._get_current_parameter_value(SCALE, context)

# FIX: NEED TO DEAL WITH PRODUCT HERE WRT TO WHICH INPUT PORT IS BEING "DERIVED"
# return self._get_current_parameter_value(SCALE, context)
# return np.diag(np.eye(len(output)) * self._get_current_parameter_value(SCALE, context))
return np.eye(len(output)) * (output * self._get_current_parameter_value(SCALE, context))
return np.eye(len(output)) * (output * jacobian)

def _get_input_struct_type(self, ctx):
# FIXME: Workaround a special case of simple array.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2588,18 +2588,20 @@ def _function(self,
# * dA(input to function)/dW
# FIX: 8/1/23: ADD COVARIATES TO CALL
activation_output = self._get_current_parameter_value(ACTIVATION_OUTPUT, context)
dA_dW = self.activation_derivative_fct(input=None, output=activation_output, context=context)
if covariates is None:
dA_dW = self.activation_derivative_fct(input=None, output=activation_output, context=context)
else:
dA_dW = self.activation_derivative_fct(input=None, output=activation_output,
covariates=covariates, context=context)

# Chain rule to get the derivative of the error with respect to the weights
# MODIFIED 8/1/23 NEW:
if dA_dW.ndim == 1:
dE_dW = dE_dA * dA_dW # FIX: <- THIS WAS THE ORIGINAL CODE
dE_dW = dE_dA * dA_dW
elif dA_dW.ndim == 2:
dE_dW = np.matmul(dE_dA, dA_dW) # FIX: <- THIS HAD BEEN COMMENTED OUT
dE_dW = np.matmul(dE_dA, dA_dW)
else:
owner_str = f" of {self.owner.name}" if self.owner else ""
raise FunctionError(f"Dimensionality of dA_dW ({dA_dW.ndim}) for {self.name}{owner_str} is not 1 or 2.")
# MODIFIED 8/1/23 END

# Weight changes = delta rule (learning rate * activity * error)
weight_change_matrix = learning_rate * activation_input * dE_dW
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@
attribute, and are used to calculate the `learning_signal <LearningMechanism.learning_signal>` (see `below
<LearningMechanism_Function>`).
.. note::
The LearningMechanism has a separate InputPort for each covariate, however their
values are combined into a single item of the LearningMechanism's `variable <LearningMechanism.variable>`
that is provided to its `function <LearningMechanism.function>` at the fourth position.
The Mechanisms from the which the `value <InputPort.values>`\\s above are received are listed in the
LearningMechanism's `input_source <LearningMechanism.input_source>`, `output_source <LearningMechanism.output_source>`,
and `error_sources <LearningMechanism.error_sources>` attributes, respectively (see
Expand Down Expand Up @@ -1053,9 +1058,9 @@ def __init__(self,
**kwargs
):
# IMPLEMENTATION NOTE:
# assign to private attribute as self.error_sources is used as a property
# assign to private attribute as self.error_sources;
# private attribute is used for validation and in _instantiate_attribute_before_function;
# thereafter, self.error_sources contains actual error_sources
# thereafter, self.error_sources contains actual error_sources.
if error_sources:
error_sources = convert_to_list(error_sources)
self._error_sources = error_sources
Expand Down Expand Up @@ -1108,12 +1113,12 @@ def _parse_function_variable(self, variable, context=None):
function_variable[ACTIVATION_INPUT_INDEX] = variable[ACTIVATION_INPUT_INDEX]
function_variable[ACTIVATION_OUTPUT_INDEX] = variable[ACTIVATION_OUTPUT_INDEX]
function_variable[ERROR_SIGNAL_INDEX] = variable[ERROR_SIGNAL_INDEX]
# If There are any covariates, add them to function_variable
if len(variable) > COVARIATES_INDEX:
# Put all covariate values into a single array and place in function_variable[COVARIATES_INDEX]
for i in range(COVARIATES_INDEX, len(variable)):
covariates = np.append(covariates, np.zeros_like(variable[i]))
function_variable = np.append(function_variable, [covariates], axis=0)
# # If There are any covariates, add them to function_variable
# # FIX: 8/1/23 - COORDINATE THIS WITH NUMBER OF ERROR SIGNALS INPUTPORTS
# if len(variable) > COVARIATES_INDEX:
# # Put all covariate values into a single array and place in function_variable[COVARIATES_INDEX]
# covariates = [covariate for covariate in variable[COVARIATES_INDEX:]]
# function_variable = function_variable.tolist() + [covariates]
return function_variable

def _validate_variable(self, variable, context=None):
Expand Down Expand Up @@ -1156,8 +1161,7 @@ def _validate_variable(self, variable, context=None):
if not (is_numeric(variable[i])):
raise LearningMechanismError("{item_num_string} of variable for {self.name} ({item_name}: "
"{variable[i]}) is not numeric.")
elif i + len(variable[i]) != len(self.input_ports):
assert True
elif len(variable[i]) != len(self.input_ports):
assert False, f"Number of items ({len(variable)}) in variable for '{self.name}' doesn't match the " \
f"number of its InputPorts ({len(self.input_ports)})"
return variable
Expand Down Expand Up @@ -1221,6 +1225,19 @@ def _validate_params(self, request_set, target_set=None, context=None):
else:
pass


def _instantiate_input_ports(self, input_ports=None, reference_value=None, context=None):
"""Instantiate COVARIATES InputPorts"""
num_covariates = len(self.defaults.variable[COVARIATES_INDEX:])
input_ports = self.input_ports
if num_covariates == 1:
input_ports += [COVARIATES]
else:
for i in range(num_covariates):
input_ports += [f'{COVARIATES}_{i}']
# input_ports[i].reference_value = reference_value[i]
super()._instantiate_input_ports(input_ports=input_ports, reference_value=reference_value, context=context)

def _instantiate_attributes_before_function(self, function=None, context=None):
"""Instantiates MappingProjection(s) from error_sources (if specified) to LearningMechanism
Expand Down Expand Up @@ -1413,11 +1430,12 @@ def _execute(
if len(variable) == COVARIATES_INDEX + 1:
function_variable = np.append(function_variable, variable[COVARIATES_INDEX])

covariates = variable[COVARIATES_INDEX] if len(variable) == COVARIATES_INDEX + 1 else None
learning_signal, error_signal = super()._execute(variable=function_variable,
context=context,
error_matrix=error_matrix,
runtime_params=runtime_params,
)
covariates=covariates,
runtime_params=runtime_params)
# Sum learning_signals and error_signals
summed_learning_signal += learning_signal
summed_error_signal += error_signal
Expand Down
15 changes: 6 additions & 9 deletions psyneulink/core/compositions/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8330,8 +8330,8 @@ def _get_acts_in_and_out(input_source, output_source, learned_projection):
# activation_output is always a single value since activation function is assumed to have only one output
activation_output = [output_source.output_ports[0].value]
# FIX: CHECK FOR USE OF PRODUCT BY ACTIVATION FUNCTION OF output_source
covariates = [[input_port.value for input_port in output_source.input_ports
if input_port is not learned_projection.receiver]]
covariates = [input_port.value for input_port in output_source.input_ports
if input_port is not learned_projection.receiver]
# FIX 8/1/23: ADD ALL OTHER INPUTS TO
# OTHER THAN
# ACTIVATION_INPUT
Expand Down Expand Up @@ -8371,14 +8371,11 @@ def _get_acts_in_and_out(input_source, output_source, learned_projection):
learning_rate=learning_rate)

# Use all error_signal_templates since LearningMechanisms handles all sources of error
default_variable = activation_input + activation_output + error_signal_template
default_variable.extend(covariates)
learning_mechanism = LearningMechanism(function=learning_function,
# default_variable=activation_input +
# activation_output +
# error_signal_template +
# covariates,
default_variable=default_variable,
default_variable=activation_input +
activation_output +
error_signal_template +
covariates,
error_sources=error_sources,
learning_enabled=learning_update,
in_composition=True,
Expand Down

0 comments on commit 6b620f7

Please sign in to comment.