fix: audit and fix nisps-core extraction issues
- Remove platform-specific code (ARM_MATH_CM33, XMOS __XS3A__, std::printf) - Add set_output()/set_outputs()/add_example() API for programmatic training - Fix release build crash: side effect inside assert() for loss function init - Replace fake smoke test with real convergence tests (5 tests, all pass) - Rewrite example to demonstrate actual training with real output - Update README, CHANGELOG, and extraction plan to match reality
This commit is contained in:
parent
be85a5cd71
commit
45193a2c01
12 changed files with 426 additions and 233 deletions
|
|
@ -1,6 +1,8 @@
|
|||
# NISPS Core Extraction Plan
|
||||
|
||||
Extract a platform-agnostic C++17 controller library from MEMLNaut-NISPS. This is **not** a synth or audio engine - it's a parameter mapping engine: control data in → ML → control data out. Use it to drive synths, effects, lights, robots, whatever.
|
||||
Extract a platform-agnostic C++20 controller library from MEMLNaut-NISPS. This is **not** a synth or audio engine - it's a parameter mapping engine: control data in → ML → control data out. Use it to drive synths, effects, lights, robots, whatever.
|
||||
|
||||
> **Note**: Originally planned as C++17, upgraded to C++20 during implementation to use `std::span` for efficient array views.
|
||||
|
||||
## What This Is
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,24 @@ All notable changes to NISPS Core will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [0.2.0] - 2026-02-08
|
||||
|
||||
### Added
|
||||
- `set_output()` / `set_outputs()` methods for programmatic output control
|
||||
- `add_example()` method for adding training pairs without interactive workflow
|
||||
- Real training convergence tests (identity mapping, multi-output)
|
||||
- Working example with actual training (`examples/simple_mapping.cpp`)
|
||||
|
||||
### Fixed
|
||||
- Release build crash: loss function pointer not initialized due to side effect inside `assert()` (mlp_impl.hpp)
|
||||
- Removed ARM CMSIS-DSP conditional code from node.hpp (`ARM_MATH_CM33`)
|
||||
- Removed XMOS `__XS3A__` conditional attributes from utils.hpp and loss.hpp
|
||||
- Removed `std::printf` logging from dataset_impl.hpp (use IML logger callback instead)
|
||||
|
||||
### Changed
|
||||
- README updated to document new APIs and remove false claims
|
||||
- CHANGELOG rewritten to accurately reflect library state
|
||||
|
||||
## [0.1.0] - 2026-02-08
|
||||
|
||||
### Added
|
||||
|
|
@ -15,39 +33,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Training and inference modes
|
||||
- Logging callback support
|
||||
- CMake build system for tests
|
||||
- Basic XOR test example
|
||||
- Comprehensive README documentation
|
||||
|
||||
### Changed
|
||||
- Converted from Arduino/RP2040 embedded code to platform-agnostic C++
|
||||
- Updated from C++17 to C++20 (required for std::span)
|
||||
- Removed all platform-specific code (Serial, SD card, Pico SDK)
|
||||
- Updated to C++20 (required for std::span)
|
||||
- Converted to header-only implementation pattern
|
||||
- Added `nisps` namespace to all code
|
||||
- Changed file extensions from .h/.cpp to .hpp
|
||||
|
||||
### Removed
|
||||
- Arduino and RP2040 dependencies
|
||||
- Serial debugging (replaced with optional callbacks)
|
||||
- SD card save/load functionality
|
||||
- Binary serialization (temporarily disabled)
|
||||
- Audio synthesis code (nisps-core is control-only)
|
||||
|
||||
### Technical Details
|
||||
- **Language**: C++20
|
||||
- **Dependencies**: None (pure standard library)
|
||||
- **Architecture**: Header-only library
|
||||
- **Lines of code**: ~3,500
|
||||
- **Build system**: CMake 3.14+
|
||||
- **Optimizer**: RMSProp with gradient clipping
|
||||
- **Activation functions**: Sigmoid, ReLU, tanh, linear, hardsigmoid, hardswish, hardtanh
|
||||
- **Loss functions**: MSE, categorical cross-entropy
|
||||
|
||||
### Known Issues
|
||||
- Binary serialization methods commented out (not needed for basic functionality)
|
||||
- No example for actual training workflow yet (requires interactive I/O)
|
||||
- Documentation references parent project URLs (MEMLNaut-NISPS)
|
||||
|
||||
### Migration from MEMLNaut-NISPS
|
||||
If you're using the old embedded IMLInterface class:
|
||||
```cpp
|
||||
|
|
@ -57,5 +55,3 @@ IMLInterface iml(n_inputs, n_outputs);
|
|||
// New (nisps-core):
|
||||
nisps::IML<float> iml(n_inputs, n_outputs);
|
||||
```
|
||||
|
||||
All method names remain the same, just add the namespace.
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ NISPS core is a **parameter mapping engine**, not a synthesizer. It takes N inpu
|
|||
## Features
|
||||
|
||||
- **Header-only**: No compilation needed, just include and use
|
||||
- **Platform-agnostic**: Pure C++20, works anywhere
|
||||
- **No dependencies**: Only standard library
|
||||
- **Platform-agnostic**: Pure C++20 with standard library only
|
||||
- **Interactive learning**: Train by demonstration, not by code
|
||||
- **Programmatic training**: `add_example()` API for non-interactive use
|
||||
- **Lightweight**: ~3,500 lines of optimized neural network code
|
||||
- **Flexible**: Map 1-100 inputs to 1-100 outputs
|
||||
|
||||
|
|
@ -55,25 +55,35 @@ void update(float x, float y) {
|
|||
}
|
||||
```
|
||||
|
||||
### Training Workflow
|
||||
### Programmatic Training
|
||||
|
||||
```cpp
|
||||
// 1. Enter training mode
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// 2. Set input position
|
||||
// 2. Add examples directly
|
||||
float in1[] = {0.1f, 0.1f}; float out1[] = {0.9f, 0.1f, 0.5f, 0.8f};
|
||||
float in2[] = {0.9f, 0.9f}; float out2[] = {0.1f, 0.9f, 0.2f, 0.3f};
|
||||
iml.add_example(in1, 2, out1, 4);
|
||||
iml.add_example(in2, 2, out2, 4);
|
||||
|
||||
// 3. Exit training mode (automatically trains the network)
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
```
|
||||
|
||||
### Interactive Training (hardware/UI)
|
||||
|
||||
```cpp
|
||||
// For interactive systems with physical controls:
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
iml.set_input(0, 0.3f);
|
||||
iml.set_input(1, 0.7f);
|
||||
iml.save_example(); // Stops inference
|
||||
// ... user adjusts output controls ...
|
||||
iml.set_output(0, 0.8f); // Or read from hardware
|
||||
iml.save_example(); // Stores the input->output mapping
|
||||
|
||||
// 3. Save example (call twice per example)
|
||||
iml.save_example(); // First call: stops inference, user positions output
|
||||
// ... user adjusts outputs manually to desired values ...
|
||||
iml.save_example(); // Second call: stores the input->output mapping
|
||||
|
||||
// 4. Repeat for multiple input positions
|
||||
// ... add more examples ...
|
||||
|
||||
// 5. Exit training mode (automatically trains the network)
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
```
|
||||
|
||||
|
|
@ -97,6 +107,8 @@ nisps::IML<Float>(
|
|||
```cpp
|
||||
void set_input(size_t index, Float value); // Set single input (0-1 range)
|
||||
void set_inputs(const Float* values, size_t count); // Set multiple inputs
|
||||
void set_output(size_t index, Float value); // Set single output (for training)
|
||||
void set_outputs(const Float* values, size_t count); // Set multiple outputs
|
||||
const Float* get_outputs() const; // Get output array
|
||||
void process(); // Run inference
|
||||
```
|
||||
|
|
@ -106,7 +118,9 @@ void process(); // Run inference
|
|||
```cpp
|
||||
enum class Mode { Inference, Training };
|
||||
void set_mode(Mode mode); // Switch modes
|
||||
void save_example(); // Store input->output pair
|
||||
void add_example(const Float* in, size_t n_in, // Add training pair directly
|
||||
const Float* out, size_t n_out);
|
||||
void save_example(); // Interactive: store input->output pair
|
||||
void clear_dataset(); // Clear training data
|
||||
void randomise_weights(); // Randomize for exploration
|
||||
```
|
||||
|
|
@ -152,20 +166,14 @@ ctest --output-on-failure
|
|||
4. **RMSProp optimizer**: Fast convergence for interactive training
|
||||
5. **Gradient clipping**: Prevents numerical instability
|
||||
|
||||
## Performance
|
||||
|
||||
Typical performance on modern hardware:
|
||||
- **Inference**: 1-10 µs for small networks (2-10-10-4)
|
||||
- **Training**: 10-100 ms for 100 examples, 1000 iterations
|
||||
- **Memory**: ~1 KB per hidden neuron
|
||||
|
||||
## Examples
|
||||
|
||||
See `test/main.cpp` for a complete example. More examples coming soon:
|
||||
- Audio synthesis control
|
||||
- Game parameter mapping
|
||||
- Sensor fusion for robotics
|
||||
- MIDI controller mapping
|
||||
See `examples/simple_mapping.cpp` for a complete working example that demonstrates:
|
||||
- Untrained inference
|
||||
- Programmatic training with `add_example()`
|
||||
- Interactive training workflow with `save_example()` + `set_output()`
|
||||
|
||||
See `test/main.cpp` for tests including convergence verification.
|
||||
|
||||
## Origin
|
||||
|
||||
|
|
@ -207,4 +215,4 @@ https://musicallyembodiedml.github.io/memlnaut/approaches/nisps
|
|||
|
||||
- **Issues**: File at parent project (MEMLNaut-NISPS repo)
|
||||
- **Docs**: https://musicallyembodiedml.github.io/memlnaut/
|
||||
- **Examples**: See `examples/` directory (coming soon)
|
||||
- **Examples**: See `examples/` directory
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* @file simple_mapping.cpp
|
||||
* @brief Simple example of using NISPS Core for parameter mapping
|
||||
* @brief Example of using NISPS Core for parameter mapping
|
||||
*
|
||||
* This example shows how to use NISPS Core to map 2D joystick input
|
||||
* to synthesizer parameters through interactive training.
|
||||
* Demonstrates creating a network, adding training examples
|
||||
* programmatically, training, and using inference.
|
||||
*
|
||||
* Compile: g++ -std=c++20 -I../include simple_mapping.cpp -o simple_mapping
|
||||
*/
|
||||
|
|
@ -12,119 +12,147 @@
|
|||
#include <iostream>
|
||||
#include <iomanip>
|
||||
|
||||
void print_separator() {
|
||||
std::cout << "\n" << std::string(60, '=') << "\n\n";
|
||||
}
|
||||
|
||||
void demo_inference() {
|
||||
std::cout << "=== NISPS Core Demo: Inference Mode ===\n\n";
|
||||
std::cout << "=== Demo 1: Untrained Inference ===\n\n";
|
||||
|
||||
// Create IML with 2 inputs (x, y), 4 outputs (filter, resonance, attack, release)
|
||||
// Hidden layers: [8, 8] - smaller network for faster training
|
||||
// Create IML: 2 inputs (x, y) -> 4 outputs (filter, resonance, attack, release)
|
||||
nisps::IML<float> iml(2, 4, {8, 8}, 2000, 0.5f, 0.0001f);
|
||||
|
||||
std::cout << "Created IML with:\n";
|
||||
std::cout << " Inputs: " << iml.num_inputs() << " (x, y joystick)\n";
|
||||
std::cout << " Outputs: " << iml.num_outputs() << " (filter, resonance, attack, release)\n";
|
||||
std::cout << " Hidden: [8, 8]\n";
|
||||
std::cout << "Created IML with " << iml.num_inputs() << " inputs, "
|
||||
<< iml.num_outputs() << " outputs\n\n";
|
||||
|
||||
print_separator();
|
||||
|
||||
// Test some input positions
|
||||
std::cout << "Testing inference (untrained network):\n\n";
|
||||
std::cout << std::fixed << std::setprecision(3);
|
||||
|
||||
struct TestPoint {
|
||||
float x, y;
|
||||
const char* description;
|
||||
};
|
||||
|
||||
TestPoint test_points[] = {
|
||||
{0.0f, 0.0f, "Bottom-left corner"},
|
||||
{1.0f, 0.0f, "Bottom-right corner"},
|
||||
{0.0f, 1.0f, "Top-left corner"},
|
||||
{1.0f, 1.0f, "Top-right corner"},
|
||||
// Untrained network produces random-ish outputs
|
||||
struct TestPoint { float x, y; const char* label; };
|
||||
TestPoint points[] = {
|
||||
{0.0f, 0.0f, "Bottom-left"},
|
||||
{1.0f, 1.0f, "Top-right"},
|
||||
{0.5f, 0.5f, "Center"},
|
||||
};
|
||||
|
||||
for (const auto& point : test_points) {
|
||||
iml.set_input(0, point.x);
|
||||
iml.set_input(1, point.y);
|
||||
std::cout << std::fixed << std::setprecision(3);
|
||||
for (const auto& p : points) {
|
||||
iml.set_input(0, p.x);
|
||||
iml.set_input(1, p.y);
|
||||
iml.process();
|
||||
|
||||
const float* outputs = iml.get_outputs();
|
||||
|
||||
std::cout << point.description << " (" << point.x << ", " << point.y << "):\n";
|
||||
std::cout << " Filter: " << outputs[0] << "\n";
|
||||
std::cout << " Resonance: " << outputs[1] << "\n";
|
||||
std::cout << " Attack: " << outputs[2] << "\n";
|
||||
std::cout << " Release: " << outputs[3] << "\n\n";
|
||||
const float* out = iml.get_outputs();
|
||||
std::cout << " " << p.label << " (" << p.x << ", " << p.y << ") -> ["
|
||||
<< out[0] << ", " << out[1] << ", " << out[2] << ", " << out[3] << "]\n";
|
||||
}
|
||||
|
||||
print_separator();
|
||||
std::cout << "Note: Untrained networks produce random-ish outputs.\n";
|
||||
std::cout << "In a real application, you would:\n";
|
||||
std::cout << " 1. Enter training mode\n";
|
||||
std::cout << " 2. Move joystick to various positions\n";
|
||||
std::cout << " 3. Adjust output parameters to desired values\n";
|
||||
std::cout << " 4. Call save_example() to store each mapping\n";
|
||||
std::cout << " 5. Exit training mode to train the network\n";
|
||||
std::cout << " 6. Use the trained network for real-time control\n";
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
void demo_training() {
|
||||
std::cout << "\n=== NISPS Core Demo: Training Workflow ===\n\n";
|
||||
std::cout << "=== Demo 2: Training a Mapping ===\n\n";
|
||||
|
||||
// Create a simple 2-input, 1-output network
|
||||
nisps::IML<float> iml(2, 1, {4}, 1000, 1.0f, 0.001f);
|
||||
|
||||
// Set up logging
|
||||
// 2 inputs -> 2 outputs, small network
|
||||
nisps::IML<float> iml(2, 2, {8, 8}, 3000, 1.0f, 0.00001f);
|
||||
iml.set_logger([](const char* msg) {
|
||||
std::cout << "[IML] " << msg << "\n";
|
||||
std::cout << " [nisps] " << msg << "\n";
|
||||
});
|
||||
|
||||
std::cout << "Teaching the network: output = 1 when both inputs > 0.5\n";
|
||||
std::cout << "(Similar to AND gate, but with gradual transitions)\n\n";
|
||||
// Goal: teach the network a cross-mapping
|
||||
// (low, low) -> (low output1, high output2)
|
||||
// (high, high) -> (high output1, low output2)
|
||||
std::cout << "Teaching cross-mapping:\n";
|
||||
std::cout << " (low, low) -> (low, high)\n";
|
||||
std::cout << " (high, high) -> (high, low)\n\n";
|
||||
|
||||
// Enter training mode
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// In a real interactive system, the user would:
|
||||
// 1. Move joystick to a position
|
||||
// 2. Call save_example() - this stops inference
|
||||
// 3. Manually adjust output to desired value
|
||||
// 4. Call save_example() again - this stores the mapping
|
||||
// Add examples using the programmatic API
|
||||
float in1[] = {0.1f, 0.1f}; float out1[] = {0.1f, 0.9f};
|
||||
float in2[] = {0.9f, 0.9f}; float out2[] = {0.9f, 0.1f};
|
||||
float in3[] = {0.5f, 0.5f}; float out3[] = {0.5f, 0.5f};
|
||||
float in4[] = {0.1f, 0.9f}; float out4[] = {0.3f, 0.7f};
|
||||
float in5[] = {0.9f, 0.1f}; float out5[] = {0.7f, 0.3f};
|
||||
|
||||
// For this demo, we'll simulate the workflow by directly
|
||||
// manipulating the dataset (this is not the normal API usage)
|
||||
iml.add_example(in1, 2, out1, 2);
|
||||
iml.add_example(in2, 2, out2, 2);
|
||||
iml.add_example(in3, 2, out3, 2);
|
||||
iml.add_example(in4, 2, out4, 2);
|
||||
iml.add_example(in5, 2, out5, 2);
|
||||
|
||||
std::cout << "Adding training examples...\n";
|
||||
std::cout << "(In a real system, the user would demonstrate these interactively)\n\n";
|
||||
std::cout << "Added 5 training examples.\n";
|
||||
|
||||
// Note: In actual usage, you'd call save_example() twice per example
|
||||
// and the user would position the outputs between calls.
|
||||
// Here we're just demonstrating the concept.
|
||||
|
||||
// Exit training mode (triggers training)
|
||||
std::cout << "\nExiting training mode (training will occur automatically)...\n";
|
||||
// Switching to inference triggers training
|
||||
std::cout << "Training...\n";
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
print_separator();
|
||||
std::cout << "Demo complete!\n";
|
||||
std::cout << "\nFor real training, see the MEMLNaut-NISPS hardware implementation\n";
|
||||
std::cout << "where users physically move controls and save mappings.\n";
|
||||
// Now test: the network should have learned the mapping
|
||||
std::cout << "\nResults after training:\n";
|
||||
std::cout << std::fixed << std::setprecision(3);
|
||||
|
||||
struct TestCase { float in[2]; float expected[2]; const char* label; };
|
||||
TestCase tests[] = {
|
||||
{{0.1f, 0.1f}, {0.1f, 0.9f}, "Trained point"},
|
||||
{{0.9f, 0.9f}, {0.9f, 0.1f}, "Trained point"},
|
||||
{{0.5f, 0.5f}, {0.5f, 0.5f}, "Trained point"},
|
||||
{{0.3f, 0.3f}, {0.0f, 0.0f}, "Interpolated"}, // Not trained on this
|
||||
};
|
||||
|
||||
for (const auto& t : tests) {
|
||||
iml.set_input(0, t.in[0]);
|
||||
iml.set_input(1, t.in[1]);
|
||||
iml.process();
|
||||
const float* out = iml.get_outputs();
|
||||
std::cout << " (" << t.in[0] << ", " << t.in[1] << ") -> ("
|
||||
<< out[0] << ", " << out[1] << ")";
|
||||
if (t.expected[0] > 0.0f) {
|
||||
std::cout << " expected ~(" << t.expected[0] << ", " << t.expected[1] << ")";
|
||||
}
|
||||
std::cout << " [" << t.label << "]\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
void demo_interactive_workflow() {
|
||||
std::cout << "=== Demo 3: Interactive Workflow (simulated) ===\n\n";
|
||||
|
||||
// This demonstrates the two-step save_example() workflow
|
||||
// used in the original MEMLNaut hardware
|
||||
nisps::IML<float> iml(1, 1, {4}, 2000, 1.0f, 0.001f);
|
||||
iml.set_logger([](const char* msg) {
|
||||
std::cout << " [nisps] " << msg << "\n";
|
||||
});
|
||||
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// Simulate the interactive workflow:
|
||||
// 1. Set input position
|
||||
// 2. save_example() -> stops inference
|
||||
// 3. set_output() -> user positions the desired output
|
||||
// 4. save_example() -> stores the mapping
|
||||
|
||||
struct Demo { float in; float out; };
|
||||
Demo demos[] = {{0.2f, 0.2f}, {0.5f, 0.5f}, {0.8f, 0.8f}};
|
||||
|
||||
for (const auto& d : demos) {
|
||||
iml.set_input(0, d.in);
|
||||
iml.save_example(); // Step 1: stop inference
|
||||
iml.set_output(0, d.out); // Step 2: user sets desired output
|
||||
iml.save_example(); // Step 3: store the mapping
|
||||
std::cout << " Saved: " << d.in << " -> " << d.out << "\n";
|
||||
}
|
||||
|
||||
std::cout << "\nSwitching to inference (triggers training)...\n";
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
std::cout << std::fixed << std::setprecision(3);
|
||||
for (float x = 0.0f; x <= 1.0f; x += 0.25f) {
|
||||
iml.set_input(0, x);
|
||||
iml.process();
|
||||
std::cout << " " << x << " -> " << iml.get_outputs()[0] << "\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "\n";
|
||||
std::cout << "╔══════════════════════════════════════════════════════════╗\n";
|
||||
std::cout << "║ NISPS Core Examples ║\n";
|
||||
std::cout << "║ Neural Interactive Shaping of Parameter Spaces ║\n";
|
||||
std::cout << "╚══════════════════════════════════════════════════════════╝\n";
|
||||
std::cout << "\nNISPS Core - Parameter Mapping Examples\n";
|
||||
std::cout << std::string(45, '=') << "\n\n";
|
||||
|
||||
demo_inference();
|
||||
demo_training();
|
||||
demo_interactive_workflow();
|
||||
|
||||
std::cout << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
#ifndef NISPS_DATASET_IMPL_HPP
|
||||
#define NISPS_DATASET_IMPL_HPP
|
||||
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
|
|
@ -42,7 +41,6 @@ inline bool Dataset::Add(const std::vector<float> &feature, const std::vector<fl
|
|||
if (data_size_ > 0) {
|
||||
if ((feature.size() != data_size_) ||
|
||||
(label.size() != output_size_)) {
|
||||
std::printf("Dataset- Wrong example size.\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +49,6 @@ inline bool Dataset::Add(const std::vector<float> &feature, const std::vector<fl
|
|||
if (replay_memory_enabled_) {
|
||||
RemoveOneExcessExample();
|
||||
} else {
|
||||
std::printf("Dataset- Max dataset size of %zu exceeded.\n", max_examples_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,8 +58,6 @@ inline bool Dataset::Add(const std::vector<float> &feature, const std::vector<fl
|
|||
timestamps_.push_back(current_timestamp_);
|
||||
current_timestamp_++;
|
||||
|
||||
std::printf("Dataset- Added example.\n");
|
||||
std::printf("Dataset- Feature size %zu, label size %zu.\n", features_.size(), labels_.size());
|
||||
_AdjustSizes();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -115,7 +110,6 @@ inline void Dataset::RemoveOneExcessExample() {
|
|||
features_.erase(features_.begin() + index_to_remove);
|
||||
labels_.erase(labels_.begin() + index_to_remove);
|
||||
timestamps_.erase(timestamps_.begin() + index_to_remove);
|
||||
std::printf("Dataset- Memory full, removing example at index %zu (mode %d).\n", index_to_remove, forget_mode_);
|
||||
}
|
||||
|
||||
inline void Dataset::Clear()
|
||||
|
|
@ -178,17 +172,12 @@ inline void Dataset::_AdjustSizes()
|
|||
inline void Dataset::ReplayMemory(bool enabled)
|
||||
{
|
||||
replay_memory_enabled_ = enabled;
|
||||
if (replay_memory_enabled_) {
|
||||
std::printf("Replay memory functionality enabled.\n");
|
||||
} else {
|
||||
std::printf("Replay memory functionality disabled.\n");
|
||||
}
|
||||
(void)replay_memory_enabled_;
|
||||
}
|
||||
|
||||
inline void Dataset::SetForgetMode(ForgetMode mode)
|
||||
{
|
||||
forget_mode_ = mode;
|
||||
std::printf("Forget mode set to %d.\n", mode);
|
||||
}
|
||||
|
||||
inline void Dataset::SetMaxExamples(size_t max)
|
||||
|
|
@ -206,7 +195,6 @@ inline void Dataset::SetMaxExamples(size_t max)
|
|||
break;
|
||||
}
|
||||
}
|
||||
std::printf("Max examples set to %zu.\n", max_examples_);
|
||||
}
|
||||
|
||||
inline std::pair<Dataset::DatasetVector, Dataset::DatasetVector> Dataset::Sample(bool with_bias)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ public:
|
|||
size_t num_inputs() const { return n_inputs_; }
|
||||
size_t num_outputs() const { return n_outputs_; }
|
||||
|
||||
// Set outputs directly (for programmatic training without hardware)
|
||||
void set_output(size_t index, Float value);
|
||||
void set_outputs(const Float* values, size_t count);
|
||||
|
||||
// Runtime
|
||||
void process();
|
||||
|
||||
|
|
@ -38,6 +42,7 @@ public:
|
|||
void set_mode(Mode mode);
|
||||
Mode get_mode() const { return mode_; }
|
||||
void save_example();
|
||||
void add_example(const Float* inputs, size_t n_in, const Float* outputs, size_t n_out);
|
||||
void clear_dataset();
|
||||
void randomise_weights();
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,21 @@ const Float* IML<Float>::get_outputs() const {
|
|||
return output_state_.data();
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::set_output(size_t index, Float value) {
|
||||
if (index >= n_outputs_) return;
|
||||
if (value < 0) value = 0;
|
||||
if (value > 1) value = 1;
|
||||
output_state_[index] = value;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::set_outputs(const Float* values, size_t count) {
|
||||
for (size_t i = 0; i < count && i < n_outputs_; ++i) {
|
||||
set_output(i, values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::process() {
|
||||
if (!perform_inference_ || !input_updated_) return;
|
||||
|
|
@ -112,6 +127,15 @@ void IML<Float>::save_example() {
|
|||
log("Example saved.");
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::add_example(const Float* inputs, size_t n_in, const Float* outputs, size_t n_out) {
|
||||
std::vector<Float> in_vec(inputs, inputs + std::min(n_in, n_inputs_));
|
||||
in_vec.resize(n_inputs_, static_cast<Float>(0));
|
||||
std::vector<Float> out_vec(outputs, outputs + std::min(n_out, n_outputs_));
|
||||
out_vec.resize(n_outputs_, static_cast<Float>(0));
|
||||
dataset_->Add(in_vec, out_vec);
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::clear_dataset() {
|
||||
if (mode_ == Mode::Training) {
|
||||
|
|
|
|||
|
|
@ -20,17 +20,8 @@
|
|||
// #include <string>
|
||||
|
||||
|
||||
#if defined(__XS3A__)
|
||||
|
||||
#define MLP_LOSS_FN __attribute__(( fptrgroup("mlp_loss") ))
|
||||
|
||||
#else
|
||||
|
||||
//#pragma message ( "PC compiler definitions enabled - check this is OK" )
|
||||
#define MLP_LOSS_FN
|
||||
|
||||
#endif
|
||||
|
||||
namespace nisps {
|
||||
|
||||
namespace loss {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,9 @@ void MLP<T>::CreateMLP(const std::vector<size_t> & layers_nodes,
|
|||
// Loss function selection
|
||||
loss::LossFunctionsManager<T> loss_mgr =
|
||||
loss::LossFunctionsManager<T>::Singleton();
|
||||
assert(loss_mgr.GetLossFunction(loss_function, &(this->loss_fn_)));
|
||||
bool loss_ok = loss_mgr.GetLossFunction(loss_function, &(this->loss_fn_));
|
||||
assert(loss_ok);
|
||||
(void)loss_ok;
|
||||
|
||||
for (size_t i = 0; i < m_layers_nodes.size() - 1; i++) {
|
||||
m_layers.emplace_back(Layer<T>(m_layers_nodes[i],
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@
|
|||
#include <span>
|
||||
#include <cstdio> // for FILE
|
||||
|
||||
#ifdef ARM_MATH_CM33
|
||||
#include <arm_math.h>
|
||||
#endif
|
||||
|
||||
#define CONSTANT_WEIGHT_INITIALIZATION 0
|
||||
|
||||
namespace nisps {
|
||||
|
|
@ -343,20 +339,9 @@ public:
|
|||
inline T GetInputInnerProdWithWeights(std::span<const T> input) {
|
||||
T res = 0;
|
||||
|
||||
#ifdef ARM_MATH_CM33
|
||||
// Use optimized CMSIS-DSP dot product (SIMD accelerated)
|
||||
arm_dot_prod_f32(
|
||||
(const float32_t*)input.data(),
|
||||
(const float32_t*)m_weights.data(),
|
||||
input.size(),
|
||||
(float32_t*)&res
|
||||
);
|
||||
#else
|
||||
// Fallback to manual loop
|
||||
for(size_t j=0; j < input.size(); j++) {
|
||||
res += input[j] * m_weights[j];
|
||||
}
|
||||
#endif
|
||||
|
||||
res += m_bias;
|
||||
inner_prod = res;
|
||||
|
|
|
|||
|
|
@ -37,12 +37,7 @@ enum ACTIVATION_FUNCTIONS {
|
|||
HARDTANH
|
||||
};
|
||||
|
||||
#if defined(__XS3A__)
|
||||
#define MLP_ACTIVATION_FN __attribute__(( fptrgroup("mlp_activation") ))
|
||||
#else
|
||||
//#pragma message ( "PC compiler definitions enabled - check this is OK" )
|
||||
#define MLP_ACTIVATION_FN
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @namespace utils
|
||||
|
|
|
|||
|
|
@ -1,68 +1,237 @@
|
|||
#include <nisps/nisps.hpp>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
|
||||
void log_callback(const char* msg) {
|
||||
std::cout << " [nisps] " << msg << "\n";
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "=== NISPS Core Test: XOR Training ===\n\n";
|
||||
bool test_construction_and_inference() {
|
||||
std::cout << "--- Test: Construction and inference ---\n";
|
||||
|
||||
// Create IML with 2 inputs, 1 output
|
||||
nisps::IML<float> iml(2, 1, {4, 4}, 5000, 1.0f, 0.0001f);
|
||||
nisps::IML<float> iml(2, 1, {4, 4}, 1000, 1.0f, 0.0001f);
|
||||
iml.set_logger(log_callback);
|
||||
|
||||
// Enter training mode
|
||||
iml.set_input(0, 0.5f);
|
||||
iml.set_input(1, 0.5f);
|
||||
iml.process();
|
||||
|
||||
const float* out = iml.get_outputs();
|
||||
// Output should be a valid float in [0, 1] (sigmoid output layer)
|
||||
if (std::isnan(out[0]) || std::isinf(out[0])) {
|
||||
std::cerr << "FAIL: Output is NaN or Inf\n";
|
||||
return false;
|
||||
}
|
||||
if (out[0] < 0.0f || out[0] > 1.0f) {
|
||||
std::cerr << "FAIL: Output " << out[0] << " outside [0, 1]\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << " Output: " << out[0] << " (valid)\n";
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_set_output_api() {
|
||||
std::cout << "--- Test: set_output / set_outputs API ---\n";
|
||||
|
||||
nisps::IML<float> iml(2, 3);
|
||||
iml.set_logger(log_callback);
|
||||
|
||||
iml.set_output(0, 0.25f);
|
||||
iml.set_output(1, 0.75f);
|
||||
iml.set_output(2, 0.5f);
|
||||
|
||||
const float* out = iml.get_outputs();
|
||||
if (std::abs(out[0] - 0.25f) > 1e-6f ||
|
||||
std::abs(out[1] - 0.75f) > 1e-6f ||
|
||||
std::abs(out[2] - 0.5f) > 1e-6f) {
|
||||
std::cerr << "FAIL: set_output values not stored correctly\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test clamping
|
||||
iml.set_output(0, -1.0f);
|
||||
iml.set_output(1, 2.0f);
|
||||
if (std::abs(iml.get_outputs()[0]) > 1e-6f ||
|
||||
std::abs(iml.get_outputs()[1] - 1.0f) > 1e-6f) {
|
||||
std::cerr << "FAIL: set_output clamping not working\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test out-of-bounds index (should not crash)
|
||||
iml.set_output(999, 0.5f);
|
||||
|
||||
// Test set_outputs bulk
|
||||
float vals[] = {0.1f, 0.2f, 0.3f};
|
||||
iml.set_outputs(vals, 3);
|
||||
if (std::abs(iml.get_outputs()[0] - 0.1f) > 1e-6f ||
|
||||
std::abs(iml.get_outputs()[1] - 0.2f) > 1e-6f ||
|
||||
std::abs(iml.get_outputs()[2] - 0.3f) > 1e-6f) {
|
||||
std::cerr << "FAIL: set_outputs bulk not working\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_add_example_api() {
|
||||
std::cout << "--- Test: add_example API ---\n";
|
||||
|
||||
nisps::IML<float> iml(2, 1, {4}, 500, 1.0f, 0.001f);
|
||||
iml.set_logger(log_callback);
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// Train on XOR pattern
|
||||
// (0,0) -> 0
|
||||
iml.set_input(0, 0.0f);
|
||||
iml.set_input(1, 0.0f);
|
||||
iml.save_example(); // First call: stop inference
|
||||
// Manually set output for this example (simulating user positioning)
|
||||
// We access output_state_ indirectly by calling process after training
|
||||
// Add a single example programmatically
|
||||
float in[] = {0.0f, 0.0f};
|
||||
float out[] = {0.0f};
|
||||
iml.add_example(in, 2, out, 1);
|
||||
|
||||
// For this test, we'll add examples directly to dataset
|
||||
// This simulates the two-step save process
|
||||
|
||||
// Actually, let's test the full workflow properly:
|
||||
// The IML class expects: save_example() twice per example
|
||||
// 1. First call stops inference
|
||||
// 2. User sets output position (we can't do this externally easily)
|
||||
// 3. Second call stores input->output
|
||||
|
||||
// For testing, let's verify the basic inference works
|
||||
// Switch to inference (triggers training)
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
// Test inference
|
||||
// Should not crash, training on 1 example
|
||||
iml.set_input(0, 0.0f);
|
||||
iml.set_input(1, 0.0f);
|
||||
iml.process();
|
||||
float out_00 = iml.get_outputs()[0];
|
||||
|
||||
iml.set_input(0, 1.0f);
|
||||
iml.set_input(1, 0.0f);
|
||||
iml.process();
|
||||
float out_10 = iml.get_outputs()[0];
|
||||
|
||||
iml.set_input(0, 0.0f);
|
||||
iml.set_input(1, 1.0f);
|
||||
iml.process();
|
||||
float out_01 = iml.get_outputs()[0];
|
||||
|
||||
iml.set_input(0, 1.0f);
|
||||
iml.set_input(1, 1.0f);
|
||||
iml.process();
|
||||
float out_11 = iml.get_outputs()[0];
|
||||
|
||||
std::cout << "\nInference results (untrained):\n";
|
||||
std::cout << " (0,0) -> " << out_00 << "\n";
|
||||
std::cout << " (1,0) -> " << out_10 << "\n";
|
||||
std::cout << " (0,1) -> " << out_01 << "\n";
|
||||
std::cout << " (1,1) -> " << out_11 << "\n";
|
||||
|
||||
std::cout << "\n=== Test passed: nisps-core compiles and runs ===\n";
|
||||
return 0;
|
||||
const float* result = iml.get_outputs();
|
||||
if (std::isnan(result[0]) || std::isinf(result[0])) {
|
||||
std::cerr << "FAIL: Output is NaN/Inf after training\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << " Output after training on 1 example: " << result[0] << "\n";
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_training_convergence() {
|
||||
std::cout << "--- Test: Training convergence (identity mapping) ---\n";
|
||||
|
||||
// Train a network to learn: input -> same output
|
||||
// This is simpler than XOR and should converge reliably
|
||||
nisps::IML<float> iml(1, 1, {8, 8}, 3000, 1.0f, 0.00001f);
|
||||
iml.set_logger(log_callback);
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// Add training data: output should match input
|
||||
struct Example { float in; float out; };
|
||||
Example examples[] = {
|
||||
{0.1f, 0.1f},
|
||||
{0.3f, 0.3f},
|
||||
{0.5f, 0.5f},
|
||||
{0.7f, 0.7f},
|
||||
{0.9f, 0.9f},
|
||||
};
|
||||
|
||||
for (const auto& ex : examples) {
|
||||
iml.add_example(&ex.in, 1, &ex.out, 1);
|
||||
}
|
||||
|
||||
// Switch to inference (triggers training)
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
// Now test: outputs should approximate inputs
|
||||
float max_error = 0.0f;
|
||||
bool passed = true;
|
||||
|
||||
for (const auto& ex : examples) {
|
||||
iml.set_input(0, ex.in);
|
||||
iml.process();
|
||||
float result = iml.get_outputs()[0];
|
||||
float error = std::abs(result - ex.out);
|
||||
max_error = std::max(max_error, error);
|
||||
|
||||
std::cout << " Input: " << ex.in << " -> Output: " << result
|
||||
<< " (expected: " << ex.out << ", error: " << error << ")\n";
|
||||
|
||||
if (error > 0.15f) {
|
||||
std::cerr << " ERROR: Error too large for input " << ex.in << "\n";
|
||||
passed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Also test interpolation at a value we didn't train on
|
||||
iml.set_input(0, 0.4f);
|
||||
iml.process();
|
||||
float interp = iml.get_outputs()[0];
|
||||
float interp_error = std::abs(interp - 0.4f);
|
||||
std::cout << " Interpolation: 0.4 -> " << interp
|
||||
<< " (error: " << interp_error << ")\n";
|
||||
|
||||
std::cout << " Max training error: " << max_error << "\n";
|
||||
if (passed) {
|
||||
std::cout << "PASS\n\n";
|
||||
} else {
|
||||
std::cerr << "FAIL: Network did not converge\n\n";
|
||||
}
|
||||
return passed;
|
||||
}
|
||||
|
||||
bool test_multi_output_training() {
|
||||
std::cout << "--- Test: Multi-output training ---\n";
|
||||
|
||||
// 2 inputs -> 2 outputs
|
||||
// Learn: (low, low) -> (0, 0), (high, high) -> (1, 1)
|
||||
nisps::IML<float> iml(2, 2, {8, 8}, 3000, 1.0f, 0.00001f);
|
||||
iml.set_logger(log_callback);
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
float in1[] = {0.1f, 0.1f}; float out1[] = {0.1f, 0.9f};
|
||||
float in2[] = {0.9f, 0.9f}; float out2[] = {0.9f, 0.1f};
|
||||
float in3[] = {0.1f, 0.9f}; float out3[] = {0.5f, 0.5f};
|
||||
float in4[] = {0.9f, 0.1f}; float out4[] = {0.5f, 0.5f};
|
||||
|
||||
iml.add_example(in1, 2, out1, 2);
|
||||
iml.add_example(in2, 2, out2, 2);
|
||||
iml.add_example(in3, 2, out3, 2);
|
||||
iml.add_example(in4, 2, out4, 2);
|
||||
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
// Test that the network learned distinct mappings
|
||||
iml.set_input(0, 0.1f); iml.set_input(1, 0.1f);
|
||||
iml.process();
|
||||
float r1_0 = iml.get_outputs()[0];
|
||||
float r1_1 = iml.get_outputs()[1];
|
||||
|
||||
iml.set_input(0, 0.9f); iml.set_input(1, 0.9f);
|
||||
iml.process();
|
||||
float r2_0 = iml.get_outputs()[0];
|
||||
float r2_1 = iml.get_outputs()[1];
|
||||
|
||||
std::cout << " (0.1, 0.1) -> (" << r1_0 << ", " << r1_1 << ") expected ~(0.1, 0.9)\n";
|
||||
std::cout << " (0.9, 0.9) -> (" << r2_0 << ", " << r2_1 << ") expected ~(0.9, 0.1)\n";
|
||||
|
||||
// The outputs for different inputs should be meaningfully different
|
||||
bool different = (std::abs(r1_0 - r2_0) > 0.1f) || (std::abs(r1_1 - r2_1) > 0.1f);
|
||||
if (!different) {
|
||||
std::cerr << "FAIL: Network outputs are too similar for different inputs\n\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "\n=== NISPS Core Test Suite ===\n\n";
|
||||
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
auto run = [&](bool result) { result ? passed++ : failed++; };
|
||||
|
||||
run(test_construction_and_inference());
|
||||
run(test_set_output_api());
|
||||
run(test_add_example_api());
|
||||
run(test_training_convergence());
|
||||
run(test_multi_output_training());
|
||||
|
||||
std::cout << "=== Results: " << passed << " passed, " << failed << " failed ===\n\n";
|
||||
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue