diff --git a/examples/legacy/Alpha_neuron_training_example.ipynb b/examples/legacy/Alpha_neuron_training_example.ipynb deleted file mode 100644 index 56141d34..00000000 --- a/examples/legacy/Alpha_neuron_training_example.ipynb +++ /dev/null @@ -1,343 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "0rklDqluPYZB" - }, - "source": [ - "## Example of training using the Alpha Neuron" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "ptd12vmR7H8D", - "outputId": "79499811-cc47-4470-f190-f3a8581df051" - }, - "outputs": [], - "source": [ - "# !git clone -b alpha_neuron --single-branch https://github.com/jeshraghian/snntorch.git\n", - "!pip install snntorch" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "5VNVo64L93Zo", - "outputId": "69255447-8261-4119-f851-066c05ea510c" - }, - "outputs": [], - "source": [ - "# %cd snntorch\n", - "import snntorch as snn" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "CEUipC6UPebM" - }, - "source": [ - "## Import Packages" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "AQeg7QZ69fSt" - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "import torch\n", - "import torch.nn as nn\n", - "from torch.utils.data import DataLoader\n", - "from torchvision import datasets, transforms\n", - "import numpy as np\n", - "import itertools\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "0OEdE6bEPf20" - }, - "source": [ - "## Define Network and Parameters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "MHmCvayV7LJR" - }, - "outputs": [], - "source": [ - "# test alpha neuron: can it learn?\n", - "\n", - "num_inputs = 28*28\n", - "num_hidden = 1000\n", - "num_outputs = 10\n", - "\n", - "# Training Parameters\n", - "batch_size=128\n", - "data_path='/tmp/data/mnist'\n", - "\n", - "# Temporal Dynamics\n", - "num_steps = 25\n", - "alpha = 0.9\n", - "beta = 0.8\n", - "\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"mps\") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", - "\n", - "# Define Network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - " \n", - " # initialize layers\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Alpha(alpha=alpha, beta=beta)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Alpha(alpha=alpha, beta=beta)\n", - "\n", - "\n", - " def forward(self, x):\n", - " spk1, syn_exc1, syn_inh1, mem1 = self.lif1.init_alpha(batch_size, num_hidden)\n", - " spk2, syn_exc2, syn_inh2, mem2 = self.lif2.init_alpha(batch_size, num_outputs)\n", - "\n", - " # Record the final layer\n", - " spk2_rec = []\n", - " mem2_rec = []\n", - "\n", - " for step in range(num_steps):\n", - "\n", - " cur1 = self.fc1(x)\n", - " spk1, syn_exc1, syn_inh1, mem1 = self.lif1(cur1, syn_exc1, syn_inh1, mem1)\n", - " cur2 = self.fc2(spk1)\n", - " spk2, syn_exc2, syn_inh2, mem2 = self.lif2(cur2, syn_exc2, syn_inh2, mem2)\n", - "\n", - " spk2_rec.append(spk2)\n", - " mem2_rec.append(mem2)\n", - "\n", - " return torch.stack(spk2_rec, dim=0), torch.stack(mem2_rec, dim=0)\n", - " \n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "2N00-2eDPl2G" - }, - "source": [ - "## dataloaders" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "NblLifHj9qO-", - "outputId": "e890995e-a9fd-490d-f278-caf6bce9c21e" - }, - "outputs": [], - "source": [ - "# Define a transform\n", - "transform = transforms.Compose([\n", - " transforms.Resize((28, 28)),\n", - " transforms.Grayscale(),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))])\n", - "\n", - "mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "kx8nATF69tEk" - }, - "outputs": [], - "source": [ - "# Create DataLoaders\n", - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "gqDmeDJcP_HL" - }, - "source": [ - "## Print Accuracy Function" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "EJYEQpHk-MzX" - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " with torch.no_grad():\n", - " output, _ = net(data.view(batch_size, -1))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(data_it, targets_it, train=True)\n", - " print_batch_accuracy(testdata_it, testtargets_it, train=False)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "m2Zz5xesQBBk" - }, - "source": [ - "## Define Loss & Optimizer" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "4Uwvn33m-OuY" - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "wY-94aYrQCXE" - }, - "source": [ - "## Training Loop" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 1000 - }, - "id": "hQvfXduS-QmB", - "outputId": "7ec1f1dc-ce3f-4834-f601-9b5c34245225" - }, - "outputs": [], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(10):\n", - " minibatch_counter = 0\n", - " train_batch = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in train_batch:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " spk_rec, mem_rec = net(data_it.view(batch_size, -1))\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros((1), dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps: BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward()\n", - "\n", - " # Weight Update\n", - " optimizer.step()\n", - "\n", - " # Store loss history for future plotting\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set forward pass\n", - " with torch.no_grad():\n", - " test_spk, test_mem = net(testdata_it.view(batch_size, -1))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, testtargets_it)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "collapsed_sections": [], - "name": "Alpha_code_example.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/legacy/CIFAR_temp.ipynb b/examples/legacy/CIFAR_temp.ipynb deleted file mode 100644 index e86b4176..00000000 --- a/examples/legacy/CIFAR_temp.ipynb +++ /dev/null @@ -1,2857 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "rtrNT4NPRp7r", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "\n", - "\n", - "# snnTorch - Gradient-based Learning in Spiking Neural Networks\n", - "## Tutorial 2\n", - "### By Jason K. Eshraghian\n", - "\n", - "\n", - " \"Open\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "TKy-qDQdRp73" - }, - "source": [ - "# Introduction\n", - "In this tutorial, you will learn how to use snnTorch to:\n", - "* create a 2-layer fully-connected spiking network;\n", - "* implement the backpropagation through time (BPTT) algorithm;\n", - "* to classify both the static and spiking MNIST datasets.\n", - "\n", - "If running in Google Colab:\n", - "* You may connect to GPU by checking `Runtime` > `Change runtime type` > `Hardware accelerator: GPU`\n", - "* Next, install the latest PyPi distribution of snnTorch by clicking into the following cell and pressing `Shift+Enter`." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 0 - }, - "id": "BgBRVUtpRp74", - "outputId": "61c32ebb-e69b-4d44-852b-0bf58481a9d1", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: snntorch in /usr/local/lib/python3.7/dist-packages (0.2.7)\n", - "Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from snntorch) (1.1.5)\n", - "Requirement already satisfied: torch>=1.2.0 in /usr/local/lib/python3.7/dist-packages (from snntorch) (1.8.0+cu101)\n", - "Requirement already satisfied: matplotlib in /usr/local/lib/python3.7/dist-packages (from snntorch) (3.2.2)\n", - "Requirement already satisfied: celluloid in /usr/local/lib/python3.7/dist-packages (from snntorch) (0.2.0)\n", - "Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.7/dist-packages (from snntorch) (1.19.5)\n", - "Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.7/dist-packages (from pandas->snntorch) (2018.9)\n", - "Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas->snntorch) (2.8.1)\n", - "Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from torch>=1.2.0->snntorch) (3.7.4.3)\n", - "Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->snntorch) (1.3.1)\n", - "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib->snntorch) (0.10.0)\n", - "Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->snntorch) (2.4.7)\n", - "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.7.3->pandas->snntorch) (1.15.0)\n" - ] - } - ], - "source": [ - "!pip install snntorch" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "Zm-D2lthRp75", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 1. Setting up the Static MNIST Dataset\n", - "### 1.1. Import packages and setup environment" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "sEygpdc8Rp76", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "import torch\n", - "import torch.nn as nn\n", - "from torch.utils.data import DataLoader\n", - "from torchvision import datasets, transforms\n", - "import numpy as np\n", - "import itertools\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "RcX7J9vVRp76" - }, - "source": [ - "### 1.2 Define network and SNN parameters\n", - "We will use a 784-1000-10 FCN architecture for a sequence of 25 time steps.\n", - "\n", - "* `alpha` is the decay rate of the synaptic current of a neuron\n", - "* `beta` is the decay rate of the membrane potential of a neuron" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "bKEj2hucRp77", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Network Architecture\n", - "num_inputs = 32*32\n", - "num_hidden = 1000\n", - "num_outputs = 10\n", - "\n", - "# Training Parameters\n", - "batch_size=128\n", - "data_path='/tmp/data/mnist'\n", - "\n", - "# Temporal Dynamics\n", - "num_steps = 25\n", - "time_step = 1e-3\n", - "tau_mem = 3e-3\n", - "tau_syn = 2.2e-3\n", - "alpha = float(np.exp(-time_step/tau_syn))\n", - "beta = float(np.exp(-time_step/tau_mem))\n", - "\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "id": "agBKaYiIQxbS" - }, - "outputs": [], - "source": [ - "alpha = 0.8\n", - "beta = 0.9" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "gM_hcwDIRp78", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.3 Download MNIST Dataset\n", - "To see how to construct a validation set, refer to Tutorial 1." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 120, - "referenced_widgets": [ - "feb965fc6abb4ee9bc9dbb5ef3b9723c", - "1a99d740897c4ea09c6f33ce3a4a3230", - "82d6d6dfb0c845ec99c25f57e6c5d9d2", - "c03ab1b2c81149feb0c1551831a6a909", - "f74b5621630e458aa55562f4ae2f36f8", - "f999b8f0ade1483c83e5c2f047bad551", - "e2f3ff56de4e424bb8e2f09a9976bf5b", - "fe66ac83ad074efbbc802309b16e08a3" - ] - }, - "id": "0xwYb15xRp79", - "outputId": "fd8cb868-a8c3-45a1-a43a-4aad7992884a", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to /data/mnist/cifar-10-python.tar.gz\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "feb965fc6abb4ee9bc9dbb5ef3b9723c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(FloatProgress(value=0.0, max=170498071.0), HTML(value='')))" - ] - }, - "metadata": { - "tags": [] - }, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Extracting /data/mnist/cifar-10-python.tar.gz to /data/mnist\n", - "Files already downloaded and verified\n" - ] - } - ], - "source": [ - "# Define a transform\n", - "transform = transforms.Compose([\n", - " transforms.Grayscale(),\n", - " transforms.Resize((32, 32)),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))])\n", - "\n", - "mnist_train = datasets.CIFAR10(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.CIFAR10(data_path, train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "DIg3vLmURp7-", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.4 Create DataLoaders" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "K4DF-odMRp7_", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "pD4Dw-RoRp7_", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 2. Define Network\n", - "snnTorch treats neurons as activations with recurrent connections. This allows for smooth integration with PyTorch.\n", - "There are a few useful neuron models and surrogate gradient functions which approximate the gradient of spikes.\n", - "\n", - "Our network will use one type of neuron model and one surrogate gradient:\n", - "1. `snntorch.Stein` is a basic leaky integrate and fire (LIF) neuron. Specifically, it assumes instantaneous rise times for synaptic current and membrane potential.\n", - "2. `snntorch.FastSigmoidSurrogate` defines separate forward and backward functions. The forward function is a Heaviside step function for spike generation. The backward function is the derivative of a fast sigmoid function, to ensure continuous differentiability.\n", - "The `FastSigmoidSurrogate` function has been adapted from:\n", - "\n", - ">Neftci, E. O., Mostafa, H., and Zenke, F. (2019) Surrogate Gradient Learning in Spiking Neural Networks. https://arxiv.org/abs/1901/09948" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "id": "Sf9RdE9jRp8A", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# from snntorch import surrogate\n", - "#\n", - "# spike_grad = surrogate.FastSigmoid.apply\n", - "# snn.slope = 50" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "Og9e57W0Rp8B", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "The surrogate is passed to `spike_grad` and overrides the default gradient of the Heaviside step function.\n", - "If we did not override the default gradient, (zero everywhere, except for $x=1$ where it is technically infinite but clipped to 1 here), then learning would not take place for as long as the neuron was not emitting post-synaptic spikes.\n", - "\n", - "`snn.slope` defines the slope of the backward surrogate.\n", - "\n", - "TO-DO: Include visualisation." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "Yo-x48mARp8C", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we can define our spiking neural network (SNN).\n", - "Creating an instance of the `Stein` neuron requires two arguments and two optional arguments:\n", - "1. $I_{syn}$ decay rate, $\\alpha$,\n", - "2. $V_{mem}$ decay rate, $\\beta$,\n", - "3. the surrogate spiking function, `spike_grad` (*default*: the gradient of the Heaviside function), and\n", - "4. the threshold for spiking, (*default*: 1.0).\n", - "\n", - "snnTorch treats the LIF neuron as a recurrent activation. Therefore, it requires initialization of its internal states.\n", - "For each layer, we initialize the synaptic current `syn1` and `syn2`, the membrane potential `mem1` and `mem2`, and the post-synaptic spikes `spk1` and `spk2` to zero.\n", - "A class method `init_stein` will take care of this.\n", - "\n", - "For rate coding, the final layer of spikes and membrane potential are used to determine accuracy and loss, respectively.\n", - "So their historical values are recorded in `spk2_rec` and `mem2_rec`.\n", - "\n", - "Keep in mind, the dataset we are using is just static MNIST. I.e., it is *not* time-varying.\n", - "Therefore, we pass the same MNIST sample to the input at each time step.\n", - "This is handled in the line `cur1 = self.fc1(x)`, where `x` is the same input over the whole for-loop." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "id": "P6RHCnXMRp8D", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define Network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # initialize layers\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Stein(alpha=alpha, beta=beta)\n", - "\n", - " def forward(self, x):\n", - " spk1, syn1, mem1 = self.lif1.init_stein(batch_size, num_hidden)\n", - " spk2, syn2, mem2 = self.lif2.init_stein(batch_size, num_outputs)\n", - "\n", - " spk2_rec = []\n", - " mem2_rec = []\n", - "\n", - " for step in range(num_steps):\n", - " cur1 = self.fc1(x)\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = self.fc2(spk1)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - "\n", - " spk2_rec.append(spk2)\n", - " mem2_rec.append(mem2)\n", - "\n", - " return torch.stack(spk2_rec, dim=0), torch.stack(mem2_rec, dim=0)\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "386KNHG7Rp8E", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 3. Training\n", - "Time for training! Let's first define a couple of functions to print out test/train accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "id": "cOKKbUnDRp8F", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " output, _ = net(data.view(batch_size, -1))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(data_it, targets_it, train=True)\n", - " print_batch_accuracy(testdata_it, testtargets_it, train=False)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "mZqfCe0KRp8J", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.1 Optimizer & Loss\n", - "* *Output Activation*: We'll apply the softmax function to the membrane potentials of the output layer, rather than the spikes.\n", - "* *Loss*: This will then be used to calculate the negative log-likelihood loss.\n", - "By encouraging the membrane of the correct neuron class to reach the threshold, we expect that neuron will fire more frequently.\n", - "The loss could be applied to the spike count as well, but the membrane is continuous whereas spike count is discrete.\n", - "* *Optimizer*: The Adam optimizer is used for weight updates.\n", - "* *Accuracy*: Accuracy is measured by counting the spikes of the output neurons. The neuron that fires the most frequently will be our predicted class." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "id": "UMZ-4uGlRp8K", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "UerqLkd1Rp8K", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.2 Training Loop\n", - "Now just sit back, relax, and wait for convergence." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 1000 - }, - "id": "76CEHI2xRp8L", - "outputId": "e29c7e0e-4504-4d08-fdcf-70ab371b0996", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 2, Minibatch 70\n", - "Train Set Loss: 53.006282806396484\n", - "Test Set Loss: 55.81739807128906\n", - "Train Set Accuracy: 0.2265625\n", - "Test Set Accuracy: 0.21875\n", - "\n", - "\n", - "Epoch 2, Minibatch 120\n", - "Train Set Loss: 50.85497283935547\n", - "Test Set Loss: 50.894222259521484\n", - "Train Set Accuracy: 0.265625\n", - "Test Set Accuracy: 0.3125\n", - "\n", - "\n", - "Epoch 2, Minibatch 170\n", - "Train Set Loss: 49.45563507080078\n", - "Test Set Loss: 50.29939651489258\n", - "Train Set Accuracy: 0.3671875\n", - "Test Set Accuracy: 0.2578125\n", - "\n", - "\n", - "Epoch 2, Minibatch 220\n", - "Train Set Loss: 52.987083435058594\n", - "Test Set Loss: 51.679054260253906\n", - "Train Set Accuracy: 0.2109375\n", - "Test Set Accuracy: 0.234375\n", - "\n", - "\n", - "Epoch 2, Minibatch 270\n", - "Train Set Loss: 49.68134307861328\n", - "Test Set Loss: 52.56451416015625\n", - "Train Set Accuracy: 0.3359375\n", - "Test Set Accuracy: 0.234375\n", - "\n", - "\n", - "Epoch 2, Minibatch 320\n", - "Train Set Loss: 54.24823760986328\n", - "Test Set Loss: 51.5257682800293\n", - "Train Set Accuracy: 0.25\n", - "Test Set Accuracy: 0.25\n", - "\n", - "\n", - "Epoch 2, Minibatch 370\n", - "Train Set Loss: 50.897029876708984\n", - "Test Set Loss: 49.811622619628906\n", - "Train Set Accuracy: 0.2890625\n", - "Test Set Accuracy: 0.34375\n", - "\n", - "\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "ignored", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0;31m# Gradient calculation\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[0moptimizer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mzero_grad\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 25\u001b[0;31m \u001b[0mloss_val\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbackward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mretain_graph\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 27\u001b[0m \u001b[0;31m# Weight Update\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/torch/tensor.py\u001b[0m in \u001b[0;36mbackward\u001b[0;34m(self, gradient, retain_graph, create_graph, inputs)\u001b[0m\n\u001b[1;32m 243\u001b[0m \u001b[0mcreate_graph\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcreate_graph\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 244\u001b[0m inputs=inputs)\n\u001b[0;32m--> 245\u001b[0;31m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mautograd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbackward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgradient\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mretain_graph\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcreate_graph\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0minputs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 246\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 247\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mregister_hook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/torch/autograd/__init__.py\u001b[0m in \u001b[0;36mbackward\u001b[0;34m(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)\u001b[0m\n\u001b[1;32m 145\u001b[0m Variable._execution_engine.run_backward(\n\u001b[1;32m 146\u001b[0m \u001b[0mtensors\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgrad_tensors_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mretain_graph\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcreate_graph\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 147\u001b[0;31m allow_unreachable=True, accumulate_grad=True) # allow_unreachable flag\n\u001b[0m\u001b[1;32m 148\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 149\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(3):\n", - " minibatch_counter = 0\n", - " train_batch = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in train_batch:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " output, mem_rec = net(data_it.view(batch_size, -1))\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros((1), dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps: BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward(retain_graph=True)\n", - "\n", - " # Weight Update\n", - " nn.utils.clip_grad_norm_(net.parameters(), 1) # gradient clipping\n", - " optimizer.step()\n", - "\n", - " # Store loss history for future plotting\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set forward pass\n", - " test_output, test_mem_rec = net(testdata_it.view(batch_size, -1))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem_rec)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, testtargets_it)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "fdOuwCWHRp8L", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 4. Results\n", - "### 4.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 334 - }, - "id": "WJGSBq6zRp8M", - "outputId": "9c224143-2579-4708-88a8-e645e32ce289", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAE9CAYAAADaqWzvAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOydeZgU1fm2n6rq7hlWFURFMCJGI4gIEcF9/VQU9xB/xiW472LUSDQGQ4yJJEZUUEGigsEVURZZ3EBkFRxg2Pd9gBmGgdmnp7ur6vuj6lSdqjrVXd0zPTPAe18XFz1d26mlz3nq3Y6k67oOgiAIgiAIIuvIjd0AgiAIgiCIwwUSXgRBEARBEA0ECS+CIAiCIIgGgoQXQRAEQRBEA0HCiyAIgiAIooEg4UUQBEEQBNFAhBq7AUE4+uij0alTp8ZuBkEQBEEQREq2bduGffv2CZcdFMKrU6dOyMvLa+xmEARBEARBpKRXr16+y8jVSBAEQRAE0UCQ8CIIgiAIgmggSHgRBEEQBEE0EAdFjBdBEARBEHUnHo+joKAA0Wi0sZtySJCbm4uOHTsiHA4H3oaEF0EQBEEcJhQUFKBVq1bo1KkTJElq7OYc1Oi6jpKSEhQUFOCkk04KvB25GgmCIAjiMCEajaJt27YkuuoBSZLQtm3btK2HJLwIgiAI4jCCRFf9kcm1JFcjQRAEQRANQklJCS6//HIAQGFhIRRFQbt27QAAixcvRiQSSbr97NmzEYlEcN5553mWjR07Fnl5eXjzzTfrv+H1CAkvgiAIgiAahLZt2yI/Px8AMGTIELRs2RJ//OMfA28/e/ZstGzZUii8DhbI1QhgfWEFPl60A7GE1thNIQiCIIjDiiVLluDiiy/GWWedhauuugp79uwBAAwfPhxdu3ZF9+7dceutt2Lbtm0YNWoUXnvtNfTo0QNz584NtP9hw4ahW7du6NatG15//XUAQFVVFfr164czzzwT3bp1w2effQYAePbZZ61jpiMI04EsXgDmb9qHF6euQb8z2iMSIi1KEARBEA2Brut4/PHHMXnyZLRr1w6fffYZnn/+ebz//vsYOnQotm7dipycHJSWluLII4/EQw89lJaVbMmSJRgzZgwWLVoEXdfRp08fXHzxxdiyZQuOP/54TJs2DQBQVlaGkpISTJw4EevWrYMkSSgtLc3KOZPwAqDIRnBcQiOLF0EQBHF48LevVmPN7vJ63WfX41vjr9edHnj92tparFq1CldccQUAQFVVtG/fHgDQvXt33H777bjxxhtx4403ZtSeefPm4aabbkKLFi0AADfffDPmzp2Lvn374umnn8af/vQnXHvttbjwwguRSCSQm5uLe++9F9deey2uvfbajI6ZCjLvwBZeqq43cksIgiAI4vBB13WcfvrpyM/PR35+PlauXIlvv/0WADBt2jQ8+uijWLp0Kc4++2wkEol6O+6pp56KpUuX4owzzsBf/vIXvPjiiwiFQli8eDH69++PqVOnom/fvvV2PB6yeIETXhoJL4IgCOLwIB3LVLbIyclBcXExFi5ciHPPPRfxeBwbNmxAly5dsHPnTlx66aW44IIL8Omnn6KyshKtWrVCeXlwK92FF16Iu+66C88++yx0XcfEiRMxbtw47N69G23atMEdd9yBI488Eu+++y4qKytRXV2Na665Bueffz46d+6clXMm4QXO1aiS8CIIgiCIhkKWZUyYMAEDBw5EWVkZEokE/vCHP+DUU0/FHXfcgbKyMui6joEDB+LII4/Eddddh/79+2Py5MkYMWIELrzwQsf+xo4di0mTJll///TTT7jrrrvQu3dvAMB9992Hnj174ptvvsEzzzwDWZYRDocxcuRIVFRU4IYbbkA0GoWu6xg2bFhWzlnS9abvX+vVqxfy8vKytv8vlxbgqfHL8eMzl+DEti2ydhyCIAiCaEzWrl2LLl26NHYzDilE1zSZbqEYL/DB9U1egxIEQRAEcRBDwgu28NJIeBEEQRAEkUVIeAEIkcWLIAiCIIgGgIQXAFmirEaCIAiCILIPCS8AIYWEF0EQBEEQ2YeEFwBFNi4DuRoJgiAIgsgmJLwAKORqJAiCIIisU1JSgh49eqBHjx447rjj0KFDB+vvWCyWdNu8vDwMHDgwreN16tQJ+/btq0uT6x0qoAqqXE8QBEEQDUHbtm2Rn58PABgyZIhnwutEIoFQSCxNevXqhV69ejVIO7MJWbxAwosgCIIgGou77roLDz30EPr06YNBgwZh8eLFOPfcc9GzZ0+cd955WL9+PQBg9uzZ1sTVQ4YMwT333INLLrkEnTt3xvDhwwMfb9u2bbjsssvQvXt3XH755dixYwcA4PPPP0e3bt1w5pln4qKLLgIArF69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", - "text/plain": [ - "
" - ] - }, - "metadata": { - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Test Loss\", \"Train Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "h7xb37iHRp8N", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 4.2 Test Set Accuracy\n", - "This function just iterates over all minibatches to obtain a measure of accuracy over the full 10,000 samples in the test set." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 0 - }, - "id": "R1ReGuNURp8N", - "outputId": "562404c5-2281-4741-cf8a-8b7119ecb839", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total correctly classified test set images: 2923/10000\n", - "Test Set Accuracy: 29.23%\n" - ] - } - ], - "source": [ - "total = 0\n", - "correct = 0\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " # If current batch matches batch_size, just do the usual thing\n", - " if images.size()[0] == batch_size:\n", - " outputs, _ = net(images.view(batch_size, -1))\n", - "\n", - " # If current batch does not match batch_size (e.g., is the final minibatch),\n", - " # modify batch_size in a temp variable and restore it at the end of the else block\n", - " else:\n", - " temp_bs = batch_size\n", - " batch_size = images.size()[0]\n", - " outputs, _ = net(images.view(images.size()[0], -1))\n", - " batch_size = temp_bs\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += labels.size(0)\n", - " correct += (predicted == labels).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "zNJjSKATRp8N", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Voila! That's it for static MNIST." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "2qUxjHwBRp8f", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 5. Spiking MNIST\n", - "Part of the appeal of SNNs is their ability to handle time-varying spiking data. So let's use rate-coding to convert MNIST into spiking MNIST using the `spikegen` module in the previous tutorial, and train our network with that instead." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "id": "8K7_C-2rRp8g", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from snntorch import spikegen\n", - "\n", - "# MNIST to spiking-MNIST\n", - "spike_data, spike_targets = spikegen.rate(data_it, targets_it, num_steps=num_steps)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "BvaNfns_Rp8h", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 5.1 Visualiser\n", - "Just so you're damn sure it's a spiking input." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "ll2D4jtdRp8i", - "outputId": "616f69c3-e838-4503-d3aa-af41a2f903d7", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: celluloid in /usr/local/lib/python3.7/dist-packages (0.2.0)\n", - "Requirement already satisfied: matplotlib in /usr/local/lib/python3.7/dist-packages (from celluloid) (3.2.2)\n", - "Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->celluloid) (2.4.7)\n", - "Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->celluloid) (1.3.1)\n", - "Requirement already satisfied: numpy>=1.11 in /usr/local/lib/python3.7/dist-packages (from matplotlib->celluloid) (1.19.5)\n", - "Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->celluloid) (2.8.1)\n", - "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib->celluloid) (0.10.0)\n", - "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.1->matplotlib->celluloid) (1.15.0)\n" - ] - } - ], - "source": [ - "!pip install celluloid # matplotlib animations made easy" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "mgpzXVbGRpm1", - "outputId": "e315266d-e073-4f2e-9d08-72dc72d91e6c" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([128, 1, 32, 32])" - ] - }, - "execution_count": 39, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - } - ], - "source": [ - "data_it.size()" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 283 - }, - "id": "ryIOfAa0VveY", - "outputId": "8fe6b9ab-198f-457c-c9a4-b6a3a10790e3" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 46, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots()\n", - "ax.imshow(data_it[0, 0].cpu(), cmap='plasma')" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 540 - }, - "id": "cJQ0XUAZRp8i", - "outputId": "0920f979-23d2-4dd5-f039-8600aeeb3464", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 38, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOcAAADnCAYAAADl9EEgAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAACtklEQVR4nO3TMQEAIAzAMMC/52GAnx6Jgj7dM7OAnvM7AHgzJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNClDkhypwQZU6IMidEmROizAlR5oQoc0KUOSHKnBBlTogyJ0SZE6LMCVHmhChzQpQ5IcqcEGVOiDInRJkToswJUeaEKHNC1AVcegTL+uSnUAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "from celluloid import Camera\n", - "from IPython.display import HTML\n", - "\n", - "# Animator\n", - "spike_data_sample = spike_data.unsqueeze(2)[:, 0, 0].cpu()\n", - "\n", - "fig, ax = plt.subplots()\n", - "camera = Camera(fig)\n", - "plt.axis('off')\n", - "\n", - "for step in range(num_steps):\n", - " im = ax.imshow(spike_data_sample[step, :, :].squeeze(0), cmap='plasma')\n", - " camera.snap()\n", - "\n", - "# interval=40 specifies 40ms delay between frames\n", - "a = camera.animate(interval=40)\n", - "HTML(a.to_html5_video())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "T-dMHLBbRp8k", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "print(spike_targets[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "I63CROTaRp8k", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 6. Define Network\n", - "The network is the same as before. The one difference is that the for-loop iterates through the first dimension of the input:\n", - "`cur1 = self.fc1(x[step])`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "a_elvRYIRp8k", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "spike_grad = snn.FastSigmoidSurrogate.apply\n", - "snn.slope = 50 # The lower the slope, the smoother the gradient\n", - "\n", - "# Define Network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # Initialize layers\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - "\n", - " def forward(self, x):\n", - " # Initialize hidden states + output spike at t=0\n", - " spk1, syn1, mem1 = self.lif1.init_stein(batch_size, num_hidden)\n", - " spk2, syn2, mem2 = self.lif2.init_stein(batch_size, num_outputs)\n", - "\n", - " spk2_rec = []\n", - " mem2_rec = []\n", - "\n", - " for step in range(num_steps):\n", - " cur1 = self.fc1(x[step])\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = self.fc2(spk1)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - "\n", - " spk2_rec.append(spk2)\n", - " mem2_rec.append(mem2)\n", - "\n", - " return torch.stack(spk2_rec, dim=0), torch.stack(mem2_rec, dim=0)\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "E8utR7I9Rp8l", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 7. Training\n", - "We make a slight modification to our print-out functions to handle the new first dimension of the input:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "FlkQt3skRp8l", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " output, _ = net(data.view(num_steps, batch_size, -1))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(spike_data, spike_targets, train=True)\n", - " print_batch_accuracy(test_spike_data, test_spike_targets, train=False)\n", - " print(\"\\n\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "vbve35sDRp8l", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 7.1 Optimizer & Loss\n", - "We'll keep our optimizer and loss the exact same as the static MNIST case." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "10CH8RKHRp8l", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "Ef0TM6yIRp8m", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 7.2 Training Loop\n", - "The training loop is identical to the static MNIST case, but we pass each minibatch through `spikegen.rate` before running it through the feedforward network." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ISLzPpwvRp8m", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(3):\n", - " minibatch_counter = 0\n", - " data = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in data:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " # Spike generator\n", - " spike_data, spike_targets = spikegen.rate(data_it, targets_it, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " # Forward pass\n", - " output, mem_rec = net(spike_data.view(num_steps, batch_size, -1))\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros((1), dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps to perform BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient Calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward(retain_graph=True)\n", - " nn.utils.clip_grad_norm_(net.parameters(), 1)\n", - "\n", - " # Weight Update\n", - " optimizer.step()\n", - "\n", - " # Store Loss history\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set spike conversion\n", - " test_spike_data, test_spike_targets = spikegen.rate(testdata_it, testtargets_it, num_outputs=num_outputs,\n", - " num_steps=num_steps, gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " # Test set forward pass\n", - " test_output, test_mem_rec = net(test_spike_data.view(num_steps, batch_size, -1))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem_rec)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, test_spike_targets)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "EuRL61AnRp8m" - }, - "source": [ - "## 8. Spiking MNIST Results\n", - "### 8.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "fl5griHBRp8n", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Test Loss\", \"Train Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "IZMH5UOlRp8n" - }, - "source": [ - "### 8.2 Test Set Accuracy" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "eO2Krz1mRp8n", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "total = 0\n", - "correct = 0\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " # If current batch matches batch_size, just do the usual thing\n", - " if images.size()[0] == batch_size:\n", - " spike_test, spike_targets = spikegen.rate(images, labels, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " outputs, _ = net(spike_test.view(num_steps, batch_size, -1))\n", - "\n", - " # If current batch does not match batch_size (e.g., is the final minibatch),\n", - " # modify batch_size in a temp variable and restore it at the end of the else block\n", - " else:\n", - " temp_bs = batch_size\n", - " batch_size = images.size()[0]\n", - " spike_test, spike_targets = spikegen.rate(images, labels, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " outputs, _ = net(spike_test.view(num_steps, images.size()[0], -1))\n", - " batch_size = temp_bs\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += spike_targets.size(0)\n", - " correct += (predicted == spike_targets).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "ztM4ogAqRp8n" - }, - "source": [ - "That's all for now!\n", - "Next time, we'll introduce how to use spiking convolutional layers to improve accuracy." - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "name": "tutorial_2_FCN.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "1a99d740897c4ea09c6f33ce3a4a3230": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "82d6d6dfb0c845ec99c25f57e6c5d9d2": { - "model_module": "@jupyter-widgets/controls", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_f999b8f0ade1483c83e5c2f047bad551", - "max": 170498071, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_f74b5621630e458aa55562f4ae2f36f8", - "value": 170498071 - } - }, - "c03ab1b2c81149feb0c1551831a6a909": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_fe66ac83ad074efbbc802309b16e08a3", - "placeholder": "​", - "style": "IPY_MODEL_e2f3ff56de4e424bb8e2f09a9976bf5b", - "value": " 170499072/? [03:00<00:00, 946963.10it/s]" - } - }, - "e2f3ff56de4e424bb8e2f09a9976bf5b": { - "model_module": "@jupyter-widgets/controls", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "f74b5621630e458aa55562f4ae2f36f8": { - "model_module": "@jupyter-widgets/controls", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "initial" - } - }, - "f999b8f0ade1483c83e5c2f047bad551": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "fe66ac83ad074efbbc802309b16e08a3": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "feb965fc6abb4ee9bc9dbb5ef3b9723c": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_82d6d6dfb0c845ec99c25f57e6c5d9d2", - "IPY_MODEL_c03ab1b2c81149feb0c1551831a6a909" - ], - "layout": "IPY_MODEL_1a99d740897c4ea09c6f33ce3a4a3230" - } - } - } - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/legacy/FCN_truncatedfromscratch.ipynb b/examples/legacy/FCN_truncatedfromscratch.ipynb deleted file mode 100644 index 63244075..00000000 --- a/examples/legacy/FCN_truncatedfromscratch.ipynb +++ /dev/null @@ -1,1168 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# snnTorch Test - Truncated BPTT\n", - "### By Jason K. Eshraghian" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## Gradient-based Learning in Spiking Neural Networks" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Looking in indexes: https://test.pypi.org/simple/\n", - "Requirement already satisfied: snntorch in c:\\users\\jason\\dropbox\\repos\\snntorch (0.0.7)\n" - ] - } - ], - "source": [ - "# Install the test PyPi Distribution of snntorch\n", - "!pip install -i https://test.pypi.org/simple/ snntorch" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 1. Setting up the Static MNIST Dataset\n", - "### 1.1. Import packages and setup environment" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "import torch\n", - "import torch.nn as nn\n", - "from torch.utils.data import DataLoader\n", - "from torchvision import datasets, transforms\n", - "import numpy as np\n", - "import itertools\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### 1.2 Define network and SNN parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Network Architecture\n", - "num_inputs = 28*28\n", - "num_hidden = 1000\n", - "num_outputs = 10\n", - "\n", - "# Training Parameters\n", - "batch_size=128\n", - "data_path='/tmp/data/mnist'\n", - "\n", - "# Temporal Dynamics\n", - "num_steps = 25\n", - "time_step = 1e-3\n", - "tau_mem = 3e-3\n", - "tau_syn = 2.2e-3\n", - "alpha = float(np.exp(-time_step/tau_syn))\n", - "beta = float(np.exp(-time_step/tau_mem))\n", - "\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.3 Download MNIST Dataset" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define a transform\n", - "transform = transforms.Compose([\n", - " transforms.Resize((28, 28)),\n", - " transforms.Grayscale(),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))])\n", - "\n", - "mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.4 Create DataLoaders" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 2. Define Network\n", - "snnTorch treats neurons as activations with recurrent connections. This allows for smooth integration with PyTorch.\n", - "There are a few useful neuron models and surrogate gradient functions which approximate the gradient of spikes." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# from snntorch import surrogate\n", - "#\n", - "# spike_grad = surrogate.FastSigmoid.apply\n", - "# snn.slope = 50" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "The following network model no longer has a for-loop. That is performed in the feedforward pass along with optimization at each time step." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define Network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # initialize layers\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Stein(alpha=alpha, beta=beta)\n", - "\n", - " def forward(self, x, syn1, mem1, spk1, syn2, mem2):\n", - " cur1 = self.fc1(x)\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = self.fc2(spk1)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - " return syn1, mem1, spk1, syn2, mem2, spk2\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 3. Training\n", - "Time for training! Let's first define a couple of functions to print out test/train accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, syn1, mem1, spk1, syn2, mem2, train=False):\n", - " spk2_rec = []\n", - " for step in range(num_steps):\n", - " syn1, mem1, spk1, syn2, mem2, spk2 = net(data.view(batch_size, -1), syn1, mem1, spk1, syn2, mem2)\n", - " spk2_rec.append(spk2)\n", - " spk2_rec = torch.stack(spk2_rec, dim=0)\n", - " _, idx = spk2_rec.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer(syn1, mem1, spk1, syn2, mem2, test_syn1, test_mem1, test_spk1, test_syn2, test_mem2):\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(data_it, targets_it, syn1, mem1, spk1, syn2, mem2, train=True)\n", - " print_batch_accuracy(testdata_it, testtargets_it, test_syn1, test_mem1, test_spk1, test_syn2, test_mem2, train=False)\n", - " print(\"\\n\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.1 Training Loop" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 0, Minibatch 0\n", - "Train Set Loss: 1.7495200634002686\n", - "Test Set Loss: 1.9518725872039795\n", - "Train Set Accuracy: 0.4375\n", - "Test Set Accuracy: 0.3515625\n", - "\n", - "\n", - "Epoch 0, Minibatch 1\n", - "Train Set Loss: 1.3979709148406982\n", - "Test Set Loss: 1.5701136589050293\n", - "Train Set Accuracy: 0.625\n", - "Test Set Accuracy: 0.5234375\n", - "\n", - "\n", - "Epoch 0, Minibatch 2\n", - "Train Set Loss: 1.026261568069458\n", - "Test Set Loss: 1.1336437463760376\n", - "Train Set Accuracy: 0.78125\n", - "Test Set Accuracy: 0.703125\n", - "\n", - "\n", - "Epoch 0, Minibatch 3\n", - "Train Set Loss: 0.9658181667327881\n", - "Test Set Loss: 1.1051888465881348\n", - "Train Set Accuracy: 0.8046875\n", - "Test Set Accuracy: 0.6640625\n", - "\n", - "\n", - "Epoch 0, Minibatch 4\n", - "Train Set Loss: 0.7896397709846497\n", - "Test Set Loss: 0.905153751373291\n", - "Train Set Accuracy: 0.796875\n", - "Test Set Accuracy: 0.71875\n", - "\n", - "\n", - "Epoch 0, Minibatch 5\n", - "Train Set Loss: 0.6378481984138489\n", - "Test Set Loss: 0.9468184113502502\n", - "Train Set Accuracy: 0.875\n", - "Test Set Accuracy: 0.734375\n", - "\n", - "\n", - "Epoch 0, Minibatch 6\n", - "Train Set Loss: 0.746235191822052\n", - "Test Set Loss: 0.9170961976051331\n", - "Train Set Accuracy: 0.8203125\n", - "Test Set Accuracy: 0.78125\n", - "\n", - "\n", - "Epoch 0, Minibatch 7\n", - "Train Set Loss: 0.7137413024902344\n", - "Test Set Loss: 0.8111165761947632\n", - "Train Set Accuracy: 0.890625\n", - "Test Set Accuracy: 0.765625\n", - "\n", - "\n", - "Epoch 0, Minibatch 8\n", - "Train Set Loss: 0.47396788001060486\n", - "Test Set Loss: 0.7326197624206543\n", - "Train Set Accuracy: 0.90625\n", - "Test Set Accuracy: 0.75\n", - "\n", - "\n", - "Epoch 0, Minibatch 9\n", - "Train Set Loss: 0.7309470176696777\n", - "Test Set Loss: 0.6419931650161743\n", - "Train Set Accuracy: 0.8203125\n", - "Test Set Accuracy: 0.8828125\n", - "\n", - "\n", - "Epoch 0, Minibatch 10\n", - "Train Set Loss: 0.4580203890800476\n", - "Test Set Loss: 0.5942947864532471\n", - "Train Set Accuracy: 0.9296875\n", - "Test Set Accuracy: 0.796875\n", - "\n", - "\n", - "Epoch 0, Minibatch 11\n", - "Train Set Loss: 0.5444415807723999\n", - "Test Set Loss: 0.624632716178894\n", - "Train Set Accuracy: 0.8828125\n", - "Test Set Accuracy: 0.75\n", - "\n", - "\n", - "Epoch 0, Minibatch 12\n", - "Train Set Loss: 0.52439945936203\n", - "Test Set Loss: 0.4426622986793518\n", - "Train Set Accuracy: 0.890625\n", - "Test Set Accuracy: 0.8671875\n", - "\n", - "\n", - "Epoch 0, Minibatch 13\n", - "Train Set Loss: 0.4285009205341339\n", - "Test Set Loss: 0.467987596988678\n", - "Train Set Accuracy: 0.875\n", - "Test Set Accuracy: 0.875\n", - "\n", - "\n", - "Epoch 0, Minibatch 14\n", - "Train Set Loss: 0.5765503644943237\n", - "Test Set Loss: 0.5667204260826111\n", - "Train Set Accuracy: 0.8671875\n", - "Test Set Accuracy: 0.8515625\n", - "\n", - "\n", - "Epoch 0, Minibatch 15\n", - "Train Set Loss: 0.4504099488258362\n", - "Test Set Loss: 0.46107131242752075\n", - "Train Set Accuracy: 0.90625\n", - "Test Set Accuracy: 0.8828125\n", - "\n", - "\n", - "Epoch 0, Minibatch 16\n", - "Train Set Loss: 0.4541475772857666\n", - "Test Set Loss: 0.4454127252101898\n", - "Train Set Accuracy: 0.875\n", - "Test Set Accuracy: 0.8828125\n", - "\n", - "\n", - "Epoch 0, Minibatch 17\n", - "Train Set Loss: 0.47475460171699524\n", - "Test Set Loss: 0.5214759707450867\n", - "Train Set Accuracy: 0.8515625\n", - "Test Set Accuracy: 0.8515625\n", - "\n", - "\n", - "Epoch 0, Minibatch 18\n", - "Train Set Loss: 0.41250237822532654\n", - "Test Set Loss: 0.4996296167373657\n", - "Train Set Accuracy: 0.8984375\n", - "Test Set Accuracy: 0.8515625\n", - "\n", - "\n", - "Epoch 0, Minibatch 19\n", - "Train Set Loss: 0.40151408314704895\n", - "Test Set Loss: 0.4587456285953522\n", - "Train Set Accuracy: 0.875\n", - "Test Set Accuracy: 0.8984375\n", - "\n", - "\n", - "Epoch 0, Minibatch 20\n", - "Train Set Loss: 0.27906960248947144\n", - "Test Set Loss: 0.5087960362434387\n", - "Train Set Accuracy: 0.9453125\n", - "Test Set Accuracy: 0.859375\n", - "\n", - "\n", - "Epoch 0, Minibatch 21\n", - "Train Set Loss: 0.6448639035224915\n", - "Test Set Loss: 0.3771025836467743\n", - "Train Set Accuracy: 0.875\n", - "Test Set Accuracy: 0.875\n", - "\n", - "\n", - "Epoch 0, Minibatch 22\n", - "Train Set Loss: 0.42335885763168335\n", - "Test Set Loss: 0.6078567504882812\n", - "Train Set Accuracy: 0.9140625\n", - "Test Set Accuracy: 0.8671875\n", - "\n", - "\n", - "Epoch 0, Minibatch 23\n", - "Train Set Loss: 0.3247307240962982\n", - "Test Set Loss: 0.4790240526199341\n", - "Train Set Accuracy: 0.921875\n", - "Test Set Accuracy: 0.84375\n", - "\n", - "\n", - "Epoch 0, Minibatch 24\n", - "Train Set Loss: 0.47793853282928467\n", - "Test Set Loss: 0.5908436179161072\n", - "Train Set Accuracy: 0.8828125\n", - "Test Set Accuracy: 0.8671875\n", - "\n", - "\n", - "Epoch 0, Minibatch 25\n", - "Train Set Loss: 0.3392049968242645\n", - "Test Set Loss: 0.48888471722602844\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 68\u001b[0m \u001b[1;31m# Print test/train loss/accuracy\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 69\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mstep_counter\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m24\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 70\u001b[1;33m \u001b[0mtrain_printer\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_syn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_mem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_spk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_syn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_mem2\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m## THIS IS A JOKE\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 71\u001b[0m \u001b[0mstep_counter\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 72\u001b[0m \u001b[0mcounter\u001b[0m \u001b[1;33m+=\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m\u001b[0m in \u001b[0;36mtrain_printer\u001b[1;34m(syn1, mem1, spk1, syn2, mem2, test_syn1, test_mem1, test_spk1, test_syn2, test_mem2)\u001b[0m\n\u001b[0;32m 17\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"Train Set Loss: {loss_hist[counter]}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 18\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"Test Set Loss: {test_loss_hist[counter]}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 19\u001b[1;33m \u001b[0mprint_batch_accuracy\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata_it\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtargets_it\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrain\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mTrue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 20\u001b[0m \u001b[0mprint_batch_accuracy\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtestdata_it\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtesttargets_it\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_syn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_mem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_spk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_syn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_mem2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrain\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mFalse\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 21\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"\\n\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m\u001b[0m in \u001b[0;36mprint_batch_accuracy\u001b[1;34m(data, targets, syn1, mem1, spk1, syn2, mem2, train)\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[0mspk2_rec\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mstep\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnum_steps\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspk2\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnet\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mview\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mbatch_size\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 5\u001b[0m \u001b[0mspk2_rec\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mspk2\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[0mspk2_rec\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mspk2_rec\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdim\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\anaconda3\\envs\\py367\\lib\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 720\u001b[0m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 721\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 722\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 723\u001b[0m for hook in itertools.chain(\n\u001b[0;32m 724\u001b[0m \u001b[0m_global_forward_hooks\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, x, syn1, mem1, spk1, syn2, mem2)\u001b[0m\n\u001b[0;32m 12\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 13\u001b[0m \u001b[0mcur1\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfc1\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 14\u001b[1;33m \u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcur1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 15\u001b[0m \u001b[0mcur2\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfc2\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mspk1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 16\u001b[0m \u001b[0mspk2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcur2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem2\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\anaconda3\\envs\\py367\\lib\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 720\u001b[0m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 721\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 722\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 723\u001b[0m for hook in itertools.chain(\n\u001b[0;32m 724\u001b[0m \u001b[0m_global_forward_hooks\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Dropbox\\repos\\snntorch\\snntorch\\__init__.py\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, input_, syn, mem)\u001b[0m\n\u001b[0;32m 133\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minput_\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0msyn\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 134\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mhidden_init\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 135\u001b[1;33m \u001b[0mspk\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mreset\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfire\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 136\u001b[0m \u001b[0msyn\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0malpha\u001b[0m \u001b[1;33m*\u001b[0m \u001b[0msyn\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0minput_\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 137\u001b[0m \u001b[0mmem\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbeta\u001b[0m \u001b[1;33m*\u001b[0m \u001b[0mmem\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0msyn\u001b[0m \u001b[1;33m-\u001b[0m \u001b[0mreset\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Dropbox\\repos\\snntorch\\snntorch\\__init__.py\u001b[0m in \u001b[0;36mfire\u001b[1;34m(self, mem)\u001b[0m\n\u001b[0;32m 30\u001b[0m Returns spk and reset.\"\"\"\n\u001b[0;32m 31\u001b[0m \u001b[0mmem_shift\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mmem\u001b[0m \u001b[1;33m-\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mthreshold\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 32\u001b[1;33m \u001b[0mspk\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mspike_grad\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmem_shift\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdevice\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 33\u001b[0m \u001b[0mreset\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mzeros_like\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 34\u001b[0m \u001b[0mspk_idx\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mmem_shift\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Dropbox\\repos\\snntorch\\snntorch\\__init__.py\u001b[0m in \u001b[0;36mforward\u001b[1;34m(ctx, input_)\u001b[0m\n\u001b[0;32m 88\u001b[0m \u001b[0mctx\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_for_backward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput_\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 89\u001b[0m \u001b[0mout\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mzeros_like\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput_\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 90\u001b[1;33m \u001b[0mout\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0minput_\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m1.0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 91\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mout\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 92\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()\n", - "\n", - "test_data = itertools.cycle(test_loader)\n", - "\n", - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(5):\n", - " train_batch = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " minibatch_counter = 0\n", - " for data_it, targets_it in train_batch:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " # Test set iterator\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # initialization\n", - " spk1, syn1, mem1 = net.lif1.init_stein(batch_size, num_hidden)\n", - " spk2, syn2, mem2 = net.lif2.init_stein(batch_size, num_outputs)\n", - "\n", - " # test: initialization\n", - " test_spk1, test_syn1, test_mem1 = net.lif1.init_stein(batch_size, num_hidden)\n", - " test_spk2, test_syn2, test_mem2 = net.lif2.init_stein(batch_size, num_outputs)\n", - "\n", - " # training loop\n", - " step_counter = 0\n", - " for steps in range(num_steps):\n", - " syn1, mem1, spk1, syn2, mem2, spk2 = net(data_it.view(batch_size, -1), syn1, mem1, spk1, syn2, mem2)\n", - "\n", - " # loss p/timestep --- can try truncated approach too\n", - " log_p_y = log_softmax_fn(mem2) # mem2 = 128 x 10\n", - " loss_val = loss_fn(log_p_y, targets_it) # targets_it = 128\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Gradient calculation - detach states so gradient can flow\n", - " optimizer.zero_grad()\n", - " loss_val.backward()\n", - "\n", - " # Weight Update\n", - " # nn.utils.clip_grad_norm_(net.parameters(), 1) # gradient clipping\n", - " optimizer.step()\n", - "\n", - " # Detach for next update - test which of these variables don't have to be detached\n", - " syn1.detach_()\n", - " mem1.detach_()\n", - " spk1.detach_()\n", - " syn2.detach_()\n", - " mem2.detach_()\n", - "\n", - " # Test set forward pass\n", - " test_syn1, test_mem1, test_spk1, test_syn2, test_mem2, test_spk2 = net(testdata_it.view(batch_size, -1), test_syn1, test_mem1, test_spk1, test_syn2, test_mem2) ### WAY TOO MANY VARS\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem2)\n", - " loss_val_test = loss_fn(log_p_ytest, testtargets_it)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - "\n", - " # Print test/train loss/accuracy\n", - " if step_counter == 24:\n", - " train_printer(syn1, mem1, spk1, syn2, mem2, test_syn1, test_mem1, test_spk1, test_syn2, test_mem2) ## THIS IS A JOKE\n", - " step_counter += 1\n", - " counter +=1\n", - "\n", - " minibatch_counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 4. Results\n", - "### 4.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Train Loss\", \"Test Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 4.2 Test Set Accuracy\n", - "This function just iterates over all minibatches to obtain a measure of accuracy over the full 10,000 samples in the test set." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "total = 0\n", - "correct = 0\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " # If current batch matches batch_size, just do the usual thing\n", - " if images.size()[0] == batch_size:\n", - " outputs, _ = net(images.view(batch_size, -1))\n", - "\n", - " # If current batch does not match batch_size (e.g., is the final minibatch),\n", - " # modify batch_size in a temp variable and restore it at the end of the else block\n", - " else:\n", - " temp_bs = batch_size\n", - " batch_size = images.size()[0]\n", - " outputs, _ = net(images.view(images.size()[0], -1))\n", - " batch_size = temp_bs\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += labels.size(0)\n", - " correct += (predicted == labels).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Voila! That's it for static MNIST." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 5. Spiking MNIST\n", - "Part of the appeal of SNNs is their ability to handle time-varying spiking data. So let's use rate-coding to convert MNIST into spiking MNIST using the `spikegen` module in the previous tutorial, and train our network with that instead." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from snntorch import spikegen\n", - "\n", - "# MNIST to spiking-MNIST\n", - "spike_data, spike_targets = spikegen.rate(data_it, targets_it, num_outputs=num_outputs, num_steps=num_steps, gain=1,\n", - " offset=0, convert_targets=False, temporal_targets=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 5.1 Visualiser\n", - "Just so you're damn sure it's a spiking input." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "!pip install celluloid # matplotlib animations made easy" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from celluloid import Camera\n", - "from IPython.display import HTML\n", - "\n", - "# Animator\n", - "spike_data_sample = spike_data[:, 0, 0].cpu()\n", - "\n", - "fig, ax = plt.subplots()\n", - "camera = Camera(fig)\n", - "plt.axis('off')\n", - "\n", - "for step in range(num_steps):\n", - " im = ax.imshow(spike_data_sample[step, :, :], cmap='plasma')\n", - " camera.snap()\n", - "\n", - "# interval=40 specifies 40ms delay between frames\n", - "a = camera.animate(interval=40)\n", - "HTML(a.to_html5_video())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "print(spike_targets[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 6. Define Network\n", - "The network is the same as before. The one difference is that the for-loop iterates through the first dimension of the input:\n", - "`cur1 = self.fc1(x[step])`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "spike_grad = snn.FastSigmoidSurrogate.apply\n", - "snn.slope = 50 # The lower the slope, the smoother the gradient\n", - "\n", - "# Define Network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # Initialize layers\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - "\n", - " def forward(self, x):\n", - " # Initialize hidden states + output spike at t=0\n", - " spk1, syn1, mem1 = self.lif1.init_stein(batch_size, num_hidden)\n", - " spk2, syn2, mem2 = self.lif2.init_stein(batch_size, num_outputs)\n", - "\n", - " spk2_rec = []\n", - " mem2_rec = []\n", - "\n", - " for step in range(num_steps):\n", - " cur1 = self.fc1(x[step])\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = self.fc2(spk1)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - "\n", - " spk2_rec.append(spk2)\n", - " mem2_rec.append(mem2)\n", - "\n", - " return torch.stack(spk2_rec, dim=0), torch.stack(mem2_rec, dim=0)\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 7. Training\n", - "We make a slight modification to our print-out functions to handle the new first dimension of the input:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " output, _ = net(data.view(num_steps, batch_size, -1))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(spike_data, spike_targets, train=True)\n", - " print_batch_accuracy(test_spike_data, test_spike_targets, train=False)\n", - " print(\"\\n\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 7.1 Optimizer & Loss\n", - "We'll keep our optimizer and loss the exact same as the static MNIST case." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 7.2 Training Loop\n", - "The training loop is identical to the static MNIST case, but we pass each minibatch through `spikegen.rate` before running it through the feedforward network." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(3):\n", - " minibatch_counter = 0\n", - " data = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in data:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " # Spike generator\n", - " spike_data, spike_targets = spikegen.rate(data_it, targets_it, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " # Forward pass\n", - " output, mem_rec = net(spike_data.view(num_steps, batch_size, -1))\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros((1), dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps to perform BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient Calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward(retain_graph=True)\n", - " nn.utils.clip_grad_norm_(net.parameters(), 1)\n", - "\n", - " # Weight Update\n", - " optimizer.step()\n", - "\n", - " # Store Loss history\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set spike conversion\n", - " test_spike_data, test_spike_targets = spikegen.rate(testdata_it, testtargets_it, num_outputs=num_outputs,\n", - " num_steps=num_steps, gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " # Test set forward pass\n", - " test_output, test_mem_rec = net(test_spike_data.view(num_steps, batch_size, -1))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem_rec)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, test_spike_targets)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## 8. Spiking MNIST Results\n", - "### 8.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Test Loss\", \"Train Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### 8.2 Test Set Accuracy" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "total = 0\n", - "correct = 0\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " # If current batch matches batch_size, just do the usual thing\n", - " if images.size()[0] == batch_size:\n", - " spike_test, spike_targets = spikegen.rate(images, labels, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " outputs, _ = net(spike_test.view(num_steps, batch_size, -1))\n", - "\n", - " # If current batch does not match batch_size (e.g., is the final minibatch),\n", - " # modify batch_size in a temp variable and restore it at the end of the else block\n", - " else:\n", - " temp_bs = batch_size\n", - " batch_size = images.size()[0]\n", - " spike_test, spike_targets = spikegen.rate(images, labels, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " outputs, _ = net(spike_test.view(num_steps, images.size()[0], -1))\n", - " batch_size = temp_bs\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += spike_targets.size(0)\n", - " correct += (predicted == spike_targets).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "That's all for now!\n", - "Next time, we'll introduce how to use spiking convolutional layers to improve accuracy." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/legacy/TBPTT.ipynb b/examples/legacy/TBPTT.ipynb deleted file mode 100644 index 8a67c574..00000000 --- a/examples/legacy/TBPTT.ipynb +++ /dev/null @@ -1,1127 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# snnTorch - Tutorial 4\n", - "### By Jason K. Eshraghian" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Note: explanation is yet to be added. For now, only code is updated to show how to automatically implement TBPTT.\n", - "\n", - "# Truncated Backpropagation through time\n", - "In this tutorial, we'll use a convolutional neural network (CNN) to classify the MNIST dataset.\n", - "We will use the truncated backpropagation through time (BPTT) algorithm to do so. This tutorial is largely the same as tutorial 2, just with a different network architecture to show how to integrate convolutions with snnTorch.\n", - "\n", - "If running in Google Colab:\n", - "* Ensure you are connected to GPU by checking Runtime > Change runtime type > Hardware accelerator: GPU\n", - "* Next, install the Test PyPi distribution of snnTorch by clicking into the following cell and pressing `Shift+Enter`." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Looking in indexes: https://test.pypi.org/simple/\n", - "Requirement already satisfied: snntorch in c:\\users\\jason\\dropbox\\repos\\snntorch (0.0.7)\n" - ] - } - ], - "source": [ - "# Install the test PyPi Distribution of snntorch\n", - "!pip install -i https://test.pypi.org/simple/ snntorch" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 1. Setting up the Static MNIST Dataset\n", - "### 1.1. Import packages and setup environment" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "from snntorch import backprop as bp\n", - "import torch\n", - "import torch.nn as nn\n", - "import torch.nn.functional as F\n", - "from torch.utils.data import DataLoader\n", - "from torchvision import datasets, transforms\n", - "import numpy as np\n", - "import itertools\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### 1.2 Define network and SNN parameters\n", - "We will use a 2conv-2MaxPool-FCN architecture for a sequence of 25 time steps.\n", - "\n", - "* `alpha` is the decay rate of the synaptic current of a neuron\n", - "* `beta` is the decay rate of the membrane potential of a neuron" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Network Architecture\n", - "num_inputs = 28*28\n", - "num_hidden = 1000\n", - "num_outputs = 10\n", - "\n", - "# Training Parameters\n", - "batch_size=128\n", - "data_path='/tmp/data/mnist'\n", - "\n", - "# Temporal Dynamics\n", - "num_steps = 25\n", - "time_step = 1e-3\n", - "tau_mem = 4e-3\n", - "tau_syn = 3e-3\n", - "alpha = float(np.exp(-time_step/tau_syn))\n", - "beta = float(np.exp(-time_step/tau_mem))\n", - "\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.3 Download MNIST Dataset\n", - "To see how to construct a validation set, refer to Tutorial 1." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define a transform\n", - "transform = transforms.Compose([\n", - " transforms.Resize((28, 28)),\n", - " transforms.Grayscale(),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))])\n", - "\n", - "mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.4 Create DataLoaders" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 2. Define Network\n", - "snnTorch contains a series of neuron models and related functions to ease the training process.\n", - "Neurons are treated as activations with recurrent connections, and integrate smoothly with PyTorch's pre-existing layer functions.\n", - "* `snntorch.Stein` is a simple Leaky Integrate and Fire (LIF) neuron. Specifically, it uses Stein's model which assumes instantaneous rise times for synaptic current and membrane potential.\n", - "* `snntorch.FastSigmoidSurrogate` defines separate forward and backward functions. The forward function is a Heaviside step function for spike generation. The backward function is the derivative of a fast sigmoid function, to ensure continuous differentiability.\n", - "FSS is mostly derived from:\n", - "\n", - ">Neftci, E. O., Mostafa, H., and Zenke, F. (2019) Surrogate Gradient Learning in Spiking Neural Networks. https://arxiv.org/abs/1901/09948\n", - "\n", - "There are a few other surrogate gradient functions included.\n", - "`snn.slope` is a variable that defines the slope of the backward surrogate.\n", - "TO-DO: Include visualisation.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we can define our spiking neural network (SNN).\n", - "If you have already worked through Tutorial 2, you may wish to skip ahead.\n", - "\n", - "The init_hidden argument will initialize the hidden states & spike outputs as instance variables.\n", - "Although the forward method looks messier with the calls to these instance variables, it eliminates the need to detach all the variables from the computational graph manually." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # initialize layers\n", - " snn.LIF.clear_instances() # boilerplate\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, num_inputs=num_hidden, batch_size=batch_size, init_hidden=True)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Stein(alpha=alpha, beta=beta, num_inputs=num_outputs, batch_size=batch_size, init_hidden=True)\n", - "\n", - "\n", - " def forward(self, x):\n", - " cur1 = self.fc1(x)\n", - " self.lif1.spk1, self.lif1.syn1, self.lif1.mem1 = self.lif1(cur1, self.lif1.syn, self.lif1.mem)\n", - " cur2 = self.fc2(self.lif1.spk)\n", - " self.lif2.spk, self.lif2.syn, self.lif2.mem = self.lif2(cur2, self.lif2.syn, self.lif2.mem)\n", - "\n", - " return self.lif2.spk, self.lif2.mem\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 3. Training\n", - "Time for training! Let's define our train and test functions." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def train(net, device, train_loader, optimizer, criterion, epoch):\n", - " for batch_idx, (data, target) in enumerate(train_loader):\n", - " data, target = data.to(device), target.to(device)\n", - "\n", - " loss = bp.TBPTT(net, data, target, num_steps, batch_size, optimizer, criterion, K=K)\n", - " # loss = bp.BPTT(net, data, target, num_steps, batch_size, optimizer, criterion)\n", - "\n", - " if batch_idx % 20 == 0:\n", - " print(f\"Train Epoch: {epoch} [{batch_idx*len(data)}/{len(train_loader.dataset)}], \"\n", - " f\"Loss: {loss.item()}\")\n", - " loss_hist.append(loss.item()) # only recording at the end of each epoch\n", - "\n", - "\n", - "def test(net, device, test_loader, criterion):\n", - " net.eval()\n", - " test_loss = 0\n", - " correct = 0\n", - " with torch.no_grad():\n", - " for data, target in test_loader:\n", - " data, target = data.to(device), target.to(device)\n", - "\n", - " spk2_rec = []\n", - " snn.Stein.zeros_hidden() # reset hidden states to 0\n", - " if data.size()[0] == batch_size:\n", - " for step in range(num_steps):\n", - " spk2, mem2 = net(data.view(batch_size, -1))\n", - " spk2_rec.append(spk2)\n", - "\n", - " # Test Loss where batch=128; only calc on final time step\n", - " # log_p_ytest = log_softmax_fn(mem2)\n", - " test_loss += criterion(mem2, target)\n", - " # Test Acc where batch=128\n", - " _, idx = torch.stack(spk2_rec, dim=0).sum(dim=0).max(1) # predicted indexes\n", - " correct += sum((target == idx).cpu().numpy())\n", - " # print(correct)\n", - "\n", - " else: # Handle drop_last = False\n", - " temp_data = torch.zeros((batch_size, *(data[0].size())), dtype=dtype, device=device) # pad out temp_data now\n", - " temp_data[:(data.size()[0])] = data\n", - "\n", - " for step in range(num_steps):\n", - " spk2, mem2 = net(temp_data.view(batch_size, -1))\n", - " spk2_rec.append(spk2)\n", - "\n", - " # Test set loss - only calc on the final time-step\n", - " # log_p_ytest = log_softmax_fn(mem2[:data.size()[0]])\n", - " test_loss += criterion(mem2[:data.size()[0]], target)\n", - " # Test Acc where batch=128\n", - " _, idx = torch.stack(spk2_rec, dim=0).sum(dim=0).max(1) # predicted indexes\n", - " correct += sum((target == idx[:data.size()[0]]).cpu().numpy())\n", - "\n", - " test_loss_hist.append(test_loss.item())\n", - " test_acc = correct / len(test_loader.dataset)\n", - " print(f\"\\nTest set: Average loss: {(test_loss/(len(test_loader.dataset)/batch_size))}, Accuracy: [{correct}/{len(test_loader.dataset)}] ({(correct/len(test_loader.dataset))})\\n\"\n", - " f\"=====================\\n\")\n", - "\n", - " return test_loss, test_acc, spk2_rec" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.1 Optimizer & Loss\n", - "* *Output Activation*: We'll apply the softmax function to the membrane potentials of the output layer, rather than the spikes.\n", - "* *Loss*: This will then be used to calculate the negative log-likelihood loss.\n", - "By encouraging the membrane of the correct neuron class to reach the threshold, we expect that neuron will fire more frequently.\n", - "The loss could be applied to the spike count as well, but the membrane is continuous whereas spike count is discrete.\n", - "* *Optimizer*: The Adam optimizer is used for weight updates.\n", - "* *Accuracy*: Accuracy is measured by counting the spikes of the output neurons. The neuron that fires the most frequently will be our predicted class.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.2 Training Loop\n", - "Now just sit back, relax, and wait for convergence." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "========Trial: 0, Learning Rate: 0.0001\n", - "Train Epoch: 0 [0/60000], Loss: 87.99430084228516\n", - "Train Epoch: 0 [2560/60000], Loss: 31.979496002197266\n", - "Train Epoch: 0 [5120/60000], Loss: 20.729297637939453\n", - "Train Epoch: 0 [7680/60000], Loss: 21.22612762451172\n", - "Train Epoch: 0 [10240/60000], Loss: 17.037744522094727\n", - "Train Epoch: 0 [12800/60000], Loss: 16.702619552612305\n", - "Train Epoch: 0 [15360/60000], Loss: 15.043231010437012\n", - "Train Epoch: 0 [17920/60000], Loss: 15.852062225341797\n", - "Train Epoch: 0 [20480/60000], Loss: 18.419878005981445\n", - "Train Epoch: 0 [23040/60000], Loss: 15.934926986694336\n", - "Train Epoch: 0 [25600/60000], Loss: 14.167062759399414\n", - "Train Epoch: 0 [28160/60000], Loss: 13.726014137268066\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 38\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"========Trial: {i}, Learning Rate: {lr}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 39\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mepoch\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mepochs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 40\u001b[1;33m \u001b[0mtrain\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnet\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdevice\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrain_loader\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0moptimizer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcriterion\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 41\u001b[0m \u001b[0mtest_loss\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_acc\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0m_\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtest\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnet\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdevice\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtest_loader\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcriterion\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 42\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m\u001b[0m in \u001b[0;36mtrain\u001b[1;34m(net, device, train_loader, optimizer, criterion, epoch)\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[0mdata\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtarget\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdevice\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtarget\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdevice\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mloss\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mbp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mTBPTT\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnet\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtarget\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnum_steps\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mbatch_size\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0moptimizer\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcriterion\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mK\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mK\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 6\u001b[0m \u001b[1;31m# loss = bp.BPTT(net, data, target, num_steps, batch_size, optimizer, criterion)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 7\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Dropbox\\repos\\snntorch\\snntorch\\backprop.py\u001b[0m in \u001b[0;36mTBPTT\u001b[1;34m(net, data, target, num_steps, batch_size, optimizer, criterion, K)\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[0mloss_avg\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 31\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mstep\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnum_steps\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 32\u001b[1;33m \u001b[0mspk_out\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mmem_out\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnet\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mview\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mbatch_size\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 33\u001b[0m \u001b[0mloss\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcriterion\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmem_out\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtarget\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 34\u001b[0m \u001b[0mloss_trunc\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[0mloss\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\anaconda3\\envs\\py367\\lib\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 720\u001b[0m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 721\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 722\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 723\u001b[0m for hook in itertools.chain(\n\u001b[0;32m 724\u001b[0m \u001b[0m_global_forward_hooks\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, x)\u001b[0m\n\u001b[0;32m 13\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 14\u001b[0m \u001b[0mcur1\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfc1\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 15\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mspk1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem1\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcur1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 16\u001b[0m \u001b[0mcur2\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfc2\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mspk\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 17\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mspk\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcur2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlif2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\anaconda3\\envs\\py367\\lib\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 720\u001b[0m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 721\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 722\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 723\u001b[0m for hook in itertools.chain(\n\u001b[0;32m 724\u001b[0m \u001b[0m_global_forward_hooks\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Dropbox\\repos\\snntorch\\snntorch\\__init__.py\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, input_, syn, mem)\u001b[0m\n\u001b[0;32m 141\u001b[0m \u001b[1;31m# intended for truncated-BPTT where instance variables are hidden states\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 142\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mhidden_init\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 143\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mspk\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mreset\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfire\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 144\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0malpha\u001b[0m \u001b[1;33m*\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0minput_\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 145\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbeta\u001b[0m \u001b[1;33m*\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmem\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msyn\u001b[0m \u001b[1;33m-\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mreset\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Dropbox\\repos\\snntorch\\snntorch\\__init__.py\u001b[0m in \u001b[0;36mfire\u001b[1;34m(self, mem)\u001b[0m\n\u001b[0;32m 33\u001b[0m \u001b[0mreset\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mzeros_like\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 34\u001b[0m \u001b[0mspk_idx\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mmem_shift\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 35\u001b[1;33m \u001b[0mreset\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mspk_idx\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mones_like\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mmem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mspk_idx\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 36\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mspk\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mreset\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 37\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], - "source": [ - "no_trials = 1\n", - "# lr_values = [1e-3, 5e-4, 1e-4] # these values are good\n", - "lr_values = [1e-4]\n", - "batch_size = 128\n", - "data_path = '/data/mnist'\n", - "# subset = 50 # can remove this line in Colab\n", - "num_steps = 25\n", - "epochs = 1\n", - "betas = (0.9, 0.999)\n", - "K = 25 # number of time steps to accumulate over -- right now I'm using BPTT anyway so this is ignored\n", - "SAVE_GOOGLE_COLAB = False\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", - "\n", - "transform = transforms.Compose([\n", - " transforms.Resize((28, 28)),\n", - " transforms.Grayscale(),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))\n", - "])\n", - "mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)\n", - "# mnist_train = data_subset(mnist_train, subset) # reduce dataset by x100 - can remove this line in Colab\n", - "# mnist_test = data_subset(mnist_test, subset)\n", - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "# Adam\n", - "# df = pd.DataFrame(columns=['lr', 'epoch', 'test_set_loss', 'test_set_accuracy'])\n", - "for i in range(no_trials):\n", - " for lr in lr_values:\n", - " net = Net().to(device)\n", - " optimizer = torch.optim.Adam(net.parameters(), lr=lr, betas=betas)\n", - " # log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - " criterion = nn.CrossEntropyLoss() # note: CrossEntropy dims must be B x num_classes. Can increase dimensionality, read docs.\n", - "\n", - " loss_hist = []\n", - " test_loss_hist = []\n", - " print(f\"========Trial: {i}, Learning Rate: {lr}\")\n", - " for epoch in range(epochs):\n", - " train(net, device, train_loader, optimizer, criterion, epoch)\n", - " test_loss, test_acc, _ = test(net, device, test_loader, criterion)\n", - "\n", - " # df = df.append(\n", - " # {'trial': i, 'lr': lr, 'epoch': epoch, 'test_set_loss': test_loss.item(),\n", - " # 'test_set_accuracy': test_acc}, ignore_index=True)\n", - " # df.to_csv('Adam_BPTT2.csv', index=False)\n", - " # if SAVE_GOOGLE_COLAB:\n", - " # shutil.copy(\"Adam_BPTT.csv\", \"/content/Adam_BPTT.csv\")\n", - "\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 4. Results\n", - "### 4.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "data": { - "image/png": "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9276A2lDmuucnLdu3Tpt3rxZb7/9NuHQQJf7GV9qzdmzZy+7LRqmMedFkiorKzVp0iRNnjxZEydObJmh27jGnJNXXnlFOTk52rp1q86cOaOKigpNmTJFL774YovNb6ygvfsOrcKjjz56wRvm582bV2tNZWWldeONN1oHDx6seYPq/v37a6274YYb+BBDE2jsOcnNzbWioqKsY8eOtejcbU0gf+83b958wRuzBw8eHPC2aJjGnJdz585ZDzzwgPXwww8HYfK2qzHn5PveffddPsRQDwTcFe748ePWnXfeaTmdTuvOO++0SktLLcuyLL/fb919990167Zs2WJFRERY4eHh1pNPPlnncxFwTaOx5+RHP/qR5XA4rNjYWCs2NtZ68MEHW/wY2oq6fsYrV660Vq5caVnWd0Hw61//2goPD7diYmKsf/7zn5fcFk2joeflvffesyRZ/fv3r/n3sWXLlqAdR1vSmH8r5xFw9cM3MQAAABiGj3wAAAAYhoADAAAwDAEHAABgGAIOAADAMAQcAACAYQg4APivdu3aKS4uruZ/GRkZTfbchYWFiomJabLnA3Bl45sYAOC/OnXqpH379gV7DAC4LK7AAcBlhIWFacGCBRoyZIiGDBmi//znP5KkL7/8UvHx8XK5XIqPj9ehQ4ckSUePHtWECRMUGxur2NhY/eMf/5AkVVdX61e/+pWio6M1evRoffPNN0E7JgBmI+AA4L+++eabC15C3bRpU81j3bp1U15enmbPnq05c+ZIkmbPnq2UlBR98sknmjx5stLT0yVJ6enpGjlypD7++GPt3btX0dHRkiSv16tZs2bp008/1bXXXqtXX321xY8RQNvANzEAwH916dJFp06dqnV/WFiY3nnnHYWHh6uyslK9e/dWaWmpevTooeLiYrVv316VlZWy2+06fvy4evbsKZ/Ppw4dOtQ8R2FhoRISEuT1eiVJTz31lCorK/X73/++xY4PQNvBFTgACIDNZqvzzxdbU5fvB127du1UVVXVNMMBuOIQcAAQgPMvp27atEm33nqrJGn48OHauHGjJOmll17S7bffLkmKj4/XypUrJX33vreKioogTAygLeNTqADwX+ffA3fe2LFja36VyLfffquhQ4fq3Llz2rBhgyRp+fLlmjZtmp5++mn17NlTa9askSQtW7ZMaWlp+stf/qJ27dpp5cqVstvtLX48ANou3gMHAJcRFhYmj8ejHj16BHsUAJDES6gAAADG4QocAACAYbgCBwAAYBgCDgAAwDAEHAAAgGEIOAAAAMMQcAAAAIYh4AAAAAzz/wFNdUgoDnnG6wAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Test Loss\", \"Train Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "That's it for static MNIST! Now let's use ``spikeplot`` to watch in real time how the output layer responds to a few different samples." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Test set: Average loss: 0.15009622275829315, Accuracy: [123/128] (0.9609375)\n", - "=====================\n", - "\n" - ] - } - ], - "source": [ - "from snntorch import utils\n", - "# Let's just test on one single batch\n", - "mnist_anim = datasets.MNIST(data_path, train=False, download=True, transform=transform)\n", - "mnist_anim = utils.data_subset(mnist_anim, subset=78)\n", - "anim_loader = DataLoader(mnist_anim, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "# Pass anim_loader into test\n", - "_, _, spk_rec = test(net, device, anim_loader, criterion)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data Size: torch.Size([1, 28, 28])\n", - "Target: 6\n" - ] - } - ], - "source": [ - "print(f\"Data Size: {anim_loader.dataset[0][0].size()}\")\n", - "print(f\"Target: {anim_loader.dataset[22][1]}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([25, 10])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# spk_rec is a T x B x N_outputs tensor\n", - "# but we only want a single sample, so T x N tensor is input to snnboard\n", - "torch.stack(spk_rec, dim=0)[:, 0, :].size()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total number of spikes at t=T for a single sample:\n", - " tensor([0., 0., 0., 0., 0., 0., 8., 0., 0., 0.])\n" - ] - } - ], - "source": [ - "spk_results = torch.stack(spk_rec, dim=0)[:, 22, :].to('cpu')\n", - "print(f\"Total number of spikes at t=T for a single sample:\\n {spk_results.sum(dim=0)}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Target: 6\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAKYAAACmCAYAAABQiPR3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAE7UlEQVR4nO3dz4uVVRyA8a9TYhBTtBCnWiRDboLAmXBT2excWDEQthIqCAvcVBtpSMLFwKQbp3aWAzEhxBTFQM2iTWlkhKgF0UaQcRGVLRQGyRC8/QHne6Z7596587zX57M892V8B5974Mx5f2xqtVqtkGCGNvoEpIxhCskwhWSYQjJMIRmmkO5e7cPhe2b6dR66Q63cnErHnTGFZJhCMkwhGaaQDFNIhikkwxSSYQrJMIVkmEIyTCEZppAMU0iGKaRVL3sbBIs7b6fj46fnirHpymV+J7Zc7uk56f85YwrJMIVkmEIyTCEZppAGflU+MTufjq/0+TzUGWdMIRmmkAxTSIYppIFa/Dx168FibGXsVnrshYlXizG3HjmcMYVkmEIyTCEZppAMU0gDtSo/tOv3to9dvlKu4CP+6t3JqCvOmEIyTCEZppAMU0gDtfjZ/dpSMXajcuzC1eQ7ubm356O1c8YUkmEKyTCFZJhCMkwhNXJVnl0QHBFxY/8fxdjwxXyp/cPm8lhxOGMKyTCFZJhCMkwhNXLxM7vvfNvHXjq+Zx3PZO1e/3c0HX9spLaJWnrymYvF2MmFp9Njm3YHqDOmkAxTSIYpJMMUkmEKqZGr8q2j7W8nnj0zVvmkf3dEZivwoz++mx5be9ZSuw7Pf51/kLwqhrxSd8YUkmEKyTCFZJhCauTip2mm3v6sGOtkkZM9ZDYiYumnR4ux2qJqZu6DYuzEwefaPod+c8YUkmEKyTCFZJhCMkwhuSrvodrFv3cd+ajtn/HF9sPF2BvXKtunyZbi1OLjlXO4UA4ebPu0+s4ZU0iGKSTDFJJhCsnFTw9lW4812SInYpWFzjp4/4FtG34ONc6YQjJMIRmmkAxTSI1c/Px9OX8+5kgytv2Ryo1r17r7TmbP6Ex3VyLi3lPlsYQFBpkzppAMU0iGKSTDFJJhCqmRq/I3P38iHf80eTzK+Om5/Ifcf6Crczh16Ku2j/3+w71d/VuduG9H/leIoeTtHeS/DDhjCskwhWSYQjJMITVy8VN73V629Ze9xi8iYnHn7WJs8uf1+Z4uX8m2ULtfeGTXU9Z+39pjZqicMYVkmEIyTCEZppAMU0iNXJXXfPJOufJ8Yf90emy2Vbl8ZDw9dua9F4uxTh77snA1+f6XO4RVtbsZX/7yaDmYbD1GRBw793ByDu2//aPfnDGFZJhCMkwhGaaQNrVarVbtw+HkNW9Nk209RkRMzM4XY92+Lq+m9jiYzOQrS8VY7e7LzHTl/4z6er6Vm1PpuDOmkAxTSIYpJMMUkmEKaeBX5TXZs4dm951Pjx2pvZy+T7ILoCMinj9Q3ulZu4iaylW5GsUwhWSYQjJMId2xi59uXf/u42Ksky3NP196Nh0/e2asGCM/yqVbLn7UKIYpJMMUkmEKyTCFNFB3SfbTpeN7irFOti63juZbh5PJ+G/JXZoR3It/e8EZU0iGKSTDFJJhCsnFzxrtWnyoGDtX2Wbc8dY3xdj1ys/95dtyS/LXoX86ObWB4IwpJMMUkmEKyTCFZJhC8kJhbSgvFFajGKaQDFNIhikkwxSSYQrJMIVkmEIyTCEZppAMU0iGKSTDFJJhCskwhWSYQjJMIRmmkAxTSIYpJMMUkmEKyTCFZJhCMkwhGaaQVn1EjLRRnDGFZJhCMkwhGaaQDFNIhimk/wDBQN7aPIW+KQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "print(f\"Target: {anim_loader.dataset[22][1]}\")\n", - "plt.figure(facecolor=\"w\")\n", - "plt.subplot(1,2,1)\n", - "plt.imshow(anim_loader.dataset[22][0].reshape((28,-1)).cpu(), cmap='plasma')\n", - "plt.axis('off')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import snntorch.spikeplot as sp\n", - "from IPython.display import HTML\n", - "\n", - "fig, ax = plt.subplots(facecolor='w', figsize=(12, 7))\n", - "labels=['0', '1', '2', '3', '4', '5', '6', '7', '8','9']\n", - "\n", - "# animation\n", - "anim = sp.spike_count(spk_results, fig, ax, labels, animate=True, interpolate=5, num_steps = num_steps, time_step=1e-3)\n", - "HTML(anim.to_html5_video())\n", - "# anim.save(\"spike_bar.gif\")\n", - "\n", - "# final count\n", - "# sp.spike_count(spk_results, fig, ax, labels, interpolate=5, num_steps = num_steps, time_step=1e-3)\n", - "# plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/legacy/tutorial_2_neuronal_dynamics.ipynb b/examples/legacy/tutorial_2_neuronal_dynamics.ipynb deleted file mode 100644 index bc81eea7..00000000 --- a/examples/legacy/tutorial_2_neuronal_dynamics.ipynb +++ /dev/null @@ -1,2266 +0,0 @@ -{ - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "name": "Untitled17.ipynb", - "provenance": [], - "collapsed_sections": [], - "include_colab_link": true - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "HzIQBw28NL8h" - }, - "source": [ - "\n", - "\n", - "# snnTorch - Neuronal Dynamics with ``snntorch``\n", - "## Tutorial 2\n", - "### By Jason K. Eshraghian (www.jasoneshraghian.com)\n", - "\n", - "\n", - " \"Open\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Ep_Qv7kzNOz6" - }, - "source": [ - "# Introduction\n", - "In this tutorial, you will:\n", - "* Learn the fundamentals of the leaky integrate-and-fire (LIF) neuron model\n", - "* Use snnTorch to implement variations of the LIF model: \n", - " * Lapicque's neuron model (1st order)\n", - " * Synaptic conductance-based model (2nd order)\n", - " * Alpha model (a hacked version of the Spike Response model)\n", - "\n", - "\n", - "\n", - "* Implement a feedforward spiking neural network\n", - "\n", - ">Part of this tutorial was inspired by the book [*Neuronal Dynamics:\n", - "From single neurons to networks and models of cognition*](https://neuronaldynamics.epfl.ch/index.html) by\n", - "Wulfram Gerstner, Werner M. Kistler, Richard Naud and Liam Paninski.\n", - "\n", - "If running in Google Colab:\n", - "* You may connect to GPU by checking `Runtime` > `Change runtime type` > `Hardware accelerator: GPU`\n", - "* Next, install the latest PyPi distribution of snnTorch by clicking into the following cell and pressing `Shift+Enter`." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "SPQITvDuNNJg" - }, - "source": [ - "!pip install snntorch" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xmTt5dvyXNNy" - }, - "source": [ - "# 1. The Spectrum of Neuron Models\n", - "A large variety of neuron models are out there, ranging from biophysically accurate models (i.e., the Hodgkin-Huxley models) to the extremely simple artificial neuron that pervades all facets of modern deep learning.\n", - "\n", - "**Hodgkin-Huxley Neuron Models**$-$While biophysical models can reproduce electrophysiological results with a high degree of accuracy, their complexity makes them difficult to use. We expect this to change as more rigorous theories of how neurons contribute to higher-order behaviors in the brain are uncovered.\n", - "\n", - "**Artificial Neuron Model**$-$On the other end of the spectrum is the artificial neuron. The inputs are multiplied by their corresponding weights and passed through an activation function. This simplification has enabled deep learning researchers to perform incredible feats in computer vision, natural language processing, and many other machine learning-domain tasks.\n", - "\n", - "**Leaky Integrate-and-Fire Neuron Models**$-$Somewhere in the middle of the divide lies the leaky integrate-and-fire (LIF) neuron model. It takes the sum of weighted inputs, much like the artificial neuron. But rather than passing it directly to an activation function, it will integrate the input over time with a leakage, much like an RC circuit. If the integrated value exceeds a threshold, then the LIF neuron will emit a voltage spike. The LIF neuron abstracts away the shape and profile of the output spike; it is simply treated as a discrete event. As a result, information is not stored within the spike, but rather the timing (or frequency) of spikes. Simple spiking neuron models have produced much insight into the neural code, memory, network dynamics, and more recently, deep learning. The LIF neuron sits in the sweet spot between biological plausibility and practicality. \n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "\n", - "\n", - "The different versions of the LIF model each have their own dynamics and use-cases. snnTorch currently supports four types of LIF neurons:\n", - "* Lapicque's RC model: ``snntorch.Lapicque``\n", - "* Non-physical 1st order model: ``snntorch.Leaky`` \n", - "* Synaptic Conductance-based neuron model: ``snntorch.Synaptic``\n", - "* Alpha neuron Model: ``snntorch.Alpha``\n", - "\n", - "Before learning how to use them, let's understand how to construct a simple LIF neuron model.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Nea8oBorr_KZ" - }, - "source": [ - "# 2. The Leaky Integrate-and-Fire Neuron Model" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "YKsrN5feQ2Dz" - }, - "source": [ - "## 2.1 Spiking Neurons: Intuition" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "YGStnfjzsGKb" - }, - "source": [ - "A neuron might be connected to 1,000 $-$ 10,000 other neurons. If one neuron spikes, all of these downhill neurons will feel it. But what determines whether a neuron spikes in the first place? The past century of experiments demonstrate that if a neuron experiences *sufficient* stimulus at its input, then we might expect it to become excited and fire its own spike. \n", - "\n", - "Where does this stimulus come from? It could be from:\n", - "* the sensory periphery, \n", - "* an invasive electrode artificially stimulating the neuron, or in most cases,\n", - "* from other pre-synaptic neurons. \n", - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TaVTRojJ2Dl_" - }, - "source": [ - "Given that these spikes are very short bursts of electrical activity, it is quite unlikely for all input spikes to arrive at the neuron body in precise unison. This indicates the presence of temporal dynamics that 'sustain' the input spikes, kind of like a delay.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "l44jI7A2ReB_" - }, - "source": [ - "## 2.2 The Passive Membrane" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Od5tfXv6_wcq" - }, - "source": [ - "Like all cells, a neuron is surrounded by a thin membrane. This membrane is a lipid bilayer that insulates the conductive saline solution within the neuron from the extracellular medium. Electrically, the two conductors separated by an insulator act as a capacitor. \n", - "\n", - "Another function of this membrane is to control what goes in and out of this cell (e.g., ions such as Na$^+$). The membrane is usually impermeable to ions which blocks them from entering and exiting the neuron body. But there are specific channels in the membrane that are triggered to open by injecting current into the neuron. This charge movement is electrically modelled by a resistor.\n", - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "wT_S4b4HL2ir" - }, - "source": [ - "Now say some arbitrary time-varying current $I_{\\rm in}(t)$ is injected into the neuron, be it via electrical stimulation or from other neurons. The total current in the circuit is conserved, so:\n", - "\n", - "$$I_{\\rm in}(t) = I_{R} + I_{C}$$\n", - "\n", - "From Ohm's Law, the membrane potential measured between the inside and outside of the neuron $U_{\\rm mem}$ is proportional to the current through the resistor:\n", - "\n", - "$$I_{R}(t) = \\frac{U_{\\rm mem}(t)}{R}$$\n", - "\n", - "The capacitance is a proportionality constant between the charge stored on the capacitor $Q$ and $U_{\\rm mem}(t)$:\n", - "\n", - "\n", - "$$Q = CU_{\\rm mem}(t)$$\n", - "\n", - "The rate of change of charge gives the capacitive current:\n", - "\n", - "$$\\frac{dQ}{dt}=I_C(t) = C\\frac{dU_{\\rm mem}(t)}{dt}$$\n", - "\n", - "Therefore:\n", - "\n", - "$$I_{\\rm in}(t) = \\frac{U_{\\rm mem}(t)}{R} + C\\frac{dU_{\\rm mem}(t)}{dt}$$\n", - "\n", - "$$\\implies RC \\frac{dU_{\\rm mem}(t)}{dt} = -U_{\\rm mem}(t) + RI_{\\rm in}(t)$$\n", - "\n", - "The right hand side of the equation is of units **\\[Voltage]**. On the left hand side of the equation, the term $\\frac{dU_{\\rm mem}(t)}{dt}$ is of units **\\[Voltage/Time]**. To equate it to the left hand side (i.e., voltage), $RC$ must be of unit **\\[Time]**. We refer to $\\tau = RC$ as the time constant of the circuit:\n", - "\n", - "$$ \\tau \\frac{dU_{\\rm mem}(t)}{dt} = -U_{\\rm mem}(t) + RI_{\\rm in}(t)$$\n", - "\n", - "The passive membrane is therefore described by a linear differential equation.\n", - "\n", - "For a derivative of a function to be of the same form as the original function, i.e., $\\frac{dU_{\\rm mem}(t)}{dt} \\propto U_{\\rm mem}(t)$, this implies the solution is exponential with a time constant $\\tau$.\n", - "\n", - "Say the neuron starts at some value $U_{0}$ with no further input, i.e., $I_{\\rm in}(t)=0$. The solution of the linear differential equation is:\n", - "\n", - "$$U_{\\rm mem}(t) = U_0e^{-\\frac{t}{\\tau}}$$\n", - "\n", - "The general solution is shown below.\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rTXS_aSmRs-3" - }, - "source": [ - "## 2.3 Lapicque's LIF Neuron Model" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Jt2q6tZiWkgT" - }, - "source": [ - "This similarity between nerve membranes and RC circuits was observed by [Louis Lapicque in 1907](https://core.ac.uk/download/pdf/21172797.pdf). He stimulated the nerve fiber of a frog with a brief electrical pulse, and found that membranes could be approximated as a capacitor with a leakage. We pay homage to his findings by naming the basic LIF neuron model in snnTorch after him. \n", - "\n", - "Most of the concepts in Lapicque's model carry forward to other LIF neuron models. Now let's simulate this neuron using snnTorch." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WtveAGG0zE0n" - }, - "source": [ - "### 2.3.1 Lapicque: Without Stimulus" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "um1s01gTzUC0" - }, - "source": [ - "First, import the packages needed to run Lapicque's neuron model: snnTorch and PyTorch." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "sUyODsBtWkAG" - }, - "source": [ - "import snntorch as snn\n", - "import torch" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6zpgNEbizd8s" - }, - "source": [ - "The membrane potential has a time constant $\\tau = RC$ associated with it. This can be equivalently represented by a decay rate $\\beta$ that specifies the ratio of potential between subsequent time steps:\n", - "\n", - "$$\\beta = \\frac{U_0e^{-\\frac{1}{\\tau}}}{U_0e^{-\\frac{0}{\\tau}}} = \\frac{U_0e^{-\\frac{2}{\\tau}}}{U_0e^{-\\frac{1}{\\tau}}} = \\frac{U_0e^{-\\frac{3}{\\tau}}}{U_0e^{-\\frac{2}{\\tau}}}=~~...$$\n", - "$$\\implies \\beta = e^{-\\frac{1}{\\tau}}$$\n", - "\n", - "Setting $\\tau = 5\\times 10^{-3} \\implies \\beta \\approx 0.819$:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "ohshwOCU6Vbm" - }, - "source": [ - "# RC time constant\n", - "tau_mem = 5e-3\n", - "time_step = 1e-3 # one time step = 1ms\n", - "\n", - "# decay p/time step\n", - "beta = float(torch.exp(torch.tensor(-time_step/tau_mem)))\n", - "\n", - "# Number of time steps to simulate\n", - "num_steps = 200\n", - "\n", - "print(f\"Membrane decay rate ('beta'): {beta}\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "qi5EnND98cz3" - }, - "source": [ - "Instantiating Lapicque's neuron only requires the following line of code:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "wI2Nahsg8d-t" - }, - "source": [ - "# leaky integrate and fire neuron\n", - "lif1 = snn.Lapicque(beta=beta)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "AUv8_QPCIsDT" - }, - "source": [ - "The same thing can also be accomplished by specifying the RC values:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "Q0tk_CBoIv6d" - }, - "source": [ - "R = 5\n", - "C = 1e-3\n", - "\n", - "lif1 = snn.Lapicque(R=R, C=C, time_step=time_step)\n", - "\n", - "print(f\"Membrane decay rate ('beta'): {lif1.beta[0]}\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "kuytxXet8lc9" - }, - "source": [ - "To use this neuron: \n", - "\n", - "**Inputs**\n", - "* `spk_in`: each element of $I_{\\rm in}$, which are all `0` for now, is sequentially passed as an input\n", - "* `mem`: the membrane potential at the present time $t$ is also passed as input. Initialize it arbitrarily as $U_0 = 0.9~V$.\n", - "\n", - "**Outputs**\n", - "* `spk_out`: output spike $S_{\\rm out}[t+1]$ at the next time step ('1' if there is a spike; '0' if there is no spike)\n", - "* `mem`: membrane potential $U_{\\rm mem}[t+1]$ at the next time step\n", - "\n", - "These all need to be of type `torch.Tensor`.\n" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "M_0O1Q3Y9KgM" - }, - "source": [ - "# Initialize membrane, input, and output\n", - "mem = torch.ones(1) * 0.9 # membrane potential of 0.9 at t=0\n", - "cur_in = torch.zeros(num_steps) # input is 0 for all t \n", - "spk_out = torch.zeros(1) # neuron needs somewhere to sequentially dump its output spikes" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "_CEuNHsN8-67" - }, - "source": [ - "These values are only for the initial time step $t=0$. We'd like to watch the evolution of `mem` over time. The list `mem_rec` is initialized to record these values at every time step." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "CUCxQBTQ9d_P" - }, - "source": [ - "# Initialize somewhere to store recordings of membrane potential\n", - "mem_rec = [mem]" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i4TxVFaV9uf6" - }, - "source": [ - "Now it's time to run a simulation! 200 time steps will be simulated, updating `mem` at each step and recording its value in `mem_rec`:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "j6GeMMiU8kc4" - }, - "source": [ - "# pass updated value of mem and cur_in[step]=0 at every time step\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif1(cur_in[step], mem)\n", - "\n", - " # Store recordings of membrane potential\n", - " mem_rec.append(mem)\n", - "\n", - "# crunch the list of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4lngTsykIc7k" - }, - "source": [ - "Let's take a look at how the membrane potential and synaptic current evolved." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "SrfeQWT6I2JC" - }, - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.title(\"Lapicque's Neuron Model Without Stimulus\")\n", - "plt.plot(mem_rec, label=\"Membrane Potential\")\n", - "plt.xlabel(\"Time step\")\n", - "plt.ylabel(\"Membrane Potential\")\n", - "plt.xlim([0, 50])\n", - "plt.ylim([0, 1])\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9k0PKng2JYcx" - }, - "source": [ - "This matches the dynamics that were previously derived. We've shown ourselves that the membrane potential will decay over time in the absence of any input stimuli. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "7Z01CioKJpkr" - }, - "source": [ - "### 2.3.2 Lapicque: Step Input" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "B6GZUEFgJ0l0" - }, - "source": [ - "Now let's apply a step current $I_{\\rm in}(t)$ that switches on at $t=t_0$. Given the linear first-order differential equation:\n", - "\n", - "$$ \\tau \\frac{dU_{\\rm mem}}{dt} = -U_{\\rm mem} + RI_{\\rm in}(t),$$\n", - "\n", - "the general solution will be:\n", - "\n", - "$$U_{\\rm mem}=I_{\\rm in}(t)R + [U_0 - I_{\\rm in}(t)R]e^{-\\frac{t}{\\tau}}$$\n", - "\n", - "If the membrane potential is initialized to $U_{\\rm mem}(t=0) = 0 V$, then:\n", - "\n", - "$$U_{\\rm mem}(t)=I_{\\rm in}(t)R [1 - e^{-\\frac{t}{\\tau}}]$$\n", - "\n", - "Let's visualize what this looks like by triggering a current pulse of $I_{in}=100mA$ at $t_0 = 10ms$." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "kD7cLKSuLu9n" - }, - "source": [ - "# Initialize input current pulse\n", - "cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.1), 0) # input current turns on at t=10\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1) # membrane potential of 0 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to sequentially dump its output spikes\n", - "\n", - "# Initialize somewhere to store recordings of membrane potential\n", - "mem_rec = [mem]" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PW8ZDbKCNW8E" - }, - "source": [ - "As before, 200 time steps will be simulated. But this time, the new values of `cur_in` will be passed:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "w9J2AsrZNXr8" - }, - "source": [ - "# pass updated value of mem and cur_in[step] at every time step\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif1(cur_in[step], mem)\n", - "\n", - " # Store recordings of membrane potential\n", - " mem_rec.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "_cC3zoy7OW_Y" - }, - "source": [ - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.2])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Step Input\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec)\n", - "ax[1].set_ylim([0, 0.6])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "\n", - "ax[1].axvline(x=10, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "uZi44ZEvNhkR" - }, - "source": [ - "The membrane potential exponentially rises and then stabilizes at $U_{\\rm mem}(t\\rightarrow \\infty) =I_{\\rm in}R$:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "1l40Efnbk92o" - }, - "source": [ - "print(f\"The calculated value of input pulse [A] x resistance [Ω] is: {cur_in[11]*lif1.R} V\")\n", - "print(f\"The simulated value of steady-state membrane potential is: {mem_rec[200][0]} V\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "H0ql9cAzpN5D" - }, - "source": [ - "Close enough!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ZqEtGdKAaIbK" - }, - "source": [ - "### 2.3.3 Lapicque: Pulse Input" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "HOgCeLPYaLKZ" - }, - "source": [ - "Now what if the step input was clipped at $t=30ms$?" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "hjtupQNxaQ5D" - }, - "source": [ - "# Initialize input current pulse.\n", - "cur_in1 = torch.cat((torch.zeros(10), torch.ones(20)*(0.1), torch.zeros(170)), 0) # input turns on at t=10, off at t=30\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1) # membrane potential of 0 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to sequentially dump its output spikes\n", - "\n", - "# Initialize somewhere to store recordings of membrane potential\n", - "mem_rec1 = [mem]" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "O8JFL9CGa0NW" - }, - "source": [ - "# pass updated value of mem and cur_in[step] at every time step\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif1(cur_in1[step], mem)\n", - "\n", - " # Store recordings of membrane potential\n", - " mem_rec1.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec1 = torch.stack(mem_rec1)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "UM8GGzWOa3GE" - }, - "source": [ - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in1, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.2])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Input Pulse\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec1)\n", - "ax[1].set_ylim([0, 1])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "\n", - "ax[1].axvline(x=10, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "ax[1].axvline(x=30, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "orJvjev6a9tR" - }, - "source": [ - "It appears to rise just as it did for the step input, but now it decays with a time constant of $\\tau$ as in our first simulation. \n", - "\n", - "Let's deliver approximately the same amount of charge $Q = I \\times t$ to the circuit in half the time. This means our input current amplitude will need to be increased by a little, and the time window will be decreased." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "j_G_gWTzbo4J" - }, - "source": [ - "# Increase amplitude of current pulse; half the time.\n", - "cur_in2 = torch.cat((torch.zeros(10), torch.ones(10)*0.111, torch.zeros(180)), 0) # input turns on at t=10, off at t=20\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1) # membrane potential of 0 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to sequentially dump its output spikes\n", - "\n", - "# Initialize somewhere to store recordings of membrane potential\n", - "mem_rec2 = [mem]" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "4YYr91yzbq-G" - }, - "source": [ - "# pass updated value of mem and cur_in[step] at every time step\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif1(cur_in2[step], mem)\n", - "\n", - " # Store recordings of membrane potential\n", - " mem_rec2.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec2 = torch.stack(mem_rec2)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "krBTZm92btXH" - }, - "source": [ - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in2, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.2])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Input Pulse: x1/2 pulse width\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec2)\n", - "ax[1].set_ylim([0, 1])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "\n", - "ax[1].axvline(x=10, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "ax[1].axvline(x=20, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "34azxvjWbqge" - }, - "source": [ - "Let's do that again, but with an even faster input pulse and higher amplitude:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "650G94v9dgr3" - }, - "source": [ - "# Increase amplitude of current pulse; quarter the time.\n", - "cur_in3 = torch.cat((torch.zeros(10), torch.ones(5)*0.147, torch.zeros(185)), 0) # input turns on at t=10, off at t=15\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1) # membrane potential of 0 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to sequentially dump its output spikes\n", - "\n", - "# Initialize somewhere to store recordings of membrane potential\n", - "mem_rec3 = [mem]\n", - "\n", - "# pass updated value of mem and cur_in[step] at every time step\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif1(cur_in3[step], mem)\n", - "\n", - " # Store recordings of membrane potential\n", - " mem_rec3.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec3 = torch.stack(mem_rec3)\n", - "\n", - "# Generate Plots\n", - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in3, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.2])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Input Pulse: x1/4 pulse width\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec3)\n", - "ax[1].set_ylim([0, 1])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "\n", - "ax[1].axvline(x=10, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "ax[1].axvline(x=15, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "fuQtNgw4sYhk" - }, - "source": [ - "Let's compare all three experiments on the same plot:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "ZL7P3mEfsdAR" - }, - "source": [ - "# Generate Plots\n", - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in1)\n", - "ax[0].plot(cur_in2)\n", - "ax[0].plot(cur_in3)\n", - "ax[0].set_ylim([0, 0.2])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Input Pulse: Varying inputs\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec1)\n", - "ax[1].plot(mem_rec2)\n", - "ax[1].plot(mem_rec3)\n", - "ax[1].set_ylim([0, 1])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "\n", - "ax[1].axvline(x=10, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "ax[1].axvline(x=15, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "ax[1].axvline(x=20, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "ax[1].axvline(x=30, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6WpZx0NStNdd" - }, - "source": [ - "As the input current pulse amplitude increases, the rise time of the membrane potential speeds up. In the limit of the input current pulse width becoming infinitesimally small, $T_W \\rightarrow 0s$, the membrane potential will jump straight up in virtually zero rise time:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "h0EG17NpuzuN" - }, - "source": [ - "# Current spike input\n", - "cur_in4 = torch.cat((torch.zeros(10), torch.ones(1)*0.5, torch.zeros(189)), 0) # input only on for 1 time step\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1) # membrane potential of 0 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to sequentially dump its output spikes\n", - "\n", - "# Initialize somewhere to store recordings of membrane potential\n", - "mem_rec4 = [mem]\n", - "\n", - "# pass updated value of mem and cur_in[step] at every time step\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif1(cur_in4[step], mem)\n", - "\n", - " # Store recordings of membrane potential\n", - " mem_rec4.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec4 = torch.stack(mem_rec4)\n", - "\n", - "# Generate Plots\n", - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in4, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.6])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Input Spike\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec4)\n", - "ax[1].set_ylim([0, 1])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axvline(x=10, ymin=0, ymax=2.2, alpha = 0.25, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "DNY_EftJvREj" - }, - "source": [ - "The current pulse width is now so short, it effectively looks like a spike. That is to say, charge is delivered in an infinitely short period of time, $I_{\\rm in}(t) = Q/t_0$ where $t_0 \\rightarrow 0$. More formally:\n", - "\n", - "$$I_{\\rm in}(t) = Q \\delta (t-t_0),$$\n", - "\n", - "where $\\delta (t-t_0)$ is the Dirac-Delta function. Physically, it is impossible to 'instantaneously' deposit charge. But integrating $I_{\\rm in}$ gives a result that makes physical sense, as we can obtain the charge delivered:\n", - "\n", - "$$1 = \\int^{t_0 + a}_{t_0 - a}\\delta(t-t_0)dt$$\n", - "\n", - "$$f(t_0) = \\int^{t_0 + a}_{t_0 - a}f(t)\\delta(t-t_0)dt$$\n", - "\n", - "Here, $f(t_0) = I_{\\rm in}(t_0=10) = 0.5A \\implies f(t) = Q = 0.5C$.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Danwy1q9xfvr" - }, - "source": [ - "### 2.3.4 Lapicque: Firing" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "CUH8IYh9xlV-" - }, - "source": [ - "So far, we have only seen how a neuron will react to spikes at the input. For a neuron to generate and emit its own spikes at the output, we need to combine the passive membrane model with a threshold.\n", - "\n", - "If the membrane potential exceeds this threshold, then a voltage spike will be generated, external to the passive membrane model. \n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "By default, `threshold=1` for all neuron models in snnTorch. So applying a step current input that is insufficient will result in the neuron to function only in the subthreshold regime. This time, we will create a list called `spk_rec` to record any output spikes if they occur. The current step will be set to $I_{\\rm in} = 0.15 A$. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "Fe61PZ33w49_" - }, - "source": [ - "# Small step current input\n", - "cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.15), 0)\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1)\n", - "spk_out = torch.zeros(1) \n", - "mem_rec = [mem]\n", - "spk_rec = [spk_out]" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "HPx-uuH90JTo" - }, - "source": [ - "# Create a new neuron with a slow time constant\n", - "lif2 = snn.Lapicque(R=5, C=10)\n", - "\n", - "print(f\"Membrane decay rate ('beta'): {lif2.beta[0]}\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "se0fnQ5EDHr9" - }, - "source": [ - "Note how this new value of $\\beta$ is much larger than `lif1.beta`$\\approx 0.82$. \n", - "\n", - "For `lif2.beta`$=0.98$, the membrane potential is 98% of the value of that of the previous time step, and experiences a much slower decay rate." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "0yqODQazwg4s" - }, - "source": [ - "# Simulation run across 200 time steps. \n", - "for step in range(num_steps):\n", - " spk_out, mem = lif2(cur_in[step], mem)\n", - "\n", - " # record outputs over time\n", - " mem_rec.append(mem)\n", - " spk_rec.append(spk_out)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)\n", - "spk_rec = torch.stack(spk_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "Z3Laa83DxPbJ" - }, - "source": [ - "# Generate Plots\n", - "fig, ax = plt.subplots(2, figsize=(8,6),sharex=True)\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.4])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Step Input\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec)\n", - "ax[1].set_ylim([0, 1.25])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axhline(y=1.0, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "vrAD1fBextPM" - }, - "source": [ - "The membrane potential fails to reach the threshold of 1.0. Instead, it reaches the steady-state value of $I_{\\rm in}R = 0.15A \\times 5Ω = 0.75V$:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "XtkMfF5Qxrvm" - }, - "source": [ - "print(f\"The calculated steady state membrane potential is: {lif1.R*cur_in[199]}\")\n", - "print(f\"The simulated steady state membrane potential is: {mem_rec[199][0]}\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "FN1Gmvuwy1ZA" - }, - "source": [ - "> Note: these are non-biologically accurate values, and are chosen for simplicity.\n", - "\n", - "To reach the threshold, we need to ensure that $I_{\\rm in}R > U_{\\rm thr}$. So set $I_{\\rm in} = 0.21 A$:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "LU9dSGi6zEOP" - }, - "source": [ - "# Larger current step\n", - "cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.21), 0)\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1)\n", - "spk_out = torch.zeros(1) \n", - "mem_rec = [mem]\n", - "spk_rec = [spk_out]" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "td4oIyzNzLft" - }, - "source": [ - "# Simulation run across 200 time steps.\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif2(cur_in[step], mem)\n", - "\n", - " # record outputs over time\n", - " mem_rec.append(mem)\n", - " spk_rec.append(spk_out)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)\n", - "spk_rec = torch.stack(spk_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "IeiswwDXGnay" - }, - "source": [ - "To plot our results, let's import `snntorch.spikeplot`. From Tutorial 1, we learnt how to use it to create raster plots of spike responses." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "gxLfSFTIGvaU" - }, - "source": [ - "from snntorch import spikeplot as splt" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "4BYXOPSizWH6" - }, - "source": [ - "# Generate Plots\n", - "fig, ax = plt.subplots(3, figsize=(8,6), sharex=True, \n", - " gridspec_kw = {'height_ratios': [1, 1, 0.4]})\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.4])\n", - "ax[0].set_xlim([0, 200])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Step Input\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec)\n", - "ax[1].set_ylim([0, 1.25])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axhline(y=1.0, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot output spike using spikeplot\n", - "splt.raster(spk_rec, ax[2], s=400, c=\"black\", marker=\"|\")\n", - "ax[2].axvline(x=162, ymin=0, ymax=6.75, alpha = 0.15, linestyle=\"dashed\", c=\"black\", linewidth=2, zorder=0, clip_on=False)\n", - "plt.ylabel(\"Output spikes\")\n", - "plt.yticks([]) \n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "cm5tEp4YEoQm" - }, - "source": [ - "The membrane potential exponentially rises and then hits the threshold, at which point it resets. We can roughly see this occurs between $155s < t_{\\rm spk} < 165s$:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "tWSGUUsgNr9D" - }, - "source": [ - "print(spk_rec[155:165])" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4xWW-ncoOWrJ" - }, - "source": [ - "The absence of a spike is represented by $S_{\\rm out}=0$, and the occurrence of a spike is $S_{\\rm out}=1$. Here, the spike occurs at $S_{\\rm out}(t=162)=1$.\n", - "\n", - "If you are wondering why each of these entries is stored as a tensor, it is because soon we will simulate large scale neural networks. Each entry will contain the spike responses of many neurons, and tensors can be loaded into GPU memory to speed up the training process.\n", - "\n", - "If $I_{\\rm in}$ is increased, then the membrane potential approaches $U_{\\rm thr}$ faster:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "HY6G9HZ1POC7" - }, - "source": [ - "# Even Larger current step\n", - "cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.3), 0)\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1)\n", - "spk_out = torch.zeros(1) \n", - "mem_rec = [mem]\n", - "spk_rec = [spk_out]\n", - "\n", - "# Simulation run across 200 time steps.\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif2(cur_in[step], mem)\n", - "\n", - " # record outputs over time\n", - " mem_rec.append(mem)\n", - " spk_rec.append(spk_out)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)\n", - "spk_rec = torch.stack(spk_rec)\n", - "\n", - "# Generate Plots\n", - "fig, ax = plt.subplots(3, figsize=(8,6), sharex=True, \n", - " gridspec_kw = {'height_ratios': [1, 1, 0.4]})\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.4])\n", - "ax[0].set_xlim([0, 200])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Periodic Firing\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec)\n", - "ax[1].set_ylim([0, 1.25])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axhline(y=1.0, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot output spike using spikeplot\n", - "splt.raster(spk_rec, ax[2], s=400, c=\"black\", marker=\"|\")\n", - "plt.ylabel(\"Output spikes\")\n", - "plt.yticks([]) \n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GOjWXepnPleo" - }, - "source": [ - "A similar increase in firing frequency can also be induced by decreasing the threshold. This requires initializing a new neuron model, but the rest of the code block is the exact same as above:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "HQ0VIBlJPrvr" - }, - "source": [ - "# Half the threshold\n", - "lif3 = snn.Lapicque(R=5, C=10, threshold=0.5)\n", - "\n", - "# Initialize membrane and output\n", - "mem = torch.zeros(1)\n", - "spk_out = torch.zeros(1) \n", - "mem_rec = [mem]\n", - "spk_rec = [spk_out]\n", - "\n", - "# Simulation run across 200 time steps.\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif3(cur_in[step], mem)\n", - "\n", - " # record outputs over time\n", - " mem_rec.append(mem)\n", - " spk_rec.append(spk_out)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)\n", - "spk_rec = torch.stack(spk_rec)\n", - "\n", - "# Generate Plots\n", - "fig, ax = plt.subplots(3, figsize=(8,6), sharex=True, \n", - " gridspec_kw = {'height_ratios': [1, 1, 0.4]})\n", - "\n", - "# Plot input current\n", - "ax[0].plot(cur_in, c=\"tab:orange\")\n", - "ax[0].set_ylim([0, 0.4])\n", - "ax[0].set_xlim([0, 200])\n", - "ax[0].set_ylabel(\"Input Current ($I_{in}$)\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Low Threshold\")\n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec)\n", - "ax[1].set_ylim([0, 1.25])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axhline(y=0.5, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot output spike using spikeplot\n", - "splt.raster(spk_rec, ax[2], s=400, c=\"black\", marker=\"|\")\n", - "plt.ylabel(\"Output spikes\")\n", - "plt.yticks([]) \n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rJwtafQ_Siav" - }, - "source": [ - "That's what happens for a constant current injection. But in both deep neural networks and in the biological brain, most neurons will be connected to other neurons. They are more likely to receive spikes, rather than injections of constant current. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sOr9akj9S2Ei" - }, - "source": [ - "### 2.3.5 Lapicque: Spike Inputs" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rw_FZDnMS-pc" - }, - "source": [ - "Let's harness some of the skills we learnt in [Tutorial 1](https://colab.research.google.com/github/jeshraghian/snntorch/blob/tutorials/examples/tutorial_1_spikegen.ipynb), and use the `snntorch.spikegen` module to create some randomly generated input spikes." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "V7AXG4QqTCoZ" - }, - "source": [ - "from snntorch import spikegen \n", - "\n", - "# Create a 1-D random spike train. Each element has a probability of 40% of firing.\n", - "spk_in = spikegen.rate_conv(torch.ones((num_steps)) * 0.40)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "5CJxZg1TTNCJ" - }, - "source": [ - "Run the following code block to see how many spikes have been generated." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "rbhLmpNSTPgK" - }, - "source": [ - "# Tell me the number of spikes\n", - "print(f\"There are {int(sum(spk_in))} total spikes out of {len(spk_in)} time steps.\")\n", - "\n", - "# Now show me the spikes\n", - "from snntorch import spikeplot as splt\n", - "\n", - "fig = plt.figure(facecolor=\"w\", figsize=(8, 1))\n", - "ax = fig.add_subplot(111)\n", - "\n", - "splt.raster(spk_in.reshape(num_steps, -1), ax, s=100, c=\"black\", marker=\"|\")\n", - "\n", - "plt.title(\"Input Spikes\")\n", - "plt.xlabel(\"Time step\")\n", - "plt.yticks([])\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "wo2_Voc3TvSc" - }, - "source": [ - "# Refresh all our hidden and output variables\n", - "mem = torch.ones(1)*0.5 # membrane potential of 0.5 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to dump its output spikes\n", - "\n", - "# Create a trace of the variables of interest\n", - "mem_rec = [mem]\n", - "spk_rec = [spk_out]\n", - "\n", - "# Run the simulation\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif3(spk_in[step], mem)\n", - "\n", - " # Store recordings of output and hidden states\n", - " spk_rec.append(spk_out)\n", - " mem_rec.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec = torch.stack(mem_rec)\n", - "spk_rec = torch.stack(spk_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "uGYz-PJAT_1q" - }, - "source": [ - "# Generate Plots\n", - "fig, ax = plt.subplots(3, figsize=(8,6), sharex=True, \n", - " gridspec_kw = {'height_ratios': [0.4, 1, 0.4]})\n", - "\n", - "# Plot input current\n", - "splt.raster(spk_in, ax[0], s=400, c=\"black\", marker=\"|\")\n", - "ax[0].set_ylabel(\"Input Spikes\")\n", - "ax[0].set_title(\"Lapicque's Neuron Model With Input Spikes\")\n", - "plt.yticks([]) \n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec)\n", - "ax[1].set_ylim([0, 1])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axhline(y=0.5, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot output spike using spikeplot\n", - "splt.raster(spk_rec, ax[2], s=400, c=\"black\", marker=\"|\")\n", - "plt.ylabel(\"Output spikes\")\n", - "plt.yticks([]) \n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "fGYnDIoTXBCo" - }, - "source": [ - "### 2.3.6 Lapicque: Reset Mechanisms" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "lQsH8VOGXF2Q" - }, - "source": [ - "The final detail of the Lapicque neuron we want to explore is the sharp drop of membrane potential every time the neuron emits an output spike. This sharp drops promotes a reduction of spike generation, which supplements part of the theory on how brains are so power efficient. Biologically, this is known as the 'refractory period' where the the neuron's firing ability is momentarily suppressed. Here, we use a reset mechanism to model the refractory period.\n", - "\n", - "There are two ways to implement the reset mechanism:\n", - "\n", - "1. *reset by subtraction* (default) $-$ subtract the threshold from the membrane potential each time a spike is generated;\n", - "2. *reset to zero* $-$ force the membrane potential to zero each time a spike is generated.\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "Let's instantiate another neuron model to demonstrate how to alternate between reset mechanisms. \n", - "\n", - "By default, snnTorch neuron models use `reset_mechanism = \"subtract\"`. This can be explicitly overridden by passing the argument `reset_mechanism = \"zero\"`." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "7bfidqhKXdri" - }, - "source": [ - "# Reset mechanism: zero\n", - "lif4 = snn.Lapicque(R=5, C=10, threshold=0.5, reset_mechanism=\"zero\")\n", - "\n", - "# Refresh all our hidden and output variables\n", - "mem = torch.ones(1)*0.5 # membrane potential of 0.5 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to dump its output spikes\n", - "\n", - "# Create a trace of the variables of interest\n", - "mem_rec0 = [mem]\n", - "spk_rec0 = [spk_out]\n", - "\n", - "# Run the simulation\n", - "for step in range(num_steps):\n", - " spk_out, mem = lif4(spk_in[step], mem)\n", - "\n", - " # Store recordings of output and hidden states\n", - " spk_rec0.append(spk_out)\n", - " mem_rec0.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "mem_rec0 = torch.stack(mem_rec0)\n", - "spk_rec0 = torch.stack(spk_rec0)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "ifU3betRYETs" - }, - "source": [ - "# Generate Plots to Compare Reset Mechanisms\n", - "fig, ax = plt.subplots(nrows=3, ncols=2, figsize=(10,6), sharex=True, \n", - " gridspec_kw = {'height_ratios': [0.4, 1, 0.4], 'wspace':0.05})\n", - "\n", - "# Reset by Subtraction: input spikes\n", - "splt.raster(spk_in, ax[0][0], s=400, c=\"black\", marker=\"|\")\n", - "ax[0][0].set_ylabel(\"Input Spikes\")\n", - "ax[0][0].set_title(\"Reset by Subtraction\")\n", - "ax[0][0].set_yticks([])\n", - "\n", - "# Reset by Subtraction: membrane potential \n", - "ax[1][0].plot(mem_rec)\n", - "ax[1][0].set_ylim([0, 0.7])\n", - "ax[1][0].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1][0].axhline(y=0.5, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "\n", - "# Reset by Subtraction: output spikes\n", - "splt.raster(spk_rec, ax[2][0], s=400, c=\"black\", marker=\"|\")\n", - "ax[2][0].set_yticks([])\n", - "ax[2][0].set_xlabel(\"Time step\")\n", - "ax[2][0].set_ylabel(\"Output Spikes\")\n", - "\n", - "# Reset to Zero: input spikes\n", - "splt.raster(spk_in, ax[0][1], s=400, c=\"black\", marker=\"|\")\n", - "ax[0][1].set_title(\"Reset to Zero\")\n", - "ax[0][1].set_yticks([])\n", - "\n", - "# Reset to Zero: membrane potential\n", - "ax[1][1].plot(mem_rec0)\n", - "ax[1][1].set_ylim([0, 0.7])\n", - "ax[1][1].axhline(y=0.5, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "ax[1][1].set_yticks([])\n", - "ax[2][1].set_xlabel(\"Time step\")\n", - "\n", - "# Reset to Zero: output spikes\n", - "splt.raster(spk_rec0, ax[2][1], s=400, c=\"black\", marker=\"|\")\n", - "ax[2][1].set_yticks([])\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Dzs7qX2Kb8j_" - }, - "source": [ - "Pay close attention to the evolution of the membrane potential, especially in the moments after it reaches the threshold. You may notice that for \"Reset to Zero\", the membrane potential is forced back to zero after each spike.\n", - "\n", - "So which one is better? Applying `\"subtract\"` (the default value in `reset_mechanism`) is less lossy, because it does not ignore how much the membrane exceeds the threshold by.\n", - "\n", - "On the other hand, applying a hard reset with `\"zero\"` promotes sparsity and potentially less power consumption when running on dedicated neuromorphic hardware. Both options are available for you to experiment with. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "3ukHPMnOieVV" - }, - "source": [ - "## 2.4 Synaptic Conductance-based LIF Neuron Model" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9b9GEzOfm_Iz" - }, - "source": [ - "The passive membrane model allows discrete current spikes to be passed directly into the neuron. In reality, a spike will result in the gradual release of neurotransmitters from the pre-synaptic neuron to the post-synaptic neuron. This model accounts for the gradual temporal dynamics of input current, and is no longer strictly modelling a LIF neuron alone." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "YO6I0Wpmo3fx" - }, - "source": [ - "### 2.4.1 Synaptic Current" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "KBuE963K_JmQ" - }, - "source": [ - "If a pre-synaptic neuron fires, the voltage spike is transmitted down the axon of the neuron. It triggers the vesicles to release neurotransmitters into the synaptic cleft. These activate the post-synaptic receptors, which directly influence the effective current that flows into the post-synaptic neuron. \n", - "\n", - "Shown below are two types of excitatory receptors.\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "The simplest model of synaptic current assumes an increasing current on a very fast time-scale (or instantaneous), followed by a relatively slow exponential decay. This is very similar to the membrane potential dynamics of Lapicque's model.\n", - "\n", - "The synaptic condutance-based neuron model combines the synaptic current dynamics with the passive membrane. It must be instantiated with two input arguments:\n", - "* $\\alpha$: the decay rate of the synaptic current\n", - "* $\\beta$: the decay rate of the membrane potential (as with Lapicque)" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "FjJQIlY9DGtR" - }, - "source": [ - "# Decay rate of LIF states\n", - "alpha = 0.9\n", - "beta = 0.8\n", - "\n", - "# Initialize 2nd-order LIF neuron\n", - "lif5 = snn.Synaptic(alpha=alpha, beta=beta)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ssWDmsKlDc34" - }, - "source": [ - "Using this neuron is the exact same as Lapcique's neuron, but now with the addition of synaptic current `syn` as an input and output:\n", - "\n", - "**Inputs**\n", - "* `spk_in`: each input voltage spike $S_{\\rm in}[t]$ is sequentially passed in\n", - "* `syn`: synaptic current $I_{\\rm syn}[t]$ at the present time $t$\n", - "* `mem`: membrane potential $U_{\\rm mem}[t]$ at the present time $t$\n", - "\n", - "**Outputs**\n", - "* `spk_out`: output spike $S_{\\rm out}[t+1]$ at the next time step ('1' if there is a spike; '0' if there is no spike)\n", - "* `syn`: synaptic current $I_{\\rm syn}[t+1]$ at the next time step\n", - "* `mem`: membrane potential $U_{\\rm mem}[t+1]$ at the next time step\n", - "\n", - "These all need to be of type `torch.Tensor`." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OCzmKeDRFeBb" - }, - "source": [ - "Apply a periodic spiking input to see how current and membrane evolve with time:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "zWSjrxQGENPE" - }, - "source": [ - "# Initialize hidden states and output\n", - "syn = torch.zeros(1) # synaptic current of 0 at t=0\n", - "mem = torch.zeros(1) # membrane potential of 0 at t=0\n", - "spk_out = torch.zeros(1) # neuron needs somewhere to dump its output spikes\n", - "\n", - "# Periodic spiking input, spk_in = 0.2 V\n", - "spk_period = torch.cat((torch.ones(1)*0.2, torch.zeros(9)), 0)\n", - "spk_in = spk_period.repeat(20)\n", - "\n", - "syn_rec = [syn]\n", - "mem_rec = [mem]\n", - "spk_rec = [spk_out]\n", - "\n", - "for step in range(num_steps):\n", - " spk_out, syn, mem = lif5(spk_in[step], syn, mem)\n", - "\n", - " # Store recordings of output and hidden states\n", - " spk_rec.append(spk_out)\n", - " syn_rec.append(syn)\n", - " mem_rec.append(mem)\n", - "\n", - "# crunch -list- of tensors into one tensor\n", - "spk_rec = torch.stack(spk_rec)\n", - "syn_rec = torch.stack(syn_rec)\n", - "mem_rec = torch.stack(mem_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "b1rVl69UG8xr" - }, - "source": [ - "# Generate Plots\n", - "fig, ax = plt.subplots(4, figsize=(8,7), sharex=True, \n", - " gridspec_kw = {'height_ratios': [0.4, 1, 1, 0.4]})\n", - "\n", - "# Plot input current\n", - "splt.raster(spk_in, ax[0], s=400, c=\"black\", marker=\"|\")\n", - "ax[0].set_ylabel(\"Input Spikes\")\n", - "ax[0].set_title(\"Synaptic Conductance-based Neuron Model With Input Spikes\")\n", - "ax[0].set_yticks([]) \n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(syn_rec)\n", - "ax[1].set_ylim([0, 0.5])\n", - "ax[1].set_ylabel(\"Synaptic Current ($I_{syn}$)\")\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot membrane potential\n", - "ax[2].plot(mem_rec)\n", - "ax[2].set_ylim([0, 1.5])\n", - "ax[2].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[2].axhline(y=1, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot output spike using spikeplot\n", - "splt.raster(spk_rec, ax[3], s=400, c=\"black\", marker=\"|\")\n", - "plt.ylabel(\"Output spikes\")\n", - "ax[3].set_yticks([]) \n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "_w1uL7kbzfwW" - }, - "source": [ - "If you're not interested in the mathematical detail, then feel free to skip this brief section. We represent the voltage spikes at the input with:\n", - "\n", - "$$S_{\\rm in} = \\sum_k \\delta(t-t_k),$$\n", - "\n", - "where each spike triggers a weighted jump in synaptic current at time $t_k$, and is followed by an exponential decay:\n", - "\n", - "$$I_{\\rm syn}(t) = \\sum_k W_{i,j} S_{in; i,j}(t) e^{-(t-t_k)/\\tau_{syn}}\\Theta(t-t_k)$$\n", - "\n", - "* $W_{i, j}$ is the weight between the the $i^{\\rm th}$ pre-synaptic neuron and the $j^{\\rm th}$ post-synaptic neuron\n", - "\n", - "* $t_k$ is the timing of each incident spike\n", - "\n", - "* $\\Theta(t)$ is the Heaviside step function, which clips the exponential term such that the contribution from each presynaptic spike commences at $t_k$\n", - "\n", - "* $\\tau_{syn}$ is the time constant of the synaptic current, independent of the membrane potential time constant\n", - "\n", - "The time constant $\\tau_{syn}$ can be equivalently represented by a decay rate $\\alpha$ that specifies the ratio of synaptic current between subsequent time steps:\n", - "\n", - "$$\\alpha = \\frac{e^{-\\frac{1}{\\tau_{syn}}}}{e^{-\\frac{0}{\\tau_{syn}}}} = \\frac{e^{-\\frac{2}{\\tau_{syn}}}}{e^{-\\frac{1}{\\tau_{syn}}}} = \\frac{e^{-\\frac{3}{\\tau_{syn}}}}{e^{-\\frac{2}{\\tau_{syn}}}}=~~...$$\n", - "$$\\implies \\alpha = e^{-\\frac{1}{\\tau_{syn}}}$$\n", - "\n", - "\n", - "When an input spike arrives at the neuron, the synaptic current will jump up $W_{i,j}S_{\\rm in}(t=t_k)$, where $S_{\\rm in}(t=t_k)=1$. \n", - "\n", - "That is to say: $\\Delta I_{\\rm syn}(t=t_k) = W_{i, j}$\n", - "\n", - "\n", - "\n", - "In summary, each spike contributes a shifted exponential decay to the synaptic current $I_{\\rm syn}$, which are all summed together. This current is then integrated by the passive membrane equation derived in the previous section, thus generating output spikes.\n", - "\n", - "If the math doesn't make sense, don't worry about it. A graphical intuition is usually sufficient to understand the essence of the synaptic conductance-based neuron model. \n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "This model has the same optional input arguments of `reset_mechanism` and `threshold` as described for Lapicque's neuron model." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "P-m632rfMXIb" - }, - "source": [ - "# 3. A Feedforward Spiking Neural Network" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "iTV4g4JjMgef" - }, - "source": [ - "So far, we have only considered how one neuron reacts to a single input stimulus. snnTorch makes it extremely straightforward to scale this up to a deep neural network. Here, we will create a 3-layer fully-connected neural network of dimensions 784-1000-10.\n", - "\n", - "Compared to our simulations so far, each neuron will now integrate over many more incoming input spikes. \n", - "\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "We will use PyTorch to form the connections between neurons, and snnTorch is used to create the neurons." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "whg4LFGFNuH1" - }, - "source": [ - "import torch \n", - "import torch.nn as nn" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "pypN9pglN2C-" - }, - "source": [ - "First, initialize all layers." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "LDE2ciwCOGa-" - }, - "source": [ - "num_inputs = 784\n", - "num_hidden = 1000\n", - "num_outputs = 10\n", - "\n", - "# initialize layers\n", - "fc1 = nn.Linear(num_inputs, num_hidden)\n", - "lif1 = snn.Synaptic(alpha=alpha, beta=beta)\n", - "fc2 = nn.Linear(num_hidden, num_outputs)\n", - "lif2 = snn.Synaptic(alpha=alpha, beta=beta)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "3bh1Op3bOeLk" - }, - "source": [ - "Next, initialize the hidden variables and outputs of each spiking neuron. \n", - "As your networks increase in size, this will become a tedious process. So we can call a static method `init_synaptic()` to take care of this. All neurons in snnTorch have their own initialization methods that follow this same syntax, e.g., `init_lapicque()`. The shape of the hidden states are automatically initialized based on the input data dimensions during the first forward pass. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "p3X1_5puO3W7" - }, - "source": [ - "# Initialize hidden states\n", - "syn1, mem1 = lif1.init_synaptic()\n", - "syn2, mem2 = lif2.init_synaptic()\n", - "\n", - "# Lists to record output traces\n", - "mem2_rec = []\n", - "spk1_rec = []\n", - "spk2_rec = []" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "jotASwAQQmVJ" - }, - "source": [ - "Create an input spike train to pass into the network. There are 200 time steps to simulate across 784 input neurons. We 'unsqueeze' the input along `dim=1` to denote this to be 'one batch' of data. So the dimensions of this input tensor must be 200 $\\times$ 1 $\\times$ 784:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "ON4KKAjTQzf1" - }, - "source": [ - "spk_in = spikegen.rate_conv(torch.rand((200, 784))*0.1).unsqueeze(1)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "CREME2dLPo-G" - }, - "source": [ - "Now it's finally time to run a full simulation. \n", - "An intuitive way to think about how PyTorch and snnTorch work together is that PyTorch routes the neurons together, and snnTorch loads the results into spiking neuron models. In terms of coding up a network, these spiking neurons can be treated like time-varying activation functions.\n", - "\n", - "Recall that the output of a spiking neuron is $S_{\\rm out}=1$ when a spike is triggered. This spike is then passed to the next layer. It is multiplied by the weight initialized by `nn.Linear` $S_{\\rm out; i}\\times W_{i, j}$, just as the output activation of a standard artificial neuron would be in a non-spiking neural network. The weighted spike is then passed as the input to the next layer of neurons for a given time step. If there is no spike, then nothing is passed to the post-synaptic neuron.\n", - "\n", - "The only difference from our simulations thus far is that we sequentially pass the output through additional layers of neurons. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "-5Zron7aNkxH" - }, - "source": [ - "for step in range(num_steps):\n", - " cur1 = fc1(spk_in[step])\n", - " spk1, syn1, mem1 = lif1(cur1, syn1, mem1)\n", - " cur2 = fc2(spk1)\n", - " spk2, syn2, mem2 = lif2(cur2, syn2, mem2)\n", - "\n", - " mem2_rec.append(mem2)\n", - " spk1_rec.append(spk1)\n", - " spk2_rec.append(spk2)\n", - "\n", - "# convert output recordings to tensors\n", - "mem2_rec = torch.stack(mem2_rec)\n", - "spk1_rec = torch.stack(spk1_rec)\n", - "spk2_rec = torch.stack(spk2_rec)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xGpWl-S2YAyS" - }, - "source": [ - "At this stage, the spikes don't have any real meaning. The inputs and weights are all randomly initialized, and no training has taken place. But let's take a look at the raster plots just to check that the spikes are propagating to the output layer." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "8FJuQGKfWEmR" - }, - "source": [ - "# Generate Plots\n", - "fig, ax = plt.subplots(3, figsize=(8,7), sharex=True, \n", - " gridspec_kw = {'height_ratios': [1, 1, 0.4]})\n", - "\n", - "# Plot input spikes\n", - "splt.raster(spk_in[:,0], ax[0], s=0.05, c=\"black\")\n", - "ax[0].set_ylabel(\"Input Spikes\")\n", - "ax[0].set_title(\"Fully Connected Spiking Neural Network\")\n", - "\n", - "# Plot hidden layer spikes\n", - "splt.raster(spk1_rec.reshape(num_steps, -1), ax[1], s = 0.05, c=\"black\")\n", - "ax[1].set_ylabel(\"Hidden Layer\")\n", - "\n", - "# Plot output spikes\n", - "splt.raster(spk2_rec.reshape(num_steps, -1), ax[2], c=\"black\", marker=\"|\")\n", - "ax[2].set_ylabel(\"Output Spikes\")\n", - "ax[2].set_ylim([0, 10])\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "jfZwr307P5-u" - }, - "source": [ - "We can also use `spikeplot.spike_count` to generate a spike counter of the output layer.
\n", - "Note: if you are running the notebook locally on your desktop, please uncomment the line below and modify the path to your ffmpeg.exe" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "fAXeoiyGOm1H" - }, - "source": [ - "from IPython.display import HTML\n", - "\n", - "fig, ax = plt.subplots(facecolor='w', figsize=(12, 7))\n", - "labels=['0', '1', '2', '3', '4', '5', '6', '7', '8','9']\n", - "spk2_rec = spk2_rec.squeeze(1).detach().cpu()\n", - "\n", - "# plt.rcParams['animation.ffmpeg_path'] = 'C:\\\\path\\\\to\\\\your\\\\ffmpeg.exe'\n", - "\n", - "# Plot spike count histogram\n", - "anim = splt.spike_count(spk2_rec, fig, ax, labels=labels, animate=True)\n", - "HTML(anim.to_html5_video())\n", - "# anim.save(\"spike_bar.gif\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xUXpWaPybVkd" - }, - "source": [ - "We can also visualize the membrane potential traces with `spikeplot.traces`. We'll plot 9 out of 10 output neurons. Compare it to the animation and raster plot above to see if you can match the traces to the neuron. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "SpqvImMPxO36" - }, - "source": [ - "splt.traces(mem2_rec.squeeze(1), spk=spk2_rec.squeeze(1))\n", - "\n", - "fig = plt.gcf() \n", - "fig.set_size_inches(8, 6)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "z98Y9ucNac3e" - }, - "source": [ - "# 4. Alpha Neuron Model (Hacked Spike Response Model)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "te-QotkqakDL" - }, - "source": [ - "To finish up this tutorial, a recursive version of the Spike Response Model (SRM), or 'Alpha' neuron, is also available, called using `snntorch.Alpha`. The neuron models thus far have all been based on the passive membrane model, using ordinary differential equations to describe their dynamics.\n", - "\n", - "The SRM family of models, on the other hand, is interpreted in terms of a filter. Upon the arrival of an input spike, this spike is convolved with the filter to give the membrane potential response. The form of this filter can be exponential, as is the case with Lapicque's neuron, or they can be more complex such as a sum of exponentials. SRM models are appealing as they can arbitrarily add refractoriness, threshold adaptation, and any number of other features simply by embedding them into the filter. \n", - "\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "\n", - " \n", - "\n", - "Formally, this process is represented by:\n", - "\n", - "$$U_{\\rm mem}(t) = \\sum_i W_{i, j}(\\epsilon * S_{\\rm in; i,j})(t)$$\n", - "\n", - "where the incoming spikes $S_{\\rm in; i,j}$ are convolved with a spike response kernel $\\epsilon( \\cdot )$. The spike response is scaled by a synaptic weight, $W_{i, j}$. In the figures above, the left kernel is an exponentially decaying function and would be the equivalent of Lapicque's neuron model. On the right, the kernel is an alpha function.\n", - "\n", - "In snnTorch, the spike response model is not directly implemented as a filter. Instead, it is recast into a recursive form such that only the previous time step of values are required to calculate the next set of values. This significantly reduces the memory overhead during learning.\n", - "\n", - "The filter adopted is unsurprisingly the alpha function on the right animation above, or equivalently a sum of two exponentials. This results in a membrane potential which peaks at some time delay $t_d$ after the input spike. This is often a desirable feature when training networks that rely on spike timing.\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "\n", - "As the membrane potential is now determined by the sum of two exponentials, each of these exponents has their own independent decay rate. $\\alpha$ defines the decay rate of the positive exponential, and $\\beta$ defines the decay rate of the negative exponential. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "3OiADDv7Z_z7" - }, - "source": [ - "alpha = 0.8\n", - "beta = 0.7\n", - "\n", - "# initialize neuron\n", - "lif6 = snn.Alpha(alpha=alpha, beta=beta, threshold=0.5)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "K8yVKbpyZV4J" - }, - "source": [ - "Using this neuron is the same as the previous neurons, but the sum of two exponential functions requires the synaptic current `syn` to be split into a `syn_exc` and `syn_inh` component:\n", - "\n", - "**Inputs**\n", - "* `spk_in`: each input voltage spike $S_{\\rm in}[t]$ is sequentially passed in\n", - "* `syn_exc`: excitatory post-synaptic current $I_{\\rm syn_exc}[t]$ at the present time $t$\n", - "* `syn_inh`: inhibitory post-synaptic current $I_{\\rm syn_inh}[t]$ at the present time $t$\n", - "* `mem`: membrane potential $U_{\\rm mem}[t]$ at the present time $t$\n", - "\n", - "**Outputs**\n", - "* `spk_out`: output spike $S_{\\rm out}[t+1]$ at the next time step ('1' if there is a spike; '0' if there is no spike)\n", - "* `syn_exc`: excitatory post-synaptic $I_{\\rm syn_exc}[t+1]$ at the next time step $t$\n", - "* `syn_inh`: inhibitory post-synaptic current $I_{\\rm syn_inh}[t+1]$ at the next time step $t$\n", - "* `mem`: membrane potential $U_{\\rm mem}[t+1]$ at the next time step\n", - "\n", - "As with all other neuron models, these must be of type `torch.Tensor`." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "Bf2fjxBZajeT" - }, - "source": [ - "# input spike: initial spike, and then period spiking \n", - "spk_in = (torch.cat((torch.zeros(10), torch.ones(1), torch.zeros(89), (torch.cat((torch.ones(1), torch.zeros(9)),0).repeat(10))), 0) * 0.85).unsqueeze(1)\n", - "print(f\"spk_in contains {spk_in.size(1)} sample of data across {spk_in.size(0)} time steps.\")\n", - "\n", - "# initialize parameters - arg '1' is passed to indicate just one sample of data\n", - "syn_exc, syn_inh, mem = lif6.init_alpha()\n", - "mem_rec = []\n", - "spk_rec = []\n", - "\n", - "# run simulation\n", - "for step in range(num_steps):\n", - " spk_out, syn_exc, syn_inh, mem = lif6(spk_in[step], syn_exc, syn_inh, mem)\n", - "\n", - " mem_rec.append(mem.squeeze(0))\n", - " spk_rec.append(spk_out.squeeze(0))\n", - "\n", - "mem_rec = torch.stack(mem_rec)\n", - "spk_rec = torch.stack(spk_rec)\n", - "\n", - "# Generate Plots\n", - "fig, ax = plt.subplots(3, figsize=(8,6), sharex=True, \n", - " gridspec_kw = {'height_ratios': [0.4, 1, 0.4]})\n", - "\n", - "# Plot input current\n", - "splt.raster(spk_in, ax[0], s=400, c=\"black\", marker=\"|\")\n", - "ax[0].set_ylabel(\"Input Spikes\")\n", - "ax[0].set_title(\"Alpha Neuron Model With Input Spikes\")\n", - "ax[0].set_yticks([]) \n", - "\n", - "# Plot membrane potential\n", - "ax[1].plot(mem_rec.detach())\n", - "ax[1].set_ylim([0, 0.6])\n", - "ax[1].set_ylabel(\"Membrane Potential ($U_{mem}$)\")\n", - "ax[1].axhline(y=0.5, alpha=0.25, linestyle=\"dashed\", c=\"black\", linewidth=2)\n", - "plt.xlabel(\"Time step\")\n", - "\n", - "# Plot output spike using spikeplot\n", - "splt.raster(spk_rec, ax[2], s=400, c=\"black\", marker=\"|\")\n", - "ax[2].set_yticks([])\n", - "ax[2].set_ylabel(\"Output Spikes\")\n", - "\n", - "plt.show()" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "63V7WsZFa_Uo" - }, - "source": [ - "As with the Lapicque and Synaptic models, the Alpha model also has options to modify the threshold and reset mechanism." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PYWhh7idiS9t" - }, - "source": [ - "# Conclusion" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BGCY7g7R0Usy" - }, - "source": [ - "Now you should understand the basics of several LIF neuron models, how to simulate them, and how to build your own feedforward spiking neural networks.\n", - "\n", - "For reference, the documentation [can be found here](https://snntorch.readthedocs.io/en/latest/snntorch.html).\n", - "\n", - "In the next tutorial, you will learn how to train these networks to classify spiking and static MNIST datasets." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OTRCyd0Xa-QK" - }, - "source": [ - "## Further Reading\n", - "* [snnTorch documentation](https://snntorch.readthedocs.io/en/latest/snntorch.html) of the Lapicque, Leaky, Synaptic, and Alpha models\n", - "* [*Neuronal Dynamics:\n", - "From single neurons to networks and models of cognition*](https://neuronaldynamics.epfl.ch/index.html) by\n", - "Wulfram Gerstner, Werner M. Kistler, Richard Naud and Liam Paninski.\n", - "* [Theoretical Neuroscience: Computational and Mathematical Modeling of Neural Systems](https://mitpress.mit.edu/books/theoretical-neuroscience) by Laurence F. Abbott and Peter Dayan" - ] - } - ] -} \ No newline at end of file diff --git a/examples/legacy/tutorial_3_FCN.ipynb b/examples/legacy/tutorial_3_FCN.ipynb deleted file mode 100644 index bf4f77f0..00000000 --- a/examples/legacy/tutorial_3_FCN.ipynb +++ /dev/null @@ -1,1095 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "uSGZ6cdmpknm", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "\n", - "\n", - "# snnTorch - Deep Learning with ``snntorch``\n", - "## Tutorial 3\n", - "### By Jason K. Eshraghian (www.jasoneshraghian.com)\n", - "\n", - "\n", - " \"Open\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Ymi3sqJg28OQ" - }, - "source": [ - "# Introduction\n", - "In this tutorial, you will:\n", - "* Learn how spiking neurons are implemented in a recurrent network\n", - "* Understand backpropagation through time, and the associated challenges in SNNs such as target labeling, and the non-differentiability of spikes\n", - "* Train a fully-connected network on the static MNIST dataset\n", - "\n", - "\n", - "\n", - ">Part of this tutorial was inspired by Friedemann Zenke's extensive work on SNNs. Check out his repo on surrogate gradients [here](https://github.com/fzenke/spytorch), and a favourite paper of mine: E. O. Neftci, H. Mostafa, F. Zenke, [Surrogate Gradient Learning in Spiking Neural Networks: Bringing the Power of Gradient-based optimization to spiking neural networks.](https://ieeexplore.ieee.org/document/8891809) IEEE Signal Processing Magazine 36, 51–63.\n", - "\n", - "As a quick recap, [Tutorial 1](https://colab.research.google.com/github/jeshraghian/snntorch/blob/tutorials/examples/tutorial_1_spikegen.ipynb) explained how to convert datasets into spikes using three encoding mechanisms:\n", - "* Rate coding\n", - "* Latency coding\n", - "* Delta modulation\n", - "\n", - "[Tutorial 2](https://colab.research.google.com/github/jeshraghian/snntorch/blob/tutorials/examples/tutorial_2_neuronal_dynamics.ipynb) showed how to build neural networks using three different leaky integrate-and-fire (LIF) neuron models:\n", - "* Lapicque's RC model\n", - "* Synaptic Conductance-based model\n", - "* Alpha neuron model\n", - "\n", - "At the end of the tutorial, a basic supervised learning algorithm will be implemented. We will use the original static MNIST dataset and train a multi-layer fully-connected spiking neural network using gradient descent to perform image classification. \n", - "\n", - "If running in Google Colab:\n", - "* You may connect to GPU by checking `Runtime` > `Change runtime type` > `Hardware accelerator: GPU`\n", - "* Next, install the latest PyPi distribution of snnTorch by clicking into the following cell and pressing `Shift+Enter`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "5tn_wUlopkon", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "!pip install snntorch" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "gt2xMbLY9dVE" - }, - "source": [ - "# 1. A Recurrent Representation of SNNs" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "v7haBG7nA_TC" - }, - "source": [ - "The following is a summary of the continuous time-domain representation LIF neurons, and applies the result to develop a recurrent representation that is more suitable for use in recurrent neural networks (RNNs). \n", - "\n", - "We derived the dynamics of the passive membrane using an RC circuit in the time-domain: \n", - "\n", - "$$ \\tau_{\\rm mem} \\frac{dU_{\\rm mem}(t)}{dt} = -U_{\\rm mem}(t) + RI_{\\rm syn}(t),$$\n", - "\n", - "where the general solution of this equation is:\n", - "\n", - "$$U_{\\rm mem}=I_{\\rm syn}(t)R + [U_0 - I_{\\rm syn}(t)R]e^{-t/\\tau_{\\rm mem}}$$\n", - "\n", - "In Lapicque's model, $I_{\\rm syn}(t)$ is also the input current, $I_{\\rm in}(t)$. \n", - "\n", - "In the Synaptic conductance-based model (which we will loosely refer to as the synaptic model), a more biologically plausible approach is taken that ensures $I_{\\rm syn}(t)$ follows an exponential decay as a function of the input:\n", - "\n", - "\n", - "$$I_{\\rm syn}(t) = \\sum_k W_{i,j} S_{in; i,j}(t) e^{-(t-t_k)/\\tau_{syn}}\\Theta(t-t_k)$$\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "The synaptic model has two exponentially decaying terms: $I_{\\rm syn}(t)$ and $U_{\\rm mem}(t)$. The ratio between subsequent terms (i.e., decay rate) of $I_{\\rm syn}(t)$ is set to $\\alpha$, and that of $U_{\\rm mem}(t)$ is set to $\\beta$:\n", - "\n", - "$$ \\alpha = e^{-1/\\tau_{\\rm syn}}$$\n", - "\n", - "$$ \\beta = e^{-1/\\tau_{\\rm mem}}$$\n", - "\n", - "\n", - "RNNs will process data sequentially, and so time must be discretised, and the neuron models must be converted into a recursive form. $\\alpha$ and $\\beta$ can be used to give a recursive representation of the Synaptic neuron model:\n", - "\n", - "$$I_{\\rm syn}[t+1]=\\underbrace{\\alpha I_{\\rm syn}[t]}_\\text{decay} + \\underbrace{WS_{\\rm in}[t+1]}_\\text{input}$$\n", - "\n", - "$$U[t+1] = \\underbrace{\\beta U[t]}_\\text{decay} + \\underbrace{I_{\\rm syn}[t+1]}_\\text{input} - \\underbrace{R[t+1]}_\\text{reset}$$\n", - "\n", - "**Spiking**\n", - "\n", - "If $U[t] > U_{\\rm thr}$, then an output spike is triggered: $S_{\\rm out}[t] = 1$. Otherwise, $S_{\\rm out}[t] = 0$. \n", - "\n", - "> Note: A variation of this is to set the output spike at the *next* time step to be triggered; i.e., $U[t] > U_{\\rm thr} \\implies S_{\\rm out}[t+1] = 1$. This is the approach taken in snnTorch, and will be explained in following sections.\n", - "\n", - "An alternative way to represent the relationship between $S_{\\rm out}$ and $U_{\\rm mem}$, which is also used to calculate the gradient in the backward pass, is:\n", - "\n", - "$$S_{\\rm out}[t] = \\Theta(U_{\\rm mem}[t] - U_{\\rm thr})$$ \n", - "\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "**Reset**\n", - "\n", - "The reset term is activated only when the neuron triggers a spike. That is to say, if $S_{\\rm out}[t+1]=1$:\n", - "\n", - " * For `reset_mechanism=\"subtract\"`: $R[t+1]=U_{\\rm thr}$ \n", - " * For `reset_mechanism=\"zero\"`: $R[t+1]=U[t+1]$\n", - "\n", - "> Note: In snnTorch, the reset will also take a one time step delay such that $R[t+1]$ is activated only when $S_{\\rm out}[t+1]=1$\n", - "\n", - "The other neurons follow a similar form, which is [detailed in the documentation](https://snntorch.readthedocs.io/en/latest/snntorch.html). The recursive neuron equations can be mapped into computation graphs, where the recurrent connections take place with a delay of a single time step, from the state at time $t$ to the state at time $t+1$. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "t256yMrzTU6M" - }, - "source": [ - "An alternative way to represent recurrent models is to unfold the computational graph, in which each component is represented by a sequence of different variables, with one variable per time step. The unfolded form of the Synaptic model is shown below:\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "\n", - "Up until now, the notation used for all variables have had an association with their electrical meanings. As we move from neuronal dynamics to deep learning, we will slightly modify the notation throughout the rest of the tutorial:\n", - "\n", - "* **Input spike:** $S_{\\rm in} \\rightarrow X$\n", - "* **Input current (weighted spike):** $I_{\\rm in} \\rightarrow Y$\n", - "* **Synaptic current:** $I_{\\rm syn} \\rightarrow I$\n", - "* **Membrane potential:** $U_{\\rm mem} \\rightarrow U$\n", - "* **Output spike:** $S_{\\rm out} \\rightarrow S$\n", - "\n", - "The benefit of an unrolled graph is that we now have an explicit description of how computations are performed. The process of unfolding illustrates the flow of information forward in time (from left to right) to compute outputs and losses, and backward in time to compute gradients. The more time steps that are simulated, the deeper the graph becomes. \n", - "\n", - "Conventional RNNs treat $\\alpha$ and $\\beta$ as learnable parameters. This is also possible for SNNs, but in snnTorch, they are treated as hyperparameters by default. This replaces the vanishing and exploding gradient problems with a parameter search." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zqJdfllYbc16" - }, - "source": [ - "# 2. Setting up the Static MNIST Dataset" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "SuOWJNEMe8l_" - }, - "source": [ - "Much of the following code has already been explained in the first two tutorials. So we'll dive straight in. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "nbunSP5TbikZ" - }, - "source": [ - "## 2.1 Import packages and setup the environment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "bEFWu3nNpkoq", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "import torch\n", - "import torch.nn as nn\n", - "from torch.utils.data import DataLoader\n", - "from torchvision import datasets, transforms\n", - "import numpy as np\n", - "import itertools\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "lI0GbgLgpkos", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Network Architecture\n", - "num_inputs = 28*28\n", - "num_hidden = 1000\n", - "num_outputs = 10\n", - "\n", - "# Training Parameters\n", - "batch_size=128\n", - "data_path='/tmp/data/mnist'\n", - "\n", - "# Temporal Dynamics\n", - "num_steps = 25\n", - "alpha = 0.7\n", - "beta = 0.8\n", - "\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "nUS6YFXbpkos", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 2.2 Download MNIST Dataset" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2fhRixcspkot", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define a transform\n", - "transform = transforms.Compose([\n", - " transforms.Resize((28, 28)),\n", - " transforms.Grayscale(),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))])\n", - "\n", - "mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RAM_dP887uTq" - }, - "source": [ - "If the above code blocks throws an error, e.g. the MNIST servers are down, then uncomment the following code instead." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "4jyJVqUNdXDo" - }, - "outputs": [], - "source": [ - "# # temporary dataloader if MNIST service is unavailable\n", - "# !wget www.di.ens.fr/~lelarge/MNIST.tar.gz\n", - "# !tar -zxvf MNIST.tar.gz\n", - "\n", - "# mnist_train = datasets.MNIST(root = './', train=True, download=True, transform=transform)\n", - "# mnist_test = datasets.MNIST(root = './', train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "aEtCbO6upkou", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Create DataLoaders\n", - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GhFyzySNeT_e" - }, - "source": [ - "# 3. Define the Network" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "RJkoAg-3pkow", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "The spiking neurons available in snnTorch are designed to be treated as activation units. The only difference is that these spiking neuron activations depend not only on their inputs, but also on their previous state (e.g., $I[t-1]$ and $U[t-1]$ for the Synaptic neuron). This can be implemented in a for-loop with ease.\n", - "\n", - "If you have a basic understanding of PyTorch, the following code block should look familiar. `nn.Linear` initializes the linear transformation layer, and instead of applying a sigmoid, ReLU or some other nonlinear activation, a spiking neuron is applied instead by calling `snn.Synaptic`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "-uquHLLmpkox", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define Network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # Initialize layers\n", - " self.fc1 = nn.Linear(num_inputs, num_hidden)\n", - " self.lif1 = snn.Synaptic(alpha=alpha, beta=beta)\n", - " self.fc2 = nn.Linear(num_hidden, num_outputs)\n", - " self.lif2 = snn.Synaptic(alpha=alpha, beta=beta)\n", - "\n", - " def forward(self, x):\n", - "\n", - " # Initialize hidden states and outputs at t=0\n", - " syn1, mem1 = self.lif1.init_synaptic()\n", - " syn2, mem2 = self.lif2.init_synaptic()\n", - " \n", - " # Record the final layer\n", - " spk2_rec = []\n", - " mem2_rec = []\n", - "\n", - " for step in range(num_steps):\n", - " cur1 = self.fc1(x)\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = self.fc2(spk1)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - "\n", - " spk2_rec.append(spk2)\n", - " mem2_rec.append(mem2)\n", - "\n", - " return torch.stack(spk2_rec, dim=0), torch.stack(mem2_rec, dim=0)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Y0fHcAKfrav6" - }, - "source": [ - "The code in the `forward()` function will only be called once the input argument `x` is explicitly passed in:\n", - "\n", - "* `fc1` applies a linear transformation to the input: $W_{i, j}^{[1]}X_{i}^{[1]}[t] \\rightarrow Y_{j}^{[1]}[t]$, i.e., `cur1`\n", - "* `lif1` integrates $Y^{[1]}_{j}[t]$ over time (with a decay), to generate $I_{j}^{[1]}[t]$ and $U_{j}^{[1]}[t]$. An output spike is triggered if $U_{j}^{[1]}[t] > U_{\\rm thr}$. Equivalently, `spk1=1` if `mem1` > `threshold=1.0`\n", - "* `fc2` applies a linear transformation to `spk1`: $W_{j, k}^{[2]}S_{j}^{[1]}[t] \\rightarrow Y_{k}^{[2]}[t]$, i.e., `cur2`\n", - "* `lif2` is another spiking neuron layer, and generates output spikes $S_{k}^{[2]}[t]$ which are returned in the variable `spk2`\n", - "\n", - "Here, $i$ denotes one of 784 input neurons, $j$ indexes one of the 1,000 neurons in the hidden layer, and $k$ points to one of 10 output neurons.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "3-yJXOSQqANb" - }, - "source": [ - "The layers in `def __init__(self)` are automatically created upon instantiating `Net()`, as is done below:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "EkTWSXj5fj2V" - }, - "outputs": [], - "source": [ - "# Load the network onto CUDA if available\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6a7MdORCtIx4" - }, - "source": [ - "# 4. Backpropagation for SNNs" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "ZlrNIMNnpkoy", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "A few questions arise when setting up a backprop-driven learning algorithm:\n", - "\n", - "1. **Targets**: What should the target of the output layer be?\n", - "2. **Backprop through time**: How might the gradient flow back in time?\n", - "3. **Spike non-differentiability**: If spikes are discrete, instantaneous bursts of information, doesn't that make them non-differentiable? If the output spike has no gradient with respect to the network parameters, wouldn't backprop be impossible?\n", - "\n", - "Let's tackle these one by one. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "NpJhgA6n8LPt" - }, - "source": [ - "## 4.1 Target Labels" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "k0At_jfe8OLY" - }, - "source": [ - "In [tutorial 1](https://colab.research.google.com/github/jeshraghian/snntorch/blob/tutorials/examples/tutorial_1_spikegen.ipynb), we learnt about rate and latency coding. Rate coding stores information in the frequency of spikes, and latency coding stores information in the timing of each spike. Previously, we used these encoding strategies to convert datasets into time-varying spikes. Here, they are used as encoding strategies for the output layer of our SNN. I.e., these codes will be used to teach the final layer of the network how to respond to certain inputs. \n", - "\n", - "The goal of the SNN is to predict a discrete variable with $n$ possible values, as is the case with MNIST where $n=10$. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OnSx21wvAoee" - }, - "source": [ - "### 4.1.1 Rate code" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4kR56xe3Ari3" - }, - "source": [ - "For rate encoding, the most naive implementation is to encourage the correct class to fire at every time step, and the incorrect classes to not fire at all. There are two ways to implement this, one of which is a lot more effective than the other:\n", - "\n", - "* Set the target of the output spike of the correct class $y_{\\rm spk} = 1$ for all $t$, or\n", - "* Set the target of the membrane potential of the correct class $y_{\\rm mem} = U_{\\rm thr}$ for all $t$ \n", - "\n", - "Which is the better approach? \n", - "\n", - "**Spiking Targets**\n", - "\n", - "Consider the first option. The output spikes are discrete events, and rely on large perturbations of the membrane potential around the threshold to have any infleunce. If the output spiking behavior goes unchanged, the gradient of the output of the network with respect to its parameters would be $0$. This is problematic, because the training process would no longer have a guide for how to improve the weights. It would be an ineffective approach for gradient descent. \n", - "\n", - "**Membrane Potential Targets**\n", - "\n", - "Instead, it is better to promote spiking by applying the target to the membrane potential. As the membrane potential is a much stronger function of the parameters, (i.e., a small perturbation of the weights would directly perturb the membrane potential), this would ensure there is a strong gradient whenever the network obtains a wrong result. So we set $y_{\\rm mem} = U_{\\rm thr}$. By default, `threshold=1`. The outputs can then be applied to a softmax unit, which are then used to find the cross-entropy loss:\n", - "\n", - "$$CE = - \\sum^n_{i=1}y_{i,\\rm mem} {\\rm log}(p_i),$$\n", - "\n", - "where $y_{i, \\rm mem}$ is the target label at a given time step, $n$ is the number of classes, and $p_i$ is the softmax probability for the $i^{th}$ class. \n", - "\n", - "The accuracy of the network would then be measured by counting up how many times each neuron fired across all time steps. We could then use `torch.max()` to choose the neuron with the most spikes, or somewhat equivalently, the highest average firing rate. \n", - "\n", - "It is possible to increase the target of membrane potential beyond the threshold to excite the neuron further. While this may be desirable in some instances, it will likely trigger high-conductance pathways for the wrong class when training other samples." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "b-xv_G0_304H" - }, - "source": [ - "*Our classifier will implement the simplest form of rate coding. It will encourage the correct class to fire 100% of time steps, and the incorrect class to fire 0% of the time. Although this is clearly not the most efficient method, it is the simplest.*" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "iknCYQEeCaHG" - }, - "source": [ - "### 4.1.2 Latency code\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "92hSKSBogC0I" - }, - "source": [ - "In latency encoding, the neuron that fires first is the predicted class. The target may be set to 1 for one of the first few time steps. Depending on the neuron model being used, it will take several time steps before the input can propagate to the output of the network. Therefore, it is inadvisable to set the target to `1` only for the first time step. \n", - "\n", - "Consider the case of a neuron receiving an input spike. Depending on the neuron model in use, the post-synaptic potential may experience a time delay $t_{\\rm psp}$ to reach the peak of its membrane potential, and subsequently emit an output spike. If this neuron is connected in a deep neural network, the minimum time before the final layer could generate output spikes *as a result of the input (and not biases)* would thus be $t_{\\rm min} = Lt_{\\rm psp}$, where $L$ is the number of layers in the network. \n", - "\n", - "For the Synaptic and Lapicque models, the membrane potential will immediately jump as a result of the input. But there is a time delay of one step before the output spike can be triggered as a result. Therefore, we set $t_{\\rm psp}=1$ time step. For the Alpha neuron model, it will take a longer time to reach the peak, and is a function of the decay rates, $\\alpha$ and $\\beta$. \n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "In absence of this post-synaptic potential delay, it becomes challenging to control the output layer in terms of spike timing. An input spike of a multi-layer SNN could effectively be transmitted straight to the output instantaneously, without considering the input data at any later time steps. A slight modification is made to the unrolled computational graph, which adds a delay of one time step between $U$ and $S$.\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "UYDvqr1oMHQp" - }, - "source": [ - "\n", - "As for the incorrect classes, it is acceptable to set their targets to 0. However, this could result in low conductance pathways that completely inhibit firing. It may be preferable to set their membrane potential target to something slightly higher, e.g., $U_{\\rm thr}/5$. The optimal point is a topic of further investigation. Note that all of the above can have a cross-entropy loss applied, just as with rate coding.\n", - "\n", - "A simple example across 4 time steps is provided in the image below, though the values and spiking periodicity should not be taken literally.\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "An alternative approach is to treat the number of time steps as a continuous variable and use a mean square error loss to dictate when firing should occur:\n", - "\n", - "$$MSE = \\sum^n_{t=1}(t_{\\rm spk} - \\hat{t_{\\rm spk}}^2),$$\n", - "\n", - "where $t$ is the time step, and $n$ is the total number of steps. In such a case, a larger number of time steps are expected to improve performance as it will allow the flow of time to look more 'continuous'.\n", - "\n", - "Is there a preference between latency and rate codes? We briefly touched on this question in the context of data encoding, and the same arguments apply here. Latency codes are desirable because they only rely on a single spike to convey all necessary information. Rate coding spreads out information across many time steps, and there is much less information transfer within each spike. Therefore, latency codes are much more power efficient when running on neuromorphic hardware. On the other hand, the redundant spikes in rate codes makes them much more noise tolerant. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9SBjiLAX-ZhX" - }, - "source": [ - "## 4.2 Backpropragation Through Time" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "8UYEmLk4-e4G" - }, - "source": [ - "Computing the gradient through an SNN is mostly the same as that of an RNN. The generalized backpropagation algorithm is applied to the unrolled computational graph. Working backward from the end of the sequence, the gradient flows from the loss to all descendents. Shown below are the various pathways of the gradient $\\nabla_W \\mathcal{L}$ from the parent ($\\mathcal{L}$: cross-entropy loss) to its leaf nodes ($W$). \n", - "\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "The learnable parameter $W$ is shared across each time step. This means that multiple backprop paths exist between the loss and the same network parameter. To resolve this, all gradients $\\nabla_W \\mathcal{L}$ are simply summed together before applying a weight update.\n", - "\n", - "To find $\\nabla_W \\mathcal{L}$, the chain rule is applied to each pathway. \n", - "\n", - "**Shortest Pathway** \n", - "\n", - "Considering only the shortest pathway at $t=3$, where the superscript $^{<1>}$ indicates this is just one of many paths to be summed:\n", - "\n", - "$$\\nabla_W \\mathcal{L}^{<1>} = \\frac{\\partial{\\mathcal{L}}}{\\partial{p_i}} \\frac{\\partial{p_i}}{\\partial{U[3]}} \\frac{\\partial{U[3]}}{\\partial{Y[3]}} \\frac{\\partial{Y[3]}}{\\partial{W}}$$\n", - "\n", - "The first two terms can be analytically solved by taking the derivative of the cross-entropy loss and the softmax function. The third term must be decomposed into the following terms:\n", - "\n", - "$$ \\frac{\\partial{U[3]}}{\\partial{Y[3]}} = \\frac{\\partial{U[3]}}{\\partial{I[3]}} \\frac{\\partial{I[3]}}{\\partial{Y[3]}}$$\n", - "\n", - "Recall the recursive form of the Synaptic neuron model:\n", - "\n", - "\n", - "$$I[t+1]=\\alpha I[t] + WX[t+1]$$\n", - "\n", - "$$U[t+1] = \\beta U[t] + I[t+1] - R[t+1]$$\n", - "\n", - "$WX=Y$ is directly added to $I$, which is directly added to $U$. Therefore, both partial derivative terms evaluate to 1:\n", - "\n", - "$$\\frac{\\partial{U[3]}}{\\partial{Y[3]}} = 1$$\n", - "\n", - "The final term $ \\frac{\\partial{Y[3]}}{\\partial{W}}$ evaluates to the input at that time step $X[3]$. \n", - "\n", - "**2nd Shortest Pathways**\n", - "\n", - "Consider the pathway that flows backwards one time step from $t=3$ to $t=2$ through $\\beta$:\n", - "\n", - "$$\\nabla_W \\mathcal{L}^{<2>} = \\frac{\\partial{\\mathcal{L}}}{\\partial{p_i}} \\frac{\\partial{p_i}}{\\partial{U[3]}} \\frac{\\partial{U[3]}}{\\partial{U[2]}} \n", - "\\frac{\\partial{U[2]}}{\\partial{Y[2]}} \\frac{\\partial{Y[2]}}{\\partial{W}} $$\n", - "\n", - "Almost all terms are the same as the shortest pathway calculation, or at least evaluate to the same values. The only major difference is the third term, which signals the backwards flow through time: $U[3] \\rightarrow U[2]$. The derivative is simply $\\beta$. \n", - "\n", - "The parallel pathway flowing through $I[3] \\rightarrow I[2]$ follows the same method, but instead, $\\frac{\\partial{I[3]}}{\\partial{I[2]}} = \\alpha$. \n", - "\n", - "An interesting result arises: for each additional time step the graph flows through, the smaller that component of the gradient becomes. This is because each backwards path is recursively multiplied by either $\\alpha$ or $\\beta$, which gradually diminish the contribution of earlier states of the network to gradient.\n", - "\n", - "Luckily for you, all of this is automatically taken care of by PyTorch's autodifferentiation framework. Variations of backprop through time are also available within snnTorch, which will be demonstrated in future tutorials.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "c7nYrxNFLybB" - }, - "source": [ - "## 4.3 Non-differentiability of Spikes" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "yXWeOeqZ5S_r" - }, - "source": [ - "The above analysis only solved for parameter updates for the final layer. This was not an issue as we used membrane potential $U$ to calculate the loss, which is a continuous function. If we backpropagate to earlier layers, we need to take the derivative of spikes, i.e., a non-differentiable, non-continuous function.\n", - "\n", - "Let's open up the computational graph of the Synaptic neuron model to identify exactly where this problem occurs.\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "Backpropagating through the shortest path gives:\n", - "$$\\frac{\\partial{S[3]}}{\\partial{Y[2]}} = \\frac{\\partial{S[3]}}{\\partial{U[2]}} \\frac{\\partial{U[2]}}{\\partial{I[2]}}\\frac{\\partial{I[2]}}{\\partial{Y[2]}}$$\n", - "\n", - "The final two terms evaluate to 1 for the same reasons described above. But the first term is non-differentiable. Recall how $S=1$ only for $U>U_{\\rm thr}$, i.e., a shifted form of the Heaviside step function. The analytical derivative evaluates to 0 everywhere, except at $U_{\\rm thr}: \\frac{\\partial{S[t]}}{\\partial{U[t-1]}} \\rightarrow \\infty$. This is the result generated by PyTorch's default autodifferentiation framework, and will zero out the gradient thus immobilizing the network's ability to learn:\n", - "\n", - "$$W := W - \\eta \\nabla_W \\mathcal{L} $$\n", - "\n", - "where $\\nabla_W \\mathcal{L} \\rightarrow 0$. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ktpVgJisQU03" - }, - "source": [ - "How do we overcome this issue? Several approaches have been taken and yielded great results. Smooth approximations of the Heaviside function have been used, taking gradients of the continuous function instead. Friedemann Zenke's extensive work on surrogate gradients is among the most rigorous on this topic, and is [very well documented here](https://github.com/fzenke/spytorch). The option to use surrogate gradients is available in snnTorch as well, and can be called from the `snntorch.surrogate` library. More details are available [here](https://snntorch.readthedocs.io/en/latest/snntorch.surrogate.html).\n", - "\n", - "snnTorch takes a wholly different approach that is simple, yet effective. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "iBHz22CdgE0k" - }, - "source": [ - "## 4.3.1 A Time-Evolution Approach to the Spiking Derivative" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "r8W_hVKkf4HM" - }, - "source": [ - "What follows is a simple, intuitive description behind the approach taken. A rigorous mathematical treatment will be made available separately. \n", - "\n", - "The analytical derivative of $S$ with respect to $U$ neglects two features of spiking neurons:\n", - "\n", - "* the discrete time representation of SNNs \n", - "* spike-induced reset and refractory periods of neurons\n", - "\n", - "**Discrete Time Representation**\n", - "\n", - "Given that SNNs (and more generally, RNNs) operate in discrete time, we can approximate the derivative to be the relative change across 1 time step:\n", - "\n", - "$$\\frac{\\partial S}{\\partial U} \\rightarrow \\frac{\\Delta S}{\\Delta U}$$\n", - "\n", - "Intuitively, the time derivative cannot be calculated by letting $\\Delta t \\rightarrow 0$, but rather, it must approach the smallest possible value $\\Delta t \\rightarrow 1$. It therefore follows that the derivative of a time-varying pair of functions must be treated similarly.\n", - "\n", - "**Spike-induced Reset**\n", - "\n", - "Next, the occurrence of a spike necessarily incurs a membrane potential reset. So when the spike mechanism switches off: $S: 1 \\rightarrow 0$, the membrane potential resets by subtraction of the threshold, which is set to one by default: $\\Delta U = U_{\\rm thr} \\rightarrow -1$:\n", - "\n", - "$$\\frac{\\Delta S}{\\Delta U} = \\frac{-1}{-1} = 1$$\n", - "\n", - "This situation is illustrated below:\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "If instead there is no spike, then $\\Delta S = 0$ for a finite change in $U$. Formally:\n", - "\n", - "\\begin{equation}\n", - " \\frac{\\partial S}{\\partial U} \\approx \\Theta(U - U_{\\rm thr}) = \n", - " \\begin{cases}\n", - " 1 & \\text{if $S$ = $1$}\\\\\n", - " 0 & \\text{if $S$ = $0$}\n", - " \\end{cases} \n", - "\\end{equation}\n", - "\n", - "This is simply the Heaviside step function shifted about the membrane threshold, $U_{\\rm thr} = \\theta$.\n", - "\n", - "
\n", - "\n", - "
\n", - "\n", - "What this suggests is that learning only takes place when neurons fire. This is generally not a concern, as a large enough network will have sufficient spiking to enable a gradient to flow through the computational graph. Armed with the knowledge that weight updates only take place when neurons fire, this approach echoes a rudimentary form of Hebbian learning.\n", - "\n", - "Importantly, the situation is more nuanced than what has been described above. But this should be sufficient to give you the big picture intuition. As a matter of interest, the Heaviside gradient takes a similar approach to how the gradient flows through a max-pooling unit, and also evaluates to the same derivative as a shifted ReLU activation. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "uDFwKmN9en4i" - }, - "source": [ - "# 5. Training on Static MNIST" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6D-fhT3Q7nXM" - }, - "source": [ - "\n", - "Time for training! Let's first define a couple of functions to print out test/train accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "-IxcnBAxpkoy", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " output, _ = net(data.view(batch_size, -1))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(data_it, targets_it, train=True)\n", - " print_batch_accuracy(testdata_it, testtargets_it, train=False)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "OxfhunW6pkoz", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 5.1 Optimizer" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "T7ULsfh9bHr1" - }, - "source": [ - "We will apply a softmax to the output of our network, and calculate the loss using the negative log-likelihood." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "iqdVyjCNtdlp" - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GiqAVKzVbfPn" - }, - "source": [ - "## 5.2 Training Loop" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "yGDs_dF2e1Sx" - }, - "source": [ - "We assume some working knowledge of PyTorch. The training loop is fairly standard, with the only exceptions being the following.\n", - "\n", - "**Inputs**\n", - "\n", - "The for-loop that iterates through each time step during the forward pass has already been nested within `net`. This means that the following line of code:\n", - "\n", - "`spk_rec, mem_rec = net(data_it.view(batch_size, -1))`\n", - "\n", - "passes the same sample at each step. That is why we refer to it as static MNIST.\n", - "\n", - "\n", - "**Targets**\n", - "\n", - "The losses generated at each time steps are summed together in the for-loop that contains:\n", - "\n", - "`loss_val += loss_fn(log_p_y[step], targets_it)`\n", - "\n", - "Also note how `targets_it` is not indexed, because the same value is used as the target for each step. '1' is applied as the target for the correct class for all of time, and '0' is applied as the target for all other classes.\n", - "\n", - "Let's train this across 3 epochs to keep things quick." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "LMZMxEV8dcTC" - }, - "outputs": [], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(3):\n", - " minibatch_counter = 0\n", - " train_batch = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in train_batch:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " spk_rec, mem_rec = net(data_it.view(batch_size, -1))\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros((1), dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps: BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward()\n", - "\n", - " # Weight Update\n", - " optimizer.step()\n", - "\n", - " # Store loss history for future plotting\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set forward pass\n", - " test_spk, test_mem = net(testdata_it.view(batch_size, -1))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, testtargets_it)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Taf6WZLojHTz" - }, - "source": [ - "If this was your first time training an SNN, then congratulations!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "HxU7P7xFpko3", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# 6. Results\n", - "## 6.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "_Pk_EScnpkpj", - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Train Loss\", \"Test Loss\"])\n", - "plt.xlabel(\"Iteration\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "g-Gd84OAl1rB" - }, - "source": [ - "Taking a look at the training / test loss, the process is somewhat noisy. This could be a result of a variety of things: minibatch gradient descent is the obvious one, but the use of improper targets likely also contributes. By encouraging the correct class to fire at every time step, the loss function conflicts with the reset mechanism that tries to prevent this." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "Z3f0vBnBpkpk", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 6.2 Test Set Accuracy\n", - "This function iterates over all minibatches to obtain a measure of accuracy over the full 10,000 samples in the test set." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "F5Rb4xHGndQh" - }, - "outputs": [], - "source": [ - "total = 0\n", - "correct = 0\n", - "\n", - "# drop_last switched to False to keep all samples\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " batch_size = images.size(0) # the final batch has a different size so must be updated\n", - " outputs, _ = net(images.view(batch_size, -1))\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += labels.size(0)\n", - " correct += (predicted == labels).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "id": "TBIXau4Zpkpl", - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Voila! That's it for static MNIST. Feel free to tweak the network parameters, hyperparameters, decay rate, using a learning rate scheduler etc. to see if you can improve the network performance. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "s0dAgWUt2o6E" - }, - "source": [ - "# Conclusion\n", - "Now you know how to construct and train a fully-connected network on a static dataset. The spiking neurons can actually be adapted to other layer types, including convolutions and skip connections. Armed with this knowledge, you should now be able to build many different types of SNNs.\n", - "\n", - "In the next tutorial, you will learn how to train a spiking convolutional network using a time-varying spiking dataset." - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "name": "tutorial_2_FCN_truncatedfromscratch.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/legacy/tutorial_4_CNN.ipynb b/examples/legacy/tutorial_4_CNN.ipynb deleted file mode 100644 index 4495eede..00000000 --- a/examples/legacy/tutorial_4_CNN.ipynb +++ /dev/null @@ -1,981 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# snnTorch - Tutorial 3\n", - "### By Jason K. Eshraghian" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Gradient-based Learning in Convolutional Spiking Neural Networks\n", - "In this tutorial, we'll use a convolutional neural network (CNN) to classify the MNIST dataset.\n", - "We will use the backpropagation through time (BPTT) algorithm to do so. This tutorial is largely the same as tutorial 2, just with a different network architecture to show how to integrate convolutions with snnTorch.\n", - "\n", - "If running in Google Colab:\n", - "* Ensure you are connected to GPU by checking Runtime > Change runtime type > Hardware accelerator: GPU\n", - "* Next, install the Test PyPi distribution of snnTorch by clicking into the following cell and pressing `Shift+Enter`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Install the test PyPi Distribution of snntorch\n", - "!pip install -i https://test.pypi.org/simple/ snntorch" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 1. Setting up the Static MNIST Dataset\n", - "### 1.1. Import packages and setup environment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "import torch\n", - "import torch.nn as nn\n", - "import torch.nn.functional as F\n", - "from torch.utils.data import DataLoader\n", - "from torchvision import datasets, transforms\n", - "import numpy as np\n", - "import itertools\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1.2 Define network and SNN parameters\n", - "We will use a 2conv-2MaxPool-FCN architecture for a sequence of 25 time steps.\n", - "\n", - "* `alpha` is the decay rate of the synaptic current of a neuron\n", - "* `beta` is the decay rate of the membrane potential of a neuron" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Network Architecture\n", - "num_inputs = 28*28\n", - "num_outputs = 10\n", - "\n", - "# Training Parameters\n", - "batch_size=128\n", - "data_path='/tmp/data/mnist'\n", - "\n", - "# Temporal Dynamics\n", - "num_steps = 25\n", - "time_step = 1e-3\n", - "tau_mem = 6.5e-4\n", - "tau_syn = 5.5e-4\n", - "alpha = float(np.exp(-time_step/tau_syn))\n", - "beta = float(np.exp(-time_step/tau_mem))\n", - "\n", - "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.3 Download MNIST Dataset\n", - "To see how to construct a validation set, refer to Tutorial 1." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define a transform\n", - "transform = transforms.Compose([\n", - " transforms.Resize((28, 28)),\n", - " transforms.Grayscale(),\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0,), (1,))])\n", - "\n", - "mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n", - "mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 1.4 Create DataLoaders" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 2. Define Network\n", - "snnTorch contains a series of neuron models and related functions to ease the training process.\n", - "Neurons are treated as activations with recurrent connections, and integrate smoothly with PyTorch's pre-existing layer functions.\n", - "* `snntorch.Stein` is a simple Leaky Integrate and Fire (LIF) neuron. Specifically, it uses Stein's model which assumes instantaneous rise times for synaptic current and membrane potential.\n", - "* `snntorch.FastSigmoidSurrogate` defines separate forward and backward functions. The forward function is a Heaviside step function for spike generation. The backward function is the derivative of a fast sigmoid function, to ensure continuous differentiability.\n", - "FSS is mostly derived from:\n", - "\n", - ">Neftci, E. O., Mostafa, H., and Zenke, F. (2019) Surrogate Gradient Learning in Spiking Neural Networks. https://arxiv.org/abs/1901/09948\n", - "\n", - "There are a few other surrogate gradient functions included.\n", - "`snn.slope` is a variable that defines the slope of the backward surrogate.\n", - "TO-DO: Include visualisation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "spike_grad = snn.FastSigmoidSurrogate.apply\n", - "snn.slope = 50" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we can define our spiking neural network (SNN).\n", - "If you have already worked through Tutorial 2, you may wish to skip ahead.\n", - "\n", - "Creating an instance of the `Stein` neuron requires two compulsory arguments and two optional arguments:\n", - "1. $I_{syn}$ decay rate, $\\alpha$,\n", - "2. $V_{mem}$ decay rate, $\\beta$,\n", - "3. *optional*: the surrogate spiking function, `spike_grad` (*default*: the gradient of the Heaviside function), and\n", - "4. *optional*: the threshold for spiking, (*default*: 1.0).\n", - "\n", - "snnTorch treats the LIF neuron as a recurrent activation. Therefore, it requires initialization of its internal states.\n", - "For each layer, we initialize the synaptic current `syn1` and `syn2`, the membrane potential `mem1` and `mem2`, and the post-synaptic spikes `spk1` and `spk2` to zero.\n", - "A class method `init_stein` will take care of this.\n", - "\n", - "For rate coding, the final layer of spikes and membrane potential are used to determine accuracy and loss, respectively.\n", - "So their historical values are recorded in `spk3_rec` and `mem3_rec`.\n", - "\n", - "Keep in mind, the dataset we are using is just static MNIST. I.e., it is *not* time-varying.\n", - "Therefore, we pass the same MNIST sample to the input at each time step.\n", - "This is handled with `cur1 = F.max_pool2d(self.conv1(x), 2)`, where `x` is the same input over the whole for-loop." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # initialize layers\n", - " self.conv1 = nn.Conv2d(in_channels=1, out_channels=12, kernel_size=5, stride=1, padding=1)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - " self.conv2 = nn.Conv2d(in_channels=12, out_channels=64, kernel_size=5, stride=1, padding=1)\n", - " self.lif2 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - " self.fc2 = nn.Linear(64*5*5, 10)\n", - " self.lif3 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - "\n", - " def forward(self, x):\n", - " # Initialize LIF state variables and spike output tensors\n", - " spk1, syn1, mem1 = self.lif1.init_hidden(batch_size, 12, 13, 13)\n", - " spk2, syn2, mem2 = self.lif1.init_hidden(batch_size, 64, 5, 5)\n", - " spk3, syn3, mem3 = self.lif2.init_hidden(batch_size, 10)\n", - "\n", - " spk3_rec = []\n", - " mem3_rec = []\n", - "\n", - " for step in range(num_steps):\n", - " cur1 = F.max_pool2d(self.conv1(x), 2)\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = F.max_pool2d(self.conv2(spk1), 2)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - " cur3 = self.fc2(spk2.view(batch_size, -1))\n", - " spk3, syn3, mem3 = self.lif3(cur3, syn3, mem3)\n", - "\n", - " spk3_rec.append(spk3)\n", - " mem3_rec.append(mem3)\n", - "\n", - " return torch.stack(spk3_rec, dim=0), torch.stack(mem3_rec, dim=0)\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 3. Training\n", - "Time for training! Let's first define a couple of functions to print out test/train accuracy." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " output, _ = net(data.view(batch_size, 1, 28, 28))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(data_it, targets_it, train=True)\n", - " print_batch_accuracy(testdata_it, testtargets_it, train=False)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.1 Optimizer & Loss\n", - "* *Output Activation*: We'll apply the softmax function to the membrane potentials of the output layer, rather than the spikes.\n", - "* *Loss*: This will then be used to calculate the negative log-likelihood loss.\n", - "By encouraging the membrane of the correct neuron class to reach the threshold, we expect that neuron will fire more frequently.\n", - "The loss could be applied to the spike count as well, but the membrane is continuous whereas spike count is discrete.\n", - "* *Optimizer*: The Adam optimizer is used for weight updates.\n", - "* *Accuracy*: Accuracy is measured by counting the spikes of the output neurons. The neuron that fires the most frequently will be our predicted class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 3.2 Training Loop\n", - "Now just sit back, relax, and wait for convergence." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(5):\n", - "\n", - " minibatch_counter = 0\n", - " train_batch = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in train_batch:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " output, mem_rec = net(data_it.view(batch_size, 1, 28, 28)) # [28x28] or [1x28x28]?\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros((1), dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps to perform BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward(retain_graph=True)\n", - "\n", - " # Weight Update\n", - " nn.utils.clip_grad_norm_(net.parameters(), 1)\n", - " optimizer.step()\n", - "\n", - " # Store loss history for future plotting\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set forward pass\n", - " test_output, test_mem_rec = net(testdata_it.view(batch_size, 1, 28, 28))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem_rec)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, testtargets_it)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 4. Results\n", - "### 4.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Test Loss\", \"Train Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 4.2 Test Set Accuracy\n", - "This function just iterates over all minibatches to obtain a measure of accuracy over the full 10,000 samples in the test set." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "total = 0\n", - "correct = 0\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " # If current batch matches batch_size, just do the usual thing\n", - " if images.size()[0] == batch_size:\n", - " outputs, _ = net(images.view(batch_size, 1, 28, 28))\n", - "\n", - " # If current batch does not match batch_size (e.g., is the final minibatch),\n", - " # modify batch_size in a temp variable and restore it at the end of the else block\n", - " else:\n", - " temp_bs = batch_size\n", - " batch_size = images.size()[0]\n", - " outputs, _ = net(images.view(images.size()[0], 1, 28, 28))\n", - " batch_size = temp_bs\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += labels.size(0)\n", - " correct += (predicted == labels).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "That's it for static MNIST!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 5. Spiking MNIST\n", - "As before, there isn't anything all that impressive about training a network on the static MNIST dataset.\n", - "So let's apply rate-coding to convert it into a time-varying stream of spikes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from snntorch import spikegen\n", - "\n", - "# MNIST to spiking-MNIST\n", - "spike_data, spike_targets = spikegen.rate(data_it, targets_it, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 5.1 Visualiser\n", - "Just so you're damn sure it's a spiking input." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "!pip install celluloid # matplotlib animations made easy" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note: if you are running the notebook locally on your desktop, please uncomment the line below and modify the path to your ffmpeg.exe" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from celluloid import Camera\n", - "from IPython.display import HTML\n", - "\n", - "# Animator\n", - "spike_data_sample = spike_data[:, 0, 0].cpu()\n", - "\n", - "fig, ax = plt.subplots()\n", - "camera = Camera(fig)\n", - "plt.axis('off')\n", - "\n", - "# plt.rcParams['animation.ffmpeg_path'] = 'C:\\\\path\\\\to\\\\your\\\\ffmpeg.exe'\n", - "\n", - "for step in range(num_steps):\n", - " im = ax.imshow(spike_data_sample[step, :, :], cmap='plasma')\n", - " camera.snap()\n", - "\n", - "# interval=40 specifies 40ms delay between frames\n", - "a = camera.animate(interval=40)\n", - "HTML(a.to_html5_video())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "print(spike_targets[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 6. Define Network\n", - "The convolutional network is the same as before. The one difference is that the for-loop iterates through the first dimension of the input:\n", - "`cur1 = F.max_pool2d(self.conv1(x[step]), 2)`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "spike_grad = snn.FastSigmoidSurrogate.apply\n", - "snn.slope = 50\n", - "\n", - "# Define a different network\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - "\n", - " # initialize layers\n", - " self.conv1 = nn.Conv2d(in_channels=1, out_channels=12, kernel_size=5, stride=1, padding=1)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - " self.conv2 = nn.Conv2d(in_channels=12, out_channels=64, kernel_size=5, stride=1, padding=1)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - " self.fc2 = nn.Linear(64*5*5, 10)\n", - " self.lif1 = snn.Stein(alpha=alpha, beta=beta, spike_grad=spike_grad)\n", - "\n", - " # self.conv1 = nn.Conv2d(in_channels=1, out_channels=3, kernel_size=3, stride=1, padding=0)\n", - " # self.lif1 = LIF(spike_fn=spike_fn, alpha=alpha, beta=beta)\n", - " # self.fc1 = nn.Linear(26*26*3, 10)\n", - " # self.lif2 = LIF(spike_fn=spike_fn, alpha=alpha, beta=beta)\n", - "\n", - " def forward(self, x):\n", - " # Initialize LIF state variables and spike output tensors\n", - " spk1, syn1, mem1 = self.lif1.init_stein(batch_size, 12, 13, 13)\n", - " spk2, syn2, mem2 = self.lif1.init_stein(batch_size, 64, 5, 5)\n", - " spk3, syn3, mem3 = self.lif2.init_stein(batch_size, 10)\n", - "\n", - " spk3_rec = []\n", - " mem3_rec = []\n", - "\n", - " for step in range(num_steps):\n", - " cur1 = F.max_pool2d(self.conv1(x[step]), 2) # add max-pooling to membrane or spikes?\n", - " spk1, syn1, mem1 = self.lif1(cur1, syn1, mem1)\n", - " cur2 = F.max_pool2d(self.conv2(spk1), 2)\n", - " spk2, syn2, mem2 = self.lif2(cur2, syn2, mem2)\n", - " cur3 = self.fc2(spk2.view(batch_size, -1))\n", - " spk3, syn3, mem3 = self.lif3(cur3, syn3, mem3)\n", - "\n", - " spk3_rec.append(spk3)\n", - " mem3_rec.append(mem3)\n", - "\n", - " return torch.stack(spk3_rec, dim=0), torch.stack(mem3_rec, dim=0)\n", - "\n", - "net = Net().to(device)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## 7. Training\n", - "We make a slight modification to our print-out functions to handle the new first dimension of the input:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def print_batch_accuracy(data, targets, train=False):\n", - " output, _ = net(data.view(num_steps, batch_size, 1, 28, 28))\n", - " _, idx = output.sum(dim=0).max(1)\n", - " acc = np.mean((targets == idx).detach().cpu().numpy())\n", - "\n", - " if train:\n", - " print(f\"Train Set Accuracy: {acc}\")\n", - " else:\n", - " print(f\"Test Set Accuracy: {acc}\")\n", - "\n", - "def train_printer():\n", - " print(f\"Epoch {epoch}, Minibatch {minibatch_counter}\")\n", - " print(f\"Train Set Loss: {loss_hist[counter]}\")\n", - " print(f\"Test Set Loss: {test_loss_hist[counter]}\")\n", - " print_batch_accuracy(spike_data, spike_targets, train=True)\n", - " print_batch_accuracy(test_spike_data, test_spike_targets, train=False)\n", - " print(\"\\n\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 7.1 Optimizer & Loss\n", - "We'll keep our optimizer and loss the exact same as the static MNIST case." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-4, betas=(0.9, 0.999))\n", - "log_softmax_fn = nn.LogSoftmax(dim=-1)\n", - "loss_fn = nn.NLLLoss()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### 7.2 Training Loop\n", - "The training loop is identical to the static MNIST case, but we pass each minibatch through `spikegen.rate` before running it through the feedforward network." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "loss_hist = []\n", - "test_loss_hist = []\n", - "counter = 0\n", - "\n", - "# Outer training loop\n", - "for epoch in range(5):\n", - " minibatch_counter = 0\n", - " data = iter(train_loader)\n", - "\n", - " # Minibatch training loop\n", - " for data_it, targets_it in data:\n", - " data_it = data_it.to(device)\n", - " targets_it = targets_it.to(device)\n", - "\n", - " # Spike generator\n", - " spike_data, spike_targets = spikegen.rate(data_it, targets_it, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " # Forward pass\n", - " output, mem_rec = net(spike_data.view(num_steps, batch_size, 1, 28, 28))\n", - " log_p_y = log_softmax_fn(mem_rec)\n", - " loss_val = torch.zeros(1, dtype=dtype, device=device)\n", - "\n", - " # Sum loss over time steps to perform BPTT\n", - " for step in range(num_steps):\n", - " loss_val += loss_fn(log_p_y[step], targets_it)\n", - "\n", - " # Gradient Calculation\n", - " optimizer.zero_grad()\n", - " loss_val.backward(retain_graph=True)\n", - " nn.utils.clip_grad_norm_(net.parameters(), 1)\n", - "\n", - " # Weight Update\n", - " optimizer.step()\n", - "\n", - " # Store Loss history\n", - " loss_hist.append(loss_val.item())\n", - "\n", - " # Test set\n", - " test_data = itertools.cycle(test_loader)\n", - " testdata_it, testtargets_it = next(test_data)\n", - " testdata_it = testdata_it.to(device)\n", - " testtargets_it = testtargets_it.to(device)\n", - "\n", - " # Test set spike conversion\n", - " test_spike_data, test_spike_targets = spikegen.rate(testdata_it, testtargets_it, num_outputs=num_outputs,\n", - " num_steps=num_steps, gain=1, offset=0, convert_targets=False,\n", - " temporal_targets=False)\n", - "\n", - " # Test set forward pass\n", - " test_output, test_mem_rec = net(test_spike_data.view(num_steps, batch_size, 1, 28, 28))\n", - "\n", - " # Test set loss\n", - " log_p_ytest = log_softmax_fn(test_mem_rec)\n", - " log_p_ytest = log_p_ytest.sum(dim=0)\n", - " loss_val_test = loss_fn(log_p_ytest, test_spike_targets)\n", - " test_loss_hist.append(loss_val_test.item())\n", - "\n", - " # Print test/train loss/accuracy\n", - " if counter % 50 == 0:\n", - " train_printer()\n", - " minibatch_counter += 1\n", - " counter += 1\n", - "\n", - "loss_hist_true_grad = loss_hist\n", - "test_loss_hist_true_grad = test_loss_hist" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 8. Spiking MNIST Results\n", - "### 8.1 Plot Training/Test Loss" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\", figsize=(10, 5))\n", - "plt.plot(loss_hist)\n", - "plt.plot(test_loss_hist)\n", - "plt.legend([\"Test Loss\", \"Train Loss\"])\n", - "plt.xlabel(\"Epoch\")\n", - "plt.ylabel(\"Loss\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 8.2 Test Set Accuracy" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "total = 0\n", - "correct = 0\n", - "test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True, drop_last=False)\n", - "\n", - "with torch.no_grad():\n", - " net.eval()\n", - " for data in test_loader:\n", - " images, labels = data\n", - " images = images.to(device)\n", - " labels = labels.to(device)\n", - "\n", - " # If current batch matches batch_size, just do the usual thing\n", - " if images.size()[0] == batch_size:\n", - " spike_test, spike_targets = spikegen.rate(images, labels, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - "\n", - " outputs, _ = net(spike_test.view(num_steps, batch_size, 1, 28, 28))\n", - "\n", - " # If current batch does not match batch_size (e.g., is the final minibatch),\n", - " # modify batch_size in a temp variable and restore it at the end of the else block\n", - " else:\n", - " temp_bs = batch_size\n", - " batch_size = images.size()[0]\n", - " spike_test, spike_targets = spikegen.rate(images, labels, num_outputs=num_outputs, num_steps=num_steps,\n", - " gain=1, offset=0, convert_targets=False, temporal_targets=False)\n", - " outputs, _ = net(spike_test.view(num_steps, images.size()[0], 1, 28, 28))\n", - " batch_size = temp_bs\n", - "\n", - " _, predicted = outputs.sum(dim=0).max(1)\n", - " total += spike_targets.size(0)\n", - " correct += (predicted == spike_targets).sum().item()\n", - "\n", - "print(f\"Total correctly classified test set images: {correct}/{total}\")\n", - "print(f\"Test Set Accuracy: {100 * correct / total}%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To-do:\n", - "* Add figures to explain in better detail\n", - "* See if SRM0 model can reach acceptable acc within reasonable num_steps\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/legacy/tutorial_7_tonic.ipynb b/examples/legacy/tutorial_7_tonic.ipynb deleted file mode 100644 index f3815c11..00000000 --- a/examples/legacy/tutorial_7_tonic.ipynb +++ /dev/null @@ -1,2342 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "id": "47d5313e-c29d-4581-a9c7-a45122337069", - "metadata": { - "id": "47d5313e-c29d-4581-a9c7-a45122337069" - }, - "source": [ - "[](https://github.com/jeshraghian/snntorch/) \n", - "[](https://github.com/neuromorphs/tonic/)\n", - "\n", - "\n", - "# Neuromorphic Datasets with Tonic + snnTorch\n", - "## Tutorial 7\n", - "### By Gregor Lenz (https://lenzgregor.com) and Jason K. Eshraghian (www.jasoneshraghian.com)\n", - "\n", - "\n", - " \"Open\n", - "\n", - "\n", - "[](https://github.com/jeshraghian/snntorch/) [](https://github.com/jeshraghian/snntorch/)" - ] - }, - { - "cell_type": "markdown", - "id": "oll2NNFeG1NG", - "metadata": { - "id": "oll2NNFeG1NG" - }, - "source": [ - "The snnTorch tutorial series is based on the following paper. If you find these resources or code useful in your work, please consider citing the following source:\n", - "\n", - "> [Jason K. Eshraghian, Max Ward, Emre Neftci, Xinxin Wang, Gregor Lenz, Girish Dwivedi, Mohammed Bennamoun, Doo Seok Jeong, and Wei D. Lu. \"Training Spiking Neural Networks Using Lessons From Deep Learning\". arXiv preprint arXiv:2109.12894, September 2021.](https://arxiv.org/abs/2109.12894) " - ] - }, - { - "cell_type": "markdown", - "id": "ClgsZMOfBVby", - "metadata": { - "id": "ClgsZMOfBVby" - }, - "source": [ - "# Introduction\n", - "In this tutorial, you will:\n", - "* Learn how to load neuromorphic datasets using [Tonic](https://github.com/neuromorphs/tonic)\n", - "* Make use of caching to speed up dataloading\n", - "* Train a CSNN with the [Neuromorphic-MNIST](https://tonic.readthedocs.io/en/latest/datasets.html#n-mnist) Dataset\n", - "\n", - "If running in Google Colab:\n", - "* You may connect to GPU by checking `Runtime` > `Change runtime type` > `Hardware accelerator: GPU`\n", - "* Next, install the latest PyPi distribution of snnTorch and Tonic by clicking into the following cell and pressing `Shift+Enter`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "hDnIEHOKB8LD", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "hDnIEHOKB8LD", - "outputId": "3d7a9981-9f06-4188-eba7-77b48ccb045d" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[K |████████████████████████████████| 92 kB 3.1 MB/s \n", - "\u001b[K |████████████████████████████████| 8.9 MB 26.5 MB/s \n", - "\u001b[K |████████████████████████████████| 395 kB 45.8 MB/s \n", - "\u001b[?25h Building wheel for importRosbag (setup.py) ... \u001b[?25l\u001b[?25hdone\n", - " Building wheel for loris (setup.py) ... \u001b[?25l\u001b[?25hdone\n", - "\u001b[K |████████████████████████████████| 59 kB 3.1 MB/s \n", - "\u001b[?25h" - ] - } - ], - "source": [ - "!pip install tonic --quiet \n", - "!pip install snntorch --quiet" - ] - }, - { - "cell_type": "markdown", - "id": "e93694d9-0f0a-46a0-b17f-c04ac9b73a63", - "metadata": { - "id": "e93694d9-0f0a-46a0-b17f-c04ac9b73a63" - }, - "source": [ - "# 1. Using Tonic to Load Neuromorphic Datasets\n", - "Loading datasets from neuromorphic sensors is made super simple thanks to [Tonic](https://github.com/neuromorphs/tonic), which works much like PyTorch vision.\n", - "\n", - "Let's start by loading the neuromorphic version of the MNIST dataset, called [N-MNIST](https://tonic.readthedocs.io/en/latest/reference/datasets.html#n-mnist). We can have a look at some raw events to get a feel for what we're working with." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7d286ef9-5fe6-4578-a686-91559a1f81d2", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 137, - "referenced_widgets": [ - "9f46301d16924494ada03e7496af98ee", - "397436bca741424c886e7f67805eebc3", - "e965cc586d5f4d5aaf8c435ebd4bf35c", - "be2f2e3ca1b34106874e6b5c8cd2051e", - "b3a18813d8684b3ab7b9d944d8e74e4f", - "37deac4746684f189a32858514a780a4", - "83b65be63ec841288496bd9ad33dbbab", - "c87ffeb8f6f54e1b871d129cd783de9f", - "2789fbacce8f4cc8a240daa6c6d45d15", - "bfe2d8fcea404d22bbc61a69ae60c802", - "64ab952b23bd4406af5cc6d4f72b257e" - ] - }, - "id": "7d286ef9-5fe6-4578-a686-91559a1f81d2", - "outputId": "f9438c1a-440f-4815-b4e6-101ba31e5cca" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloading https://uca6892c4e08b631fef762aee80b.dl.dropboxusercontent.com/zip_download_get/A8yubn6iJIfzLC4NXQBrS1I7ATTQ14Bx1aKLTntxskbCo02s7rFtnwUSW2bHzvIbJVxXlmWSWs7kTHeWQty4Q8hQHssWjWM0UpkC79TENd9K1A?dl=1 to ./data/NMNIST/nmnist-archive.zip\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9f46301d16924494ada03e7496af98ee", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/1181572961 [00:00" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "tonic.utils.plot_event_grid(events)" - ] - }, - { - "cell_type": "markdown", - "id": "f6bcc031-d11a-4471-b3aa-335eec76d7ad", - "metadata": { - "id": "f6bcc031-d11a-4471-b3aa-335eec76d7ad" - }, - "source": [ - "## 1.1 Transformations\n", - "\n", - "However, neural nets don't take lists of events as input. The raw data must be converted into a suitable representation, such as a tensor. We can choose a set of transforms to apply to our data before feeding it to our network. The neuromorphic camera sensor has a temporal resolution of microseconds, which when converted into a dense representation, ends up as a very large tensor. That is why we bin events into a smaller number of frames using the [ToFrame transformation](https://tonic.readthedocs.io/en/latest/reference/transformations.html#frames), which reduces temporal precision but also allows us to work with it in a dense format.\n", - "\n", - "* `time_window=1000` integrates events into 1000$~\\mu$s bins\n", - "\n", - "* Denoise removes isolated, one-off events. If no event occurs within a neighbourhood of 1 pixel across `filter_time` microseconds, the event is filtered. Smaller `filter_time` will filter more events." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30f249be-8a65-4c1c-a21c-d561e904b4bf", - "metadata": { - "id": "30f249be-8a65-4c1c-a21c-d561e904b4bf" - }, - "outputs": [], - "source": [ - "import tonic.transforms as transforms\n", - "\n", - "sensor_size = tonic.datasets.NMNIST.sensor_size\n", - "\n", - "# Denoise removes isolated, one-off events\n", - "# time_window\n", - "frame_transform = transforms.Compose([transforms.Denoise(filter_time=10000), \n", - " transforms.ToFrame(sensor_size=sensor_size, \n", - " time_window=1000)\n", - " ])\n", - "\n", - "trainset = tonic.datasets.NMNIST(save_to='./data', transform=frame_transform, train=True)\n", - "testset = tonic.datasets.NMNIST(save_to='./data', transform=frame_transform, train=False)" - ] - }, - { - "cell_type": "markdown", - "id": "70be0b77-9405-44f6-af49-dfc4d49566c6", - "metadata": { - "id": "70be0b77-9405-44f6-af49-dfc4d49566c6" - }, - "source": [ - "## 1.2 Fast Dataloading via Caching\n", - "\n", - "The original data is stored in a format that is slow to read. To speed up dataloading, we can make use of disk caching. That means that once files are loaded from the original file, they are written to disk in an efficient format in our cache directory. Let's compare some file reading speeds to read 100 examples." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3a6bf2a2-ff9f-4cdc-8cb3-02a1a9d71a11", - "metadata": { - "id": "3a6bf2a2-ff9f-4cdc-8cb3-02a1a9d71a11" - }, - "outputs": [], - "source": [ - "def load_sample_simple():\n", - " for i in range(100):\n", - " events, target = trainset[i]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a9d3b28-b303-4a17-be78-b9918911a7cd", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "1a9d3b28-b303-4a17-be78-b9918911a7cd", - "outputId": "47fb99f1-0685-460a-bfd3-5aca4bc069ed" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1 loop, best of 5: 2.76 s per loop\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%timeit -o load_sample_simple()" - ] - }, - { - "cell_type": "markdown", - "id": "b957b32f-d76b-42c0-8c1c-b6b63e84e2ef", - "metadata": { - "id": "b957b32f-d76b-42c0-8c1c-b6b63e84e2ef" - }, - "source": [ - "We can decrease the time it takes to read 100 samples by using a PyTorch DataLoader in addition to disk caching." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f47e0798-5259-491a-9d3a-59b13b1b0983", - "metadata": { - "id": "f47e0798-5259-491a-9d3a-59b13b1b0983" - }, - "outputs": [], - "source": [ - "from torch.utils.data import DataLoader\n", - "from tonic import CachedDataset\n", - "\n", - "cached_trainset = CachedDataset(trainset, cache_path='./cache/nmnist/train')\n", - "cached_dataloader = DataLoader(cached_trainset)\n", - "\n", - "def load_sample_cached():\n", - " for i, (events, target) in enumerate(iter(cached_dataloader)):\n", - " if i > 99: break" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17a0219b-4d15-4f0b-b5be-8c728b5e24a9", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "17a0219b-4d15-4f0b-b5be-8c728b5e24a9", - "outputId": "fe8526e1-ffef-4190-a07c-0f62e1ba32fd" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1 loop, best of 20: 1.8 s per loop\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%timeit -o -r 20 load_sample_cached()" - ] - }, - { - "cell_type": "markdown", - "id": "3831428d-0511-4fde-84d9-11d08fa45df7", - "metadata": { - "id": "3831428d-0511-4fde-84d9-11d08fa45df7" - }, - "source": [ - "## 1.3 Even Faster DataLoading via Batching\n", - "\n", - "Now that we've reduced our loading time, we also want to use batching to make efficient use of the GPU. \n", - "\n", - "Because event recordings have different lengths, we are going to provide a collation function `tonic.collation.PadTensors()` that will pad out shorter recordings to ensure all samples in a batch have the same dimensions. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b35c7cd-d292-47cd-9203-7f31aa7f7207", - "metadata": { - "id": "5b35c7cd-d292-47cd-9203-7f31aa7f7207" - }, - "outputs": [], - "source": [ - "batch_size = 100\n", - "trainloader = DataLoader(cached_trainset, batch_size=batch_size, collate_fn=tonic.collation.PadTensors())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14b9af4f-141e-4301-8451-445957ec8707", - "metadata": { - "id": "14b9af4f-141e-4301-8451-445957ec8707" - }, - "outputs": [], - "source": [ - "def load_sample_batched():\n", - " events, target = next(iter(cached_dataloader))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3dc4b27a-63ac-4edc-94e9-589d548c4769", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "3dc4b27a-63ac-4edc-94e9-589d548c4769", - "outputId": "e3a16676-9c5d-4c9e-e692-96bb4f8f5099" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "100 loops, best of 10: 17.4 ms per loop\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%timeit -o -r 10 load_sample_batched()" - ] - }, - { - "cell_type": "markdown", - "id": "a82a7afd-c011-4cd6-ba04-1e7cd438bc1f", - "metadata": { - "id": "a82a7afd-c011-4cd6-ba04-1e7cd438bc1f" - }, - "source": [ - "By using disk caching and a PyTorch dataloader with multithreading and batching support, we have reduced loading times to less than a tenth per sample in comparison to naively iterating over the dataset!" - ] - }, - { - "cell_type": "markdown", - "id": "2ded1bd9-e2f1-479e-899c-c6c2652e6fc9", - "metadata": { - "id": "2ded1bd9-e2f1-479e-899c-c6c2652e6fc9" - }, - "source": [ - "# 2. Training our network using frames created from events" - ] - }, - { - "cell_type": "markdown", - "id": "9be82d75-69ef-4c1b-ad85-4eca84c73ccf", - "metadata": { - "id": "9be82d75-69ef-4c1b-ad85-4eca84c73ccf" - }, - "source": [ - "Now let's actually train a network on the N-MNIST classification task. We start by defining our caching wrappers and dataloaders. While doing that, we're also going to apply some augmentations to the training data. The samples we receive from the cached dataset are frames, so we can make use of PyTorch Vision to apply whatever random transform we would like." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ace6cd0b-7b56-4422-b3bd-23bac65db9bd", - "metadata": { - "id": "ace6cd0b-7b56-4422-b3bd-23bac65db9bd" - }, - "outputs": [], - "source": [ - "import torch\n", - "import torchvision\n", - "\n", - "transform = tonic.transforms.Compose([torch.from_numpy,\n", - " torchvision.transforms.RandomRotation([-10,10])])\n", - "\n", - "cached_trainset = CachedDataset(trainset, transform=transform, cache_path='./cache/nmnist/train')\n", - "\n", - "# no augmentations for the testset\n", - "cached_testset = CachedDataset(testset, cache_path='./cache/nmnist/test')\n", - "\n", - "batch_size = 128\n", - "trainloader = DataLoader(cached_trainset, batch_size=batch_size, collate_fn=tonic.collation.PadTensors(), shuffle=True)\n", - "testloader = DataLoader(cached_testset, batch_size=batch_size, collate_fn=tonic.collation.PadTensors())" - ] - }, - { - "cell_type": "markdown", - "id": "528fe384-a365-4b53-bbfc-ed4dd261d222", - "metadata": { - "id": "528fe384-a365-4b53-bbfc-ed4dd261d222" - }, - "source": [ - "A mini-batch now has the dimensions (time steps, batch size, channels, height, width). The number of time steps will be set to that of the longest recording in the mini-batch, and all other samples will be padded with zeros to match it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c9e37337-ad4a-43d5-b429-81a18de5148e", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "c9e37337-ad4a-43d5-b429-81a18de5148e", - "outputId": "88a6c0b0-aaaa-43ee-a62b-0eabcc90ebf0" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "torch.Size([311, 128, 2, 34, 34])\n" - ] - } - ], - "source": [ - "event_tensor, target = next(iter(trainloader))\n", - "print(event_tensor.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "61ae5d4b-2bb3-4191-9f96-04b3c6ba4c41", - "metadata": { - "id": "61ae5d4b-2bb3-4191-9f96-04b3c6ba4c41" - }, - "source": [ - "## 2.1 Defining our network\n", - "We will use snnTorch + PyTorch to construct a CSNN, just as in the previous tutorial. The convolutional network architecture to be used is: 12C5-MP2-32C5-MP2-800FC10\n", - "\n", - "- 12C5 is a 5$\\times$5 convolutional kernel with 12 filters\n", - "- MP2 is a 2$\\times$2 max-pooling function\n", - "- 800FC10 is a fully-connected layer that maps 800 neurons to 10 outputs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "HpKDIkRKUIAB", - "metadata": { - "id": "HpKDIkRKUIAB" - }, - "outputs": [], - "source": [ - "import snntorch as snn\n", - "from snntorch import surrogate\n", - "from snntorch import functional as SF\n", - "from snntorch import utils\n", - "from snntorch import spikeplot as splt\n", - "\n", - "import torch\n", - "import torch.nn as nn" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "107cb645-0227-4290-9e1b-25d6ae7eac87", - "metadata": { - "id": "107cb645-0227-4290-9e1b-25d6ae7eac87" - }, - "outputs": [], - "source": [ - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", - "\n", - "# neuron and simulation parameters\n", - "spike_grad = surrogate.fast_sigmoid(slope=75)\n", - "beta = 0.5\n", - "\n", - "# Initialize Network\n", - "net = nn.Sequential(nn.Conv2d(2, 12, 5),\n", - " nn.MaxPool2d(2),\n", - " snn.Leaky(beta=beta, spike_grad=spike_grad, init_hidden=True),\n", - " nn.Conv2d(12, 32, 5),\n", - " nn.MaxPool2d(2),\n", - " snn.Leaky(beta=beta, spike_grad=spike_grad, init_hidden=True),\n", - " nn.Flatten(),\n", - " nn.Linear(32*5*5, 10),\n", - " snn.Leaky(beta=beta, spike_grad=spike_grad, init_hidden=True, output=True)\n", - " ).to(device)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "zPFvlqOGi_uW", - "metadata": { - "id": "zPFvlqOGi_uW" - }, - "outputs": [], - "source": [ - "# this time, we won't return membrane as we don't need it \n", - "\n", - "def forward_pass(net, data): \n", - " spk_rec = []\n", - " utils.reset(net) # resets hidden states for all LIF neurons in net\n", - "\n", - " for step in range(data.size(0)): # data.size(0) = number of time steps\n", - " spk_out, mem_out = net(data[step])\n", - " spk_rec.append(spk_out)\n", - " \n", - " return torch.stack(spk_rec)" - ] - }, - { - "cell_type": "markdown", - "id": "23569dfc-e4a7-490f-8a68-c9ade5e03028", - "metadata": { - "id": "23569dfc-e4a7-490f-8a68-c9ade5e03028" - }, - "source": [ - "## 2.2 Training\n", - "\n", - "In the previous tutorial, Cross Entropy Loss was applied to the total spike count to maximize the number of spikes from the correct class.\n", - "\n", - "Another option from the `snn.functional` module is to specify the target number of spikes from correct and incorrect classes. The approach below uses the *Mean Square Error Spike Count Loss*, which aims to elicit spikes from the correct class 80\\% of the time, and 20\\% of the time from incorrect classes. Encouraging incorrect neurons to fire could be motivated to avoid dead neurons." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "VocYbtD7Vwp7", - "metadata": { - "id": "VocYbtD7Vwp7" - }, - "outputs": [], - "source": [ - "optimizer = torch.optim.Adam(net.parameters(), lr=2e-2, betas=(0.9, 0.999))\n", - "loss_fn = SF.mse_count_loss(correct_rate=0.8, incorrect_rate=0.2)" - ] - }, - { - "cell_type": "markdown", - "id": "7xkKLsqnmzcw", - "metadata": { - "id": "7xkKLsqnmzcw" - }, - "source": [ - "Training neuromorphic data is expensive as it requires sequentially iterating through many time steps (approximately 300 time steps in the N-MNIST dataset). The following simulation will take some time, so we will just stick to training across 50 iterations (which is roughly 1/10th of a full epoch). Feel free to change `num_iters` if you have more time to kill. As we are printing results at each iteration, the results will be quite noisy and will also take some time before we start to see any sort of improvement.\n", - "\n", - "In our own experiments, it took about 20 iterations before we saw any improvement, and after 50 iterations, managed to crack ~60% accuracy. \n", - "\n", - "> Warning: the following simulation will take a while. Go make yourself a coffee, or ten. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "R4GbPSdTUcUR", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "R4GbPSdTUcUR", - "outputId": "76a3bfd8-7038-4e4d-d597-f864b92740a0" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 0, Iteration 0 \n", - "Train Loss: 30.96\n", - "Accuracy: 5.47%\n", - "\n", - "Epoch 0, Iteration 1 \n", - "Train Loss: 30.90\n", - "Accuracy: 8.59%\n", - "\n", - "Epoch 0, Iteration 2 \n", - "Train Loss: 30.90\n", - "Accuracy: 9.38%\n", - "\n", - "Epoch 0, Iteration 3 \n", - "Train Loss: 30.90\n", - "Accuracy: 14.06%\n", - "\n", - "Epoch 0, Iteration 4 \n", - "Train Loss: 31.00\n", - "Accuracy: 6.25%\n", - "\n", - "Epoch 0, Iteration 5 \n", - "Train Loss: 30.90\n", - "Accuracy: 7.03%\n", - "\n", - "Epoch 0, Iteration 6 \n", - "Train Loss: 30.90\n", - "Accuracy: 6.25%\n", - "\n", - "Epoch 0, Iteration 7 \n", - "Train Loss: 30.90\n", - "Accuracy: 10.16%\n", - "\n", - "Epoch 0, Iteration 8 \n", - "Train Loss: 30.90\n", - "Accuracy: 14.84%\n", - "\n", - "Epoch 0, Iteration 9 \n", - "Train Loss: 31.00\n", - "Accuracy: 7.03%\n", - "\n", - "Epoch 0, Iteration 10 \n", - "Train Loss: 30.90\n", - "Accuracy: 10.16%\n", - "\n", - "Epoch 0, Iteration 11 \n", - "Train Loss: 30.58\n", - "Accuracy: 12.50%\n", - "\n", - "Epoch 0, Iteration 12 \n", - "Train Loss: 29.67\n", - "Accuracy: 5.47%\n", - "\n", - "Epoch 0, Iteration 13 \n", - "Train Loss: 17.50\n", - "Accuracy: 6.25%\n", - "\n", - "Epoch 0, Iteration 14 \n", - "Train Loss: 13.11\n", - "Accuracy: 12.50%\n", - "\n", - "Epoch 0, Iteration 15 \n", - "Train Loss: 18.77\n", - "Accuracy: 14.06%\n", - "\n", - "Epoch 0, Iteration 16 \n", - "Train Loss: 19.44\n", - "Accuracy: 16.41%\n", - "\n", - "Epoch 0, Iteration 17 \n", - "Train Loss: 17.33\n", - "Accuracy: 15.62%\n", - "\n", - "Epoch 0, Iteration 18 \n", - "Train Loss: 13.39\n", - "Accuracy: 10.16%\n", - "\n", - "Epoch 0, Iteration 19 \n", - "Train Loss: 12.56\n", - "Accuracy: 12.50%\n", - "\n", - "Epoch 0, Iteration 20 \n", - "Train Loss: 14.20\n", - "Accuracy: 18.75%\n", - "\n", - "Epoch 0, Iteration 21 \n", - "Train Loss: 14.74\n", - "Accuracy: 20.31%\n", - "\n", - "Epoch 0, Iteration 22 \n", - "Train Loss: 11.32\n", - "Accuracy: 24.22%\n", - "\n", - "Epoch 0, Iteration 23 \n", - "Train Loss: 11.99\n", - "Accuracy: 27.34%\n", - "\n", - "Epoch 0, Iteration 24 \n", - "Train Loss: 12.59\n", - "Accuracy: 39.06%\n", - "\n", - "Epoch 0, Iteration 25 \n", - "Train Loss: 12.47\n", - "Accuracy: 42.97%\n", - "\n", - "Epoch 0, Iteration 26 \n", - "Train Loss: 10.73\n", - "Accuracy: 42.19%\n", - "\n", - "Epoch 0, Iteration 27 \n", - "Train Loss: 9.97\n", - "Accuracy: 36.72%\n", - "\n", - "Epoch 0, Iteration 28 \n", - "Train Loss: 11.09\n", - "Accuracy: 32.03%\n", - "\n", - "Epoch 0, Iteration 29 \n", - "Train Loss: 11.04\n", - "Accuracy: 45.31%\n", - "\n", - "Epoch 0, Iteration 30 \n", - "Train Loss: 10.03\n", - "Accuracy: 50.78%\n", - "\n", - "Epoch 0, Iteration 31 \n", - "Train Loss: 9.99\n", - "Accuracy: 46.88%\n", - "\n", - "Epoch 0, Iteration 32 \n", - "Train Loss: 9.43\n", - "Accuracy: 51.56%\n", - "\n", - "Epoch 0, Iteration 33 \n", - "Train Loss: 9.78\n", - "Accuracy: 48.44%\n", - "\n", - "Epoch 0, Iteration 34 \n", - "Train Loss: 9.56\n", - "Accuracy: 53.91%\n", - "\n", - "Epoch 0, Iteration 35 \n", - "Train Loss: 9.20\n", - "Accuracy: 54.69%\n", - "\n", - "Epoch 0, Iteration 36 \n", - "Train Loss: 9.02\n", - "Accuracy: 50.78%\n", - "\n", - "Epoch 0, Iteration 37 \n", - "Train Loss: 9.06\n", - "Accuracy: 51.56%\n", - "\n", - "Epoch 0, Iteration 38 \n", - "Train Loss: 8.93\n", - "Accuracy: 57.81%\n", - "\n", - "Epoch 0, Iteration 39 \n", - "Train Loss: 8.71\n", - "Accuracy: 60.94%\n", - "\n", - "Epoch 0, Iteration 40 \n", - "Train Loss: 8.68\n", - "Accuracy: 53.12%\n", - "\n", - "Epoch 0, Iteration 41 \n", - "Train Loss: 8.58\n", - "Accuracy: 56.25%\n", - "\n", - "Epoch 0, Iteration 42 \n", - "Train Loss: 8.17\n", - "Accuracy: 64.84%\n", - "\n", - "Epoch 0, Iteration 43 \n", - "Train Loss: 8.36\n", - "Accuracy: 53.12%\n", - "\n", - "Epoch 0, Iteration 44 \n", - "Train Loss: 7.77\n", - "Accuracy: 58.59%\n", - "\n", - "Epoch 0, Iteration 45 \n", - "Train Loss: 8.26\n", - "Accuracy: 60.94%\n", - "\n", - "Epoch 0, Iteration 46 \n", - "Train Loss: 7.71\n", - "Accuracy: 68.75%\n", - "\n", - "Epoch 0, Iteration 47 \n", - "Train Loss: 7.06\n", - "Accuracy: 78.12%\n", - "\n", - "Epoch 0, Iteration 48 \n", - "Train Loss: 7.97\n", - "Accuracy: 61.72%\n", - "\n", - "Epoch 0, Iteration 49 \n", - "Train Loss: 7.73\n", - "Accuracy: 58.59%\n", - "\n", - "Epoch 0, Iteration 50 \n", - "Train Loss: 7.49\n", - "Accuracy: 65.62%\n", - "\n" - ] - } - ], - "source": [ - "num_iters = 50\n", - "\n", - "loss_hist = []\n", - "acc_hist = []\n", - "\n", - "# training loop\n", - "for epoch in range(num_epochs):\n", - " for i, (data, targets) in enumerate(iter(trainloader)):\n", - " data = data.to(device)\n", - " targets = targets.to(device)\n", - "\n", - " net.train()\n", - " spk_rec = forward_pass(net, data)\n", - " loss_val = loss_fn(spk_rec, targets)\n", - "\n", - " # Gradient calculation + weight update\n", - " optimizer.zero_grad()\n", - " loss_val.backward()\n", - " optimizer.step()\n", - "\n", - " # Store loss history for future plotting\n", - " loss_hist.append(loss_val.item())\n", - " \n", - " print(f\"Epoch {epoch}, Iteration {i} \\nTrain Loss: {loss_val.item():.2f}\")\n", - "\n", - " acc = SF.accuracy_rate(spk_rec, targets) \n", - " acc_hist.append(acc)\n", - " print(f\"Accuracy: {acc * 100:.2f}%\\n\")\n", - "\n", - " if i == num_iters:\n", - " break" - ] - }, - { - "cell_type": "markdown", - "id": "YVjUzNcX0wld", - "metadata": { - "id": "YVjUzNcX0wld" - }, - "source": [ - "# 3. Results\n", - "## 3.1 Plot Test Accuracy" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "yp2aTX2_1zFG", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 295 - }, - "id": "yp2aTX2_1zFG", - "outputId": "9b85de65-dbae-4818-bfc9-776625498e1e" - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEWCAYAAABrDZDcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOzdd1RUd/o/8PcwQ+9laEOTDqOACoIaFTWKIYqaqEFNcVPIZo1p3035bTaamKbpRZOIyUZNFIy6q9iIDaxRRFEjI1KkDEMH6QwDM5/fH8SJhDaUC8g8r3M4h3vv5977XA/OM/dTeYwxBkIIIVpLZ6gDIIQQMrQoERBCiJajREAIIVqOEgEhhGg5SgSEEKLlKBEQQoiWo0RA7lkPPPAAtm7dOtRhEHLPo0RABpWJiYn6R0dHB4aGhurt7du39+pahw8fxhNPPNGnOM6cOYNJkybB3NwcVlZWmDx5Mi5evKjRuTweD9nZ2T2WS05OBo/Hw/r16/sUIyGDhRIBGVT19fXqHxcXF+zfv1+9vXz5cnW51tZWzmKora3F3LlzsWrVKlRVVUEmk2HNmjXQ19cf0Pts3boVVlZW2LZt24BetyeMMahUqkG9J7m3USIgw0JycjKcnJywfv162Nvb429/+xtu376NuXPnQigUwtLSEnPnzkVhYaH6nPDwcHz//fcAgC1btuC+++7DP//5T1haWmLUqFE4fPhwp/fKzMwEACxduhR8Ph+GhoaYPXs2AgIC1GX+85//wM/PD5aWloiIiEB+fj4AYOrUqQCAwMBAmJiYYOfOnZ3eo6GhAbt378bGjRuRlZWF1NTUdsc3b94MPz8/mJqawt/fH5cvXwYASKVSPPTQQxAKhbC2tsbzzz8PAHj77bfx6KOPqs/Py8sDj8dTJ8zw8HC8+eabmDx5MoyMjHDr1i38+OOP6nu4u7tj06ZN7WLYt28fgoKCYGZmBg8PDyQmJmLXrl0YP358u3KfffYZ5s+f3+lzkhGCETJEXF1d2dGjRxljjCUlJTE+n89ee+01JpfLWWNjI6uoqGC7d+9mDQ0NrLa2li1atIjNnz9fff60adPY5s2bGWOM/fjjj0wgELDY2FjW2trKvvnmG+bg4MBUKlWH+9bU1DArKyv2+OOPs0OHDrGqqqp2x/fu3cs8PDyYRCJhLS0t7N1332UTJ05UHwfAsrKyun22bdu2MXt7e9ba2srmzp3Lnn/+efWxX375hTk6OrKUlBSmUqlYVlYWy8vLY62trSwgIIC99NJLrL6+njU1NbHTp08zxhhbs2YNW758ufoaubm5DABraWlR/1s4Ozuz69evs5aWFqZQKNiBAwdYdnY2U6lULDk5mRkaGrJLly4xxhi7cOECMzMzY0eOHGFKpZIVFhayGzduMLlcziwtLZlEIlHfKygoiO3evbvb5yX3NkoEZMj8NRHo6uqypqamLsunpaUxCwsL9fZfE4GHh4f6WENDAwPAiouLO72WRCJhTzzxBBOJRIzP57N58+axkpISxhhjc+bMYd9//726rFKpZIaGhiwvL48xplkimDlzJnvxxRcZY4zt2LGD2djYMIVCwRhjbPbs2eyLL77ocM65c+eYjY2N+sP9bpokgrfeeqvbmObPn6++b0xMDHvppZc6Lff3v/+d/etf/2KMMXb9+nVmYWHB5HJ5t9cm9zaqGiLDhlAohIGBgXq7sbERzz77LFxdXWFmZoapU6eiuroaSqWy0/Pt7e3VvxsZGQFoa5PojJ+fH7Zs2YLCwkJcv34dRUVFeOmllwAA+fn5ePHFF2FhYQELCwtYWVmBMQaZTKbRc0ilUiQlJanbPObPnw+5XI6DBw+qj3t4eHR6nqurKwQCgUb3+StnZ+d224cPH0ZYWBisrKxgYWGBQ4cOoaKiotsYAOCJJ57Ajh07wBjDTz/9hCVLlgx4+wkZXigRkGGDx+O12/70009x8+ZNXLhwAbW1tTh16hSAtsbQgeTr64sVK1bg+vXrANo+UDdt2oTq6mr1T1NTEyZNmqTR9X766SeoVCrMmzcP9vb2cHd3h1wuV3d1dXZ2Rk5OTofznJ2dUVBQ0GlDubGxMRobG9XbJSUlHcrc/e/X3NyMhx9+GP/85z9RWlqK6upqREZGqv/tuooBAMLCwqCnp4fTp09jx44deOyxxzR6bnLvokRAhq26ujoYGhrCwsICVVVVeOeddwbkuhkZGfj000/VDc9SqRRxcXEICwsDAPz973/Hhx9+iPT0dABATU0Ndu3apT7fzs4Ot27d6vL6W7duxZo1a3DlyhX1z549e3Do0CFUVlbi6aefxieffIJLly6BMYbs7Gzk5+djwoQJcHBwwBtvvIGGhgbI5XKcPXsWABAUFIRTp06hoKAANTU1+PDDD7t9RoVCgebmZgiFQggEAhw+fBhHjhxRH3/qqafw448/4vjx41CpVJDJZMjIyFAff/zxx/H8889DV1cX9913Xy//hcm9hhIBGbZeeuklNDU1wcbGBmFhYZgzZ86AXNfU1BQXLlxAaGgojI2NERYWhtGjR+PTTz8FACxcuBCvv/46oqOjYWZmhtGjR7frgfT222/jiSeegIWFBX755Zd21z5//jzy8/OxcuVK2Nvbq3+ioqLg6emJuLg4LF68GG+++SaWLVsGU1NTLFiwAFVVVeDz+di/fz+ys7Ph4uICJycnda+kWbNm4ZFHHkFAQADGjx+PuXPn9viMX331FZYsWQJLS0vs2LEDUVFR6uMTJkzAjz/+iJdffhnm5uaYNm2aumcUADz22GO4fv16u55KZOTisYF+zyaE3POamppga2uLy5cvw8vLa6jDIRyjNwJCSAfffvstQkJCKAloib51TyCEjFhubm5gjGHv3r1DHQoZJFQ1RAghWo6qhgghRMvdc1VDNjY2cHNzG+owCCHknpKXl6ceUPhX91wicHNz6zCBFyGEkO4FBwd3eYzTqqHExET4+PjA09MT69at63C8oKAA06dPx9ixYxEQEIBDhw5xGQ4hhJBOcJYIlEolVq5cicOHD0MikSAuLg4SiaRdmffeew9LlixBWloa4uPj8Y9//IOrcAghhHSBs0SQkpICT09PuLu7Q09PD9HR0di3b1+7MjweD7W1tQDahvE7OjpyFQ4hhJAucNZGIJPJ2s2G6OTkhAsXLrQr8/bbb2P27Nn4+uuv0dDQgGPHjnV6rdjYWMTGxgIAysvLuQqZEEK00pB2H42Li8OKFStQWFiIQ4cO4bHHHut0ib2YmBikpqYiNTUVQqFwCCIlhJCRi7NEIBKJIJVK1duFhYUQiUTtyvzwww9YsmQJAGDixImQy+Vddm8ihBDCDc4SQUhICLKyspCbmwuFQoH4+Ph2sx8CgIuLC44fPw4AuHHjBuRyOX3jJ4SQQcZZIhAIBNiwYQMiIiLg5+eHJUuWQCwWY/Xq1UhISADQtvDI5s2bERgYiKVLl2LLli0dFichhJB7VUNzK35JlQ74YkoD7Z6bayg4OJgGlBFC7gk/nMnFuwckOPTCFPg7mg1pLN19dtJcQ4QQwpGU3EoAQEFVwxBH0j1KBIQQwgHGGFLzbgMA8isbeyg9tCgREEIIB3LKG1DZoAAA5FdRIiCEEK2TklsFALA21kMBvREQQoj2uZhXBRsTPUzytEE+tREQQoj2ScmtQrCrFdysjVBULUeLsuOsCcMFJQJCCBlgRdVNkFU3YcIoK7hYGUGpYpDdbhrqsLpEiYAQQgbYxby29oEJo6zgam0MYHg3GFMiIISQAZaSWwUTfQH8HMzgam0EACioHL7tBJQICCFkgF3Mq8I4V0vwdXiwNdWHga7OsB5LQImAEEIG0O0GBTJL6zHBzRJA2wJcLlZGVDVECCHaIjW/bTRxiJuVep+LlTGklAgIIUQ7XMyrgh5fB4HOFup9rtZGKKhqHLazkFIiIISQAZSSW4UAJ3MY6PLV+1ysjNCoUKK8vnkII+saJQJCCBkgjYpWXJfVIGSUVbv9LuqeQ8OzeogSASGEDJC0gmq0qhgmuLVPBK5WbYlguPYcokRACCEDJCW3CjweMM7Vst1+J0sj6PCG76AySgSEEDJALuZVwdfeDOaGuu326wl04GBuOGwHlXGaCBITE+Hj4wNPT0+sW7euw/GXX34ZQUFBCAoKgre3NywsLDq5CiGEDH8tShXSCqoR+pf2gTtcrYfvWAIBVxdWKpVYuXIljh49CicnJ4SEhCAqKgr+/v7qMp9//rn696+//hppaWlchUMIIZy6LqtBU4uy3fiBu7laG+FIeukgR6UZzt4IUlJS4OnpCXd3d+jp6SE6Ohr79u3rsnxcXByWLl3KVTiEEMKpOxPNhYyy7PS4i5UxKhsUqG9uHcywNMJZIpDJZHB2dlZvOzk5QSaTdVo2Pz8fubm5mDFjRqfHY2NjERwcjODgYJSXl3MSLyGE9EdK7m24WRvB1tSg0+N3Jp/LH4btBMOisTg+Ph6LFi0Cn8/v9HhMTAxSU1ORmpoKoVA4yNERQkj3VCqG1PyqLquFgLZBZcDwHEvAWSIQiUSQSqXq7cLCQohEok7LxsfHU7UQIeSelV1ej+rGlg4Dye6mHlQ2DBuMOUsEISEhyMrKQm5uLhQKBeLj4xEVFdWhXEZGBm7fvo2JEydyFQohhHDqwh8L1f91INndzAx0YWmkOyx7DnGWCAQCATZs2ICIiAj4+flhyZIlEIvFWL16NRISEtTl4uPjER0dDR6Px1UohBDCqZM3yyGyMFS3A3TFxdp4WFYNcdZ9FAAiIyMRGRnZbt/atWvbbb/99ttchkAIIZxqblXiXE4FFo4V9fiF1tXKCGnS24MUmeaGRWMxIYTcq1LzbqNRocR0H9sey7paG6GoWo4WpWoQItMcJQJCCOmHpIwy6PF1MMnTuseyLlZGUKoYZLebBiEyzVEiIISQfkjOLEeouxWM9HquaXe1NgYw/Cafo0RACCF9JK1qRHZZPaZ5aza+yVW9LsHwGlRGiYAQQvooObNtpoNwDdoHAMDWVB/6Ap1hty4BJQJCCOmjkzfL4GxlCA+hsUbleTweXKx6PwupSsXw4aEbuC6r6UuYPaJEQAghfdDcqsTZ7EqEe9v2ahyUq7VRr8cSXC2sxqZTt5BZWtfbMDVCiYAQMuLdKq9H4e2BrY5Jya1CU4sS4T69m//MxcoYBVWNYIxpfM6v6aUQ6PAw09eut2FqhBIBIWTEe2ZbKt7Y8/uAXjP5Zjn0BDqY6NFzt9G7uVoboalFifL6Zo3KM8bwa3oJJnpYw9xIt+cT+oASASFkRCuqbkJOeQOuFVb36lt4T5JvliF0lGbdRu+mnnxOw+qhrLJ65FY0YLbYvtcxaooSASFkRDuXUwkAqJW3onCABnJJqxqRU96gcW+hu7la3VmXQLNEkHi9BDweEOHPTbUQQImAEDLCncuuwJ223PSi2gG5ZvLNMgDA9F62DwCAk6URdHiaDyr7Nb0EY50tYGvW+YI3A4ESASFkxGKM4VxOJWb62oKvw4OkaGC6XybfLIeLlRFG2WjWbfRuegIdOJgbajSoTFrViPSiWswZzV21EECJgBAygt2qaEBJrRzTfW3hITQekDcCeYsS53IqEe4j7PP0+a7Wmo0l+DW9BAAQwWH7AECJgBAygp3LrgAATPawgdjRfEASwZ1uo5rMNtoVFyvNxhIcSS+Fr72peo4irlAiIISMWOdyKtULxogdzVBSK0elht02u3Kn22iYe++6jd7NxdoIlQ0K1De3dlmmvK4ZF/OrOH8bACgREEJGKJWK4bdblZjoYQ0ejwd/BzMA/W8wTs4sQ5i7NQz1+H2+hqtV2zf8G8Vdx3LsRikY475aCKBEQAgZoSTFtahubMHkP9YJ8HfsfyIoqGzErfKGPvUWutskD2vYmOjhrb3XIW9Rdlom8XoJXKyM4Odg2q97aYLTRJCYmAgfHx94enpi3bp1nZb55Zdf4O/vD7FYjGXLlnEZDiHkHtKkUOLVXVdxrbC6T+efy2lrH5jkYQMAsDDSg8jCEOn96Dl08PdiAJrPNtoVS2M9fLwoEBkldVifmNHheK28BedyKhAhthuU9dw5W7NYqVRi5cqVOHr0KJycnBASEoKoqCj4+/ury2RlZeHDDz/E2bNnYWlpibKyMq7CIYTcY94/JMGuS4UAgI8XW/T6/LPZlfAQGsPurv73YkczSLqpjunOr+kl+OTITUzxsulTt9G/mu5rixWT3PDj2TxM9Ra2a3xOyihDi5Jx3m30Ds7eCFJSUuDp6Ql3d3fo6ekhOjoa+/bta1dm8+bNWLlyJSwtLQEAtrb9y7KEkJHhqKQUP58vgKEuH8mZ5VCpejc1hKJVhZTcKkz2tGm3X+xojtyKBjR000jbmZOZ5Vi1Iw1jROb49tHxvTq3O2884Atfe1O8uusqyuv+bMT+Nb0EQlN9jHW2HLB7dYezRCCTyeDs7KzednJygkwma1cmMzMTmZmZmDx5MsLCwpCYmMhVOISQe0RprRyv7b4KfwczvDXXH+V1zb3+Fn+1sBpNLUp1tdAdYkczMAZklGh+vQu3KvHsT6nwtDXB1r9NgIn+wFWkGOjy8WX0WNTJW/Hq7qtgjEHeokTyzXLM9reDjg731ULAEDcWt7a2IisrC8nJyYiLi8MzzzyD6uqO9YGxsbEIDg5GcHAwysvLhyBSQshgUKkY/u+Xq2hqUeKrpWMx64/5dU5m9u7//dk/ppUIc7dqt18s6l2D8RVpNZ7cchFOlkb46akJnMz+6WNvijcf9EPyzXJsOZeH01kVaFQoB6W30B2cJQKRSASpVKreLiwshEgkalfGyckJUVFR0NXVxahRo+Dt7Y2srKwO14qJiUFqaipSU1MhFPavtZ4QMnz9cCYXZ7Ir8NZcf3jamkBoqo/RIjMkZfSu/fBcdiVGO5rDwkiv3X57MwNYGukiXdZzIpAU1eLxHy7A2kQfPz8VCmsT/V7F0BuPhblipq8tPjycgdhTOTA1EPRrnEJvcZYIQkJCkJWVhdzcXCgUCsTHxyMqKqpdmQULFiA5ORkAUFFRgczMTLi7u3MVEiFkGLsuq8FHv2Zgtr8dlk1wUe+f7mOLywW3UdPYotF1GhWtSJPexiTPjh+kPB6vbYRxcfc9h3LK6/HYDxdgrC/A9qdDYW/O3YRvd+L6aFEAzA11cTHvNu73s4OeYPAqbDi7k0AgwIYNGxAREQE/Pz8sWbIEYrEYq1evRkJCAgAgIiIC1tbW8Pf3x/Tp0/Hxxx/D2nrwsiAhZHhoUijxYnwarIz1sP7hgHZdJsN9hFAx4HS2ZtVDF/Nuo0XJOrQP3CF2NENmST1alKour7FmXzpUjGH706Fw/mPaaK5Zm+jj08WBEOjwEBXkOCj3vIOz7qMAEBkZicjIyHb71q5dq/6dx+Phs88+w2effcZlGISQYe7dgxLcqmjAz0+FwtK4fXVOkLMlzA11kZRRjrkBPX9AnsuugC6fhxC3znvc+DuaQaFUIau0Xj3I7G65FQ04k12B/5vlDXehSd8eqI+megtxZc3sAW2Q1gSNLCaEDClJUS12XCjAM1PcO3T3BAC+Dg9TvYU4qWE30rM5FRjrYtnlymFiR/O2+3bRE2nHhXwIdHh4JMS50+NcG+wkAFAiIIQMsf+lFUKgw8Nz0zy6LBPuLURFfc/dSKsbFUgvqsWkbtYRHmVjDENdfqcjjOUtSuy6VIjZYjtOF4IZbigREEKGjFLFkHC1COE+th2qhO421butt+CdlcG6cv5WJRhDp28Wd/B1ePB1MO20C+nBa8WobmzBo6GuGj7ByECJgBAyZM7fqkRpbTMWjO2+7l9oqo8AJ3Mk3ey+wfhsdiWM9PgIdOp+SgqxoxluFNV2qGr6+UI+3IXGmNjNG8VIRImAEDJk/pcmg4m+APf79bwwe7i3EGkFt1HdqOj0eJNCiRMZZQhxs+qx66XY0Rx1za2Q3v5zcZj0ohqkFVRjeajroEz0NpxQIiCEDAl5ixKJ10vwwGh7GOj2PLf/NB/btm6kWRWdHn//kASy6iY8M6XnsUjiTqak3n6hAPoCHTw8TtTVaSMWJQJCyJA4dqMU9c2tWDBWsw/eIGcLWBjpIqmTdoI7k9Q9M2UU7vPqun3gDm87U/B1eOoG4zp5C/amyTAv0LHDaGRtQImAEDIk9qbJYGemr/FUCnwdHqZ6CXHqL91I70xSJ3Y0wz8jfDS6loEuH162Juo3gr1XitCoUOLRMO1qJL6DEgEhpNdyKxrw5bEsvLLzCpoUna+w1Z2qBgWSb5ZjfpAI/F7MsBnuI0RFvUL9AX73JHVfRo+FvkDz5SP9Hc0gKaoFYwzbz+djtMgMgU7mvX6WkWDwRy4QQu5JJTVyHLhWhISrRbhWWAMeD2AMcLU2xov3e/XqWgd/L0arimFBUO/q4+90I026WYYxTubqSeo+WDgGnra9GwXs72CG/16W4df0EmSU1GHdQ2O0rpH4DkoEhJBuZZfV4d97r+NCbhUYA8aIzPHvB/0wN8AR7x6Q4LuTOVgS4gQHc0ONr7kvTQZvO5Ner8drY6KPQCdzJN8swwxfW3z0awYixHZYOqH3o4DvjDB+98ANmOoLBn1+n+GEqoYIId364FAGJEW1eHGmF0783zTsX3Ufnp7iDntzA7zxgC+UjGH94Y7r7naloLIRqfm3sWCsqE/fwKf52OKKtBrP77gMK2M9rHsooE/XuTPPkKy6CQ+NE3U5JYU2oERACOmStKoRSTfL8MQkN7x0f8dJ2JytjPDMlFHYe6UIlwtua3TNfVfaViqc38tqoTvuzEaaX9WIz5cEdTsiuTvmhrpwtmp7i1mupY3Ed1AiIIR0Kf5iAXgAou9aH+Cv/hHuCVtTfazdL+lxUjjGGPZekWHCKCuILDSvSrpboJMFvO1M8MIML0zqZioJTczys8ccsT287XpXRTXSaO+7ECGkW4pWFXZelGKGr223H9rG+gK8NscX/9x1FfuuyrBwrFOXZa/LapFT3oCnNRj01RW+Dg9HXp7W5/Pvtnqe/4Bc515HbwSEkE79ml6CinqFRtUmD40VIcDJHOsP30SjorXLcnuvyKDH10HkaIeBDJX0EyUCQrRMk0Kp0bKPP5/Ph7OVIaZ59bxOuI4OD6vn+qOkVo7vTt7qtEyLUoWEq0WY7ivkZBF40neUCAjRMjE/pWLmZ8kor2vuskx2WR0u5FZh2QRX6Gg44CvYzQpzAxyw6WQOZNVNANoGfKXmVeGtvdcR9sFxlNc1Y9H4oVnwhXSN2ggIucc1tyrB5/Eg4Pf8ve5S/m31pG2v7r6KH1eEdNr18ufzBdDl87A4uOv6/s78v0g/HJWUYvXe6/CyM8X+q0WQVTdBX6CD+/3t8NBYEWZqMNMoGVycvhEkJibCx8cHnp6eWLduXYfjW7ZsgVAoRFBQEIKCgvD9999zGQ4hIw5jDAs3nsOquDSNyn+bnA1LI1288YAvkm+WY8u5vA5lGhWt2HO5EA+MdoCNiX6v4hFZGOLZqe44nlGGzadvwdvOBJ8/EohLb83CxmXjKAkMU5y9ESiVSqxcuRJHjx6Fk5MTQkJCEBUVBX//9q30jzzyCDZs2MBVGISMaL/dqoSkuBaS4lqczirHlG7q828U1+LYjTK8Mssbz051x8XcKnx4KANh7tbwc/hzEff9V4tQJ2/t8wRsz8/wglhkjhA3K1j1sY8/GVycvRGkpKTA09MT7u7u0NPTQ3R0NPbt28fV7QjRStvPF8DcUBcuVkZ494AErUpVl2W/Sc6BsR4fT0x0A4/Hw0eLAmBupIsX4tIgb/lz4rjtFwrgbWeCEDfLPsWkJ9BBhNieksA9hLNEIJPJ4Oz8Z6OQk5MTZDJZh3J79uxBQEAAFi1aBKlU2um1YmNjERwcjODgYJSXd79UHSHaoqxWjl/TS7B4vBP+FemHzNJ6xKUUdFo2t6IBB68V4dGJruoeO9Ym+vh0cSCyyurxwaEbAIBrhdW4Vlijlat0abMh7TU0b9485OXl4dq1a5g1axaeeOKJTsvFxMQgNTUVqampEAp77spGiDbYeVGKVhXD8jBXRIjtEOZuhc+OZnbaNXTTyRzo8nXw9H3tB3JN9Rbi6ftGYdtv+TgmKcXP5/NhqMvHQi1cpUub9ZgI9u/fD5Wq69fNrohEonbf8AsLCyEStf/jsra2hr5+W2PU008/jUuXLvX6PoRoI6WKIS6lAPd52mCUjTF4PB5WzxWjpqkFXx7Pale2qLoJey4X4pEQZwhNOzb+vjrHB/4OZnhtzzUkXC3CgrGOMDOgfv7apMdEsHPnTnh5eeG1115DRobmMwyGhIQgKysLubm5UCgUiI+PR1RUVLsyxcXF6t8TEhLg5+fXi9AJ0V5JGWUoqpFjeeifcwD5O5rhkRAXbPstD9ll9er9m0/fAmNAzNTOp3XQF/Dx1dIgNCpaIW9RYXmodk/Apo16TAQ///wz0tLS4OHhgRUrVmDixImIjY1FXV1dt+cJBAJs2LABERER8PPzw5IlSyAWi7F69WokJCQAAL766iuIxWIEBgbiq6++wpYtWwbkoQgZ6X6+kA9bU33c79++O+b/zfaGoS4f7x+UAAAq65sRl1KA+UEiOFkadXk9T1tTfBk9Fv8I98BokXau0qXNeIyx7qcL/ENlZSV++uknfPHFF/Dz80N2djZeeOEFrFq1iusY2wkODkZqauqg3pOQ4URa1YipHydh1QwvvDLLu8Px2FM5+OBQBrb8LQSpebexMTkbR1+e1usVvMjI0t1nZ49vBAkJCVi4cCHCw8PR0tKClJQUHD58GFevXsWnn3464MESQrq3I6VtauiuVuVaMWkU3KyNsPaABFt/y8MDo+0pCZBu9TigbM+ePXj55ZcxderUdvuNjIzwww8/cBYYIaSj5lYlfrkoxUw/uy6XhtQT6OBfkX6I+amt88U/wj0HM0RyD+oxEbz99ttwcPhzytimpiaUlpbCzc0NM2fO5DQ4Qkh7iddLUNmg6HHU7yx/O8z2t4OeQIfq/EmPeqwaWrx4MXR0/izG5/OxePFiToMihHRu+/kCuFgZYUoPK3PxeDzEPh6MDcvGDVJk5F7WYyJobW2Fnt6fQ8X19AjJKYMAACAASURBVPSgUCg4DYoQ0tHNkjqk5FVhWaiLxlNDE6KJHhOBUChUd/cEgH379sHGpn/rhBJCem/HhXzo8XWweHzvpoYmpCc9thF89913WL58OZ5//nkwxuDs7Ixt27YNRmyEkD8wxpBwtQgRo+1h3cupoQnpSY+JwMPDA+fPn0d9fdtIRRMT6oZGyGArrW3G7caWPs8ISkh3NFqP4ODBg0hPT4dcLlfvW716NWdBEULayyxtG8nvZWs6xJGQkajHNoK///3v2LlzJ77++mswxrBr1y7k5+cPRmyEkD/cSQTedvRGTgZej4ng3Llz2LZtGywtLbFmzRr89ttvyMzMHIzYCCF/yCqth7WxHrUPEE70mAgMDAwAtI0kLioqgq6ubrtZQwkh3Mssq4MXvQ0QjvSYCObNm4fq6mq8+uqrGDduHNzc3LBs2bLBiI2Qe0ZmaR3mbzyL9w9K8HthDTScy1EjjDFkldbD247aBwg3um0sVqlUmDlzJiwsLPDwww9j7ty5kMvlMDenIeuE3MEYw5p96bhZUgtJUQ02n86Fu40x5gU6IirIER7C/n2TL6qRo765FV6UCAhHun0j0NHRwcqVK9Xb+vr6lAQI+YsjklL8dqsS/4r0w8U378e6h8bAzswAX53IwsxPTyJqwxnIqpv6fP07DcU+lAgIR3qsGpo5cyb27NkzoK+6hIwUza1KfHDoBrxsTbBsggssjPQQPcEFcTFhOP//ZuLfD/oho7gOm0/d6vM9sqjHEOFYj4lg06ZNWLx4MfT19WFmZgZTU1OYmZkNRmyEDHtbzuYhv7IRb831h4Df/r+TnZkBnp7ijgfG2GPPpUI0Klr7dI/M0noITfVhYaTXc2FC+qDHRFBXVweVSgWFQoHa2lrU1dWhtrZ2MGIjZFgrr2vG1yeyMdPXFlO9hV2WezTMFXXNrdh/tahP98kqraO3AcKpHhPBqVOnOv3RRGJiInx8fODp6Yl169Z1WW7Pnj3g8Xi0BCW5p3x29CbkLUq8+aBft+WCXS3hY2eKn88X9PoeKhVDVlk9jSgmnOpxiomPP/5Y/btcLkdKSgrGjx+PEydOdHueUqnEypUrcfToUTg5OSEkJARRUVHw9/dvV66urg5ffvklQkND+/gIhAy+9KIaxF+U4snJo+DeQ68gHo+H5WEuWL0vHVel1Qh0ttD4PrLqJjQqlNR1lHCqxzeC/fv3q3+OHj2K69evw9Ky54mvUlJS4OnpCXd3d+jp6SE6Ohr79u3rUO6tt97C66+/rh64RshwxxjD2v0SWBrp4YWZXhqds3CsCEZ6fPx8vnfTs9DUEmQw9JgI/srJyQk3btzosZxMJoOz85+Lazs5OUEmk7Urc/nyZUilUjz44IPdXis2NhbBwcEIDg5GeXl5b0MmZED9ml6CC7lVeGWWN8wNdTU6x9RAF/ODRNh/rQg1jS0a3yuztG3WXxpDQLjUY9XQqlWrwOO1rYakUqlw5coVjBvX/+XvVCoVXnnlFWzZsqXHsjExMYiJiQEABAcH9/vehPSVvEWJ9w/dgI+dKaJDnHs+4S7LQ10Ql1KAPZcL8eR9ozQ6J6u0DvZmBhonHEL6osdEcPcHr0AgwNKlSzF58uQeLywSiSCVStXbhYWFEIlE6u26ujpcv34d4eHhAICSkhJERUUhISGBPuzJsHX4ejGkVU3Y9uSEDt1FezJaZI4gZwtsv5CPv012U3/B6g7NMUQGQ4+JYNGiRTAwMACfzwfQ1gjc2NgIIyOjbs8LCQlBVlYWcnNzIRKJEB8fjx07dqiPm5ubo6KiQr0dHh6OTz75hJIAGdauSmtgqMvH5B4Wj+/Ko2Gu+Oeuqzh/qwoTPay7LatSMWSX1WN5qGuf7kWIpjQaWdzU9Ofw+KamJtx///09XlggEGDDhg2IiIiAn58flixZArFYjNWrV7dbA5mQe4mkqBZ+Dqbg93Hx+LkBDjA31MXPF3puNJbeboS8RUUNxYRzPb4RyOXydstTmpiYoLGxUaOLR0ZGIjIyst2+tWvXdlo2OTlZo2sSMlRUKgZJcS0WjhX1XLgLBrp8LB7vhC3n8lBWJ4etade95W6W/LEqGTUUE471+EZgbGyMy5cvq7cvXboEQ0NDToMiZDiS3m5EfXMrxI79m2JlWagLWlUMv1yUdlsuq+yPHkO29EZAuNXjG8EXX3yBxYsXw9HREYwxlJSUYOfOnYMRGyHDSnpR29QqYsf+zcDrLjTBZE9rxKVI8Vy4Z5fVTJmldXA0N4CpAfUYItzqMRGEhIQgIyMDN2/eBAD4+PhAV5f+MIn2SS+qgUCHNyC9eB4NdcVz2y/jREYZZvnbdVoms7Qe3vZULUS412PV0MaNG9HQ0IDRo0dj9OjRqK+vxzfffDMYsREyrKQX1cLT1gQGuvx+X+t+fzuILAzx3cmcTqd4V6oYcsppVTIyOHpMBJs3b4aFxZ9zo1haWmLz5s2cBkXIcJReVAv/frYP3KHL18Gz09xxKf82LuRWdTieX9kARauK2gfIoOgxESiVynbfWJRKJRQKBadBETLclNXJUV7X3O/2gbstCXaGjYk+NiZldzh2Z2oJeiMgg6HHRDBnzhw88sgjOH78OI4fP46lS5figQceGIzYCBk2/mwoHrhFmQx0+Xh6yiiczqrAtcLqdsfuTDbnSW8EZBD0mAjWr1+PGTNm4LvvvsN3332HMWPGtBtgRog2kPyRCAaqauiO5aEuMDMQ4JuknHb7M0vr4GRpCGP9HvtzENJvPSYCHR0dhIaGws3NDSkpKThx4gT8/LpfiIOQkSa9qAYuVkYwG+CunKYGulgxyQ2J6SXqtYkBIKuUGorJ4OkyEWRmZuKdd96Br68vVq1aBRcXFwBAUlISnn/++UELkJDhIL2odkCrhe62YvIoGOry8e3JtreCFqUKtyrqabI5Mmi6TAS+vr44ceIEDhw4gDNnzmDVqlXqiecI0SZ18hbkVzbC34GbRGBlrIdloS7Yd6UI0qpG5Fc2oEXJ4ENvBGSQdJkI/vvf/8LBwQHTp0/HM888g+PHj3fa35mQke5GcVuVjVjETSIAgGemuEOHB2w6lUM9hsig6zIRLFiwAPHx8cjIyMD06dPxxRdfoKysDM899xyOHDkymDESMqTSi2oA9H9qie7Ymxtg0Xgn/JJaiDPZFeDxAI8e1kImZKBoNOncsmXLsH//fhQWFmLs2LFYv379YMRGyLCQXlQLGxM92Jrqc3qfZ6d6oFWpQlxKAVysjGCoR1WxZHD0aoklS0tLxMTE4Pjx41zFQ8iw0zai2FyjFcX6w83GGPMCHcEY4GVL1UJk8PR68XpCtElzqxJZpXWc9Rj6q+fCPQAAfg6UCMjgodEqhHQjq7QerSo2aInA194MO2PC4EOzjpJBRImAkG7caSjmqutoZ0Ldu1/LmJCBxmnVUGJiInx8fODp6Yl169Z1OH5nyoqgoCDcd999kEgkXIZDSK+lF9XCWI8PN2vjoQ6FEM5wlgiUSiVWrlyJw4cPQyKRIC4ursMH/bJly/D777/jypUreO211/DKK69wFQ4hfZJeVAs/BzPo9HGxekLuBZwlgpSUFHh6esLd3R16enqIjo7Gvn372pUxM/vzdbuhoYHzXhmE9IZKxXCjmLupJQgZLjhrI5DJZHB2dlZvOzk54cKFCx3Kbdy4EZ999hkUCgVOnDjR6bViY2MRGxsLACgvL+cmYEL+Iq+yAY0KJacDyQgZDoa8++jKlSuRk5OD9evX47333uu0TExMDFJTU5GamgqhUDjIERJtlc7R1NOEDDecJQKRSASpVKreLiwshEgk6rJ8dHQ09u7dy1U4hPRaelEtdPk8mvOHjHicJYKQkBBkZWUhNzcXCoUC8fHxiIqKalcmKytL/fvBgwfh5eXFVTiE9Fp6UQ28bE2hJxjyF2dCOMVZG4FAIMCGDRsQEREBpVKJJ598EmKxGKtXr0ZwcDCioqKwYcMGHDt2DLq6urC0tMTWrVu5CoeQXmGMQVJUi+m+tkMdCiGc43RAWWRkJCIjI9vtW7t2rfr3L7/8ksvbE9JnpbXNqGxQUI8hohXonZeQTgzG1NOEDBeUCAj5C8YYdlwogJ5Ah3oMEa1AiYCQv/j5fD6OZ5Th9Tm+MNGn6bjIyEeJgJC7ZJbW4b2DNzDNW4i/TXIb6nAIGRSUCAj5g7xFiRfi0mCiL8AniwNpfiGiNei9l5A/rE/MQEZJHf6zIhhCjpelJGQ4oTcCQgAk3yzDj2fzsGKSG2b42g11OIQMKkoEROtV1Dfjn7uuwcfOFG884DvU4RAy6KhqiGg1xhhe3XUVtfIWbH86FAa6/KEOiZBBR28ERKttv1CApJvleDPSj9YJJlqLEgHRWtWNCnxy5CYmeVjj8YmuQx0OIUOGEgHRWl8cy0JtUwtWz/On1fGIVqNEQLRSdlkdfjqfj6UTXOBrT9NIEO1GiYBopXcP3ICRHh+vzPIe6lAIGXKUCIjWScoow8nMcrw40wvWJjRwjBBKBESrtChVePegBO42xnh8ottQh0PIsECJgGiVn37Lx63yBrz5oB8tQUnIH+h/AtEatxsU+OJYJqZ42WAGLUFJiBqniSAxMRE+Pj7w9PTEunXrOhz/7LPP4O/vj4CAAMycORP5+flchkO03OfHMlHf3Iq35lJ3UULuxlkiUCqVWLlyJQ4fPgyJRIK4uDhIJJJ2ZcaOHYvU1FRcu3YNixYtwmuvvcZVOETLZZbWYfuFAiwPdYW3HY0gJuRunCWClJQUeHp6wt3dHXp6eoiOjsa+ffvalZk+fTqMjIwAAGFhYSgsLOQqHKLFzmZX4In/pMBYj4+XqbsoIR1wlghkMhmcnZ3V205OTpDJZF2W/+GHH/DAAw90eiw2NhbBwcEIDg5GeXn5gMdKRiZ5ixLvHpBg+fcXYKjHx/anw2BlrDfUYREy7AyL2Ud//vlnpKam4uTJk50ej4mJQUxMDAAgODh4MEMj9yhJUS1e2pmGzNJ6PBbmin9F+sFQj2YWJaQznCUCkUgEqVSq3i4sLIRIJOpQ7tixY3j//fdx8uRJ6OvT4B7SP0oVw+bTt/DpkZuwMNLDj38LwXQf6iFESHc4SwQhISHIyspCbm4uRCIR4uPjsWPHjnZl0tLS8OyzzyIxMRG2tvSflfTfW/uuY8eFAswR2+ODh8ZQVRAhGuAsEQgEAmzYsAERERFQKpV48sknIRaLsXr1agQHByMqKgqvvvoq6uvrsXjxYgCAi4sLEhISuAqJjHB18hbsuVSIxeOd8NGiAOoiSoiGOG0jiIyMRGRkZLt9a9euVf9+7NgxLm9PtEzi9RI0t6qwNNSFkgAhvUAji8mIsfeKDK7WRhjrbDHUoRByT6FEQEaE0lo5zuVUYn6QiN4GCOklSgRkREi4UgTGgAVBjkMdCiH3HEoEZNAoWlX4+ngW9qbJ0NDcOqDX/l+aDIHOFnAXmgzodQnRBsNiQBkZ+VqVKry88woO/l4MADDQ1cH9fnaYHyTCVG8b6Av6Ptgrs7QOkuJarJnnP1DhEqJVKBEQzqlUDK/v+R0Hfy/GvyJ9EeRsiYSrMhy8VowD14phZiDAA6Md8PIsb9ibG/T6+nvTZODr8DA3gKqFCOkLSgSEU4wxrElIx57LhXj5fm/ETPUAAEwYZYU188Q4k12B/VeKsPeKDDVNLfjusfG9ur5KxbDvShGmeNlAaEoj0wnpC0oE9xBpVSN0dHgQWRgOdSgaYYxh3eEM/HQ+H89Oc8cLMz3bHdfl62C6jy2m+9hCaKqP78/koqRG3qu3gtT825BVN+HVCJ+BDp8QrUGNxfeIVqUKy74/j39svzzUoWjsq+PZ2HTqFh4Lc8Ubc3y77da5LNQFShVD/MWCXt3jf2kyGOnxMVts199wCdFalAj6QFbdBMbYoN5z/7UiSKuacFVajfK65kG9d19sPnULnx/LxKLxTngnStxj335Xa2NM8bJBfIoUrUqVRvdoblXi0O/FiBDbw0iPXm4J6StKBL10LqcCk9edwPEbZYN2T5WK4ZukHFj/MYHaqczhuyZDfXMrXt99De8fuoEHAxyw/uEA6OhoNsDr0TBXlNTKcTxDs3/b5JvlqGlqwXwaO0BIv1Ai6KUNJ7IBAEckJYN2zyOSUmSV1WP1PH8ITfWRdHPwklBvXMqvQuSXp7HrkhT/CPfAF48Ega9hEgCAmb62cDA3wM/nNVu7em+aDDYmerjP06avIRNCQImgVy4X3Ma5nEoY6fFxMrN8UKqHGGP4JjkbrtZGeHCMA6Z5C3E6q0Lj6pPBoGhV4eNfM7D4u9/AwLDz2Yl4bY4vdPm9+/MS8HUQHeKC01kVyKto6LZsTVMLjmeUYV6gIwS9vA8hpD36H9QL3yTlwMJIF6/P8UVpbTNuFNdxfs8z2RW4VliD56Z5QPBHL5uaphZcLazm/N6ayC6rw0PfnsXGpBw8PM4Jh16YghA3qz5f75EQZ/B1eIhL6b7ROPF6MRStKiwI6rjYESGkdygRaCijpBbHbpTib5NG4YHR9gAwKFU0G05kw97MAAvHtX3g3edlA74OD0kZfW8n2HAiC4u+PYeWfr5V5JTXY+7XZyC73YTvHh2HjxcHwtRAt1/XtDc3wCw/O/ySKoW8RdlpmYbmVmw5lw93G2MEOJn3636EEEoEGvsmKQfGenw8MckVtmYGEDua4eRNbhttU/OqcCG3Cs9MdVdPwWBuqItxLhZIzuxbEvomORufHMlEav5tXLhV1a/4frkoRauS4cALUzBntEO/rnW3R8NccbuxBYevF3c4Jm9R4pltqcgsrcP/i/SjmUYJGQCUCDSQV9GAA9eK8GiYKyyM2nruhPsIcangNmqaWji77zfJObA00sXSCc7t9of72OK6rBZldfJeXW/L2Vx8lHgTDwY4wFCXj8T0jh+0mlL+MaJ3mrdwwAe4TfKwxigbY2w/3756SNGqwj+2X8ZvtyrxyeIAzPKnsQOEDARKBBrYdCoHAr4OnpoySr0v3McWShXDmawKTu6ZXlSDExlleHLyqA595MN9hADQqzeSXy5K8fZ+CWb72+GLR4IQ7iPEkfRSqFR9a/C+kFuJklo5Fowd+Dp6HR0elk1wQWr+bWSU1AL4c9K6ExlleH/BGCwc6zTg9yVEW3GaCBITE+Hj4wNPT0+sW7euw/FTp05h3LhxEAgE2L17N5eh9FlJjRy7LxXikWBn2Jr+OfXBWGcLmBkIkMxRO8G3yTkw0Rfg8YluHY75O5jB1lQfyRqOJ0i4WoTX/3sNU72F+HrZWOjydRAhtkdZXTPSpH1rdN6bJoOJvgD3+3HzrXzReCfoCXTw8/l8qFQMr+25hoO/F+PfD/phWagLJ/ckRFtxlgiUSiVWrlyJw4cPQyKRIC4uDhKJpF0ZFxcXbNmyBcuWLeMqjH6LPXULKgbETHVvt1/A18EUbyEn3Uhvldfj4O/FeGyiK8yNOja+8ng8hPsIcTqzvMdupEfSS/DyzisIcbPCpkfHq9sapvvaQqDDw5H03o+HkLcocfj3EswZbQ9Dvb5PH90dS2M9zA1wwP8uy/D//vs7/ntZhv+b5Y2np7j3fDIhpFc4SwQpKSnw9PSEu7s79PT0EB0djX379rUr4+bmhoCAAOjoDM8aqsr6ZsSlFGB+kCOcrYw6HA/3FqKsrhmS4toBve+3yTnQ4+vgycmjuiwT7mOLWnkrrnTzjf5cdgWe35GG0SJz/GdFSLsPbXNDXUzytEFiekmvE9mJjDLUNbdy3nXz0TBXNCiU2Jkqxd+neeD5GZ49n0QI6TXOPoFlMhmcnf9s5HRycoJMJuPqdpzYci4P8lYl/hHu0enxaX/U1ScPYO+hmyV12HO5EEsnuHQ7rfJkzz+6kXZRNVVWJ8equDS4Whth699CYKLfcS6eCLEd8isbcbO0d+Mh/pcmg62pPiZ6WPfqvN4a62yBeYGO+Ee4B16f40M9hAjhyPD8Kv4XsbGxCA4ORnBwMMrLB2eenYySWmw5l4cIf3t42pp2WsbW1ACjRWYD1k7AGMO7ByQwNdDFizO9ui1rbqiL8a6WnSYhlYrh1V3XUN/cio3Lx6l7Ov3VLH878HhA4nXNq4eqGxVIvlmG+UGOvZo+oi94PB6+XjoWr/UwcykhpH84SwQikQhSqVS9XVhYCJGob1UJMTExSE1NRWpqKoRC4UCF2IG0qhEbk7Ix54tTmPPFaShaVVg1s/vqiHBvW1wuqEZNY/+7kR6/UYYz2RV46X4vWBp3/uHd7t4+QqQX1aKstn030i3n8nAysxxvPugHb7vOkxjQlsjGu1ji1/RSjWM8+HsxWpQM82lELyEjBmeJICQkBFlZWcjNzYVCoUB8fDyioqK4ul2fNbcqsfVcHh765iymfJSEj3+9CWN9AdbOF+PsGzMgdux+5Gq4jxBKFcPp7P69qShaVXj/0A14CI3xaJirRueEe9sCQLveQ5KiWqw7nIGZvrZ4TIPrzBltjxvFtSiobNTonnvTZPCyNYHY0Uyj8oSQ4Y+zRCAQCLBhwwZERETAz88PS5YsgVgsxurVq5GQkAAAuHjxIpycnLBr1y48++yzEIvFXIXTpdV707EmIR2NCiVen+OLM69Px57nJuHxiW6wMel56cMgZwuYG+r2u51g2295yK1owL/n+ms8WZufgynszPTV4wnkLUq8GJ8GcyNdfLQoQKPqlAhx23QZv2rQe0ha1YiLebexYKyIqmoIGUE4Xc0jMjISkZGR7fatXbtW/XtISAgKCwu5DKFbh38vxs5UKZ4L98Drc3z7dA0BXwdTvGxwMrMcKhXTeO79u1XWN+PL41kI9xFiuo+txufxeDyEe9vi0PVitCpVeP/gDWSV1WPbkxNgrUESAwBnKyP4O5jh1/QSPDO1+66ZCVeLAIDm/ydkhLknGou5UFTdhDf++zsCnczxyizvfl0r3McW5f3oRvrZ0Uw0KpT494N+fbi3EHXyVnz86038dD4fT983ClO9e9eOEiG2x6WC291OWcEYw//SZJjgZgUny45daQkh9y6tTARKFcPLO6+gRanCl9Fjez1v/l9N877TjbTz3kPNrcou++rfKK5FXEoBHgtz7bJ3Uncme9lAoMPDplO34O9ghlfn9H4R94jRdmAMOCrputE4vagW2WX1nEwpQQgZWlqZCL47mYMLuVV4J0oMNxvjfl9PaKqPMSLzdu0EDc2t2Jsmw5NbLkK8+leEf5KMT4/cRNZdffbvdBc1M9TFS/d33120K2YGugh2s4SBrg6+WhqkHjncGz52pnCzNuq2G+neNBl0+TxEjrHvU5yEkOFL61b8viqtxudHM/FggAMWjR+4icvCfYTYmJSNfVdkOCopxbEbpZC3qOBgboBHw1yRXVaPjUnZ+PpENnztTTE/SAQzQwHO5VTinShxl339NbHuoQDUNLX06Y0CaGtriBDb44czuahpaoG5YftpLZQqhoSrRZjuY9uvOAkhw5NWJYL65la8GJ8GOzMDfLBgzID2fAn3scXXJ7LxYvwVWBrpYtF4J0QFihDsaqluQC6rk+PQtWLsu1qE9YkZAAAvWxMs7+ckagPxVhMx2h6bTt1CUkaZuvqnSaHE8YxS/PeyDGV1zVhI1UKEjEhalQjeTkhHQVUj4mMmdjqZW3+Mc7HAO1FiuFgb4T5Pm07bHWxNDbBi8iismDwKBZWNOCIpwX1eNsNizd0gJwvYmurjwLVimBvqIuFqEY6kl6BBoYTQVB/PhXvQ/P+EjFBakwgOXCvC7kuFWDXDExNG9X1N3a7weDw8MclN4/Iu1kbDaiZNHZ226qGfzufj2I1SmBkIMC/QEVGBjgh1t+Z8OglCyNDRmkRgbqiL2f52eKGHOXy02VP3jQIDwzRvW0z1tulTwzMh5N6jNYlgipcQU7y4m6doJHCzMcZ7C8YMdRiEkEE29JXThBBChhQlAkII0XKUCAghRMtRIiCEEC1HiYAQQrQcJQJCCNFylAgIIUTLUSIghBAtx2NdTZQ/TNnY2MDNza1P55aXl0Mo1K5BZfTM2oGeWTv055nz8vJQUVHR6bF7LhH0R3BwMFJTU4c6jEFFz6wd6Jm1A1fPTFVDhBCi5SgREEKIluO//fbbbw91EINp/PjxQx3CoKNn1g70zNqBi2fWqjYCQgghHVHVECGEaDlKBIQQouW0JhEkJibCx8cHnp6eWLdu3VCHw4knn3wStra2GD16tHpfVVUVZs2aBS8vL8yaNQu3b98ewggHllQqxfTp0+Hv7w+xWIwvv/wSwMh+ZrlcjgkTJiAwMBBisRhr1qwBAOTm5iI0NBSenp545JFHoFAohjjSgadUKjF27FjMnTsXwMh/Zjc3N4wZMwZBQUEIDg4GwN3ftlYkAqVSiZUrV+Lw4cOQSCSIi4uDRCIZ6rAG3IoVK5CYmNhu37p16zBz5kxkZWVh5syZIyoJCgQCfPrpp5BIJDh//jw2btwIiUQyop9ZX18fJ06cwNWrV3HlyhUkJibi/PnzeP311/Hyyy8jOzsblpaW+OGHH4Y61AH35Zdfws/PT72tDc+clJSEK1euqMcOcPa3zbTAuXPn2OzZs9XbH3zwAfvggw+GMCLu5ObmMrFYrN729vZmRUVFjDHGioqKmLe391CFxrmoqCh25MgRrXnmhoYGNnbsWHb+/HlmbW3NWlpaGGMd/95HAqlUymbMmMGOHz/OHnzwQaZSqUb8M7u6urLy8vJ2+7j629aKNwKZTAZnZ2f1tpOTE2Qy2RBGNHhKS0vh4OAAALC3t0dpaekQR8SNvLw8pKWlITQ0dMQ/s1KpRFBQEGxtbTFr1ix4eHjAwsICAkHbEuQj8e/7pZdewkcffQQdnbaPrMrKyhH/zDweD7Nnz8b48eMRGxsLgLv/z1qzeD1p+8Pi8XhDHcaAq6+vx8MPP4wvvvgCZmZm7Y6NxGfm8/m4cuUKACohWwAABNFJREFUqqursXDhQmRkZAx1SJw6cOAAbG1tMX78eCQnJw91OIPmzJkzEIlEKCsrw6xZs+Dr69vu+ED+bWtFIhCJRJBKpertwsJCiESiIYxo8NjZ2aG4uBgODg4oLi6Gra3tUIc0oFpaWvDwww9j+fLleOihhwCM/Ge+w8LCAtOnT8dvv/2G6upqtLa2QiAQjLi/77NnzyIhIQGHDh2CXC5HbW0tXnzxxRH9zADUz2Nra4uFCxciJSWFs79tragaCgkJQVZWFnJzc6FQKBAfH4+oqKihDmtQREVFYevWrQCArVu3Yv78+UMc0cBhjOGpp56Cn58fXnnlFfX+kfzM5eXlqK6uBgA0NTXh6NGj8PPzw/Tp07F7924AI++ZP/zwQxQWFiIvLw/x8fGYMWMGtm/fPqKfuaGhAXV1derfjxw5gtGjR3P3tz0gLQ33gIMHDzIvLy/m7u7O3nvvvaEOhxPR0dHM3t6eCQQCJhKJ2Pfff88qKirYjBkzmKenJ5s5cyarrKwc6jAHzOnTpxkANmbMGBYYGMgCAwPZwYMHR/QzX716lQUFBbExY8YwsVjM3nnnHcYYYzk5OSwkJIR5eHiwRYsWMblcPsSRciMpKYk9+OCDjLGR/cw5OTksICCABQQEMH9/f/VnFld/2zTFBCGEaDmtqBoihBDSNUoEhBCi5SgREEKIlqNEQAghWo4SASGEaDlKBERrmZiYAGibnmLHjh0Deu0PPvig3fakSZMG9PqEDCRKBETr9SURtLa2dnv8r4ng3LlzvY6LkMFCiYBovTfeeAOnT59GUFAQPv/8cyiVSrz66qsICQlBQEAANm3aBABITk7GlClTEBUVBX9/fwDAggULMH78eIjFYvXEYG+88QaampoQFBSE5cuXA/jz7YMxhldffRWjR4/GmDFjsHPnTvW1w8PDsWjRIvj6+mL58uWgIT5k0AzIsDRC7kHGxsaMsfajVRljbNOmTezdd99ljDEml8vZ+PHj2a1bt1hSUhIzMjJit27dUpe9M7KzsbGRicViVlFR0e7af73X7t272f33389aW1tZSUkJc3Z2ZkVFRSwpKYmZmZkxqVTKlEolCwsLY6dPn+bu4Qm5C70REPIXR44cwbZt2xAUFITQ0FBUVlYiKysLADBhwgSMGjVKXfarr75CYGAgwsLCIJVK1eW6cubMGSxduhR8Ph92dnaYNm0aLl68qL62k5MTdHR0EBQUhLy8PM6ekZC7acXso4T0BmMMX3/9NSIiItrtT05OhrGxcbvtY8eO4bfffoORkRHCw8Mhl8v7fF99fX3173w+v8d2CEIGCr0REK1namqqnukRACIiIvDtt9+ipaUFAJCZmYmGhoYO59XU1MDS0hJGRkbIyMjA+fPn1cd0dXXV599typQp2LlzJ5RKJcrLy3Hq1ClMmDCBg6ciRHP0RkC0XkBAAPh8PgIDA7FixQq8+OKLyMvLw7hx48AYg1AoxN69ezuc9//bu2MaAEIgCqIfFzggNJBQIYEWE4hBAQbQgi06NFxu55WLgEm2WFprWmspxqgQgmqt722MoZSSSinae795713nHOWc5ZzTnFPe+99/LoNv4/ooABjHaggAjCMEAGAcIQAA4wgBABhHCADAOEIAAMYRAgAw7gKS+UQuni37kQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "# Plot Loss\n", - "fig = plt.figure(facecolor=\"w\")\n", - "plt.plot(acc_hist)\n", - "plt.title(\"Train Set Accuracy\")\n", - "plt.xlabel(\"Iterationspk_rec, mem_rec = forward_pass(net, num_steps, data)\")\n", - "plt.ylabel(\"Accuracy\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "1gb1wCQb2bMd", - "metadata": { - "id": "1gb1wCQb2bMd" - }, - "source": [ - "## 3.2 Spike Counter\n", - "\n", - "Run a forward pass on a batch of data to obtain spike recordings." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "qLAfvj9D2AYd", - "metadata": { - "id": "qLAfvj9D2AYd" - }, - "outputs": [], - "source": [ - "spk_rec = forward_pass(net, data)" - ] - }, - { - "cell_type": "markdown", - "id": "VQnj40YC2hUV", - "metadata": { - "id": "VQnj40YC2hUV" - }, - "source": [ - "Changing `idx` allows you to index into various samples from the simulated minibatch. Use `splt.spike_count` to explore the spiking behaviour of a few different samples. Generating the following animation will take some time.\n", - "\n", - "> Note: if you are running the notebook locally on your desktop, please uncomment the line below and modify the path to your ffmpeg.exe\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "oTKhuyk22M57", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 991 - }, - "id": "oTKhuyk22M57", - "outputId": "f6e0b621-0084-4fa2-d8a9-30644c0b7e65" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The target label is: 3\n" - ] - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 88, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAs0AAAHBCAYAAACSZYZnAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nO3deZxVBf3/8feFgWGQ1Q0RRRTJXTFM09RySUihXMsNzQ0tTTRL03JJy9Qs0cqvotlCVv6+hSVmaImiuYY7aa7si4rAgKwy3N8f1CQRnhnTe4evz+fj0aO55557z2c8j9Pj1fHMOaVyuVwOAACwWq2qPQAAALR0ohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqsEdH8pa9eUO0RAAD4AFsjonlu/bxqjwAAwAfYGhHNAABQTaIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAAChQU+kNTpg0JZcP+58898JL6dq5c4Z+4bjstcdulR4DAACarKJnmpcta8hZX78ku+/6kYy57Vf5+ldOy/nf/l4mTZlWyTEAAKBZKhrNEydPyeuzZueoww5M69at85EP75Adtt06d9w1ppJjAABAs1T9muZyuZyXJ0yq9hgAALBaFb2muVfPjbJ21875+a9/m6MOOzDjnng6jz81PjvtuN0q644cNTq3jhqdJJlTX1/JMQEAYCWlcrlcruQGX3x5Qr57zfV5ecKkbLXF5unauXPatG2TC84eutrPDB5yRkYMH1bBKQEA4F8qfveMPr03zfCrL2t8ffypX8kB/fep9BgAANBkFb+m+cWXJ2TJkqVZvHhxRvx6ZGa9MSeDBuxb6TEAAKDJKn6m+Y677snv/nBnli1ryI7bb5MfXXlJ2rZtU+kxAACgySp+TfO74ZpmAACqqeq3nAMAgJZONAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFCgptIbnD7j1Vw27No887e/p22bNtn74x/LWacNSU1N60qPAgAATVLxM82XDbs2a3fpktG/HZGbb/xBHn9qfH7z+z9UegwAAGiyikfz9BmvZt+9dk9tbdusu07X7LZzv7w8cXKlxwAAgCareDQfcehncteY+7J48eK89vqsPPDIuOy284crPQYAADRZxa9p/vAO2+bW20fn4/t/Ng3Ll2dg/33yid13XWW9kaNG59ZRo5Mkc+rrKz0mAAA0KpXL5XKlNrZ8+fJ8+ogTctDAARn8uYOzcNGiXHzF1dlk4x4Zesrxq/3c4CFnZMTwYZUaEwAAVlLRyzPmzZufma++ns8dNDBt27ZJl86d8ukB++aBh8dVcgwAAGiWikZzly6d06N7t/zm93dk2bKGzJ//Zm6/8+706b1pJccAAIBmqfgfAl5x8dfz4KOP5ZMHHpkDjxqSmpqafPnUEys9BgAANFnF/xBwiz6bZfjVl1V6swAA8K55jDYAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFaiq5sT0GHLrS6yVLl+bQz+yfs4eeUskxAACgWSoazfeP/k3jzwsXLkr/gwdn30/sXskRAACg2ap2ecaY+x7M2l07Z8ftt6nWCAAA0CRVi+bb77w7+++3d0qlUrVGAACAJqno5Rn/NGPma3n8qfE5/+zTV7vOyFGjc+uo0UmSOfX1lRoNAABWUZVo/sNdY9J3u63To/sGq13n4EEDcvCgAUmSwUPOqNRoAACwiqpcnnHHXWNyQP+9q7FpAABotopH81Pjn8trs95w1wwAANYYFY/m2++8O3vtsVvWat++0psGAIB3peLXNH/9rNMqvUkAAPiveIw2AAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUqEo033n32Bx6zCnZfcAh+cyRJ+aJp8dXYwwAAGiSmkpv8OFxT+QHw3+a71xwTrbZ6kOZ9cbsSo8AAADNUvFoHv6Tm3PiMUdku222TJKsv966lR4BAACapaKXZzQ0NOTZ51/K3Ln1OfDIk7L/ocfm8mH/k8VLllRyDAAAaJaKnmmePWduli1blrvHPpAbf3B5alq3zpe/8a38eMQtOfXEY1Zad+So0bl11OgkyZz6+kqOCQAAK6nomeba2tokyecOHpR111k7Xbp0zlGHHZgHHh63yroHDxqQEcOHZcTwYenauXMlxwQAgJVUNJo7deyQbuutm5T+taxUKq3+AwAA0AJU/JZzgz61b/7fyNsze87czJv/Zn75v7/LHrt+pNJjAABAk1X87hknHnN45tbPy8FHn5zatm2y71575PijP1fpMQAAoMlK5XK5XO0higweckZGDB9W7TEAAPiA8hhtAAAoIJoBAKCAaAYAgAKiGQAACohmAAAoIJoBAKCAaAYAgAKiGQAACohmAAAoIJoBAKCAaAYAgAKiGQAACohmAAAoIJoBAKCAaAYAgAKiGQAACohmAAAoIJoBAKCAaAYAgAKiGQAACohmAAAoIJoBAKCAaAYAgAKiGQAACohmAAAoIJoBAKCAaAYAgAKiGQAACtQ0Z+UJk6ZkwqQp6d5t/Wy1xeb5+wsv5Yc3/Cxz5s7Lbrv0yxeOPzqtWr1zhw8Z+rWMf/b5tG7dOkmy3nrrZOSI69/9bwAAAO+zZkXz8J/+MnePfSBf+dKQfGjzTfPl8y7JrNlzUi6X8+LLE1LXrl2OP/qzhd9z9tBTcuDA/u96aAAAqKRmXZ7x3PMvJUl22WnHPPf8S3n9jdlZZ+0u2XrLPimXy7nz7rHvy5AAAFBNzYrmN2bPSZJ077Z+Xnh5QpLkuKM+m2HfuTBJMvO115v0PT+84WfZ59NH5vjTvppxTzzdnBEAAKDimnV5RqvWKxr7zQUL8uLLE1IqlbJZr55pX1eXJCkvLxd+x+knH5dNe22cNjVtcteY+/Ll8y7JL2+8Jhv16L7SeiNHjc6to0YnSebU1zdnTAAAeE8160xzj+4bJElOOO2rue2Pf04pSZ/em+a112clSdbu2qXwO7bdeous1b592rZtk4ED9skO226VvzwybpX1Dh40ICOGD8uI4cPStXPn5owJAADvqWZF80ED+6dcLmfq9JlZunRpdt9153Tu1DF//cclFltv2afZA5RKpaRcfIYaAACqpVmXZxx24AHp3Kljnhr/XLp3Wy+HHnhAkqRzp4456dgjsnO/vu/4+fnz38z4557Ph3fYLq1bt86f7rkvjz89Pmd9aci7/w0AAOB9ViqXK3ead87c+gw956JMnDw1rVq1Sq+eG+WUE47OR3fa8R0/N3jIGRkxfFiFpgQAgJUVnmm+/c67m/WFA/vvs9r3unbpnJ9ff1Wzvg8AAKqtMJq/edmwFdcdN0Ep7xzNAACwJmrSNc1NvoKjiXENAABrksJovu6qSysxBwAAtFiF0dyv73aVmAMAAFqsZt1yLkmWLVuW2/7454x74unMn/9mfvDdi/PE0+NTLidb9umd9u3r3o85AQCgapoVzQsXLsopXz4vf3/h5ZTL5cY/EPzFLb/L/Q89mrNOOymfO3jQ+zIoAABUS7OeCHj9T2/Oc8+/tMofBh40aMWTAsf+5eH3dDgAAGgJmhXNY8Y+mFKplCsuPnel5X233TpJMnHy1PduMgAAaCGaFc2zZs9Okuz+0Y+stLympnWSZG79vPdoLAAAaDmaFc2dOnZMkkybPnOl5WPue2jF+506vkdjAQBAy9GsaP7n7efO+sa3G5d99YJLc/EVV6dUKuUjO27/3k4HAAAtQLOiecjnj0xdu9pMnjqt8c4ZY//ycBoaGlLXrjYnHHP4+zIkAABUU7OiuVfPjXLDNZdnpx23S6lUWnHbuaw4A3391ZelV8+N3qcxAQCgepr9cJMPbb5Z/uf7l2bxkiWZP//NdOzYIe1qa9+P2QAAoEVodjQnyZPPPJunxj+b12e9kfXWXSd9t9smO2y71Xs9GwAAtAjNiub589/MeRdfkUcee3KV9z6604759vlfTceOHd6z4QAAoCVo1jXNV1x9XR4e90TK5fIq/3l43BO54prr3685AQCgapp1pvm+Bx9JqVTKTn23y/GDP5f11l0nr896IzeNuCV/feLp3PeAx2gDAPB/T7OiuU2bNlm0eEkuveDsdOnSOUmyycY9slmvnul/8ODU+oNAAAD+D2rW5RkH9N87STJr9pyVls+eM3fF+/vt/R6NBQAALUfhmebb77y78edNe26ctbt2yelnX5jPHLBfuq23bl59fVZuu+NPWXedrtlk4x7v67AAAFANpXK5XH6nFT6y16DGp/+9k3K5nFalUh4Zc9t7Ntw/DR5yRkYMH/aefy8AADRFk65pLujqf633X40CAAAtU2E0X3fVpZWYAwAAWqzCaO7Xd7tKzAEAAC3Wu3qM9vMvvpLJU6dlydKlq7w3sP8+//VQAADQkjQrmufWz8uZ534zf/v7i//x/VJEMwAA//c0K5qvvfHnGf/cC6tfoQl32QAAgDVNsx5u8uCjj6VUKmXI549MkpRKpVx16QXZYdutsnGP7rnqOxe8L0MCAEA1NSua3/jHkwCPPOwzjct23/Uj+fb5Z2fKtBm59y8PN/m7Jk+dlt0+eVDO/9aVzRkBAAAqrlnR3LZt2yRJbdva1Nau+Hny1Glp1WrFZRl33/uXJn/X5cOuy9Zb9mnO5gEAoCqadU3zOmt3zdRpMzK3fl569tgwL02YlJPPODetWrVe8WU1Tfu6O+8em44d1sr222yZqdNmNH9qAACooGadad6yT++Uy+U8+/wLGfDJT6RcLmfWG3Py2uuzkiT77b1H4Xe8uWBhrv/JzTnziye+u4kBAKDCmnWm+ezTT86Qzx+Ztbt2yZ677ZJWrVplzNgH8tZby9J7001y2IEDC7/juptG5NP775du66/7juuNHDU6t44anSSZU1/fnDEBAOA9VSqXy+X/9kseevSxnH7ORWlVKuWRMbetdr3nX3wl53/7ytx8w9Vp06ZNrv/JzZk6bUYu+cZX3vH7Bw85IyOGD/tvxwQAgHflXT0RcHWK6vuxJ5/J9JmvZuBnj0uSLFy0OMuXL88rJw3NzTdc/V6OAgAA75n3NJqLHDyof/bbe8/G17+4ZWSmz3w153751EqOAQAAzVLRaG7Xrl3atWvX+Lqurl1q27ZN1y6dKzkGAAA0S2E0P/7U+MIveemVSe9q4ycfd9S7+hwAAFRSYTSffMa5KZVKlZgFAABapCZdnvEe3GADAADWWIXRPLD/PpWYAwAAWqzCaL7wa2dUYg4AAGixmvUYbQAA+CASzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAFaiq9wfO/dWUeffypLF68OOus3TXHHH5IDhzYv9JjAABAk1U8mj9/1GE5/+yhadu2TSZOmpKTzzg3W/Tpna222LzSowAAQJNU/PKM3ptukrZt26x4USolpVKmTp9R6TEAAKDJKn6mOUkuu+rajBp9d5YsWZIt+vTOx3bZqRpjAABAk5TK5XK5GhtuaGjIM3/7e8Y9+Uw+f+ShqalZud9HjhqdW0eNTpLMqa/P7bf8pBpjAgBA9aL5ny793g+zWa+eOfyQT692ncFDzsiI4cMqOBUAAPxL1W8519Cw3DXNAAC0aBWN5tlz5ubOu8dm4cJFaWhoyEOPPpY7x4zNRz7ct5JjAABAs1T0DwFLpVJ+e9sf853vX5tyeXk26LZ+zjrtpHz8Y7tUcgwAAGiWikZz1y6dM/zqyyq5SQAA+K9V/ZpmAABo6UQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABUQzAAAUEM0AAFBANAMAQAHRDAAABWoqubGlS9/KZcOuzaOPPZl5897MRhtukFOHHJuP7bJTJccAAIBmqeiZ5oaGhnRbb90MH3ZZ7v3DLfnCCYNz7kWXZ/qMVys5BgAANEtFzzTX1bXLyccd1fh6j912zobdu+W5F17Kht27VXIUAABosopG8797Y/acTJ4yLb179VzlvZGjRufWUaOTJHPq6ys9GgAANCqVy+VyNTa8bNmynH72henRo3u+ftZp77ju4CFnZMTwYRWaDAAAVlaVu2csX74853/7e6lpU5Nzhp5SjREAAKDJKn55RrlcziVXXJPZc+bm6ssvSk1NVa8QAQCAQhUv1u98/0eZMGlKrv3et9KutrbSmwcAgGaraDTPmPlaRo4anbZt2qT/wYMbl5931qn51Cf3quQoAADQZBWN5u4brJ9x995eyU0CAMB/zWO0AQCggGgGAIACohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqIZgAAKCCaAQCggGgGAIACohkAAAqIZgAAKFDxaL5l5KgMHnJGdv3kgbnoO1dVevMAANBsNZXe4HrrrpMTBn8uD/318SxZsrTSmwcAgGareDTvveduSZJnn38pr70+q9KbBwCAZnNNMwAAFKj4meamGjlqdG4dNTpJMqe+vsrTAADwQdZio/ngQQNy8KABSZLBQ86o8jQAAHyQuTwDAAAKVPxM87JlDWloaMjy5Q1pWL48S5YsTevWrVNT07rSowAAQJNUPJp/POLXueFnv2p8/cc/3ZOTjj0iJx93VKVHAQCAJql4NJ983FECGQCANYprmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAAChQ8Wiunzc/X/nGt7L7gEMy8HPHZfSf7630CAAA0Cw1ld7g5cP+J23atMldI3+RF156JUPP/Wb69N40vTfdpNKjAABAk1T0TPOiRYsz5r4Hc8rxR6d9+7r03X6b7LnbLrnjrnsqOQYAADRLRaN50tRpad26dTbZuEfjsg/13jSvTJxUyTEAAKBZKnp5xqJFi9Khfd1Kyzp0aJ8FCxetsu7IUaNz66jRSZI59fUVmQ8AAP6TikZzXV1d3vy3QF6wYGHW+reQTpKDBw3IwYMGJEkGDzmjIvMBAMB/UtHLMzbZqEcaGhoyeeq0xmUvvDwhm/XyR4AAALRcFY3murp22WuPXXPdTTdn0aLFefKZZzP2gUey/357VXIMAABolorfp/lrZ34xS5YszScPOipfv+S7OffML7rdHAAALVrF79PcuVPHfO/b36j0ZgEA4F3zGG0AACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKiGYAACggmgEAoIBoBgCAAqIZAAAKrBHR3KVzp2qPAADAB1ipXC6Xqz0EAAC0ZGvEmWYAAKgm0QwAAAVEMwAAFBDNAABQQDQDAEAB0QwAAAVEMwAAFBDNAABQQDQDAEAB0QwAAAVEMwAAFBDNAABQQDQDAECBmmoP0BSf/fwXU9u2bbXHoInm1Nena+fO1R6DJrCv1iz215rF/lpz2Fdrlvd7f3Xp3Ck/+O7FqyxfI6K5tm3bjBg+rNpj0ESDh5xhf60h7Ks1i/21ZrG/1hz21ZqlWvvL5RkAAFBANAMAQIHWF1100UXVHqIpttpi82qPQDPYX2sO+2rNYn+tWeyvNYd9tWapxv4qlcvlcsW3CgAAaxCXZwAAQAHRDAAABVr0Lefq583PJVdcnYfHPZEunTvltJOOzYB9P1HtsfiHIUO/lvHPPp/WrVsnSdZbb52MHHF9kmT0n+/ND2/4WebWz8su/XbMBecMTedOHas57gfKLSNH5fbRd+elCRPTf++P56Jzz2x879HHnszlw67LzNdez7ZbfSgXfe3MdN9g/STJ0qVv5TtX/Shjxj6QdrW1GXzEITn6swdV69f4wFjd/po+49V8+ogTUteuXeO6xx55SE485ogk9lc1LF36Vi4bdm0efezJzJv3ZjbacIOcOuTYfGyXnZI4vlqad9pfjq+W5/xvXZlHH38qixcvzjprd80xhx+SAwf2T9JCjq1yC3buNy8vf+2iy8oLFiwsP/HU+PKe+x9WfumVidUei3846fRzyreOGr3K8pdemVjeY8Ch5ceefKa8YMHC8nkXX1H+2kWXVWHCD667xz5Qvue+B8uXfu+H5Qsv/X7j8jlz5pb33P+w8p/uub+8ePGS8rBrf1w+9pQvN77/g+t/Uj7htK+W6+fNL78ycXJ5vwOPKj/w8Lhq/AofKKvbX9Omzyz3+/gB5bfeWvYfP2d/Vd7ChYvK1930i/K06TPLDQ0N5fseeKS8x4BDy9Omz3R8tUDvtL8cXy3PS69MLC9ZsrRcLpfLE/7xz/zZv7/YYo6tFnt5xqJFizPmvgdzyvFHp337uvTdfpvsudsuueOue6o9GgVG//ne7LHbzvnwDtumffu6fOH4o3PP/Q9lwcKF1R7tA2PvPXfLJ/bYNZ07dVpp+Zj7H0rvXj2z7yd2T21t2wz5/JF58eUJmThpSpLk9jvH5MRjDk+njh2y6SYb58CB/TNq9J+r8St8oKxufxWxvyqvrq5dTj7uqGzYvVtatWqVPXbbORt275bnXnjJ8dUCvdP+KmJ/VV7vTTdJ27ZtVrwolZJSKVOnz2gxx1aLjeZJU6eldevW2WTjHo3LPtR707wycVIVp+Lf/fCGn2WfTx+Z40/7asY98XSS5OWJk9On96aN62zUo3va1NRk8pTp1RqTf3hl4qSV9k1dXbv02HCDvDxxcubNfzOz3pi90vt9em+aVyZOrsaovM2gw4/L/ocem29eNixz59Ynif3VQrwxe04mT5mW3r16Or7WAG/fX//k+GpZLrvq2nys/yE59JhTsu46a+dju+zUYo6tFhvNixYtSof2dSst69ChfRYsXFSlifh3p598XH7/qxvzx9/8LAcPHJAvn3dJpk6bsWLfrdV+pXVX7Dtnmqtt4aLF6bDWWist69BhrSxcuCgLF604tt7+/j/fozq6dO6Un193VUb9+icZMXxYFixcmG98+8oksb9agGXLluX8b12ZAwbsk16bbOz4auH+fX85vlqmr535xdx3x//Ljddcnr322DVt27ZpMcdWi43murq6vPlvv/CCBQuz1r+FNNWz7dZbZK327dO2bZsMHLBPdth2q/zlkXGpq6tbJZAXLFiUtdq3X803USnt69r9h32zMO3b16V93Ypj6+3v//M9qqN9+7psvWWf1NS0zjprd83ZQ7+Qh//6RBYsXGh/Vdny5ctz/re/l5o2NTln6ClJHF8t2X/cX46vFqt169bpu/02ee31WfnN7+9oMcdWi43mTTbqkYaGhkyeOq1x2QsvT8hmvTap4lS8k1KplJTL6d2rZ158eULj8qnTZ2bpW2+l58YbVnE6kmSzXpvkhbftm0WLFmfq9Jnp3atnOnXskHXXWXul9198eUI2e9u/xqS6SqUV/11eXra/qqhcLueSK67J7Dlzc8XF56WmZsWNqBxfLdPq9te/c3y1PA0NyzN1+owWc2y12Giuq2uXvfbYNdfddHMWLVqcJ595NmMfeCT777dXtUcjyfz5b+ahRx/LkiVLs2xZQ/74p3vy+NPjs+vO/TJg30/k/gcfzRNPj8+iRYtz/U2/yF577OpMcwUtW9aQJUuWZvnyhjQsX964n/baY9e8PGFS7h77QJYsWZobfv6r9NmsV3ptsnGS5ID99s5NI27JvPlvZuKkKbn19jszaMC+Vf5t/u9b3f4a/+zzmTh5apYvX5659fNy5Q+Gp1/f7dKhw4p/DWl/Vcd3vv+jTJg0JVddekHa1dY2Lnd8tUyr21+Or5Zl9py5ufPusVm4cFEaGhry0KOP5c4xY/ORD/dtMcdWi36Mdv28+bn48qvzyGNPpHOnTvnSEPdpbinmzK3P0HMuysTJU9OqVav06rlRTjnh6Hx0px2TrLiDxg+G/yz18+Zl5359c+E5Z7hPcwVd/5Obc8PPfrXSspOOPSInH3dUHhn3ZK64+rrMfPW1bPOPe11u2L1bkpXvdVlbW5tj3Je0Ila3vzbpuVGuveHnmT13btZq3z677NQ3p598fNZdp2sS+6saZsx8LYMOPz5t27RpvEd9kpx31qn51Cf3cny1MO+0v0qtWjm+WpA5c+tzzoXfyQsvTUi5vDwbdFs/hx8yKAcNHJAkLeLYatHRDAAALUGLvTwDAABaCtEMAAAFRDMAABQQzQAAUEA0AwBAAdEMAAAF/vNjcQD4jwZ97vjMePW1wvWuu+rSzAzqSsIAAAaQSURBVJj5Wr55+bDG1zvtuP37PV6hZcuW5X9/94fc9sc/Zdr0V1MqJV26dM5mvXqm/z4fz4B9Pt647vU/uTlJssXmm+UTe+xarZEBWgTRDPABcvEVV+eOu+5ZadmChYsybfrMtKutXSma//nAlYH99xHNwAeeaAZohlG33NT487gnns4pZ56XZEVYXnTumausP+hTLeexu7PemN0YzAcN7J9TTzwmtbW1mTp9Rh545LEsWbKkyhMCtFyiGeB9MuqPf17l8oy3h/Y5Z3whf3/h5fzpnvvTsWOHnHLcUdl/v71y/U9+md/e9sfU1LTO/p/cK6eedGxqav71CODxzz6fH//iljw9/rksWLgoG26wfvbfb698/sjDUlOz+v9Znz7z1caf++24fbp06Zwk6dN70/TpvWnje2+fMUluv/Pu3H7n3UmSC885I4M+tW+WL1+e//e7P+S2O/6USVOmpVWplK222DwnDD48u+zUt/GzQ4Z+LY8/NT7du62fi849M1dde2NemTA53Tfoli+ccHT2/cTu/80/YoCKEc0AVXLdTb9I/bz5SZKFixbl4iuuzpj7H8z9Dz7auM6IW0amx4Yb5NDP7J8keejRx3LmeZdk2bJljetMnjo91910c/723Au56jsXrnZ7662zTuPPl1x+dcaMfSD9+m6Xfn23y+ab9WrW7BdddtUql3k8/tT4PPH0+bn0grPzyb32WOm9OfX1Of3sC7Nk6dIkyaQpU3PexVdk3bW7pu/22zRr2wDV4O4ZAFXSrrY2t/5ieK669IIkSblczgMPj8sPrvhmbvvVj9O+ri5JMmbsA42fuXzYdVm2bFm233arjPr1TXngzpH58qknJUnuf+ivefCRx1a7ve4brJ/ddumXJFmydGnG3PdgvnvN9Tn8+NNy+PGn5Zm//T1JVpwRv/f2xs8N7L9Pxt17e8bde3sGfWrfPPH0+MZg/uKJx+S+O/43o387Iv36bpdyuZzv/+iGLF++fKVtL168JIcdeEDuvf2WfPeS81IqlbJ8+fJc/9Ob/9t/jAAVIZoBqmTggH2y8UYbZud+/7qcYYdtt8quO/fLht27ZfPNNkmSzHx9VpJk0pRpmTp9RpLk6fHPZdDhx+dj/Q/O9390Q+Pnxz359Dtu84qLz8vgww/O2l27rLT8pVcm5ivf+FbeXLCwcO63h/m1N/48e+5/WAYcMjiPPflMkuT1WbMzacq0lT7Tpk1NTjnh6HTosFb22mO39N1u63/8Hn9PuVwu3CZAtYlmgCrp3m39JEltbdvGZRusv17jz/+8PvmtpW8lSebMrS/8znn/uNxjddrV1mboKcfnj7/5WX523fcz5PNHpnOnjkmSN+bMzfhn/164jabMUf9vc3Tu1CntamsbX6+37opLRZYsXbrKugAtkWuaAaqkdevWTVr2T106d2r8+YhDP5OzTjtplXXe6azt0qVvpVWrUmpqatK6detss+WHss2WH0qnTh1z5TXXJ1k1dovm+OWN1+RDm2+2ygylUmmlZfXz5mXJkqWN/wfh9VlvJElq27ZtjHaAlsyZZoA1xCYb90iP7t2SJL//w1154JFxWbJkaebMrc+f7/1Lhgz9WmbMXP2DV6bPmJlDBp+cEb8emYmTpuStt97K67PeyIOPjPvXNnpu1Phzp44dkiRTps3I4rfdjm7Xnfs1/vy9H96QKVOn56233srEyVPz05v/N1+/5LurbPutt5blup/8Im8uWJh77n8wTz7zbJJk+223XCWwAVoiZ5oB1hClUilfHXpKzvr6t7Jw0aIMPeeiZn/HtBmv5urrbsrV1920yns7f3iHbNmnd+Prrbfsk4f/+kSeGv9sdu9/SJLk1l8MT7++26X/Ph/PnXePzWNPPpODjh6y0vd8eIdtV/nu9nV1+c3v7siIX49sXNaqVauc/Pmjmv07AFSDM80Aa5DdP/qR3HjN5dnzY7ukc6dOadOmJt3WXy+77dIvX//KaVlv3bVX+9kNuq2Xs4eekj0/tkt6bLhB6tq1S5s2Nem50YY55ohDcuW3vrHS+l/50snp13e7rNW+bpXvuuTrZ+Urp5+cLfr0Tm3btmlfV5dePTfKQYMG5IsnHrPK+p07dcwPr7wkW2/ZJ23btEnPjXvk0gvOdrs5YI1RKvuzZQDeJ29/uMnbn6YIsKZxphkAAAqIZgAAKODyDAAAKOBMMwAAFBDNAABQQDQDAEAB0QwAAAVEMwAAFBDNAABQ4P8DdJ7/tZssjnsAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from IPython.display import HTML\n", - "\n", - "idx = 0\n", - "\n", - "fig, ax = plt.subplots(facecolor='w', figsize=(12, 7))\n", - "labels=['0', '1', '2', '3', '4', '5', '6', '7', '8','9']\n", - "print(f\"The target label is: {targets[idx]}\")\n", - "\n", - "# plt.rcParams['animation.ffmpeg_path'] = 'C:\\\\path\\\\to\\\\your\\\\ffmpeg.exe'\n", - "\n", - "# Plot spike count histogram\n", - "anim = splt.spike_count(spk_rec[:, idx].detach().cpu(), fig, ax, labels=labels, \n", - " animate=True, interpolate=1)\n", - "\n", - "HTML(anim.to_html5_video())\n", - "# anim.save(\"spike_bar.mp4\")" - ] - }, - { - "cell_type": "markdown", - "id": "-iSGTq0Q3Lcm", - "metadata": { - "id": "-iSGTq0Q3Lcm" - }, - "source": [ - "# Conclusion\n", - "If you made it this far, then congratulations - you have the patience of a monk. You should now also understand how to load neuromorphic datasets using Tonic and then train a network using snnTorch. [In the next tutorial](https://snntorch.readthedocs.io/en/latest/tutorials/index.html), we will learn more advanced techniques, such as introducing long-term temporal dynamics into our SNNs.\n" - ] - }, - { - "cell_type": "markdown", - "id": "h-K_DUnsMKnv", - "metadata": { - "id": "h-K_DUnsMKnv" - }, - "source": [ - "# Additional Resources\n", - "* [Check out the snnTorch GitHub project here.](https://github.com/jeshraghian/snntorch)\n", - "* [The Tonic GitHub project can be found here.](https://github.com/neuromorphs/tonic)\n", - "* The N-MNIST Dataset was originally published in the following paper: [Orchard, G.; Cohen, G.; Jayawant, A.; and Thakor, N. “Converting Static Image Datasets to Spiking Neuromorphic Datasets Using Saccades\", Frontiers in Neuroscience, vol.9, no.437, Oct. 2015.](https://www.frontiersin.org/articles/10.3389/fnins.2015.00437/full) \n", - "* For further information about how N-MNIST was created, please refer to [Garrick Orchard's website here.](https://www.garrickorchard.com/datasets/n-mnist)" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "include_colab_link": true, - "name": "Copy of tutorial_5_neuromorphic_datasets.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.11" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "2789fbacce8f4cc8a240daa6c6d45d15": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "37deac4746684f189a32858514a780a4": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "397436bca741424c886e7f67805eebc3": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "64ab952b23bd4406af5cc6d4f72b257e": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "83b65be63ec841288496bd9ad33dbbab": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "9f46301d16924494ada03e7496af98ee": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_e965cc586d5f4d5aaf8c435ebd4bf35c", - "IPY_MODEL_be2f2e3ca1b34106874e6b5c8cd2051e", - "IPY_MODEL_b3a18813d8684b3ab7b9d944d8e74e4f" - ], - "layout": "IPY_MODEL_397436bca741424c886e7f67805eebc3" - } - }, - "b3a18813d8684b3ab7b9d944d8e74e4f": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_64ab952b23bd4406af5cc6d4f72b257e", - "placeholder": "​", - "style": "IPY_MODEL_bfe2d8fcea404d22bbc61a69ae60c802", - "value": " 1181573120/? [00:20<00:00, 60231750.25it/s]" - } - }, - "be2f2e3ca1b34106874e6b5c8cd2051e": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_2789fbacce8f4cc8a240daa6c6d45d15", - "max": 1181572961, - "min": 0, - "orientation": "horizontal", - "style": "IPY_MODEL_c87ffeb8f6f54e1b871d129cd783de9f", - "value": 1181572961 - } - }, - "bfe2d8fcea404d22bbc61a69ae60c802": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "DescriptionStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "DescriptionStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "" - } - }, - "c87ffeb8f6f54e1b871d129cd783de9f": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "ProgressStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "ProgressStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "bar_color": null, - "description_width": "" - } - }, - "e965cc586d5f4d5aaf8c435ebd4bf35c": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "HTMLView", - "description": "", - "description_tooltip": null, - "layout": "IPY_MODEL_83b65be63ec841288496bd9ad33dbbab", - "placeholder": "​", - "style": "IPY_MODEL_37deac4746684f189a32858514a780a4", - "value": "" - } - } - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/quickstart.ipynb b/examples/quickstart.ipynb index 2abaa53b..262004a1 100644 --- a/examples/quickstart.ipynb +++ b/examples/quickstart.ipynb @@ -88,8 +88,8 @@ "outputs": [], "source": [ "batch_size = 128\n", - "data_path='/tmp/data/mnist'\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" + "data_path='/data/mnist'\n", + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")" ] }, { diff --git a/examples/tutorial_5_FCN.ipynb b/examples/tutorial_5_FCN.ipynb index a430b29b..6a38c78b 100644 --- a/examples/tutorial_5_FCN.ipynb +++ b/examples/tutorial_5_FCN.ipynb @@ -401,7 +401,7 @@ "data_path='/tmp/data/mnist'\n", "\n", "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")" ] }, { diff --git a/examples/tutorial_6_CNN.ipynb b/examples/tutorial_6_CNN.ipynb index d6f8f903..891e7f98 100644 --- a/examples/tutorial_6_CNN.ipynb +++ b/examples/tutorial_6_CNN.ipynb @@ -3,8 +3,8 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "view-in-github" + "id": "view-in-github", + "colab_type": "text" }, "source": [ "\"Open" @@ -261,10 +261,10 @@ "source": [ "# dataloader arguments\n", "batch_size = 128\n", - "data_path='/tmp/data/mnist'\n", + "data_path='/data/mnist'\n", "\n", "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")" ] }, { @@ -630,11 +630,6 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "y_VQ9es-gSO3" - }, - "outputs": [], "source": [ "optimizer = torch.optim.Adam(net.parameters(), lr=1e-2, betas=(0.9, 0.999))\n", "num_epochs = 1\n", @@ -676,7 +671,12 @@ " test_acc_hist.append(test_acc.item())\n", "\n", " counter += 1" - ] + ], + "metadata": { + "id": "y_VQ9es-gSO3" + }, + "execution_count": null, + "outputs": [] }, { "cell_type": "markdown", @@ -816,9 +816,9 @@ "metadata": { "accelerator": "GPU", "colab": { - "include_colab_link": true, "name": "tutorial_6_CNN.ipynb", - "provenance": [] + "provenance": [], + "include_colab_link": true }, "kernelspec": { "display_name": "Python 3", diff --git a/examples/tutorial_7_neuromorphic_datasets.ipynb b/examples/tutorial_7_neuromorphic_datasets.ipynb index 8a7f894a..0d0c38fe 100644 --- a/examples/tutorial_7_neuromorphic_datasets.ipynb +++ b/examples/tutorial_7_neuromorphic_datasets.ipynb @@ -3,8 +3,8 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "view-in-github" + "id": "view-in-github", + "colab_type": "text" }, "source": [ "\"Open" @@ -386,7 +386,8 @@ }, "outputs": [], "source": [ - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")\n", + "\n", "# neuron and simulation parameters\n", "spike_grad = surrogate.atan()\n", "beta = 0.5\n", @@ -640,11 +641,10 @@ "metadata": { "accelerator": "GPU", "colab": { - "include_colab_link": true, "name": "Copy of tutorial_5_neuromorphic_datasets.ipynb", - "provenance": [] + "provenance": [], + "include_colab_link": true }, - "gpuClass": "standard", "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", @@ -661,7 +661,8 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.11" - } + }, + "gpuClass": "standard" }, "nbformat": 4, "nbformat_minor": 5 diff --git a/examples/tutorial_regression_1.ipynb b/examples/tutorial_regression_1.ipynb index cd19fd7e..960256a3 100644 --- a/examples/tutorial_regression_1.ipynb +++ b/examples/tutorial_regression_1.ipynb @@ -387,9 +387,8 @@ "outputs": [], "source": [ "hidden = 128\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", - "\n", - "model = Net(timesteps=num_steps, hidden=hidden).to(device)" + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")\n", + "model = Net(timesteps=num_steps, hidden=hidden).to(device)\n" ] }, { diff --git a/examples/tutorial_regression_2.ipynb b/examples/tutorial_regression_2.ipynb index d0f1be46..fa84e58d 100644 --- a/examples/tutorial_regression_2.ipynb +++ b/examples/tutorial_regression_2.ipynb @@ -407,8 +407,7 @@ "outputs": [], "source": [ "hidden = 128\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", - "\n", + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")\n", "model = Net(timesteps=num_steps, hidden=hidden, beta=0.9).to(device)" ] }, diff --git a/examples/tutorial_sae.ipynb b/examples/tutorial_sae.ipynb index e7e4f9d6..b8ed467d 100755 --- a/examples/tutorial_sae.ipynb +++ b/examples/tutorial_sae.ipynb @@ -199,10 +199,10 @@ "source": [ "# dataloader arguments\n", "batch_size = 250\n", - "data_path='/tmp/data/mnist'\n", + "data_path='/data/mnist'\n", "\n", "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")" + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")\n" ] }, { @@ -762,7 +762,7 @@ "\n", "#setup GPU\n", "dtype = torch.float\n", - "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device("mps") if torch.backends.mps.is_available() else torch.device(\"cpu\")\n", + "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")\n", "\n", "# neuron and simulation parameters\n", "spike_grad = surrogate.atan(alpha=2.0)# alternate surrogate gradient fast_sigmoid(slope=25) \n",