This hands-on tutorial introduces the basics of N2S3. We will simulate the execution of a simple neural network, and make it recognize hexa-decimal digits (0-F) under a seven segment representation.
This tutorial assumes you already have installed everything that is necessary to use N2S3, and that you're running it from an IDE (like eclipse or IntelliJ IDEA).
Open N2S3 in your preferred IDE, and create a new scala object. Let's call it FirstExperiment
.
package myPackage object FirstExperiment { }
To be able to execute our experiment, we need to tell Scala that this is an executable object. Something like a Java main
. For this, we only need to make FirstExperiment
a subclass of App
.
package fr.cristal.emeraude.n2s3.apps object FirstExperiment extends App { }
Once FirstExperiment
extends from App
, the IDE will offer you in the contextual menu to run it! To test that your environment is well configured you can add a println
and execute it.
package fr.cristal.emeraude.n2s3.apps object FirstExperiment extends App { println("works!") }
You will see that the console outputs something like…
works! Process finished with exit code 0
To start using N2S3, we need to import the N2S3 class and create an instance of it:
package MyPackage import fr.univ_lille.cristal.emeraude.n2s3.features.builder.N2S3 object FirstExperiment extends App { var n2s3 = new N2S3 }
A N2S3 object provides the main API to manipulate and run a simulation. For this first simulation let's create a simple neural network with 20 neurons that will recognize hexadecimal digits (i.e., [0-9,A-F]). We need to tell N2S3 to create a neuron group of 20 neurons. Let's use for this example the neurons inspired in the QBG model that are provided directly by the simulator, and initialize the threshold of those neurons to 6 volts.
package MyPackage import fr.univ_lille.cristal.emeraude.n2s3.core.models.properties.MembranePotentialThreshold import fr.univ_lille.cristal.emeraude.n2s3.features.builder.N2S3 import fr.univ_lille.cristal.emeraude.n2s3.models.neurons.LIF import squants.electro.ElectricPotentialConversions.ElectricPotentialConversions object FirstExperiment extends App { var n2s3 = new N2S3 val unsupervisedLayer = n2s3.createNeuronGroup("out", 20) .setNeuronModel(LIF, Seq( (MembranePotentialThreshold, 1 volts) )) }
Once we have our neuron group, we need to provide them with some input! Let's install an input layer that will feed the neurons with the already existing Digital Hex input. And let's also make a neuron inhibit all the rest of the neurons in the same group when it spikes.
package MyPackage object FirstExperiment extends App { var n2s3 = new N2S3 val unsupervisedLayer = n2s3.createNeuronGroup("out", 20) .setNeuronModel(QBGNeuron, Seq( (MembraneThresholdPotential, 1 volts) )) val inputstream = InputDigitalHex.Entry >> StreamOversampling(2, 2) >> SampleToSpikeTrainConverter[Float, InputSample2D[Float]](0, 23, 350 MilliSecond, 150 MilliSecond) >> N2S3Entry val inputLayer = n2s3.createInput(inputstream) inputstream.append(new DigitalHexInputStream().repeat(1000).shuffle()) inputLayer.connectTo(unsupervisedLayer, new FullConnection(() => new SimplifiedSTDPWithNegative())) unsupervisedLayer.connectTo(unsupervisedLayer, new FullConnection(() => new InhibitorySynapse())) }
Finally, let's put the simulation to run, and wait until it's finished.
package MyPackage import fr.univ_lille.cristal.emeraude.n2s3.core.models.properties.MembranePotentialThreshold import fr.univ_lille.cristal.emeraude.n2s3.features.builder.N2S3 import fr.univ_lille.cristal.emeraude.n2s3.features.builder.connection.types.FullConnection import fr.univ_lille.cristal.emeraude.n2s3.features.io.input._ import fr.univ_lille.cristal.emeraude.n2s3.models.neurons.LIF import fr.univ_lille.cristal.emeraude.n2s3.models.synapses.{InhibitorySynapse, SimplifiedSTDPWithNegative} import squants.electro.ElectricPotentialConversions.ElectricPotentialConversions import fr.univ_lille.cristal.emeraude.n2s3.support.UnitCast._ import fr.univ_lille.cristal.emeraude.n2s3.features.io.input.N2S3InputStreamCombinators._ object FirstExperiment extends App { var n2s3 = new N2S3 val unsupervisedLayer = n2s3.createNeuronGroup("out", 20) .setNeuronModel(QBGNeuron, Seq( (MembraneThresholdPotential, 1 volts) )) val inputstream = DigitalHexEntry() >> StreamOversampling(2, 2) >> SampleToSpikeTrainConverter(0, 23, 350 MilliSecond, 150 MilliSecond) val inputLayer = n2s3.createInput(inputstream) inputstream.append(new DigitalHexInputStream().repeat(1000).shuffle()) inputLayer.connectTo(unsupervisedLayer, new FullConnection(() => new QBGNeuronConnectionWithNegative)) unsupervisedLayer.connectTo(unsupervisedLayer, new FullConnection(() => new QBGInhibitorConnection)) n2s3.runAndWait() println("Finished!") }
Of course… now that you know how to run a simulation, you'd like to extract the results…
To understand how the weights of the synapses react to our input, and see wether they learn or not, N2S3 already provides a prebuilt visualisation. It suffices to create a weight graph, using as argument the neuron group we want to observe.
-> n2s3.createSynapseWeightGraphOn(inputLayer, unsupervisedLayer) n2s3.runAndWait()
Making this change, and re-executing should open a graph showing the 20 neurons of our network, and how the synapses of each our neurons learn using a color spectrum:
You can see in the screenshot above, how neurons specialized to recognize different numbers (3,A,5,7,9,4,E,C …).
As you can see, the learning was not great: there is still a lot of green and yello in the image. A better result would have been reds and blues. If we wanted better results, we should have trained a bit more our network. To do that, it suffices to repeat the input several times. To do that you just need to import the N2S3InputStreamCombinators
package and use the repeat()
method on the input stream:
package fr.univ_lille.cristal.emeraude.n2s3.apps import fr.univ_lille.cristal.emeraude.n2s3.core.models.properties.MembranePotentialThreshold import fr.univ_lille.cristal.emeraude.n2s3.features.builder.N2S3 import fr.univ_lille.cristal.emeraude.n2s3.features.builder.connection.types.FullConnection import fr.univ_lille.cristal.emeraude.n2s3.features.io.input._ import fr.univ_lille.cristal.emeraude.n2s3.models.neurons.LIF import fr.univ_lille.cristal.emeraude.n2s3.models.synapses.{InhibitorySynapse, SimplifiedSTDPWithNegative} import squants.electro.ElectricPotentialConversions.ElectricPotentialConversions import fr.univ_lille.cristal.emeraude.n2s3.support.UnitCast._ import fr.univ_lille.cristal.emeraude.n2s3.features.io.input.N2S3InputStreamCombinators._ /** * Created by Guille Polito on 30/01/17. * Example class using N2S3. * * This class shows that imports worked right. */ object Main extends App { // QBGParameters. var n2s3 = new N2S3 val unsupervisedLayer = n2s3.createNeuronGroup("out", 20) .setNeuronModel(LIF, Seq( (MembranePotentialThreshold, 1 volts) )) val inputstream = InputDigitalHex.Entry >> StreamOversampling(2, 2) >> SampleToSpikeTrainConverter[Float, InputSample2D[Float]](0, 23, 350 MilliSecond, 150 MilliSecond) >> N2S3Entry val inputLayer = n2s3.createInput(inputstream) inputstream.append(new DigitalHexInputStream().repeat(1000).shuffle()) inputLayer.connectTo(unsupervisedLayer, new FullConnection(() => new SimplifiedSTDPWithNegative())) unsupervisedLayer.connectTo(unsupervisedLayer, new FullConnection(() => new InhibitorySynapse())) n2s3.runAndWait() println("Finished!") }
Once our network has learnt, N2S3 provides several components to test if it's able to actually recognize the input as expected. This test is done by the benchmark monitor. N2S3's benchmark monitor is what we call a network observer. A network observer is some process that subscribe itself to events in the network (such as spikes, new inputs) to perform some processing.
In this case, the benchmark monitor will check wether a neuron spikes for a given input, and then compare the label associated to each input (i.e., a number in hexa 0-9A-F) to the label associated for a neuron. Like this, it can test what is the number of inputs that the network recognized.
To perform the test, we can fix the neurons in out neuron group so it stops learning while we test.
unsupervisedLayer.fixNeurons()
Then, create a benchmark monitor giving as argument the labels we want him to recognize and what is the layer he has to observe,
val benchmarkMonitor = n2s3.createBenchmarkMonitor(unsupervisedLayer)
And finally, set a new input stream for testing:
inputstream.clean() inputstream.append(new DigitalHexInputStream().repeat(10).shuffle())
The final code will look like this:
package fr.univ_lille.cristal.emeraude.n2s3.apps import fr.univ_lille.cristal.emeraude.n2s3.core.models.properties.MembranePotentialThreshold import fr.univ_lille.cristal.emeraude.n2s3.features.builder.N2S3 import fr.univ_lille.cristal.emeraude.n2s3.features.builder.connection.types.FullConnection import fr.univ_lille.cristal.emeraude.n2s3.features.io.input._ import fr.univ_lille.cristal.emeraude.n2s3.models.neurons.LIF import fr.univ_lille.cristal.emeraude.n2s3.models.synapses.{InhibitorySynapse, SimplifiedSTDPWithNegative} import squants.electro.ElectricPotentialConversions.ElectricPotentialConversions import fr.univ_lille.cristal.emeraude.n2s3.support.UnitCast._ import fr.univ_lille.cristal.emeraude.n2s3.features.io.input.N2S3InputStreamCombinators._ /** * Created by Guille Polito on 30/01/17. * Example class using N2S3. * * This class shows that imports worked right. */ object Main extends App { // QBGParameters. var n2s3 = new N2S3 val unsupervisedLayer = n2s3.createNeuronGroup("out", 20) .setNeuronModel(LIF, Seq( (MembranePotentialThreshold, 1 volts) )) val inputstream = InputDigitalHex.Entry >> StreamOversampling(2, 2) >> SampleToSpikeTrainConverter[Float, InputSample2D[Float]](0, 23, 350 MilliSecond, 150 MilliSecond) >> N2S3Entry val inputLayer = n2s3.createInput(inputstream) inputstream.append(new DigitalHexInputStream().repeat(1000).shuffle()) inputLayer.connectTo(unsupervisedLayer, new FullConnection(() => new SimplifiedSTDPWithNegative())) unsupervisedLayer.connectTo(unsupervisedLayer, new FullConnection(() => new InhibitorySynapse())) n2s3.runAndWait() unsupervisedLayer.fixNeurons() inputstream.clean() inputstream.append(new DigitalHexInputStream().repeat(10).shuffle()) val benchmarkMonitor = n2s3.createBenchmarkMonitor(unsupervisedLayer) n2s3.runAndWait() println("Finished!") }
Once the simulation finishes, we will see as output in the console the rate of recognition:
Score = 14/15