CARLsim  3.1.3
CARLsim: a GPU-accelerated SNN simulator
Tutorial 7: Parameter Tuning Interface (PTI)
Author
Kristofor D. Carlson

7.1 Introduction

In this tutorial we introduce the parameter tuning interface and how to tune the weight ranges of a simple SNN to achieve a particular target firing rate. We will be tuning four parameter values that represent the weight ranges. The layout of the SNN is as follows. We have 10 input excitatory neurons, 10 regular spiking (RS) excitatory neurons that receive this input, and 10 fast spiking (FS) izhikevich neurons that receive input from the excitatory RS neurons. Additionally, the inhibitory group is connected to the RS excitatory group and the RS excitatory group is connected to itself recurrently. There are therefore 4 connection weight ranges to be tuned. The goal of the tuning is make the RS excitatory group have an average firing rate of 10 Hz and make the FS inhibitory group have an average firing rate of 20 Hz.

All the required files are found in the doc/source/tutorial/3_pti directory. The user is encouraged to follow along and experiment with the source code found there.

This tutorial assumes:

  • ECJ version 22 or greater is installed.
  • CARLsim 3.o or greater is installed.
  • The user.mk and associated CARLsim environment variables are configured correctly.

The overview of parameter tuning is as follows. ECJ handles all steps of the evolutionary algorithm (EA) except for the fitness evaluation which is done by CARLsim through the implementation of an experiment class function called run.

A more in-depth overview of CARLsim and ECJ can be found in Chapter 10: ECJ.

7.2 The ECJ Parameter File

This example has an ECJ parameter file named TuneFiringRatesECJExperiment.params. As mentioned in Chapter 10: ECJ, users must first configure this file if they want to parameter tune CARLsim programs.

We first specify that we want a maximum number of 50 generations and a population size of 10. This means 10 individuals (each a separate SNN) will be run in parallel by CARLsim:

generations = 50
pop.subpop.0.size = 10

There are four parameter values to be tuned so we set:

pop.subpop.0.species.genome-size = 4

We will allow all four weight ranges to be tuned within the same parameter range although each individual parameter can be given it's own range (see Chapter 10: ECJ). The minimum weight value for each weight is then 0.0005 while the maximum value is 0.5 as shown below:

pop.subpop.0.species.min-gene = 0.0005
pop.subpop.0.species.max-gene = 0.5

Finally we must specify the name of the carlsim binary that ECJ should execute every generation to obtain fitness values. This is done below. Notice there is a $ before the filename. This indicates that the location of the file is relative to the location of the parameter file.

eval.problem.simulationCommand = $carlsim_tuneFiringRatesECJ

Of course any of these parameters can be changed. For more information on ECJ parameter file configuration please visit the ECJ homepage.

5.3 The Experiment Class

We next discuss the CARLsim code found in the experiment class. This code is found in the main_tuneFiringRatesECJ.cpp file. Users should use this tutorial as template for writing their own code. First a specific experiment class must be inherited from the base class and default constructor must be defined.:

class TuneFiringRatesECJExperiment : public Experiment {
public:
TuneFiringRatesECJExperiment() {}

After that, the user should only be concerned with writing the run member function.

...
void run(const ParameterInstances &parameters, std::ostream &outputStream) const {
int indiNum = parameters.getNumInstances();
int poissonGroup[indiNum];
int excGroup[indiNum];
int inhGroup[indiNum];
SpikeMonitor* excMonitor[indiNum];
SpikeMonitor* inhMonitor[indiNum];
float excHz[indiNum];
float inhHz[indiNum];
float excError[indiNum];
float inhError[indiNum];
float fitness[indiNum];
...

Notice we can use the parameters object to query how many instances/individuals we have defined in the ECJ parameter file. Now that we have that value, we manually create arrays neuron groups of size indiNum. In our case we have defined 10 individuals per generation.

Next we create our CARLsim network object and assign the 4 parameter values generated by ECJ to our 10 SNN individuals. We iterate over all 10 individuals and assign them 4 distinct parameters each.

CARLsim* const network = new CARLsim("tuneFiringRatesECJ", GPU_MODE, SILENT);
for(unsigned int i = 0; i < parameters.getNumInstances(); i++) {
poissonGroup[i] = network->createSpikeGeneratorGroup("poisson", NUM_NEURONS, EXCITATORY_NEURON);
excGroup[i] = network->createGroup("exc", NUM_NEURONS, EXCITATORY_NEURON);
inhGroup[i] = network->createGroup("inh", NUM_NEURONS, INHIBITORY_NEURON);
network->setNeuronParameters(excGroup[i], REG_IZH[0], REG_IZH[1], REG_IZH[2], REG_IZH[3]);
network->setNeuronParameters(inhGroup[i], FAST_IZH[0], FAST_IZH[1], FAST_IZH[2], FAST_IZH[3]);
network->setConductances(true,COND_tAMPA,COND_tNMDA,COND_tGABAa,COND_tGABAb);
network->connect(poissonGroup[i], excGroup[i], "random", RangeWeight(parameters.getParameter(i,0)), 0.5f, RangeDelay(1));
network->connect(excGroup[i], excGroup[i], "random", RangeWeight(parameters.getParameter(i,1)), 0.5f, RangeDelay(1));
network->connect(excGroup[i], inhGroup[i], "random", RangeWeight(parameters.getParameter(i,2)), 0.5f, RangeDelay(1));
network->connect(inhGroup[i], excGroup[i], "random", RangeWeight(parameters.getParameter(i,3)), 0.5f, RangeDelay(1));
}

We then setup the SNNs, begin recording their spike data with a SpikeMonitor, and run the SNNs.

...
network->setupNetwork();
...
excMonitor[i] = network->setSpikeMonitor(excGroup[i], "/dev/null");
inhMonitor[i] = network->setSpikeMonitor(inhGroup[i], "/dev/null");
excMonitor[i]->startRecording();
inhMonitor[i]->startRecording();
...
network->runNetwork(runTime,0);

Finally we stop recording, get the average firing rate of each group and compute a fitness function which is output to ECJ using standard Linux streams.

excMonitor[i]->stopRecording();
inhMonitor[i]->stopRecording();
excHz[i] = excMonitor[i]->getPopMeanFiringRate();
inhHz[i] = inhMonitor[i]->getPopMeanFiringRate();
excError[i] = fabs(excHz[i] - EXC_TARGET_HZ);
inhError[i] = fabs(inhHz[i] - INH_TARGET_HZ);
fitness[i] = 1/(excError[i] + inhError[i]);
outputStream << fitness[i] << endl;

To compile and run the tuning framework we simply type:

make
./tuneFiringRatesECJ
Warning
Do not make the mistake of running carlsim_tuneFiringRatesECJ instead of tuneFiringRatesECJ as carlsim_tuneFiringRatesECJ is the program ECJ executes to receive fitness values from. If you execute carlsim_tuneFiringRatesECJ it will just hang as it is waiting for input from ECJ.

The simulation should run to completion and output the best fitness each generation.

This results in an ECJ out.stat file that we talk about in the next section.

7.4 ECJ Output Files

The output.stat file generated contains the best fitness for that generation along with the four parameter values associated with that individual. CARLsim users can then use these parameters for their tuned SNN models.

See also
Chapter 10: ECJ