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:
parent
cf386f4e36
commit
52d91e974f
7 changed files with 130 additions and 4 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
11
objects/grid_v3.sh
Normal 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"'
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
51
objects/make_knp_features.py
Normal file
51
objects/make_knp_features.py
Normal 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()
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue