Exact spiking pipeline simulator, fresh test set, night log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
52d91e974f
commit
8f126bc29a
6 changed files with 222 additions and 3 deletions
|
|
@ -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 × медиана входа за шаг).
|
||||
|
|
|
|||
40
objects/fetch_fresh_test.py
Normal file
40
objects/fetch_fresh_test.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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"'
|
||||
|
|
|
|||
3
objects/grid_v3_sparse.sh
Normal file
3
objects/grid_v3_sparse.sh
Normal file
|
|
@ -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
|
||||
17
objects/prepare_fresh_test.py
Normal file
17
objects/prepare_fresh_test.py
Normal file
|
|
@ -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)
|
||||
150
objects/spiking_pipeline.py
Normal file
150
objects/spiking_pipeline.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue