- Implement reward-modulated STDP: spike timing, eligibility traces, reward-gated weight updates (w += η * reward * trace), clamp [-2,2] - Add integration smoke test vs SittingDuck (compile + battle completion) - Add tests/config.nims and nimble test task
This commit is contained in:
@@ -10,3 +10,6 @@ bin = @["SNNBot"]
|
||||
requires "nim >= 2.0.0"
|
||||
requires "robocode_tankroyale_botapi >= 1.0.7"
|
||||
# radar_lock is vendored in-tree (common_libs/) and wired via config.nims --path
|
||||
|
||||
task test, "Run integration tests":
|
||||
exec "nim c -r --path:../common_libs tests/test_basic_battle.nim"
|
||||
|
||||
@@ -23,6 +23,11 @@ const
|
||||
THRESH = 1.0 # LIF spike threshold
|
||||
MAX_GUN_TURN = 20.0 # max gun turn per tick (degrees)
|
||||
AIM_TOL = 2.0 # arrive tolerance (degrees)
|
||||
STDP_WIN = 20 # STDP timing window (ticks)
|
||||
ETA = 0.01 # learning rate
|
||||
# ponytail: global learning rate, no per-synapse adaptation; add when performance plateaus
|
||||
ELG_DECAY = 0.95 # eligibility trace decay per tick
|
||||
W_CLAMP = 2.0 # weight magnitude clamp
|
||||
|
||||
# ── SNN types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -32,12 +37,19 @@ type
|
||||
who: array[N_HID, float] # 6×1 hidden→output weights
|
||||
vHid: array[N_HID, float] # hidden membrane potentials
|
||||
vOut: float # output membrane potential (readout)
|
||||
lastSpikeIn: array[N_IN, int] # tick of last input spike (-1 = never)
|
||||
lastSpikeHid: array[N_HID, int] # tick of last hidden spike (-1 = never)
|
||||
eligibility: array[N_IN * N_HID, float] # per-synapse eligibility traces
|
||||
tick: int # internal tick counter for spike timing
|
||||
|
||||
proc initSNN(snn: var SNN) =
|
||||
# ponytail: uniform random init, fine until STDP training added in #155
|
||||
randomize()
|
||||
for w in snn.wih.mitems: w = rand(1.0) - 0.5
|
||||
for w in snn.who.mitems: w = rand(1.0) - 0.5
|
||||
for t in snn.lastSpikeIn.mitems: t = -STDP_WIN - 1
|
||||
for t in snn.lastSpikeHid.mitems: t = -STDP_WIN - 1
|
||||
for e in snn.eligibility.mitems: e = 0.0
|
||||
snn.tick = 0
|
||||
|
||||
proc encodeInput(bearing: float): array[N_IN, float] =
|
||||
## Population-code bearing into N_IN neurons.
|
||||
@@ -53,7 +65,17 @@ proc encodeInput(bearing: float): array[N_IN, float] =
|
||||
|
||||
proc forward(snn: var SNN, inputs: array[N_IN, float]): float =
|
||||
## One SNN tick. Returns target angle in degrees (-180..+180).
|
||||
# Hidden layer: LIF update
|
||||
inc snn.tick
|
||||
|
||||
# Decay eligibility traces each tick
|
||||
for e in snn.eligibility.mitems: e *= ELG_DECAY
|
||||
|
||||
# Record input spikes
|
||||
for i in 0 ..< N_IN:
|
||||
if inputs[i] > 0.0:
|
||||
snn.lastSpikeIn[i] = snn.tick
|
||||
|
||||
# Hidden layer: LIF update + STDP trace
|
||||
var spikes: array[N_HID, float]
|
||||
for h in 0 ..< N_HID:
|
||||
var wsum = 0.0
|
||||
@@ -63,8 +85,20 @@ proc forward(snn: var SNN, inputs: array[N_IN, float]): float =
|
||||
if snn.vHid[h] >= THRESH:
|
||||
spikes[h] = 1.0
|
||||
snn.vHid[h] = 0.0
|
||||
snn.lastSpikeHid[h] = snn.tick
|
||||
# STDP: post fires → check recent pre-spikes (potentiation)
|
||||
for i in 0 ..< N_IN:
|
||||
let dt = snn.tick - snn.lastSpikeIn[i]
|
||||
if dt >= 0 and dt <= STDP_WIN:
|
||||
snn.eligibility[i * N_HID + h] += exp(-float(dt) / float(STDP_WIN))
|
||||
else:
|
||||
spikes[h] = 0.0
|
||||
# STDP: pre fires after post → depression for synapses where post spiked recently
|
||||
for i in 0 ..< N_IN:
|
||||
if inputs[i] > 0.0:
|
||||
let dt = snn.tick - snn.lastSpikeHid[h]
|
||||
if dt >= 0 and dt <= STDP_WIN:
|
||||
snn.eligibility[i * N_HID + h] -= exp(-float(dt) / float(STDP_WIN))
|
||||
|
||||
# Output layer: membrane readout (no threshold)
|
||||
var osum = 0.0
|
||||
@@ -72,8 +106,7 @@ proc forward(snn: var SNN, inputs: array[N_IN, float]): float =
|
||||
osum += spikes[h] * snn.who[h]
|
||||
snn.vOut = LEAK * snn.vOut + osum
|
||||
|
||||
# Scale vOut to [-180, +180]. vOut is unbounded so clamp after tanh-like squash.
|
||||
# ponytail: simple linear scale; replace with proper output neuron tuning in #155
|
||||
# Scale vOut to [-180, +180]
|
||||
result = tanh(snn.vOut) * 180.0
|
||||
|
||||
# ── Bot state machine ─────────────────────────────────────────────────────────
|
||||
@@ -274,6 +307,11 @@ method run*(bot: SNNBot) =
|
||||
of EVALUATE:
|
||||
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
|
||||
let reward = 1.0 / (1.0 + err)
|
||||
# R-STDP weight update: w += η * R * e; then clamp and decay traces
|
||||
for idx in 0 ..< N_IN * N_HID:
|
||||
bot.snn.wih[idx] += ETA * reward * bot.snn.eligibility[idx]
|
||||
bot.snn.wih[idx] = bot.snn.wih[idx].clamp(-W_CLAMP, W_CLAMP)
|
||||
bot.snn.eligibility[idx] *= ELG_DECAY
|
||||
echo "tick=" & $bot.tick & " error=" & $err & "° reward=" & $reward
|
||||
bot.phase = DECIDE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user