fix(SNNBot): enforce sparse reservoir state via k-Winners-Take-All
Dense firing (~50%) caused all readout bins to saturate identically. Replace threshold-based firing with k-WTA: only top 50 neurons fire per tick (~5% sparsity). Sparse patterns give low inter-angle overlap -> readout bins can discriminate between different bearings.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# ponytail: binary reservoir aimer — prototype; if reservoir projection is poor, scale RESERVOIR_SIZE to 4096
|
||||
|
||||
import std/[bitops, math]
|
||||
import std/[algorithm, bitops, math]
|
||||
|
||||
# ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -11,6 +11,8 @@ const
|
||||
INPUT_BITS* = 80
|
||||
SPARSITY_IN = 0.1
|
||||
SPARSITY_REC = 0.05
|
||||
K_ACTIVE = 50 # ~5% of RESERVOIR_SIZE fire per tick
|
||||
# ponytail: K_ACTIVE=50 gives ~5% sparsity; increase if readout can't discriminate, decrease if patterns overlap too much
|
||||
|
||||
# ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -19,13 +21,12 @@ type
|
||||
BitVec* = array[16, uint64] # 1024 bits
|
||||
|
||||
Reservoir* = object
|
||||
wIn: array[RESERVOIR_SIZE, BitVec80]
|
||||
wRec: array[RESERVOIR_SIZE, BitVec]
|
||||
threshold: array[RESERVOIR_SIZE, int]
|
||||
state: BitVec
|
||||
readout: array[N_BINS, BitVec]
|
||||
scores*: array[N_BINS, int]
|
||||
rngState: uint64
|
||||
wIn: array[RESERVOIR_SIZE, BitVec80]
|
||||
wRec: array[RESERVOIR_SIZE, BitVec]
|
||||
state: BitVec
|
||||
readout: array[N_BINS, BitVec]
|
||||
scores*: array[N_BINS, int]
|
||||
rngState: uint64
|
||||
|
||||
# ── PRNG ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -61,27 +62,33 @@ proc initReservoir*(seed: int): Reservoir =
|
||||
for w in 0 ..< 16:
|
||||
result.wRec[i][w] = sparseBits(result, SPARSITY_REC)
|
||||
|
||||
# ponytail: threshold=1 gives ~50% firing; raise if reservoir saturates (all neurons fire every tick)
|
||||
result.threshold[i] = 1
|
||||
|
||||
# readout and state are zero-initialized by default
|
||||
|
||||
# ── Forward ────────────────────────────────────────────────────────────────────
|
||||
|
||||
proc forward*(r: var Reservoir, input: BitVec80): int =
|
||||
var newState: BitVec
|
||||
|
||||
# Compute activation scores for all neurons
|
||||
var activations: array[RESERVOIR_SIZE, int]
|
||||
for i in 0 ..< RESERVOIR_SIZE:
|
||||
let inScore = popcount(input[0] and r.wIn[i][0]) +
|
||||
popcount(input[1] and r.wIn[i][1])
|
||||
let inScore = popcount(input[0] and r.wIn[i][0]).int +
|
||||
popcount(input[1] and r.wIn[i][1]).int
|
||||
var recScore = 0
|
||||
for w in 0 ..< 16:
|
||||
recScore += popcount(r.state[w] and r.wRec[i][w])
|
||||
recScore += popcount(r.state[w] and r.wRec[i][w]).int
|
||||
activations[i] = inScore + recScore
|
||||
|
||||
if inScore + recScore > r.threshold[i]:
|
||||
let wordIdx = i shr 6 # i div 64
|
||||
let bitIdx = i and 63 # i mod 64
|
||||
newState[wordIdx] = newState[wordIdx] or (1'u64 shl bitIdx)
|
||||
# k-WTA: find K-th highest activation via sort on a copy
|
||||
var sorted = activations
|
||||
sort(sorted, order = SortOrder.Descending)
|
||||
let kThreshold = sorted[min(K_ACTIVE - 1, RESERVOIR_SIZE - 1)]
|
||||
|
||||
# Fire exactly K_ACTIVE neurons (tie-break: first in index order)
|
||||
var newState: BitVec
|
||||
var count = 0
|
||||
for i in 0 ..< RESERVOIR_SIZE:
|
||||
if activations[i] >= kThreshold and count < K_ACTIVE:
|
||||
newState[i shr 6] = newState[i shr 6] or (1'u64 shl (i and 63))
|
||||
inc count
|
||||
|
||||
r.state = newState
|
||||
|
||||
|
||||
Reference in New Issue
Block a user