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:
parent
ec913f51c1
commit
2f210b16f6
6 changed files with 92 additions and 6 deletions
|
|
@ -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).
|
||||||
|
|
|
||||||
67
objects/dopamine_readout.py
Normal file
67
objects/dopamine_readout.py
Normal 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)})
|
||||||
|
|
@ -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
7
objects/grid_v3_more.sh
Normal 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
|
||||||
7
objects/grid_v3_sparser.sh
Normal file
7
objects/grid_v3_sparser.sh
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue