Column limits on learned features; dopamine-trained spiking readout with calibrated decision neuron

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
int 2026-10-15 01:54:37 +03:00
parent ec913f51c1
commit 2f210b16f6
6 changed files with 92 additions and 6 deletions

View file

@ -321,3 +321,9 @@ KNP, validation: `fc_v_plain` 81,0%, `fc_v_augB` 79,5%. Симулятор (2000
### 01:00 – : слой признаков нейронным правилом; число шагов ### 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%** — правило на нейронах даёт признаки не хуже. - `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). - `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).

View file

@ -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)})

View file

@ -1,11 +1,11 @@
#!/bin/bash #!/bin/bash
# Cats/dogs v3: KNP AltAI columns (STDP) on learned k-means features, validation only. # Cats/dogs v3: KNP AltAI columns (STDP) on learned k-means features, validation only.
cd /home/int/run/cats-dogs 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 [ -f $n/metrics.json ] && return
rm -rf $n 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 \ 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; } --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"' 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"'

7
objects/grid_v3_more.sh Normal file
View file

@ -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

View file

@ -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

View file

@ -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("--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("--train-limit", type=int, default=0)
ap.add_argument("--out", required=True) 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() a = ap.parse_args()
global ACTIVE, STEPS globals().update(ACTIVE=a.active, STEPS=a.active + 5)
ACTIVE, STEPS = a.active, a.active + 5
model = dict(np.load(a.model)) model = dict(np.load(a.model))
w, b = fold(model, a.alpha) w, b = fold(model, a.alpha)
wq, bq, s = quant_layer1(w, b) wq, bq, s = quant_layer1(w, b)