All-on-platform object pipeline: KNP program (retina, competitive features, readout), vehicles data, tuning tools
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8ea8beca0
commit
ded57ee2f2
10 changed files with 884 additions and 2 deletions
515
altai-objects/altai_objects.cpp
Normal file
515
altai-objects/altai_objects.cpp
Normal file
|
|
@ -0,0 +1,515 @@
|
||||||
|
// Object recognition entirely in KNP with AltAI neurons: retina → learned feature detectors → readout.
|
||||||
|
//
|
||||||
|
// altai_objects features <patches.bin> <count> <K> <out_weights.bin> <backend.so>
|
||||||
|
// Learns K feature detectors on 12×12 RGB crops (uint8, 3×12×12 each). Network: pixels → retina (fixed
|
||||||
|
// ON/OFF centre–surround neurons, 6 maps 6×6) → K competing AltAI neurons with trainable synapses. Each
|
||||||
|
// detector inhibits the others and its own spike gives it dopamine, so only winners learn; with the delta
|
||||||
|
// backend (KNP_DELTA_RULE, KNP_DELTA_DECAY, KNP_DELTA_LAG) the winner moves its weights towards the input.
|
||||||
|
// Writes float32 weights [K][216] (retina channel, y, x order).
|
||||||
|
//
|
||||||
|
// altai_objects run <images.bin> <labels.txt> <train> <eval> <features.bin> <K> <classes> <backend.so> <prefix>
|
||||||
|
// Full network: pixels (3×32×32) → retina (6×32×32) → detectors (K per position, stride 2, weights from
|
||||||
|
// features.bin minus their mean, integers) → readout (one neuron per class, delta-rule training with the
|
||||||
|
// label dopamine +a every active step and own-spike dopamine −b, KNP_DELTA=a,b). After <train> images the
|
||||||
|
// readout weights are read back from the backend and fixed; then <eval> images are run with a plain AltAI
|
||||||
|
// readout. Writes <prefix>_readout.bin (float32 [classes][neurons]) and <prefix>_eval.csv
|
||||||
|
// (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).
|
||||||
|
|
||||||
|
#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 <knp/synapse-traits/stdp_synaptic_resource_rule.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <numeric>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
|
using DeltaSynapse = knp::synapse_traits::DeltaSynapse;
|
||||||
|
using DeltaProjection = knp::core::Projection<DeltaSynapse>;
|
||||||
|
using ResourceSynapse = knp::synapse_traits::SynapticResourceSTDPDeltaSynapse;
|
||||||
|
using ResourceProjection = knp::core::Projection<ResourceSynapse>;
|
||||||
|
using AltAINeuron = knp::neuron_traits::AltAILIF;
|
||||||
|
using StdpNeuron = knp::neuron_traits::SynapticResourceSTDPAltAILIFNeuron;
|
||||||
|
using AltAIPopulation = knp::core::Population<AltAINeuron>;
|
||||||
|
using StdpPopulation = knp::core::Population<StdpNeuron>;
|
||||||
|
|
||||||
|
constexpr int steps_per_image = 15, active_steps = 10, patch = 6, dog_r = 3, maps = 6;
|
||||||
|
constexpr float w_min = -253.122F, w_max = 92.3957F;
|
||||||
|
|
||||||
|
|
||||||
|
float env_f(const char *name, float def)
|
||||||
|
{
|
||||||
|
const char *v = std::getenv(name);
|
||||||
|
return v ? std::stof(v) : def;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<float> env_list(const char *name, std::vector<float> def)
|
||||||
|
{
|
||||||
|
const char *v = std::getenv(name);
|
||||||
|
if (!v) return def;
|
||||||
|
std::vector<float> out;
|
||||||
|
std::string s(v);
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos <= s.size())
|
||||||
|
{
|
||||||
|
size_t next = s.find(',', pos);
|
||||||
|
if (next == std::string::npos) next = s.size();
|
||||||
|
out.push_back(std::stof(s.substr(pos, next - pos)));
|
||||||
|
pos = next + 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Difference of Gaussians, sigma 0.5 and 1.5, 7×7, scaled by 1000 (as in altai-objects/retina_proto.py).
|
||||||
|
std::vector<float> dog_kernel()
|
||||||
|
{
|
||||||
|
auto gauss = [](float sigma)
|
||||||
|
{
|
||||||
|
std::vector<float> g((2 * dog_r + 1) * (2 * dog_r + 1));
|
||||||
|
float sum = 0;
|
||||||
|
for (int y = -dog_r; y <= dog_r; ++y)
|
||||||
|
for (int x = -dog_r; x <= dog_r; ++x)
|
||||||
|
sum += g[(y + dog_r) * (2 * dog_r + 1) + x + dog_r] = std::exp(-(x * x + y * y) / (2 * sigma * sigma));
|
||||||
|
for (auto &v : g) v /= sum;
|
||||||
|
return g;
|
||||||
|
};
|
||||||
|
auto a = gauss(0.5F), b = gauss(1.5F);
|
||||||
|
std::vector<float> d(a.size());
|
||||||
|
for (size_t i = 0; i < a.size(); ++i) d[i] = std::round((a[i] - b[i]) * 1000);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float resource_from_weight(float w)
|
||||||
|
{
|
||||||
|
const double diff = w_max - w_min, over = w - w_min;
|
||||||
|
return static_cast<float>(diff * over / (diff - over));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
knp::neuron_traits::neuron_parameters<AltAINeuron> plain_neuron(float threshold)
|
||||||
|
{
|
||||||
|
knp::neuron_traits::neuron_parameters<AltAINeuron> p;
|
||||||
|
p.activation_threshold_ = static_cast<uint16_t>(std::min(threshold, 65535.F));
|
||||||
|
p.negative_activation_threshold_ = 0;
|
||||||
|
p.potential_reset_value_ = 0;
|
||||||
|
p.potential_leak_ = 0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
knp::neuron_traits::neuron_parameters<StdpNeuron> learning_neuron(float threshold)
|
||||||
|
{
|
||||||
|
knp::neuron_traits::neuron_parameters<StdpNeuron> p;
|
||||||
|
p.activation_threshold_ = static_cast<uint16_t>(std::min(threshold, 65535.F));
|
||||||
|
p.negative_activation_threshold_ = 0;
|
||||||
|
p.potential_reset_value_ = 0;
|
||||||
|
p.potential_leak_ = 0;
|
||||||
|
p.dopamine_plasticity_time_ = 10;
|
||||||
|
p.isi_max_ = 10;
|
||||||
|
p.d_h_ = 0;
|
||||||
|
p.stability_change_parameter_ = 0;
|
||||||
|
p.synapse_sum_threshold_coefficient_ = 0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Retina projection: input channel (pixels c×h×w) → retina population (maps×oh×ow): ON map c uses +DoG,
|
||||||
|
// OFF map c uses −DoG around pixel (oy + off, ox + off) of channel c; taps outside the image are skipped.
|
||||||
|
DeltaProjection make_retina(const knp::core::UID &post, int h, int w, int oh, int ow, int off)
|
||||||
|
{
|
||||||
|
const auto dog = dog_kernel();
|
||||||
|
const int taps = (2 * dog_r + 1) * (2 * dog_r + 1);
|
||||||
|
std::vector<DeltaProjection::Synapse> synapses;
|
||||||
|
for (int m = 0; m < maps; ++m)
|
||||||
|
for (int oy = 0; oy < oh; ++oy)
|
||||||
|
for (int ox = 0; ox < ow; ++ox)
|
||||||
|
for (int t = 0; t < taps; ++t)
|
||||||
|
{
|
||||||
|
const int y = oy + off + t / (2 * dog_r + 1) - dog_r, x = ox + off + t % (2 * dog_r + 1) - dog_r;
|
||||||
|
if (y < 0 || y >= h || x < 0 || x >= w || dog[t] == 0) continue;
|
||||||
|
const int c = m % 3;
|
||||||
|
const float weight = m < 3 ? dog[t] : -dog[t];
|
||||||
|
synapses.push_back(DeltaProjection::Synapse{
|
||||||
|
{weight, 1, knp::synapse_traits::OutputType::EXCITATORY},
|
||||||
|
static_cast<size_t>(c * h * w + y * w + x), static_cast<size_t>((m * oh + oy) * ow + ox)});
|
||||||
|
}
|
||||||
|
return DeltaProjection(
|
||||||
|
knp::core::UID{false}, post, [&](size_t i) { return std::optional(synapses[i]); }, synapses.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Spike raster of an image (uint8 values), incrementing converter from rest, active steps only.
|
||||||
|
std::vector<knp::core::messaging::SpikeData> raster(const uint8_t *px, size_t n)
|
||||||
|
{
|
||||||
|
std::vector<knp::core::messaging::SpikeData> out(active_steps);
|
||||||
|
std::vector<float> st(n, 0.F);
|
||||||
|
for (int t = 0; t < active_steps; ++t)
|
||||||
|
for (size_t i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
st[i] += px[i] / 255.F;
|
||||||
|
if (st[i] >= 1.F)
|
||||||
|
{
|
||||||
|
st[i] -= 1.F;
|
||||||
|
out[t].push_back(static_cast<uint32_t>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<uint8_t> read_bin(const std::string &path)
|
||||||
|
{
|
||||||
|
std::ifstream f(path, std::ios::binary);
|
||||||
|
return std::vector<uint8_t>((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Read the trainable projection back from the backend: float weights [post][pre-index-in-population].
|
||||||
|
template <class Backend>
|
||||||
|
std::vector<float> trained_weights(Backend &backend, const knp::core::UID &proj_uid, size_t pre_n, size_t post_n)
|
||||||
|
{
|
||||||
|
std::vector<float> w(pre_n * post_n, 0.F);
|
||||||
|
auto data = backend->get_network_data();
|
||||||
|
for (auto &it = *data.projection_range.first; it != *data.projection_range.second; ++it)
|
||||||
|
{
|
||||||
|
auto proj = *it;
|
||||||
|
std::visit(
|
||||||
|
[&](auto &p)
|
||||||
|
{
|
||||||
|
using P = std::decay_t<decltype(p)>;
|
||||||
|
if constexpr (std::is_same_v<P, ResourceProjection>)
|
||||||
|
if (p.get_uid() == proj_uid)
|
||||||
|
for (const auto &s : p)
|
||||||
|
w[std::get<2>(s) * pre_n + std::get<1>(s)] = std::get<knp::core::synapse_data>(s).weight_;
|
||||||
|
},
|
||||||
|
proj);
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int features_mode(int argc, const char *const argv[])
|
||||||
|
{
|
||||||
|
const std::string patches_path = argv[2], out_path = argv[5], backend_path = argv[6];
|
||||||
|
const size_t count = std::stoul(argv[3]), k = std::stoul(argv[4]);
|
||||||
|
constexpr int crop = 12, in_n = 3 * crop * crop, ret_n = maps * patch * patch;
|
||||||
|
const auto px = read_bin(patches_path);
|
||||||
|
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);
|
||||||
|
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::uniform_real_distribution<float> init(0.F, env_f("KNP_INIT_W", 30));
|
||||||
|
std::vector<ResourceProjection::Synapse> learn_syn;
|
||||||
|
for (size_t j = 0; j < k; ++j)
|
||||||
|
for (size_t i = 0; i < ret_n; ++i)
|
||||||
|
{
|
||||||
|
knp::synapse_traits::synapse_parameters<ResourceSynapse> s;
|
||||||
|
s.rule_.w_min_ = w_min;
|
||||||
|
s.rule_.w_max_ = w_max;
|
||||||
|
s.rule_.dopamine_plasticity_period_ = 10;
|
||||||
|
s.weight_ = init(rng);
|
||||||
|
s.rule_.synaptic_resource_ = resource_from_weight(s.weight_);
|
||||||
|
s.delay_ = 1;
|
||||||
|
s.output_type_ = knp::synapse_traits::OutputType::EXCITATORY;
|
||||||
|
learn_syn.push_back(ResourceProjection::Synapse{s, i, j});
|
||||||
|
}
|
||||||
|
ResourceProjection learn_proj(
|
||||||
|
retina.get_uid(), detectors.get_uid(), [&](size_t i) { return std::optional(learn_syn[i]); },
|
||||||
|
learn_syn.size());
|
||||||
|
learn_proj.unlock_weights();
|
||||||
|
|
||||||
|
// Lateral inhibition and own-spike reward.
|
||||||
|
const float inhib = env_f("KNP_INHIB", 5000), reward = env_f("KNP_WTA_REWARD", 1);
|
||||||
|
DeltaProjection lateral(
|
||||||
|
detectors.get_uid(), detectors.get_uid(),
|
||||||
|
[&](size_t i) -> std::optional<DeltaProjection::Synapse>
|
||||||
|
{
|
||||||
|
const size_t pre = i / k, post = i % k;
|
||||||
|
if (pre == post)
|
||||||
|
return DeltaProjection::Synapse{{reward, 1, knp::synapse_traits::OutputType::DOPAMINE}, pre, post};
|
||||||
|
return DeltaProjection::Synapse{{-inhib, 1, knp::synapse_traits::OutputType::EXCITATORY}, pre, post};
|
||||||
|
},
|
||||||
|
k * k);
|
||||||
|
|
||||||
|
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;
|
||||||
|
knp::framework::Model model(std::move(network));
|
||||||
|
model.add_input_channel(in_channel, retina_uid);
|
||||||
|
model.add_output_channel(out_channel, det_uid);
|
||||||
|
|
||||||
|
std::vector<knp::core::messaging::SpikeData> current;
|
||||||
|
auto input_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) current = raster(px.data() + img * in_n, in_n);
|
||||||
|
if (local >= active_steps) return {};
|
||||||
|
return current[local];
|
||||||
|
};
|
||||||
|
knp::framework::BackendLoader loader;
|
||||||
|
auto backend = loader.load(backend_path);
|
||||||
|
knp::framework::ModelExecutor executor(model, backend, {{in_channel, input_gen}});
|
||||||
|
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; });
|
||||||
|
std::vector<size_t> wins(k, 0);
|
||||||
|
for (const auto &m : out.update())
|
||||||
|
for (auto i : m.neuron_indexes_) ++wins[i];
|
||||||
|
auto w = trained_weights(backend, learn_uid, ret_n, k);
|
||||||
|
std::ofstream(out_path, std::ios::binary).write(reinterpret_cast<const char *>(w.data()), w.size() * 4);
|
||||||
|
size_t used = 0;
|
||||||
|
for (auto v : wins) used += v > 0;
|
||||||
|
std::cout << "features: " << count << " patches, " << k << " detectors, "
|
||||||
|
<< std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count() << " s; detectors that "
|
||||||
|
<< "fired: " << used << "; spikes in last batch: " << std::accumulate(wins.begin(), wins.end(), 0UL)
|
||||||
|
<< std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int run_mode(int argc, const char *const argv[])
|
||||||
|
{
|
||||||
|
const std::string images_path = argv[2], labels_path = argv[3], feat_path = argv[6], backend_path = argv[9],
|
||||||
|
prefix = argv[10];
|
||||||
|
const size_t train = std::stoul(argv[4]), eval = std::stoul(argv[5]), k = std::stoul(argv[7]),
|
||||||
|
classes = std::stoul(argv[8]);
|
||||||
|
constexpr int side = 32, in_n = 3 * side * side, ret_side = side, ret_n = maps * side * side, stride = 2;
|
||||||
|
constexpr int per_side = (side - patch) / stride + 1, positions = per_side * per_side, fan_in = maps * patch * patch;
|
||||||
|
const size_t det_n = k * positions;
|
||||||
|
const auto px = read_bin(images_path);
|
||||||
|
std::vector<size_t> labels;
|
||||||
|
{
|
||||||
|
std::ifstream f(labels_path);
|
||||||
|
for (size_t v; f >> v;) labels.push_back(v);
|
||||||
|
}
|
||||||
|
// Detector weights: learned prototypes minus their mean, scaled to integers in ±KNP_DET_WMAX.
|
||||||
|
std::vector<float> proto(k * fan_in);
|
||||||
|
{
|
||||||
|
std::ifstream f(feat_path, std::ios::binary);
|
||||||
|
f.read(reinterpret_cast<char *>(proto.data()), proto.size() * 4);
|
||||||
|
}
|
||||||
|
std::vector<float> mean(fan_in, 0.F);
|
||||||
|
for (size_t j = 0; j < k; ++j)
|
||||||
|
for (int i = 0; i < fan_in; ++i) mean[i] += proto[j * fan_in + i] / k;
|
||||||
|
float max_abs = 1e-6F;
|
||||||
|
for (size_t j = 0; j < k; ++j)
|
||||||
|
for (int i = 0; i < fan_in; ++i) max_abs = std::max(max_abs, std::fabs(proto[j * fan_in + i] - mean[i]));
|
||||||
|
const float det_wmax = env_f("KNP_DET_WMAX", 127);
|
||||||
|
std::vector<float> det_w(k * fan_in);
|
||||||
|
for (size_t j = 0; j < k; ++j)
|
||||||
|
for (int i = 0; i < fan_in; ++i)
|
||||||
|
det_w[j * fan_in + i] = std::round((proto[j * fan_in + i] - mean[i]) / max_abs * det_wmax);
|
||||||
|
|
||||||
|
const float out_thr = env_f("KNP_OUT_THR", 4000);
|
||||||
|
auto build = [&](bool learning, const std::vector<float> &readout_w, knp::core::UID &image_proj_uid,
|
||||||
|
knp::core::UID &label_proj_uid, knp::core::UID &readout_proj_uid, knp::core::UID &out_uid)
|
||||||
|
{
|
||||||
|
AltAIPopulation retina([&](size_t) { return plain_neuron(env_f("KNP_RETINA_THR", 250)); }, ret_n);
|
||||||
|
AltAIPopulation detectors([&](size_t) { return plain_neuron(env_f("KNP_DET_THR", 2000)); }, det_n);
|
||||||
|
auto retina_proj = make_retina(retina.get_uid(), side, side, ret_side, ret_side, 0);
|
||||||
|
// Detector index = proto * positions + position; synapse index = detector * fan_in + element.
|
||||||
|
DeltaProjection det_proj(
|
||||||
|
retina.get_uid(), detectors.get_uid(),
|
||||||
|
[&](size_t index) -> std::optional<DeltaProjection::Synapse>
|
||||||
|
{
|
||||||
|
const size_t neuron = index / fan_in, e = index % fan_in;
|
||||||
|
const size_t j = neuron / positions, pos = neuron % positions;
|
||||||
|
const int py = static_cast<int>(pos / per_side) * stride, pxx = static_cast<int>(pos % per_side) * stride;
|
||||||
|
const int m = static_cast<int>(e / (patch * patch)), dy = static_cast<int>(e % (patch * patch)) / patch,
|
||||||
|
dx = static_cast<int>(e % patch);
|
||||||
|
const float weight = det_w[j * fan_in + e];
|
||||||
|
if (weight == 0) return std::nullopt;
|
||||||
|
return DeltaProjection::Synapse{
|
||||||
|
{weight, 1, knp::synapse_traits::OutputType::EXCITATORY},
|
||||||
|
static_cast<size_t>((m * ret_side + py + dy) * ret_side + pxx + dx), neuron};
|
||||||
|
},
|
||||||
|
det_n * fan_in);
|
||||||
|
image_proj_uid = retina_proj.get_uid();
|
||||||
|
knp::framework::Network network;
|
||||||
|
if (learning)
|
||||||
|
{
|
||||||
|
StdpPopulation readout([&](size_t) { return learning_neuron(out_thr); }, classes);
|
||||||
|
ResourceProjection rproj(
|
||||||
|
detectors.get_uid(), readout.get_uid(),
|
||||||
|
[&](size_t i) -> std::optional<ResourceProjection::Synapse>
|
||||||
|
{
|
||||||
|
knp::synapse_traits::synapse_parameters<ResourceSynapse> s;
|
||||||
|
s.rule_.w_min_ = w_min;
|
||||||
|
s.rule_.w_max_ = w_max;
|
||||||
|
s.rule_.dopamine_plasticity_period_ = 10;
|
||||||
|
s.weight_ = 0;
|
||||||
|
s.rule_.synaptic_resource_ = resource_from_weight(0);
|
||||||
|
s.delay_ = 1;
|
||||||
|
s.output_type_ = knp::synapse_traits::OutputType::EXCITATORY;
|
||||||
|
return ResourceProjection::Synapse{s, i % det_n, i / det_n};
|
||||||
|
},
|
||||||
|
det_n * classes);
|
||||||
|
rproj.unlock_weights();
|
||||||
|
const auto delta = env_list("KNP_DELTA", {1, 4});
|
||||||
|
DeltaProjection label_proj(
|
||||||
|
knp::core::UID{false}, readout.get_uid(),
|
||||||
|
[&](size_t i) {
|
||||||
|
return std::optional(DeltaProjection::Synapse{
|
||||||
|
{delta[0], 1, knp::synapse_traits::OutputType::DOPAMINE}, i, i});
|
||||||
|
},
|
||||||
|
classes);
|
||||||
|
DeltaProjection self_proj(
|
||||||
|
readout.get_uid(), readout.get_uid(),
|
||||||
|
[&](size_t i) {
|
||||||
|
return std::optional(DeltaProjection::Synapse{
|
||||||
|
{-delta[1], 1, knp::synapse_traits::OutputType::DOPAMINE}, i, i});
|
||||||
|
},
|
||||||
|
classes);
|
||||||
|
label_proj_uid = label_proj.get_uid();
|
||||||
|
readout_proj_uid = rproj.get_uid();
|
||||||
|
out_uid = readout.get_uid();
|
||||||
|
network.add_population(std::move(readout));
|
||||||
|
network.add_projection(std::move(rproj));
|
||||||
|
network.add_projection(std::move(label_proj));
|
||||||
|
network.add_projection(std::move(self_proj));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AltAIPopulation readout([&](size_t) { return plain_neuron(out_thr); }, classes);
|
||||||
|
DeltaProjection rproj(
|
||||||
|
detectors.get_uid(), readout.get_uid(),
|
||||||
|
[&](size_t i) -> std::optional<DeltaProjection::Synapse>
|
||||||
|
{
|
||||||
|
const float weight = readout_w[(i / det_n) * det_n + i % det_n];
|
||||||
|
if (weight == 0) return std::nullopt;
|
||||||
|
return DeltaProjection::Synapse{
|
||||||
|
{weight, 1, knp::synapse_traits::OutputType::EXCITATORY}, i % det_n, i / det_n};
|
||||||
|
},
|
||||||
|
det_n * classes);
|
||||||
|
out_uid = readout.get_uid();
|
||||||
|
network.add_population(std::move(readout));
|
||||||
|
network.add_projection(std::move(rproj));
|
||||||
|
}
|
||||||
|
network.add_population(std::move(retina));
|
||||||
|
network.add_population(std::move(detectors));
|
||||||
|
network.add_projection(std::move(retina_proj));
|
||||||
|
network.add_projection(std::move(det_proj));
|
||||||
|
return network;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<knp::core::messaging::SpikeData> current;
|
||||||
|
auto image_gen = [&](size_t first)
|
||||||
|
{
|
||||||
|
return [&, first](knp::core::Step step) -> knp::core::messaging::SpikeData
|
||||||
|
{
|
||||||
|
const size_t img = first + step / steps_per_image, local = step % steps_per_image;
|
||||||
|
if (local == 0) current = raster(px.data() + img * in_n, in_n);
|
||||||
|
if (local >= active_steps) return {};
|
||||||
|
return current[local];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
knp::framework::BackendLoader loader;
|
||||||
|
std::vector<float> readout_w(det_n * classes, 0.F);
|
||||||
|
if (train > 0)
|
||||||
|
{
|
||||||
|
knp::core::UID image_proj, label_proj, readout_proj, out_uid;
|
||||||
|
knp::framework::Model model(build(true, readout_w, image_proj, label_proj, readout_proj, out_uid));
|
||||||
|
knp::core::UID in_channel, label_channel;
|
||||||
|
model.add_input_channel(in_channel, image_proj);
|
||||||
|
model.add_input_channel(label_channel, label_proj);
|
||||||
|
auto label_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData
|
||||||
|
{
|
||||||
|
if (step % steps_per_image >= active_steps) return {};
|
||||||
|
return {static_cast<uint32_t>(labels[step / steps_per_image])};
|
||||||
|
};
|
||||||
|
auto backend = loader.load(backend_path);
|
||||||
|
knp::framework::ModelExecutor executor(model, backend, {{in_channel, image_gen(0)}, {label_channel, label_gen}});
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
executor.start(
|
||||||
|
[&](size_t step)
|
||||||
|
{
|
||||||
|
if (step % (1000 * steps_per_image) == 0 && step)
|
||||||
|
std::cout << "trained " << step / steps_per_image << " images, "
|
||||||
|
<< std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count() << " s"
|
||||||
|
<< std::endl;
|
||||||
|
return step < train * steps_per_image;
|
||||||
|
});
|
||||||
|
readout_w = trained_weights(backend, readout_proj, det_n, classes);
|
||||||
|
std::ofstream(prefix + "_readout.bin", std::ios::binary)
|
||||||
|
.write(reinterpret_cast<const char *>(readout_w.data()), readout_w.size() * 4);
|
||||||
|
std::cout << "training done in " << std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count()
|
||||||
|
<< " s" << std::endl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::ifstream f(prefix + "_readout.bin", std::ios::binary);
|
||||||
|
f.read(reinterpret_cast<char *>(readout_w.data()), readout_w.size() * 4);
|
||||||
|
}
|
||||||
|
if (eval == 0) return 0;
|
||||||
|
knp::core::UID image_proj, label_proj, readout_proj, out_uid;
|
||||||
|
knp::framework::Model model(build(false, readout_w, image_proj, label_proj, readout_proj, out_uid));
|
||||||
|
knp::core::UID in_channel, out_channel;
|
||||||
|
model.add_input_channel(in_channel, image_proj);
|
||||||
|
model.add_output_channel(out_channel, out_uid);
|
||||||
|
// Evaluation starts right after the training images unless KNP_EVAL_FIRST is given.
|
||||||
|
const size_t first = std::getenv("KNP_EVAL_FIRST") ? static_cast<size_t>(env_f("KNP_EVAL_FIRST", 0)) : train;
|
||||||
|
auto backend = loader.load(backend_path);
|
||||||
|
knp::framework::ModelExecutor executor(model, backend, {{in_channel, image_gen(first)}});
|
||||||
|
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 < eval * steps_per_image; });
|
||||||
|
std::vector<std::vector<int>> counts(eval, std::vector<int>(classes, 0));
|
||||||
|
for (const auto &m : out.update())
|
||||||
|
{
|
||||||
|
// A spike sent on step s belongs to image s / steps_per_image (output arrives within the same image window).
|
||||||
|
const size_t img = m.header_.send_time_ / steps_per_image;
|
||||||
|
if (img < eval)
|
||||||
|
for (auto i : m.neuron_indexes_) ++counts[img][i];
|
||||||
|
}
|
||||||
|
std::ofstream csv(prefix + "_eval.csv");
|
||||||
|
csv << "index,label";
|
||||||
|
for (size_t c = 0; c < classes; ++c) csv << ",c" << c;
|
||||||
|
csv << "\n";
|
||||||
|
size_t correct = 0, silent = 0;
|
||||||
|
for (size_t i = 0; i < eval; ++i)
|
||||||
|
{
|
||||||
|
const auto &c = counts[i];
|
||||||
|
const int best = static_cast<int>(std::max_element(c.begin(), c.end()) - c.begin());
|
||||||
|
const bool none = *std::max_element(c.begin(), c.end()) == 0;
|
||||||
|
silent += none;
|
||||||
|
correct += !none && static_cast<size_t>(best) == labels[first + i];
|
||||||
|
csv << first + i << ',' << labels[first + i];
|
||||||
|
for (auto v : c) csv << ',' << v;
|
||||||
|
csv << "\n";
|
||||||
|
}
|
||||||
|
std::cout << "eval: " << correct << " / " << eval << " correct, " << silent << " silent, "
|
||||||
|
<< std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count() << " s" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, const char *const argv[])
|
||||||
|
{
|
||||||
|
const std::string mode = argc > 1 ? argv[1] : "";
|
||||||
|
if (mode == "features" && argc == 7) return features_mode(argc, argv);
|
||||||
|
if (mode == "run" && argc == 11) return run_mode(argc, argv);
|
||||||
|
std::cerr << "usage: see the header of altai_objects.cpp" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
92
altai-objects/bench_conv.cpp
Normal file
92
altai-objects/bench_conv.cpp
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Speed check: a convolution-like feature layer built from AltAI neurons in KNP.
|
||||||
|
// usage: bench_conv <K> <stride> <images> <backend.so>
|
||||||
|
// K prototypes × positions (6×6×3 patches of a 32×32×3 image with the given stride) AltAI neurons; random integer
|
||||||
|
// weights; random input spikes (pixel rate ~0.4 per active step); 15 steps per image, 10 of them active.
|
||||||
|
|
||||||
|
#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 <iostream>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, const char *const argv[])
|
||||||
|
{
|
||||||
|
const int k = std::stoi(argv[1]), stride = std::stoi(argv[2]), images = std::stoi(argv[3]);
|
||||||
|
const int per_side = (side - patch) / stride + 1, positions = per_side * per_side;
|
||||||
|
const size_t neurons = static_cast<size_t>(k) * positions, fan_in = patch * patch * channels;
|
||||||
|
|
||||||
|
AltAIPopulation population(
|
||||||
|
[](size_t)
|
||||||
|
{
|
||||||
|
knp::neuron_traits::neuron_parameters<knp::neuron_traits::AltAILIF> p;
|
||||||
|
p.activation_threshold_ = 1000;
|
||||||
|
p.negative_activation_threshold_ = 0;
|
||||||
|
p.potential_reset_value_ = 0;
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
neurons);
|
||||||
|
|
||||||
|
std::mt19937 rng(1);
|
||||||
|
std::uniform_int_distribution<int> weight(-60, 60);
|
||||||
|
std::vector<int> w(fan_in * k);
|
||||||
|
for (auto &v : w) v = weight(rng);
|
||||||
|
|
||||||
|
// Synapse index → (neuron, patch element); the source is the input channel index of that pixel.
|
||||||
|
DeltaProjection input_projection(
|
||||||
|
knp::core::UID{false}, population.get_uid(),
|
||||||
|
[&](size_t index) -> std::optional<DeltaProjection::Synapse>
|
||||||
|
{
|
||||||
|
const size_t neuron = index / fan_in, e = index % fan_in;
|
||||||
|
const int proto = static_cast<int>(neuron / positions), pos = static_cast<int>(neuron % positions);
|
||||||
|
const int py = (pos / per_side) * stride, px = (pos % per_side) * stride;
|
||||||
|
const int c = static_cast<int>(e / (patch * patch)), dy = static_cast<int>(e % (patch * patch)) / patch,
|
||||||
|
dx = static_cast<int>(e % patch);
|
||||||
|
const size_t source = static_cast<size_t>(c * side * side + (py + dy) * side + px + dx);
|
||||||
|
return DeltaProjection::Synapse{
|
||||||
|
{static_cast<float>(w[proto * fan_in + e]), 1, knp::synapse_traits::OutputType::EXCITATORY}, source,
|
||||||
|
neuron};
|
||||||
|
},
|
||||||
|
neurons * fan_in);
|
||||||
|
|
||||||
|
const knp::core::UID input_uid = input_projection.get_uid(), output_uid = population.get_uid();
|
||||||
|
knp::framework::Network network;
|
||||||
|
network.add_population(std::move(population));
|
||||||
|
network.add_projection(std::move(input_projection));
|
||||||
|
knp::core::UID in_channel, out_channel;
|
||||||
|
knp::framework::Model model(std::move(network));
|
||||||
|
model.add_input_channel(in_channel, input_uid);
|
||||||
|
model.add_output_channel(out_channel, output_uid);
|
||||||
|
|
||||||
|
std::bernoulli_distribution fire(0.4);
|
||||||
|
auto input_gen = [&](knp::core::Step step) -> knp::core::messaging::SpikeData
|
||||||
|
{
|
||||||
|
knp::core::messaging::SpikeData s;
|
||||||
|
if (step % steps_per_image >= active_steps) return s;
|
||||||
|
for (size_t i = 0; i < side * side * channels; ++i)
|
||||||
|
if (fire(rng)) s.push_back(i);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
knp::framework::BackendLoader loader;
|
||||||
|
knp::framework::ModelExecutor executor(model, loader.load(argv[4]), {{in_channel, input_gen}});
|
||||||
|
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 < static_cast<size_t>(images * steps_per_image); });
|
||||||
|
const double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||||
|
size_t spikes = 0;
|
||||||
|
for (const auto &m : out.update()) spikes += m.neuron_indexes_.size();
|
||||||
|
std::cout << "K=" << k << " stride=" << stride << " neurons=" << neurons << " synapses=" << neurons * fan_in
|
||||||
|
<< " images=" << images << " seconds=" << sec << " per_image=" << sec / images
|
||||||
|
<< " output_spikes_per_image=" << spikes / images << std::endl;
|
||||||
|
}
|
||||||
36
altai-objects/build.py
Normal file
36
altai-objects/build.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Build a standalone KNP program from one .cpp using the working mnist-learn compile and link settings.
|
||||||
|
|
||||||
|
usage: python3 build.py <source.cpp> [more.cpp ...] → /home/int/run/altai-objects/<first source stem>
|
||||||
|
"""
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
build = Path('/home/int/run/altai-objects')
|
||||||
|
build.mkdir(parents=True, exist_ok=True)
|
||||||
|
old = Path('/home/int/knp-build/examples/mnist-learn')
|
||||||
|
flags = {}
|
||||||
|
for line in (old / 'CMakeFiles/mnist_learn_example.dir/flags.make').read_text().splitlines():
|
||||||
|
if ' = ' in line:
|
||||||
|
key, value = line.split(' = ', 1)
|
||||||
|
flags[key] = shlex.split(value)
|
||||||
|
link = shlex.split((old / 'CMakeFiles/mnist_learn_example.dir/link.txt').read_text())
|
||||||
|
sources = [Path(s).resolve() for s in sys.argv[1:]]
|
||||||
|
|
||||||
|
|
||||||
|
def compile_file(src):
|
||||||
|
target = build / (src.stem + '.o')
|
||||||
|
subprocess.run(['/usr/bin/c++', '-std=c++17', *flags['CXX_DEFINES'], '-I' + str(src.parent), *flags['CXX_INCLUDES'],
|
||||||
|
*flags['CXX_FLAGS'], '-Wno-error', '-c', str(src), '-o', str(target)], check=True)
|
||||||
|
return str(target)
|
||||||
|
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||||
|
objects = list(pool.map(compile_file, sources))
|
||||||
|
tail = link[link.index('-o') + 2:]
|
||||||
|
tail = [str((old / x).resolve()) if x.startswith('../../') else x for x in tail]
|
||||||
|
out = build / sources[0].stem
|
||||||
|
subprocess.run(['/usr/bin/c++', *objects, '-o', str(out), *tail], cwd=old, check=True)
|
||||||
|
print(out, flush=True)
|
||||||
23
altai-objects/export_knp.py
Normal file
23
altai-objects/export_knp.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""Export vehicles data for altai_objects: train+val images (uint8 3×32×32, train first), labels, random 12×12 crops.
|
||||||
|
|
||||||
|
usage: ~/anvenv/bin/python export_knp.py [--crops 200000] → /home/int/run/cifar/vehicles/knp_{images.bin,labels.txt,crops.bin}
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--crops", type=int, default=200000)
|
||||||
|
a = ap.parse_args()
|
||||||
|
d = "/home/int/run/cifar/vehicles"
|
||||||
|
tr, va = np.load(f"{d}/train.npz"), np.load(f"{d}/val.npz")
|
||||||
|
xt, xv = tr["rgb"], va["rgb"] # load once: indexing an npz entry re-reads the whole array
|
||||||
|
x = np.concatenate([xt, xv]).astype(np.uint8)
|
||||||
|
y = np.concatenate([tr["labels"], va["labels"]])
|
||||||
|
x.tofile(f"{d}/knp_images.bin")
|
||||||
|
open(f"{d}/knp_labels.txt", "w").write("\n".join(map(str, y)) + "\n")
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
i, oy, ox = rng.integers(0, len(xt), a.crops), rng.integers(0, 21, a.crops), rng.integers(0, 21, a.crops)
|
||||||
|
crops = np.stack([xt[j, :, b:b + 12, c:c + 12] for j, b, c in zip(i, oy, ox)]).astype(np.uint8)
|
||||||
|
crops.tofile(f"{d}/knp_crops.bin")
|
||||||
|
print("images", x.shape, "crops", crops.shape)
|
||||||
11
altai-objects/feature_grid.sh
Normal file
11
altai-objects/feature_grid.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Tuning grid for KNP feature learning (K=64, 20000 crops); each line: name rate decay det_thr inhib
|
||||||
|
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 \
|
||||||
|
../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; }
|
||||||
|
export -f one
|
||||||
|
cat ${SPECFILE:-/dev/stdin} | xargs -P ${JOBS:-12} -L 1 bash -c 'one "$@"' _
|
||||||
41
altai-objects/prepare_vehicles.py
Normal file
41
altai-objects/prepare_vehicles.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""CIFAR-10 vehicles (airplane, automobile, ship, truck → 0..3) from the Hugging Face parquet copy (uoft-cs/cifar10).
|
||||||
|
|
||||||
|
train: first 4000 training images of each class (16000), val: the last 1000 of each class (4000),
|
||||||
|
test: the official test images of these classes (4000) — kept for one final evaluation.
|
||||||
|
usage: ~/anvenv/bin/python prepare_vehicles.py → /home/int/run/cifar/vehicles/{train,val,test}.npz
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
src, out = Path('/home/int/run/cifar'), Path('/home/int/run/cifar/vehicles')
|
||||||
|
out.mkdir(exist_ok=True)
|
||||||
|
CLASSES = {0: 0, 1: 1, 8: 2, 9: 3} # CIFAR label → our label
|
||||||
|
NAMES = ['airplane', 'automobile', 'ship', 'truck']
|
||||||
|
|
||||||
|
|
||||||
|
def load(name):
|
||||||
|
t = pq.read_table(src / f'hf_{name}.parquet').to_pydict()
|
||||||
|
imgs, labels = [], []
|
||||||
|
for img, lab in zip(t['img'], t['label']):
|
||||||
|
if lab in CLASSES:
|
||||||
|
imgs.append(np.asarray(Image.open(io.BytesIO(img['bytes'])).convert('RGB')).transpose(2, 0, 1))
|
||||||
|
labels.append(CLASSES[lab])
|
||||||
|
return np.stack(imgs), np.array(labels, np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
x, y = load('train')
|
||||||
|
idx = {c: np.flatnonzero(y == c) for c in range(4)}
|
||||||
|
train = np.concatenate([idx[c][:4000] for c in range(4)])
|
||||||
|
val = np.concatenate([idx[c][4000:] for c in range(4)])
|
||||||
|
rng = np.random.default_rng(20261015)
|
||||||
|
train, val = rng.permutation(train), rng.permutation(val)
|
||||||
|
np.savez_compressed(out / 'train.npz', rgb=x[train], labels=y[train])
|
||||||
|
np.savez_compressed(out / 'val.npz', rgb=x[val], labels=y[val])
|
||||||
|
xt, yt = load('test')
|
||||||
|
p = rng.permutation(len(yt))
|
||||||
|
np.savez_compressed(out / 'test.npz', rgb=xt[p], labels=yt[p])
|
||||||
|
print('train', len(train), np.bincount(y[train]), 'val', len(val), 'test', len(yt), np.bincount(yt), NAMES)
|
||||||
113
altai-objects/retina_proto.py
Normal file
113
altai-objects/retina_proto.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
"""Design check for the all-on-platform pipeline (numpy prototype, not the final result).
|
||||||
|
|
||||||
|
Retina: per colour channel a centre–surround (difference of Gaussians) filter; ON = max(0, DoG), OFF = max(0, −DoG)
|
||||||
|
→ 6 maps 32×32 (fixed weights, i.e. one fixed layer of neurons). Features: online winner-take-all on 6×6×6 retina
|
||||||
|
patches (unit-norm weights, winner = largest w·x, winner moves towards the patch), activation max(0, w·x − θ) with
|
||||||
|
θ a quantile of the winners' responses; sum pooling over quadrants; linear readout (ridge here, delta rule in KNP).
|
||||||
|
|
||||||
|
usage: ~/anvenv/bin/python retina_proto.py <data dir with train.npz/val.npz> [--k 200 --stride 2]
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
PATCH = 6
|
||||||
|
|
||||||
|
|
||||||
|
def gauss(sigma, r=3):
|
||||||
|
y, x = np.mgrid[-r:r + 1, -r:r + 1]
|
||||||
|
g = np.exp(-(x * x + y * y) / (2 * sigma * sigma))
|
||||||
|
return g / g.sum()
|
||||||
|
|
||||||
|
|
||||||
|
DOG = gauss(0.8) - gauss(2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def retina(rgb, gain=4.0):
|
||||||
|
"""rgb uint8 [n,3,32,32] → float32 [n,6,32,32] in [0,1]: ON and OFF maps per colour."""
|
||||||
|
x = rgb.astype(np.float32) / 255
|
||||||
|
r = DOG.shape[0] // 2
|
||||||
|
xp = np.pad(x, ((0, 0), (0, 0), (r, r), (r, r)), mode="edge")
|
||||||
|
d = np.zeros_like(x)
|
||||||
|
for dy in range(DOG.shape[0]):
|
||||||
|
for dx in range(DOG.shape[1]):
|
||||||
|
d += DOG[dy, dx] * xp[:, :, dy:dy + 32, dx:dx + 32]
|
||||||
|
return np.clip(np.concatenate([np.maximum(d, 0), np.maximum(-d, 0)], axis=1) * gain, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def patches(maps, stride):
|
||||||
|
pos = range(0, 32 - PATCH + 1, stride)
|
||||||
|
return np.stack([maps[:, :, y:y + PATCH, x:x + PATCH].reshape(len(maps), -1) for y in pos for x in pos], axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
def learn_wta(maps, k, n=300000, lr0=0.05, seed=0):
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
i, y, x = rng.integers(0, len(maps), n), rng.integers(0, 27, n), rng.integers(0, 27, n)
|
||||||
|
p = np.stack([maps[a, :, b:b + PATCH, c:c + PATCH].reshape(-1) for a, b, c in zip(i, y, x)])
|
||||||
|
p = p[p.sum(1) > 0.5] # skip empty patches (no retina activity)
|
||||||
|
w = p[rng.choice(len(p), k, replace=False)].copy()
|
||||||
|
w /= np.linalg.norm(w, axis=1, keepdims=True)
|
||||||
|
for t, v in enumerate(p):
|
||||||
|
win = int(np.argmax(w @ v))
|
||||||
|
w[win] += lr0 / (1 + t / 20000) * (v / (np.linalg.norm(v) + 1e-6) - w[win])
|
||||||
|
w[win] /= np.linalg.norm(w[win])
|
||||||
|
return w, p
|
||||||
|
|
||||||
|
|
||||||
|
def encode(maps, w, theta, stride, batch=500, centre=False):
|
||||||
|
"""centre=True: subtract the mean prototype (activation max(0, (w_k − mean_j w_j)·x − θ)); still linear."""
|
||||||
|
if centre:
|
||||||
|
w = w - w.mean(0)
|
||||||
|
out = []
|
||||||
|
for s in range(0, len(maps), batch):
|
||||||
|
p = patches(maps[s:s + batch], stride)
|
||||||
|
a = np.maximum(p @ w.T - theta, 0)
|
||||||
|
side = int(np.sqrt(p.shape[1]))
|
||||||
|
a = a.reshape(len(p), side, side, -1)
|
||||||
|
h = side // 2
|
||||||
|
out.append(np.concatenate([a[:, :h, :h].sum((1, 2)), a[:, :h, h:].sum((1, 2)),
|
||||||
|
a[:, h:, :h].sum((1, 2)), a[:, h:, h:].sum((1, 2))], axis=1))
|
||||||
|
return np.concatenate(out)
|
||||||
|
|
||||||
|
|
||||||
|
def ridge_acc(ft, yt, fv, yv, lam, classes):
|
||||||
|
mu, sd = ft.mean(0), ft.std(0) + 1e-3
|
||||||
|
f = lambda z: np.hstack([(z - mu) / sd, np.ones((len(z), 1), np.float32)])
|
||||||
|
w = np.linalg.solve(f(ft).T @ f(ft) + lam * np.eye(ft.shape[1] + 1), f(ft).T @ (np.eye(classes)[yt] * 2 - 1))
|
||||||
|
return 100 * ((f(fv) @ w).argmax(1) == yv).mean()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("data")
|
||||||
|
ap.add_argument("--k", type=int, default=200)
|
||||||
|
ap.add_argument("--stride", type=int, default=2)
|
||||||
|
ap.add_argument("--flip", action="store_true")
|
||||||
|
ap.add_argument("--centre", action="store_true")
|
||||||
|
ap.add_argument("--sigmas", default="0.8,2.0")
|
||||||
|
ap.add_argument("--gain", type=float, default=4.0)
|
||||||
|
a = ap.parse_args()
|
||||||
|
global DOG
|
||||||
|
s1, s2 = (float(v) for v in a.sigmas.split(","))
|
||||||
|
DOG = gauss(s1) - gauss(s2)
|
||||||
|
tr, va = np.load(f"{a.data}/train.npz"), np.load(f"{a.data}/val.npz")
|
||||||
|
xt, yt, xv, yv = tr["rgb"], tr["labels"].astype(int), va["rgb"], va["labels"].astype(int)
|
||||||
|
classes = int(yt.max()) + 1
|
||||||
|
mt, mv = retina(xt, a.gain), retina(xv, a.gain)
|
||||||
|
w, p = learn_wta(mt, a.k)
|
||||||
|
wc = w - w.mean(0) if a.centre else w
|
||||||
|
resp = (p[:20000] @ wc.T).max(1)
|
||||||
|
for q in ((0, 50) if a.centre else (50, 75)):
|
||||||
|
theta = float(np.percentile(resp, q)) if q else 0.0
|
||||||
|
ft, fv = encode(mt, w, theta, a.stride, centre=a.centre), encode(mv, w, theta, a.stride, centre=a.centre)
|
||||||
|
if a.flip:
|
||||||
|
ft = np.vstack([ft, encode(retina(xt[:, :, :, ::-1], a.gain), w, theta, a.stride, centre=a.centre)])
|
||||||
|
ytt = np.r_[yt, yt]
|
||||||
|
else:
|
||||||
|
ytt = yt
|
||||||
|
print(f"K {a.k} stride {a.stride} theta=q{q} ({theta:.3f}): "
|
||||||
|
+ " | ".join(f"lam {lam}: {ridge_acc(ft, ytt, fv, yv, lam, classes):.1f}%" for lam in (100, 1000)), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
30
altai-objects/score_features.py
Normal file
30
altai-objects/score_features.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Quick quality score of KNP-learned feature detectors (tuning aid only; final numbers come from KNP).
|
||||||
|
|
||||||
|
Takes float32 weights [K][216] written by `altai_objects features`, encodes vehicles train (first N) and validation
|
||||||
|
with the numpy retina prototype (centred weights, threshold 0, quadrant pooling) and reports ridge accuracy.
|
||||||
|
usage: ~/anvenv/bin/python score_features.py <weights.bin> <K> [--train 6000]
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
sys.path.insert(0, "/mnt/c/knp/altai-objects")
|
||||||
|
import retina_proto as rp # noqa: E402
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("weights")
|
||||||
|
ap.add_argument("k", type=int)
|
||||||
|
ap.add_argument("--train", type=int, default=6000)
|
||||||
|
a = ap.parse_args()
|
||||||
|
rp.DOG = rp.gauss(0.5) - rp.gauss(1.5)
|
||||||
|
w = np.fromfile(a.weights, np.float32).reshape(a.k, -1)
|
||||||
|
d = "/home/int/run/cifar/vehicles"
|
||||||
|
tr, va = np.load(f"{d}/train.npz"), np.load(f"{d}/val.npz")
|
||||||
|
xt, yt = tr["rgb"][:a.train], tr["labels"][:a.train].astype(int)
|
||||||
|
xv, yv = va["rgb"], va["labels"].astype(int)
|
||||||
|
ft = rp.encode(rp.retina(xt), w, 0.0, 2, centre=True)
|
||||||
|
fv = rp.encode(rp.retina(xv), w, 0.0, 2, centre=True)
|
||||||
|
dead = int((ft.reshape(len(ft), 4, -1).sum((0, 1)) == 0).sum())
|
||||||
|
print(f"{a.weights}: ridge validation " + " | ".join(
|
||||||
|
f"lam {lam}: {rp.ridge_acc(ft, yt, fv, yv, lam, 4):.1f}%" for lam in (100, 1000)) + f"; silent detectors {dead}/{a.k}")
|
||||||
|
|
@ -52,3 +52,14 @@ MNIST, признаки coarse, 30 000 обучение, 10 000 проверка
|
||||||
- Три прохода по обучению (обучающая часть ×3 с новым перемешиванием, `*_x3_validation`, 54 000): K=512 — r1 b4 67,3%, r1 b8 65,0%, r3 b4 66,4%; K=1600 r1 b8 63,2%. Не помогает.
|
- Три прохода по обучению (обучающая часть ×3 с новым перемешиванием, `*_x3_validation`, 54 000): K=512 — r1 b4 67,3%, r1 b8 65,0%, r3 b4 66,4%; K=1600 r1 b8 63,2%. Не помогает.
|
||||||
- Вывод по объектам: в KNP дофамин по ошибке даёт 65–69% (колонки STDP — 64–65% на разреженных, ≈0% на плотных); ridge на тех же признаках — 74%. Вероятная причина разрыва — ограниченный и несимметричный диапазон весов KNP (−253…+92) и насыщение ресурса.
|
- Вывод по объектам: в KNP дофамин по ошибке даёт 65–69% (колонки STDP — 64–65% на разреженных, ≈0% на плотных); ridge на тех же признаках — 74%. Вероятная причина разрыва — ограниченный и несимметричный диапазон весов KNP (−253…+92) и насыщение ресурса.
|
||||||
- `KNP_W_RANGE=<min>,<max>` (MNIST и cats-dogs `construct_network.cpp`): диапазон обучаемых весов. Кошки/собаки r3 b4: ±120 — 66,1%, ±250 — 66,4% (r1 b8 — 61,8%), ±500 — 64,9% (по умолчанию −253…+92 — 69,1%). Гипотеза про диапазон не подтвердилась. Разброс весов большой (σ 91 при стандартном диапазоне, 177 при ±250); вероятнее, мешает онлайн-обучение за один проход с крупными шагами.
|
- `KNP_W_RANGE=<min>,<max>` (MNIST и cats-dogs `construct_network.cpp`): диапазон обучаемых весов. Кошки/собаки r3 b4: ±120 — 66,1%, ±250 — 66,4% (r1 b8 — 61,8%), ±500 — 64,9% (по умолчанию −253…+92 — 69,1%). Гипотеза про диапазон не подтвердилась. Разброс весов большой (σ 91 при стандартном диапазоне, 177 при ±250); вероятнее, мешает онлайн-обучение за один проход с крупными шагами.
|
||||||
|
|
||||||
|
## Утро: всё на платформе — распознавание транспорта (цель — демо для беспилотников)
|
||||||
|
|
||||||
|
Решения пользователя: живое демо; тезис «нейроморфный Алтай заменяет нейросеть для распознавания объектов в беспилотных системах»; всё (и признаки, и обучение) должно работать в KNP.
|
||||||
|
|
||||||
|
- Скорость KNP на свёрточном слое (`altai-objects/bench_conv.cpp`, однопоточный бэкенд, случайный вход 0,4): K=32 шаг 2 — 0,06 с/картинку, 0,2 ГБ; K=100 шаг 2 — 0,25 с, 0,7 ГБ; K=200 шаг 2 — 0,95 с, 1,4 ГБ; K=100 шаг 1 — 1,8 с, 2,6 ГБ (~320 байт на синапс).
|
||||||
|
- Данные: CIFAR-10 с Hugging Face (`uoft-cs/cifar10`, parquet; официальный сервер давал ~37 КБ/с). `altai-objects/prepare_vehicles.py` → `~/run/cifar/vehicles`: самолёт/автомобиль/корабль/грузовик; train 16 000 (первые 4000 каждого класса), val 4000 (последние 1000), test 4000 (официальный, не просмотрен). pyarrow установлен в `~/anvenv`.
|
||||||
|
- Проверка замысла в numpy (`altai-objects/retina_proto.py`): сетчатка ON/OFF (DoG σ 0,5/1,5, усиление 4) → онлайн WTA на кусочках 6×6×6 → вычитание среднего узора (линейно) → пулинг по четвертям → ridge. Кошки/собаки v3, K=200 шаг 2: 70,2% (с нормировкой и ZCA при том же размере — 73,6%). Транспорт: **76,0%**.
|
||||||
|
- `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% входов спайкают за шаг.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"""Build an isolated CPU single-threaded backend with an error-driven (delta-rule) dopamine plasticity option.
|
"""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,
|
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,
|
each trainable synapse whose presynaptic spike arrived KNP_DELTA_LAG steps earlier changes its synaptic resource by
|
||||||
whether or not the neuron itself spiked; stability is not used. Without the variable the backend behaves like
|
rate * D, whether or not the neuron itself spiked; stability is not used. KNP_DELTA_DECAY=<d> additionally changes
|
||||||
|
every other synapse of that neuron by -rate * D * d (competitive learning). Without the variable the backend behaves like
|
||||||
the stock one. Original sources and libraries are not modified.
|
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
|
usage: python3 build_delta_cpu.py → /home/int/run/delta-cpu/libknp-cpu-delta-backend.so
|
||||||
|
|
@ -30,6 +31,13 @@ s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=<rate>),
|
||||||
const char *value = std::getenv("KNP_DELTA_LAG");
|
const char *value = std::getenv("KNP_DELTA_LAG");
|
||||||
return value ? std::stol(value) : 0L;
|
return value ? std::stol(value) : 0L;
|
||||||
}();
|
}();
|
||||||
|
// Competitive (winner-take-all) learning: with KNP_DELTA_DECAY=<d>, synapses whose input did not arrive change by
|
||||||
|
// -rate * D * d, so a rewarded neuron moves its weights towards the current input pattern.
|
||||||
|
static const float delta_decay = []
|
||||||
|
{
|
||||||
|
const char *value = std::getenv("KNP_DELTA_DECAY");
|
||||||
|
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");
|
||||||
|
|
@ -53,6 +61,8 @@ s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=<rate>),
|
||||||
// The presynaptic spike reaches the neuron on this step.
|
// The presynaptic spike reaches the neuron on this step.
|
||||||
if (lag == DELTA_LAG)
|
if (lag == DELTA_LAG)
|
||||||
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)
|
||||||
|
synapse.get().rule_.synaptic_resource_ -= delta_rate * neuron.dopamine_value_ * delta_decay;
|
||||||
}
|
}
|
||||||
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