Readings: Serial Working Memory; Associative Memory
In neural network research, attractor networks (networks with dynamical attractors) have long been thought relevant for various behaviours
The neural integrator is an example of an attractor network
The neural integrator can be thought of as a line attractor as in a)
Note: The hill and ball analogy doesn't work anymore.
This is an oscillator. Technically a nonlinear one, since it should be stable (the Simple Harmonic Oscillator is linear). Let's build a stable oscillator.
In [42]:
%pylab inline
import nengo
from nengo.utils.ensemble import response_curves
from nengo.dists import Uniform
model = nengo.Network('Oscillator')
freq = .25
scale = 1.1
N=300
with model:
stim = nengo.Node(lambda t: [.5,.5] if .1<t<.12 else [0,0])
inhib = nengo.Node(lambda t: [-1]*N if .8<t<.82 else [0]*N)
osc = nengo.Ensemble(N, dimensions=2, intercepts=Uniform(.3,1))
def feedback(x):
return scale*x[0]+freq*x[1], -freq*x[0]+scale*x[1]
nengo.Connection(osc, osc, function=feedback)
nengo.Connection(inhib, osc.neurons)
nengo.Connection(stim, osc)
osc_p = nengo.Probe(osc, synapse=.01)
sim = nengo.Simulator(model)
sim.run(1)
x, A = response_curves(osc, sim)
figure(figsize=(4,2))
plot(x, A)
xlabel('x')
ylabel('firing rate (Hz)')
figure(figsize=(12,4))
subplot(1,2,1)
plot(sim.trange(), sim.data[osc_p]);
xlabel('Time (s)')
ylabel('State value')
subplot(1,2,2)
plot(sim.data[osc_p][:,0],sim.data[osc_p][:,1])
xlabel('$x_0$')
ylabel('$x_1$');
In [31]:
#A 1D integrator with a few hundred neurons works great
import nengo
from nengo.utils.functions import piecewise
model = nengo.Network(label='1D Line Attractor', seed=5)
N = 300
tau = 0.01
with model:
stim = nengo.Node(piecewise({.3:[1], .5:[0] }))
neurons = nengo.Ensemble(N, dimensions=1)
nengo.Connection(stim, neurons, transform=tau, synapse=tau)
nengo.Connection(neurons, neurons, synapse=tau)
stim_p = nengo.Probe(stim)
neurons_p = nengo.Probe(neurons, synapse=.01)
sim = nengo.Simulator(model)
sim.run(4)
t=sim.trange()
plot(t, sim.data[stim_p], label = "stim")
plot(t, sim.data[neurons_p], label = "position")
legend(loc="best");
In [32]:
#Need lots of neurons to get reasonable performance in higher D
import nengo
from nengo.utils.functions import piecewise
model = nengo.Network(label='2D Plane Attractor', seed=4)
N = 2000
tau = 0.01
with model:
stim = nengo.Node(piecewise({.3:[1, -1], .5:[0, 0] }))
neurons = nengo.Ensemble(N, dimensions=2)
nengo.Connection(stim, neurons, transform=tau, synapse=tau)
nengo.Connection(neurons, neurons, synapse=tau)
stim_p = nengo.Probe(stim)
neurons_p = nengo.Probe(neurons, synapse=.01)
sim = nengo.Simulator(model)
sim.run(4)
t=sim.trange()
plot(t, sim.data[stim_p], label = "stim")
plot(t, sim.data[neurons_p], label = "position")
legend(loc="best");
In [33]:
#Note that the representation saturates at the radius
with model:
stim.output = piecewise({.2:[1, -1], 1.2:[0, 0] })
sim = nengo.Simulator(model)
sim.run(4)
t=sim.trange()
plot(t, sim.data[stim_p], label = "stim")
plot(t, sim.data[neurons_p], label = "position");
In [34]:
model = nengo.Network(label='Ensemble Array', seed=123)
N = 300
tau = 0.01
with model:
stim = nengo.Node(piecewise({.3:[1, -1], .5:[0, 0] }))
neurons = nengo.networks.EnsembleArray(N, n_ensembles=2)
nengo.Connection(stim, neurons.input, transform=tau, synapse=tau)
nengo.Connection(neurons.output, neurons.input, synapse=tau)
stim_p = nengo.Probe(stim)
neurons_p = nengo.Probe(neurons.output, synapse=.01)
sim = nengo.Simulator(model)
sim.run(4)
t=sim.trange()
plot(t, sim.data[stim_p], label = "stim")
plot(t, sim.data[neurons_p], label = "position");
In [17]:
from nengo_gui.ipython import IPythonViz
IPythonViz(model, "ensemble_array.py.cfg")
In [35]:
#Note that the representation saturates at the radius
with model:
stim.output = piecewise({.2:[1, -1], 1.2:[0, 0] })
sim = nengo.Simulator(model)
sim.run(4)
t=sim.trange()
plot(t, sim.data[stim_p], label = "stim")
plot(t, sim.data[neurons_p], label = "position");
In [36]:
import nengo
from nengo import spa
def color_input(t):
if t < 0.15:
return 'BLUE'
elif 1.0 < t < 1.15:
return 'GREEN'
elif 1.7 < t < 1.85:
return 'RED'
else:
return '0'
model = spa.SPA(label="HighD Working Memory", seed=5)
dimensions = 32
with model:
model.color_in = spa.Buffer(dimensions=dimensions)
model.mem = spa.Memory(dimensions=dimensions, subdimensions=4,
synapse=0.1, neurons_per_dimension=50)
# Connect the buffers
cortical_actions = spa.Actions(
'mem = color_in'
)
model.cortical = spa.Cortical(cortical_actions)
model.inp = spa.Input(color_in=color_input)
model.config[nengo.Probe].synapse = nengo.Lowpass(0.03)
color_in = nengo.Probe(model.color_in.state.output)
mem = nengo.Probe(model.mem.state.output)
sim = nengo.Simulator(model)
sim.run(3.)
plt.figure(figsize=(10, 10))
vocab = model.get_default_vocab(dimensions)
plt.subplot(2, 1, 1)
plt.plot(sim.trange(), model.similarity(sim.data, color_in))
plt.legend(model.get_output_vocab('color_in').keys, fontsize='x-small')
plt.ylabel("color")
plt.subplot(2, 1, 2)
plt.plot(sim.trange(), model.similarity(sim.data, mem))
plt.legend(fontsize='x-small')
plt.legend(model.get_output_vocab('color_in').keys, fontsize='x-small')
plt.ylabel("memory")
plt.xlabel("time [s]");
In [19]:
from nengo_gui.ipython import IPythonViz
IPythonViz(model, "simple_spa_wm.py.cfg")
In [29]:
model.all_ensembles
model.all_ensembles[2].n_neurons
Out[29]:
If you recalled things from this memory, you'd probably have a 'recency effect'
This is seen in human memory, but so is primacy
In [67]:
import nengo
from nengo import spa
def color_input(t):
if t < 0.15:
return 'BLUE'
elif 1.0 < t < 1.15:
return 'GREEN'
elif 1.7 < t < 1.85:
return 'RED'
else:
return '0'
model = spa.SPA(label="HighD Working Memory", seed=5)
dimensions = 32
with model:
model.color_in = spa.Buffer(dimensions=dimensions)
model.mem = spa.Memory(dimensions=dimensions, subdimensions=4,
synapse=0.1, neurons_per_dimension=50, tau=-.2)
# Connect the buffers
cortical_actions = spa.Actions(
'mem = color_in'
)
model.cortical = spa.Cortical(cortical_actions)
model.inp = spa.Input(color_in=color_input)
model.config[nengo.Probe].synapse = nengo.Lowpass(0.03)
color_in = nengo.Probe(model.color_in.state.output)
mem = nengo.Probe(model.mem.state.output)
sim = nengo.Simulator(model)
sim.run(3.)
plt.figure(figsize=(10, 10))
vocab = model.get_default_vocab(dimensions)
plt.subplot(2, 1, 1)
plt.plot(sim.trange(), model.similarity(sim.data, color_in))
plt.legend(model.get_output_vocab('color_in').keys, fontsize='x-small')
plt.ylabel("color")
plt.subplot(2, 1, 2)
plt.plot(sim.trange(), model.similarity(sim.data, mem))
plt.legend(fontsize='x-small')
plt.legend(model.get_output_vocab('color_in').keys, fontsize='x-small')
plt.ylabel("memory")
plt.xlabel("time [s]");
To represent structures, we can use what we learned last time: vector binding
In this case, we can do something like:
$Pos_0\circledast Item_0 + Pos_1\circledast Item_1 + ...$
This gives a way of 'counting' positions:
We can put this representation together with primacy and recency, and we get something like this
Arbitrary list lengths
This model can actually do lots more as well
These kind of memories are often called 'associative memories'
Typical solutions in ANNs are
Often worried about how to learn these
How to compute these kinds of mappings with the NEF/SPA?
Can do it in one layer with a feedforward approach
We've seen several techniques that will make this easy
In [45]:
import nengo
from nengo import spa
seed=1
np.random.seed(seed)
model = spa.SPA("Associative Memory", seed=seed)
D = 32
vocab = spa.Vocabulary(D)
vocab.parse('BLUE+GREEN+RED')
noise_RED = vocab.parse("RED").v + .2*np.random.randn(D)
noise_RED = noise_RED/np.linalg.norm(noise_RED)
noise_GREEN = vocab.parse("GREEN").v + .2*np.random.randn(D)
noise_GREEN = noise_GREEN/np.linalg.norm(noise_GREEN)
def memory_input(t):
if t < 0.2:
return vocab.parse("BLUE").v
elif .2 < t < .5:
return noise_RED
elif .5 < t < .8:
return vocab.parse("RED").v
elif .8 < t < 1:
return noise_GREEN
else:
return vocab.parse("0").v
with model:
stim = nengo.Node(output=memory_input, label='input')
model.am = spa.AssociativeMemory(vocab)
nengo.Connection(stim, model.am.input)
in_p = nengo.Probe(stim)
out_p = nengo.Probe(model.am.output, synapse=0.03)
sim = nengo.Simulator(model)
sim.run(1)
t = sim.trange()
figure(figsize=(10,10))
plt.subplot(2, 1, 1)
plt.plot(t, spa.similarity(sim.data[in_p], vocab))
plt.ylabel("Input")
plt.ylim(top=1.1)
plt.legend(vocab.keys, loc='best')
plt.subplot(2, 1, 2)
plt.plot(t, nengo.spa.similarity(sim.data[out_p], vocab))
plt.ylabel("Output")
plt.legend(vocab.keys, loc='best');
In [64]:
%pylab inline
import nengo
from nengo import spa
def color_input(t):
if t < 0.25:
return 'RED'
elif t < 0.5:
return 'BLUE'
else:
return '0'
def shape_input(t):
if t < 0.25:
return 'CIRCLE'
elif t < 0.5:
return 'SQUARE'
else:
return '0'
def cue_input(t):
if t < 0.5:
return '0'
sequence = ['0', 'CIRCLE', 'RED', '0', 'SQUARE', 'BLUE']
idx = int(((t - 0.5) // (1. / len(sequence))) % len(sequence))
return sequence[idx]
seed=1
model = spa.SPA(label="Simple question answering", seed=seed)
dimensions = 32
vocab = model.get_default_vocab(dimensions)
vocab.parse('BLUE+RED+CIRCLE+SQUARE')
with model:
model.color_in = spa.Buffer(dimensions=dimensions)
model.shape_in = spa.Buffer(dimensions=dimensions)
model.conv = spa.Memory(dimensions=dimensions, subdimensions=4, synapse=0.4)
model.cue = spa.Buffer(dimensions=dimensions)
model.out = spa.Buffer(dimensions=dimensions)
model.am = spa.AssociativeMemory(vocab, threshold=0.1)
model.inp = spa.Input(color_in=color_input, shape_in=shape_input, cue=cue_input)
# Connect the buffers
cortical_actions = spa.Actions(
'conv = color_in * shape_in',
'out = conv * ~cue',
'am = out'
)
model.cortical = spa.Cortical(cortical_actions)
model.config[nengo.Probe].synapse = nengo.Lowpass(0.03)
color_in = nengo.Probe(model.color_in.state.output)
shape_in = nengo.Probe(model.shape_in.state.output)
cue = nengo.Probe(model.cue.state.output)
conv = nengo.Probe(model.conv.state.output)
out = nengo.Probe(model.out.state.output)
clean = nengo.Probe(model.am.output)
sim = nengo.Simulator(model)
sim.run(3.)
In [66]:
plt.figure(figsize=(12, 10))
plt.subplot(6, 1, 1)
plt.plot(sim.trange(), model.similarity(sim.data, color_in))
plt.legend(model.get_output_vocab('color_in').keys, fontsize='x-small')
plt.ylabel("color")
plt.subplot(6, 1, 2)
plt.plot(sim.trange(), model.similarity(sim.data, shape_in))
plt.legend(model.get_output_vocab('shape_in').keys, fontsize='x-small')
plt.ylabel("shape")
plt.subplot(6, 1, 3)
plt.plot(sim.trange(), model.similarity(sim.data, cue))
plt.legend(model.get_output_vocab('cue').keys, fontsize='x-small')
plt.ylabel("cue")
plt.subplot(6, 1, 4)
for pointer in ['RED * CIRCLE', 'BLUE * SQUARE']:
plt.plot(sim.trange(), vocab.parse(pointer).dot(sim.data[conv].T), label=pointer)
plt.legend(fontsize='x-small')
plt.ylabel("convolved")
plt.subplot(6, 1, 5)
plt.plot(sim.trange(), spa.similarity(sim.data[out], vocab))
plt.legend(model.get_output_vocab('out').keys, fontsize='x-small')
plt.ylabel("Output")
plt.xlabel("time [s]");
plt.subplot(6, 1, 6)
plt.plot(sim.trange(), spa.similarity(sim.data[clean], vocab))
plt.legend(model.get_output_vocab('am').keys, fontsize='x-small')
plt.ylabel("Cleaned Up Output")
plt.xlabel("time [s]");
In [ ]: