Inference-on-KNP pipeline: altai_infer, offline model building and readout training

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
int 2026-10-15 10:42:35 +03:00
parent ded57ee2f2
commit 151e9f0b23
8 changed files with 411 additions and 6 deletions

View file

@ -0,0 +1,223 @@
// Object recognition inference entirely in KNP on AltAI neurons: pixels → feature detectors → class neurons.
//
// altai_infer <images.bin> <first> <count> <model_dir> <backend.so> <out_prefix>
//
// 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: <out_prefix>_classes.csv (index, spike count per class) if a readout is given, and
// <out_prefix>_counts.bin (int32 [count][4·K], detector spikes per quadrant and type) for training the readout.
#include <knp/framework/model_executor.h>
#include <knp/framework/network.h>
#include <knp/neuron-traits/altai_lif.h>
#include <knp/synapse-traits/delta.h>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
using DeltaProjection = knp::core::Projection<knp::synapse_traits::DeltaSynapse>;
using AltAIPopulation = knp::core::Population<knp::neuron_traits::AltAILIF>;
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 <class T>
std::vector<T> read_vec(const std::filesystem::path &path)
{
std::ifstream f(path, std::ios::binary);
if (!f) return {};
std::vector<char> raw((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::vector<T> out(raw.size() / sizeof(T));
std::memcpy(out.data(), raw.data(), out.size() * sizeof(T));
return out;
}
knp::neuron_traits::neuron_parameters<knp::neuron_traits::AltAILIF> neuron(int threshold)
{
knp::neuron_traits::neuron_parameters<knp::neuron_traits::AltAILIF> p;
p.activation_threshold_ = static_cast<uint16_t>(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 <images.bin> <first> <count> <model_dir> <backend.so> <out_prefix>\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<size_t>(k) * positions;
const auto det_w = read_vec<int32_t>(model_dir / "det_w.bin"), det_b = read_vec<int32_t>(model_dir / "det_b.bin");
const auto ro_w = read_vec<int32_t>(model_dir / "readout_w.bin"), ro_b = read_vec<int32_t>(model_dir / "readout_b.bin");
const bool readout = !ro_w.empty();
std::ifstream images(argv[1], std::ios::binary);
std::vector<uint8_t> px(count * in_n);
images.seekg(static_cast<std::streamoff>(first * in_n));
images.read(reinterpret_cast<char *>(px.data()), static_cast<std::streamsize>(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<DeltaProjection::Synapse>
{
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<float>(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<DeltaProjection::Synapse>
{
const int32_t b = det_b[d / positions];
if (b == 0) return std::nullopt;
return DeltaProjection::Synapse{{static_cast<float>(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<DeltaProjection::Synapse>
{
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<float>(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<DeltaProjection::Synapse>
{
if (ro_b[c] == 0) return std::nullopt;
return DeltaProjection::Synapse{
{static_cast<float>(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<knp::core::messaging::SpikeData> 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<float> 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<uint32_t>(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<int32_t> counts(count * 4 * k, 0);
std::vector<int32_t> 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<const char *>(counts.data()), static_cast<std::streamsize>(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<double>(std::chrono::steady_clock::now() - t0).count() << " s" << std::endl;
}

View file

@ -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 <knp/framework/model_executor.h>
#include <knp/framework/network.h>
@ -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<uint16_t>(env_f("KNP_DET_NEG", 0));
p.potential_leak_ = static_cast<int16_t>(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<unsigned>(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<knp::core::messaging::SpikeData> 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<uint32_t>((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; });

View file

@ -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 <model_dir> [--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()}")

View file

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

View file

@ -0,0 +1,10 @@
#!/bin/bash
# Collect detector spike counts from KNP for readout training: run_counts.sh <model_dir> <out_dir> [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 "$@"' _

View file

@ -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 <model_dir> <counts_dir> [--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])

View file

@ -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, смещение по шагам, порог по обучающей выборке.

View file

@ -38,6 +38,14 @@ s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=<rate>),
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=<c>, 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=<rate>),
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",