From 52d91e974f41be8af5a0323f4235cec35b833122 Mon Sep 17 00:00:00 2001 From: int Date: Wed, 14 Oct 2026 21:54:09 +0300 Subject: [PATCH] Cats/dogs v3 learned features for KNP, punishment in cats_dogs, readout variants Co-Authored-By: Claude Opus 5.5 --- augment/punish_runs.sh | 2 +- .../source/models/altai/construct_network.cpp | 24 +++++++++ changes-2026-10-14.md | 7 +++ objects/grid_v3.sh | 11 ++++ objects/kmeans_features.py | 7 ++- objects/make_knp_features.py | 51 +++++++++++++++++++ readout/spiking_readout.py | 32 +++++++++++- 7 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 objects/grid_v3.sh create mode 100644 objects/make_knp_features.py diff --git a/augment/punish_runs.sh b/augment/punish_runs.sh index 4360f73..dab09ef 100644 --- a/augment/punish_runs.sh +++ b/augment/punish_runs.sh @@ -4,7 +4,7 @@ cd ~/run/exp B="KNP_COL_JITTER=20 KNP_MNIST_THR=60 KNP_MNIST_ADD_THR_SCALE=0 KNP_MNIST_INFER_INHIB=255" done_run() { grep -q "^CLASS" "$1/result.csv" 2>/dev/null || pgrep -f "run.sh $1 " >/dev/null; } for seed in 42 1; do - for p in 0 0.005 0.012; do + for p in ${PUNISH_LIST:-0 0.005 0.012}; do n=pun_fc_p${p}_s$seed done_run $n || MODEL=altai DATA=fcoarse_val ./run.sh $n 30000 $B KNP_COL_SEED=$seed KNP_COL_PUNISH=$p > $n.err 2>&1 & done diff --git a/cats-dogs/source/models/altai/construct_network.cpp b/cats-dogs/source/models/altai/construct_network.cpp index 41d57c4..43bd0b2 100644 --- a/cats-dogs/source/models/altai/construct_network.cpp +++ b/cats-dogs/source/models/altai/construct_network.cpp @@ -39,6 +39,8 @@ // KNP_COL_JITTER= random initial raster->input weights, normal(0, sigma), seed KNP_COL_SEED (default 42). // KNP_COL_LATERAL= input->input inhibition inside a column: weight -w, delay 1. // KNP_COL_GATE= gate->input pulses: weight g_k with delay k (default: single 10000 pulse with delay 1). +// KNP_COL_PUNISH= target->input dopamine -d*1000 to every column except the label's one (reward is 0.179376): +// neurons of wrong columns that fired recently lose the synapses that made them fire. namespace { float env_float(const char *name, float default_value) @@ -186,6 +188,28 @@ static void create_projections( // Record dopamine projection for label processing. network.data_.projections_from_classes_.push_back(target_to_input_proj_dopamine); + // Dopamine punishment for the columns of the other classes. + if (const float punish = env_float("KNP_COL_PUNISH", 0); punish > 0) + { + DeltaSynapseParams punish_synapse = target_to_input_synapse_dopamine; + auto punish_creator = [punish]( + const knp::core::UID &pre_uid, const knp::core::UID &post_uid, size_t pre_size, + size_t post_size, auto syn_gen) + { + return knp::framework::projection::creators::all_to_all( + pre_uid, post_uid, pre_size, post_size, + [syn_gen, punish](size_t pre, size_t post) + { + auto params = syn_gen(pre, post); + params.weight_ = post / neurons_per_column == pre ? 0.F : -punish * 1000; + return params; + }); + }; + auto target_to_input_proj_punish = constructor.add_projection( + punish_synapse, punish_creator, pops.target_pop_, pops.input_pop_, false, false); + network.data_.projections_from_classes_.push_back(target_to_input_proj_punish); + } + // Configure excitatory synapse for target to input projection. DeltaSynapseParams target_to_input_synapse_excitatory; target_to_input_synapse_excitatory.output_type_ = knp::synapse_traits::OutputType::EXCITATORY; diff --git a/changes-2026-10-14.md b/changes-2026-10-14.md index 0992e79..b81ab35 100644 --- a/changes-2026-10-14.md +++ b/changes-2026-10-14.md @@ -287,3 +287,10 @@ KNP, validation: `fc_v_plain` 81,0%, `fc_v_augB` 79,5%. Симулятор (2000 ### Наказание неверных колонок в STDP (MNIST) - `KNP_COL_PUNISH=` в `models/altai/construct_network.cpp`: проекция target→input, дофамин −d·1000 всем колонкам, кроме колонки метки (награда 0,179376). В KNP отрицательный дофамин ослабляет синапсы, внёсшие вклад, у нейронов, спайкавших в последние 10 шагов; принудительно сработавший нейрон наказание игнорирует. - Короткие прогоны (3000 обучение, 1000 проверка): пиксели — без наказания 85,9%, 0,005–0,06 — 83,7–84,8%, 0,18 — 76,6%. Coarse-признаки: без наказания 69,8%, 0,0005–0,003 — 62–71%, 0,005–0,02 — 73–79%, 0,18 — сеть молчит. Разброс большой; ночью полные прогоны `augment/punish_runs.sh`: 30 000, p ∈ {0; 0,005; 0,012} × seed {42, 1}. + +### 21:30–22:00 +- Кошки/собаки, v3 (9000 обучение + зеркало): K=800, с делением на контраст — 78,4% (lam 300); без деления (линейная центровка, складывается в веса нейрона) — 77,1% (α 0,25). K=1600 на 4000 — 78,1%. +- MNIST `sz80_60k_s42` (колонки 80, полный MNIST, тест): 93,49% — лучший результат колонок на пикселях (один seed). +- Наказание, полные прогоны (coarse-признаки, 30 000, validation): без — 81,0/78,4% (ср. 79,7); 0,005 — 79,0/83,1% (81,0); 0,012 — 83,0/83,9% (**83,5**). Запущены 0,02/0,035/0,06. +- Кошки/собаки в KNP: `cats-dogs/source` получил `KNP_COL_PUNISH` (по умолчанию выключено, поведение v2 не меняется); `cats_dogs` пересобран, прежний бинарник — `build/cats_dogs.v2-backup`. `objects/make_knp_features.py` → `features-v3/kmeans_validation.{bin,target}`: K=512 × 4 четверти = 2048 входов, без деления на контраст, α 0,25, усиление по 99-му перцентилю; ~6200 спайков на картинку. Сетка `objects/grid_v3.sh`: порог {1600, 4000, 8000, 16000} × наказание {0; 0,012; 0,03}. +- Выход на этих 2048 входах: ridge без спайков 74,2%; один спайковый нейрон-разность «собака − кошка» (сработал → собака) — 72,4% (порог 500). Пара нейронов «за/против» не работает (почти равные огромные суммы, 10–15 спайков не различают разницу). diff --git a/objects/grid_v3.sh b/objects/grid_v3.sh new file mode 100644 index 0000000..1dd8ba3 --- /dev/null +++ b/objects/grid_v3.sh @@ -0,0 +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_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_validation --train 18000 --count 1000 --threshold $t --columns 20 --seed 42 --jitter 20 \ + --quant weights --backend single > $n.log 2>&1; } +export -f run +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/kmeans_features.py b/objects/kmeans_features.py index 910a97f..f2fc595 100644 --- a/objects/kmeans_features.py +++ b/objects/kmeans_features.py @@ -20,9 +20,14 @@ def patches_at(img, ys, xs): return out +# CONTRAST_DIVIDE=False keeps only the (linear) patch mean subtraction, so mean removal, ZCA and the +# prototypes fold into one weight matrix of a spiking neuron; division by patch contrast is not linear. +CONTRAST_DIVIDE = True + + def normalise(p): p = p - p.mean(-1, keepdims=True) - return p / np.sqrt(p.var(-1, keepdims=True) + 10 / 255) + return p / np.sqrt(p.var(-1, keepdims=True) + 10 / 255) if CONTRAST_DIVIDE else p * 4 def learn(images, k=400, n_patches=200000, iters=15, seed=0): diff --git a/objects/make_knp_features.py b/objects/make_knp_features.py new file mode 100644 index 0000000..e1d221f --- /dev/null +++ b/objects/make_knp_features.py @@ -0,0 +1,51 @@ +"""Learned-feature dataset for the cats_dogs KNP binary (2048 inputs = K 512 × 4 quadrants). + +Features: kmeans_features without contrast division (linear patch centring, so layer 1 folds into +neuron weights), alpha 0.25. Each pooled feature is scaled by its 99th percentile on the training set +to 0–255 (a fixed gain per pooling neuron). Training part = 9000 images + their mirror copies, shuffled; +then the 1000 validation images. Writes /kmeans_validation.{bin,target} and the model. + +usage: ~/anvenv/bin/python make_knp_features.py --out /home/int/run/cats-dogs/features-v3 +""" +import argparse +import json +from pathlib import Path + +import numpy as np + +import kmeans_features as kf + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--data", default="/home/int/run/cats-dogs/data-v3") + ap.add_argument("--out", required=True) + ap.add_argument("--k", type=int, default=512) + ap.add_argument("--alpha", type=float, default=0.25) + ap.add_argument("--seed", type=int, default=20261014) + a = ap.parse_args() + out = Path(a.out) + out.mkdir(exist_ok=True) + kf.CONTRAST_DIVIDE = False + tr, va = np.load(f"{a.data}/train.npz"), np.load(f"{a.data}/val.npz") + model = kf.learn(tr["rgb"], k=a.k) + ft = np.vstack([kf.encode(tr["rgb"], model, alpha=a.alpha), kf.encode(tr["rgb"][:, :, :, ::-1], model, alpha=a.alpha)]) + yt = np.r_[tr["labels"], tr["labels"]] + fv = kf.encode(va["rgb"], model, alpha=a.alpha) + gain = np.percentile(ft, 99, axis=0) + 1e-6 + q = lambda f: np.clip(np.rint(f / gain * 255), 0, 255).astype(np.uint8) + order = np.random.default_rng(a.seed).permutation(len(ft)) + x = np.vstack([q(ft)[order], q(fv)]) + y = np.r_[yt[order], va["labels"]] + assert x.shape[1] == 2048 + x.tofile(out / "kmeans_validation.bin") + (out / "kmeans_validation.target").write_text("\n".join(map(str, y)) + "\n") + np.savez(out / "kmeans_model.npz", gain=gain, **model) + info = {"k": a.k, "alpha": a.alpha, "train": len(ft), "validation": len(fv), "contrast_divide": False, + "mean_value": float(x.mean()), "spikes_per_image": float(x.mean() / 255 * 10 * 2048)} + (out / "manifest.json").write_text(json.dumps(info, indent=2) + "\n") + print(info) + + +if __name__ == "__main__": + main() diff --git a/readout/spiking_readout.py b/readout/spiking_readout.py index 5d63ce9..9192763 100644 --- a/readout/spiking_readout.py +++ b/readout/spiking_readout.py @@ -51,7 +51,17 @@ def quantize(w, wmax=255): return np.round(w * scale).astype(np.int32), scale -def spiking_predict(x, wq, thr, batch=2000): +def spiking_predict(x, wq, thr, batch=2000, split=False): + """split=True: each class has an excitatory neuron (positive weights) and an opponent neuron (negated + negative weights); both only accumulate, so the result does not depend on spike order; the answer is the + class with the largest difference of their spike counts.""" + if split: + pos, neg = np.maximum(wq, 0), np.maximum(-wq, 0) + counts = [spiking_counts(x, w, thr, batch) for w in (pos, neg)] + score = counts[0] - counts[1] + p = score.argmax(1) + p[(counts[0] + counts[1]).max(1) == 0] = -1 + return p preds = [] for i in range(0, len(x), batch): r = raster(x[i:i + batch]) @@ -76,6 +86,23 @@ def spiking_predict(x, wq, thr, batch=2000): return np.concatenate(preds) +def spiking_counts(x, wq, thr, batch=2000): + """Spike counts of integrate-and-fire output neurons with weights wq (last row: bias input).""" + res = [] + for i in range(0, len(x), batch): + r = raster(x[i:i + batch]) + pot = np.zeros((r.shape[1], wq.shape[1]), dtype=np.int64) + count = np.zeros_like(pot) + for t in range(STEPS): + if t < ACTIVE: + pot += r[t].astype(np.int64) @ wq[:-1] + wq[-1] + fired = pot >= thr + count += fired + pot[fired] = 0 + res.append(count) + return np.concatenate(res) + + def main(): ap = argparse.ArgumentParser() ap.add_argument("data") @@ -88,6 +115,7 @@ def main(): ap.add_argument("--lam", type=float, default=10.0) ap.add_argument("--thr", default="") ap.add_argument("--save") + ap.add_argument("--split", action="store_true", help="excitatory/opponent neuron pair per class") a = ap.parse_args() x = np.fromfile(a.data, dtype=np.uint8).reshape(-1, a.width) y = np.array([int(v) for v in open(a.labels).read().split()]) @@ -100,7 +128,7 @@ def main(): thrs = [float(t) for t in a.thr.split(",")] if a.thr else [ref * k for k in (0.5, 1, 2, 4, 8)] best = None for thr in thrs: - p = spiking_predict(xe, wq, thr) + p = spiking_predict(xe, wq, thr, split=a.split) acc = 100 * (p == ye).mean() print(f"spiking, thr {thr:g}: correct {acc:.2f}% silent {100 * (p < 0).mean():.2f}%") if best is None or acc > best[0]: