Skip to content

Commit

Permalink
Feat/learn nested direct (#2812)
Browse files Browse the repository at this point in the history
* • composition.py
  - create_cim_ports(): 
    - assign as internal_only input_ports on the input_CIM of a nested Composition that receive projections from Nodes in the enclosing Composition
    - prune any errant projections to input_CIM
    - modify check for errant afferents to input_CIM to only include projections in current Composition
  - add_linear_processing_pathway:
     - don't implement MappingProjection for ControlMechanism -> next node (and warn about this)
  - _determine_node_roles:
    - assign as INPUT Node any Mechanism with no path_afferents (other than ControlMechansisms)
    - assign as INPUT Node any Mechanism with no path_afferents (other than ControlMechansisms or from input_CIM)
      (this prevents assignment of ModulatoryProjection from masking role as INPUT Node)
  - add_linear_processing_pathway():
      - use graph.connect_components to assign dependence of
        receiver on sender before ControlMechanism in pathway
  - _check_nested_target_mechs():
     implement, so can be overridden in autodiffcomposition.py
  - add graph edge between control mechanisms in pathway

• compositioninterfacemechanism.py
  - add_ports:  make adding from command line an error

• showgraph.py
  - fix bug in representation of ControlProjections vs. MappingProjections
  - assign standard arrow/color to errant MappingProjections from ControlMechanism (to expose them; were previously assigned attributes of ControlProjection)
  - remove checks for MappingProjection in representation of ControlMechanism efferents (since sometimes, e.g. for projection to parameter_CIM of nested Composition for iconfied nested comp) the should be shown as ControlProjections



• autodiffcomposition.py

• test_force_two_control_mechanisms_as_OUTPUT():
    reinstate assertion that ctl_mech_B is TERMINAL

• test_autodiffcomposition.py
  - test_direct_input_to_nested(): modify to test for successful execution

* [skip ci]

• composition.py:
  - add_linear_processing_pathway():
      - use graph.connect_components to assign dependence of
        receiver on sender before ControlMechanism in pathway
  - _check_nested_target_mechs():
     implement, so can be overridden in autodiffcomposition.py
  - _get_input_receivers():
    restrict to Nodes that have direct inputs from
    outermost Composition (i.e., the "environment")

• compositioninterfacemechanism.py
  - _get_source_node_for_input_CIM(): return None if source is input to outermost Composition (i.e., the "environment")

• autodiffcomposition.py
  - __init__(): deal with assignment of _is_input to pytorch_nodes
  - _check_nested_target_mechs(): override; not needed since comps with nested comps are flattened



• autodiffcomposition.py
• composition.py
  - add_linear_processing_pathway:
    - if ControlMechanism is first in a pathway, add warning that one after it is now the first
    - if ControlMechanism is not the first, add MappingProjection from its sender to its receiver and warn
  - _determine_node_roles: refactor to deal with OUTPUT CYCLE
  - _check_for_unused_projections: make warning depend on verbosePref==True
  - additions and mods to source and destination tracking methods.
  - support direct and indirect pathways to/from nested Compositions
  - bug fixes for validation of input_dict shapes for nested comps

• autodiffcomposition.py
  - __init__(): deal with assignment of _is_input to pytorch_nodes
  - learn():
    - error for learning with non-Autodiff composition
    - error for learning with nested in Python mode
    - additions and mods to source and destination tracking methods.
    - support direct and indirect pathways to/from nested Compositions
  - _parse_learning_spec(): override to support inputs dict for flatted Composition
  - _check_nested_target_mechs(): override; not needed since comps with nested comps are flattened
  - _parse_learning_spec():
    - override to support inputs dict for flatted Composition
  - docstring mods

• test_autodiff_composition.py
  add test_error_for_running_nested_learning_in_Python_mode
  - add test_one_time_warning_for_run_with_no_inputs
  - add test_no_learning_of_spanning_nested_compositions
  - add test_nested_autodiff_learning_with_input_func
  - some consolidation in _get_pytorchbackprop_pathway()
  - test_asymmetric_inputs_to_nested_one_direct_one_via_node_in_outer_comp:  PASSES
  - test_direct_input_to_nested(): modify to test for successful execution

• pathway.py
  update docstring re: handling of ControlMechanism


---------

Co-authored-by: jdcpni <pniintel55>
Co-authored-by: Katherine Mantel <[email protected]>
  • Loading branch information
jdcpni and kmantel authored Oct 11, 2023
1 parent ca2a82e commit c24135b
Show file tree
Hide file tree
Showing 23 changed files with 1,227 additions and 517 deletions.
16 changes: 8 additions & 8 deletions psyneulink/core/components/mechanisms/mechanism.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@
associated with an instantiated OutputPort at the time the Mechanism is executed are ignored.
The `PathwayProjections <PathwayProjection>` (e.g., `MappingProjections <MappingProjection>`) it receives are listed
in its `path_afferents <Port.path_afferents>` attribute. If the Mechanism is an `ORIGIN` Mechanism of a
in its `path_afferents <Port_Base.path_afferents>` attribute. If the Mechanism is an `ORIGIN` Mechanism of a
`Composition`, this includes a Projection from the Composition's `input_CIM <Composition.input_CIM>`. Any
`ControlProjections <ControlProjection>` or `GatingProjections <GatingProjection>` it receives are listed in its
`mod_afferents <Port.mod_afferents>` attribute.
Expand Down Expand Up @@ -1412,20 +1412,20 @@ class Mechanism_Base(Mechanism):
projections : ContentAddressableList
a list of all of the Mechanism's `Projections <Projection>`, composed from the
`path_afferents <Port.path_afferents>` of all of its `input_ports <Mechanism_Base.input_ports>`,
`path_afferents <Port_Base.path_afferents>` of all of its `input_ports <Mechanism_Base.input_ports>`,
the `mod_afferents` of all of its `input_ports <Mechanism_Base.input_ports>`,
`parameter_ports <Mechanism)Base.parameter_ports>`, and `output_ports <Mechanism_Base.output_ports>`,
and the `efferents <Port.efferents>` of all of its `output_ports <Mechanism_Base.output_ports>`.
and the `efferents <Port_Base.efferents>` of all of its `output_ports <Mechanism_Base.output_ports>`.
afferents : ContentAddressableList
a list of all of the Mechanism's afferent `Projections <Projection>`, composed from the
`path_afferents <Port.path_afferents>` of all of its `input_ports <Mechanism_Base.input_ports>`,
`path_afferents <Port_Base.path_afferents>` of all of its `input_ports <Mechanism_Base.input_ports>`,
and the `mod_afferents` of all of its `input_ports <Mechanism_Base.input_ports>`,
`parameter_ports <Mechanism)Base.parameter_ports>`, and `output_ports <Mechanism_Base.output_ports>`.,
path_afferents : ContentAddressableList
a list of all of the Mechanism's afferent `PathwayProjections <PathwayProjection>`, composed from the
`path_afferents <Port.path_afferents>` attributes of all of its `input_ports
`path_afferents <Port_Base.path_afferents>` attributes of all of its `input_ports
<Mechanism_Base.input_ports>`.
mod_afferents : ContentAddressableList
Expand All @@ -1435,7 +1435,7 @@ class Mechanism_Base(Mechanism):
efferents : ContentAddressableList
a list of all of the Mechanism's efferent `Projections <Projection>`, composed from the `efferents
<Port.efferents>` attributes of all of its `output_ports <Mechanism_Base.output_ports>`.
<Port_Base.efferents>` attributes of all of its `output_ports <Mechanism_Base.output_ports>`.
senders : ContentAddressableList
a list of all of the Mechanisms that send `Projections <Projection>` to the Mechanism (i.e., the senders of
Expand Down Expand Up @@ -2646,8 +2646,8 @@ def _get_variable_from_input(self, input, context=None):
else:
input_port.parameters.value._set(value, context)
else:
raise MechanismError(f"Length ({len(input_item)}) of input ({input_item}) does not match "
f"required length ({input_port.default_input_shape.size}) for input "
raise MechanismError(f"Shape ({input_item.shape}) of input ({input_item}) does not match "
f"required shape ({input_port.default_input_shape.shape}) for input "
f"to {InputPort.__name__} {repr(input_port.name)} of {self.name}.")

# Return values of input_ports for use as variable of Mechanism
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
that receives its input from a ComparatorMechanism. If the `primary_learned_projection` is part of a `multilayer
learning pathway <LearningMechanism_Multilayer_Learning>`, then the LearningMechanism will have one or more
*ERROR_SIGNAL* InputPorts, that receive their input from the next LearningMechanism(s) in the sequence; that is,
the one(s) associated with the `efferents <Port.efferents>` (outgoing Projections) of its `output_source`,
the one(s) associated with the `efferents <Port_Base.efferents>` (outgoing Projections) of its `output_source`,
with one *ERROR_SIGNAL* InputPort for each of those Projections. The `value <InputPort.value>`\\s of the
*ERROR_SIGNAL* InputPorts are summed by the LearningMechanism's `function <LearningMechanism.function>` to
calculate the `learning_signal <LearningMechanism.learning_signal>` (see `below <LearningMechanism_Function>`);
Expand Down Expand Up @@ -349,7 +349,7 @@
..
* `error_matrices` - the `matrix <MappingProjection.matrix>` parameters of the Projections associated with the
`error_sources <LearningMechanism.error_sources>`; that is, of any of the `output_source
<LearningMechanism.output_source>`'s `efferents <OutputPorts.efferents>` that are also being learned.
<LearningMechanism.output_source>`'s `efferents <Port_Base.efferents>` that are also being learned.
..
* `covariates_sources` - the `InputPort`s of `Mechanism`(s) that provide covariates used in calculating the derivative
of the `output_source <LearningMechanism.output_source>`'s `function <Mechanism_Base.function>` (see `above
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,17 @@ def __init__(self,
def add_ports(self, ports, context=None):
ports = super(CompositionInterfaceMechanism, self).add_ports(ports, context=context)
if context.source == ContextFlags.COMMAND_LINE:
warnings.warn(
'You are attempting to add custom ports to a CIM, which can result in unpredictable behavior and '
'is therefore recommended against. If suitable, you should instead add ports to the mechanism(s) '
'that project to or are projected to from the CIM.')
if ports[INPUT_PORTS]:
self.user_added_ports[INPUT_PORTS].update([port for port in ports[INPUT_PORTS].data])
if ports[OUTPUT_PORTS]:
self.user_added_ports[OUTPUT_PORTS].update([port for port in ports[OUTPUT_PORTS].data])
# warnings.warn(
# 'You are attempting to add custom ports to a CIM, which can result in unpredictable behavior and '
# 'is therefore recommended against. If suitable, you should instead add ports to the mechanism(s) '
# 'that project to or are projected to from the CIM.')
# if ports[INPUT_PORTS]:
# self.user_added_ports[INPUT_PORTS].update([port for port in ports[INPUT_PORTS].data])
# if ports[OUTPUT_PORTS]:
# self.user_added_ports[OUTPUT_PORTS].update([port for port in ports[OUTPUT_PORTS].data])
from psyneulink.core.compositions.composition import CompositionError
raise CompositionError(f"Adding ports to a {self.__class__.__name__} is not supported at this time; "
f"these are handled automatically when a Composition is created.")
return ports

@handle_external_context()
Expand All @@ -236,8 +239,9 @@ def remove_ports(self, ports, context=None):
self.user_added_ports[OUTPUT_PORTS] = self.user_added_ports[OUTPUT_PORTS] - output_ports_marked_for_deletion

# def _get_source_node_for_input_CIM(self, port, start_comp=None, end_comp=None):
def _get_source_node_for_input_CIM(self, port, comp=None):
def _get_source_node_for_input_CIM(self, port, start_comp=None)->tuple or None:
"""Return Port, Node and Composition for source of projection to input_CIM from (possibly nested) outer comp
Return None if there is no source Node (i.e., port receives input from environment)
**port** InputPort or OutputPort of the input_CIM from which the local projection projects;
**comp** Composition at which to begin the search (or continue it when called recursively;
assumes the current CompositionInterfaceMechanism's Composition by default
Expand All @@ -252,9 +256,8 @@ def _get_source_node_for_input_CIM(self, port, comp=None):
input_port = [port_map[k][0] for k in port_map if port_map[k][idx] is port]
assert len(input_port)==1, f"PROGRAM ERROR: Expected exactly 1 input_port for {port.name} " \
f"in port_map for {port.owner}; found {len(input_port)}."
assert len(input_port[0].path_afferents)==1, f"PROGRAM ERROR: Port ({input_port.name}) expected to have " \
f"just one path_afferent; has {len(input_port.path_afferents)}."

if not len(input_port[0].path_afferents):
return None
sender = input_port[0].path_afferents[0].sender
if not isinstance(sender.owner, CompositionInterfaceMechanism):
return sender, sender.owner, comp
Expand Down Expand Up @@ -333,7 +336,7 @@ def _get_source_of_modulation_for_parameter_CIM(self, port, comp=None):
return sender, sender.owner, comp
return self._get_source_of_modulation_for_parameter_CIM(sender, sender.owner.composition)

def _get_source_info_from_output_CIM(self, port, comp=None):
def _get_source_info_from_output_CIM(self, port, comp=None)->tuple:
"""Return Port, Node and Composition for "original" source of projection from **port**.
**port** InputPort or OutputPort of the output_CIM from which the projection of interest projects;
used to find source (key) in output_CIM's port_map.
Expand All @@ -358,15 +361,16 @@ def _get_source_info_from_output_CIM(self, port, comp=None):
return sender, sender.owner, comp
return self._get_source_info_from_output_CIM(sender, sender.owner.composition)

def _get_destination_info_for_output_CIM(self, port, comp=None)-> list:
def _get_destination_info_for_output_CIM(self, port, comp=None)-> list or None:
"""Return Port, Node and Composition for "ultimate" destination(s) of projection to **port**.
**port**: InputPort or OutputPort of the output_CIM to which the projection of interest projects;
used to find source (key=SENDER PORT) of the projection to the output_CIM.
**comp**: Composition at which to begin the search (or continue it when called recursively);
assumes the Composition for the output_CIM to which **port** belongs by default
If there is more than one destination, return list of tuples, one for each destination;
this occurs if the source of the projection to the output_CIM (SENDER PORT) is a Node in a nested Composition
that is specified to project to more than one Node in the outer Composition
Return list of tuples, one for each destination, if there is more than one destination;
this occurs if the source of the projection to the output_CIM (SENDER PORT) is a Node in a nested Composition
that is specified to project to more than one Node in the outer Composition
Return None if there are no destination Nodes (i.e., source is output of outermost Composition
"""
from psyneulink.core.compositions.composition import get_composition_for_node

Expand All @@ -393,7 +397,7 @@ def _get_destination_info_for_output_CIM(self, port, comp=None)-> list:
else:
receivers_info.append(self._get_destination_info_for_output_CIM(efferent.receiver,
efferent.receiver.owner.composition))
return receivers_info
return receivers_info if any(receivers_info) else None

def _sender_is_probe(self, output_port):
"""Return True if source of output_port is a PROBE Node of the Composition to which it belongs"""
Expand Down
10 changes: 5 additions & 5 deletions psyneulink/core/components/ports/outputport.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
of a `DDM` Mechanism generates several results (such as decision accuracy and response time), each of which can be
assigned as the `value <OutputPort.value>` of a different OutputPort. The OutputPort(s) of a Mechanism can serve
as the input to other Mechanisms (by way of `projections <Projections>`), or as the output of a Process and/or
System. The OutputPort's `efferents <Port.efferents>` attribute lists all of its outgoing
System. The OutputPort's `efferents <Port_Base.efferents>` attribute lists all of its outgoing
projections.
.. _OutputPort_Creation:
Expand Down Expand Up @@ -249,7 +249,7 @@
When an OutputPort is created, it can be assigned one or more `Projections <Projection>`, using either the
**projections** argument of its constructor, or in an entry of a dictionary assigned to the **params** argument with
the key *PROJECTIONS*. An OutputPort can be assigned either `MappingProjection(s) <MappingProjection>` or
`GatingProjection(s) <GatingProjection>`. MappingProjections are assigned to its `efferents <Port.efferents>`
`GatingProjection(s) <GatingProjection>`. MappingProjections are assigned to its `efferents <Port_Base.efferents>`
attribute and GatingProjections to its `mod_afferents <OutputPort.mod_afferents>` attribute. See
`Port Projections <Port_Projections>` for additional details concerning the specification of Projections when
creating a Port.
Expand All @@ -259,7 +259,7 @@
An OutputPort can also be specified by specifying one or more Components to or from which it should be assigned
Projection(s). Specifying an OutputPort in this way creates both the OutputPort and any of the specified or
implied Projection(s) (if they don't already exist). `MappingProjections <MappingProjection>`
are assigned to the OutputPort's `efferents <Port.efferents>` attribute, while `ControlProjections
are assigned to the OutputPort's `efferents <Port_Base.efferents>` attribute, while `ControlProjections
<ControlProjection>` and `GatingProjections <GatingProjection>` are assigned to its `mod_afferents
<Port.mod_afferents>` attribute. Any of the following can be used to specify an InputPort by the Components that
projection to it (see `below <OutputPort_Compatibility_and_Constraints>` for an explanation of the relationship
Expand Down Expand Up @@ -576,7 +576,7 @@
.. _OutputPort_Efferent_Projections:
* `efferents <Port.efferents>` -- `MappingProjections <MappingProjection>` that project from the OutputPort.
* `efferents <Port_Base.efferents>` -- `MappingProjections <MappingProjection>` that project from the OutputPort.
.. _OutputPort_Modulatory_Projections:
Expand Down Expand Up @@ -814,7 +814,7 @@ class OutputPort(Port_Base):
projections : list of Projection specifications
specifies the `MappingProjection(s) <MappingProjection>` to be sent by the OutputPort, and/or
`ControlProjections <ControlProjection>` and/or `GatingProjections(s) <GatingProjection>` to be received (see
`OutputPort_Projections` for additional details); these are listed in its `efferents <Port.efferents>` and
`OutputPort_Projections` for additional details); these are listed in its `efferents <Port_Base.efferents>` and
`mod_afferents <Port.mod_afferents>` attributes, respectively (see `OutputPort_Projections` for additional
details).
Expand Down
35 changes: 21 additions & 14 deletions psyneulink/core/components/projections/pathway/mappingprojection.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
* in the **matrix** argument of the MappingProjection's constructor, using the `tuple format
<MappingProjection_Tuple_Specification>` described above;
..
* specifying a MappingProjection in the `learning method <Composition_Learning_Methods>` of a `Composition`,
using the `tuple format <MappingProjection_Learning_Tuple_Specification>` to include a learning specification;
* specifying the MappingProjection (or its *MATRIX* `ParameterPort`) as the `receiver
<LearningProjection.receiver>` of a `LearningProjection`;
..
Expand All @@ -125,17 +128,15 @@
..
* specifying the MappingProjection (or its *MATRIX* `ParameterPort`) in the **learning_signals** argument of
the constructor for a `LearningMechanism <LearningSignal_Specification>`
..
* specifying a MappingProjection in the `pathway <Process.pathway>` for a `Process`,
using the `tuple format <MappingProjection_Learning_Tuple_Specification>` to include a learning specification;
..
* `specifying learning <Process_Learning_Sequence>` for a `Process`, which assigns `LearningProjections
<LearningProjection>` to all of the MappingProjections in the Process' `pathway <Process.pathway>`;
See `LearningMechanism` documentation for an overview of `learning components <LearningMechanism_Overview>` and a
detailed description of `LearningMechanism_Learning_Configurations`; see `MappingProjection_Learning` below for a
description of how learning modifies a MappingProjection.
Learning can be disabled for a MappingProjection by specifying its **learnable** argument as `False` in its
constructor. This can be useful for disabling specific MappingProjections in the `learning pathway
`learning pathway <Composition_Learning_Pathway>` of a `Composition`.
See `LearningMechanism` for an overview of `learning components <LearningMechanism_Overview>` and a detailed
description of `LearningMechanism_Learning_Configurations`; `Composition_Learning` for a description of how learning
is implemented within a `Composition`; `MappingProjection_Learning` below for a description of how learning
modifies a MappingProjection.
.. _MappingProjection_Learning_Tuple_Specification:
Expand All @@ -155,10 +156,10 @@
Specifying Learning for AutodiffCompositions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, all MappingProjections in an `AutodiffComposition` are treated as trainable PyTorch Parameters whose
matrices are updated during backwards passes through the network. Optionally, users can specify during
instantiation that a projection should not be updated. To do so, set the `learnable` argument to False in the
constructor of the projection.
By default, all MappingProjections in an `AutodiffComposition` are treated as trainable PyTorch parameters that
are updated during backwards passes through the network, and then used to update the MappingProjection's
`matrix <MappingProjection.matrix>` after each batch of train. However, this can be disabled for an individual
MappingProjection by specifying the **learnable** argument as False in its constructor.
.. _MappingProjection_Deferred_Initialization:
Expand Down Expand Up @@ -334,7 +335,8 @@ class MappingProjection(PathwayProjection_Base):
MappingProjection( \
sender=None, \
receiver=None, \
matrix=DEFAULT_MATRIX)
matrix=DEFAULT_MATRIX, \
learnable) \
Subclass of `Projection` that transmits the `value <OutputPort.value>` of the `OutputPort` of one `Mechanism
<Mechanism>` to the `InputPort` of another (or possibly itself). See `Projection <Projection_Class_Reference>`
Expand All @@ -361,6 +363,11 @@ class MappingProjection(PathwayProjection_Base):
for the `variable <InputPort.variable>` of its `receiver <MappingProjection.receiver>` `InputPort`
(see `MappingProjection_Matrix_Specification` for additional details).
learnable : bool : default True
specifies whether the MappingProjection's `matrix <MappingProjection.matrix>` parameter can be modified by
`learning <LearningMechanism>` (see `MappingProjection_Learning` for additional details).
Attributes
----------
Expand Down
Loading

0 comments on commit c24135b

Please sign in to comment.