{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"*Copyright (C) 2021 Intel Corporation*
\n",
"*SPDX-License-Identifier: BSD-3-Clause*
\n",
"*See: https://spdx.org/licenses/*\n",
"\n",
"---\n",
"\n",
"# Spike-timing Dependent Plasticity (STDP)\n",
"\n",
"_**Motivation**: In this tutorial, we will demonstrate usage of a software model of Loihi's learning engine, exposed in Lava. This involves the LearningRule object for learning rule and other learning-related information encapsulation and the LearningDense Lava Process modelling learning-enabled connections._\n",
"\n",
"#### This tutorial assumes that you:\n",
"- have the [Lava framework installed](../../in_depth/tutorial01_installing_lava.ipynb \"Tutorial on Installing Lava\")\n",
"- are familiar with the [Process concept in Lava](../../in_depth/tutorial02_processes.ipynb \"Tutorial on Processes\")\n",
"- are familiar with the [ProcessModel concept in Lava](../../in_depth/tutorial02_process_models.ipynb \"Tutorial on ProcessModels\")\n",
"- are familiar with how to [connect Lava Processes](../../in_depth/tutorial05_connect_processes.ipynb \"Tutorial on connecting Processes\")\n",
"\n",
"This tutorial gives a bird's-eye view of how to make use of the available learning rules in Lavas Process Library. For this purpose, we will create a network of LIF and Dense processes with one plastic connection and generate frozen patterns of activity. We can easily choose between a floating point simulation of the learning engine and a fixed point simulation, which approximates the behavior on the Loihi neuromorphic hardware. We also will create monitors to observe the behavior of the weights and activity traces of the neurons and learning rules."
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"## STDP from Lavas Process Library\n",
"\n",
"Let's first generate the random, frozen input and define all parameters for the network.\n",
"\n",
"### Parameters"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"# Set this tag to \"fixed_pt\" or \"floating_pt\" to choose the corresponding models.\n",
"SELECT_TAG = \"floating_pt\"\n",
"\n",
"# LIF parameters\n",
"if SELECT_TAG == \"fixed_pt\":\n",
" du = 4095\n",
" dv = 4095\n",
"elif SELECT_TAG == \"floating_pt\":\n",
" du = 1\n",
" dv = 1\n",
"vth = 240\n",
"\n",
"# Number of neurons per layer\n",
"num_neurons = 1\n",
"shape_lif = (num_neurons, )\n",
"shape_conn = (num_neurons, num_neurons)\n",
"\n",
"# Connection parameters\n",
"\n",
"# SpikePattern -> LIF connection weight\n",
"wgt_inp = np.eye(num_neurons) * 250\n",
"\n",
"# LIF -> LIF connection initial weight (learning-enabled)\n",
"wgt_plast_conn = np.full(shape_conn, 50)\n",
" \n",
"# Number of simulation time steps\n",
"num_steps = 200\n",
"time = list(range(1, num_steps + 1))\n",
"\n",
"# Spike times\n",
"spike_prob = 0.03\n",
"\n",
"# Create spike rasters\n",
"np.random.seed(123)\n",
"spike_raster_pre = np.zeros((num_neurons, num_steps))\n",
"np.place(spike_raster_pre, np.random.rand(num_neurons, num_steps) < spike_prob, 1)\n",
"\n",
"spike_raster_post = np.zeros((num_neurons, num_steps))\n",
"np.place(spike_raster_post, np.random.rand(num_neurons, num_steps) < spike_prob, 1)"
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"### Define STDP learning rule\n",
"\n",
"Next, lets instatiate the STDP learning rule from the Lava Process Library. The STDPLoihi learning rule provides the parameters as described in Gerstner and al. 1996 (see also http://www.scholarpedia.org/article/Spike-timing_dependent_plasticity)."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"from lava.proc.learning_rules.stdp_learning_rule import STDPLoihi"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"stdp = STDPLoihi(learning_rate=1,\n",
" A_plus=-1,\n",
" A_minus=1,\n",
" tau_plus=10,\n",
" tau_minus=10,\n",
" t_epoch=4)"
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"### Create Network\n",
"The following diagram depics the Lava Process architecture used in this tutorial. It consists of:\n",
"- 2 Constant pattern generators for injection spike trains to LIF neurons.\n",
"- 2 _LIF_ Processes representing pre- and post-synaptic Leaky Integrate-and-Fire neurons.\n",
"- 1 _Dense_ Process representing learning-enable connection between LIF neurons.\n",
"\n",
">**Note:** \n",
"All neuronal population (spike generator, LIF) are composed of only 1 neuron in this tutorial."
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"#### The plastic connection Process\n",
"We now instantiate our plastic Dense process. The Dense Process provides the following Vars and Ports relevant for plasticity:\n",
"\n",
"| Component | Name | Description |\n",
"| :- | :- | :- |\n",
"| **InPort** | `s_in_bap` | Receives spikes from post-synaptic neurons.\n",
"| **Var** | `tag_2` | Delay synaptic variable.\n",
"| | `tag_1` | Tag synaptic variable.\n",
"| | `x0` | State of $x_0$ dependency.\n",
"| | `tx` | Within-epoch spike times of pre-synaptic neurons.\n",
"| | `x1` | State of $x_1$ trace.\n",
"| | `x2` | State of $x_2$ trace.\n",
"| | `y0` | State of $y_0$ dependency.\n",
"| | `ty` | Within-epoch spike times of post-synaptic neurons.\n",
"| | `y1` | State of $y_1$ trace.\n",
"| | `y2` | State of $y_2$ trace.\n",
"| | `y3` | State of $y_3$ trace.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"pycharm": {
"name": "#%%\n"
},
"tags": []
},
"outputs": [],
"source": [
"from lava.proc.lif.process import LIF\n",
"from lava.proc.io.source import RingBuffer\n",
"from lava.proc.dense.process import LearningDense, Dense"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"# Create input devices\n",
"pattern_pre = RingBuffer(data=spike_raster_pre.astype(int))\n",
"pattern_post = RingBuffer(data=spike_raster_post.astype(int))\n",
"\n",
"# Create input connectivity\n",
"conn_inp_pre = Dense(weights=wgt_inp)\n",
"conn_inp_post = Dense(weights=wgt_inp)\n",
"\n",
"# Create pre-synaptic neurons\n",
"lif_pre = LIF(u=0,\n",
" v=0,\n",
" du=du,\n",
" dv=du,\n",
" bias_mant=0,\n",
" bias_exp=0,\n",
" vth=vth,\n",
" shape=shape_lif,\n",
" name='lif_pre')\n",
"\n",
"# Create plastic connection\n",
"plast_conn = LearningDense(weights=wgt_plast_conn,\n",
" learning_rule=stdp,\n",
" name='plastic_dense')\n",
"\n",
"# Create post-synaptic neuron\n",
"lif_post = LIF(u=0,\n",
" v=0,\n",
" du=du,\n",
" dv=du,\n",
" bias_mant=0,\n",
" bias_exp=0,\n",
" vth=vth,\n",
" shape=shape_lif,\n",
" name='lif_post')\n",
"\n",
"# Connect network\n",
"pattern_pre.s_out.connect(conn_inp_pre.s_in)\n",
"conn_inp_pre.a_out.connect(lif_pre.a_in)\n",
"\n",
"pattern_post.s_out.connect(conn_inp_post.s_in)\n",
"conn_inp_post.a_out.connect(lif_post.a_in)\n",
"\n",
"lif_pre.s_out.connect(plast_conn.s_in)\n",
"plast_conn.a_out.connect(lif_post.a_in)\n",
"\n",
"# Connect back-propagating actionpotential (BAP)\n",
"lif_post.s_out.connect(plast_conn.s_in_bap)"
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"### Create monitors to observe traces"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"pycharm": {
"name": "#%%\n"
},
"tags": []
},
"outputs": [],
"source": [
"from lava.proc.monitor.process import Monitor"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"pycharm": {
"name": "#%%\n"
},
"tags": []
},
"outputs": [],
"source": [
"# Create monitors\n",
"mon_pre_trace = Monitor()\n",
"mon_post_trace = Monitor()\n",
"mon_pre_spikes = Monitor()\n",
"mon_post_spikes = Monitor()\n",
"mon_weight = Monitor()\n",
"\n",
"# Connect monitors\n",
"mon_pre_trace.probe(plast_conn.x1, num_steps)\n",
"mon_post_trace.probe(plast_conn.y1, num_steps)\n",
"mon_pre_spikes.probe(lif_pre.s_out, num_steps)\n",
"mon_post_spikes.probe(lif_post.s_out, num_steps)\n",
"mon_weight.probe(plast_conn.weights, num_steps)"
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
},
"tags": []
},
"source": [
"### Running"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"from lava.magma.core.run_conditions import RunSteps\n",
"from lava.magma.core.run_configs import Loihi2SimCfg"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"# Running\n",
"pattern_pre.run(condition=RunSteps(num_steps=num_steps), run_cfg=Loihi2SimCfg(select_tag=SELECT_TAG))"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"# Get data from monitors\n",
"pre_trace = mon_pre_trace.get_data()['plastic_dense']['x1']\n",
"post_trace = mon_post_trace.get_data()['plastic_dense']['y1']\n",
"pre_spikes = mon_pre_spikes.get_data()['lif_pre']['s_out']\n",
"post_spikes = mon_post_spikes.get_data()['lif_post']['s_out']\n",
"weights = mon_weight.get_data()['plastic_dense']['weights'][:, :, 0]"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"# Stopping\n",
"pattern_pre.stop()"
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"### Results\n",
"\n",
"Now, we can take a look at the results of the simulation. "
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"pycharm": {
"name": "#%%\n"
},
"tags": []
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt"
]
},
{
"cell_type": "markdown",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"#### Plot spike trains"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"