diff --git a/changes-2026-10-14.md b/changes-2026-10-14.md index b81ab35..456e435 100644 --- a/changes-2026-10-14.md +++ b/changes-2026-10-14.md @@ -294,3 +294,12 @@ KNP, validation: `fc_v_plain` 81,0%, `fc_v_augB` 79,5%. Симулятор (2000 - Наказание, полные прогоны (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 спайков не различают разницу). + +### 22:00 – 00:30 +- KNP-колонки на плотных выученных признаках (`features-v3`, 2048 входов, ~6200 спайков/картинку): молчат на 99–100% картинок при любых порогах 1600–16000 и наказании 0–0,03; в симуляторе даже при пороге 1 — 25,8% верно, 48,6% молчит. Средний вес после STDP −13 (как у ON/OFF): у плотного входа сумма уходит в минус. +- Разреженные варианты (порог колонки × наказание, validation 1000): top-256 — 61,4–64,3% (лучший t1600 p0,012 **64,3%**); выше 70-го перцентиля — 56,3–60,3%; выше 50-го — 51,6–58,8%. Наказание 0,012 лучше 0 во всех 9 парах (+0,2…+2,5 п.п.). Линейный выход на тех же входах: top-256 66,2%, c70 68,2%, c50 69,6%. +- MNIST, наказание (coarse, 30 000, validation, seed 42/1): 0 — 81,0/78,4 (79,7); 0,005 — 81,0; 0,012 — 83,0/83,9 (**83,5**); 0,02 — 81,4/85,4 (83,4); 0,035 — 83,3/80,5 (81,9); 0,06 — 77,1/73,9 (75,5). Оптимум 0,012–0,02, +3,8 п.п. +- Полностью спайковая цепочка в приближении (признаки float → uint8 → нейрон-разность «собака − кошка»): K=800 — **74,1%** (порог 1000, выбран по validation). +- Ошибка ожидания: условие `pgrep -f grid_v3_sparse.sh` находило саму команду ожидания, компьютер простаивал ~22:40–00:10. +- `objects/fetch_fresh_test.py`: докачаны test 0700–0999 cat/dog (600, нигде не использовались) — для одной итоговой проверки. +- `objects/spiking_pipeline.py`: точная пошаговая симуляция пиксели → слой 1 (K × 27×27 нейронов, целые веса ±127, смещение каждый активный шаг, порог, сброс 0, не ниже 0) → нейрон «собака − кошка» (веса общие для узора и четверти, задержка 1). Проба на 300 обучающих: линейный на точных счётчиках 63,3%, спайковый выход 63,0% (порог 4 × медиана входа за шаг). diff --git a/objects/fetch_fresh_test.py b/objects/fetch_fresh_test.py new file mode 100644 index 0000000..b32767c --- /dev/null +++ b/objects/fetch_fresh_test.py @@ -0,0 +1,40 @@ +"""Download CIFAR-10 JPEG mirror TEST images 0700–0999 for cat and dog (never used: a fresh final test set). + +Same source and commit as cats-dogs/prepare_jpeg.py. Existing files are skipped. +usage: python3 fetch_more.py +""" +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import time +import urllib.request + +COMMIT = 'd7d05618061fe37d830fb0419063d113c575ccbc' +BASE = f'https://raw.githubusercontent.com/YoongiKim/CIFAR-10-images/{COMMIT}' +cache = Path('/home/int/run/cats-dogs/data/github-jpeg-cache') + + +def load(item): + label, index = item + path = cache / f'test-{label}-{index:04d}.jpg' + if path.exists(): + return 0 + for attempt in range(6): + try: + with urllib.request.urlopen(f'{BASE}/test/{label}/{index:04d}.jpg', timeout=30) as r: + data = r.read() + tmp = path.with_suffix('.part') + tmp.write_bytes(data) + tmp.rename(path) + return 1 + except Exception: + if attempt == 5: + raise + time.sleep(3 * (attempt + 1)) + + +items = [(label, i) for label in ('cat', 'dog') for i in range(700, 1000)] +with ThreadPoolExecutor(max_workers=8) as pool: + for n, _ in enumerate(pool.map(load, items)): + if n % 500 == 0: + print('done', n, flush=True) +print('all', len(items), flush=True) diff --git a/objects/grid_v3.sh b/objects/grid_v3.sh index 1dd8ba3..104a243 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_t${t}_p${p} +run() { t=$1; p=$2; n=v3${DS:+_$DS}_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 \ + --dataset kmeans${DS:+_$DS}_validation --train 18000 --count 1000 --threshold $t --columns 20 --seed 42 --jitter 20 \ --quant weights --backend single > $n.log 2>&1; } -export -f run +export -f run; export DS 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_sparse.sh b/objects/grid_v3_sparse.sh new file mode 100644 index 0000000..0c14bd1 --- /dev/null +++ b/objects/grid_v3_sparse.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Sparse variants of v3 features (top-256, above 70th/50th percentile) × thresholds × punishment. +for ds in top256 c70 c50; do DS=$ds THRS="200 600 1600" PUNS="0 0.012" JOBS=6 /mnt/c/knp/objects/grid_v3.sh; done diff --git a/objects/prepare_fresh_test.py b/objects/prepare_fresh_test.py new file mode 100644 index 0000000..0939c49 --- /dev/null +++ b/objects/prepare_fresh_test.py @@ -0,0 +1,17 @@ +"""Fresh cats/dogs test: CIFAR-10 JPEG mirror test 0700–0999 per class (600 images never used before). + +usage: ~/anvenv/bin/python prepare_fresh_test.py → /home/int/run/cats-dogs/data-v3/fresh_test.npz +""" +from pathlib import Path + +import numpy as np +from PIL import Image + +cache = Path('/home/int/run/cats-dogs/data/github-jpeg-cache') +items = [(label, cls, i) for cls, label in enumerate(('cat', 'dog')) for i in range(700, 1000)] +order = np.random.default_rng(20261015).permutation(len(items)) +items = [items[j] for j in order] +rgb = np.stack([np.asarray(Image.open(cache / f'test-{l}-{i:04d}.jpg').convert('RGB')).transpose(2, 0, 1) for l, _, i in items]) +np.savez_compressed('/home/int/run/cats-dogs/data-v3/fresh_test.npz', rgb=rgb, + labels=np.array([c for _, c, _ in items], np.uint8), names=np.array([f'test/{l}/{i:04d}' for l, _, i in items])) +print(rgb.shape) diff --git a/objects/spiking_pipeline.py b/objects/spiking_pipeline.py new file mode 100644 index 0000000..f518aee --- /dev/null +++ b/objects/spiking_pipeline.py @@ -0,0 +1,150 @@ +"""Exact step-by-step spiking simulation of the object pipeline: pixels → layer 1 → readout neuron. + +Pixels (3×32×32, value/255 per step, 10 active steps of 15) drive layer-1 neurons: one neuron per learned +prototype k and patch position (27×27), integer weights over its 6×6×3 patch. Patch centring, ZCA and the +prototype fold into one weight vector; the prototype offset and alpha become a per-step bias (the bias of a +neuron that also receives a constant input). A neuron adds its input each step, fires at `thr1`, resets to 0 +and never goes below 0 (AltAI). Its spikes go, with delay 1, to the readout neuron "dog − cat" with a weight +shared by prototype k and image quadrant (this replaces pooling neurons, which could fire only once a step). +The readout weights are trained off-chip (ridge) on layer-1 spike counts of the training images. + +usage: ~/anvenv/bin/python spiking_pipeline.py --model /home/int/run/cats-dogs/features-v3/kmeans_model_k800.npz +""" +import argparse +import json +import sys +import time +from pathlib import Path + +import numpy as np + +sys.path.insert(0, "/mnt/c/knp/objects") +sys.path.insert(0, "/mnt/c/knp/readout") +import kmeans_features as kf # noqa: E402 + +STEPS, ACTIVE, PATCH, SIDE = 15, 10, 6, 27 +WSCALE = 127 # layer-1 weights are integers in ±WSCALE + + +def fold(model, alpha): + """Float layer-1 weights [108, K] and bias [K] for raw patches in [0, 1]: act = p @ W + b.""" + n = PATCH * PATCH * 3 + centre = (np.eye(n) - 1 / n) * 4 # kf.normalise with CONTRAST_DIVIDE=False + w = centre @ model["zca"] @ model["d"].T + b = -(model["mean"] @ model["zca"] @ model["d"].T) - alpha + return w.astype(np.float32), b.astype(np.float32) + + +def quant_layer1(w, b): + s = WSCALE / np.abs(w).max() + # Over ACTIVE steps the pixel spikes sum to ~ACTIVE * p, so the bias is added on each active step. + return np.rint(w * s).astype(np.int32), np.rint(b * s).astype(np.int32), s + + +def raster(x): + """x uint8 [n, 3, 32, 32] → bool [ACTIVE, n, 3, 32, 32], incrementing converter from rest.""" + st = np.zeros(x.shape, np.float32) + inc = x.astype(np.float32) * np.float32(1 / 255.0) + out = np.zeros((ACTIVE,) + x.shape, bool) + for t in range(ACTIVE): + st += inc + out[t] = st >= 1.0 + st[out[t]] -= 1.0 + return out + + +def layer1_counts(x, wq, bq, thr1, batch=40, per_step=False): + """Spike counts of layer-1 neurons pooled per (quadrant, k): [n, 4 * K] (or [n, STEPS, 4 * K] with + per_step); also total spikes per image.""" + pos = range(SIDE) + edges = [0, 14, 27] + res, total = [], [] + for i in range(0, len(x), batch): + r = raster(x[i:i + batch]) + n = r.shape[1] + pot = np.zeros((n, SIDE * SIDE, wq.shape[1]), np.int32) + cnt = np.zeros_like(pot, dtype=np.int16) + steps = [] + for t in range(STEPS): + if t < ACTIVE: + p = kf.patches_at(r[t].astype(np.float32), pos, pos).astype(np.int8) + pot += (p.reshape(-1, p.shape[-1]).astype(np.float32) @ wq.astype(np.float32)).astype(np.int32).reshape(pot.shape) + bq + fired = pot >= thr1 + cnt += fired + pot[fired] = 0 + np.maximum(pot, 0, out=pot) + if per_step: + steps.append(pool(fired, edges)) + res.append(np.stack(steps, axis=1) if per_step else pool(cnt, edges)) + total.append(cnt.reshape(n, -1).sum(1)) + return np.concatenate(res), np.concatenate(total) + + +def pool(c, edges): + c = c.reshape(c.shape[0], SIDE, SIDE, -1) + return np.concatenate([c[:, edges[gy]:edges[gy + 1], edges[gx]:edges[gx + 1]].sum((1, 2)) + for gy in range(2) for gx in range(2)], axis=1).astype(np.int32) + + +def readout_neuron(steps, wq, bq, thr): + """Exact readout neuron: steps [n, STEPS, F] layer-1 spikes arrive with delay 1; integer weights wq [F] and a + bias input bq on every active step; fires at thr, resets to 0, never below 0. Returns 1 (dog) if it fired.""" + pot = np.zeros(len(steps), np.int64) + fired_any = np.zeros(len(steps), bool) + for t in range(1, STEPS): + pot += steps[:, t - 1] @ wq + (bq if t - 1 < ACTIVE else 0) + f = pot >= thr + fired_any |= f + pot[f] = 0 + np.maximum(pot, 0, out=pot) + return fired_any.astype(int) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--model", required=True) + ap.add_argument("--data", default="/home/int/run/cats-dogs/data-v3") + ap.add_argument("--alpha", type=float, default=0.25) + 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) + a = ap.parse_args() + model = dict(np.load(a.model)) + w, b = fold(model, a.alpha) + wq, bq, s = quant_layer1(w, b) + tr, va = np.load(f"{a.data}/train.npz"), np.load(f"{a.data}/val.npz") + xt = tr["rgb"] if not a.train_limit else tr["rgb"][:a.train_limit] + yt = tr["labels"][:len(xt)].astype(int) + if not a.thr1: + # Typical positive per-step drive of a layer-1 neuron on training patches. + p = kf.patches_at(xt[:200].astype(np.float32) / 255, range(SIDE), range(SIDE)).reshape(-1, 108) + act = p @ w + b + a.thr1 = float(np.percentile(act[act > 0], 99) * s) + print("layer-1 threshold", a.thr1, "weight scale", round(s, 2), flush=True) + t0 = time.time() + ct, tt = layer1_counts(np.concatenate([xt, xt[:, :, :, ::-1]]), wq, bq, a.thr1) + sv, tv = layer1_counts(va["rgb"], wq, bq, a.thr1, per_step=True) + cv = sv.sum(1) + print("simulated", len(ct) + len(cv), "images in", round(time.time() - t0), "s; layer-1 spikes per image", + int(tv.mean()), flush=True) + yy = np.r_[yt, yt] + np.savez(a.out, ct=ct, sv=sv, yt=yy, yv=va["labels"], thr1=a.thr1, wq=wq, bq=bq) + f = lambda c: np.hstack([c.astype(np.float32), np.ones((len(c), 1), np.float32)]) + res = {} + for lam in [1e3, 1e4, 1e5, 1e6]: + sol = np.linalg.solve(f(ct).T @ f(ct) + lam * np.eye(ct.shape[1] + 1), f(ct).T @ (yy * 2 - 1.0)) + acc = 100 * (((f(cv) @ sol) > 0) == va["labels"]).mean() + rq = np.rint(sol / np.abs(sol).max() * 255).astype(np.int64) + # The readout bias input fires on each of the ACTIVE steps, so its weight is spread over them. + bias = int(np.rint(rq[-1] / ACTIVE)) + # Readout threshold relative to the typical size of its per-step input. + unit = float(np.median(np.abs(sv[:, :ACTIVE].reshape(-1, sv.shape[-1]) @ rq[:-1] + bias))) + 1 + spk = {q: 100 * (readout_neuron(sv, rq[:-1], bias, q * unit) == va["labels"]).mean() for q in (0.5, 1, 2, 4, 8)} + res[lam] = {"linear": acc, "spiking": spk, "unit": unit} + print(f"lam {lam:g}: linear on exact layer-1 counts {acc:.1f}%; spiking readout by threshold " + + ", ".join(f"{q}×: {v:.1f}%" for q, v in spk.items()) + f" (unit {unit:.0f})", flush=True) + Path(a.out).with_suffix(".json").write_text(json.dumps({"thr1": a.thr1, "acc": res}, indent=1)) + + +if __name__ == "__main__": + main()