Error-driven dopamine (delta-rule) training mode in KNP: network option, isolated backend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
int 2026-10-15 04:58:42 +03:00
parent 39079670c0
commit 64fc5161e0
8 changed files with 267 additions and 32 deletions

View file

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

View file

@ -41,6 +41,10 @@
// 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.
// KNP_DELTA=<a>,<b> 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<float> 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.
// 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<DeltaSynapse>,
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.
// 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<DeltaSynapse>, 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<DeltaSynapse>(
pre_uid, post_uid, pre_size, post_size,
[self_dopamine](size_t pre, size_t post) -> std::optional<DeltaSynapseParams>
{
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)
{

View file

@ -20,6 +20,8 @@
*/
// cppcheck-suppress missingInclude
#include <cstdlib>
#include "global_config.h"
#include "network_functions.h"
@ -37,7 +39,10 @@ make_training_labels_spikes_generator<knp::neuron_traits::AltAILIF>(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;
};
}

14
changes-2026-10-15.md Normal file
View file

@ -0,0 +1,14 @@
# Журнал работы — 15 октября 2026
## Ночь (продолжение): выход, обучаемый дофамином по ошибке, внутри KNP
Пользователь: «начинай, связь со мной не нужна». Цель — перенести в KNP правило, проверенное в симуляторе (выход «цель − ответ» × вход).
### Код
- `examples/.../models/altai/construct_network.cpp`: режим `KNP_DELTA=<a>,<b>`. Метка подаёт дофамин +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).

View file

@ -41,6 +41,10 @@
// 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.
// KNP_DELTA=<a>,<b> 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<float> 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.
// 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<DeltaSynapse>,
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.
// 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<DeltaSynapse>, 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<DeltaSynapse>(
pre_uid, post_uid, pre_size, post_size,
[self_dopamine](size_t pre, size_t post) -> std::optional<DeltaSynapseParams>
{
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)
{

View file

@ -20,6 +20,8 @@
*/
// cppcheck-suppress missingInclude
#include <cstdlib>
#include "global_config.h"
#include "network_functions.h"
@ -37,7 +39,10 @@ make_training_labels_spikes_generator<knp::neuron_traits::AltAILIF>(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;
};
}

View file

@ -0,0 +1,90 @@
"""Build an isolated CPU single-threaded backend with an error-driven (delta-rule) dopamine plasticity option.
KNP_DELTA_RULE=<rate> (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=<rate>), 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<std::reference_wrapper<SynapseParamType>> delta_synapses =
training::stdp::get_all_connected_synapses<SynapseType>(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<int64_t>(step) -
static_cast<int64_t>(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<unsigned long>(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 <cstdlib>' not in s:
s = s.replace('#include <algorithm>', '#include <algorithm>\n#include <cstdio>\n#include <cstdlib>\n#include <string>', 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)

20
readout/delta_runs.sh Normal file
View file

@ -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 <<SPECS | xargs -P 6 -L 1 bash -c 'run "$@"' _
dl_r1_a1_b1_j20 1 1 1 20
dl_r3_a1_b1_j20 3 1 1 20
dl_r10_a1_b1_j20 10 1 1 20
dl_r3_a1_b0.5_j20 3 1 0.5 20
dl_r3_a1_b2_j20 3 1 2 20
dl_r3_a1_b1_j0 3 1 1 0
SPECS