Lab 3: Neuron Basics using Nengo#

The objectives of this lab are to:

  1. Build Nengo neurons to best represent input values and functions by tuning thresholds, encoders, numbers of neurons, synapses, radii, and simulation time.

  2. Implement neuron transformations using 1D and multidimensional neurons.

Note: your Neuron Basics tutorials will be extremely helpful in completing this lab. Be sure to reference those as you go.


Grading Specifications#

◻ Filename changed to reflect last name of person submitting assignment
◻ Code runs error free
◻ All lines of code edited/changed contain comments with thought process behind methodology - help me understand what you’re doing!
◻ Input node correctly implemented.
◻ Use np.piecewise to plot your input outside of Nengo.
◻ Choose neuron parameters of a two neuron ensemble to best represent your input piecewise function.
◻ Choose neuron parameters of a 100 neuron ensemble to best represent the reLu function performed on your input piecewise function.
◻ Correctly use neuron transformations with a 100 neuron ensemble to implement the reLu function performed on your input piecewise function.
◻ Correctly use neuron transformations with a 100 neuron ensemble to make the entire Piecewise input function negative (i.e. same magnitudes, all negative).
◻ Correctly utilize a multi-dimensional neuron consisting of 1000 neurons to build the output function defined in the instructions.
◻ Jupyter notebook is saved such that outputs appear upon opening file (and therefore on gradescope)
◻ (Recommended) Fully understand your results by analyzing and discussing outputs with classmates. Doing so will help you on your quizzes!
◻ (Recommended) All markdown and resources read thoroughly

Set up#

Ensure you are using your 495 Virtual Environment before you begin!

Import required libraries:

import numpy as np
import matplotlib.pyplot as plt
from nengo.dists import Uniform

import nengo
from nengo.processes import Piecewise

Define the following piecewise function within your Nengo model using a Nengo node: Piecewise({0: -.4, 1: -.2, 2: .2, 3: .4,  4: -.1})

model = nengo.Network(label="Lab1")

with model:
    # 
    input_node = ???

Neuron Representation#

Plot your function using np.piecewise to ensure you have intuition of what your outputs should be outside of your Nengo model.

def piecewise_function(x):
    return np.piecewise(x, 
                        [(x >= 0) & (x < 1),
                         ???, 
                         (x >= 2) & (x < 3), 
                         ???, 
                         (x >= 4)], 
                        [lambda x: ???, 
                         lambda x: -.2, 
                         lambda x: ???,
                         lambda x: .4,
                         lambda x: ???])

x = np.linspace(0,5,500)
input = piecewise_function(x)
plt.figure()
plt.plot(x,input, label="Input")
plt.legend(loc="best")
plt.ylim(-.6, .6)

Using a neuron ensemble consisting of only two neurons, best represent your Piecewise function. To do so, you must:

  • Create an ensemble with the appropriate intercepts (thresholds), encoders, and radii

    • Choose your maximum firing rate to be 100Hz.

    • To select the right parameters, consider plotting or drawing your tuning curves

    • Definitely take a look at this Nengo example here

    • By combining your knowledge of tuning curves and the above example, note that a non-zero intercept works better. Make sure you understand why!

  • Connect your input to your 2-neuron ensemble

  • Probe the ensemble with an appropriate synapse value

  • Run the simulator long enough to see the entire Piecewise function

  • Plot your results

Note: the results will not be perfect, but with a maximum firing rate of 100Hz, it’s pretty good! Think through why that might be the case.

with model:
    #--CREATE ENSEMBLE--
    input_ensemble = nengo.Ensemble(n_neurons=???, dimensions=???, 
                                    max_rates=Uniform(???, ???),
                                    # 
                                    intercepts=[???, ???],
                                    # 
                                    encoders=[[???],[???]],
                                    radius=???)

    #--CONNECT INPUT TO ENSEMBLE--
    inp_conn = nengo.Connection(???, ???)

    #--PROBE ENSEMBLE--
    input_probe= nengo.Probe(input_ensemble, synapse=???)

#--RUN SIMULATOR--
with nengo.Simulator(model) as sim:
    sim.run(???)

#--PLOT RESULTS OUTSIDE OF MODEL AND SIMULATOR--
plt.figure()
plt.plot(sim.trange(), sim.data[???], label="Decoded Estimate of Input")
plt.plot(x,input, label="Input")
plt.legend()
plt.ylim(-.6, .6)
plt.xlabel("time [s]")
plt.title("Neural Representation of Input")

Using a neuron ensemble consisting of 100 neurons, implement the reLu function on your Piecewise input by choosing your ensemble parameters (i.e. not using transformations - that’s next). To do so, you must:

  • Create an ensemble with the appropriate intercepts (threshold), encoders, and radii.

    • Do not choose your maximimum firing rates. Allow Nengo to do that for you. Recall what that means your firing rates will be per Tutorial3a!

    • Choose the same intercepts and encoders for all 100 neurons.

    • Similar to the 2-neuron ensemble, a non-zero intercept works the best. However, there’s a point at which it performs poorly. Choose the correct non-zero intercept. Be sure you understand why it breaks when you choose a smaller value.

  • Connect your original input node to your 100-neuron ensemble

  • Probe the ensemble with an appropriate synapse value

  • Run the simulator long enough to see the entire Piecewise function

  • Plot your results

Note: Consider why having only one maximum firing rate would perform worse than having a lot of different rates. You can play with your max_rates to test out your theory. Consider why using 100 neurons works better than just 1, even when they all have the same intercepts and encoders. You can play with the variable num_neurons to test your theory. However, be sure to save your file so that it loads to Gradescope with your plot for 100 neurons and using Nengo-determined firing rates

with model:
    #--CREATE ENSEMBLE--
    num_neurons=???
    # Called this relu1 because you will do reLu a second time using transformations
    relu1_ensemble = nengo.Ensemble(n_neurons=num_neurons, dimensions=???,
                                    #max_rates=Uniform(100, 100),
                                    # 
                                    intercepts=Uniform(???,???),
                                    # 
                                    encoders=np.tile([[???]], (num_neurons, 1)),
                                    radius=???)

    #--CONNECT INPUT TO ENSEMBLE--
    relu1_conn = nengo.Connection(???, ???)

    #--PROBE ENSEMBLE--
    relu1_probe= nengo.Probe(???, synapse=???)

#--RUN SIMULATOR--
with nengo.Simulator(model) as sim:
    sim.run(???)

#--PLOT RESULTS OUTSIDE OF MODEL AND SIMULATOR--
plt.figure()
plt.plot(sim.trange(), sim.data[???], label="Decoded Estimate of relu1(Input)")
plt.plot(x,input, label="Input")
plt.legend()
plt.ylim(-.6, .6)
plt.xlabel("time [s]")
plt.title("Neural Representation of relu1(Input) using Neuron Parameters")

Neuron Transformation#

Using a neuron ensemble consisting of 100 neurons, implement the reLu function on your Piecewise input using neuron transformations. To do so, you must:

  • Create an ensemble with the appropriate radii

    • Do not manually choose all 100 intercepts and encoders - we are now adequately spanning the space by allowing Nengo to randomly choose our parameters

  • Build a function that computes the reLu function outside of your model

  • Connect your original input node to your 100-neuron ensemble with the reLu function

  • Probe the ensemble with an appropriate synapse value

  • Run the simulator long enough to see the entire Piecewise function

  • Plot your results

Note: This one should look pretty perfect. Think through why that’s the case.

def relu(x):
    ???

with model:
    #--CREATE ENSEMBLE--
    relu_ensemble = nengo.Ensemble(n_neurons=???, dimensions=???, 
                                   radius=???)

    #--CONNECT INPUT TO ENSEMBLE--
    relu_conn = nengo.Connection(???, ???, function=???)

    #--PROBE ENSEMBLE--
    relu_probe= nengo.Probe(???, synapse=???)

#--RUN SIMULATOR--
with nengo.Simulator(model) as sim:
    sim.run(???)

#--PLOT RESULTS OUTSIDE OF MODEL AND SIMULATOR--
plt.figure()
plt.plot(sim.trange(), sim.data[???], label="Decoded Estimate of reLu(Input)")
plt.plot(x,input, label="Input")
plt.legend()
plt.ylim(-.6, .6)
plt.xlabel("time [s]")
plt.title("Neural Representation of reLu(Input) using Neuron Transformation")

Using a neuron ensemble consisting of 100 neurons, implement a function that makes all values of your Piecewise Input negative using neuron transformations (i.e. the function should maintain the same magnitudes of your Piecewise input but all should be less than 0). To do so, you must:

  • Create an ensemble with the appropriate radii

    • Do not manually choose all 100 intercepts and encoders - we are now adequately spanning the space by allowing Nengo to randomly choose our parameters

  • Build a function that computes the correct function outside of your model

  • Connect your original input node to your 100-neuron ensemble with the reLu function

  • Probe the ensemble with an appropriate synapse value

  • Run the simulator long enough to see the entire Piecewise function

  • Plot your results

def negvals(x):
    ???

with model:
    #--CREATE ENSEMBLE--
    negvals_ensemble = ???

    #--CONNECT INPUT TO ENSEMBLE--
    negvals_conn = ???

    #--PROBE ENSEMBLE--
    negvals_probe= ???

#--RUN SIMULATOR--
with nengo.Simulator(model) as sim:
    sim.run(???)

#--PLOT RESULTS OUTSIDE OF MODEL AND SIMULATOR--
plt.figure()
plt.plot(sim.trange(), sim.data[???], label="Decoded Estimate of Negative Input")
plt.plot(x,input, label="Input")
plt.legend()
plt.ylim(-.6, .6)
plt.xlabel("time [s]")
plt.title("Neural Representation of Negative Input")

Multi-dimensional Neurons#

Using a multidimensional neuron ensemble consisting of 1000 neurons and an output node, implement a function that

  • Adds 1 to your reLu outputs

  • Multiplies your negative values output by 2

  • Squares your original piecewise input (you can use the node for better accuracy)

  • Then takes the norm of the three new values using neuron transformations

There are a few ways to do this. Regardless of which way you choose, you most definitely must:

  • Create your multidimensional ensemble with the appropriate number of dimensions and radii

    • Do not manually choose your intercepts and encoders - we are now adequately spanning the space by allowing Nengo to randomly choose our parameters

  • Create your output node.

    • This only requires the parameter size_in=1

  • Build a function (or functions) that computes the correct computations outside of your model

  • Make the right connections with the right functions

  • Probe the 1-dimensional ensemble with an appropriate synapse value

  • Run the simulator long enough to see the entire output

  • Plot your results

If you’re not sure if your output is correct, you can hand compute each step to check your work - it shouldn’t take long and it’s well worth it to be sure you’ve performed your transformations correctly!

def ???

with model:
    combine_neurons = ???
    output_node = ???

    # --- CONNECTIONS HERE ---

    #--PROBE ENSEMBLE--
    output_probe= ???

#--RUN SIMULATOR--
with nengo.Simulator(model) as sim:
    sim.run(???)

#--PLOT RESULTS OUTSIDE OF MODEL AND SIMULATOR--
plt.figure()
plt.plot(sim.trange(), sim.data[???], label="Decoded Estimate of Output")
plt.plot(x,input, label="Input")
plt.legend()
#plt.ylim(-.6, .6)
plt.xlabel("time [s]")
plt.title("Neural Representation of Output")