This example will show you how to multiply two values. The model architecture can be thought of as a combination of the combining demo and the squaring demo. Essentially, we project both inputs independently into a 2D space, and then decode a nonlinear transformation of that space (the product of the first and second vector elements).
The model has four ensembles: two input ensembles ('A' and 'B'), a 2D combined ensemble ('Combined'), and an output ensemble ('D').
In [ ]:
# Create the model object
import nengo
model = nengo.Network(label='Multiplication')
with model:
# Create 4 ensembles of leaky integrate-and-fire neurons
A = nengo.Ensemble(nengo.LIF(100), dimensions=1, radius=10)
B = nengo.Ensemble(nengo.LIF(100), dimensions=1, radius=10)
combined = nengo.Ensemble(nengo.LIF(224), dimensions=2, radius=15) # This radius is ~sqrt(10^2+10^2)
prod = nengo.Ensemble(nengo.LIF(100), dimensions=1, radius=20)
# This next two lines make all of the encoders in the Combined population point at the
# corners of the cube. This improves the quality of the computation.
# Note the number of neurons is assumed to be divisible by 4
import numpy as np
# Comment out the line below for 'normal' encoders
combined.encoders = np.tile([[1,1],[-1,1],[1,-1],[-1,-1]], (combined.n_neurons // 4, 1))
We will use two varying scalar values for the two input signals that drive activity in ensembles A and B.
In [ ]:
from nengo.utils.functions import piecewise
with model:
# Create a piecewise step function for input
inputA = nengo.Node(piecewise({0: 0, 2.5: 10, 4: -10}))
inputB = nengo.Node(piecewise({0: 10, 1.5: 2, 3: 0, 4.5: 2}))
correct_ans = piecewise({0: 0, 1.5: 0, 2.5: 20, 3: 0, 4: 0, 4.5: -20})
In [ ]:
with model:
# Connect the input nodes to the appropriate ensembles
nengo.Connection(inputA, A)
nengo.Connection(inputB, B)
# Connect input ensembles A and B to the 2D combined ensemble
nengo.Connection(A, combined, transform=[[1], [0]])
nengo.Connection(B, combined, transform=[[0], [1]])
# Define a function that computes the multiplication of two inputs
def product(x):
return x[0] * x[1]
# Connect the combined ensemble to the output ensemble D
nengo.Connection(combined, prod, function=product)
Collect output data from each ensemble and input.
In [ ]:
with model:
inputA_probe = nengo.Probe(inputA, 'output')
inputB_probe = nengo.Probe(inputB, 'output')
A_probe = nengo.Probe(A, 'decoded_output', synapse=0.01)
B_probe = nengo.Probe(B, 'decoded_output', synapse=0.01)
combined_probe = nengo.Probe(combined, 'decoded_output', synapse=0.01)
prod_probe = nengo.Probe(prod, 'decoded_output', synapse=0.01)
In [ ]:
# Create the simulator
sim = nengo.Simulator(model)
# Run it for 5 seconds
sim.run(5)
To check the performance of the model, we can plot the input signals and decoded ensemble values.
In [ ]:
import matplotlib.pyplot as plt
# Plot the input signals and decoded ensemble values
plt.plot(sim.trange(), sim.data[A_probe], label="Decoded A")
plt.plot(sim.trange(), sim.data[B_probe], label="Decoded B")
plt.plot(sim.trange(), sim.data[prod_probe], label="Decoded product")
plt.plot(sim.trange(), [correct_ans(t) for t in sim.trange()], c='k', label="Actual product")
plt.legend(loc='best')
plt.ylim(-25, 25)
The input signals we chose make it obvious when things are working, as the inputs are zero often (so the product should be). When choosing encoders randomly around the circle (the default in Nengo), you may see more unwanted interactions between the inputs. To see this, comment the above code that sets the encoders to the corners of the cube (in Step 1 where it says #Comment out the line below for 'normal' encoders).