[nengo-user] Answer from and to the Nengo group
Peer Ueberholz
peer at ueberholz.de
Wed Apr 6 07:19:03 EDT 2016
Hi Terry,
thank you very much for all your explanations, and please apologize that
it took so long to respond. Your answer already helps a lot to
understand Nengo a bit better, but please, let me ask some more
questions, because I still don't know much about Nengo.
First I would like to ask two questions concerning your example: Why did
you choose a 2 dimensional ensemble instead of a 1 dimensional one for
just one angle?
And secondly, you write in your example after the node "angle_to_pt" was
connected to the ensemble "optic_tectum": "You now have neurons with
tuning curves in different directions." If I look at the tuning curves,
they look like a 2 dimensional version of the curves in the Nengo 1.4
documentation, chapter 1.1.4 and not like the ones in figure 6 in the
Fischer and Pena paper. Therefore, why do I have tuning curves in
different directions now?
Please, let my say a few more words about the problem I want to
simulate. The work of Fischer and Pena from 2011 will only be one part
of the simulation. I want to simulate the auditory system starting with
two input nodes for the left and right ear with a sinus function
including a phase difference as well as different amplitudes of the two
input signals. The input nodes for the two ears could look like
# input of right and left ear
input_left = nengo.Node(lambda t: np.sin(frequency*2*math.pi*t))
input_right = nengo.Node(lambda t:
0.8*np.sin(frequency*2*math.pi*t-phase_factor))
The simulation should process these input signals and should end in the
optic tectum as described for example by Masakazu Konishi in Scientific
American in 1993. To do this, I want to use a simple model. First, I
have to compute the interaural time difference of this input signals.
For this purpose I want to use the delay class of the Nengo examples. I
want to connect the input nodes to an array of delay nodes with
different delays and then the delay nodes to an array of ensembles. If
the shift due to the delay and the phase factor add up to half the
wavelength, the ensemble will only have a small activity, if they add up
to a multiple of the wavelength, the ensemble will have a large
activity. In this way I can get the phase or the interaural time difference.
Here an simple example with 2 delay nodes and one ensemble
# delay nodes (currently I am using 40 delay nodes)
delay_1 = Delay(1, timesteps=1)
delay_2 = Delay(1, timesteps=3)
delaynode_l1 = nengo.Node(output=delay_1.step)
delaynode_r1 = nengo.Node(output=delay_2.step)
# connect input to delay nodes
nengo.Connection(input_left, delaynode_l1)
nengo.Connection(input_right, delaynode_r1)
# connect the output of the delay nodes to an ensembles (currently I
also have 40 ensembles)
ln = nengo.Ensemble(500, dimensions=1,radius=3)
nengo.Connection(delaynode_l1, ln)
nengo.Connection(delaynode_r1, ln)
In the end I will have the activities of 40 ensembles. The ensemble with
the largest activity will give an approximation for the phase and
therefor also for the horizontal angle the sound comes from, but it
would be better to compute the average over all ensembles of the
normalized activity times the angles each ensemble represents.
Therefore I think I can't use the ensemble of your example and I have to
compute the average as in your weighted average approach example.
I hope you know what I mean, because my English is not good and
therefore it is difficult for me to explain, what I want to do.
I don't know if the way I try to compute the interaural time difference
and the angle is a good way to do it. Do you have any idea how improve
this or how to avoid the division in this case?
One more problem is that I also want is to compute the interaural level
difference for the vertical angle of the direction the sound comes from.
To get this difference I have to divide the amplitude values. Here I
also have the problem of a division. Could it be that the problem with a
division is the fact that in principle the denominator could be zero and
that then a linear decoding is not working very well?
In Nengo one can restrict the values of an ensemble with the radius
parameter. Is it possible to restrict the values to a positive interval
to avoid the zero and would this give better results?
Thank you very much for your help and your patience with me.
Best regards,
Peer
Am 11.02.2016 um 23:47 schrieb Terry Stewart:
> Hello Peer,
>
> Thank you for the context! That helps a lot. And, it turns out that
> with a couple slightly counter-intuitive tweaks, this sort of model is
> quite nicely suited for Nengo.
>
> The main thing we first need is to make tuning curves that look like
> Figure 6a of (Fischer and Pena, 2011). These neurons have preferred
> directions that range between -100 and +100 degrees. Preferred
> directions are called "encoders" in Nengo/NEF. However, because Nengo
> allows you to generalize the idea of preferred directions up to
> hundreds of dimensions, we need to be explicit about exactly how we
> want to represent an angle.
>
> The easiest way is to think of the group of neurons (the optic tectum)
> as representing a two-dimensional space, and each neuron has some
> preferred direction in that space. By default, Nengo will randomly
> distribute the neuron's encoders around that 2-dimensional space,
> although we can control that if we want to (e.g. we may want to
> distribute neurons more densely in the middle of the space, for example).
>
> So, the code for making the optic_tectum is:
>
> optic_tectum = nengo.Ensemble(n_neurons=500, dimensions=2)
>
> Now, however, we want to provide input to that set of neurons. We
> want that input to be an angle, and we want that angle to stimulate
> the neurons in a consistent way (i.e. when the input is an angle of 0,
> the neurons with preferred direction vectors near 0 should be
> stimulated). We can do this with one Node for the input angle, and
> one Node to do the conversion into the represented space:
>
> stim_angle = nengo.Node([0])
> import numpy as np
> def convert_angle(t, x):
> return np.sin(x[0]), np.cos(x[0])
> angle_to_pt = nengo.Node(convert_angle, size_in=1)
> nengo.Connection(stim_angle, angle_to_pt, synapse=None)
>
> Now we connect that input into the optic_tectum
>
> nengo.Connection(angle_to_pt, optic_tectum)
>
> You now have neurons with tuning curves in different directions. If
> you plot the spikes from the optic_tectum as you change the input
> stim_angle, you should see this behaviour. Also, it's important to
> note that these neurons in Nengo have much more variety in their
> tuning curves (heights and widths) than the perfectly regular tuning
> curves done in Figure 6a. We tend to keep a large degree of
> variability in our neurons. You can control the width with the
> intercepts parameter, if you're interested in doing that.
>
> So all of that above was just to get the stimulus and neuron
> parameters set up right to give the sorts of tuning curves we want in
> the optic tectum. Now we come to the part where we want to decode out
> from these tuning curves what the currently represented angle is. In
> (Fischer and Pena, 2011), they do this: "The population vector is
> obtained by averaging the preferred direction vectors of neurons in
> the population, weighted by the firing rates of the neurons". This is
> one way of decoding neural activity, but it's not the only way.
>
> In Nengo, when you ask it to compute a function, it finds the optimal
> way of weighting the outputs of neural activity to approximate that
> function. Furthermore, it does this with a fixed set of linear
> weights -- i.e. it does not require that division step in the weighted
> average. So let's try it the standard Nengo way, and then compare
> that to the weighted average approach.
>
> To do it the standard Nengo way, we define a function that goes from
> the represented 2-D space and decodes out the angle:
>
> decoded_angle = nengo.Node(None, size_in=1) # a place to store
> the result
> # a function to map from 2-d space to an angle
> def decode_angle(x):
> return np.arctan2(x[0], x[1])
> # make the connection
> nengo.Connection(optic_tectum, decoded_angle, function=decode_angle)
>
> This tells Nengo to find the optimal set of connection weights from
> the neurons to decode out the angle. That is, it finds the vector d
> such that sum(a_i * d_i) best approximates the input angle.
>
> If you try running this, it works okay, but we we can make it do
> better. In particular, right now Nengo is trying to approximate the
> function across the whole 2-D space, but we only need it to be good at
> a particular range of points. If we tell Nengo to just focus on those
> points, it gets much better:
>
> decoded_angle = nengo.Node(None, size_in=1)
> def decode_angle(x):
> return np.arctan2(x[0], x[1])
>
> # define the set of points
> def make_pt():
> theta = np.random.uniform(-2, 2)
> return [np.sin(theta), np.cos(theta)]
> pts = [make_pt() for i in range(1000)]
> nengo.Connection(optic_tectum, decoded_angle,
> function=decode_angle, eval_points=pts)
>
> If you run this and plot the decoded_angle you'll see it very closely
> follows the stim_angle.
>
> However, it'd be good to also do the weighted average approach, so we
> can compare the two. Doing that requires us to to know the angles for
> all the neurons, and do a weighted average of those angles (weighted
> by activity). To do that, we explicitly define the angles for the
> neurons, and then do the math in a Node:
>
> N = 500
>
> def make_pt():
> theta = np.random.uniform(-2, 2)
> return [np.sin(theta), np.cos(theta)]
> # generate random preferred direction vectors
> encoders = np.array([make_pt() for i in range(N)])
> optic_tectum = nengo.Ensemble(n_neurons=N, dimensions=2,
> encoders=encoders)
>
> # compute the angle for each preferred direction vector
> angles = np.arctan2(encoders[:,0], encoders[:,1])
>
> # compute the weighted average
> def weighted_average(t, a):
> total = np.sum(a)
> if total == 0:
> return 0
> return np.sum(a*angles) / total
> computed = nengo.Node(weighted_average, size_in=N)
> nengo.Connection(optic_tectum.neurons, computed, synapse=None)
>
> Now we can plot "computed" (the approach used in (Fischer and Pena,
> 2011)) and compare it to "decoded_angle" (the default approach used in
> Nengo). In this case, the Nengo approach is more accurate, and it
> doesn't require any division!
>
> Here's a script that should let you directly compare the two approaches:
>
> -------------------
> import nengo
> import numpy as np
>
> N = 500 # the number of neurons
>
> model = nengo.Network()
> with model:
> stim_angle = nengo.Node([0]) # the input angle
> # convert the angle into a 2-D space
> def convert_angle(t, x):
> return np.sin(x[0]), np.cos(x[0])
> angle_to_pt = nengo.Node(convert_angle, size_in=1)
> nengo.Connection(stim_angle, angle_to_pt, synapse=None)
> # make a point in 2-D space that is at random angle
> def make_pt():
> theta = np.random.uniform(-2, 2)
> return [np.sin(theta), np.cos(theta)]
> # the preferred direction vectors for the neurons
> encoders = np.array([make_pt() for i in range(N)])
> # create the group of neurons
> optic_tectum = nengo.Ensemble(n_neurons=N, dimensions=2,
> encoders=encoders)
> nengo.Connection(angle_to_pt, optic_tectum)
> ### Standard Nengo/NEF Approach
> # decode out the angle in the optimal Nengo/NEF approach
> decoded_angle = nengo.Node(None, size_in=1)
> # function that the neural connections should approximate
> def decode_angle(x):
> return np.arctan2(x[0], x[1])
> # define the set of values over which the approximate should be good
> pts = [make_pt() for i in range(1000)]
> nengo.Connection(optic_tectum, decoded_angle,
> function=decode_angle, eval_points=pts)
>
> ### Weighted average approach
>
> # determine the angles for each neuron
> angles = np.arctan2(encoders[:,0], encoders[:,1])
> # compute the weighted sum
> def weighted_average(t, a):
> total = np.sum(a)
> if total == 0:
> return 0
> return np.sum(a*angles) / total
> computed = nengo.Node(weighted_average, size_in=N)
> nengo.Connection(optic_tectum.neurons, computed, synapse=None)
> ---------
>
> So, the main differences between the Nengo/NEF approach rather than
> the weighted average approach are:
> - The Nengo/NEF approach gives a more accurate result
> - The Nengo/NEF approach handles variability in the tuning curves
> - The Nengo/NEF approach does not require division (which is
> complicated to biologically justify)
>
> In any case, while it's certainly possible to do the weighted average
> approach (using that "computed" Node defined above) in Nengo, we tend
> not to. But it'd be interesting to do a more rigorous direct
> comparison in this case.
>
> Notice also that, right now, the neurons are being optimized to just
> figure out what the input angle is. If you also want it to take into
> account some sort of bayesian prior, then all you have to do is put
> that into the decode_angle function. This lets you implement a wide
> variety of possible priors.
>
> Does that help? What I've presented here is definitely a very
> different way of thinking about things than is taken in (Fischer and
> Pena, 2011). But hopefully I've shown a) how to implement their
> approach in Nengo, and b) that there are other ways of decoding
> information that are a little bit more flexible.
>
> Let me know if that helps, and thank you again for the question!
>
> Terry
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://artsservices.uwaterloo.ca/pipermail/nengo-user/attachments/20160406/122b12e3/attachment-0002.html>
More information about the nengo-user
mailing list