feat(nisps-core,vcv): complete Phases 6 + 7 — persistence, derived outputs
Phase 6 — State persistence: - Full state serialization: version, weights (3D), examples (features+labels), mlpConfig, noiseLevel, slewMs, output/input ranges - Validation on load: version check, graceful missing field handling - .nisps preset save/load via right-click menu (osdialog file dialogs) - Param values included in preset files Phase 7 — Derived outputs: - Mean, STD, delta computed on audio thread (trivial cost) - Novelty/confidence: nearest_example_distance() computed on background thread after each training/perturbation job, cached for audio thread - Defaults with 0 examples: novelty=10V, confidence=0V nisps-core IML additions: - get_weights() / set_weights() for MLP weight serialization - get_example_features/labels() / load_examples() for dataset serialization - nearest_example_distance() for novelty/confidence metric - get_example_count() / get_max_examples() for UI display
This commit is contained in:
parent
6a76f15736
commit
74c52fadc7
3 changed files with 285 additions and 3 deletions
|
|
@ -54,6 +54,26 @@ public:
|
||||||
// speed: noise magnitude, spread: 0 = flat noise, 1 = Xavier-scaled + weight decay
|
// speed: noise magnitude, spread: 0 = flat noise, 1 = Xavier-scaled + weight decay
|
||||||
void move_weights(Float speed, Float spread);
|
void move_weights(Float speed, Float spread);
|
||||||
|
|
||||||
|
// ── Serialization accessors ───────────────────────────────────────
|
||||||
|
|
||||||
|
// Weight access (delegates to MLP)
|
||||||
|
typename MLP<Float>::mlp_weights get_weights() const;
|
||||||
|
void set_weights(typename MLP<Float>::mlp_weights& weights);
|
||||||
|
|
||||||
|
// Dataset access
|
||||||
|
size_t get_example_count() const;
|
||||||
|
size_t get_max_examples() const;
|
||||||
|
// Returns copies of the dataset vectors
|
||||||
|
std::vector<std::vector<Float>> get_example_features() const;
|
||||||
|
std::vector<std::vector<Float>> get_example_labels() const;
|
||||||
|
// Bulk-load examples (clears existing, adds all)
|
||||||
|
void load_examples(const std::vector<std::vector<Float>>& features,
|
||||||
|
const std::vector<std::vector<Float>>& labels);
|
||||||
|
|
||||||
|
// Nearest-neighbor distance for novelty/confidence computation
|
||||||
|
// Returns the minimum Euclidean distance from `input` to any training example
|
||||||
|
Float nearest_example_distance(const Float* input, size_t n_in) const;
|
||||||
|
|
||||||
// Optional logging
|
// Optional logging
|
||||||
void set_logger(LogFn fn) { log_fn_ = fn; }
|
void set_logger(LogFn fn) { log_fn_ = fn; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
#ifndef NISPS_IML_IMPL_HPP
|
#ifndef NISPS_IML_IMPL_HPP
|
||||||
#define NISPS_IML_IMPL_HPP
|
#define NISPS_IML_IMPL_HPP
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace nisps {
|
namespace nisps {
|
||||||
|
|
||||||
template<typename Float>
|
template<typename Float>
|
||||||
|
|
@ -226,6 +229,84 @@ void IML<Float>::train() {
|
||||||
log("Training complete.");
|
log("Training complete.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Serialization accessors ───────────────────────────────────────
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
typename MLP<Float>::mlp_weights IML<Float>::get_weights() const {
|
||||||
|
return mlp_->GetWeights();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
void IML<Float>::set_weights(typename MLP<Float>::mlp_weights& weights) {
|
||||||
|
mlp_->SetWeights(weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
size_t IML<Float>::get_example_count() const {
|
||||||
|
Dataset::DatasetVector* feats;
|
||||||
|
Dataset::DatasetVector* labels;
|
||||||
|
const_cast<Dataset*>(dataset_.get())->Fetch(feats, labels);
|
||||||
|
return feats ? feats->size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
size_t IML<Float>::get_max_examples() const {
|
||||||
|
return Dataset::kMax_examples;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
std::vector<std::vector<Float>> IML<Float>::get_example_features() const {
|
||||||
|
auto feats = const_cast<Dataset*>(dataset_.get())->GetFeatures(false);
|
||||||
|
std::vector<std::vector<Float>> result;
|
||||||
|
result.reserve(feats.size());
|
||||||
|
for (auto& f : feats) {
|
||||||
|
result.emplace_back(f.begin(), f.end());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
std::vector<std::vector<Float>> IML<Float>::get_example_labels() const {
|
||||||
|
auto& labels = const_cast<Dataset*>(dataset_.get())->GetLabels();
|
||||||
|
std::vector<std::vector<Float>> result;
|
||||||
|
result.reserve(labels.size());
|
||||||
|
for (auto& l : labels) {
|
||||||
|
result.emplace_back(l.begin(), l.end());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
void IML<Float>::load_examples(const std::vector<std::vector<Float>>& features,
|
||||||
|
const std::vector<std::vector<Float>>& labels) {
|
||||||
|
dataset_->Clear();
|
||||||
|
size_t count = std::min(features.size(), labels.size());
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
std::vector<float> feat(features[i].begin(), features[i].end());
|
||||||
|
std::vector<float> label(labels[i].begin(), labels[i].end());
|
||||||
|
dataset_->Add(feat, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Float>
|
||||||
|
Float IML<Float>::nearest_example_distance(const Float* input, size_t n_in) const {
|
||||||
|
auto feats = const_cast<Dataset*>(dataset_.get())->GetFeatures(false);
|
||||||
|
if (feats.empty()) return static_cast<Float>(-1);
|
||||||
|
|
||||||
|
Float minDist = std::numeric_limits<Float>::max();
|
||||||
|
size_t dims = std::min(n_in, n_inputs_);
|
||||||
|
for (auto& f : feats) {
|
||||||
|
Float dist = 0;
|
||||||
|
for (size_t d = 0; d < dims && d < f.size(); d++) {
|
||||||
|
Float diff = static_cast<Float>(f[d]) - input[d];
|
||||||
|
dist += diff * diff;
|
||||||
|
}
|
||||||
|
dist = std::sqrt(dist);
|
||||||
|
if (dist < minDist) minDist = dist;
|
||||||
|
}
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace nisps
|
} // namespace nisps
|
||||||
|
|
||||||
#endif // NISPS_IML_IMPL_HPP
|
#endif // NISPS_IML_IMPL_HPP
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
#include "plugin.hpp"
|
#include "plugin.hpp"
|
||||||
#include <nisps/nisps.hpp>
|
#include <nisps/nisps.hpp>
|
||||||
|
#include <osdialog.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
static constexpr int NUM_ML_INPUTS = 2;
|
static constexpr int NUM_ML_INPUTS = 2;
|
||||||
static constexpr int NUM_ML_OUTPUTS = 12;
|
static constexpr int NUM_ML_OUTPUTS = 12;
|
||||||
|
|
@ -75,6 +77,9 @@ struct MEMLNaut : Module {
|
||||||
bool outputRangeUnipolar[NUM_ML_OUTPUTS] = {}; // true = 0-10V, false = ±5V
|
bool outputRangeUnipolar[NUM_ML_OUTPUTS] = {}; // true = 0-10V, false = ±5V
|
||||||
bool inputRangeUnipolar[MAX_ML_INPUTS] = {}; // true = 0-10V, false = ±5V
|
bool inputRangeUnipolar[MAX_ML_INPUTS] = {}; // true = 0-10V, false = ±5V
|
||||||
float clearHoldTime = 0.f;
|
float clearHoldTime = 0.f;
|
||||||
|
float cachedNovelty = 10.f; // default: everything novel (10V)
|
||||||
|
float cachedConfidence = 0.f; // default: no confidence (0V)
|
||||||
|
float lastInputs[MAX_ML_INPUTS] = {};
|
||||||
|
|
||||||
// ── Triggers ──────────────────────────────────────────────────────
|
// ── Triggers ──────────────────────────────────────────────────────
|
||||||
dsp::BooleanTrigger randTrigger;
|
dsp::BooleanTrigger randTrigger;
|
||||||
|
|
@ -186,6 +191,18 @@ struct MEMLNaut : Module {
|
||||||
iml.move_weights(job.noiseLevel, job.spread);
|
iml.move_weights(job.noiseLevel, job.spread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update novelty/confidence for current input position
|
||||||
|
if (iml.get_example_count() > 0) {
|
||||||
|
float dist = iml.nearest_example_distance(lastInputs, NUM_ML_INPUTS);
|
||||||
|
// Novelty: scale distance to 0-10V (1.0 distance = 10V, saturates)
|
||||||
|
cachedNovelty = std::min(dist * 10.f, 10.f);
|
||||||
|
// Confidence: inverse of distance (close = high confidence)
|
||||||
|
cachedConfidence = std::max(0.f, 10.f - dist * 10.f);
|
||||||
|
} else {
|
||||||
|
cachedNovelty = 10.f;
|
||||||
|
cachedConfidence = 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
swapReady.store(true);
|
swapReady.store(true);
|
||||||
isTraining.store(false);
|
isTraining.store(false);
|
||||||
|
|
||||||
|
|
@ -340,6 +357,8 @@ struct MEMLNaut : Module {
|
||||||
// Read and normalize inputs
|
// Read and normalize inputs
|
||||||
float x = normalizeInput(INPUT_X, 0);
|
float x = normalizeInput(INPUT_X, 0);
|
||||||
float y = normalizeInput(INPUT_Y, 1);
|
float y = normalizeInput(INPUT_Y, 1);
|
||||||
|
lastInputs[0] = x;
|
||||||
|
lastInputs[1] = y;
|
||||||
|
|
||||||
iml.set_input(0, x);
|
iml.set_input(0, x);
|
||||||
iml.set_input(1, y);
|
iml.set_input(1, y);
|
||||||
|
|
@ -410,14 +429,15 @@ struct MEMLNaut : Module {
|
||||||
}
|
}
|
||||||
outputs[OUTPUT_DELTA].setVoltage(std::sqrt(delta) * 10.f);
|
outputs[OUTPUT_DELTA].setVoltage(std::sqrt(delta) * 10.f);
|
||||||
|
|
||||||
// Novelty + Confidence (placeholder — computed on training thread in Phase 7)
|
// Novelty + Confidence (computed on background thread, cached)
|
||||||
outputs[OUTPUT_NOVELTY].setVoltage(10.f); // default: everything is novel
|
outputs[OUTPUT_NOVELTY].setVoltage(cachedNovelty);
|
||||||
outputs[OUTPUT_CONFIDENCE].setVoltage(0.f); // default: no confidence
|
outputs[OUTPUT_CONFIDENCE].setVoltage(cachedConfidence);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Serialization ─────────────────────────────────────────────────
|
// ── Serialization ─────────────────────────────────────────────────
|
||||||
json_t* dataToJson() override {
|
json_t* dataToJson() override {
|
||||||
json_t* root = json_object();
|
json_t* root = json_object();
|
||||||
|
json_object_set_new(root, "version", json_integer(1));
|
||||||
json_object_set_new(root, "noiseLevel", json_real(noiseLevel));
|
json_object_set_new(root, "noiseLevel", json_real(noiseLevel));
|
||||||
json_object_set_new(root, "slewMs", json_real(slewMs));
|
json_object_set_new(root, "slewMs", json_real(slewMs));
|
||||||
|
|
||||||
|
|
@ -435,6 +455,52 @@ struct MEMLNaut : Module {
|
||||||
}
|
}
|
||||||
json_object_set_new(root, "inputRangeUnipolar", inRanges);
|
json_object_set_new(root, "inputRangeUnipolar", inRanges);
|
||||||
|
|
||||||
|
// MLP weights (3D: layer → node → weight)
|
||||||
|
auto weights = iml.get_weights();
|
||||||
|
json_t* jWeights = json_array();
|
||||||
|
for (auto& layer : weights) {
|
||||||
|
json_t* jLayer = json_array();
|
||||||
|
for (auto& node : layer) {
|
||||||
|
json_t* jNode = json_array();
|
||||||
|
for (float w : node) {
|
||||||
|
json_array_append_new(jNode, json_real(w));
|
||||||
|
}
|
||||||
|
json_array_append_new(jLayer, jNode);
|
||||||
|
}
|
||||||
|
json_array_append_new(jWeights, jLayer);
|
||||||
|
}
|
||||||
|
json_object_set_new(root, "weights", jWeights);
|
||||||
|
|
||||||
|
// Training examples
|
||||||
|
auto features = iml.get_example_features();
|
||||||
|
auto labels = iml.get_example_labels();
|
||||||
|
json_t* jExamples = json_object();
|
||||||
|
json_t* jFeatures = json_array();
|
||||||
|
for (auto& f : features) {
|
||||||
|
json_t* jF = json_array();
|
||||||
|
for (float v : f) json_array_append_new(jF, json_real(v));
|
||||||
|
json_array_append_new(jFeatures, jF);
|
||||||
|
}
|
||||||
|
json_t* jLabels = json_array();
|
||||||
|
for (auto& l : labels) {
|
||||||
|
json_t* jL = json_array();
|
||||||
|
for (float v : l) json_array_append_new(jL, json_real(v));
|
||||||
|
json_array_append_new(jLabels, jL);
|
||||||
|
}
|
||||||
|
json_object_set_new(jExamples, "features", jFeatures);
|
||||||
|
json_object_set_new(jExamples, "labels", jLabels);
|
||||||
|
json_object_set_new(root, "examples", jExamples);
|
||||||
|
|
||||||
|
// MLP config (for validation on load)
|
||||||
|
json_t* jConfig = json_object();
|
||||||
|
json_t* jLayers = json_array();
|
||||||
|
// [3, 16, 24, 16, 12] for default config
|
||||||
|
json_array_append_new(jLayers, json_integer(NUM_ML_INPUTS + 1)); // +bias
|
||||||
|
for (int h : {16, 24, 16}) json_array_append_new(jLayers, json_integer(h));
|
||||||
|
json_array_append_new(jLayers, json_integer(NUM_ML_OUTPUTS));
|
||||||
|
json_object_set_new(jConfig, "layers", jLayers);
|
||||||
|
json_object_set_new(root, "mlpConfig", jConfig);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,6 +511,7 @@ struct MEMLNaut : Module {
|
||||||
if ((j = json_object_get(root, "slewMs")))
|
if ((j = json_object_get(root, "slewMs")))
|
||||||
slewMs = json_real_value(j);
|
slewMs = json_real_value(j);
|
||||||
|
|
||||||
|
// Output ranges
|
||||||
json_t* outRanges = json_object_get(root, "outputRangeUnipolar");
|
json_t* outRanges = json_object_get(root, "outputRangeUnipolar");
|
||||||
if (outRanges) {
|
if (outRanges) {
|
||||||
for (int i = 0; i < NUM_ML_OUTPUTS && i < (int)json_array_size(outRanges); i++) {
|
for (int i = 0; i < NUM_ML_OUTPUTS && i < (int)json_array_size(outRanges); i++) {
|
||||||
|
|
@ -452,12 +519,58 @@ struct MEMLNaut : Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input ranges
|
||||||
json_t* inRanges = json_object_get(root, "inputRangeUnipolar");
|
json_t* inRanges = json_object_get(root, "inputRangeUnipolar");
|
||||||
if (inRanges) {
|
if (inRanges) {
|
||||||
for (int i = 0; i < MAX_ML_INPUTS && i < (int)json_array_size(inRanges); i++) {
|
for (int i = 0; i < MAX_ML_INPUTS && i < (int)json_array_size(inRanges); i++) {
|
||||||
inputRangeUnipolar[i] = json_boolean_value(json_array_get(inRanges, i));
|
inputRangeUnipolar[i] = json_boolean_value(json_array_get(inRanges, i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MLP weights
|
||||||
|
json_t* jWeights = json_object_get(root, "weights");
|
||||||
|
if (jWeights && json_is_array(jWeights)) {
|
||||||
|
nisps::MLP<float>::mlp_weights weights;
|
||||||
|
for (size_t li = 0; li < json_array_size(jWeights); li++) {
|
||||||
|
json_t* jLayer = json_array_get(jWeights, li);
|
||||||
|
std::vector<std::vector<float>> layer;
|
||||||
|
for (size_t ni = 0; ni < json_array_size(jLayer); ni++) {
|
||||||
|
json_t* jNode = json_array_get(jLayer, ni);
|
||||||
|
std::vector<float> node;
|
||||||
|
for (size_t wi = 0; wi < json_array_size(jNode); wi++) {
|
||||||
|
node.push_back(json_real_value(json_array_get(jNode, wi)));
|
||||||
|
}
|
||||||
|
layer.push_back(node);
|
||||||
|
}
|
||||||
|
weights.push_back(layer);
|
||||||
|
}
|
||||||
|
iml.set_weights(weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training examples
|
||||||
|
json_t* jExamples = json_object_get(root, "examples");
|
||||||
|
if (jExamples) {
|
||||||
|
json_t* jFeatures = json_object_get(jExamples, "features");
|
||||||
|
json_t* jLabels = json_object_get(jExamples, "labels");
|
||||||
|
if (jFeatures && jLabels) {
|
||||||
|
std::vector<std::vector<float>> features, labels;
|
||||||
|
for (size_t i = 0; i < json_array_size(jFeatures); i++) {
|
||||||
|
json_t* jF = json_array_get(jFeatures, i);
|
||||||
|
std::vector<float> f;
|
||||||
|
for (size_t fi = 0; fi < json_array_size(jF); fi++)
|
||||||
|
f.push_back(json_real_value(json_array_get(jF, fi)));
|
||||||
|
features.push_back(f);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < json_array_size(jLabels); i++) {
|
||||||
|
json_t* jL = json_array_get(jLabels, i);
|
||||||
|
std::vector<float> l;
|
||||||
|
for (size_t li = 0; li < json_array_size(jL); li++)
|
||||||
|
l.push_back(json_real_value(json_array_get(jL, li)));
|
||||||
|
labels.push_back(l);
|
||||||
|
}
|
||||||
|
iml.load_examples(features, labels);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -619,6 +732,74 @@ struct MEMLNautWidget : ModuleWidget {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ── Preset save/load ──────────────────────────────────────────
|
||||||
|
menu->addChild(new MenuSeparator);
|
||||||
|
menu->addChild(createMenuLabel("Presets (.nisps)"));
|
||||||
|
|
||||||
|
menu->addChild(createMenuItem("Save .nisps preset...", "", [=]() {
|
||||||
|
osdialog_filters* filters = osdialog_filters_parse("NISPS preset:nisps");
|
||||||
|
char* path = osdialog_file(OSDIALOG_SAVE, nullptr, "preset.nisps", filters);
|
||||||
|
osdialog_filters_free(filters);
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
json_t* root = module->dataToJson();
|
||||||
|
// Also save all param values
|
||||||
|
json_t* jParams = json_array();
|
||||||
|
for (int i = 0; i < MEMLNaut::PARAMS_LEN; i++) {
|
||||||
|
json_array_append_new(jParams, json_real(module->params[i].getValue()));
|
||||||
|
}
|
||||||
|
json_object_set_new(root, "params", jParams);
|
||||||
|
|
||||||
|
char* jsonStr = json_dumps(root, JSON_INDENT(2));
|
||||||
|
json_decref(root);
|
||||||
|
|
||||||
|
std::ofstream file(path);
|
||||||
|
if (file.is_open()) {
|
||||||
|
file << jsonStr;
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
free(jsonStr);
|
||||||
|
free(path);
|
||||||
|
}));
|
||||||
|
|
||||||
|
menu->addChild(createMenuItem("Load .nisps preset...", "", [=]() {
|
||||||
|
osdialog_filters* filters = osdialog_filters_parse("NISPS preset:nisps");
|
||||||
|
char* path = osdialog_file(OSDIALOG_OPEN, nullptr, nullptr, filters);
|
||||||
|
osdialog_filters_free(filters);
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
std::ifstream file(path);
|
||||||
|
free(path);
|
||||||
|
if (!file.is_open()) return;
|
||||||
|
|
||||||
|
std::string content((std::istreambuf_iterator<char>(file)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
json_error_t error;
|
||||||
|
json_t* root = json_loads(content.c_str(), 0, &error);
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
// Validate version
|
||||||
|
json_t* jVersion = json_object_get(root, "version");
|
||||||
|
if (!jVersion || json_integer_value(jVersion) < 1) {
|
||||||
|
json_decref(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
module->dataFromJson(root);
|
||||||
|
|
||||||
|
// Restore param values if present
|
||||||
|
json_t* jParams = json_object_get(root, "params");
|
||||||
|
if (jParams && json_is_array(jParams)) {
|
||||||
|
for (size_t i = 0; i < json_array_size(jParams) && i < MEMLNaut::PARAMS_LEN; i++) {
|
||||||
|
module->params[i].setValue(json_real_value(json_array_get(jParams, i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json_decref(root);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue