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:
parent
ded57ee2f2
commit
151e9f0b23
8 changed files with 411 additions and 6 deletions
223
altai-objects/altai_infer.cpp
Normal file
223
altai-objects/altai_infer.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
// (index,label,spike counts per class).
|
// (index,label,spike counts per class).
|
||||||
//
|
//
|
||||||
// Environment: KNP_RETINA_THR (default 250), KNP_DET_THR (detector threshold, default 2000),
|
// 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/model_executor.h>
|
||||||
#include <knp/framework/network.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");
|
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);
|
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);
|
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)));
|
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);
|
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();
|
const auto retina_uid = retina_proj.get_uid(), learn_uid = learn_proj.get_uid(), det_uid = detectors.get_uid();
|
||||||
knp::framework::Network network;
|
knp::framework::Network network;
|
||||||
network.add_population(std::move(retina));
|
network.add_population(std::move(retina));
|
||||||
network.add_population(std::move(detectors));
|
network.add_population(std::move(detectors));
|
||||||
network.add_projection(std::move(retina_proj));
|
network.add_projection(std::move(retina_proj));
|
||||||
network.add_projection(std::move(learn_proj));
|
network.add_projection(std::move(learn_proj));
|
||||||
|
if (imprint)
|
||||||
|
network.add_projection(std::move(teacher));
|
||||||
|
else
|
||||||
network.add_projection(std::move(lateral));
|
network.add_projection(std::move(lateral));
|
||||||
knp::core::UID in_channel, out_channel;
|
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));
|
knp::framework::Model model(std::move(network));
|
||||||
model.add_input_channel(in_channel, retina_uid);
|
model.add_input_channel(in_channel, retina_uid);
|
||||||
model.add_output_channel(out_channel, det_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;
|
std::vector<knp::core::messaging::SpikeData> current;
|
||||||
auto input_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData
|
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;
|
knp::framework::BackendLoader loader;
|
||||||
auto backend = loader.load(backend_path);
|
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);
|
auto &out = executor.get_loader().get_output_channel(out_channel);
|
||||||
const auto t0 = std::chrono::steady_clock::now();
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
executor.start([&](size_t step) { return step < count * steps_per_image; });
|
executor.start([&](size_t step) { return step < count * steps_per_image; });
|
||||||
|
|
|
||||||
42
altai-objects/build_model.py
Normal file
42
altai-objects/build_model.py
Normal 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()}")
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
cd ~/run/altai-objects/fgrid
|
cd ~/run/altai-objects/fgrid
|
||||||
export LD_LIBRARY_PATH=$HOME/knp-build/bin:$HOME/deps/lib
|
export LD_LIBRARY_PATH=$HOME/knp-build/bin:$HOME/deps/lib
|
||||||
one() { n=$1; [ -f $n.score ] && return
|
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 \
|
../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
|
$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; }
|
~/anvenv/bin/python /mnt/c/knp/altai-objects/score_features.py $n.bin 64 > $n.score 2>&1; }
|
||||||
|
|
|
||||||
10
altai-objects/run_counts.sh
Normal file
10
altai-objects/run_counts.sh
Normal 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 "$@"' _
|
||||||
65
altai-objects/train_readout.py
Normal file
65
altai-objects/train_readout.py
Normal 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])
|
||||||
|
|
@ -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 (выученные веса минус средний, целые) → выход (дофамин по ошибке), затем веса читаются из бэкенда и фиксируются для проверки.
|
- `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 (соревновательное обучение).
|
- Бэкенд: `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% входов спайкают за шаг.
|
- `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, смещение по шагам, порог по обучающей выборке.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,14 @@ s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=<rate>),
|
||||||
const char *value = std::getenv("KNP_DELTA_DECAY");
|
const char *value = std::getenv("KNP_DELTA_DECAY");
|
||||||
return value ? std::stof(value) : 0.F;
|
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 = []
|
static const float delta_rate = []
|
||||||
{
|
{
|
||||||
const char *value = std::getenv("KNP_DELTA_RULE");
|
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_;
|
synapse.get().rule_.synaptic_resource_ += delta_rate * neuron.dopamine_value_;
|
||||||
else if (delta_decay > 0)
|
else if (delta_decay > 0)
|
||||||
synapse.get().rule_.synaptic_resource_ -= delta_rate * neuron.dopamine_value_ * delta_decay;
|
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)
|
if (debug && ++calls % 500 == 0)
|
||||||
std::fprintf(stderr, "KNP_DELTA_DEBUG calls=%lu lag-3..3: %lu %lu %lu %lu %lu %lu %lu\\n",
|
std::fprintf(stderr, "KNP_DELTA_DEBUG calls=%lu lag-3..3: %lu %lu %lu %lu %lu %lu %lu\\n",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue