From 2f210b16f6512cade685f4937c058791e86573df Mon Sep 17 00:00:00 2001 From: int Date: Thu, 15 Oct 2026 01:54:37 +0300 Subject: [PATCH] Column limits on learned features; dopamine-trained spiking readout with calibrated decision neuron Co-Authored-By: Claude Opus 5.5 --- changes-2026-10-14.md | 6 ++++ objects/dopamine_readout.py | 67 +++++++++++++++++++++++++++++++++++++ objects/grid_v3.sh | 6 ++-- objects/grid_v3_more.sh | 7 ++++ objects/grid_v3_sparser.sh | 7 ++++ objects/spiking_pipeline.py | 5 ++- 6 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 objects/dopamine_readout.py create mode 100644 objects/grid_v3_more.sh create mode 100644 objects/grid_v3_sparser.sh diff --git a/changes-2026-10-14.md b/changes-2026-10-14.md index aec74b1..b55cddb 100644 --- a/changes-2026-10-14.md +++ b/changes-2026-10-14.md @@ -321,3 +321,9 @@ KNP, validation: `fc_v_plain` 81,0%, `fc_v_augB` 79,5%. Симулятор (2000 ### 01:00 – : слой признаков нейронным правилом; число шагов - `kmeans_features.learn_online`: обучение «победитель забирает всё» по одному кусочку за проход (как WTA-слой с Хеббом/STDP): победивший нейрон сдвигает нормированные веса к кусочку, скорость lr0/(1 + t/20000), 200 000 кусочков, то же выбеливание. Validation, K=400, без деления на контраст, +зеркало, ridge 300: пакетные k-средние 74,9%, **онлайн WTA 75,9%** — правило на нейронах даёт признаки не хуже. - `spiking_pipeline.py --active N`: число активных шагов входа (по умолчанию 10). Запущены 20 и 30 шагов (порог слоя 1 тот же 108,08). +- Число активных шагов (точная симуляция, K=800, порог слоя 1 108,08, validation): 10 — 71,6%; 20 — **72,9%** (1,49 млн спайков, 1515 с); 30 — 73,0% (2,32 млн, 2079 с). +1,3 п.п. за 2–3× шагов. (Первый запуск 20/30 упал на `global` в `main` — исправлено через `globals().update`.) + +### 01:20 – 02:10: предел колонок и выход, обучаемый дофамином +- Колонки KNP на выученных признаках (validation): top-384 — 45,7–54,7% (молчат 96–308), top-512 — 22–37% (молчат 459–695); top-256 c40 — 62,2–**64,8%** (t4000 p0,012); top-256 t1000/t2500 p0,02 — 63,0/63,4%; top-128 — 50,9–55,9%; top-64 — 50% (один класс); top-256 c80 — 64,8/**65,1%**; top-128 c80 — 50,0/55,2%. Предел колонок на этих признаках ≈ 65%: плотнее — молчат, реже — теряют информацию. +- `objects/dopamine_readout.py`: выход «собака − кошка», обучаемый локальным трёхфакторным правилом: на каждой картинке синапс меняется на η·e·(число спайков входа), e = цель − tanh(сумма) — знаковый дофамин; усреднение весов по ходу; 20 эпох; вход нормирован средним числом спайков (постоянное усиление синапса). На точных счётчиках слоя 1 (K=800, 10 шагов): линейно 69,2% (ridge 71,0%). +- Нейрон-решатель без подбора порога по validation: смещение поднимается на thr/ACTIVE за шаг, порог k × медиана входа (k = 10…200 — результат не зависит), так что «дошёл до порога» = «сумма > 0». Ridge — **71,8%**; дофаминовые веса после обрезки |w| по 99,5-му перцентилю перед целочислением — **69,3–69,6%** (без обрезки 53,9%: редкие входы получают огромные веса, остальные округляются в 0). diff --git a/objects/dopamine_readout.py b/objects/dopamine_readout.py new file mode 100644 index 0000000..e890860 --- /dev/null +++ b/objects/dopamine_readout.py @@ -0,0 +1,67 @@ +"""On-line three-factor learning of the readout neuron (dog − cat) on exact layer-1 spike counts. + +For each training image: drive = Σ w·count + b; the neuron answers dog if drive > 0. A signed dopamine +signal carries the error e = target − tanh(drive / scale) and every synapse changes by η · e · count +(pre-synaptic activity × dopamine): a local rule that needs no weight transport. Weights are clipped to +integers in ±255 after each epoch for the spiking check. + +usage: ~/anvenv/bin/python dopamine_readout.py /home/int/run/cats-dogs/spiking_v3_k800.npz +""" +import sys + +import numpy as np + +sys.path.insert(0, "/mnt/c/knp/objects") +import spiking_pipeline as sp # noqa: E402 + +s = np.load(sys.argv[1]) +ct, sv, yt, yv = s["ct"].astype(np.float32), s["sv"], s["yt"] * 2 - 1, s["yv"] +gain = ct.mean(0) + 1 # per-input normalisation = fixed synaptic gain +x = ct / gain +xv = sv.sum(1) / gain +rng = np.random.default_rng(0) +eta0 = float(sys.argv[2]) if len(sys.argv) > 2 else 2e-4 +w = np.zeros(x.shape[1], np.float32) +b = 0.0 +w_avg, b_avg, n_avg = np.zeros_like(w), 0.0, 0 +for epoch in range(20): + eta = eta0 / (1 + epoch / 5) + for i in rng.permutation(len(x)): + d = x[i] @ w + b + e = yt[i] - np.tanh(d) + w += eta * e * x[i] + b += eta * e + # Running average of the weights (a slow synaptic trace); used for the answer. + w_avg += w + b_avg += b + n_avg += 1 + acc = 100 * (((xv @ (w_avg / n_avg) + b_avg / n_avg) > 0) == yv).mean() + if epoch % 4 == 3: + print(f"epoch {epoch + 1}: validation (linear on spike counts) {acc:.1f}%", flush=True) +w, b = w_avg / n_avg, b_avg / n_avg +# Spiking check: fold the gain into integer weights of the readout neuron. +wf = np.r_[w / gain, b] +# Rarely active inputs get large weights; clip at the 99.5th percentile of |w| so that 8-bit-like integer +# weights keep the bulk of the synapses (the bias keeps its full value relative to the clip). +cap = np.percentile(np.abs(wf[:-1]), 99.5) +wf[:-1] = np.clip(wf[:-1], -cap, cap) +rq = np.rint(wf / cap * 255).astype(np.int64) +bias = int(np.rint(rq[-1] / sp.ACTIVE)) +unit = float(np.median(np.abs(sv[:, :sp.ACTIVE].reshape(-1, sv.shape[-1]) @ rq[:-1] + bias))) + 1 +print("spiking readout:", {q: round(100 * (sp.readout_neuron(sv, rq[:-1], bias, q * unit) == yv).mean(), 1) for q in (2, 3, 4, 5, 6)}) + + +def calibrated(sv, rq, bias, k=50): + """Decision neuron: threshold k × unit, bias raised by thr / ACTIVE per active step, so that reaching the + threshold over the active steps means total drive > 0 (exact when the potential does not hit the floor + of 0 on the way). Returns accuracy on the validation labels.""" + unit = float(np.median(np.abs(sv[:, :sp.ACTIVE].reshape(-1, sv.shape[-1]) @ rq + bias))) + 1 + thr = k * unit + return round(100 * (sp.readout_neuron(sv, rq, bias + int(np.ceil(thr / sp.ACTIVE)), thr) == yv).mean(), 1) + + +print("dopamine-trained, calibrated decision neuron:", {k: calibrated(sv, rq[:-1], bias, k) for k in (10, 50, 200)}) +f = lambda c: np.hstack([c.astype(np.float32), np.ones((len(c), 1), np.float32)]) +sol = np.linalg.solve(f(ct).T @ f(ct) + 1e6 * np.eye(ct.shape[1] + 1), f(ct).T @ yt.astype(np.float64)) +rr = np.rint(sol / np.abs(sol).max() * 255).astype(np.int64) +print("ridge, calibrated decision neuron:", {k: calibrated(sv, rr[:-1], int(np.rint(rr[-1] / sp.ACTIVE)), k) for k in (10, 50, 200)}) diff --git a/objects/grid_v3.sh b/objects/grid_v3.sh index 104a243..0718c5f 100644 --- a/objects/grid_v3.sh +++ b/objects/grid_v3.sh @@ -1,11 +1,11 @@ #!/bin/bash # Cats/dogs v3: KNP AltAI columns (STDP) on learned k-means features, validation only. cd /home/int/run/cats-dogs -run() { t=$1; p=$2; n=v3${DS:+_$DS}_t${t}_p${p} +run() { t=$1; p=$2; n=v3${DS:+_$DS}${COLS:+_c$COLS}_t${t}_p${p} [ -f $n/metrics.json ] && return rm -rf $n KNP_COL_PUNISH=$p python3 /mnt/c/knp/cats-dogs/run.py $n --data-dir /home/int/run/cats-dogs/features-v3 \ - --dataset kmeans${DS:+_$DS}_validation --train 18000 --count 1000 --threshold $t --columns 20 --seed 42 --jitter 20 \ + --dataset kmeans${DS:+_$DS}_validation --train 18000 --count 1000 --threshold $t --columns ${COLS:-20} --seed 42 --jitter 20 \ --quant weights --backend single > $n.log 2>&1; } -export -f run; export DS +export -f run; export DS COLS for t in ${THRS:-1600 4000 8000 16000}; do for p in ${PUNS:-0 0.012 0.03}; do echo "$t $p"; done; done | xargs -P ${JOBS:-6} -n 2 bash -c 'run "$0" "$1"' diff --git a/objects/grid_v3_more.sh b/objects/grid_v3_more.sh new file mode 100644 index 0000000..b5904c9 --- /dev/null +++ b/objects/grid_v3_more.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Columns on learned features: denser top-k, punishment 0.02, 40-neuron columns (validation only). +G=/mnt/c/knp/objects/grid_v3.sh +DS=top384 THRS="1600 4000" PUNS="0.012 0.02" JOBS=6 $G +DS=top512 THRS="1600 4000" PUNS="0.012 0.02" JOBS=6 $G +DS=top256 COLS=40 THRS="1600 4000" PUNS="0.012 0.02" JOBS=6 $G +DS=top256 THRS="1000 2500" PUNS="0.02" JOBS=6 $G diff --git a/objects/grid_v3_sparser.sh b/objects/grid_v3_sparser.sh new file mode 100644 index 0000000..417917f --- /dev/null +++ b/objects/grid_v3_sparser.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Sparser inputs and larger columns (validation only). +G=/mnt/c/knp/objects/grid_v3.sh +DS=top128 THRS="600 1600" PUNS="0.012" JOBS=6 $G +DS=top64 THRS="300 800" PUNS="0.012" JOBS=6 $G +DS=top256 COLS=80 THRS="1600 4000" PUNS="0.012" JOBS=6 $G +DS=top128 COLS=80 THRS="600 1600" PUNS="0.012" JOBS=6 $G diff --git a/objects/spiking_pipeline.py b/objects/spiking_pipeline.py index 71eaee4..736b394 100644 --- a/objects/spiking_pipeline.py +++ b/objects/spiking_pipeline.py @@ -108,10 +108,9 @@ def main(): ap.add_argument("--thr1", type=float, default=0, help="layer-1 threshold in quantised units (0: auto)") ap.add_argument("--train-limit", type=int, default=0) ap.add_argument("--out", required=True) - ap.add_argument("--active", type=int, default=ACTIVE, help="active (spiking) input steps; 5 silent steps follow") + ap.add_argument("--active", type=int, default=10, help="active (spiking) input steps; 5 silent steps follow") a = ap.parse_args() - global ACTIVE, STEPS - ACTIVE, STEPS = a.active, a.active + 5 + globals().update(ACTIVE=a.active, STEPS=a.active + 5) model = dict(np.load(a.model)) w, b = fold(model, a.alpha) wq, bq, s = quant_layer1(w, b)