diff --git a/altai-objects/altai_infer.cpp b/altai-objects/altai_infer.cpp new file mode 100644 index 0000000..e135647 --- /dev/null +++ b/altai-objects/altai_infer.cpp @@ -0,0 +1,223 @@ +// Object recognition inference entirely in KNP on AltAI neurons: pixels → feature detectors → class neurons. +// +// altai_infer +// +// images.bin: uint8 images 3×32×32. Pixels spike with the incrementing converter (value/255 per step, 10 active +// steps of 15). model_dir (written by build_model.py / train_readout.py): +// meta.txt K stride det_threshold classes out_threshold +// det_w.bin int32 [K][108] detector weights on a 6×6×3 pixel patch (channel, y, x) +// det_b.bin int32 [K] detector bias, delivered on every active step by an always-on input +// readout_w.bin int32 [classes][4·K] (optional) class weights per (quadrant, detector type); index q·K + k +// readout_b.bin int32 [classes] (optional) class bias per active step +// Detectors: one AltAI neuron per detector type and position (stride), threshold det_threshold, reset to 0, the +// potential does not go below 0. Class neurons: AltAI, threshold out_threshold. +// Output: _classes.csv (index, spike count per class) if a readout is given, and +// _counts.bin (int32 [count][4·K], detector spikes per quadrant and type) for training the readout. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +using DeltaProjection = knp::core::Projection; +using AltAIPopulation = knp::core::Population; + +constexpr int side = 32, patch = 6, channels = 3, steps_per_image = 15, active_steps = 10; +constexpr int fan_in = patch * patch * channels, in_n = channels * side * side; + + +template +std::vector read_vec(const std::filesystem::path &path) +{ + std::ifstream f(path, std::ios::binary); + if (!f) return {}; + std::vector raw((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + std::vector out(raw.size() / sizeof(T)); + std::memcpy(out.data(), raw.data(), out.size() * sizeof(T)); + return out; +} + + +knp::neuron_traits::neuron_parameters neuron(int threshold) +{ + knp::neuron_traits::neuron_parameters p; + p.activation_threshold_ = static_cast(threshold); + p.negative_activation_threshold_ = 0; + p.potential_reset_value_ = 0; + p.potential_leak_ = 0; + return p; +} + + +int main(int argc, const char *const argv[]) +{ + if (argc != 7) + { + std::cerr << "usage: altai_infer \n"; + return 1; + } + const std::filesystem::path model_dir = argv[4]; + const size_t first = std::stoul(argv[2]), count = std::stoul(argv[3]); + const std::string out_prefix = argv[6]; + int k = 0, stride = 0, det_thr = 0, classes = 0, out_thr = 0; + { + std::ifstream meta(model_dir / "meta.txt"); + meta >> k >> stride >> det_thr >> classes >> out_thr; + } + const int per_side = (side - patch) / stride + 1, positions = per_side * per_side, half = per_side / 2; + const size_t det_n = static_cast(k) * positions; + const auto det_w = read_vec(model_dir / "det_w.bin"), det_b = read_vec(model_dir / "det_b.bin"); + const auto ro_w = read_vec(model_dir / "readout_w.bin"), ro_b = read_vec(model_dir / "readout_b.bin"); + const bool readout = !ro_w.empty(); + std::ifstream images(argv[1], std::ios::binary); + std::vector px(count * in_n); + images.seekg(static_cast(first * in_n)); + images.read(reinterpret_cast(px.data()), static_cast(px.size())); + + auto quadrant = [&](size_t pos) { return (pos / per_side >= half) * 2 + (pos % per_side >= half); }; + + AltAIPopulation detectors([&](size_t) { return neuron(det_thr); }, det_n); + AltAIPopulation classes_pop([&](size_t) { return neuron(out_thr); }, readout ? classes : 0); + // Detector index = type * positions + position; synapse index = detector * fan_in + patch element. + DeltaProjection pixel_proj( + knp::core::UID{false}, detectors.get_uid(), + [&](size_t index) -> std::optional + { + const size_t d = index / fan_in, e = index % fan_in, j = d / positions, pos = d % positions; + const int32_t w = det_w[j * fan_in + e]; + if (w == 0) return std::nullopt; + const size_t c = e / (patch * patch), dy = (e % (patch * patch)) / patch, dx = e % patch; + const size_t y = (pos / per_side) * stride + dy, x = (pos % per_side) * stride + dx; + return DeltaProjection::Synapse{ + {static_cast(w), 1, knp::synapse_traits::OutputType::EXCITATORY}, c * side * side + y * side + x, + d}; + }, + det_n * fan_in); + // Always-on input 0 → every detector (its bias) and every class neuron (its bias). + DeltaProjection bias_det( + knp::core::UID{false}, detectors.get_uid(), + [&](size_t d) -> std::optional + { + const int32_t b = det_b[d / positions]; + if (b == 0) return std::nullopt; + return DeltaProjection::Synapse{{static_cast(b), 1, knp::synapse_traits::OutputType::EXCITATORY}, 0, d}; + }, + det_n); + DeltaProjection readout_proj( + detectors.get_uid(), classes_pop.get_uid(), + [&](size_t index) -> std::optional + { + const size_t c = index / det_n, d = index % det_n, j = d / positions, pos = d % positions; + const int32_t w = ro_w[c * 4 * k + quadrant(pos) * k + j]; + if (w == 0) return std::nullopt; + return DeltaProjection::Synapse{{static_cast(w), 1, knp::synapse_traits::OutputType::EXCITATORY}, d, c}; + }, + readout ? det_n * classes : 0); + // The class bias arrives one step later than the detector bias, in step with the detector spikes. + DeltaProjection bias_cls( + knp::core::UID{false}, classes_pop.get_uid(), + [&](size_t c) -> std::optional + { + if (ro_b[c] == 0) return std::nullopt; + return DeltaProjection::Synapse{ + {static_cast(ro_b[c]), 2, knp::synapse_traits::OutputType::EXCITATORY}, 0, c}; + }, + readout ? classes : 0); + + const auto pixel_uid = pixel_proj.get_uid(), bias_det_uid = bias_det.get_uid(), bias_cls_uid = bias_cls.get_uid(), + det_uid = detectors.get_uid(), cls_uid = classes_pop.get_uid(); + knp::framework::Network network; + network.add_population(std::move(detectors)); + network.add_projection(std::move(pixel_proj)); + network.add_projection(std::move(bias_det)); + if (readout) + { + network.add_population(std::move(classes_pop)); + network.add_projection(std::move(readout_proj)); + network.add_projection(std::move(bias_cls)); + } + knp::framework::Model model(std::move(network)); + knp::core::UID pixel_channel, bias_channel, det_channel, cls_channel; + model.add_input_channel(pixel_channel, pixel_uid); + model.add_input_channel(bias_channel, bias_det_uid); + if (readout) model.add_input_channel(bias_channel, bias_cls_uid); + model.add_output_channel(det_channel, det_uid); + if (readout) model.add_output_channel(cls_channel, cls_uid); + + std::vector current(active_steps); + auto pixel_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData + { + const size_t img = step / steps_per_image, local = step % steps_per_image; + if (local == 0) + { + std::vector st(in_n, 0.F); + for (auto &s : current) s.clear(); + const uint8_t *p = px.data() + img * in_n; + for (int t = 0; t < active_steps; ++t) + for (int i = 0; i < in_n; ++i) + { + st[i] += p[i] / 255.F; + if (st[i] >= 1.F) + { + st[i] -= 1.F; + current[t].push_back(static_cast(i)); + } + } + } + if (local >= active_steps) return {}; + return current[local]; + }; + auto bias_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData + { + if (step % steps_per_image >= active_steps) return {}; + return {0}; + }; + + knp::framework::BackendLoader loader; + knp::framework::ModelExecutor executor( + model, loader.load(argv[5]), {{pixel_channel, pixel_gen}, {bias_channel, bias_gen}}); + auto &det_out = executor.get_loader().get_output_channel(det_channel); + std::vector counts(count * 4 * k, 0); + std::vector class_counts(count * std::max(classes, 1), 0); + const auto t0 = std::chrono::steady_clock::now(); + executor.start([&](size_t step) { return step < count * steps_per_image; }); + for (const auto &m : det_out.update()) + { + const size_t im = m.header_.send_time_ / steps_per_image; + if (im >= count) continue; + for (auto d : m.neuron_indexes_) ++counts[(im * 4 + quadrant(d % positions)) * k + d / positions]; + } + if (readout) + for (const auto &m : executor.get_loader().get_output_channel(cls_channel).update()) + { + const size_t im = m.header_.send_time_ / steps_per_image; + if (im < count) + for (auto c : m.neuron_indexes_) ++class_counts[im * classes + c]; + } + std::ofstream(out_prefix + "_counts.bin", std::ios::binary) + .write(reinterpret_cast(counts.data()), static_cast(counts.size() * 4)); + if (readout) + { + std::ofstream csv(out_prefix + "_classes.csv"); + csv << "index"; + for (int c = 0; c < classes; ++c) csv << ",c" << c; + csv << "\n"; + for (size_t i = 0; i < count; ++i) + { + csv << first + i; + for (int c = 0; c < classes; ++c) csv << ',' << class_counts[i * classes + c]; + csv << "\n"; + } + } + std::cout << "images " << first << ".." << first + count - 1 << ", " + << std::chrono::duration(std::chrono::steady_clock::now() - t0).count() << " s" << std::endl; +} diff --git a/altai-objects/altai_objects.cpp b/altai-objects/altai_objects.cpp index e8feaa0..59142ee 100644 --- a/altai-objects/altai_objects.cpp +++ b/altai-objects/altai_objects.cpp @@ -16,7 +16,8 @@ // (index,label,spike counts per class). // // Environment: KNP_RETINA_THR (default 250), KNP_DET_THR (detector threshold, default 2000), -// KNP_OUT_THR (readout threshold, default 4000), KNP_INHIB (detector lateral inhibition, default 5000). +// KNP_OUT_THR (readout threshold, default 4000), KNP_INHIB (detector lateral inhibition, default 5000), +// KNP_SUMTHR (features: threshold increase per unit of positive weight sum, default 0). #include #include @@ -211,7 +212,20 @@ int features_mode(int argc, const char *const argv[]) if (px.size() < count * in_n) throw std::runtime_error("not enough patches"); AltAIPopulation retina([&](size_t) { return plain_neuron(env_f("KNP_RETINA_THR", 250)); }, ret_n); - StdpPopulation detectors([&](size_t) { return learning_neuron(env_f("KNP_DET_THR", 2000)); }, k); + StdpPopulation detectors( + [&](size_t) + { + auto p = learning_neuron(env_f("KNP_DET_THR", 2000)); + // AltAI homeostasis: the threshold grows with the sum of positive weights (KNP_SUMTHR × sum), which + // works like weight normalisation, so a detector wins by pattern match rather than by weight mass. + p.synapse_sum_threshold_coefficient_ = env_f("KNP_SUMTHR", 0); + // Lasting inhibition: the potential may go down to -KNP_DET_NEG, and a leak of KNP_DET_LEAK per step + // brings it back towards 0 (in the 5 silent steps between crops). + p.negative_activation_threshold_ = static_cast(env_f("KNP_DET_NEG", 0)); + p.potential_leak_ = static_cast(env_f("KNP_DET_LEAK", 0)); + return p; + }, + k); auto retina_proj = make_retina(retina.get_uid(), crop, crop, patch, patch, dog_r); std::mt19937 rng(static_cast(env_f("KNP_SEED", 1))); @@ -248,17 +262,42 @@ int features_mode(int argc, const char *const argv[]) }, k * k); + // Self-inhibition after a spike (KNP_SELF_INHIB), so a winner fires about once per crop. + const float self_inhib = env_f("KNP_SELF_INHIB", 0); + DeltaProjection self_proj( + detectors.get_uid(), detectors.get_uid(), + [&](size_t i) { + return std::optional(DeltaProjection::Synapse{ + {-self_inhib, 1, knp::synapse_traits::OutputType::EXCITATORY}, i, i}); + }, + self_inhib > 0 ? k : 0); + // Imprinting (KNP_IMPRINT=1): crop number c teaches detector c mod K through a dopamine channel on every active + // step; with KNP_DELTA_WDECAY the detector's weights settle at the retina activity of that crop. + const bool imprint = env_f("KNP_IMPRINT", 0) > 0; + DeltaProjection teacher( + knp::core::UID{false}, detectors.get_uid(), + [&](size_t i) { + return std::optional(DeltaProjection::Synapse{ + {env_f("KNP_TEACH", 1), 1, knp::synapse_traits::OutputType::DOPAMINE}, i, i}); + }, + k); + const auto teacher_uid = teacher.get_uid(); const auto retina_uid = retina_proj.get_uid(), learn_uid = learn_proj.get_uid(), det_uid = detectors.get_uid(); knp::framework::Network network; network.add_population(std::move(retina)); network.add_population(std::move(detectors)); network.add_projection(std::move(retina_proj)); network.add_projection(std::move(learn_proj)); - network.add_projection(std::move(lateral)); - knp::core::UID in_channel, out_channel; + if (imprint) + network.add_projection(std::move(teacher)); + else + network.add_projection(std::move(lateral)); + if (self_inhib > 0 && !imprint) network.add_projection(std::move(self_proj)); + knp::core::UID in_channel, out_channel, teach_channel; knp::framework::Model model(std::move(network)); model.add_input_channel(in_channel, retina_uid); model.add_output_channel(out_channel, det_uid); + if (imprint) model.add_input_channel(teach_channel, teacher_uid); std::vector current; auto input_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData @@ -270,7 +309,14 @@ int features_mode(int argc, const char *const argv[]) }; knp::framework::BackendLoader loader; auto backend = loader.load(backend_path); - knp::framework::ModelExecutor executor(model, backend, {{in_channel, input_gen}}); + auto teach_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData + { + if (step % steps_per_image >= active_steps) return {}; + return {static_cast((step / steps_per_image) % k)}; + }; + knp::framework::ModelLoader::InputChannelMap channels{{in_channel, input_gen}}; + if (imprint) channels.insert({teach_channel, teach_gen}); + knp::framework::ModelExecutor executor(model, backend, std::move(channels)); auto &out = executor.get_loader().get_output_channel(out_channel); const auto t0 = std::chrono::steady_clock::now(); executor.start([&](size_t step) { return step < count * steps_per_image; }); diff --git a/altai-objects/build_model.py b/altai-objects/build_model.py new file mode 100644 index 0000000..32f4474 --- /dev/null +++ b/altai-objects/build_model.py @@ -0,0 +1,42 @@ +"""Train the feature detectors offline and write integer weights for altai_infer (inference runs in KNP). + +k-means on 6×6×3 patches of the vehicles training images (patch centring and ZCA, no contrast division), then +centring, ZCA and the prototype are folded into one weight vector per detector type on raw pixels, the offset +and alpha into a per-step bias (objects/spiking_pipeline.py); weights are scaled to integers in ±wscale. +usage: ~/anvenv/bin/python build_model.py [--k 200 --stride 2 --alpha 0.25 --wscale 127] +""" +import argparse +import sys +from pathlib import Path + +import numpy as np + +sys.path.insert(0, "/mnt/c/knp/objects") +import kmeans_features as kf # noqa: E402 +import spiking_pipeline as sp # noqa: E402 + +ap = argparse.ArgumentParser() +ap.add_argument("out") +ap.add_argument("--k", type=int, default=200) +ap.add_argument("--stride", type=int, default=2) +ap.add_argument("--alpha", type=float, default=0.25) +ap.add_argument("--wscale", type=float, default=127) +ap.add_argument("--classes", type=int, default=4) +ap.add_argument("--data", default="/home/int/run/cifar/vehicles") +a = ap.parse_args() +out = Path(a.out) +out.mkdir(parents=True, exist_ok=True) +kf.CONTRAST_DIVIDE = False +xt = np.load(f"{a.data}/train.npz")["rgb"] +model = kf.learn(xt, k=a.k) +w, b = sp.fold(model, a.alpha) # [108, K], [K] for patches in [0, 1] +scale = a.wscale / np.abs(w).max() +wq, bq = np.rint(w * scale).astype(np.int32), np.rint(b * scale).astype(np.int32) +p = kf.patches_at(xt[:300].astype(np.float32) / 255, range(0, 27, a.stride), range(0, 27, a.stride)).reshape(-1, 108) +act = p @ w + b +thr = int(round(float(np.percentile(act[act > 0], 99)) * scale)) +wq.T.copy().tofile(out / "det_w.bin") +bq.tofile(out / "det_b.bin") +(out / "meta.txt").write_text(f"{a.k} {a.stride} {thr} {a.classes} 0\n") +np.savez(out / "kmeans.npz", **model) +print(f"K {a.k} stride {a.stride}: weight scale {scale:.2f}, detector threshold {thr}, bias range {bq.min()}..{bq.max()}") diff --git a/altai-objects/feature_grid.sh b/altai-objects/feature_grid.sh index 6fbe34c..dfadb55 100644 --- a/altai-objects/feature_grid.sh +++ b/altai-objects/feature_grid.sh @@ -3,7 +3,7 @@ cd ~/run/altai-objects/fgrid export LD_LIBRARY_PATH=$HOME/knp-build/bin:$HOME/deps/lib one() { n=$1; [ -f $n.score ] && return - env KNP_DELTA_RULE=$2 KNP_DELTA_DECAY=$3 KNP_DELTA_LAG=1 KNP_DET_THR=$4 KNP_INHIB=$5 \ + env KNP_DELTA_RULE=$2 KNP_DELTA_DECAY=$3 KNP_DELTA_WDECAY=${7:-0} KNP_DELTA_LAG=1 KNP_DET_THR=$4 KNP_INHIB=$5 KNP_SUMTHR=${6:-0} \ ../altai_objects features /home/int/run/cifar/vehicles/knp_crops.bin ${NPATCH:-20000} 64 $n.bin \ $HOME/run/delta-cpu/libknp-cpu-delta-backend.so 2>&1 | grep "^features" > $n.log ~/anvenv/bin/python /mnt/c/knp/altai-objects/score_features.py $n.bin 64 > $n.score 2>&1; } diff --git a/altai-objects/run_counts.sh b/altai-objects/run_counts.sh new file mode 100644 index 0000000..15a15c0 --- /dev/null +++ b/altai-objects/run_counts.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Collect detector spike counts from KNP for readout training: run_counts.sh [chunk] +M=$1; O=$2; C=${3:-1000} +D=/home/int/run/cifar/vehicles +export LD_LIBRARY_PATH=$HOME/knp-build/bin:$HOME/deps/lib +one() { f=$1; first=$2; n=$3; tag=$4; [ -f $O/${tag}_$first.done ] && return + ~/run/altai-objects/altai_infer $D/${f}_images.bin $first $n $M $HOME/knp-build/bin/libknp-cpu-single-threaded-backend.so $O/${tag}_$first > $O/${tag}_$first.log 2>&1 && touch $O/${tag}_$first.done; } +export -f one; export M O D +{ for s in train train_flip; do for i in $(seq 0 $C 15999); do echo "$s $i $C $s"; done; done + for i in $(seq 0 $C 3999); do echo "val $i $C val"; done; } | xargs -P ${JOBS:-14} -L 1 bash -c 'one "$@"' _ diff --git a/altai-objects/train_readout.py b/altai-objects/train_readout.py new file mode 100644 index 0000000..b80b731 --- /dev/null +++ b/altai-objects/train_readout.py @@ -0,0 +1,65 @@ +"""Train the class neurons offline on detector spike counts produced by KNP (altai_infer), write integer weights. + +Ridge regression (one-vs-rest, targets ±1) on per-(quadrant, detector type) spike counts of the training images +(+ mirrored copies). Standardisation is folded into the weights and bias; weights are scaled to integers in ±255, +the bias is spread over the active steps. The class-neuron threshold is chosen on the training counts with a rate +approximation (spikes ≈ positive drive / threshold) so that the most-spiking class matches the ridge decision. +usage: ~/anvenv/bin/python train_readout.py [--lam 1000] +""" +import argparse +from pathlib import Path + +import numpy as np + +ACTIVE = 10 +ap = argparse.ArgumentParser() +ap.add_argument("model") +ap.add_argument("counts") +ap.add_argument("--lam", type=float, default=1000) +ap.add_argument("--data", default="/home/int/run/cifar/vehicles") +a = ap.parse_args() +model, cdir = Path(a.model), Path(a.counts) +k, stride, det_thr, classes, _ = (int(v) for v in (model / "meta.txt").read_text().split()) + + +def load(tag, n): + parts = sorted(cdir.glob(f"{tag}_*_counts.bin"), key=lambda p: int(p.name.split("_")[-2])) + x = np.concatenate([np.fromfile(p, np.int32).reshape(-1, 4 * k) for p in parts]).astype(np.float32) + assert len(x) == n, (tag, len(x), n) + return x + + +yt = np.array(open(f"{a.data}/train_labels.txt").read().split(), int) +yv = np.array(open(f"{a.data}/val_labels.txt").read().split(), int) +xt = np.vstack([load("train", len(yt)), load("train_flip", len(yt))]) +yt2 = np.r_[yt, yt] +xv = load("val", len(yv)) +mu, sd = xt.mean(0), xt.std(0) + 1e-3 +f = lambda z: np.hstack([(z - mu) / sd, np.ones((len(z), 1), np.float32)]) +T = np.eye(classes)[yt2] * 2 - 1 +for lam in sorted({a.lam, 100.0, 10000.0}): + sol = np.linalg.solve(f(xt).T @ f(xt) + lam * np.eye(4 * k + 1), f(xt).T @ T) + print(f"lam {lam:g}: ridge on KNP counts, validation {100 * ((f(xv) @ sol).argmax(1) == yv).mean():.2f}%") +sol = np.linalg.solve(f(xt).T @ f(xt) + a.lam * np.eye(4 * k + 1), f(xt).T @ T) +w = sol[:-1] / sd[:, None] # on raw counts +b = sol[-1] - (mu / sd) @ sol[:-1] +s = 255 / np.abs(w).max() +wq = np.rint(w * s).astype(np.int32) # [4K, classes] +bq_total = b * s +bq = np.rint(bq_total / ACTIVE).astype(np.int32) +drive_t = xt @ wq + bq * ACTIVE # total drive per image and class +best = None +# A class neuron fires at most once a step (about ACTIVE + 1 spikes per image), so the threshold is a fraction +# of the typical positive per-step drive; spikes are capped accordingly. +unit = float(np.percentile(drive_t[drive_t > 0], 50)) / ACTIVE +for q in (0.1, 0.2, 0.3, 0.5, 0.8): + thr = int(min(max(unit * q, 1), 65535)) + spikes = np.minimum(np.floor(np.maximum(drive_t, 0) / thr), ACTIVE + 1) + acc = 100 * (spikes.argmax(1) == yt2).mean() + print(f"threshold {thr}: train accuracy (rate approximation) {acc:.2f}%, max spikes {spikes.max():.0f}") + if best is None or acc > best[0]: + best = (acc, thr) +wq.T.copy().tofile(model / "readout_w.bin") +bq.tofile(model / "readout_b.bin") +(model / "meta.txt").write_text(f"{k} {stride} {det_thr} {classes} {best[1]}\n") +print("written", model, "threshold", best[1]) diff --git a/changes-2026-10-15.md b/changes-2026-10-15.md index e550d90..eccfefa 100644 --- a/changes-2026-10-15.md +++ b/changes-2026-10-15.md @@ -63,3 +63,11 @@ MNIST, признаки coarse, 30 000 обучение, 10 000 проверка - `altai-objects/altai_objects.cpp` (сборка `altai-objects/build.py`): режим `features` — кусочки 12×12 → сетчатка (216 нейронов AltAI, фиксированные веса DoG×1000) → K детекторов STDP-AltAI, взаимное торможение и дофамин от собственного спайка; режим `run` — полная сеть пиксели → сетчатка 6×32×32 → детекторы K×196 (выученные веса минус средний, целые) → выход (дофамин по ошибке), затем веса читаются из бэкенда и фиксируются для проверки. - Бэкенд: `KNP_DELTA_DECAY=d` — у нейрона с дофамином D неактивные синапсы меняются на −скорость·D·d (соревновательное обучение). - `altai-objects/export_knp.py` (ошибка: многократное чтение npz съело 31 ГБ — исправлено), `score_features.py` — быстрая оценка узоров (только для настройки). Ориентиры (K=64, 6000 обучение): узоры WTA в numpy 68,0%, случайные веса 62,2%; первая проба KNP (5000 кусочков, убавка 0,2) — 60,9%. Сетчатка: в среднем 10% входов спайкают за шаг. + +### Обучение узоров в KNP (до уточнения пользователя) и смена курса +- Соревновательное обучение узоров в KNP (`features`, `altai-objects/feature_grid.sh`, оценка `score_features.py`, K=64, ориентиры: WTA в numpy 68,0%, случайные 62,2%): постоянная убавка (`KNP_DELTA_DECAY`) — 64–65,6% при любых скорости, пороге, торможении (26 вариантов); нормировка порогом AltAI (`KNP_SUMTHR`) — без изменений; сильнее обучение — детекторы замолкают; убавка, пропорциональная весу (`KNP_DELTA_WDECAY`), — 54,8% и очень медленно (55–98 мин на 100 000 кусочков, прогоны остановлены). Длительное торможение (`KNP_DET_NEG`, `KNP_DET_LEAK`, `KNP_SELF_INHIB`): утечка AltAI отводит отрицательный потенциал дальше от нуля при положительном значении; при отрицательном — слишком много спайков. +- Запечатление (`KNP_IMPRINT=1`: кусочек c учит детектор c через канал дофамина; numpy: K=200 — 71,4% против 73,0% у WTA): в KNP K=200 — 69,6–70,9% (лучшее 70,9%: 10 повторов, скорость 100). +- **Уточнение пользователя: важен инференс на Алтае, обучение может быть вне платформы.** Курс: обучение обычным кодом, распознавание целиком в KNP. +- `altai-objects/altai_infer.cpp`: распознавание в KNP — пиксели (спайки) → детекторы K × позиции (целые веса на 6×6×3 пикселей, смещение через постоянно активный вход) → нейроны классов (веса на (четверть, тип детектора), смещение с задержкой 2). Пишет числа спайков детекторов по четвертям (для обучения выхода) и спайки классов. +- `altai-objects/build_model.py`: k-средние без деления на контраст на кусочках транспорта, свёртка центровки/ZCA/узора в целые веса ±127, порог — 99-й перцентиль. K=200 шаг 2: масштаб 37,6, порог детектора 135, смещения −13…−5. KNP: 0,8 с/картинку, 1,5 ГБ, ~36 тыс. спайков детекторов на картинку. +- `altai-objects/run_counts.sh`: параллельный сбор чисел спайков из KNP (train, train_flip, val; 14 процессов). `altai-objects/train_readout.py`: ridge на этих числах → целые веса классов ±255, смещение по шагам, порог по обучающей выборке. diff --git a/readout/build_delta_cpu.py b/readout/build_delta_cpu.py index c1af385..005a02e 100644 --- a/readout/build_delta_cpu.py +++ b/readout/build_delta_cpu.py @@ -38,6 +38,14 @@ s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=), const char *value = std::getenv("KNP_DELTA_DECAY"); return value ? std::stof(value) : 0.F; }(); + // Competitive rule of k-means type: with KNP_DELTA_WDECAY=, every synapse of the neuron also changes by + // -rate * D * c * weight, so with dopamine after the neuron's own spikes the weight settles where + // c * weight equals the probability that its input was active (dw ~ input - c * w). + static const float delta_wdecay = [] + { + const char *value = std::getenv("KNP_DELTA_WDECAY"); + return value ? std::stof(value) : 0.F; + }(); static const float delta_rate = [] { const char *value = std::getenv("KNP_DELTA_RULE"); @@ -63,6 +71,9 @@ s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=), synapse.get().rule_.synaptic_resource_ += delta_rate * neuron.dopamine_value_; else if (delta_decay > 0) synapse.get().rule_.synaptic_resource_ -= delta_rate * neuron.dopamine_value_ * delta_decay; + if (delta_wdecay > 0) + synapse.get().rule_.synaptic_resource_ -= + delta_rate * neuron.dopamine_value_ * delta_wdecay * synapse.get().weight_; } if (debug && ++calls % 500 == 0) std::fprintf(stderr, "KNP_DELTA_DEBUG calls=%lu lag-3..3: %lu %lu %lu %lu %lu %lu %lu\\n",