KNP_COL_PUNISH: dopamine punishment of wrong columns; learned k-means features for objects; extra data fetcher

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
int 2026-10-14 21:06:28 +03:00
parent b8ea9dc6ce
commit 795f75a65b
4 changed files with 142 additions and 0 deletions

12
augment/punish_runs.sh Normal file
View file

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

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_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_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
{
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<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.
DeltaSynapseParams target_to_input_synapse_excitatory;
target_to_input_synapse_excitatory.output_type_ = knp::synapse_traits::OutputType::EXCITATORY;

40
objects/fetch_more.py Normal file
View file

@ -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)

View file

@ -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, <patch, d_k> - 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)