Auryn simulator

Simulator for spiking neural networks with synaptic plasticity

User Tools

Site Tools


tutorials:writing_your_own_plasticity_model

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
tutorials:writing_your_own_plasticity_model [2015/01/23 21:12] – [Weight updates in continuous time (evolve)] zenketutorials:writing_your_own_plasticity_model [2018/02/07 23:11] (current) – Adds final paragraph on unit tests zenke
Line 1: Line 1:
-**Under construction!** Please note, I only have started writing this tutorial. 
- 
- 
 ====== Synapse and Plasticity Models ====== ====== Synapse and Plasticity Models ======
  
 Auryn was written with the aim to simplify the development of spike-timing-dependent plasticity (STDP) models and to run them efficiently in distributed network simulations. Behind the scenes Auryn diverts from existing standard simulators (like NEST) in that it has the inbuilt functionality to back-propagate action potentials to the presynaptic neurons. This largely simplifies implementing plasticity models which can be written as the product of "some synaptic quantities" and the pre- or postsynaptic spike train. For instance, most standard STDP models fall into this category. In particular the synaptic weight only changes upon the arrival of pre- or postsynaptic spikes. If you do not know what any of this means, it might be good to have a look at the following papers: Auryn was written with the aim to simplify the development of spike-timing-dependent plasticity (STDP) models and to run them efficiently in distributed network simulations. Behind the scenes Auryn diverts from existing standard simulators (like NEST) in that it has the inbuilt functionality to back-propagate action potentials to the presynaptic neurons. This largely simplifies implementing plasticity models which can be written as the product of "some synaptic quantities" and the pre- or postsynaptic spike train. For instance, most standard STDP models fall into this category. In particular the synaptic weight only changes upon the arrival of pre- or postsynaptic spikes. If you do not know what any of this means, it might be good to have a look at the following papers:
  
-  * Gerstner, W., and Kistler, W.M. (2002). Mathematical formulations of Hebbian learning. Biol Cybern 87, 404–415. [[http://link.springer.com/article/10.1007/s00422-002-0353-y|url]] +  * Gerstner, W., and Kistler, W.M. (2002). Mathematical formulations of Hebbian learning. Biol Cybern 87, 404–415. [[http://link.springer.com/article/10.1007/s00422-002-0353-y|full text]] 
-  * Zenke, F., and Gerstner, W. (2014). Limits to high-speed simulations of spiking neural networks using general-purpose computers. Front Neuroinform 8, 76. [[http://journal.frontiersin.org/Journal/10.3389/fninf.2014.00076/abstract|url]]+  * Zenke, F., and Gerstner, W. (2014). Limits to high-speed simulations of spiking neural networks using general-purpose computers. Front Neuroinform 8, 76. [[http://journal.frontiersin.org/Journal/10.3389/fninf.2014.00076/abstract|full text]]
  
-If you can write down a learning rule as a differential equation involving spike trains, synaptic traces and specific postsynaptic quantities, such as the membrane potential, Auryn will bring everything need to so so intuitively. Here is an example from Gerstner and Kistler (2002):+If you can write down a learning rule as a differential equation involving spike trains, synaptic traces and specific postsynaptic quantities, such as the membrane potential, Auryn will bring everything you need to implement this learning rule intuitively. Here is an example from Gerstner and Kistler (2002):
  
 {{ :tutorials:stdpgeneral.png |}} {{ :tutorials:stdpgeneral.png |}}
  
-In Auryn you can implement this type of learning rule very intuitively if the ''u'' can be written as a function of synaptic traces and postsynaptic quantities (for many standard cases the ''u'' are synaptic traces themselves). To that end, Auryn has native support for such pre- or postsynaptic traces. +In Auryn you can implement this type of learning rule if the ''u'' can be written as a function of synaptic traces and postsynaptic quantities (for many standard cases the ''u'' are synaptic traces themselves). To that end, Auryn has native support for such pre- or postsynaptic traces.  For historical reasons, I typically use the letter ''z'' for synaptic traces, which is what you will find below.
  
  
Line 20: Line 17:
 ===== Understanding plasticity in Auryn ===== ===== Understanding plasticity in Auryn =====
  
-Before writing your own code, it might be a good idea to have a look at some existing codeA good example to start off with is the event-based implementation for [[examples:sim_background|Triplet STDP]].  +In most cases you will want to use Auryn to implement your own synapse modelThe easiest to do this is by understanding and modifying an existing model. Most of the plasticity models in Auryn are implemented in source files which contain the acronym STDP, e.g. [[manual:STDPConnection]], [[manual:STDPwdConnection]], [[manual:SymmetricSTDPConnection]] or "Triplet" in the case of [[manual:TripletConnection]] which is used in this [[examples:sim_background|example]]. You will find them in the ''src/auryn'' directoryor take a look at the [[http://fzenke.net/auryn/doxygen/current/annotated.html|class index]]. All the classes implement [[manual:SparseConnection]] objects with plasticity on top. They already implement all the purely virtual functions of the base class [[manual:Connection]] and specific functions to implement plasticity. In the following, I will explain how to understand these classes and how to easily modify them according to your needs. In particular, I will cover how to define your own synaptic traces and how to use them to define weight updates. Finally, I will illustrate how synaptic weights can be integrate in a time continuous manner, for instance to implement a weight decay, homeostatic scaling or other continuous processes.
- +
-In most cases it will be best to writing your own synapse modelby modifying an existing model (like [[examples:sim_background|Triplet STDP]]) which already implements all the purely virtual functions of the base class [[manual:Connection]]. In the following, I will explain how to do this. In particular, I will cover how to define your own synaptic traces and how to use them to define weight updates. Finally, I will illustrate how synaptic weights can be integrate in a time continuous manner, for instance to implement a weight decay, homeostatic scaling or other continuous processes.+
  
  
Line 40: Line 35:
 DEFAULT_TRACE_MODEL * tr_post_hom; DEFAULT_TRACE_MODEL * tr_post_hom;
 </code> </code>
-The macros ''DEFAULT_TRACE_MODEL'' and ''PRE_TRACE_MODEL'' are defined in ''auryn_definitions.h'' to facilitate the change of the underlying integration scheme. Per default they refer to [[manual:EulerTrace]], because Euler integration is most efficient in most situations (see Zenke and Gerstner (2014) for details).+The macros ''DEFAULT_TRACE_MODEL'' and ''PRE_TRACE_MODEL'' are defined in ''auryn_definitions.h'' to facilitate the change of the underlying integration scheme. Per default they refer to [[manual:EulerTrace]], because Euler integration is most efficient in most situations (see [[http://journal.frontiersin.org/article/10.3389/fninf.2014.00076/abstract|Zenke and Gerstner (2014)]] for details).
  
-This declaration should be matched in the ''.cpp'' file by+This declaration should be matched in the ''.cpp'' file. Typically there should be an init function in your plastic connection class in which you can write:
 <code c++> <code c++>
 /* Initialization of presynaptic traces */ /* Initialization of presynaptic traces */
Line 52: Line 47:
 tr_post_hom = dst->get_post_trace(tau_hom); tr_post_hom = dst->get_post_trace(tau_hom);
 </code> </code>
-which initializes the traces using their respective timeconstants tau_* and registers them to either the presynaptic (''src'') or the postsynaptic (''dst'') [[manual:NeuronGroup]] respectively. By doing so, the traces will be automatically incremented by one upon each spike of the corresponding [[manual:NeuronGroup]]. Moreover, if multiple [[manual:Connection]] objects were to define a trace with the same timeconstant for the same [[manual:NeuronGroup]], Auryn knows about this and internally only computes the trace once. +which initializes the traces using their respective time constants tau_* and registers them to either the presynaptic (''src'') or the postsynaptic (''dst'') [[manual:NeuronGroup]] respectively. By doing so, the traces will be automatically evolved (decay over time) and incremented by one upon each spike of the corresponding [[manual:NeuronGroup]]. Moreover, if multiple [[manual:Connection]] objects were to define a trace with the same time constant for the same [[manual:NeuronGroup]], Auryn 'knowsabout this and internally only computes the trace once to speed up computation. The current value of a trace can then be accessed in the code via ''tr_post->get(NeuronID id)'' which we will use in the next section to compute weight updates.
- +
-The current value of a trace can then be accessed in the code via ''tr_post->get(NeuronID id)'' which we will use in the next section to compute weight updates.+
 ==== Weight updates at spiking events (propagate) ==== ==== Weight updates at spiking events (propagate) ====
  
-The code described in the previous section ensures that you have a bunch of pre- and postsynaptic traces at your hands, but it does not implement any weight update. To do so you will have to implement the function ''propagate()'' which is defined as purely virtual function in [[manual:Connection]]. For reasons of clarity it is generally a good idea to split this function logically into spike progagation (in forward direction, pre->post) and spike back-proapation. The following example shows how this is done in [[manual:TripletConnection]]:+The code described in the previous section ensures that you have a bunch of pre- and postsynaptic traces at your hands, but it does not implement any weight update. To do so you will have to implement the function ''propagate()'' which is defined as purely virtual function in [[manual:Connection]]. For reasons of clarity it is generally a good idea to split this function logically into spike propagation (in forward direction, pre->post) and spike back-propagation. The following example shows how this is done in [[manual:TripletConnection]]:
 <code c++> <code c++>
 void TripletConnection::propagate() void TripletConnection::propagate()
Line 72: Line 65:
 By forward propagation we mean the actions that are triggered by a presynaptic spike postsynaptically. This includes on the one hand evoking a postsynaptic conductance, but in the case of plastic connection object also updating the weight values. By forward propagation we mean the actions that are triggered by a presynaptic spike postsynaptically. This includes on the one hand evoking a postsynaptic conductance, but in the case of plastic connection object also updating the weight values.
  
-Let's have a look at how this is done in [[manual:TripletConnection]].+Let's have a look at how this is done in [[manual:TripletConnection]] (starting from revision number 54dd8f36bc3f4a300b61cd60d93bd7fcee7a3b77 -- 0.4.2 and onwards).
  
 <code c++> <code c++>
Line 118: Line 111:
     return dw;     return dw;
 </code> </code>
-The code describes how each weight should be updated upon a presynaptic spike (hence the suffix). Since spikes in Auryn are labelled with the [[global id]] of a neuron within a [[manual:SpikingGroup]], we need to translate this to the local ID, i.e. the index of the neuron on that rank. This is done by the function [[manual:global2rank]]. Next, we compute the weight update. In the minimal triplet STDP model by Pfister and Gerstner (2006) which is implemented in [[manual:TripletConnection]] a presynaptic spike causes LTD. The amount of LTD is proportional to one of the postsynaptic traces ''tr_post'' Moreover, the amount of LTD is homeostatically modulated by a factor derived from a slower trace (''get_hom(translated_spike)''). Finally hom_fudge is a factor which contains the learning rate and a normalization constant for the value returned by ''get_hom''. By precomputing this product, we save a few multiplications each time this function is called.+The code describes how each weight should be updated upon a presynaptic spike (hence the suffix). Since spikes in Auryn are labeled with the [[global id]] of a neuron within a [[manual:SpikingGroup]], we need to translate this to the local ID, i.e. the index of the neuron on that rank. This is done by the function [[manual:global2rank]]. Next, we compute the weight update. In the minimal triplet STDP model by Pfister and Gerstner (2006) which is implemented in [[manual:TripletConnection]] a presynaptic spike causes LTD. The amount of LTD is proportional to one of the postsynaptic traces ''tr_post'' Moreover, the amount of LTD is homeostatically modulated by a factor derived from a slower trace (''get_hom(translated_spike)''). Finally hom_fudge is a factor which contains the learning rate and a normalization constant for the value returned by ''get_hom''. By precomputing this product, we save a few multiplications each time this function is called.
 The complete weight update ''dw'' is ultimately returned to our ''propagate_forward()'' function where it is added to the weight value.  The complete weight update ''dw'' is ultimately returned to our ''propagate_forward()'' function where it is added to the weight value. 
  
-This describes the plastic update upon presynaptic spikes, but what happens upon a postsynaptic spike? +One thing you should pay close attention to, because it could might easily introduce errors: **You need to translate postsynaptic IDs (as shown above)**, but not the presynaptic ones! Copies of presynaptic trances are available on each rank, whereas postsynaptic traces are distributed with the neuronal dynamics. This only makes a difference when you are running simulations in parallel with MPI, but then it's an important difference. 
 + 
 +We now covered weight updates triggered by presynaptic spikes, but what happens upon a postsynaptic spike?  
  
 === Backward spike propagation === === Backward spike propagation ===
Line 175: Line 171:
 ===== Changing the plasticity model ===== ===== Changing the plasticity model =====
  
-Suppose, you would like to change the plasticity model, all you need to do is to copy TripletConnection.h and TripletConnection.cpp to YourNameConnection.h and .cpp and then the ''dw_pre'' and ''dw_post'' would be the first places start to change things. Of course you can declare new parameters in the header of the Connection object and either directly set them from the main program, hard code them m( or access them via a bunch of getters or setters. Moreover, in many cases it might be important for you to redefine ''dw_pre'' and ''dw_post'' altogether. For instance if you would like to implement a weight dependence, you will need to include our above ''*weight'' variable into the parameter list of these functions. Similarly, you might want to access the postsynaptic membrane voltage through ''dst->get_mem(NeuronID id)'' and pass it to you ''dw'' functions ... I will let you experiment and hope that I will have the time to include some more examples here soon.+Most of the plasticity models in Auryn follow the design principles introduced above (e.g. http://fzenke.net/auryn/doxygen/current/classauryn_1_1STDPConnection.html). Suppose, you would like to change the plasticity model, all you need to do is to copy TripletConnection.h and TripletConnection.cpp to YourNameConnection.h and .cpp and then the ''dw_pre'' and ''dw_post'' would be the first places start to change things. Of course you can declare new parameters in the header of the Connection object and either directly set them from the main program, hard code them m( or access them via a bunch of getters or setters. Moreover, in many cases it might be important for you to redefine ''dw_pre'' and ''dw_post'' altogether. For instance if you would like to implement a weight dependence, you will need to include our above ''*weight'' variable into the parameter list of these functions. Similarly, you might want to access the postsynaptic membrane voltage through ''dst->get_mem(NeuronID id)'' and pass it to you ''dw'' functions ... I will let you experiment and hope that I will have the time to include some more examples here soon. 
  
-One thing you should pay attention to, because it could might easily introduce errors: You need to translate postsynaptic IDs (as shown above), but not the presynaptic ones! Copies of presynaptic trances are integrated on each rank; that sounds inefficient, but turned out to be the most efficient way to do it in most cases. 
  
 ==== Weight updates in continuous time (evolve) ==== ==== Weight updates in continuous time (evolve) ====
Line 197: Line 193:
 if ( sys->get_clock()%timestep_scaling == 0 && stdp_active ) { if ( sys->get_clock()%timestep_scaling == 0 && stdp_active ) {
   for (AurynLong i = 0 ; i < w->get_nonzero() ; ++i ) {   for (AurynLong i = 0 ; i < w->get_nonzero() ; ++i ) {
-    AurynWeight * weight = w->get_data(i);+    AurynWeight * weight = w->get_data_ptr(i);
     // do something with the weight     // do something with the weight
     }     }
Line 205: Line 201:
 where you will have to define and initialize the variable time_scaling somewhere in your connection object. It will give you the new step width in multiples of Auryn's time step dt. where you will have to define and initialize the variable time_scaling somewhere in your connection object. It will give you the new step width in multiples of Auryn's time step dt.
  
 +This should give you the necessary tools to start out with writing your own plasticity models. As with all code, write simple unit tests to ensure you are simulating what you want to simulate. Happy coding. 
tutorials/writing_your_own_plasticity_model.1422047555.txt.gz · Last modified: 2015/01/23 21:12 by zenke