Cats/dogs v3 learned features for KNP, punishment in cats_dogs, readout variants

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
int 2026-10-14 21:54:09 +03:00
parent cf386f4e36
commit 52d91e974f
7 changed files with 130 additions and 4 deletions

View file

@ -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" 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; } 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 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 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_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 done

View file

@ -39,6 +39,8 @@
// KNP_COL_JITTER=<sigma> random initial raster->input weights, normal(0, sigma), seed KNP_COL_SEED (default 42). // KNP_COL_JITTER=<sigma> random initial raster->input weights, normal(0, sigma), seed KNP_COL_SEED (default 42).
// KNP_COL_LATERAL=<w> input->input inhibition inside a column: weight -w, delay 1. // KNP_COL_LATERAL=<w> input->input inhibition inside a column: weight -w, delay 1.
// KNP_COL_GATE=<g1,g2,..> gate->input pulses: weight g_k with delay k (default: single 10000 pulse with delay 1). // KNP_COL_GATE=<g1,g2,..> gate->input pulses: weight g_k with delay k (default: single 10000 pulse with delay 1).
// KNP_COL_PUNISH=<d> 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 namespace
{ {
float env_float(const char *name, float default_value) float env_float(const char *name, float default_value)
@ -186,6 +188,28 @@ static void create_projections(
// Record dopamine projection for label processing. // Record dopamine projection for label processing.
network.data_.projections_from_classes_.push_back(target_to_input_proj_dopamine); 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<DeltaSynapse>(
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. // Configure excitatory synapse for target to input projection.
DeltaSynapseParams target_to_input_synapse_excitatory; DeltaSynapseParams target_to_input_synapse_excitatory;
target_to_input_synapse_excitatory.output_type_ = knp::synapse_traits::OutputType::EXCITATORY; target_to_input_synapse_excitatory.output_type_ = knp::synapse_traits::OutputType::EXCITATORY;

View file

@ -287,3 +287,10 @@ KNP, validation: `fc_v_plain` 81,0%, `fc_v_augB` 79,5%. Симулятор (2000
### Наказание неверных колонок в STDP (MNIST) ### Наказание неверных колонок в STDP (MNIST)
- `KNP_COL_PUNISH=<d>` в `models/altai/construct_network.cpp`: проекция target→input, дофамин −d·1000 всем колонкам, кроме колонки метки (награда 0,179376). В KNP отрицательный дофамин ослабляет синапсы, внёсшие вклад, у нейронов, спайкавших в последние 10 шагов; принудительно сработавший нейрон наказание игнорирует. - `KNP_COL_PUNISH=<d>` в `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}. - Короткие прогоны (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 спайков не различают разницу).

11
objects/grid_v3.sh Normal file
View file

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

View file

@ -20,9 +20,14 @@ def patches_at(img, ys, xs):
return out 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): def normalise(p):
p = p - p.mean(-1, keepdims=True) 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): def learn(images, k=400, n_patches=200000, iters=15, seed=0):

View file

@ -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 <out>/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()

View file

@ -51,7 +51,17 @@ def quantize(w, wmax=255):
return np.round(w * scale).astype(np.int32), scale 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 = [] preds = []
for i in range(0, len(x), batch): for i in range(0, len(x), batch):
r = raster(x[i:i + batch]) r = raster(x[i:i + batch])
@ -76,6 +86,23 @@ def spiking_predict(x, wq, thr, batch=2000):
return np.concatenate(preds) 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(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("data") ap.add_argument("data")
@ -88,6 +115,7 @@ def main():
ap.add_argument("--lam", type=float, default=10.0) ap.add_argument("--lam", type=float, default=10.0)
ap.add_argument("--thr", default="") ap.add_argument("--thr", default="")
ap.add_argument("--save") ap.add_argument("--save")
ap.add_argument("--split", action="store_true", help="excitatory/opponent neuron pair per class")
a = ap.parse_args() a = ap.parse_args()
x = np.fromfile(a.data, dtype=np.uint8).reshape(-1, a.width) 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()]) 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)] 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 best = None
for thr in thrs: for thr in thrs:
p = spiking_predict(xe, wq, thr) p = spiking_predict(xe, wq, thr, split=a.split)
acc = 100 * (p == ye).mean() acc = 100 * (p == ye).mean()
print(f"spiking, thr {thr:g}: correct {acc:.2f}% silent {100 * (p < 0).mean():.2f}%") print(f"spiking, thr {thr:g}: correct {acc:.2f}% silent {100 * (p < 0).mean():.2f}%")
if best is None or acc > best[0]: if best is None or acc > best[0]: