# Lecture 4B: Hebbian Synaptic Plasticity In ngc-learn, synaptic plasticity is a key concept at the forefront of its design in order to promote research into novel ideas and framings of how adaptation might occur at various time-scales in biomimetic systems such as those composed of neurons. One of the simplest and most useful ways that one may implement plasticity is through a scheme that embodies principles of Hebbian learning -- characterized by the well-known popularized phrase "neurons that fire together, wire together" [1][^1]. In the [evolving synapse lesson](../model_basics/evolving_synapses.md) under "Model Basics", we cover how one would construct a basic two-factor Hebbian rule for adjusting the values of a synapse connecting two `RateCell` cell components. For the purposes of this lesson, we will just point out a key points of the `HebbianSynapse`, found within `ngclearn.components.synapses.hebbian.hebbianSynapse`, as this synaptic component highlights a few programmatic elements unique to how synapses operate within ngc-learn's nodes-and-cables system. Specifically, we will zoom in on two particular code snippets from [evolving synapses tutorial](../model_basics/evolving_synapses.md), reproduced below: ```python Wab = HebbianSynapse( name="Wab", shape=(1, 1), eta=1., signVal=-1., wInit=("constant", 1., None), w_bound=0., key=subkeys[3] ) # wire output compartment (rate-coded output zF) of RateCell `a` to input compartment of HebbianSynapse `Wab` a.zF >> Wab.inputs # wire output compartment of HebbianSynapse `Wab` to input compartment (electrical current j) RateCell `b` Wab.outputs >> b.j # wire output compartment (rate-coded output zF) of RateCell `a` to presynaptic compartment of HebbianSynapse `Wab` a.zF >> Wab.pre # wire output compartment (rate-coded output zF) of RateCell `b` to postsynaptic compartment of HebbianSynapse `Wab` b.zF >> Wab.post ``` as well as (a bit later in the model construction code): ```python evolve_process = (MethodProcess() >> a.evolve) advance_process = (MethodProcess() >> a.advance_state) ``` Notice that beyond wiring component `a`'s values into the synapse `Wab`'s input compartment `Wab.inputs` (and wiring `Wab`'s output compartment `Wab.outputs` to node `b`), we also wired values to `Wab`'s pre-synaptic compartment `Wab.pre` and `Wab`'s post-synaptic compartment `Wab.post`. These compartments are specifically used in `Wab`'s `evolve` call and are not strictly required to be exactly the same as its input and output compartments. Note that, if one wanted `pre` and `post` to be exactly identical to `inputs` and `outputs`, one would simply need to write `Wab.inputs >> Wab.pre` and `Wab.outputs >> Wab.post` in place of the pre- and post-synaptic compartment calls above. The above snippets highlight two key aspects of functionality that a synapse entails as opposed to the cell components (like `RateCell` or `AdExCell`): 1. Synapse components contain additional compartments that specifically relate to their long-term evolution/adaptation, i.e., their "learning" ability, that might be directly the same as their input and output compartments; 2. The `evolve` command is specifically used by synapse components, as this command triggers a different set of dynamics that might be the same as those entailed by `advance_state` (meaning that a call to evolve for synapse components entails a possibly different time-scale of adaptation/evolution). Technically, all components in ngc-learn inherit the ability to run an `advance_state` as well as an `evolve`, but most only use `advance_state` with the notable exception of synapse components (which, as seen above, make use of both). This particular separation of time-scales with the `advance_state` and `evolve` are important for two reasons: 1. synapses might be updated at very specific time steps within a simulation as "events" rather than being run continuously as cell components typically are, i.e., in other words, a call to a synapse component's `evolve` might only happen a few times within a window of time (and we wish to save on calls to the computation underlying `evolve` that are not needed to speed up our simulations greatly); 2. a call to a synapse's `evolve` might involve statistics that are not available every time step (or are not meant to be visible every time step). A good modeling use-case that embodies the above two reasons is in predictive coding or sparse coding/dictionary learning models -- these kinds of models are often formulated from the perspective of expectation-maximization (EM) where the activity values of neuronal layers are evolved continuously over a window of time (the E-step) and then synaptic efficacies are adjusted based on the final state of the layer-wise activities at the end of this window (the M-step). One does not want to waste the call to an M-step after each E-step (since only the M-step at the end of several E-steps is of interest) and, furthermore, the inputs to the M-step (which would be the compartment values involved in the `evolve` call of a synapse component) involve values beyond just the exact input and output of the synapse itself (usually the M-step involves using the input to the synapse and the activity values of nearby error neurons). In the model museum, we see explicitly how the EM adaptation scheme of a predictive coding circuit is implemented in [the walkthrough](../../museum/pcn_discrim.md) and the corresponding Github code [PCN model](https://github.com/NACLab/ngc-museum/tree/main/exhibits/pc_discrim). A final note with respect to `evolve` and `advance_state` is that one does not have to use or implement `evolve` if one wants synaptic connection update to occur exactly within a synapse component's `advance_state` (say, in a custom component that the experimenter decides to write for their work); the only tricky part of implementing a change in synaptic efficacy directly inside of `advance_state` is that the designer would need to ensure that all statistics needed for changing the synaptic component's internal values are available in the synapse's compartments exactly each time the `advance_state` is called (at the exact same simulation time step that, for example, that forwarding of signals across a synapse occurs). This would possibly be appropriate for synapses that frame their change in synaptic efficacies in terms of differential equations that need to be clocked in the exact same way as the dynamics of the relevant neuronal cell components (though the same effect can be emulated with a pairing of calls to the component's `advance_state` and `evolve`). ## References [1] Donald, Hebb. "The organization of behavior." Wiley: New York (1949). [^1]: The actual statement made by Donald Hebb was: "When an axon of cell A is near enough to excite a cell B and repeatedly or persistently takes part in firing it,...A’s efficiency, as one of the cells firing B, is increased".