From 795f75a65ba0c30173e597369c9c8ca7277d6084 Mon Sep 17 00:00:00 2001 From: int Date: Wed, 14 Oct 2026 21:06:28 +0300 Subject: [PATCH] KNP_COL_PUNISH: dopamine punishment of wrong columns; learned k-means features for objects; extra data fetcher Co-Authored-By: Claude Opus 5.5 --- augment/punish_runs.sh | 12 ++++ .../models/altai/construct_network.cpp | 24 +++++++ objects/fetch_more.py | 40 +++++++++++ objects/kmeans_features.py | 66 +++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 augment/punish_runs.sh create mode 100644 objects/fetch_more.py create mode 100644 objects/kmeans_features.py diff --git a/augment/punish_runs.sh b/augment/punish_runs.sh new file mode 100644 index 0000000..4360f73 --- /dev/null +++ b/augment/punish_runs.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# 2026-10-14 night: dopamine punishment of wrong columns (KNP_COL_PUNISH) on coarse stroke features, 30k train, validation. +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 + 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 +done +wait diff --git a/examples/examples/mnist-learn/models/altai/construct_network.cpp b/examples/examples/mnist-learn/models/altai/construct_network.cpp index e380305..81302ce 100644 --- a/examples/examples/mnist-learn/models/altai/construct_network.cpp +++ b/examples/examples/mnist-learn/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/objects/fetch_more.py b/objects/fetch_more.py new file mode 100644 index 0000000..c9420bd --- /dev/null +++ b/objects/fetch_more.py @@ -0,0 +1,40 @@ +"""Download CIFAR-10 JPEG mirror train images 2000–4499 for cat and dog (unused by v1/v2) into the same cache. + +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'train-{label}-{index:04d}.jpg' + if path.exists(): + return 0 + for attempt in range(6): + try: + with urllib.request.urlopen(f'{BASE}/train/{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(2000, 4500)] +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/kmeans_features.py b/objects/kmeans_features.py new file mode 100644 index 0000000..910a97f --- /dev/null +++ b/objects/kmeans_features.py @@ -0,0 +1,66 @@ +"""Learned local features for small colour images (Coates & Ng 2011 style), prototype for a spiking layer. + +Layer 1 is learned without labels: random 6×6×3 patches are contrast-normalised and ZCA-whitened, then +spherical k-means finds K prototypes. This is the same rule as winner-take-all competitive Hebbian learning: +the closest prototype moves towards the patch. Activation of prototype k at a position is +max(0, - alpha) (a neuron with threshold alpha). Activations are sum-pooled over a grid of +regions, which gives tolerance to position. + +API: learn(images) -> model dict; encode(images, model) -> float32 features [n, K * grid * grid]. +images: uint8 [n, 3, 32, 32]. +""" +import numpy as np + +PATCH = 6 + + +def patches_at(img, ys, xs): + """img [n,3,32,32] float32 → patches [n, len(ys)*len(xs), 108].""" + out = np.stack([img[:, :, y:y + PATCH, x:x + PATCH].reshape(len(img), -1) for y in ys for x in xs], axis=1) + return out + + +def normalise(p): + p = p - p.mean(-1, keepdims=True) + return p / np.sqrt(p.var(-1, keepdims=True) + 10 / 255) + + +def learn(images, k=400, n_patches=200000, iters=15, seed=0): + rng = np.random.default_rng(seed) + x = images.astype(np.float32) / 255 + idx = rng.integers(0, len(x), n_patches) + ys, xs = rng.integers(0, 32 - PATCH + 1, n_patches), rng.integers(0, 32 - PATCH + 1, n_patches) + p = np.stack([x[i, :, y:y + PATCH, xx:xx + PATCH].reshape(-1) for i, y, xx in zip(idx, ys, xs)]) + p = normalise(p) + mean = p.mean(0) + cov = np.cov(p - mean, rowvar=False) + e, v = np.linalg.eigh(cov) + zca = (v * (1 / np.sqrt(e + 0.1))) @ v.T + pw = (p - mean) @ zca + d = rng.normal(size=(k, pw.shape[1])).astype(np.float32) + d /= np.linalg.norm(d, axis=1, keepdims=True) + for _ in range(iters): + s = pw @ d.T + win = s.argmax(1) + new = np.zeros_like(d) + np.add.at(new, win, pw * s[np.arange(len(pw)), win, None]) + empty = np.linalg.norm(new, axis=1) < 1e-6 + new[empty] = rng.normal(size=(empty.sum(), d.shape[1])) + d = new / np.linalg.norm(new, axis=1, keepdims=True) + return {"mean": mean.astype(np.float32), "zca": zca.astype(np.float32), "d": d.astype(np.float32)} + + +def encode(images, model, alpha=0.25, grid=2, batch=250): + x = images.astype(np.float32) / 255 + pos = list(range(0, 32 - PATCH + 1)) + side = len(pos) + edges = np.linspace(0, side, grid + 1).astype(int) + out = [] + for i in range(0, len(x), batch): + p = normalise(patches_at(x[i:i + batch], pos, pos)) + a = np.maximum(((p - model["mean"]) @ model["zca"]) @ model["d"].T - alpha, 0) + a = a.reshape(len(p), side, side, -1) + pooled = [a[:, edges[gy]:edges[gy + 1], edges[gx]:edges[gx + 1]].sum((1, 2)) + for gy in range(grid) for gx in range(grid)] + out.append(np.concatenate(pooled, axis=1)) + return np.concatenate(out)