ON/OFF input coding for mnist-learn: KNP_MNIST_INPUT_SIZE, dataset builder, simulator --pix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
int 2026-10-14 18:35:27 +03:00
parent 979aa90bb4
commit db4b1d08b8
6 changed files with 90 additions and 4 deletions

View file

@ -42,7 +42,7 @@ Dataset process_dataset(ModelDescription const& model_desc)
// Process images and labels into spike representations. // Process images and labels into spike representations.
dataset.process_labels_and_images( dataset.process_labels_and_images(
images_stream, labels_stream, model_desc.train_images_amount_ + model_desc.inference_images_amount_, images_stream, labels_stream, model_desc.train_images_amount_ + model_desc.inference_images_amount_,
classes_amount, input_size, steps_per_image, classes_amount, get_input_size(), steps_per_image,
dataset.make_incrementing_image_to_spikes_converter(active_steps, state_increment_factor)); dataset.make_incrementing_image_to_spikes_converter(active_steps, state_increment_factor));
// Split dataset into training and inference portions. // Split dataset into training and inference portions.

View file

@ -22,6 +22,8 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <cstdlib>
#include <string>
/** /**
@ -36,7 +38,22 @@ constexpr size_t classes_amount = 10;
* *
* @details MNIST images are 28×28 pixels, resulting in 784 total pixels per image. * @details MNIST images are 28×28 pixels, resulting in 784 total pixels per image.
*/ */
constexpr size_t input_size = 28 * 28; constexpr size_t mnist_input_size = 28 * 28;
/**
* @brief Actual input size: `KNP_MNIST_INPUT_SIZE` environment variable or 784.
*
* @details ON/OFF coding stores two channels per pixel, so the dataset has 2 × 784 = 1568 bytes per image.
*/
inline size_t get_input_size()
{
static const size_t size = []
{
const char *value = std::getenv("KNP_MNIST_INPUT_SIZE");
return value ? static_cast<size_t>(std::stoul(value)) : mnist_input_size;
}();
return size;
}
/** /**
* @brief Total number of simulation steps per input image. * @brief Total number of simulation steps per input image.

View file

@ -122,7 +122,7 @@ static NetworkPopulations create_populations(NetworkConstructor &constructor)
constructor.add_population(default_neuron, classes_amount, PopulationRole::NORMAL, false, "GATE"); constructor.add_population(default_neuron, classes_amount, PopulationRole::NORMAL, false, "GATE");
// Create raster population for channelized image processing (28x28 = 784 neurons). // Create raster population for channelized image processing (28x28 = 784 neurons).
const auto &raster_pop = constructor.add_channeled_population(input_size, true); const auto &raster_pop = constructor.add_channeled_population(get_input_size(), true);
// Create target population for label processing (10 neurons for 10 digit classes). // Create target population for label processing (10 neurons for 10 digit classes).
const auto &target_pop = constructor.add_channeled_population(classes_amount, false); const auto &target_pop = constructor.add_channeled_population(classes_amount, false);

View file

@ -91,7 +91,7 @@ static NetworkPopulations create_populations(NetworkConstructor &constructor)
constructor.add_population(default_neuron, classes_amount, PopulationRole::NORMAL, false, "GATE"); constructor.add_population(default_neuron, classes_amount, PopulationRole::NORMAL, false, "GATE");
// Create raster population for channelized image processing (28x28 = 784 neurons). // Create raster population for channelized image processing (28x28 = 784 neurons).
const auto &raster_pop = constructor.add_channeled_population(input_size, true); const auto &raster_pop = constructor.add_channeled_population(get_input_size(), true);
// Create target population for label processing (10 neurons for 10 digit classes). // Create target population for label processing (10 neurons for 10 digit classes).
const auto &target_pop = constructor.add_channeled_population(classes_amount, false); const auto &target_pop = constructor.add_channeled_population(classes_amount, false);

65
onoff/prepare_onoff.py Normal file
View file

@ -0,0 +1,65 @@
"""Build ON/OFF-coded MNIST datasets for mnist_learn_example (KNP_MNIST_INPUT_SIZE=1568).
Each image becomes 1568 bytes: 784 ON pixels (unchanged) followed by 784 OFF pixels.
OFF modes:
full - 255 - p everywhere;
half - (255 - p) / 2 everywhere;
local - 255 - p only within `--radius` pixels of the digit (p > 0), zero elsewhere.
usage: python prepare_onoff.py <src.bin> <src.target> <out_prefix> --mode local [--radius 2]
Writes <out_prefix>.bin and <out_prefix>.target (labels copied as is).
"""
import argparse
import shutil
import numpy as np
SIDE = 28
def dilate(mask, radius):
out = mask.copy()
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
if dy * dy + dx * dx > radius * radius:
continue
shifted = np.zeros_like(mask)
ys, yd = (slice(dy, None), slice(None, -dy or None)) if dy >= 0 else (slice(None, dy), slice(-dy, None))
xs, xd = (slice(dx, None), slice(None, -dx or None)) if dx >= 0 else (slice(None, dx), slice(-dx, None))
shifted[:, ys, xs] = mask[:, yd, xd]
out |= shifted
return out
def off_channel(imgs, mode, radius):
inv = 255 - imgs.astype(np.int32)
if mode == "full":
off = inv
elif mode == "half":
off = inv // 2
elif mode == "local":
off = inv * dilate(imgs > 0, radius)
else:
raise ValueError(mode)
return off.astype(np.uint8)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("images")
ap.add_argument("labels")
ap.add_argument("out")
ap.add_argument("--mode", choices=["full", "half", "local"], required=True)
ap.add_argument("--radius", type=int, default=2)
a = ap.parse_args()
imgs = np.fromfile(a.images, dtype=np.uint8).reshape(-1, SIDE, SIDE)
off = off_channel(imgs, a.mode, a.radius)
out = np.concatenate([imgs.reshape(len(imgs), -1), off.reshape(len(imgs), -1)], axis=1)
out.tofile(a.out + ".bin")
shutil.copyfile(a.labels, a.out + ".target")
on_spk, off_spk = imgs.mean() / 255 * 10 * SIDE * SIDE, off.mean() / 255 * 10 * SIDE * SIDE
print(f"{a.out}: {len(out)} images, ~{on_spk:.0f} ON / ~{off_spk:.0f} OFF spikes per image")
if __name__ == "__main__":
main()

View file

@ -4,6 +4,7 @@ Reproduces KNP inference: raster -> INPUT (delay 1), optional INPUT -> INPUT inh
INPUT -> OUTPUT column vote (delay 1), 15 steps per image, potentials carried across images. INPUT -> OUTPUT column vote (delay 1), 15 steps per image, potentials carried across images.
usage: python3 altai_sim.py <dump_dir> <images.bin> <labels.target> [--inhib 255] [--thr 40,60,80] [--first N] [--test 10000] usage: python3 altai_sim.py <dump_dir> <images.bin> <labels.target> [--inhib 255] [--thr 40,60,80] [--first N] [--test 10000]
[--pix 1568] (ON/OFF input, KNP_MNIST_INPUT_SIZE)
""" """
import argparse import argparse
import csv import csv
@ -91,7 +92,10 @@ def main():
ap.add_argument("--first", type=int, default=None, help="index of first test image (default: last --test)") ap.add_argument("--first", type=int, default=None, help="index of first test image (default: last --test)")
ap.add_argument("--test", type=int, default=10000) ap.add_argument("--test", type=int, default=10000)
ap.add_argument("--thr", default="", help="comma-separated thresholds to sweep instead of the dumped ones") ap.add_argument("--thr", default="", help="comma-separated thresholds to sweep instead of the dumped ones")
ap.add_argument("--pix", type=int, default=784, help="input size per image (1568 for ON/OFF)")
a = ap.parse_args() a = ap.parse_args()
global PIX
PIX = a.pix
w, thr = load_dump(a.dump) w, thr = load_dump(a.dump)
total = len(open(a.labels).read().split()) total = len(open(a.labels).read().split())
first = total - a.test if a.first is None else a.first first = total - a.test if a.first is None else a.first