diff --git a/examples/examples/mnist-learn/dataset.cpp b/examples/examples/mnist-learn/dataset.cpp index c5c3e0b..33af791 100644 --- a/examples/examples/mnist-learn/dataset.cpp +++ b/examples/examples/mnist-learn/dataset.cpp @@ -42,7 +42,7 @@ Dataset process_dataset(ModelDescription const& model_desc) // Process images and labels into spike representations. dataset.process_labels_and_images( 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)); // Split dataset into training and inference portions. diff --git a/examples/examples/mnist-learn/global_config.h b/examples/examples/mnist-learn/global_config.h index cf0d4c3..4ca2e4a 100644 --- a/examples/examples/mnist-learn/global_config.h +++ b/examples/examples/mnist-learn/global_config.h @@ -22,6 +22,8 @@ #pragma once #include +#include +#include /** @@ -36,7 +38,22 @@ constexpr size_t classes_amount = 10; * * @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(std::stoul(value)) : mnist_input_size; + }(); + return size; +} /** * @brief Total number of simulation steps per input image. diff --git a/examples/examples/mnist-learn/models/altai/construct_network.cpp b/examples/examples/mnist-learn/models/altai/construct_network.cpp index 41d57c4..e380305 100644 --- a/examples/examples/mnist-learn/models/altai/construct_network.cpp +++ b/examples/examples/mnist-learn/models/altai/construct_network.cpp @@ -122,7 +122,7 @@ static NetworkPopulations create_populations(NetworkConstructor &constructor) constructor.add_population(default_neuron, classes_amount, PopulationRole::NORMAL, false, "GATE"); // 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). const auto &target_pop = constructor.add_channeled_population(classes_amount, false); diff --git a/examples/examples/mnist-learn/models/blifat/construct_network.cpp b/examples/examples/mnist-learn/models/blifat/construct_network.cpp index 0a60949..7d7eb6e 100644 --- a/examples/examples/mnist-learn/models/blifat/construct_network.cpp +++ b/examples/examples/mnist-learn/models/blifat/construct_network.cpp @@ -91,7 +91,7 @@ static NetworkPopulations create_populations(NetworkConstructor &constructor) constructor.add_population(default_neuron, classes_amount, PopulationRole::NORMAL, false, "GATE"); // 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). const auto &target_pop = constructor.add_channeled_population(classes_amount, false); diff --git a/onoff/prepare_onoff.py b/onoff/prepare_onoff.py new file mode 100644 index 0000000..2b5fa3a --- /dev/null +++ b/onoff/prepare_onoff.py @@ -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 --mode local [--radius 2] +Writes .bin and .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() diff --git a/viz/altai_sim.py b/viz/altai_sim.py index 513c307..c489e6d 100644 --- a/viz/altai_sim.py +++ b/viz/altai_sim.py @@ -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. usage: python3 altai_sim.py [--inhib 255] [--thr 40,60,80] [--first N] [--test 10000] + [--pix 1568] (ON/OFF input, KNP_MNIST_INPUT_SIZE) """ import argparse 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("--test", type=int, default=10000) 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() + global PIX + PIX = a.pix w, thr = load_dump(a.dump) total = len(open(a.labels).read().split()) first = total - a.test if a.first is None else a.first