77e6dace01
Implement the BitBrain (Address Decoder Element + Sparse Binary Coincidence) classifier as a generic, deterministic Nim library under common_libs/bitbrain/, written from the published algorithm (Front. Neuroinform. 17:1125844), not from the GPL-3.0 reference C. - ade.nim: signed thresholded random projection (scale 64 / centre 127 defaults reproduce the reference), multi-width ADs, optional deterministic homeostatic threshold adaptation. Hebbian longevity and Metropolis-Hastings sampling are described but not implemented. - sbc.nim: packed class-bit coincidence memory; idempotent learn, counting inference. - bitbrain.nim: container over several ADs and SBCs, online learn/infer, argmax readout, memory accounting. - tests: 32 unit checks (idempotence, planted rule + monotone online curve, shuffled-label chance control, unseen input, homeostasis, memory). - tests/test_bitbrain_mnist.nim: loads the reference pretrained ADs/thresholds and MNIST from /tmp, reproduces the reference exactly - 97.210% corrected and 96.540% bug-compatible - confirming the port. No gun/wiring integration yet; inputs and outputs to be agreed separately.
137 lines
5.2 KiB
Nim
137 lines
5.2 KiB
Nim
## BitBrain container — ADs + SBC memories + a counting readout.
|
|
##
|
|
## Clean-room implementation of the classification pipeline described in
|
|
##
|
|
## "BitBrain and Sparse Binary Coincidence (SBC) memories",
|
|
## Frontiers in Neuroinformatics 17:1125844, 2023.
|
|
##
|
|
## See `ade.nim` for the clean-room note (the reference C is GPL-3.0 and was not
|
|
## copied).
|
|
##
|
|
## The container holds several Address Decoders (possibly of different widths)
|
|
## and several SBC memories, each built from a pair of ADs. `learn` populates the
|
|
## SBCs from a labelled sample; `infer` drives every AD, reads every SBC with the
|
|
## same coincidence rule and sums the per-class set-bit counts. The class with the
|
|
## highest total wins. Everything is per-sample and order-free: the memory is
|
|
## monotone, so `learn` and `infer` may be interleaved arbitrarily.
|
|
##
|
|
## Reference setup used by the paper and the MNIST acceptance harness: 4 ADs of
|
|
## 2048 ADEs with widths {6, 8, 10, 12} and 6 cross-AD SBCs. The paper's
|
|
## 10-SBC variant adds 4 within-AD ("half-size") SBCs; `withinPairs` builds those
|
|
## (this implementation stores them full-size — the half-size packing is a
|
|
## separate memory optimisation).
|
|
|
|
import std/random
|
|
import ade, sbc
|
|
|
|
export ade, sbc
|
|
|
|
type
|
|
SbcSpec* = object
|
|
## Which pair of ADs feeds one SBC memory.
|
|
row*: int
|
|
col*: int
|
|
|
|
BitBrain* = object
|
|
ades*: seq[AddressDecoder]
|
|
sbcs*: seq[Sbc]
|
|
specs*: seq[SbcSpec]
|
|
nClasses*: int
|
|
|
|
proc crossPairs*(nAdes: int): seq[SbcSpec] =
|
|
## All unordered pairs of distinct ADs. For 4 ADs this is the paper's 6 SBCs.
|
|
for a in 0 ..< nAdes:
|
|
for b in a + 1 ..< nAdes:
|
|
result.add SbcSpec(row: a, col: b)
|
|
|
|
proc withinPairs*(nAdes: int): seq[SbcSpec] =
|
|
## The square/within-AD pairs (the paper's "half-size" SBCs).
|
|
for a in 0 ..< nAdes:
|
|
result.add SbcSpec(row: a, col: a)
|
|
|
|
proc initBitBrain*(ades: seq[AddressDecoder], specs: seq[SbcSpec],
|
|
nClasses: int): BitBrain =
|
|
## Build the container. Every AD must have the same number of ADEs because an
|
|
## SBC's axes are both `w` long (the paper's setup).
|
|
doAssert ades.len > 0, "need at least one AD"
|
|
doAssert nClasses > 0, "need at least one class"
|
|
let w = ades[0].nAde
|
|
for ad in ades:
|
|
doAssert ad.nAde == w, "all ADs must have the same number of ADEs"
|
|
result.ades = ades
|
|
result.nClasses = nClasses
|
|
result.specs = specs
|
|
result.sbcs = newSeq[Sbc](specs.len)
|
|
for s in 0 ..< specs.len:
|
|
doAssert specs[s].row >= 0 and specs[s].row < ades.len
|
|
doAssert specs[s].col >= 0 and specs[s].col < ades.len
|
|
result.sbcs[s] = initSbc(w, nClasses)
|
|
|
|
proc buildRandomBitBrain*(widths: seq[int], nAde, inputWidth, nClasses: int,
|
|
seed: int64,
|
|
specs: seq[SbcSpec] = @[]): BitBrain =
|
|
## Initialise a whole BitBrain with random ADs. If `specs` is empty the
|
|
## paper's 6 cross-AD SBCs are used. Fully deterministic given `seed`.
|
|
var rng = initRand(seed)
|
|
var ades: seq[AddressDecoder]
|
|
for w in widths:
|
|
ades.add initRandomAddressDecoder(nAde, w, inputWidth, rng)
|
|
let pairs = if specs.len == 0: crossPairs(widths.len) else: specs
|
|
result = initBitBrain(ades, pairs, nClasses)
|
|
|
|
proc nAdes*(bb: BitBrain): int {.inline.} =
|
|
bb.ades.len
|
|
|
|
proc resetLearning*(bb: var BitBrain) =
|
|
## Wipe every SBC. The ADs (and their thresholds) are left untouched.
|
|
for s in 0 ..< bb.sbcs.len:
|
|
bb.sbcs[s].clear()
|
|
|
|
proc fireInto*[T: SomeInteger](bb: BitBrain, input: openArray[T],
|
|
lists: var seq[seq[int32]]) =
|
|
## Compute the sparse firing pattern of every AD for `input`. `lists` is resized
|
|
## to `nAdes` and each entry is filled with that AD's firing ADE indices.
|
|
if lists.len != bb.ades.len:
|
|
lists.setLen(bb.ades.len)
|
|
for a in 0 ..< bb.ades.len:
|
|
bb.ades[a].activeList(input, lists[a])
|
|
|
|
proc learn*[T: SomeInteger](bb: var BitBrain, input: openArray[T], class: int) =
|
|
## One online supervised step: set the `class` bit of every observed
|
|
## coincidence. Idempotent — repeating the same sample is a no-op.
|
|
var lists: seq[seq[int32]]
|
|
bb.fireInto(input, lists)
|
|
for s in 0 ..< bb.sbcs.len:
|
|
let spec = bb.specs[s]
|
|
discard bb.sbcs[s].learn(lists[spec.row], lists[spec.col], class)
|
|
|
|
proc infer*[T: SomeInteger](bb: BitBrain, input: openArray[T]):
|
|
tuple[label: int, counts: seq[int]] =
|
|
## Drive every AD, count set class bits in every SBC, and return the argmax
|
|
## class plus the aggregated per-class counts. Ties go to the lowest class
|
|
## index (matching the reference's `>` scan that keeps the first maximum).
|
|
var lists: seq[seq[int32]]
|
|
bb.fireInto(input, lists)
|
|
result.counts = newSeq[int](bb.nClasses)
|
|
for s in 0 ..< bb.sbcs.len:
|
|
let spec = bb.specs[s]
|
|
bb.sbcs[s].infer(lists[spec.row], lists[spec.col], result.counts)
|
|
result.label = 0
|
|
var best = result.counts[0]
|
|
for k in 1 ..< bb.nClasses:
|
|
if result.counts[k] > best:
|
|
best = result.counts[k]
|
|
result.label = k
|
|
|
|
proc memoryBytes*(bb: BitBrain): int =
|
|
## Total bytes: AD synapse codes + thresholds + counters + SBC bit tensors.
|
|
for ad in bb.ades:
|
|
result += ad.memoryBytes
|
|
for s in bb.sbcs:
|
|
result += s.memoryBytes
|
|
|
|
proc sbcMemoryBytes*(bb: BitBrain): int =
|
|
## Just the SBC bit tensors — the dominant term for realistic configurations.
|
|
for s in bb.sbcs:
|
|
result += s.memoryBytes
|