From 64fc5161e0b13787b8cbc749464f09a81ad55bef Mon Sep 17 00:00:00 2001 From: int Date: Thu, 15 Oct 2026 04:58:42 +0300 Subject: [PATCH] Error-driven dopamine (delta-rule) training mode in KNP: network option, isolated backend Co-Authored-By: Claude Opus 5.5 --- cats-dogs/run.py | 5 +- .../source/models/altai/construct_network.cpp | 78 +++++++++++++--- .../source/models/altai/spike_generators.cpp | 7 +- changes-2026-10-15.md | 14 +++ .../models/altai/construct_network.cpp | 78 +++++++++++++--- .../models/altai/spike_generators.cpp | 7 +- readout/build_delta_cpu.py | 90 +++++++++++++++++++ readout/delta_runs.sh | 20 +++++ 8 files changed, 267 insertions(+), 32 deletions(-) create mode 100644 changes-2026-10-15.md create mode 100644 readout/build_delta_cpu.py create mode 100644 readout/delta_runs.sh diff --git a/cats-dogs/run.py b/cats-dogs/run.py index 3fced34..3bcc288 100644 --- a/cats-dogs/run.py +++ b/cats-dogs/run.py @@ -19,7 +19,7 @@ ap.add_argument('--columns', type=int, default=20) ap.add_argument('--seed', type=int, default=42) ap.add_argument('--jitter', type=float, default=20) ap.add_argument('--quant', choices=['default', 'weights'], default='default') -ap.add_argument('--backend', choices=['single', 'multi', 'tuned', 'fast'], default='single') +ap.add_argument('--backend', choices=['single', 'multi', 'tuned', 'fast', 'delta'], default='single') ap.add_argument('--threads', type=int, default=2) a = ap.parse_args() if a.backend == 'multi': @@ -41,7 +41,8 @@ env['KNP_CPU_THREADS'] = str(a.threads) backends = {'single': '/home/int/knp-build/bin/libknp-cpu-single-threaded-backend.so', 'multi': '/home/int/knp-build/bin/libknp-cpu-multi-threaded-backend.so', 'tuned': str(root / 'acceleration/libknp-cpu-tuned-backend.so'), - 'fast': str(root / 'fast-cpu/libknp-cpu-fast-backend.so')} + 'fast': str(root / 'fast-cpu/libknp-cpu-fast-backend.so'), + 'delta': '/home/int/run/delta-cpu/libknp-cpu-delta-backend.so'} command = [str(root / 'build/cats_dogs'), '-m', 'altai', '--images', str(a.data_dir / (a.dataset + '.bin')), '--labels', str(a.data_dir / (a.dataset + '.target')), diff --git a/cats-dogs/source/models/altai/construct_network.cpp b/cats-dogs/source/models/altai/construct_network.cpp index 43bd0b2..2e2edec 100644 --- a/cats-dogs/source/models/altai/construct_network.cpp +++ b/cats-dogs/source/models/altai/construct_network.cpp @@ -41,6 +41,10 @@ // 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. +// KNP_DELTA=, error-driven (delta-rule) training; needs a backend built with KNP_DELTA_RULE support +// (augment/build_delta_cpu.py). The label sends dopamine +a to its neurons on every active +// step, every input neuron's own spike sends it dopamine -b on the next step, so dopamine = +// target - output; no forcing gate, no label inhibition, no Hebbian/stability changes. namespace { float env_float(const char *name, float default_value) @@ -110,6 +114,13 @@ static NetworkPopulations create_populations(NetworkConstructor &constructor) input_neuron.stability_change_parameter_ = stability_change_parameter; input_neuron.resource_drain_coefficient_ = resource_drain_coefficient; input_neuron.synapse_sum_threshold_coefficient_ = synapse_sum_threshold_coefficient; + if (env_float_list("KNP_DELTA", {}).size() == 2) + { + // Only the dopamine (error) rule changes synapses. + input_neuron.d_h_ = 0; + input_neuron.stability_change_parameter_ = 0; + input_neuron.synapse_sum_threshold_coefficient_ = 0; + } // Create input population with specialized parameters for image processing. const auto &input_pop = constructor.add_population( @@ -179,6 +190,14 @@ static void create_projections( target_to_input_synapse_dopamine.output_type_ = knp::synapse_traits::OutputType::DOPAMINE; target_to_input_synapse_dopamine.weight_ = 0.179376 * 1000; target_to_input_synapse_dopamine.delay_ = 3; + const std::vector delta = env_float_list("KNP_DELTA", {}); + const bool delta_mode = delta.size() == 2; + if (delta_mode) + { + // Label spikes come on every active step, in step with the image spikes. + target_to_input_synapse_dopamine.weight_ = delta[0]; + target_to_input_synapse_dopamine.delay_ = 1; + } // Create dopamine projection connecting target to input population. auto target_to_input_proj_dopamine = constructor.add_projection( @@ -216,26 +235,32 @@ static void create_projections( target_to_input_synapse_excitatory.weight_ = -30 * 1000; target_to_input_synapse_excitatory.delay_ = 4; - // Create excitatory projection connecting target population to input population. - auto target_to_input_proj_excitatory = constructor.add_projection( - target_to_input_synapse_excitatory, knp::framework::projection::creators::all_to_all, - pops.target_pop_, pops.input_pop_, false, false); - - // Record excitatory projection for label processing. - network.data_.projections_from_classes_.push_back(target_to_input_proj_excitatory); + // Create excitatory projection connecting target population to input population (not in delta mode). + if (!delta_mode) + { + auto target_to_input_proj_excitatory = constructor.add_projection( + target_to_input_synapse_excitatory, knp::framework::projection::creators::all_to_all, + pops.target_pop_, pops.input_pop_, false, false); + + // Record excitatory projection for label processing. + network.data_.projections_from_classes_.push_back(target_to_input_proj_excitatory); + } // Configure synapse for target to gate projection. DeltaSynapseParams target_to_gate_synapse; target_to_gate_synapse.output_type_ = knp::synapse_traits::OutputType::EXCITATORY; target_to_gate_synapse.weight_ = 10 * 1000; - // Create projection connecting target population to gate population. - auto target_to_gate_proj = constructor.add_projection( - target_to_gate_synapse, knp::framework::projection::creators::aligned, pops.target_pop_, - pops.gate_pop_, false, false); - - // Record gate projection for label processing. - network.data_.projections_from_classes_.push_back(target_to_gate_proj); + // Create projection connecting target population to gate population (no forcing in delta mode). + if (!delta_mode) + { + auto target_to_gate_proj = constructor.add_projection( + target_to_gate_synapse, knp::framework::projection::creators::aligned, pops.target_pop_, + pops.gate_pop_, false, false); + + // Record gate projection for label processing. + network.data_.projections_from_classes_.push_back(target_to_gate_proj); + } // Configure synapse for input to output projection. DeltaSynapseParams input_to_output_synapse; @@ -276,6 +301,31 @@ static void create_projections( pops.input_pop_, false, false); } + // Delta mode: a neuron's own spike sends it negative dopamine on the next step (dopamine = target - output). + if (delta_mode) + { + DeltaSynapseParams self_dopamine; + self_dopamine.output_type_ = knp::synapse_traits::OutputType::DOPAMINE; + self_dopamine.weight_ = -delta[1]; + self_dopamine.delay_ = 1; + auto self_creator = [self_dopamine]( + const knp::core::UID &pre_uid, const knp::core::UID &post_uid, size_t pre_size, + size_t post_size, auto) + { + return knp::framework::projection::creators::index_based( + pre_uid, post_uid, pre_size, post_size, + [self_dopamine](size_t pre, size_t post) -> std::optional + { + if (pre != post) return std::nullopt; + return self_dopamine; + }); + }; + auto self_proj = + constructor.add_projection(self_dopamine, self_creator, pops.input_pop_, pops.input_pop_, false, false); + // Training signal only: not part of the inference network. + network.data_.inference_internal_projection_.erase(self_proj); + } + // Lateral inhibition inside each column. if (const float lateral = env_float("KNP_COL_LATERAL", 0); lateral > 0) { diff --git a/cats-dogs/source/models/altai/spike_generators.cpp b/cats-dogs/source/models/altai/spike_generators.cpp index eea9d86..0d75ab5 100644 --- a/cats-dogs/source/models/altai/spike_generators.cpp +++ b/cats-dogs/source/models/altai/spike_generators.cpp @@ -20,6 +20,8 @@ */ // cppcheck-suppress missingInclude +#include + #include "global_config.h" #include "network_functions.h" @@ -37,7 +39,10 @@ make_training_labels_spikes_generator(const Datase knp::core::messaging::SpikeData message; knp::core::Step local_step = step % steps_per_image; - if (local_step == 11) message.push_back(dataset.get_data_for_training().first[step / steps_per_image].first); + // KNP_DELTA (error-driven training): the label is sent on every active step, in step with the image. + static const bool every_step = std::getenv("KNP_DELTA") != nullptr; + if (every_step ? local_step < active_steps : local_step == 11) + message.push_back(dataset.get_data_for_training().first[step / steps_per_image].first); return message; }; } diff --git a/changes-2026-10-15.md b/changes-2026-10-15.md new file mode 100644 index 0000000..7a69ca6 --- /dev/null +++ b/changes-2026-10-15.md @@ -0,0 +1,14 @@ +# Журнал работы — 15 октября 2026 + +## Ночь (продолжение): выход, обучаемый дофамином по ошибке, внутри KNP + +Пользователь: «начинай, связь со мной не нужна». Цель — перенести в KNP правило, проверенное в симуляторе (выход «цель − ответ» × вход). + +### Код +- `examples/.../models/altai/construct_network.cpp`: режим `KNP_DELTA=,`. Метка подаёт дофамин +a нейронам своего класса с задержкой 1 на каждом активном шаге; собственный спайк нейрона даёт ему дофамин −b на следующем шаге (проекция input→input, исключена из сети распознавания). Нет торможения по метке (−30000) и принуждения через гейт; у нейронов выключены Хебб (d_h=0), стабильность и порог от суммы весов. Используется с `KNP_COL_SIZE=1` (один нейрон на класс). +- `.../models/altai/spike_generators.cpp`: при `KNP_DELTA` метка отправляется на шагах 0–9 (а не на 11-м). +- `readout/build_delta_cpu.py` → `/home/int/run/delta-cpu/libknp-cpu-delta-backend.so`: изолированная копия бэкенда; `KNP_DELTA_RULE=<скорость>` — у нейрона с дофамином D ≠ 0 каждый обучаемый синапс, чей входной спайк пришёл с отставанием `KNP_DELTA_LAG` шагов, меняет ресурс на скорость·D (независимо от спайка самого нейрона); `KNP_DELTA_DEBUG` печатает гистограмму отставаний. Оригинальные библиотеки не менялись. +- Отладка: при отставании 0 обновлений не было (веса не менялись); гистограмма: отставания 1/2/3 = 406 тыс./252 тыс./120 тыс., 0 — ни одного. Дофамин приходит на шаг позже входа → `KNP_DELTA_LAG=1`. +- Пробный прогон (признаки coarse, 3000 обучение, 1000 проверка, 10 нейронов): скорость 0,3 — 48,5%, 1 — 76,8%, 3 — **77,4%** (колонки 200 нейронов на том же объёме: 69,8% без наказания, до 79% с наказанием). +- `cats-dogs/source`: те же изменения (`construct_network.cpp`, `spike_generators.cpp`), пересобран `cats_dogs`; `cats-dogs/run.py --backend delta`. +- Запущены: `readout/delta_runs.sh` (MNIST coarse, 30 000, 6 вариантов) и кошки/собаки на плотных признаках `features-v3/kmeans_validation` (2048 входов, 18 000 обучение, скорости 0,3/1/3). diff --git a/examples/examples/mnist-learn/models/altai/construct_network.cpp b/examples/examples/mnist-learn/models/altai/construct_network.cpp index 81302ce..4919671 100644 --- a/examples/examples/mnist-learn/models/altai/construct_network.cpp +++ b/examples/examples/mnist-learn/models/altai/construct_network.cpp @@ -41,6 +41,10 @@ // 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. +// KNP_DELTA=, error-driven (delta-rule) training; needs a backend built with KNP_DELTA_RULE support +// (augment/build_delta_cpu.py). The label sends dopamine +a to its neurons on every active +// step, every input neuron's own spike sends it dopamine -b on the next step, so dopamine = +// target - output; no forcing gate, no label inhibition, no Hebbian/stability changes. namespace { float env_float(const char *name, float default_value) @@ -110,6 +114,13 @@ static NetworkPopulations create_populations(NetworkConstructor &constructor) input_neuron.stability_change_parameter_ = stability_change_parameter; input_neuron.resource_drain_coefficient_ = resource_drain_coefficient; input_neuron.synapse_sum_threshold_coefficient_ = synapse_sum_threshold_coefficient; + if (env_float_list("KNP_DELTA", {}).size() == 2) + { + // Only the dopamine (error) rule changes synapses. + input_neuron.d_h_ = 0; + input_neuron.stability_change_parameter_ = 0; + input_neuron.synapse_sum_threshold_coefficient_ = 0; + } // Create input population with specialized parameters for image processing. const auto &input_pop = constructor.add_population( @@ -179,6 +190,14 @@ static void create_projections( target_to_input_synapse_dopamine.output_type_ = knp::synapse_traits::OutputType::DOPAMINE; target_to_input_synapse_dopamine.weight_ = 0.179376 * 1000; target_to_input_synapse_dopamine.delay_ = 3; + const std::vector delta = env_float_list("KNP_DELTA", {}); + const bool delta_mode = delta.size() == 2; + if (delta_mode) + { + // Label spikes come on every active step, in step with the image spikes. + target_to_input_synapse_dopamine.weight_ = delta[0]; + target_to_input_synapse_dopamine.delay_ = 1; + } // Create dopamine projection connecting target to input population. auto target_to_input_proj_dopamine = constructor.add_projection( @@ -216,26 +235,32 @@ static void create_projections( target_to_input_synapse_excitatory.weight_ = -30 * 1000; target_to_input_synapse_excitatory.delay_ = 4; - // Create excitatory projection connecting target population to input population. - auto target_to_input_proj_excitatory = constructor.add_projection( - target_to_input_synapse_excitatory, knp::framework::projection::creators::all_to_all, - pops.target_pop_, pops.input_pop_, false, false); - - // Record excitatory projection for label processing. - network.data_.projections_from_classes_.push_back(target_to_input_proj_excitatory); + // Create excitatory projection connecting target population to input population (not in delta mode). + if (!delta_mode) + { + auto target_to_input_proj_excitatory = constructor.add_projection( + target_to_input_synapse_excitatory, knp::framework::projection::creators::all_to_all, + pops.target_pop_, pops.input_pop_, false, false); + + // Record excitatory projection for label processing. + network.data_.projections_from_classes_.push_back(target_to_input_proj_excitatory); + } // Configure synapse for target to gate projection. DeltaSynapseParams target_to_gate_synapse; target_to_gate_synapse.output_type_ = knp::synapse_traits::OutputType::EXCITATORY; target_to_gate_synapse.weight_ = 10 * 1000; - // Create projection connecting target population to gate population. - auto target_to_gate_proj = constructor.add_projection( - target_to_gate_synapse, knp::framework::projection::creators::aligned, pops.target_pop_, - pops.gate_pop_, false, false); - - // Record gate projection for label processing. - network.data_.projections_from_classes_.push_back(target_to_gate_proj); + // Create projection connecting target population to gate population (no forcing in delta mode). + if (!delta_mode) + { + auto target_to_gate_proj = constructor.add_projection( + target_to_gate_synapse, knp::framework::projection::creators::aligned, pops.target_pop_, + pops.gate_pop_, false, false); + + // Record gate projection for label processing. + network.data_.projections_from_classes_.push_back(target_to_gate_proj); + } // Configure synapse for input to output projection. DeltaSynapseParams input_to_output_synapse; @@ -276,6 +301,31 @@ static void create_projections( pops.input_pop_, false, false); } + // Delta mode: a neuron's own spike sends it negative dopamine on the next step (dopamine = target - output). + if (delta_mode) + { + DeltaSynapseParams self_dopamine; + self_dopamine.output_type_ = knp::synapse_traits::OutputType::DOPAMINE; + self_dopamine.weight_ = -delta[1]; + self_dopamine.delay_ = 1; + auto self_creator = [self_dopamine]( + const knp::core::UID &pre_uid, const knp::core::UID &post_uid, size_t pre_size, + size_t post_size, auto) + { + return knp::framework::projection::creators::index_based( + pre_uid, post_uid, pre_size, post_size, + [self_dopamine](size_t pre, size_t post) -> std::optional + { + if (pre != post) return std::nullopt; + return self_dopamine; + }); + }; + auto self_proj = + constructor.add_projection(self_dopamine, self_creator, pops.input_pop_, pops.input_pop_, false, false); + // Training signal only: not part of the inference network. + network.data_.inference_internal_projection_.erase(self_proj); + } + // Lateral inhibition inside each column. if (const float lateral = env_float("KNP_COL_LATERAL", 0); lateral > 0) { diff --git a/examples/examples/mnist-learn/models/altai/spike_generators.cpp b/examples/examples/mnist-learn/models/altai/spike_generators.cpp index eea9d86..0d75ab5 100644 --- a/examples/examples/mnist-learn/models/altai/spike_generators.cpp +++ b/examples/examples/mnist-learn/models/altai/spike_generators.cpp @@ -20,6 +20,8 @@ */ // cppcheck-suppress missingInclude +#include + #include "global_config.h" #include "network_functions.h" @@ -37,7 +39,10 @@ make_training_labels_spikes_generator(const Datase knp::core::messaging::SpikeData message; knp::core::Step local_step = step % steps_per_image; - if (local_step == 11) message.push_back(dataset.get_data_for_training().first[step / steps_per_image].first); + // KNP_DELTA (error-driven training): the label is sent on every active step, in step with the image. + static const bool every_step = std::getenv("KNP_DELTA") != nullptr; + if (every_step ? local_step < active_steps : local_step == 11) + message.push_back(dataset.get_data_for_training().first[step / steps_per_image].first); return message; }; } diff --git a/readout/build_delta_cpu.py b/readout/build_delta_cpu.py new file mode 100644 index 0000000..c97fc74 --- /dev/null +++ b/readout/build_delta_cpu.py @@ -0,0 +1,90 @@ +"""Build an isolated CPU single-threaded backend with an error-driven (delta-rule) dopamine plasticity option. + +KNP_DELTA_RULE= (environment, read once): for every AltAI STDP neuron with non-zero dopamine D on a step, +each trainable synapse whose presynaptic spike arrives on that step changes its synaptic resource by rate * D, +whether or not the neuron itself spiked; stability is not used. Without the variable the backend behaves like +the stock one. Original sources and libraries are not modified. + +usage: python3 build_delta_cpu.py → /home/int/run/delta-cpu/libknp-cpu-delta-backend.so +""" +from pathlib import Path +import shlex +import shutil +import subprocess + +root = Path('/home/int/run/delta-cpu') +root.mkdir(parents=True, exist_ok=True) +source_root = Path('/mnt/c/KNP/examples/knp/backends/cpu') +shutil.copytree(source_root / 'cpu-library/include', root / 'include', dirs_exist_ok=True) +altai = root / 'include/knp/backends/cpu-library/impl/populations/altai/altai_stdp.h' +s = altai.read_text() +marker = ''' for (size_t neuron_index = 0; neuron_index < population.size(); ++neuron_index) + { + auto &neuron = population[neuron_index]; + // Dopamine processing. Dopamine punishment if forced does nothing.''' +assert marker in s +s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=), see build_delta_cpu.py. + // Step offset between the dopamine step and the presynaptic arrival step (KNP_DELTA_LAG, default 0). + static const int64_t DELTA_LAG = [] + { + const char *value = std::getenv("KNP_DELTA_LAG"); + return value ? std::stol(value) : 0L; + }(); + static const float delta_rate = [] + { + const char *value = std::getenv("KNP_DELTA_RULE"); + return value ? std::stof(value) : 0.F; + }(); + for (size_t neuron_index = 0; neuron_index < population.size(); ++neuron_index) + { + auto &neuron = population[neuron_index]; + if (delta_rate > 0) + { + if (neuron.dopamine_value_ == 0.0) continue; + std::vector> delta_synapses = + training::stdp::get_all_connected_synapses(working_projections, neuron_index); + static const bool debug = std::getenv("KNP_DELTA_DEBUG") != nullptr; + static uint64_t calls = 0, hist[7] = {0, 0, 0, 0, 0, 0, 0}; + for (auto &synapse : delta_synapses) + { + const int64_t lag = static_cast(step) - + static_cast(synapse.get().rule_.last_spike_step_ + synapse.get().delay_ - 1); + if (debug && lag >= -3 && lag <= 3) ++hist[lag + 3]; + // The presynaptic spike reaches the neuron on this step. + if (lag == DELTA_LAG) + synapse.get().rule_.synaptic_resource_ += delta_rate * neuron.dopamine_value_; + } + if (debug && ++calls % 500 == 0) + std::fprintf(stderr, "KNP_DELTA_DEBUG calls=%lu lag-3..3: %lu %lu %lu %lu %lu %lu %lu\\n", + static_cast(calls), hist[0], hist[1], hist[2], hist[3], hist[4], hist[5], hist[6]); + training::stdp::recalculate_synapse_weights(delta_synapses); + continue; + } + // Dopamine processing. Dopamine punishment if forced does nothing.''') +if '#include ' not in s: + s = s.replace('#include ', '#include \n#include \n#include \n#include ', 1) +altai.write_text(s) +build = Path('/home/int/knp-build/knp/backends/cpu/cpu-single-threaded-backend') +folder = build / 'CMakeFiles/knp-cpu-single-threaded-backend.dir' +flags = {} +for line in (folder / 'flags.make').read_text().splitlines(): + if ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +obj = root / 'backend.o' +subprocess.run(['/usr/bin/c++', *flags['CXX_DEFINES'], '-I' + str(root / 'include'), + *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-c', + str(source_root / 'cpu-single-threaded-backend/impl/backend.cpp'), '-o', str(obj)], check=True) +link = shlex.split((folder / 'link.txt').read_text()) +rebuilt = [] +for item in link: + if item.startswith('-Wl,--dependency-file='): + continue + if item.endswith('/impl/backend.cpp.o'): + item = str(obj) + elif item.startswith('-Wl,-soname,'): + item = '-Wl,-soname,libknp-cpu-delta-backend.so' + rebuilt.append(item) +rebuilt[rebuilt.index('-o') + 1] = str(root / 'libknp-cpu-delta-backend.so') +subprocess.run(rebuilt, cwd=build, check=True) +print(root / 'libknp-cpu-delta-backend.so', flush=True) diff --git a/readout/delta_runs.sh b/readout/delta_runs.sh new file mode 100644 index 0000000..e33ff81 --- /dev/null +++ b/readout/delta_runs.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Error-driven (delta-rule) readout in KNP on coarse stroke features, 30k train, validation. +# spec: name rate a b jitter +cd ~/run/exp +export LD_LIBRARY_PATH=$HOME/knp-build/bin:$HOME/deps/lib +run() { n=$1; [ -f $n/done ] && return; mkdir -p $n + echo | env KNP_COL_SIZE=1 KNP_COL_JITTER=$5 KNP_MNIST_THR=60 KNP_MNIST_ADD_THR_SCALE=0 KNP_MNIST_INFER_INHIB=255 \ + KNP_DELTA=$3,$4 KNP_DELTA_RULE=$2 KNP_DELTA_LAG=1 KNP_MNIST_DUMP=$HOME/run/exp/$n \ + $HOME/knp-build/bin/mnist_learn_example -m altai --images ${DATA:-fcoarse_val}30000.bin --labels ${DATA:-fcoarse_val}30000.target \ + -t 30000 -i 10000 -l error --training_backend $HOME/run/delta-cpu/libknp-cpu-delta-backend.so > $n/log.txt 2>&1 + touch $n/done; } +export -f run +cat <