spiking-experiments / exp3_stdp_run1_null.py
LAWLESS1987
Claude Opus 4.8
spiking-neural-network experiments (CPU, from scratch)
1ec3d14
Raw History Blame Contribute Delete
3.92 kB
"""Experiment 3: learning from spike TIMING (STDP), with no labels and no backpropagation.
Spike-timing-dependent plasticity is the learning rule neuromorphic chips can run on-chip:
- if an input spike arrives just BEFORE the neuron fires, that connection is strengthened (it helped cause the spike);
- if it arrives just AFTER, the connection is weakened.
Setup: 100 input neurons fire at random. Inputs 0-19 are a hidden "pattern group": now and then all 20 fire together.
One output neuron listens to all 100 and learns by STDP alone. Nothing tells it which inputs matter.
If STDP works, the pattern group's connections grow and the rest shrink.
Run: .venv\\Scripts\\python.exe exp3_stdp.py Out: exp3_stdp.png
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from brian2 import (Hz, NeuronGroup, PoissonGroup, SpikeGeneratorGroup, SpikeMonitor, StateMonitor, Synapses, ms,
mV, prefs, run, second, seed, start_scope)
prefs.codegen.target = "numpy"
start_scope()
seed(1)
rng = np.random.default_rng(1)
N, GROUP, DUR = 100, 20, 30 * second
# Background noise: every input fires at random, 5 spikes per second.
noise = PoissonGroup(N, rates=5 * Hz)
# The hidden pattern: inputs 0-19 fire together about 4 times per second, with a little jitter.
times, idx = [], []
for t in np.arange(0.05, float(DUR / second), 0.25):
for i in range(GROUP):
times.append(t + rng.normal(0, 0.002))
idx.append(i)
order = np.argsort(times)
pattern = SpikeGeneratorGroup(N, np.array(idx)[order], np.array(times)[order] * second)
out = NeuronGroup(1, "dv/dt = -v / (10*ms) : volt", threshold="v > 10*mV", reset="v = 0*mV", method="exact")
taupre = taupost = 20 * ms
wmax, Apre = 2.0, 0.01
Apost = -Apre * 1.05 # slightly more weakening than strengthening keeps weights in check
stdp = """w : 1
dapre/dt = -apre / taupre : 1 (event-driven)
dapost/dt = -apost / taupost : 1 (event-driven)"""
on_pre = """v_post += w * mV
apre += Apre
w = clip(w + apost, 0, wmax)"""
on_post = """apost += Apost
w = clip(w + apre, 0, wmax)"""
syn_noise = Synapses(noise, out, stdp, on_pre=on_pre, on_post=on_post)
syn_pat = Synapses(pattern, out, stdp, on_pre=on_pre, on_post=on_post)
for s in (syn_noise, syn_pat):
s.connect()
s.w = "rand() * 0.6" # everyone starts with small random weights
start_w = np.array(syn_noise.w)
wmon = StateMonitor(syn_noise, "w", record=True, dt=500 * ms)
wmon_p = StateMonitor(syn_pat, "w", record=True, dt=500 * ms)
spk = SpikeMonitor(out)
run(DUR)
# Each input has two pathways (its noise spikes and its pattern spikes); its total weight is their sum.
w_end = np.array(syn_noise.w) + np.array(syn_pat.w)
grp, rest = w_end[:GROUP].mean(), w_end[GROUP:].mean()
print(f"output spikes: {spk.num_spikes}")
print(f"average weight at the start: {start_w.mean():.2f} (all inputs alike)")
print(f"average weight now, pattern inputs 0-19: {grp:.2f}")
print(f"average weight now, other inputs 20-99: {rest:.2f}")
print("STDP found the hidden group" if grp > 2 * rest else "STDP did NOT separate the group on this run")
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].bar(range(N), w_end, color=["tab:orange"] * GROUP + ["tab:gray"] * (N - GROUP))
ax[0].set_title("learned weight per input (orange = hidden pattern group)")
ax[0].set_xlabel("input neuron"); ax[0].set_ylabel("weight")
tw = wmon.t / second
total = wmon.w + wmon_p.w
ax[1].plot(tw, total[:GROUP].mean(0), c="tab:orange", label="pattern group 0-19")
ax[1].plot(tw, total[GROUP:].mean(0), c="tab:gray", label="other inputs")
ax[1].set_xlabel("time (s)"); ax[1].set_ylabel("average weight"); ax[1].legend()
ax[1].set_title("learning over time, from spike timing alone")
plt.tight_layout(); plt.savefig("exp3_stdp.png", dpi=110)
print("saved exp3_stdp.png")