Stream 12: cleanup + docs (delete nisps-core, rewrite MAP/CLAUDE, create ALIGNMENT)
- Delete nisps-core/ (lessons absorbed into nisps/ml; firmware is canonical) - Rewrite MAP.md to reflect new clean-slate layout (nisps/ + firmware/ + playground/ + schemas/ + codegen/) - Rewrite CLAUDE.md as new architecture narrative - Create ALIGNMENT.md with current strategic gaps + open mission questions (meml-quc)
This commit is contained in:
parent
e45822e5a2
commit
3a8e2b8116
21 changed files with 340 additions and 5342 deletions
98
ALIGNMENT.md
Normal file
98
ALIGNMENT.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# ALIGNMENT
|
||||
|
||||
> Opinionated diagnosis of how well the codebase serves its mission, ranked by impact. Dated entries; remove when resolved rather than checking off. **Pruned every few weeks** — a stale diagnosis is worse than none.
|
||||
|
||||
## Mission
|
||||
|
||||
A research platform for interactive ML control of audio. We're building it to figure out what works and what doesn't — different ergonomics and ergodynamics of parameter sets, modes, ML architectures, audio engines, UI, and UX. Therefore: keep most/all parameters tweakable, ML/engine/UI/UX should each be configurable on their own axis, and the codebase has to enable/assist agentic AI coding patterns (confident changes, verifiable without hardware).
|
||||
|
||||
The clean-slate rewrite (2026-04-29) consolidates everything into one C++20 codebase compiling to firmware AND WASM, with SolidJS playground primitives composed into TSX mode components, and JSON schemas as the firmware↔browser parameter contract.
|
||||
|
||||
## Top defects (ranked by mission impact)
|
||||
|
||||
### 1. Browser-only audio engines incomplete (2026-04-29)
|
||||
|
||||
**What.** Stream 9 stubbed `C15Mode` as a placeholder ("C15 mode TODO"). The C15 worklet/bridge from the legacy playground is in `.local/playground-archive/js/synth/` but not wired in. Mic input for XIASRI / SoundAnalysisMIDI is also TODO — the UI renders but doesn't capture audio.
|
||||
|
||||
**Why it blocks the mission.** "Browser engines ⊇ firmware engines" was a non-negotiable. Without C15 + mic input, the playground can't fully demonstrate the modes; users can't audition XIASRI or SoundAnalysisMIDI in the browser.
|
||||
|
||||
**Rough cost.** ~1–2 days. C15 wiring is mostly straightforward porting from the archived bridge. Mic input requires the engine-host to expose an input stream to the worklet (small worklet refactor).
|
||||
|
||||
### 2. Per-iteration loss curve not plumbed through WASM (2026-04-29)
|
||||
|
||||
**What.** Stream 7's WASM C API exposes `_nisps_ml_train` but only returns the final loss; `MLP::loss_history()` exists in C++ but isn't reached. `lossHistory` in `mlStore` is a single-element array per training run.
|
||||
|
||||
**Why it blocks the mission.** Gradient flow / loss visualization is a core UX affordance for "is the network learning?" — a research-mode debugging tool that's been implemented end-to-end in C++ but stops at the WASM boundary.
|
||||
|
||||
**Rough cost.** Half a day. Add `nisps_ml_train_with_history` (or extend the existing call) returning a pointer to the loss array; copy on the JS side.
|
||||
|
||||
### 3. WASM MLP architecture is fixed (2026-04-29)
|
||||
|
||||
**What.** WASM compiles `MLP<2, 10, 14, 18, 126>` only. Modes with smaller `output_size` use a slice; modes that want different hidden layer shapes (e.g. `[10, 10, 14]` for smaller modes) can't get them in the browser.
|
||||
|
||||
**Why it blocks the mission.** Per-mode ML architecture variation is one of our four "research dimensions". The fix isn't urgent for the current mode set (all schemas are within the universal arch's capacity), but the moment we want to experiment with bigger networks or different shapes, this hits.
|
||||
|
||||
**Rough cost.** Medium. Either compile multiple WASM modules (one per arch shape; load on demand) or move to a runtime-shaped MLP (loses some compile-time perf). Templates-vs-runtime is a research-vs-firmware-perf tradeoff worth a separate decision doc.
|
||||
|
||||
### 4. `NISPS_AUDIO_FUNC` host fallback is misshapen (2026-04-29)
|
||||
|
||||
**What.** `nisps/core/perf.hpp` defines `NISPS_AUDIO_FUNC(decl) decl` for the host but the firmware path `__not_in_flash_func(name)` takes only a function name (it stringifies into a section attribute). The two forms don't match. Stream 6 (firmware glue) avoided the macro to dodge the inconsistency, but it's still a footgun.
|
||||
|
||||
**Why it blocks the mission.** Future agents touching `nisps/` will hit this. Either decoration form in the codebase is fine; what's wrong is that the same call site shape doesn't work both places.
|
||||
|
||||
**Rough cost.** Tiny. Pick one form and apply consistently:
|
||||
- Option A: `NISPS_AUDIO_FUNC` decorates a function name (e.g. `void NISPS_AUDIO_FUNC(my_callback)(...) { ... }`). Host stub: `#define NISPS_AUDIO_FUNC(name) name`.
|
||||
- Option B: separate `NISPS_AUDIO_FUNC_BEGIN` / `_END` markers around the function, or a different macro.
|
||||
Pick A. Update perf.hpp + every `nisps/` use site.
|
||||
|
||||
### 5. RMSProp deferred from `nisps/ml/` (2026-04-29)
|
||||
|
||||
**What.** The legacy MLP supported both SGD and RMSProp paths. Stream 2 shipped only SGD as MVP. The architecture spec called for both. Documented as "follow-up bd issue when needed".
|
||||
|
||||
**Why it blocks the mission.** Optimizer choice is one of the things research wants to vary. Not blocking for the current XOR-style fits, but as soon as we tune for harder loss landscapes, RMSProp will matter.
|
||||
|
||||
**Rough cost.** A day. Port the firmware's RMSProp from `src/memlp/MLP.cpp:415-543` (decay 0.9, epsilon 1e-6, gradient accumulation, batch size). Add tests for batch training convergence.
|
||||
|
||||
### 6. bd Dolt remote sync flaky (2026-04-29)
|
||||
|
||||
**What.** During the rewrite, `bd close` repeatedly failed with "database `beads_meml` not found on Dolt server" or similar lock conflicts. Several agent-side bd closures could not be performed and have orchestrator-side closure notes instead. May leave stream issues in inconsistent states.
|
||||
|
||||
**Why it blocks the mission.** Beads is the canonical task tracker; if it can't reliably sync, future agents lose visibility into what's done vs in-progress.
|
||||
|
||||
**Rough cost.** Investigate Dolt server config + lock semantics. Out of scope for the rewrite itself.
|
||||
|
||||
## Open mission questions
|
||||
|
||||
### Q1: Per-mode MLP architectures or one shared shape? (2026-04-29)
|
||||
|
||||
Schemas currently declare per-mode `hidden_layers` (some `[10, 10, 14]`, some `[10, 14, 18]`). Browser is fixed at one shape; firmware compiles per-mode. This works for now. Is the mission served by maintaining per-mode shapes (research diversity) or by collapsing to one (simpler ops)?
|
||||
|
||||
### Q2: How to express "advanced" features (gradient flow, weight health) without cluttering modes? (2026-04-29)
|
||||
|
||||
Current playground reproduces the a-immersive "Advanced" toggle. Power features hide behind it. Is this the right model, or should the mode UI itself decide what's exposed (some modes are "expert-only", some are simpler)?
|
||||
|
||||
### Q3: Engine event taxonomy (2026-04-29)
|
||||
|
||||
`nisps/modes/base.hpp` exposes a `ControlEvent` ring buffer pop_events interface for sequencer modes (BreakOr, Elysiamorf). Currently events are a flat enum. As we add more event-emitting modes (custom MIDI mappings, lighting, networked control), how should the event vocabulary grow? Open question; revisit when we add the third event-emitting mode.
|
||||
|
||||
### Q4: Should the playground stay desktop-first? (2026-04-29)
|
||||
|
||||
The original a-immersive was mobile-first ("designed for touch / foldable phone use"). The SolidJS rewrite is desktop-first by default. If the research story is "the user holds a phone and pinches to zoom while a synth runs in their pocket", we'll need a responsive pass. Defer until we have user data.
|
||||
|
||||
## Deferred / accepted debt
|
||||
|
||||
- **EOC effects chain integration** — out of v1 rewrite (recon flagged as legacy complexity).
|
||||
- **ShapeSeq sequencer** — gated behind `?shapeseq=1` in legacy; out of v1.
|
||||
- **Modular engine (Phase E)** — newer JS-side feature in legacy; out of v1.
|
||||
- **Engine configuration panel** (SPEC-controls Part 8) — backlog. Would let users tune network architecture, loss, optimizer at runtime. Currently compile-time only.
|
||||
- **VCV Rack module** — used to consume `nisps-core/`. Now gone. If revived, it'd consume `nisps/` directly via CMake; not currently maintained.
|
||||
|
||||
## Recently resolved (delete after a few weeks)
|
||||
|
||||
- 2026-04-29: Three-implementation ML duplication (firmware `memlp`, `nisps-core`, JS engine) collapsed to single `nisps/` C++ codebase.
|
||||
- 2026-04-29: Firmware mode forks (~280–400 lines duplicated across 8 modes) collapsed via `nisps/modes/base.hpp` CRTP scaffold; concrete modes are now ~50–130 lines.
|
||||
- 2026-04-29: meml-ues double-scaling MSE bug fixed in `nisps/ml/loss.hpp` + `mlp.hpp`.
|
||||
- 2026-04-29: `nisps-core/` retired; firmware is the canonical source of truth for ML.
|
||||
- 2026-04-29: `src/memlp/` submodule deleted.
|
||||
- 2026-04-29: Legacy playground variants (a-immersive.html, b-workbench, c-journey, designs.html, all `js/`) deleted in favor of SolidJS scaffold.
|
||||
- 2026-04-29: Native↔WASM parity verified within 1e-5 (max delta 2.4e-7) for representative ML + engine outputs.
|
||||
420
CLAUDE.md
420
CLAUDE.md
|
|
@ -4,307 +4,193 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Overview
|
||||
|
||||
MEMLNaut-NISPS (Neural Interactive Shaping of Parameter Spaces) is firmware for the MEMLNaut hardware platform - a custom embedded audio device built on Raspberry Pi Pico (RP2040). It implements interactive machine learning for real-time audio synthesis and processing, enabling users to shape sound parameters through reinforcement learning.
|
||||
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. A research platform for interactive ML control of audio. **One C++20 codebase** (`nisps/`) compiles to two targets:
|
||||
|
||||
1. **RP2350 firmware** for the MEMLNaut hardware platform (`firmware/`).
|
||||
2. **WASM** in a SolidJS browser playground (`playground/`) — same engines + ML, run through an AudioWorklet.
|
||||
|
||||
Browser audio engines are a superset of firmware engines (C15 is browser-only). Parameter contracts are JSON schemas (`schemas/`) with codegen producing both C++ headers and TypeScript types.
|
||||
|
||||
Project documentation: https://musicallyembodiedml.github.io/memlnaut/approaches/nisps
|
||||
|
||||
## NISPS Core Library
|
||||
For the codebase index, see `MAP.md`. For strategic gaps and open mission questions, see `ALIGNMENT.md`.
|
||||
|
||||
The `nisps-core/` directory contains a platform-agnostic C++20 extraction of the interactive ML engine. This header-only library can be used in any C++ project for neural network-based parameter mapping.
|
||||
|
||||
**Key differences from firmware**:
|
||||
- ✅ Platform-agnostic (no Arduino/RP2040 dependencies)
|
||||
- ✅ Header-only (just include and use)
|
||||
- ✅ C++20 (uses std::span)
|
||||
- ✅ Namespaced (`nisps::`)
|
||||
- ❌ No audio synthesis (use it to *control* your synth)
|
||||
- ❌ No hardware drivers
|
||||
|
||||
**Use case**: Control synthesizers, effects, lights, game parameters, or any system that responds to continuous parameters.
|
||||
|
||||
See `nisps-core/README.md` for complete documentation and examples.
|
||||
|
||||
## Web Playground
|
||||
|
||||
The `playground/` directory contains a browser-based interactive demo of the NISPS ML engine. No build step or dependencies — serve statically.
|
||||
|
||||
- **2 inputs** (virtual joystick X/Y) mapped through a `[3, 32, 48, 64, 126]` MLP to **126 outputs**
|
||||
- **Four output modes**:
|
||||
- **Visual**: first 20 outputs control a Canvas2D flow-field particle system
|
||||
- **Synth (C15)**: all 126 outputs control the C15 WASM synthesizer
|
||||
- **MIDI CC**: outputs mapped to configurable MIDI CC messages via WebMIDI
|
||||
- **Audio Canvas**: 36 outputs drive a generative audio sampler
|
||||
- **Two learning modes**: Examples (set slider targets, add examples, train) and RL Feedback (thumbs up/down with exploration noise)
|
||||
- **Serve statically**: `cd playground && python3 -m http.server`
|
||||
- **Mobile-first**: designed for touch/foldable phone use
|
||||
|
||||
Key files: `js/nisps/` (WASM engine + dataset), `js/ui/` (visualizer, joystick, controls, input pipeline, control surface), `js/synth/` (C15 bridge, param map, arpeggiator), `js/a-app.js` (immersive app wiring).
|
||||
|
||||
### WASM ML Engine
|
||||
|
||||
The immersive app (`a-immersive.html` / `a-app.js`) uses a WASM-compiled MLP for all inference and training. The legacy JS engine (`iml.js`, `mlp.js`, `layer.js`, `node.js`) is still used by the three older playground variants (`app.js`, `b-app.js`, `c-app.js`) but is slated for migration to WASM (see meml-dj9).
|
||||
|
||||
**Architecture:**
|
||||
## The `nisps/` core
|
||||
|
||||
```
|
||||
Main thread Worker thread
|
||||
WasmIML (nisps-wasm.js) nisps-wasm-worker.js
|
||||
├─ WASM instance A (persistent) └─ WASM instance B (lazy)
|
||||
│ inference() — every rAF tick trainEx() — off-thread
|
||||
│ inferBatch() — heatmap sampling returns: weights + loss curve
|
||||
│ moveWeightsEx() — RL exploration
|
||||
│ evalLoss() — non-destructive query
|
||||
│ getLayerStats() — per-layer health
|
||||
│ getWeights/setWeights — sync w/ worker
|
||||
│
|
||||
└─ Dataset (JS-side, dataset.js)
|
||||
├─ FIFO ring buffer (max 100 examples)
|
||||
└─ computeWeights() — recency/spatial/combined sample weighting
|
||||
nisps/
|
||||
├── core/ types, perf attrs, concepts (AudioEngine, MLEngine, Mode), fixed/ring buffers, deterministic RNG, math
|
||||
├── ml/ MLP class template (4-layer, 3 hidden); SGD, gradient clipping, spread-aware Xavier init,
|
||||
│ RL move_weights with output pin mask + per-layer scaling + weight decay
|
||||
├── dsp/ biquad, delay, reverb, filter, env, osc, pitch_shift, dc_blocker
|
||||
├── engines/ 8 audio engines (paf_synth, channel_strip, xiasri, verb_fx, memlcelium, breakor,
|
||||
│ elysiamorf, analysis) + NoOpEngine. Each satisfies the AudioEngine concept.
|
||||
├── modes/ 8 platform-agnostic modes binding {ML, engine, voice space, abstract I/O channels}.
|
||||
│ CRTP base eliminates the duplication that plagued firmware modes.
|
||||
└── wasm/ Emscripten C API bindings (compiled only for WASM target)
|
||||
```
|
||||
|
||||
**WASM bindings** (`playground/wasm/nisps_bindings.cpp`) expose a flat C API compiled via Emscripten:
|
||||
Build: `cmake -S nisps -B nisps/build -G Ninja && cmake --build nisps/build && ctest --test-dir nisps/build`.
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `nisps_mlp_create/destroy` | Lifecycle |
|
||||
| `nisps_mlp_inference` | Single forward pass |
|
||||
| `nisps_mlp_infer_batch` | N forward passes in one call (heatmap) |
|
||||
| `nisps_mlp_train` | SGD training, returns final loss only |
|
||||
| `nisps_mlp_train_ex` | SGD training with full per-iteration loss curve |
|
||||
| `nisps_mlp_draw_weights_spread` | Xavier-aware weight randomization |
|
||||
| `nisps_mlp_move_weights_spread` | RL noise with weight decay |
|
||||
| `nisps_mlp_move_weights_ex` | Same + native output pin mask |
|
||||
| `nisps_mlp_eval_loss` | Forward pass + MSE, no weight update |
|
||||
| `nisps_mlp_get_layer_stats` | Per-layer: mean|w|, max|w|, dead%, saturating% |
|
||||
| `nisps_mlp_get/set_weights` | Flat weight serialization |
|
||||
| `nisps_mlp_weight_count` | Total weight count |
|
||||
Tests: 4 executables (`nisps_core_tests`, `nisps_dsp_engine_tests`, `nisps_modes_tests`, `nisps_golden_tests`). Run all: `bash scripts/build-cpp-tests.sh`. Parity vs WASM: `bash scripts/parity-check.sh` (asserts native and WASM produce identical outputs within 1e-5).
|
||||
|
||||
**Building the WASM:**
|
||||
```bash
|
||||
cd playground/wasm && ./build.sh # requires emcc (Emscripten)
|
||||
### Performance contract (RP2350)
|
||||
|
||||
These rules apply to **all** code under `nisps/`. They are inert in WASM but kept globally for consistency.
|
||||
|
||||
- **No heap.** No `new`, `malloc`, `std::vector` in hot paths. Use `nisps::FixedBuffer<T, N>` or `std::array<T, N>`.
|
||||
- **Constants discipline.** Float literals >255 used in hot paths must be `static const float val = X.f;` not inline.
|
||||
- **`.f` suffix on all float literals.** No double promotion in audio/inference paths.
|
||||
- **Memory section attributes.** Apply `NISPS_AUDIO_MEM` / `NISPS_AUDIO_FUNC` / `NISPS_APP_SRAM` / `NISPS_HOT` / `NISPS_FORCE_INLINE` (from `nisps/core/perf.hpp`).
|
||||
- **No virtual dispatch in audio path.** `AudioEngine` and `Mode` are C++20 concepts, not interfaces.
|
||||
- **Deterministic RNG.** All RNG state is per-instance; constructors take a seed; cross-platform parity tests rely on this.
|
||||
|
||||
Lint: `bash scripts/lint-cpp.sh` warns on missing `.f` and fails on heap/`Arduino.h` use under `nisps/`.
|
||||
|
||||
## The `firmware/` target
|
||||
|
||||
```
|
||||
firmware/MEMLNaut-NISPS/
|
||||
├── MEMLNaut-NISPS.ino # Entry point; mode selected via #define MEMLNAUT_MODE_TYPE
|
||||
├── glue/
|
||||
│ ├── audio_driver.hpp # memllib AudioDriver block callback → Mode::process per-sample
|
||||
│ ├── peripherals.hpp # joystick / pots / buttons → Mode::set_input + ML primitives
|
||||
│ ├── midi_io.hpp # MIDI in → mode handlers; drain ControlEvent ring → MIDI UART
|
||||
│ ├── mode_select.hpp # type aliases firmware mode name → nisps::modes::*Mode
|
||||
│ ├── input_router.hpp # wire_inputs() entry point
|
||||
│ └── output_router.hpp # drain_outputs() entry point
|
||||
└── src/{memllib,daisysp,nisps} # symlinks (Arduino-CLI sketch tree convention)
|
||||
```
|
||||
|
||||
**Key difference from JS engine:** WASM uses float32 (not float64). The `spread`-aware `drawWeights`/`moveWeights` functions are implemented in the bindings file, not in nisps-core proper — they're playground-specific.
|
||||
Build: `scripts/build-firmware.sh [VARIANT]`. Verified compiling for PAFSynth, ChannelStrip, BreakOr on `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3` with `-std=gnu++20`. Flash: `scripts/flash-firmware.sh`. One-shot: `scripts/build-and-flash-firmware.sh`.
|
||||
|
||||
**Known issue:** Both the C++ `Train()` and WASM `train_ex` double-scale the loss when no sample weights are provided (each sample loss is weighted by 1/n, then the sum is multiplied by 1/n again). This is a backward-compat pattern from the C++ core (meml-ues).
|
||||
### Dual-core orchestration (firmware)
|
||||
|
||||
### Debug Probe
|
||||
- **Core 0**: UI loop, ML inference (`Mode::tick_control`), peripheral polling (5ms period).
|
||||
- **Core 1**: Real-time audio processing (`Mode::process`), MIDI polling.
|
||||
- **Sync**: `nisps::core::ring_buffer` (templated SPSC lock-free, replaces pico/util/queue) + memory barriers (`nisps::core::memory_barrier`, `write_volatile`/`read_volatile`).
|
||||
|
||||
The immersive app exposes `window.__nisps` when loaded with `?debug=1`. Used by Playwright e2e tests. Zero footprint in production.
|
||||
## The `playground/` target
|
||||
|
||||
| Method | Returns |
|
||||
|--------|---------|
|
||||
| `getOutputs()` | Current 126-element output vector |
|
||||
| `getLoss()` | Last training loss (or null) |
|
||||
| `getWeights()` | Flat weight array (~13K floats) |
|
||||
| `getExampleCount()` | Number of training examples |
|
||||
| `setInputs(x, y)` | Set joystick position + run inference |
|
||||
| `thumbsUp()` / `thumbsDown()` | Trigger RL feedback |
|
||||
| `train()` | Sync training with full UI update |
|
||||
| `trainAsync()` | Async training (returns Promise) |
|
||||
| `randomise()` | Randomize weights |
|
||||
| `clearExamples()` | Clear dataset |
|
||||
| `saveState()` | Force localStorage save |
|
||||
| `evalLoss()` | Non-destructive loss query |
|
||||
| `inferBatch(points)` | Batch inference |
|
||||
| `getLayerStats()` | Per-layer weight health |
|
||||
```
|
||||
playground/ # Vite + SolidJS + TypeScript
|
||||
├── src/
|
||||
│ ├── primitives/ # 16 UI building blocks (Slider, JoyMap, Heatmap, …) + .demo.tsx for /dev/primitives
|
||||
│ ├── modes/ # one TSX per firmware mode + C15Mode (browser-only); ModeShell + ModeSwitcher + mode-runtime
|
||||
│ ├── stores/ # Solid stores (ml, input, output, mode, control, session, exploration, bus + persistence)
|
||||
│ ├── audio/ # engine-host + AudioWorklet processor (loads nisps.wasm separately on each thread)
|
||||
│ ├── ml/ # WasmIML class + disposable async-training Worker + dataset
|
||||
│ ├── input/, output/ # pure-fn pipelines (deadzone→zoom→curve→smoothing→momentum, then global curve→smoothing→slew→freeze)
|
||||
│ ├── features/ # heatmap, snapshots, A/B compare, region/param pin, trail, weight health, gradient flow
|
||||
│ └── debug/probe.ts # synchronous window.__nisps for Playwright
|
||||
├── public/ # nisps.{wasm,js}, c15.{wasm,glue}
|
||||
└── tests/e2e/ # Playwright specs + helpers
|
||||
```
|
||||
|
||||
### URL Parameters
|
||||
Dev: `cd playground && bun install && bun run dev`. Build: `bun run build`. Typecheck: `bun run typecheck`. E2E: `bunx playwright test`.
|
||||
|
||||
### Stores + reactivity
|
||||
|
||||
All stores use SolidJS `createStore` for objects, `createSignal` for primitives. ML outputs are stored in a separate Float32Array signal (per the migration plan's perf guidance). Persistence (debounced 200ms localStorage round-trip) wired in `playground/src/stores/persistence.ts`. The signal bus (`bus.ts`) handles cross-store events (`ml.*`, `mode.*`, `pin.*`, `ui.*`).
|
||||
|
||||
### Control surface
|
||||
|
||||
Three compound axes (Boldness / Memory / Precision) interpolate per-axis tables to drive ~6 underlying parameters each, with offset overrides ("trim-pot" model). State is in `control-store`. Six built-in control presets (Default, First Touch, Jazz Hands, Sculptor, Improviser, Microscope) available via the ModeShell control bar.
|
||||
|
||||
### Debug probe (Playwright)
|
||||
|
||||
`window.__nisps` is exposed synchronously and bypasses Solid reactivity (uses `untrack`/`batch`). API matches the `.local/recon/04-playground.md` spec — `setInputs`, `getOutputs`, `getLoss`, `train`, `thumbsUp`/`thumbsDown`, `randomise`, `clearExamples`, `inferBatch`, `getLayerStats`, `saveState`, etc.
|
||||
|
||||
## The `schemas/` + `codegen/` contract
|
||||
|
||||
Each mode has a `schemas/modes/<mode>.json` describing its parameters (name, label, range, default, curve, group), ML config (input/output sizes, hidden layers), voice spaces (names — bodies are inline lambdas in the C++ engine), and UI config. The meta-schema at `schemas/schema.json` validates these.
|
||||
|
||||
Codegen (`bun run codegen/generate.ts`) emits:
|
||||
- `nisps/modes/generated/<mode>_schema.hpp` — `constexpr` C++ data, namespace `nisps::modes::generated`, re-exports `nisps::Curve` from `nisps/core/math.hpp`.
|
||||
- `playground/src/modes/generated/<mode>_schema.ts` — typed const objects + per-mode params interface.
|
||||
|
||||
Codegen is idempotent. Golden test ensures regenerating produces byte-identical output.
|
||||
|
||||
## WASM bridge
|
||||
|
||||
Two WASM instances at runtime:
|
||||
|
||||
1. **Main thread** (`playground/src/ml/wasm-iml.ts`): ML inference + sync training + RL primitives. Update store after each call. Async training via disposable Web Worker (`wasm-worker.ts`).
|
||||
2. **AudioWorklet** (`playground/src/audio/worklet/nisps-processor.ts`): runs engine `process_block` per audio block. Loads `nisps.wasm` directly via `WebAssembly.compile` (no Emscripten glue in worklet). Bytes posted from main thread.
|
||||
|
||||
C API is in `nisps/wasm/bindings.cpp`. Build: `bash scripts/build-wasm.sh` (~94KB output to `playground/public/`).
|
||||
|
||||
The WASM target is fixed at `MLP<2, 10, 14, 18, 126>`. Modes with smaller `output_size` use the first N outputs only.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- Loss history not yet plumbed through C API; `lossHistory` in the store is a single-element array per training run.
|
||||
- Engine MLP architecture is fixed at compile time — supporting per-mode hidden-layer shapes would need either multiple WASM modules or runtime variation.
|
||||
- Mic input through the worklet for XIASRI / SoundAnalysisMIDI is not wired; UI scaffolds render but feature is TODO.
|
||||
- C15 voice space integration in C15Mode is a placeholder.
|
||||
|
||||
## URL parameters (playground)
|
||||
|
||||
| Param | Range | Default | Effect |
|
||||
|-------|-------|---------|--------|
|
||||
| `tame` | 0–1 | 1 | Constrains synth output ranges toward safe limits |
|
||||
| `spread` | 0–1 | 0.6 | Controls weight initialization, RL noise scaling, and weight decay (see below) |
|
||||
| `preset` | preset id | _(none)_ | Auto-loads a synth parameter preset on first visit (e.g. `?preset=beginner-1`) |
|
||||
| `tame` | 0–1 | 1 | Constrains synth output ranges toward safe limits. |
|
||||
| `spread` | 0–1 | 0.6 | Master noise regime (init scale, RL noise cap, per-layer Xavier scaling, weight decay). |
|
||||
| `preset` | preset id | _(none)_ | Auto-loads a synth preset on first visit. |
|
||||
| `debug` | 1 | _(off)_ | Exposes `window.__nisps` debug probe. |
|
||||
|
||||
#### `spread` — sigmoid saturation control
|
||||
### `spread` — sigmoid saturation control
|
||||
|
||||
The MLP uses ReLU hidden layers with a sigmoid output layer. With uniform [-1,1] weights, the sum of many weighted inputs at each layer drives sigmoid pre-activations far from zero (std dev ≈ √fan_in), causing outputs to saturate near 0 or 1. The `spread` parameter addresses this:
|
||||
The MLP uses ReLU hidden layers with a sigmoid output. With uniform [-1,1] weights, the sum of many weighted inputs at each layer drives sigmoid pre-activations far from zero (std dev ≈ √fan_in), causing outputs to saturate. The `spread` parameter addresses this:
|
||||
|
||||
- **`spread=0`** (polarised): Weights drawn from uniform [-1,1]. RL noise cap = 0.3. Noise applied uniformly across layers. Outputs cluster at extremes — good for exploration of radical mappings.
|
||||
- **`spread=1`** (centered): Weights scaled by 1/√fan_in per layer (Xavier initialization). RL noise cap = 0.05. Noise also scaled per-layer. Weight decay prevents magnitude drift. Outputs spread across the full [0,1] range — better for fine-grained RL shaping.
|
||||
- **Intermediate values** interpolate linearly between these two regimes.
|
||||
- `spread=0` (polarised): uniform [-1,1] weights, RL noise cap 0.3, no decay. Outputs cluster at extremes — good for radical exploration.
|
||||
- `spread=1` (centered): Xavier-scaled weights, RL noise cap 0.05, 10% weight decay per move. Outputs spread across [0,1] — better for fine-grained shaping.
|
||||
- Intermediate values interpolate.
|
||||
|
||||
Affects four code paths:
|
||||
1. **`drawWeights(spread)`** — initial randomisation weight scale
|
||||
2. **`moveWeights(speed, spread)`** — RL exploration noise scale per layer
|
||||
3. **Weight decay in `moveWeights`** — each call decays weights by `10% * spread` before adding noise, preventing unbounded magnitude drift from repeated thumbs-down. At spread=0 there is no decay (original behavior). At spread=1, weights decay ~10% per call, creating a natural equilibrium where exploration noise and decay balance out rather than weights growing until sigmoid permanently saturates.
|
||||
4. **Noise cap** in thumbs-down handler — `0.3*(1-spread) + 0.05*spread`
|
||||
## Verification chokepoints (user-confirmed)
|
||||
|
||||
### C15 Parameter Map
|
||||
- **A. Hardware**: each firmware mode flashes and produces correct audio on RP2350.
|
||||
- **B. RP2350 perf**: no regression vs current main.
|
||||
- **C. Browser parity**: each firmware mode runs in browser via WASM, sounds equivalent.
|
||||
- **D. a-immersive feature parity**: control surface, snapshots, A/B compare, region/param pins, heatmap, weight health, gradient flow, output pipeline, session presets.
|
||||
- **E. CI green**: `bash scripts/run-all-tests.sh` (cmake build + ctest + WASM build + parity + lint + Playwright).
|
||||
|
||||
The 126 synth parameters in `js/synth/param-map.js` were curated from the C15's 287 total parameters. Excluded categories:
|
||||
|
||||
| Excluded | Count | Reason |
|
||||
|----------|-------|--------|
|
||||
| Hardware Amount/Source | 56 | No physical MIDI hardware in browser |
|
||||
| Macro Controls/Times | 12 | Meta-routing layer conflicts with direct ML control |
|
||||
| Scale offsets | 13 | Microtuning would break pitch unpredictably |
|
||||
| Key tracking (`*_KT`) | 11 | Pitch-dependent scaling needs calibrated defaults |
|
||||
| Velocity (`*_Vel`) | 11 | Velocity-dependent, ML can't observe key velocity |
|
||||
| Envelope mod depths (`*_Env_A/B/C`) | 19 | Multiplicative interaction with envelope shapes makes space too hard to learn |
|
||||
| Discrete/structural | 15 | Osc Pitch (full sweep), Master Vol/Tune, Voice Mute/Fade, Unison Voices, Mono modes, Split, Osc Reset |
|
||||
| Secondary config | 7 | Att Curve, Elevate, Chirp, Decay Gate, Retrigger |
|
||||
| PM shaper blend | 4 | Secondary routing params |
|
||||
| FB Mix source selects | 4 | Discrete A/B selectors |
|
||||
|
||||
### Synth Presets
|
||||
|
||||
Presets (`js/synth/presets.js`) control which parameters the ML engine can modify, with unselected params muted at safe defaults. Each preset defines per-param `{ muted, fixedValue, min, max, curve }` — no training examples or model weights.
|
||||
|
||||
4 tiers of progressive complexity:
|
||||
|
||||
| Tier | Presets | Active params | What's exposed |
|
||||
|------|---------|---------------|----------------|
|
||||
| 1 (Beginner) | 1.1–1.4 | 15 | Basic ADSR, SVF cutoff/res, Shaper A drive/fold, output levels, reverb mix |
|
||||
| 2 (Intermediate) | 2.1–2.4 | 40 | + Env B/C, filter FM, effects (reverb/echo/flanger), cabinet, stereo panning |
|
||||
| 3 (Advanced) | 3.1–3.3 | ~95 | + Cross-oscillator PM, feedback mixer, dual shapers, comb/gap filters, ring mod |
|
||||
| 4 (Expert) | 4.1–4.2 | 126 | Full engine |
|
||||
|
||||
Presets use `curve` values to bias parameter distributions (< 0.5 = spend more time low, > 0.5 = bias high) without clamping extremes. Users can tweak any preset via the group drawer after loading.
|
||||
|
||||
### Control Surface (Phase 1)
|
||||
|
||||
The immersive app (`a-immersive.html`) has a control surface system for tuning how exploration and learning feel. Full spec: `playground/SPEC-controls.md`.
|
||||
|
||||
**Architecture** — modular ES modules organized by phase, wired into `a-app.js`:
|
||||
|
||||
| Module | Phase | Purpose |
|
||||
|--------|-------|---------|
|
||||
| `js/ui/input-pipeline.js` | 1 | Processes raw joystick input through deadzone → zoom → curve → smoothing → momentum-as-zoom. Pure math, no DOM. |
|
||||
| `js/ui/control-surface.js` | 1 | Compound axes (Boldness, Memory, Precision) that map single sliders to multiple underlying params. Offset-based override resolution (trim-pot model). 6 built-in control presets. |
|
||||
| `js/ui/control-surface-ui.js` | 1 | DOM layer: 3 axis sliders on floating bar, gear icon settings drawer with per-param overrides. Injects its own CSS. |
|
||||
| `js/ui/joy-map-enhanced.js` | 1 | Enhanced joy-map canvas: zoom minimap with adaptive grid, vanishing trail with Catmull-Rom spline and tap-to-return, dual concentric noise rings, frozen state overlay. |
|
||||
| `js/ui/snapshot-stack.js` | 2 | Ring buffer (20 max) of weight snapshots. Auto-snapshot on train/randomize/thumbs-down. Multi-level undo. |
|
||||
| `js/ui/ab-compare.js` | 2 | Rapid A/B weight state comparison. Capture, toggle, accept or revert. |
|
||||
| `js/ui/region-pin.js` | 2 | Pins rectangular input-space regions (Approach A: example pinning). Pinned examples always included in training. |
|
||||
| `js/ui/param-pin.js` | 2 | Per-output pin flags. Pin mask passed to `moveWeights()` to skip pinned output nodes. |
|
||||
| `js/ui/phase2-ui.js` | 2 | DOM: undo button with history popup, A/B toggle, region pin via long-press, param pin via double-tap. |
|
||||
| `js/ui/pressure-feedback.js` | 3 | Touch force + hold duration → intensity multiplier for noise growth/decay. |
|
||||
| `js/ui/auto-explore.js` | 3 | Automated thumbs-down at configurable interval. Zoom-scaled intensity. |
|
||||
| `js/ui/input-heatmap.js` | 3 | 2D color field sampling MLP across input space. 3 color modes, zoom-aware resampling. Supports `inferBatchFn` for single-call WASM batch inference. |
|
||||
| `js/ui/phase3-ui.js` | 3 | DOM: auto-explore toggle with progress ring, heatmap eye icon, pressure indicators. |
|
||||
| `js/ui/output-pipeline.js` | 4 | Global curve → smoothing → slew rate → freeze gate on MLP outputs before synth/visual routing. |
|
||||
| `js/ui/weight-health.js` | 4 | Weight magnitude histogram, dead/saturating/healthy status detection, ambient visualization. |
|
||||
| `js/ui/gradient-flow.js` | 4 | Per-layer weight-delta analysis after training. Vanishing/exploding/converged detection. |
|
||||
| `js/ui/session-presets.js` | 4 | Save/load full session state. URL sharing via compact params. |
|
||||
| `js/ui/phase4-ui.js` | 4 | DOM: freeze button, network health panel, session preset UI, output pipeline slider wiring. |
|
||||
|
||||
**Compound Axes** — each controls 4-6 underlying parameters via interpolation tables:
|
||||
|
||||
- **Boldness** (Caution ↔ Bold): input zoom, noise cap, noise growth, learning rate, weight decay, noise distribution
|
||||
- **Memory** (Amnesia ↔ Elephant): max examples, example decay, weight decay, noise decay, convergence threshold
|
||||
- **Precision** (Raw ↔ Precise): input curve, deadzone, smoothing, slew rate, momentum-zoom mode
|
||||
|
||||
When a user manually overrides an individual param, the offset from the axis-derived value persists as the axis moves (like a trim pot on a mixing desk). Double-tap an axis to re-link all params.
|
||||
|
||||
**Input Pipeline** — sits between physical joystick and MLP. Key feature: **zoom** narrows the effective input window around an anchor point (`effective = anchor + (raw - 0.5) * zoom_level`). Zoom-at-zero freezes input. Three anchor modes: auto (anchor follows current position when zoom changes), sticky (explicit anchor), center (always 0.5).
|
||||
|
||||
**Control Presets**: Default, First Touch, Jazz Hands, Sculptor, Improviser, Microscope. These set compound axis positions — they don't include network weights or synth preset selection.
|
||||
|
||||
**Integration** — the control surface dispatches `controlsurface:change` CustomEvents. `a-app.js` listens and updates the input pipeline config, spread level, and RL parameters (noise cap, growth, decay, floor, zoom-aware feedback scaling). Pipeline-processed coordinates are cached (`_lastPipeX/Y`) so `getCurrentInputs()` and `setCurrentInputs()` use the same values the MLP sees. State is persisted to localStorage alongside existing app state.
|
||||
|
||||
**Remaining**: Engine configuration panel (Part 8 of spec) — network architecture, loss function, optimizer selection.
|
||||
|
||||
## Testing
|
||||
|
||||
Playwright e2e tests cover the immersive app's ML engine, UI state machines, input pipeline, and persistence. Tests run headless Chromium against a Python HTTP server.
|
||||
## Build system summary
|
||||
|
||||
```bash
|
||||
# Run all tests (starts server automatically on port 7331)
|
||||
npx playwright test
|
||||
|
||||
# Run with browser visible
|
||||
npx playwright test --headed
|
||||
|
||||
# Run a specific test file
|
||||
npx playwright test tests/e2e/ml-engine.spec.js
|
||||
```
|
||||
|
||||
**Test files** (`tests/e2e/`):
|
||||
|
||||
| File | Coverage |
|
||||
|------|----------|
|
||||
| `ml-engine.spec.js` | WASM inference bounds, training loss, thumbs up/down, async training, example capture |
|
||||
| `ui-interactions.spec.js` | Drawer open/close, mode switching, heatmap bars, presets, keyboard shortcuts (1/2/Z) |
|
||||
| `input-pipeline.spec.js` | Input→output variation, clamping, joystick drag, post-training bounds |
|
||||
| `persistence.spec.js` | URL params (?preset, ?spread), localStorage round-trip |
|
||||
| `wasm-api.spec.js` | Batch inference, evalLoss, getLayerStats, loss history curve, pin mask |
|
||||
|
||||
Tests use the `?debug=1` probe (`window.__nisps`) for programmatic access to the ML engine. The `helpers.js` module provides `loadApp(page)` which clears localStorage, sets `nisps-help-seen`, and waits for WASM initialization.
|
||||
|
||||
## Build System
|
||||
|
||||
This is an Arduino project targeting the MEMLNaut RP2350 hardware. Build and flash it with the repo-local helper scripts, which wrap the correct board target and compiler settings.
|
||||
|
||||
```bash
|
||||
# Initialize submodules (required for memllib)
|
||||
# Initialize submodules (required for memllib + daisysp)
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Build only
|
||||
scripts/build-firmware.sh
|
||||
# Codegen (run after editing any schemas/modes/*.json)
|
||||
cd codegen && bun install && bun run generate.ts
|
||||
|
||||
# Build a specific variant
|
||||
scripts/build-firmware.sh MEMLCelium
|
||||
# C++ host tests
|
||||
bash scripts/build-cpp-tests.sh
|
||||
|
||||
# Flash a previously-built UF2
|
||||
# WASM
|
||||
bash scripts/build-wasm.sh
|
||||
|
||||
# Cross-platform parity
|
||||
bash scripts/parity-check.sh
|
||||
|
||||
# Lint
|
||||
bash scripts/lint-cpp.sh
|
||||
|
||||
# Firmware
|
||||
scripts/build-firmware.sh PAFSynth # or any other variant
|
||||
scripts/flash-firmware.sh
|
||||
|
||||
# Build then flash
|
||||
scripts/build-and-flash-firmware.sh
|
||||
|
||||
# Playground
|
||||
cd playground && bun install
|
||||
bun run dev # Vite dev (COOP/COEP enabled)
|
||||
bun run typecheck
|
||||
bun run build
|
||||
bunx playwright test
|
||||
|
||||
# All tests
|
||||
bash scripts/run-all-tests.sh
|
||||
```
|
||||
|
||||
The scripts build for `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3` and force C++20 via `compiler.cpp.extra_flags=-std=gnu++20`. The sketch lives at `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino`; everything platform-agnostic (ML, DSP, engines, modes) is under `nisps/` and reached via in-sketch `src/` symlinks.
|
||||
If no variant is passed to `build-firmware.sh` in an interactive shell, it parses the available `MEMLNautMode*` options from the sketch, prompts for one, and rewrites the active `MEMLNAUT_MODE_TYPE` before compiling.
|
||||
## Issue tracking
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual-Core Design
|
||||
|
||||
The RP2040's dual cores are used for separation of concerns:
|
||||
- **Core 0**: UI loop, ML inference, hardware interface polling (5ms period)
|
||||
- **Core 1**: Real-time audio processing, parameter updates, MIDI polling
|
||||
|
||||
Inter-core synchronization uses memory barriers (`MEMORY_BARRIER()`, `WRITE_VOLATILE()`, `READ_VOLATILE()`) and RP2040 queues (`queue_t`).
|
||||
|
||||
### Mode System
|
||||
|
||||
The active mode is selected at compile-time via `#define MEMLNAUT_MODE_TYPE` in `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino`. The macro expands to a type alias defined in `firmware/MEMLNaut-NISPS/glue/mode_select.hpp` that maps each `MEMLNautMode<Name>` identifier to a concrete `nisps::modes::*Mode` type. Each mode satisfies the C++20 `nisps::Mode` concept (`nisps/core/concepts.hpp`):
|
||||
|
||||
| Mode | Purpose |
|
||||
|------|---------|
|
||||
| `MEMLNautModeChannelStrip` | Audio channel strip (EQ, compression, gain staging) |
|
||||
| `MEMLNautModePAFSynth` | PAF (Phase Aligned Formant) synthesis with MIDI |
|
||||
| `MEMLNautModeXIASRI` | Audio-reactive mode using machine listening analysis |
|
||||
| `MEMLNautModeSoundAnalysisMIDI` | Sound analysis with MIDI output |
|
||||
| `MEMLNautModeBreakOr` | Break\|\| 8-track ratio sequencer |
|
||||
| `MEMLNautModeVerbFX` | Reverb/effects engine |
|
||||
| `MEMLNautModeElysiamorfs` | Elysiamorf granular sequencer |
|
||||
| `MEMLNautModeMEMLCelium` | MEMLCelium dual-voice synth + sequencer |
|
||||
|
||||
### Voice Spaces
|
||||
|
||||
Voice spaces map ML output vectors → engine parameters. They live as static lambdas inside each engine in `nisps/engines/*.hpp` (no longer in a separate `voicespaces/` directory). The mode picks a voice space at runtime via `engine().set_voice_space(idx)`.
|
||||
|
||||
### Key Components
|
||||
|
||||
- **`nisps::ModeBase`** (`nisps/modes/base.hpp`): CRTP scaffold; absorbs input forwarding, ML inference, voice-space dispatch, control-event ring buffer.
|
||||
- **`nisps::ml::MLP<NIn, NHidden..., NOut>`** (`nisps/ml/mlp.hpp`): Templated MLP with SGD/RMSProp, RL primitives (`move_weights`, `draw_weights`).
|
||||
- **`firmware/glue/audio_driver.hpp`**: Bridge from memllib `AudioDriver` per-block callback to `Mode::process(stereosample_t)` per-sample.
|
||||
- **`firmware/glue/peripherals.hpp`**: Joystick / pots / buttons → `Mode::set_input` and ML primitives.
|
||||
- **`nisps::AnalysisEngine`** (`nisps/engines/analysis.hpp`): Real-time audio feature extraction (pitch, aperiodicity, energy, brightness).
|
||||
|
||||
### Submodules (in `src/`)
|
||||
|
||||
- **memllib**: Hardware abstraction, audio drivers, MIDI, display.
|
||||
- **daisysp**: DSP library (filters, drums, effects, synthesis).
|
||||
|
||||
## Memory Sections
|
||||
|
||||
The codebase uses RP2040-specific memory placement:
|
||||
- `AUDIO_MEM` / `AUDIO_FUNC`: Place audio-critical code/data in SRAM
|
||||
- `APP_SRAM` / `__not_in_flash("app")`: Keep frequently-accessed data out of flash
|
||||
|
||||
## Audio Parameters
|
||||
|
||||
Sample rate is defined in `AudioDriver::GetSampleRate()`. The audio callback `audio_block_callback` runs on Core 1 and processes stereo audio (`stereosample_t`).
|
||||
This project uses **bd (beads)** for ALL task tracking. See `AGENTS.md` for conventions. Do not create markdown TODO lists or use other trackers.
|
||||
|
|
|
|||
174
MAP.md
174
MAP.md
|
|
@ -1,106 +1,110 @@
|
|||
# MAP
|
||||
|
||||
MEMLNaut-NISPS: Neural Interactive Shaping of Parameter Spaces. Two living artefacts share one ML core: (1) Arduino/RP2040 firmware for the MEMLNaut hardware, and (2) a browser playground that uses a WASM build of the same MLP to drive a C15 synth + other outputs. A header-only `nisps-core/` extraction is reused by the playground (via WASM bindings) and a VCV Rack module. See `CLAUDE.md` for the long-form architecture narrative.
|
||||
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 codebase (`nisps/`) compiles to two targets: (1) Arduino/RP2350 firmware for the MEMLNaut hardware, (2) WASM in a SolidJS browser playground that runs the same engines + ML through an AudioWorklet. Browser audio engines are a superset of firmware engines (C15 is browser-only). See `CLAUDE.md` for the long-form architecture narrative and `ALIGNMENT.md` for current strategic gaps.
|
||||
|
||||
## Layout
|
||||
|
||||
### Firmware (Arduino, RP2350)
|
||||
- `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` — sketch entry point; dual-core orchestration, mode selected at compile-time via `#define MEMLNAUT_MODE_TYPE`.
|
||||
### `nisps/` — platform-agnostic C++20 library (the only ML/DSP/engine code)
|
||||
- `nisps/core/` — `perf.hpp` (memory section attrs), `types.hpp`, `concepts.hpp` (`MLEngine`, `AudioEngine`, `Mode`), `fixed_buffer.hpp`, `ring_buffer.hpp` (SPSC lock-free, replaces pico/util/queue), `rng.hpp` (xoshiro256+ deterministic), `math.hpp` (fast_sigmoid, `Curve` enum + `apply_curve`).
|
||||
- `nisps/ml/` — MLP class template `MLP<NIn, NH1, NH2, NH3, NOut>`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (SGD + grad clipping), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `stats.hpp`.
|
||||
- `nisps/dsp/` — `biquad.hpp`, `delay.hpp`, `reverb.hpp`, `filter.hpp`, `env.hpp`, `osc.hpp`, `pitch_shift.hpp`, `dc_blocker.hpp`. Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.
|
||||
- `nisps/engines/` — eight audio engines, each satisfying `AudioEngine`: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp` (sequencer, NoOp audio), `elysiamorf.hpp` (sequencer, NoOp audio), `analysis.hpp` (input-side spectral features). Plus `base.hpp` (`NoOpEngine`, engine_id "thru").
|
||||
- `nisps/modes/` — eight platform-agnostic modes binding `{ML config, engine, voice space lambdas, abstract I/O channels}`. Files: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp`, `elysiamorf.hpp`, `sound_analysis_midi.hpp`. `base.hpp` provides a CRTP scaffold eliminating the duplication that previously plagued firmware modes. `voice_space.hpp` holds engine-side voice space dispatch helpers. `generated/` contains codegen output (do not edit by hand).
|
||||
- `nisps/wasm/bindings.cpp` — flat C API exported to WASM (Emscripten target only).
|
||||
- `nisps/CMakeLists.txt` + `nisps/build/` — host-target builds + ctest.
|
||||
|
||||
### `firmware/` — Arduino sketch + hardware glue
|
||||
- `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` — entry point. Selects active mode at compile time via `#define MEMLNAUT_MODE_TYPE`.
|
||||
- `firmware/MEMLNaut-NISPS/glue/` — hardware bindings:
|
||||
- `audio_driver.hpp` — bridges memllib `AudioDriver` block callback → `Mode::process(stereosample_t)` per-sample loop.
|
||||
- `peripherals.hpp` — wires joystick / pots / buttons → `Mode::set_input` and ML primitives (`draw_weights`, `move_weights`, `train`, `reset`).
|
||||
- `midi_io.hpp` — incoming MIDI → mode `note_on/update_bpm/set_playing`; drains mode `ControlEvent` ring → MIDI UART.
|
||||
- `mode_select.hpp` — type aliases mapping `MEMLNautMode<Name>` identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line.
|
||||
- `audio_driver.hpp` — bridges memllib `AudioDriver` callback → `Mode::process(stereosample_t)`.
|
||||
- `peripherals.hpp` — joystick / pots / buttons → `Mode::set_input` and ML primitives.
|
||||
- `midi_io.hpp` — MIDI in → mode `note_on`/`update_bpm`/`set_playing`; drains `ControlEvent` ring → MIDI UART.
|
||||
- `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line.
|
||||
- `input_router.hpp`, `output_router.hpp` — top-level `wire_inputs()` / `drain_outputs()` entry points.
|
||||
- `firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}` — symlinks into the repo's submodules and `nisps/` library. Required by Arduino-CLI's sketch tree convention.
|
||||
- `firmware/README.md` — structure, build, and verification notes.
|
||||
- `nisps/` — platform-agnostic C++20 ML / DSP / engines / modes (see "nisps library" below). The firmware glue layer composes these.
|
||||
- `src/memllib/` — git submodule (hardware abstraction). **Not auto-initialized** — build breaks without `git submodule update --init --recursive`.
|
||||
- `src/daisysp/` — vendored DSP library (filters, drums, effects).
|
||||
- `data/` — preset/asset CSVs.
|
||||
- `firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}` — symlinks (Arduino-CLI requires sketch-tree includes; preprocessor refuses `..` in headers).
|
||||
- `firmware/README.md` — structure + build instructions.
|
||||
|
||||
### nisps library (platform-agnostic C++20)
|
||||
- `nisps/core/` — foundational types, perf attrs, concepts (`AudioEngine`, `Mode`, `MLEngine`), fixed buffers, RNG, math.
|
||||
- `nisps/ml/mlp.hpp` + `activations.hpp`, `loss.hpp`, `training.hpp`, `init.hpp`, `rl.hpp`, `stats.hpp` — MLP class template with SGD, RMSProp, RL primitives.
|
||||
- `nisps/dsp/` — biquad, delay, reverb, filter, env, osc, pitch_shift, dc_blocker — extracted DSP primitives.
|
||||
- `nisps/engines/` — `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp`, `elysiamorf.hpp`, `analysis.hpp`, plus `base.hpp` with `NoOpEngine`. Each satisfies `AudioEngine`.
|
||||
- `nisps/modes/` — one mode per engine, all derive from `ModeBase` CRTP scaffold. Schemas in `nisps/modes/generated/` (codegen output).
|
||||
- `nisps/wasm/` — Emscripten bindings (used by the playground build, not the firmware).
|
||||
- `nisps/CMakeLists.txt` + `nisps/build/` — host-target tests.
|
||||
### `playground/` — SolidJS + Vite + TypeScript app
|
||||
- `playground/index.html`, `vite.config.ts`, `tsconfig.json`, `package.json` — scaffold. COOP/COEP headers configured.
|
||||
- `playground/src/main.tsx`, `App.tsx` — entry + router (`/`, `/dev/primitives`, `/modes`).
|
||||
- `playground/src/primitives/` — 16 UI building blocks: `Slider`, `SliderBank`, `VirtualJoystick`, `XYPad`, `Heatmap`, `OutputDisplay`, `TrainingControls`, `Drawer`, `ControlAxis`, `ProgressRing`, `PillToggle`, `ParamEditor`, `JoyMap`, `WeightHealth`, `GradientFlow`, `LossPlot`. Each has a `.demo.tsx` showcased on `/dev/primitives`.
|
||||
- `playground/src/modes/` — one TSX per firmware mode (+ `C15Mode` browser-only). `ModeShell.tsx` is the shared scaffold; `ModeSwitcher.tsx` picks the active mode; `mode-runtime.ts` is the schema → ML → audio wiring hook; `mode-helpers.ts` for SliderBank configs. `generated/` holds codegen-produced TS schemas (do not edit).
|
||||
- `playground/src/stores/` — Solid stores: `ml-store`, `input-store`, `output-store`, `mode-store`, `control-store` (compound axes Boldness/Memory/Precision), `session-store` (snapshots, A/B, presets), `exploration-store`, `bus` (typed signal bus). `persistence.ts` debounces localStorage writes.
|
||||
- `playground/src/audio/engine-host.ts`, `worklet/nisps-processor.ts` — main-thread engine host + AudioWorklet processor. WASM loaded twice (main thread for ML, worklet for engines).
|
||||
- `playground/src/ml/wasm-iml.ts`, `wasm-worker.ts`, `dataset.ts`, `types.ts` — main-thread WasmIML class + disposable async-training worker + FIFO dataset.
|
||||
- `playground/src/input/pipeline.ts`, `playground/src/output/pipeline.ts`, `playground/src/output/curves.ts` — pure-fn pipelines (deadzone→zoom→curve→smoothing→momentum, then global curve→smoothing→slew→freeze).
|
||||
- `playground/src/features/` — additional feature modules (heatmap sampling, snapshot stack, A/B compare, region pin, param pin, trail, weight health, etc.).
|
||||
- `playground/src/debug/probe.ts` — synchronous `window.__nisps` debug probe for Playwright.
|
||||
- `playground/public/nisps.{wasm,js}`, `c15.wasm`, `c15-glue.js` — compiled WASM artifacts (built by `scripts/build-wasm.sh`).
|
||||
- `playground/tests/e2e/` — Playwright specs (`ml-engine`, `modes`, `persistence`, `ui-interactions`) + `helpers.ts`.
|
||||
- `playground/playwright.config.ts` — Vite preview server setup.
|
||||
|
||||
### nisps-core (legacy; superseded by `nisps/`)
|
||||
- `nisps-core/` — earlier header-only extraction. Kept until cleanup stream.
|
||||
### `schemas/` — JSON parameter contracts (firmware/browser source of truth)
|
||||
- `schemas/schema.json` — Draft 2020-12 meta-schema validating mode files.
|
||||
- `schemas/modes/<mode>.json` (×8) — each mode's params, ranges, defaults, curves, voice spaces, ML config.
|
||||
- `schemas/modes/params_notes.md` — provenance notes and judgement calls per mode.
|
||||
|
||||
### Playground (browser ML demo)
|
||||
- `playground/index.html` — hub linking to the three variants.
|
||||
- `playground/a-immersive.html` + `js/a-app.js` — **primary** app. WASM engine, full control surface, modular/engine-switcher, C15 + MIDI + audio-canvas outputs.
|
||||
- `playground/b-workbench.html` + `js/b-app.js`, `c-journey.html` + `js/c-app.js` — older variants on the legacy JS engine. Feature-frozen; drift vs. a-app is intentional (see `CLAUDE.md` memory on `playground/RECONCILIATION.md` — note: file does **not** currently exist).
|
||||
- `playground/designs.html`, `js/app.js` — oldest experimental app. Kept for reference.
|
||||
- `playground/wasm/` — Emscripten build: `nisps_bindings.cpp` (C API, float32), `build.sh`, compiled `nisps.wasm`/`nisps.js`.
|
||||
- `playground/js/nisps/` — `nisps-wasm.js` (WasmIML wrapper), `nisps-wasm-worker.js` (off-thread train), `dataset.js` (FIFO ring buffer, max 100), legacy pure-JS engine (`iml.js`, `mlp.js`, `layer.js`, `node.js`) used by b/c apps.
|
||||
- `playground/js/synth/` — `c15-bridge.js`, `param-map.js` (126 curated C15 params), `presets.js` (4 tiers), `arpeggiator.js`.
|
||||
- `playground/js/ui/` — UI modules. Categories:
|
||||
- Input: `input-pipeline.js`, `joystick.js`, `joy-map-enhanced.js`, `gamepad.js`, `hand-tracker.js`, `eoc-*.js`.
|
||||
- Control surface: `control-surface.js`, `control-surface-ui.js` (3 compound axes: Boldness / Memory / Precision).
|
||||
- Training/exploration: `snapshot-stack.js`, `ab-compare.js`, `region-pin.js`, `param-pin.js`, `auto-explore.js`, `pressure-feedback.js`, `input-heatmap.js`.
|
||||
- Output/debug: `output-pipeline.js`, `weight-health.js`, `gradient-flow.js`, `session-presets.js`, `visualizer.js`, `param-display.js`, `dev-panel.js`.
|
||||
- Phase wiring: `phase2-ui.js`, `phase3-ui.js`, `phase4-ui.js`.
|
||||
- Modular mode: `modular-ui.js` (~52k, large), `engine-switcher.js` — newer; not yet documented in `CLAUDE.md`.
|
||||
- `playground/c15/`, `playground/faust/`, `playground/osc-bridge/` — external synth/bridge assets.
|
||||
- `playground/SPEC-controls.md`, `SPEC-shapeseq.md`, `ARCHITECTURE.md`, `PLAN-solidjs-migration.md`, `TODOS.md`, `README.md`, `devlog/` — docs.
|
||||
### `codegen/` — schema → C++/TS code
|
||||
- `codegen/generate.ts` — Bun script: validates schemas via ajv, emits per-mode `nisps/modes/generated/<mode>_schema.hpp` (`constexpr`, `nisps::modes::generated`) and `playground/src/modes/generated/<mode>_schema.ts`. Idempotent.
|
||||
- `codegen/templates/`, `codegen/tests/golden/` — reference templates + golden snapshot for paf_synth.
|
||||
|
||||
### Other consumers
|
||||
- `vcv/` — VCV Rack plugin using `nisps-core` (`src/MEMLNaut.cpp`, `SPEC.md`, `NISPS-FORMAT.md`).
|
||||
### `tests/cpp/` — host C++ tests
|
||||
- Per-component tests: `test_dsp_*.cpp`, `test_engine_*.cpp`, `test_mlp_*.cpp`, `test_mode_*.cpp`, `test_fixed_buffer.cpp`, `test_ring_buffer.cpp`, `test_rng.cpp`, `test_math.cpp`. Helpers in `test_helpers.hpp`.
|
||||
- Verification: `ml_golden_vectors.cpp`, `engine_impulse.cpp` (+ `engine_impulse_baseline.bin`), `parity_check.cpp` + `parity_wasm.mjs` + `parity_diff.mjs` — native-vs-WASM bit-equivalence within 1e-5.
|
||||
|
||||
### Tests
|
||||
- `tests/e2e/*.spec.js` — Playwright e2e against the immersive app via the `?debug=1` probe (`window.__nisps`). Covers ml-engine, wasm-api, ui-interactions, input-pipeline, persistence, engine-switching, modular-mode. Shared helpers in `tests/e2e/helpers.js`.
|
||||
- `playwright.config.js`, `package.json` — auto-starts a static server on port 7331.
|
||||
### `scripts/` — build + verify entry points
|
||||
- `build-firmware.sh`, `flash-firmware.sh`, `build-and-flash-firmware.sh`, `firmware-common.sh` — Arduino-CLI wrapper for RP2350 target with C++20 flag.
|
||||
- `build-wasm.sh` — Emscripten compile producing `playground/public/nisps.{wasm,js}`.
|
||||
- `build-cpp-tests.sh` — CMake configure + build + ctest (Ninja).
|
||||
- `parity-check.sh` — runs native + WASM and diffs binary outputs.
|
||||
- `lint-cpp.sh` — `.f` literal warn + heap/`Arduino.h` violation fail.
|
||||
- `run-all-tests.sh` — master verification script.
|
||||
|
||||
### Top-level docs / planning
|
||||
- `CLAUDE.md` — architecture narrative for both firmware and playground.
|
||||
- `AGENTS.md` — beads/bd conventions.
|
||||
- `NISPS_CORE_EXTRACTION_PLAN.md`, `NISPS_CORE_TASKS.md` — extraction task list; status unclear, likely stale now that `nisps-core/` exists.
|
||||
### `.github/workflows/`
|
||||
- `ci.yml` — GitHub Actions: cmake build + ctest + WASM build + parity check + lint + Playwright (cpp-tests + playground-tests jobs). Firmware compile is documented as manual.
|
||||
|
||||
### Submodules (in `src/`)
|
||||
- `src/memllib/` — hardware abstraction (audio driver, peripherals, MIDI). **Not auto-initialized** — fresh clones need `git submodule update --init --recursive`.
|
||||
- `src/daisysp/` — vendored DSP library. Used by some firmware glue; nisps replaced its PitchShifter with a custom granular impl.
|
||||
|
||||
### Top-level docs
|
||||
- `CLAUDE.md` — long-form architecture narrative.
|
||||
- `MAP.md` — this file.
|
||||
- `ALIGNMENT.md` — strategic gaps + open mission questions, dated, opinionated.
|
||||
- `README.md` — short quickstart.
|
||||
- `AGENTS.md` — beads/bd conventions.
|
||||
|
||||
## Entry points
|
||||
- **Firmware**: `scripts/build-firmware.sh`, `scripts/flash-firmware.sh`, or `scripts/build-and-flash-firmware.sh` (requires submodules initialised). `build-firmware.sh` can take an explicit variant name like `MEMLCelium` or prompt interactively from the parsed `MEMLNautMode*` list and rewrite the active mode in `MEMLNaut-NISPS.ino`. Matching is case-insensitive, but user-facing prompts preserve the canonical mode capitalization. The scripts target `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3` and force C++20. Execution = `setup()`/`loop()` on Core 0, `setup1()`/`loop1()` on Core 1, audio ISR on Core 1.
|
||||
- **Playground**: `cd playground && python3 -m http.server` (or `serve.sh` / `serve-coop.py`), open `a-immersive.html`. Append `?debug=1` to expose `window.__nisps`.
|
||||
- **WASM rebuild**: `cd playground/wasm && ./build.sh` (needs `emcc`).
|
||||
- **Tests**: `npx playwright test` (auto-spawns server on 7331).
|
||||
- **VCV module**: built inside `vcv/` with the VCV Rack SDK.
|
||||
|
||||
- **Firmware**: `scripts/build-firmware.sh [VARIANT]` (interactive prompt if omitted), `scripts/flash-firmware.sh`, `scripts/build-and-flash-firmware.sh`. Target: `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3`, `-std=gnu++20`.
|
||||
- **Playground dev**: `cd playground && bun install && bun run dev` (Vite, port 5173, COOP/COEP headers).
|
||||
- **Playground build**: `cd playground && bun run build`.
|
||||
- **WASM rebuild**: `bash scripts/build-wasm.sh` (needs `emcc`).
|
||||
- **Host C++ tests**: `bash scripts/build-cpp-tests.sh`.
|
||||
- **Parity check**: `bash scripts/parity-check.sh`.
|
||||
- **All tests**: `bash scripts/run-all-tests.sh`.
|
||||
- **Playwright**: `cd playground && bunx playwright test`.
|
||||
- **Codegen**: `cd codegen && bun run generate.ts` (regenerates `nisps/modes/generated/` and `playground/src/modes/generated/`).
|
||||
|
||||
## Conventions
|
||||
- Firmware mode selection is compile-time only; only one `MEMLNAUT_MODE_TYPE` uncommented at a time in `MEMLNaut-NISPS.ino`.
|
||||
- RP2040 memory placement via `APP_SRAM`, `AUDIO_MEM`, `AUDIO_FUNC`, `__not_in_flash("app")`. Audio hot paths use `__force_inline` / `__hot` / `__flatten`.
|
||||
- Cross-core sync: `MEMORY_BARRIER()`, `WRITE_VOLATILE`/`READ_VOLATILE`, RP2040 `queue_t`.
|
||||
- Voice spaces are header-only structs whose mappings are lambdas capturing synth state — **implicit coupling** to synth members.
|
||||
- Playground ML engines (`IML`, `WasmIML`) share a duck-typed interface (`inference`, `train`, `getWeights`/`setWeights`, …); WASM uses **float32**, JS engine uses float64.
|
||||
- `Dataset` is a **FIFO ring buffer**, default max 100 examples; recency/spatial sample weighting is computed JS-side.
|
||||
- Spread-aware weight init / RL noise (`drawWeightsSpread`, `moveWeightsEx`) live in `playground/wasm/nisps_bindings.cpp`, **not** in `nisps-core` proper — they are playground-specific.
|
||||
- Playground UI modules dispatch `controlsurface:change` CustomEvents; `a-app.js` listens and reconfigures the input pipeline, spread, and RL params.
|
||||
- URL params: `?tame`, `?spread`, `?preset`, `?debug=1`, `?shapeseq=1`.
|
||||
- Persistent memory (`bd remember`) notes:
|
||||
- ShapeSeq is gated behind `?shapeseq=1` until solid — arp remains default.
|
||||
- ShapeSeq MLP plan is **switchable mode** (unified single-MLP first, then dual-MLP option).
|
||||
- `playground/RECONCILIATION.md` is supposed to track features landed in a-app but not yet in b/c. File does not currently exist — if you add a-only features, either create it or explicitly accept the drift.
|
||||
|
||||
- Firmware mode selection is compile-time only — `#define MEMLNAUT_MODE_TYPE` in the `.ino`.
|
||||
- `nisps/` follows Chris's RP2350 perf rules globally: no heap, `static const float` for non-trivial constants, strict `.f` suffix, memory section attrs (`NISPS_AUDIO_MEM`, `NISPS_AUDIO_FUNC`, `NISPS_APP_SRAM`, `NISPS_HOT`, `NISPS_FORCE_INLINE`).
|
||||
- C++ identifiers: `PascalCase` types, `snake_case` functions/variables, `kPascalCase` constexpr. JSON keys `snake_case`. TS types `PascalCase`, components `PascalCase.tsx`, modules `kebab-case.ts`.
|
||||
- `Curve` enum lives in `nisps/core/math.hpp` (lowercase: `linear/exp/log/square/sqrt/sigmoid/cubic`); generated mode headers re-export via `using Curve = ::nisps::Curve;`. TS mirror at `playground/src/output/curves.ts` with same names.
|
||||
- Modes are TSX components composed of primitives; mode parameter contracts are JSON schemas with codegen → C++/TS types. **No declarative JSON UI.**
|
||||
- WASM and firmware share the same C++; WASM is fixed at `MLP<2, 10, 14, 18, 126>` and modes use a slice of outputs based on schema's `output_size`.
|
||||
- Cross-platform parity: `scripts/parity-check.sh` enforces native vs WASM agreement within 1e-5.
|
||||
|
||||
## Gotchas
|
||||
- `memllib` submodule is **not auto-checked-out** — a fresh clone will fail to compile the firmware silently. (`src/memlp` was deleted; `nisps/ml/mlp.hpp` replaces it.)
|
||||
- The Arduino sketch lives at `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` (Arduino-CLI requires sketch dir name == sketch file name). It reaches `nisps/`, `src/memllib/`, `src/daisysp/` via symlinks under `firmware/MEMLNaut-NISPS/src/` because Arduino's preprocessor refuses `..` in includes from sketch headers.
|
||||
- `firmware/MEMLNaut-NISPS/glue/mode_select.hpp` `#undef`s Arduino's `sq`/`min`/`max`/`abs`/`round` macros before pulling nisps headers — the nisps engines use those identifiers as method names.
|
||||
- The audio bridge struct `nisps_firmware::g_active_mode_bridge` is `extern` in `glue/audio_driver.hpp` and defined in the .ino; combining `inline` with the `__not_in_flash` section attribute creates a comdat / section conflict at link time.
|
||||
- Double-scaled loss: C++ `Train()` and WASM `train_ex` both divide by `n` when no sample weights are supplied (known, backward-compat, tracked as `meml-ues`).
|
||||
- Recent fixes cluster around the modular voice: matrix rebuild, `mod_amp` positive-only floor, MLP bypass when untrained, worklet blob-url registration — modular mode is under active churn, so expect rough edges.
|
||||
- `a-app.js` is the single source of truth. **Do not** reflexively mirror changes to `b-app.js` / `c-app.js` — they are frozen legacy variants.
|
||||
- `window.__nisps` only exists with `?debug=1`; Playwright helpers expect this.
|
||||
|
||||
## Open questions / smells
|
||||
- `modular-ui.js` is ~52k and undocumented in `CLAUDE.md`; needs a `docs/modular.md` stub, especially given recent bug cluster.
|
||||
- `engine-switcher.js` + `engine-switching.spec.js` + `modular-mode.spec.js` — newer engine-selection mechanism not described in `CLAUDE.md`. Verify whether there is now a supported alternative engine besides WASM-MLP.
|
||||
- `playground/RECONCILIATION.md` is referenced by persistent memory but missing on disk. Either the memory is stale or the file needs creating.
|
||||
- `NISPS_CORE_TASKS.md` / `NISPS_CORE_EXTRACTION_PLAN.md` at the repo root likely describe completed work — candidates for deletion or archiving under `docs/history/`.
|
||||
- `PLAN-solidjs-migration.md` (34k) describes an unstarted rewrite. Either flag it "aspirational / not started" at the top or move to `docs/`.
|
||||
- No `README.md` for `playground/wasm/` — a 10-line binding table (C API ↔ JS wrapper ↔ nisps-core call) would save future agents a trip through `nisps_bindings.cpp`.
|
||||
- Firmware `IMLInterface`'s STORE_VALUE vs STORE_POSITION modes have no docs — decide which modes use which and document.
|
||||
- Global `std::shared_ptr<MIDIInOut>` in the sketch introduces refcount traffic on the 1 ms MIDI poll — likely benign, worth confirming.
|
||||
- Two duplicated CLAUDE.md copies at `~/.claude/CLAUDE.md` and `~/.claude-gp/CLAUDE.md` (symlinked), and a per-repo one — not a repo smell, just noted so future agents don't try to "reconcile".
|
||||
- `src/memllib` submodule is not auto-checked-out.
|
||||
- Firmware sketch path is `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` (Arduino-CLI requires sketch dir name == sketch file name); `firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}` are symlinks because Arduino's preprocessor refuses `..` in includes from sketch headers.
|
||||
- `firmware/MEMLNaut-NISPS/glue/mode_select.hpp` `#undef`s Arduino macros (`sq`, `min`, `max`, `abs`, `round`) before pulling nisps headers — engines use those identifiers as method names.
|
||||
- `nisps_firmware::g_active_mode_bridge` is `extern` in `glue/audio_driver.hpp` and defined in the `.ino`; combining `inline` with `__not_in_flash` produces a comdat conflict at link time.
|
||||
- The host fallback of `NISPS_AUDIO_FUNC` in `nisps/core/perf.hpp` is misshapen for use as a function-name decorator (firmware path expands to `__not_in_flash_func` which takes only a name); firmware glue avoids the macro to dodge the inconsistency. See `ALIGNMENT.md`.
|
||||
- `nisps_modes_tests` builds against generated schemas under `nisps/modes/generated/`; if you add a new mode, regenerate via `bun run codegen/generate.ts` before building.
|
||||
|
||||
## Smells / strategic concerns
|
||||
|
||||
See `ALIGNMENT.md`.
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
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
|
||||
- Initial extraction from MEMLNaut-NISPS firmware
|
||||
- Header-only C++20 library structure
|
||||
- Core IML (Interactive Machine Learning) interface
|
||||
- MLP (Multi-Layer Perceptron) neural network implementation
|
||||
- Dataset management with replay memory support
|
||||
- Training and inference modes
|
||||
- Logging callback support
|
||||
- CMake build system for tests
|
||||
|
||||
### Changed
|
||||
- Converted from Arduino/RP2040 embedded code to platform-agnostic C++
|
||||
- Updated to C++20 (required for std::span)
|
||||
- Converted to header-only implementation pattern
|
||||
- Added `nisps` namespace to all code
|
||||
|
||||
### Removed
|
||||
- Arduino and RP2040 dependencies
|
||||
- Serial debugging (replaced with optional callbacks)
|
||||
- SD card save/load functionality
|
||||
- Audio synthesis code (nisps-core is control-only)
|
||||
|
||||
### Migration from MEMLNaut-NISPS
|
||||
If you're using the old embedded IMLInterface class:
|
||||
```cpp
|
||||
// Old (embedded):
|
||||
IMLInterface iml(n_inputs, n_outputs);
|
||||
|
||||
// New (nisps-core):
|
||||
nisps::IML<float> iml(n_inputs, n_outputs);
|
||||
```
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(nisps-core VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Header-only library
|
||||
add_library(nisps INTERFACE)
|
||||
target_include_directories(nisps INTERFACE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
# Tests
|
||||
option(NISPS_BUILD_TESTS "Build tests" ON)
|
||||
if(NISPS_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
# NISPS Core
|
||||
|
||||
**N**eural **I**nteractive **S**haping of **P**arameter **S**paces - Core Library
|
||||
|
||||
A platform-agnostic C++20 header-only library for interactive machine learning. Train neural networks to map input parameters to output parameters through interactive demonstration.
|
||||
|
||||
## What Is This?
|
||||
|
||||
NISPS core is a **parameter mapping engine**, not a synthesizer. It takes N input parameters (joystick position, sensor data, audio features) and maps them to M output parameters through an interactively-trained neural network.
|
||||
|
||||
**Use it to control**: synthesizers, effects, lights, robots, game parameters, or anything that responds to control data.
|
||||
|
||||
## Features
|
||||
|
||||
- **Header-only**: No compilation needed, just include and use
|
||||
- **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
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
Copy the `include/nisps/` directory to your project, or add it to your include path:
|
||||
|
||||
```bash
|
||||
# Option 1: Copy headers
|
||||
cp -r nisps-core/include/nisps /path/to/your/project/include/
|
||||
|
||||
# Option 2: Add to CMakeLists.txt
|
||||
target_include_directories(your_target PRIVATE /path/to/nisps-core/include)
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```cpp
|
||||
#include <nisps/nisps.hpp>
|
||||
|
||||
// Create IML with 2 inputs, 4 outputs
|
||||
nisps::IML<float> iml(2, 4);
|
||||
|
||||
// Runtime: set inputs and get outputs
|
||||
void update(float x, float y) {
|
||||
iml.set_input(0, x);
|
||||
iml.set_input(1, y);
|
||||
iml.process();
|
||||
|
||||
const float* outputs = iml.get_outputs();
|
||||
my_synth.set_filter_cutoff(outputs[0] * 10000.f);
|
||||
my_synth.set_resonance(outputs[1]);
|
||||
my_synth.set_envelope_attack(outputs[2] * 5.0f);
|
||||
my_synth.set_envelope_release(outputs[3] * 10.0f);
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic Training
|
||||
|
||||
```cpp
|
||||
// 1. Enter training mode
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// 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
|
||||
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### IML Constructor
|
||||
|
||||
```cpp
|
||||
nisps::IML<Float>(
|
||||
size_t n_inputs, // Number of input parameters
|
||||
size_t n_outputs, // Number of output parameters
|
||||
std::vector<size_t> hidden_layers = {10, 10, 14}, // Hidden layer sizes
|
||||
size_t max_iterations = 1000, // Training iterations
|
||||
Float learning_rate = 1.0f, // Learning rate
|
||||
Float convergence_threshold = 0.00001f // Stop training threshold
|
||||
);
|
||||
```
|
||||
|
||||
### Input/Output
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Training
|
||||
|
||||
```cpp
|
||||
enum class Mode { Inference, Training };
|
||||
void set_mode(Mode mode); // Switch modes
|
||||
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
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```cpp
|
||||
void set_logger(LogFn fn); // Set callback for messages
|
||||
// LogFn = void(*)(const char*)
|
||||
```
|
||||
|
||||
## Building the Tests
|
||||
|
||||
```bash
|
||||
cd nisps-core
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
ctest --output-on-failure
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **C++20** compiler (GCC 10+, Clang 10+, MSVC 2019+)
|
||||
- **CMake 3.14+** (for building tests only)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **IML**: High-level interactive ML interface
|
||||
- **MLP**: Multi-layer perceptron (feedforward neural network)
|
||||
- **Dataset**: Training data management
|
||||
- **Layer/Node**: Neural network building blocks
|
||||
- **Loss**: MSE and categorical cross-entropy functions
|
||||
- **Utils**: Activation functions (sigmoid, ReLU, tanh, etc.)
|
||||
|
||||
### Design Decisions
|
||||
|
||||
1. **Header-only**: Simplifies integration, allows template specialization
|
||||
2. **C++20**: Enables `std::span` for efficient array views
|
||||
3. **No SIMD**: Portable code, relies on compiler auto-vectorization
|
||||
4. **RMSProp optimizer**: Fast convergence for interactive training
|
||||
5. **Gradient clipping**: Prevents numerical instability
|
||||
|
||||
## Examples
|
||||
|
||||
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
|
||||
|
||||
Extracted from [MEMLNaut-NISPS](https://github.com/musicallyembodiedml/memlnaut) - an embedded ML platform for audio synthesis on Raspberry Pi Pico.
|
||||
|
||||
**Key changes from MEMLNaut-NISPS**:
|
||||
- Removed Arduino/RP2040 dependencies
|
||||
- Removed audio synthesis code (use this to *control* your synth)
|
||||
- Added proper namespacing
|
||||
- Converted to header-only library
|
||||
- Updated to modern C++20
|
||||
|
||||
## License
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
|
||||
Original MLP code derived from [David Alberto Nogueira's MLP project](https://github.com/davidalbertonogueira/MLP).
|
||||
|
||||
## Contributing
|
||||
|
||||
This library is extracted from an active research project. Contributions welcome:
|
||||
- Bug fixes
|
||||
- Performance optimizations
|
||||
- Example code
|
||||
- Documentation improvements
|
||||
|
||||
Please keep the library dependency-free and platform-agnostic.
|
||||
|
||||
## Citation
|
||||
|
||||
If you use this in research, please cite:
|
||||
|
||||
```
|
||||
MEMLNaut-NISPS: Neural Interactive Shaping of Parameter Spaces
|
||||
https://musicallyembodiedml.github.io/memlnaut/approaches/nisps
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: File at parent project (MEMLNaut-NISPS repo)
|
||||
- **Docs**: https://musicallyembodiedml.github.io/memlnaut/
|
||||
- **Examples**: See `examples/` directory
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
/**
|
||||
* @file simple_mapping.cpp
|
||||
* @brief Example of using NISPS Core for parameter mapping
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include <nisps/nisps.hpp>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
|
||||
void demo_inference() {
|
||||
std::cout << "=== Demo 1: Untrained Inference ===\n\n";
|
||||
|
||||
// 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 " << iml.num_inputs() << " inputs, "
|
||||
<< iml.num_outputs() << " outputs\n\n";
|
||||
|
||||
// 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"},
|
||||
};
|
||||
|
||||
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* out = iml.get_outputs();
|
||||
std::cout << " " << p.label << " (" << p.x << ", " << p.y << ") -> ["
|
||||
<< out[0] << ", " << out[1] << ", " << out[2] << ", " << out[3] << "]\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
void demo_training() {
|
||||
std::cout << "=== Demo 2: Training a Mapping ===\n\n";
|
||||
|
||||
// 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 << " [nisps] " << msg << "\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";
|
||||
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// 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};
|
||||
|
||||
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 << "Added 5 training examples.\n";
|
||||
|
||||
// Switching to inference triggers training
|
||||
std::cout << "Training...\n";
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
// 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 << "\nNISPS Core - Parameter Mapping Examples\n";
|
||||
std::cout << std::string(45, '=') << "\n\n";
|
||||
|
||||
demo_inference();
|
||||
demo_training();
|
||||
demo_interactive_workflow();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
/**
|
||||
* @file dataset.hpp
|
||||
* @brief Dataset management and replay memory functionality for NISPS
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#ifndef NISPS_DATASET_HPP
|
||||
#define NISPS_DATASET_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
/**
|
||||
* @brief Manages a dataset of feature-label pairs with optional replay memory functionality.
|
||||
*
|
||||
* This class provides dataset management capabilities including loading, storing, and sampling
|
||||
* feature-label pairs. It includes legacy replay memory functionality which is now deprecated
|
||||
* in favor of the ReplayMemory class.
|
||||
*/
|
||||
class Dataset {
|
||||
public:
|
||||
static constexpr size_t kMax_examples = 100;
|
||||
using DatasetVector = std::vector<std::vector<float>>;
|
||||
|
||||
/**
|
||||
* @brief Enumeration of forgetting modes for replay memory functionality.
|
||||
* @deprecated Use ReplayMemory::FORGETMODES instead.
|
||||
*/
|
||||
enum ForgetMode {
|
||||
FIFO, /**< First-In-First-Out: Removes the oldest item. */
|
||||
RANDOM_EQUAL, /**< Random Equal: Removes a random item with equal probability. */
|
||||
RANDOM_OLDER /**< Random Older: Removes an older item with higher probability. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Default constructor that initializes an empty dataset.
|
||||
*/
|
||||
Dataset();
|
||||
|
||||
/**
|
||||
* @brief Constructs a dataset with initial feature and label vectors.
|
||||
*
|
||||
* @param features Vector of feature vectors to initialize with
|
||||
* @param labels Vector of label vectors to initialize with
|
||||
*/
|
||||
Dataset(DatasetVector &features, DatasetVector &labels);
|
||||
|
||||
/**
|
||||
* @brief Adds a new feature-label pair to the dataset.
|
||||
*
|
||||
* @param feature Vector containing input features
|
||||
* @param label Vector containing output labels
|
||||
* @return true if addition was successful, false otherwise
|
||||
*/
|
||||
bool Add(const std::vector<float> &feature, const std::vector<float> &label);
|
||||
|
||||
/**
|
||||
* @brief Clears all data from the dataset.
|
||||
*/
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* @brief Loads feature and label vectors into the dataset.
|
||||
*
|
||||
* @param features Vector of feature vectors to load
|
||||
* @param labels Vector of label vectors to load
|
||||
*/
|
||||
void Load(DatasetVector &features, DatasetVector &labels);
|
||||
|
||||
/**
|
||||
* @brief Provides direct access to internal feature and label vectors.
|
||||
*
|
||||
* @param features Pointer to feature vectors will be stored here
|
||||
* @param labels Pointer to label vectors will be stored here
|
||||
*/
|
||||
void Fetch(DatasetVector *&features, DatasetVector *&labels);
|
||||
|
||||
/**
|
||||
* @brief Returns a copy of the feature vectors, optionally with bias terms.
|
||||
*
|
||||
* @param with_bias If true, adds a bias term (1.0f) to each feature vector
|
||||
* @return DatasetVector Copy of feature vectors with optional bias terms
|
||||
*/
|
||||
DatasetVector GetFeatures(bool with_bias = true);
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the label vectors.
|
||||
*
|
||||
* @return DatasetVector& Reference to the label vectors
|
||||
*/
|
||||
DatasetVector &GetLabels();
|
||||
|
||||
/**
|
||||
* @brief Returns the size of feature vectors, accounting for optional bias term.
|
||||
*
|
||||
* @param with_bias If true, includes the bias term in the size
|
||||
* @return size_t Size of feature vectors
|
||||
*/
|
||||
inline size_t GetFeatureSize(bool with_bias = true) { return data_size_ + with_bias; }
|
||||
|
||||
/**
|
||||
* @brief Returns the size of label vectors.
|
||||
*
|
||||
* @return size_t Size of label vectors
|
||||
*/
|
||||
inline size_t GetOutputSize() { return output_size_; }
|
||||
|
||||
/**
|
||||
* @brief Enables or disables replay memory functionality.
|
||||
* @deprecated Use ReplayMemory class instead.
|
||||
*
|
||||
* @param enabled True to enable replay memory, false to disable
|
||||
*/
|
||||
void ReplayMemory(bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Sets the forgetting mode for replay memory.
|
||||
* @deprecated Use ReplayMemory::FORGETMODES instead.
|
||||
*
|
||||
* @param mode The forgetting mode to use
|
||||
*/
|
||||
void SetForgetMode(ForgetMode mode);
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum number of examples in the dataset.
|
||||
*
|
||||
* @param max Maximum number of examples to store
|
||||
*/
|
||||
void SetMaxExamples(size_t max);
|
||||
|
||||
/**
|
||||
* @brief Samples from the dataset, optionally with bias terms.
|
||||
* @deprecated Use ReplayMemory::sample() instead for replay memory functionality.
|
||||
*
|
||||
* @param with_bias If true, adds bias terms to feature vectors
|
||||
* @return std::pair<DatasetVector, DatasetVector> Pair of feature and label vectors
|
||||
*/
|
||||
std::pair<DatasetVector, DatasetVector> Sample(bool with_bias = true);
|
||||
|
||||
protected:
|
||||
size_t data_size_;
|
||||
size_t output_size_;
|
||||
|
||||
inline void _InitSizes() { data_size_ = 0; output_size_ = 0; }
|
||||
void _AdjustSizes();
|
||||
|
||||
DatasetVector features_;
|
||||
DatasetVector labels_;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Utility static method that returns a copy of the given feature vectors,
|
||||
* adding a bias term (1.0f) to each vector if with_bias is true.
|
||||
*
|
||||
* @param features Feature vectors to copy
|
||||
* @param with_bias If true, adds a bias term to each vector
|
||||
* @return DatasetVector Copy of feature vectors with optional bias terms
|
||||
*/
|
||||
static DatasetVector AddBias(const DatasetVector &features, bool with_bias);
|
||||
|
||||
/**
|
||||
* @brief Removes one excess example based on the current forget mode.
|
||||
*/
|
||||
void RemoveOneExcessExample();
|
||||
|
||||
// Replay memory functionality flag.
|
||||
bool replay_memory_enabled_ = false;
|
||||
std::mt19937 rng_;
|
||||
|
||||
// Additional members for extended replay memory functionality:
|
||||
// Timestamps for each example (used in RANDOM_OLDER mode).
|
||||
std::vector<size_t> timestamps_;
|
||||
size_t current_timestamp_ = 0;
|
||||
// Current forgetting mode.
|
||||
ForgetMode forget_mode_ = FIFO;
|
||||
|
||||
// Maximum number of examples allowed.
|
||||
size_t max_examples_;
|
||||
};
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#include "dataset_impl.hpp"
|
||||
|
||||
#endif // NISPS_DATASET_HPP
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
/**
|
||||
* @file dataset_impl.hpp
|
||||
* @brief Implementation of Dataset class methods
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#ifndef NISPS_DATASET_IMPL_HPP
|
||||
#define NISPS_DATASET_IMPL_HPP
|
||||
|
||||
#include <cassert>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
inline Dataset::Dataset() : rng_(std::random_device{}()) {
|
||||
_InitSizes();
|
||||
max_examples_ = kMax_examples; // Default maximum.
|
||||
}
|
||||
|
||||
inline Dataset::Dataset(DatasetVector &features, DatasetVector &labels)
|
||||
: features_(features), labels_(labels), rng_(std::random_device{}()) {
|
||||
_InitSizes();
|
||||
_AdjustSizes();
|
||||
max_examples_ = kMax_examples; // Default maximum.
|
||||
// Initialize timestamps for each loaded example.
|
||||
timestamps_.resize(features_.size());
|
||||
for (size_t i = 0; i < timestamps_.size(); i++) {
|
||||
timestamps_[i] = i;
|
||||
}
|
||||
current_timestamp_ = timestamps_.size();
|
||||
}
|
||||
|
||||
inline bool Dataset::Add(const std::vector<float> &feature, const std::vector<float> &label)
|
||||
{
|
||||
// Enforce consistent dimensions if at least one example exists.
|
||||
if (data_size_ > 0) {
|
||||
if ((feature.size() != data_size_) ||
|
||||
(label.size() != output_size_)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// When capacity is reached:
|
||||
if (features_.size() >= max_examples_) {
|
||||
if (replay_memory_enabled_) {
|
||||
RemoveOneExcessExample();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Add new example.
|
||||
features_.push_back(feature);
|
||||
labels_.push_back(label);
|
||||
timestamps_.push_back(current_timestamp_);
|
||||
current_timestamp_++;
|
||||
|
||||
_AdjustSizes();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void Dataset::RemoveOneExcessExample() {
|
||||
// Remove one example according to the current forget mode.
|
||||
size_t index_to_remove = 0;
|
||||
switch (forget_mode_) {
|
||||
case FIFO:
|
||||
index_to_remove = 0;
|
||||
break;
|
||||
case RANDOM_EQUAL:
|
||||
{
|
||||
std::uniform_int_distribution<size_t> dist(0, features_.size() - 1);
|
||||
index_to_remove = dist(rng_);
|
||||
break;
|
||||
}
|
||||
case RANDOM_OLDER:
|
||||
{
|
||||
size_t total_weight = 0;
|
||||
std::vector<size_t> weights;
|
||||
weights.reserve(timestamps_.size());
|
||||
for (size_t t : timestamps_) {
|
||||
size_t age = current_timestamp_ - t;
|
||||
weights.push_back(age);
|
||||
total_weight += age;
|
||||
}
|
||||
if (total_weight == 0) {
|
||||
std::uniform_int_distribution<size_t> dist(0, features_.size() - 1);
|
||||
index_to_remove = dist(rng_);
|
||||
} else {
|
||||
std::uniform_int_distribution<size_t> dist(0, total_weight - 1);
|
||||
size_t r = dist(rng_);
|
||||
size_t cumulative = 0;
|
||||
for (size_t i = 0; i < weights.size(); i++) {
|
||||
cumulative += weights[i];
|
||||
if (r < cumulative) {
|
||||
index_to_remove = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
index_to_remove = 0;
|
||||
break;
|
||||
}
|
||||
// Remove the selected example from all parallel vectors.
|
||||
features_.erase(features_.begin() + index_to_remove);
|
||||
labels_.erase(labels_.begin() + index_to_remove);
|
||||
timestamps_.erase(timestamps_.begin() + index_to_remove);
|
||||
}
|
||||
|
||||
inline void Dataset::Clear()
|
||||
{
|
||||
features_.clear();
|
||||
labels_.clear();
|
||||
timestamps_.clear();
|
||||
current_timestamp_ = 0;
|
||||
_InitSizes();
|
||||
}
|
||||
|
||||
inline void Dataset::Load(DatasetVector &features, DatasetVector &labels)
|
||||
{
|
||||
features_ = features;
|
||||
labels_ = labels;
|
||||
_AdjustSizes();
|
||||
// Reinitialize timestamps for loaded examples.
|
||||
timestamps_.resize(features_.size());
|
||||
for (size_t i = 0; i < timestamps_.size(); i++) {
|
||||
timestamps_[i] = i;
|
||||
}
|
||||
current_timestamp_ = timestamps_.size();
|
||||
}
|
||||
|
||||
inline void Dataset::Fetch(DatasetVector *&features, DatasetVector *&labels)
|
||||
{
|
||||
features = &features_;
|
||||
labels = &labels_;
|
||||
}
|
||||
|
||||
inline Dataset::DatasetVector Dataset::AddBias(const DatasetVector &features, bool with_bias)
|
||||
{
|
||||
DatasetVector result = features; // make a copy
|
||||
if (with_bias) {
|
||||
for (auto &f : result) {
|
||||
f.push_back(1.f);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline Dataset::DatasetVector Dataset::GetFeatures(bool with_bias)
|
||||
{
|
||||
return AddBias(features_, with_bias);
|
||||
}
|
||||
|
||||
inline Dataset::DatasetVector &Dataset::GetLabels()
|
||||
{
|
||||
return labels_;
|
||||
}
|
||||
|
||||
inline void Dataset::_AdjustSizes()
|
||||
{
|
||||
if (!features_.empty()) {
|
||||
data_size_ = features_[0].size();
|
||||
output_size_ = labels_[0].size();
|
||||
}
|
||||
}
|
||||
|
||||
inline void Dataset::ReplayMemory(bool enabled)
|
||||
{
|
||||
replay_memory_enabled_ = enabled;
|
||||
(void)replay_memory_enabled_;
|
||||
}
|
||||
|
||||
inline void Dataset::SetForgetMode(ForgetMode mode)
|
||||
{
|
||||
forget_mode_ = mode;
|
||||
}
|
||||
|
||||
inline void Dataset::SetMaxExamples(size_t max)
|
||||
{
|
||||
max_examples_ = max;
|
||||
// If the current dataset size exceeds the new maximum, remove extra examples.
|
||||
while (features_.size() > max_examples_) {
|
||||
if (replay_memory_enabled_) {
|
||||
RemoveOneExcessExample();
|
||||
} else {
|
||||
// When replay memory is disabled, trim the extra examples from the end.
|
||||
features_.resize(max_examples_);
|
||||
labels_.resize(max_examples_);
|
||||
timestamps_.resize(max_examples_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::pair<Dataset::DatasetVector, Dataset::DatasetVector> Dataset::Sample(bool with_bias)
|
||||
{
|
||||
std::pair<DatasetVector, DatasetVector> samplePair;
|
||||
size_t currentSize = features_.size();
|
||||
if (currentSize == 0) {
|
||||
return samplePair;
|
||||
}
|
||||
|
||||
if (replay_memory_enabled_) {
|
||||
// Create a list of indices and shuffle them.
|
||||
std::vector<size_t> indices(currentSize);
|
||||
for (size_t i = 0; i < currentSize; ++i) {
|
||||
indices[i] = i;
|
||||
}
|
||||
std::shuffle(indices.begin(), indices.end(), rng_);
|
||||
|
||||
DatasetVector sampledFeatures;
|
||||
DatasetVector sampledLabels;
|
||||
sampledFeatures.reserve(currentSize);
|
||||
sampledLabels.reserve(currentSize);
|
||||
|
||||
for (size_t idx : indices) {
|
||||
sampledFeatures.push_back(features_[idx]);
|
||||
sampledLabels.push_back(labels_[idx]);
|
||||
}
|
||||
// Add bias if requested.
|
||||
samplePair.first = AddBias(sampledFeatures, with_bias);
|
||||
samplePair.second = sampledLabels;
|
||||
} else {
|
||||
// Replay memory disabled: return the entire dataset.
|
||||
samplePair.first = GetFeatures(with_bias);
|
||||
samplePair.second = labels_;
|
||||
}
|
||||
return samplePair;
|
||||
}
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif // NISPS_DATASET_IMPL_HPP
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
#ifndef NISPS_IML_HPP
|
||||
#define NISPS_IML_HPP
|
||||
|
||||
#include "mlp.hpp"
|
||||
#include "dataset.hpp"
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
template<typename Float = float>
|
||||
class IML {
|
||||
public:
|
||||
enum class Mode { Inference, Training };
|
||||
|
||||
using LogFn = void(*)(const char*);
|
||||
|
||||
IML(size_t n_inputs, size_t n_outputs,
|
||||
std::vector<size_t> hidden_layers = {10, 10, 14},
|
||||
size_t max_iterations = 1000,
|
||||
Float learning_rate = 1.0f,
|
||||
Float convergence_threshold = 0.00001f);
|
||||
|
||||
// Input
|
||||
void set_input(size_t index, Float value);
|
||||
void set_inputs(const Float* values, size_t count);
|
||||
|
||||
// Output (valid after process())
|
||||
const Float* get_outputs() const;
|
||||
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();
|
||||
|
||||
// Training workflow
|
||||
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();
|
||||
|
||||
// Spread-aware weight randomization
|
||||
// spread: 0 = uniform [-1,1], 1 = Xavier-scaled per layer
|
||||
void randomise_weights(Float spread);
|
||||
|
||||
// Spread-aware weight perturbation (for RL exploration)
|
||||
// speed: noise magnitude, spread: 0 = flat noise, 1 = Xavier-scaled + weight decay
|
||||
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
|
||||
void set_logger(LogFn fn) { log_fn_ = fn; }
|
||||
|
||||
private:
|
||||
void log(const char* msg) const {
|
||||
if (log_fn_) log_fn_(msg);
|
||||
}
|
||||
void train();
|
||||
|
||||
size_t n_inputs_;
|
||||
size_t n_outputs_;
|
||||
size_t max_iterations_;
|
||||
Float learning_rate_;
|
||||
Float convergence_threshold_;
|
||||
|
||||
Mode mode_ = Mode::Inference;
|
||||
bool input_updated_ = false;
|
||||
bool perform_inference_ = true;
|
||||
|
||||
std::vector<Float> input_state_;
|
||||
std::vector<Float> output_state_;
|
||||
|
||||
std::unique_ptr<Dataset> dataset_;
|
||||
std::unique_ptr<MLP<Float>> mlp_;
|
||||
typename MLP<Float>::mlp_weights stored_weights_;
|
||||
bool weights_randomised_ = false;
|
||||
|
||||
LogFn log_fn_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#include "iml_impl.hpp"
|
||||
|
||||
#endif // NISPS_IML_HPP
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
#ifndef NISPS_IML_IMPL_HPP
|
||||
#define NISPS_IML_IMPL_HPP
|
||||
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
template<typename Float>
|
||||
IML<Float>::IML(size_t n_inputs, size_t n_outputs,
|
||||
std::vector<size_t> hidden_layers,
|
||||
size_t max_iterations,
|
||||
Float learning_rate,
|
||||
Float convergence_threshold)
|
||||
: n_inputs_(n_inputs)
|
||||
, n_outputs_(n_outputs)
|
||||
, max_iterations_(max_iterations)
|
||||
, learning_rate_(learning_rate)
|
||||
, convergence_threshold_(convergence_threshold)
|
||||
{
|
||||
// Build layer sizes: input + hidden + output
|
||||
const size_t kBias = 1;
|
||||
std::vector<size_t> layer_sizes;
|
||||
layer_sizes.push_back(n_inputs + kBias);
|
||||
for (size_t h : hidden_layers) {
|
||||
layer_sizes.push_back(h);
|
||||
}
|
||||
layer_sizes.push_back(n_outputs);
|
||||
|
||||
// Activation functions: RELU for hidden, SIGMOID for output
|
||||
std::vector<ACTIVATION_FUNCTIONS> activations;
|
||||
for (size_t i = 0; i < hidden_layers.size(); ++i) {
|
||||
activations.push_back(RELU);
|
||||
}
|
||||
activations.push_back(SIGMOID);
|
||||
|
||||
dataset_ = std::make_unique<Dataset>();
|
||||
mlp_ = std::make_unique<MLP<Float>>(
|
||||
layer_sizes,
|
||||
activations,
|
||||
loss::LOSS_MSE,
|
||||
false, // use_constant_weight_init
|
||||
0.0f // constant_weight_init
|
||||
);
|
||||
|
||||
input_state_.resize(n_inputs, static_cast<Float>(0.5));
|
||||
output_state_.resize(n_outputs, static_cast<Float>(0));
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::set_input(size_t index, Float value) {
|
||||
if (index >= n_inputs_) return;
|
||||
if (value < 0) value = 0;
|
||||
if (value > 1) value = 1;
|
||||
input_state_[index] = value;
|
||||
input_updated_ = true;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::set_inputs(const Float* values, size_t count) {
|
||||
for (size_t i = 0; i < count && i < n_inputs_; ++i) {
|
||||
set_input(i, values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
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;
|
||||
|
||||
// Add bias term
|
||||
std::vector<Float> input_with_bias = input_state_;
|
||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
||||
|
||||
// Run inference
|
||||
std::vector<Float> output(n_outputs_);
|
||||
mlp_->GetOutput(input_with_bias, &output);
|
||||
|
||||
output_state_ = output;
|
||||
input_updated_ = false;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::set_mode(Mode mode) {
|
||||
if (mode == Mode::Inference && mode_ == Mode::Training) {
|
||||
train();
|
||||
}
|
||||
mode_ = mode;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::save_example() {
|
||||
// First call: stop inference, user will position output
|
||||
if (perform_inference_) {
|
||||
perform_inference_ = false;
|
||||
log("Move to desired output position...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Second call: store the example
|
||||
dataset_->Add(input_state_, output_state_);
|
||||
perform_inference_ = true;
|
||||
|
||||
// Run inference with new example
|
||||
std::vector<Float> input_with_bias = input_state_;
|
||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
||||
std::vector<Float> output(n_outputs_);
|
||||
mlp_->GetOutput(input_with_bias, &output);
|
||||
output_state_ = output;
|
||||
|
||||
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) {
|
||||
dataset_->Clear();
|
||||
log("Dataset cleared.");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::randomise_weights() {
|
||||
if (mode_ == Mode::Training) {
|
||||
stored_weights_ = mlp_->GetWeights();
|
||||
mlp_->DrawWeights();
|
||||
weights_randomised_ = true;
|
||||
|
||||
// Run inference to show effect
|
||||
std::vector<Float> input_with_bias = input_state_;
|
||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
||||
std::vector<Float> output(n_outputs_);
|
||||
mlp_->GetOutput(input_with_bias, &output);
|
||||
output_state_ = output;
|
||||
|
||||
log("Weights randomised.");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::randomise_weights(Float spread) {
|
||||
if (mode_ == Mode::Training) {
|
||||
stored_weights_ = mlp_->GetWeights();
|
||||
mlp_->DrawWeightsSpread(spread);
|
||||
weights_randomised_ = true;
|
||||
|
||||
// Run inference to show effect
|
||||
std::vector<Float> input_with_bias = input_state_;
|
||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
||||
std::vector<Float> output(n_outputs_);
|
||||
mlp_->GetOutput(input_with_bias, &output);
|
||||
output_state_ = output;
|
||||
|
||||
log("Weights randomised (spread).");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::move_weights(Float speed, Float spread) {
|
||||
mlp_->MoveWeightsSpread(speed, spread);
|
||||
|
||||
// Run inference to show effect of perturbation
|
||||
input_updated_ = true;
|
||||
process();
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::train() {
|
||||
// Restore weights if they were randomised
|
||||
if (weights_randomised_) {
|
||||
mlp_->SetWeights(stored_weights_);
|
||||
weights_randomised_ = false;
|
||||
}
|
||||
|
||||
auto features = dataset_->GetFeatures(true); // with bias
|
||||
auto& labels = dataset_->GetLabels();
|
||||
|
||||
if (features.empty() || labels.empty()) {
|
||||
log("Empty dataset, skipping training.");
|
||||
return;
|
||||
}
|
||||
|
||||
typename MLP<Float>::training_pair_t training_data(features, labels);
|
||||
|
||||
log("Training...");
|
||||
Float loss = mlp_->Train(
|
||||
training_data,
|
||||
learning_rate_,
|
||||
static_cast<int>(max_iterations_),
|
||||
convergence_threshold_,
|
||||
false // output_log
|
||||
);
|
||||
|
||||
// Run inference after training
|
||||
std::vector<Float> input_with_bias = input_state_;
|
||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
||||
std::vector<Float> output(n_outputs_);
|
||||
mlp_->GetOutput(input_with_bias, &output);
|
||||
output_state_ = output;
|
||||
|
||||
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
|
||||
|
||||
#endif // NISPS_IML_IMPL_HPP
|
||||
|
|
@ -1,536 +0,0 @@
|
|||
/**
|
||||
* @file layer.hpp
|
||||
* @brief Neural network layer implementation managing multiple nodes and their connections
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
*/
|
||||
|
||||
#ifndef NISPS_LAYER_HPP
|
||||
#define NISPS_LAYER_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cassert> // for assert()
|
||||
#include "node.hpp"
|
||||
#include "utils.hpp"
|
||||
#include <span>
|
||||
#include <random>
|
||||
#include <span>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
/**
|
||||
* @brief Definition of activation function pointer
|
||||
* @tparam T The numeric type used for calculations
|
||||
*/
|
||||
template<typename T>
|
||||
using activation_func_t = T(*)(T);
|
||||
|
||||
/**
|
||||
* @class Layer
|
||||
* @brief Represents a layer of neural network nodes with shared inputs and activation function
|
||||
* @tparam T The numeric type used for calculations (typically float or double)
|
||||
*/
|
||||
template<typename T>
|
||||
class Layer {
|
||||
public:
|
||||
/**
|
||||
* @brief Default constructor
|
||||
*/
|
||||
Layer() {
|
||||
m_num_nodes = 0;
|
||||
m_nodes.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Constructor with initialization parameters
|
||||
* @param num_inputs_per_node Number of inputs for each node in the layer
|
||||
* @param num_nodes Number of nodes in this layer
|
||||
* @param activation_function Activation function type for all nodes
|
||||
* @param use_constant_weight_init Flag to use constant weight initialization
|
||||
* @param constant_weight_init Value for constant weight initialization
|
||||
*/
|
||||
Layer(int num_inputs_per_node,
|
||||
int num_nodes,
|
||||
const ACTIVATION_FUNCTIONS & activation_function,
|
||||
bool use_constant_weight_init = true,
|
||||
T constant_weight_init = 0.5) {
|
||||
m_num_inputs_per_node = num_inputs_per_node;
|
||||
m_num_nodes = num_nodes;
|
||||
m_nodes.resize(num_nodes);
|
||||
|
||||
|
||||
std::pair<activation_func_t<T>,
|
||||
activation_func_t<T> > *pair;
|
||||
bool ret_val = utils::ActivationFunctionsManager<T>::Singleton().
|
||||
GetActivationFunctionPair(activation_function,
|
||||
&pair);
|
||||
assert(ret_val);
|
||||
m_activation_function = (*pair).first;
|
||||
m_deriv_activation_function = (*pair).second;
|
||||
m_activation_function_type = activation_function;
|
||||
for (int i = 0; i < num_nodes; i++) {
|
||||
m_nodes[i].WeightInitialization(num_inputs_per_node,
|
||||
use_constant_weight_init,
|
||||
constant_weight_init);
|
||||
}
|
||||
|
||||
// InitXavier();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~Layer() {
|
||||
m_num_inputs_per_node = 0;
|
||||
m_num_nodes = 0;
|
||||
m_nodes.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Controls output caching behavior
|
||||
* @param onOrOff True to enable output caching, false to disable
|
||||
*/
|
||||
void SetCachedOutputs(bool onOrOff) {
|
||||
m_cacheOutputs = onOrOff;
|
||||
if (m_cacheOutputs) {
|
||||
//nothing yet
|
||||
}else{
|
||||
cachedOutputs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the number of inputs per node
|
||||
* @return Number of inputs per node
|
||||
*/
|
||||
int GetInputSize() const {
|
||||
return m_num_inputs_per_node;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Gets the number of nodes in the layer
|
||||
* @return Number of nodes in the layer
|
||||
*/
|
||||
int GetOutputSize() const {
|
||||
return m_num_nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Gets the list of nodes in the layer
|
||||
* @return Constant reference to the list of nodes
|
||||
*/
|
||||
std::span<const Node<T>> GetNodes() {
|
||||
return m_nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return the internal list of nodes, but modifiable
|
||||
* @return Reference to the list of nodes
|
||||
*/
|
||||
std::vector<Node<T>> & GetNodesChangeable() {
|
||||
return m_nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes layer outputs using the activation function
|
||||
* @param input Input vector
|
||||
* @param output Pointer to store output vector
|
||||
*/
|
||||
inline void GetOutputAfterActivationFunction(const std::vector<T> &input,
|
||||
std::vector<T> * output) {
|
||||
assert(input.size() == m_num_inputs_per_node);
|
||||
|
||||
// Reserve capacity if needed to avoid reallocation
|
||||
if (output->capacity() < m_num_nodes) {
|
||||
output->reserve(m_num_nodes);
|
||||
}
|
||||
output->resize(m_num_nodes);
|
||||
|
||||
for (size_t i = 0; i < m_num_nodes; ++i) {
|
||||
m_nodes[i].GetOutputAfterActivationFunction(input,
|
||||
m_activation_function,
|
||||
&((*output)[i]));
|
||||
// sleep_us(70);
|
||||
}
|
||||
if (m_cacheOutputs) {
|
||||
cachedOutputs = *output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize gradient accumulators for all nodes
|
||||
*/
|
||||
void InitializeGradientAccumulators() {
|
||||
for (auto& node : m_nodes) {
|
||||
node.InitializeGradientAccumulator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear gradient accumulators for all nodes
|
||||
*/
|
||||
void ClearGradientAccumulators() {
|
||||
for (auto& node : m_nodes) {
|
||||
node.ClearGradientAccumulator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Accumulate gradients without updating weights (for batch training)
|
||||
* @param input_layer_activation Activation values of the input layer
|
||||
* @param deriv_error Derivative of the error with respect to outputs
|
||||
* @param deltas Pointer to store computed deltas for previous layer
|
||||
*/
|
||||
void AccumulateGradients(const std::vector<T>& input_layer_activation,
|
||||
const std::vector<T>& deriv_error,
|
||||
std::vector<T>* deltas) {
|
||||
assert(input_layer_activation.size() == m_num_inputs_per_node);
|
||||
assert(deriv_error.size() == m_nodes.size());
|
||||
|
||||
deltas->resize(m_num_inputs_per_node, 0);
|
||||
|
||||
for (size_t i = 0; i < m_nodes.size(); i++) {
|
||||
T dE_doj = deriv_error[i];
|
||||
T doj_dnetj = m_deriv_activation_function(m_nodes[i].GetInnerProd());
|
||||
T error_signal = dE_doj * doj_dnetj;
|
||||
|
||||
// Accumulate gradients in the node
|
||||
m_nodes[i].AccumulateGradients(input_layer_activation, error_signal);
|
||||
|
||||
// Calculate deltas for previous layer
|
||||
for (size_t j = 0; j < m_num_inputs_per_node; j++) {
|
||||
(*deltas)[j] += error_signal * m_nodes[i].GetWeights()[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply accumulated gradients to all nodes
|
||||
* @param learning_rate Learning rate
|
||||
* @param batch_size Batch size for averaging
|
||||
*/
|
||||
void ApplyAccumulatedGradients(float learning_rate, T batch_size_inv) {
|
||||
for (auto& node : m_nodes) {
|
||||
node.ApplyAccumulatedGradients(learning_rate, batch_size_inv);
|
||||
}
|
||||
}
|
||||
|
||||
float GetGradSumSquared( float batch_size_inv ) {
|
||||
float sumsq = 0.0f;
|
||||
for (auto& node : m_nodes) {
|
||||
sumsq += node.GetGradSumSquared(batch_size_inv);
|
||||
}
|
||||
return sumsq;
|
||||
}
|
||||
|
||||
void ScaleAccumulatedGradients(T clip_coef) {
|
||||
for (auto& node : m_nodes) {
|
||||
node.ScaleAccumulatedGradients(clip_coef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset optimizer state for all nodes in this layer
|
||||
*/
|
||||
void ResetOptimizerState() {
|
||||
for (auto& node : m_nodes) {
|
||||
node.ResetOptimizerState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check and fix NaN/Inf in all node weights
|
||||
* @return true if any corruption was detected and fixed
|
||||
*/
|
||||
bool CheckAndFixWeights() {
|
||||
bool had_corruption = false;
|
||||
for (auto& node : m_nodes) {
|
||||
had_corruption |= node.CheckAndFixWeights();
|
||||
}
|
||||
return had_corruption;
|
||||
}
|
||||
|
||||
|
||||
// /**
|
||||
// * @brief Updates weights of the layer nodes
|
||||
// * @param input_layer_activation Activation values of the input layer
|
||||
// * @param deriv_error Derivative of the error with respect to outputs
|
||||
// * @param m_learning_rate Learning rate for weight updates
|
||||
// * @param deltas Pointer to store computed deltas
|
||||
// */
|
||||
// void UpdateWeights(const std::vector<T> &input_layer_activation,
|
||||
// const std::vector<T> &deriv_error,
|
||||
// float m_learning_rate,
|
||||
// std::vector<T> * deltas) {
|
||||
// assert(input_layer_activation.size() == m_num_inputs_per_node);
|
||||
// assert(deriv_error.size() == m_nodes.size());
|
||||
|
||||
// deltas->resize(m_num_inputs_per_node, 0);
|
||||
|
||||
// for (size_t i = 0; i < m_nodes.size(); i++) {
|
||||
|
||||
// //dE/dwij = dE/doj . doj/dnetj . dnetj/dwij
|
||||
// T dE_doj = 0;
|
||||
// T doj_dnetj = 0;
|
||||
// T dnetj_dwij = 0;
|
||||
|
||||
// dE_doj = deriv_error[i];
|
||||
// doj_dnetj = m_deriv_activation_function(m_nodes[i].inner_prod); //cached from earlier calculation
|
||||
|
||||
// for (size_t j = 0; j < m_num_inputs_per_node; j++) {
|
||||
// (*deltas)[j] += dE_doj * doj_dnetj * m_nodes[i].GetWeights()[j];
|
||||
|
||||
// dnetj_dwij = input_layer_activation[j];
|
||||
|
||||
// m_nodes[i].UpdateWeight(j,
|
||||
// static_cast<float>( -(dE_doj * doj_dnetj * dnetj_dwij) ),
|
||||
// m_learning_rate);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* @brief Update weights with optional gradient accumulation
|
||||
* @param input_layer_activation Activation values
|
||||
* @param deriv_error Error derivatives
|
||||
* @param learning_rate Learning rate
|
||||
* @param deltas Computed deltas
|
||||
* @param accumulate If true, accumulate gradients instead of immediate update
|
||||
*/
|
||||
void UpdateWeights(const std::vector<T>& input_layer_activation,
|
||||
const std::vector<T>& deriv_error,
|
||||
float learning_rate,
|
||||
std::vector<T>* deltas,
|
||||
bool accumulate = false) {
|
||||
|
||||
if (accumulate) {
|
||||
AccumulateGradients(input_layer_activation, deriv_error, deltas);
|
||||
} else {
|
||||
assert(input_layer_activation.size() == m_num_inputs_per_node);
|
||||
assert(deriv_error.size() == m_nodes.size());
|
||||
|
||||
deltas->resize(m_num_inputs_per_node, 0);
|
||||
|
||||
for (size_t i = 0; i < m_nodes.size(); i++) {
|
||||
T dE_doj = deriv_error[i];
|
||||
T doj_dnetj = m_deriv_activation_function(m_nodes[i].GetInnerProd());
|
||||
|
||||
for (size_t j = 0; j < m_num_inputs_per_node; j++) {
|
||||
(*deltas)[j] += dE_doj * doj_dnetj * m_nodes[i].GetWeights()[j];
|
||||
T dnetj_dwij = input_layer_activation[j];
|
||||
m_nodes[i].UpdateWeight(j,
|
||||
static_cast<float>(-(dE_doj * doj_dnetj * dnetj_dwij)),
|
||||
learning_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Calculates gradients for optimization
|
||||
* @param input_layer_activation Activation values of the input layer
|
||||
* @param deriv_error Derivative of the error with respect to outputs
|
||||
* @param deltas Pointer to store computed deltas
|
||||
*/
|
||||
void CalcGradients(const std::vector<T> &input_layer_activation,
|
||||
const std::vector<T> &deriv_error,
|
||||
std::vector<T> * deltas) {
|
||||
assert(input_layer_activation.size() == m_num_inputs_per_node);
|
||||
assert(deriv_error.size() == m_nodes.size());
|
||||
// grads = deriv_error; //keep a copy
|
||||
deltas->resize(m_num_inputs_per_node, 0);
|
||||
for (size_t i = 0; i < m_nodes.size(); i++) {
|
||||
//dE/dwij = dE/doj . doj/dnetj . dnetj/dwij
|
||||
T dE_doj=0,doj_dnetj =0;
|
||||
dE_doj = deriv_error[i];
|
||||
doj_dnetj = m_deriv_activation_function(m_nodes[i].GetInnerProd()); //cached from earlier calculation
|
||||
for (size_t j = 0; j < m_num_inputs_per_node; j++) {
|
||||
(*deltas)[j] += dE_doj * doj_dnetj * m_nodes[i].GetWeights()[j];
|
||||
}
|
||||
}
|
||||
grads = *deltas;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Sets gradients for optimization
|
||||
* @param newGrads New gradients to set
|
||||
*/
|
||||
void SetGrads(std::vector<T> newGrads) {
|
||||
grads = newGrads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the stored gradients
|
||||
* @return Reference to the stored gradients
|
||||
*/
|
||||
std::vector<T>& GetGrads() {
|
||||
return grads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets weights for the layer nodes
|
||||
* @param weights 2D vector of weights for each node
|
||||
*/
|
||||
void SetWeights( std::vector<std::vector<T>> & weights )
|
||||
{
|
||||
assert(0 <= weights.size() && weights.size() <= m_num_nodes /* Incorrect layer number in SetWeights call */);
|
||||
{
|
||||
// traverse the list of nodes
|
||||
size_t node_i = 0;
|
||||
for( Node<T> & node : m_nodes )
|
||||
{
|
||||
node.SetWeights( weights[node_i] );
|
||||
node_i++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Smoothly updates weights using another layer's weights
|
||||
* @param l Reference to another layer
|
||||
* @param alpha Smoothing factor
|
||||
* @param alphaInv Inverse of smoothing factor
|
||||
*/
|
||||
inline void SmoothUpdateWeights(Layer<T> &l, const float alpha, const float alphaInv) {
|
||||
// traverse the list of nodes
|
||||
for(size_t n=0; n < m_nodes.size(); n++) {
|
||||
m_nodes[n].SmoothUpdateWeights(l.m_nodes[n].GetWeights(), alpha, alphaInv);
|
||||
// sleep_us(70);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate weight norm for the layer
|
||||
* @return Weight norm
|
||||
*/
|
||||
T getWeightNorm() {
|
||||
float sum_sq = 0.0f;
|
||||
|
||||
// Sum weights
|
||||
for(size_t n = 0; n < m_nodes.size(); n++) {
|
||||
const std::vector<T>& weight_grads = m_nodes[n].GetWeights();
|
||||
for (const auto& grad : weight_grads) {
|
||||
sum_sq += grad * grad;
|
||||
}
|
||||
}
|
||||
|
||||
return std::sqrt(sum_sq);
|
||||
}
|
||||
|
||||
void InitXavier() {
|
||||
|
||||
float limit = (T)1.0;
|
||||
|
||||
switch(m_activation_function_type) {
|
||||
case ACTIVATION_FUNCTIONS::SIGMOID:
|
||||
case ACTIVATION_FUNCTIONS::TANH:
|
||||
limit = std::sqrt(6.0 / (m_num_inputs_per_node + m_num_nodes));
|
||||
break;
|
||||
case ACTIVATION_FUNCTIONS::RELU:
|
||||
limit = std::sqrt(6.0 / (m_num_inputs_per_node));
|
||||
break;
|
||||
case ACTIVATION_FUNCTIONS::LINEAR:
|
||||
limit = std::sqrt(6.0f / (m_num_inputs_per_node + m_num_nodes));
|
||||
break;
|
||||
default:
|
||||
limit = std::sqrt(6.0f / (m_num_inputs_per_node + m_num_nodes));
|
||||
break;
|
||||
}
|
||||
utils::gen_rand<T> randf(limit);
|
||||
for(auto & node : m_nodes) {
|
||||
for(auto & weight : node.GetWeights()) {
|
||||
weight = randf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Saves the layer to a file
|
||||
* @param file File pointer to save the layer
|
||||
* @return true if save was successful, false if there was an error
|
||||
*/
|
||||
bool SaveLayer(FILE * file) const {
|
||||
if (fwrite(&m_num_nodes, sizeof(m_num_nodes), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (fwrite(&m_num_inputs_per_node, sizeof(m_num_inputs_per_node), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (fwrite(&m_activation_function_type, sizeof(ACTIVATION_FUNCTIONS), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_nodes.size(); i++) {
|
||||
if (!m_nodes[i].SaveNode(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Loads the layer from a file
|
||||
* @param file File pointer to load the layer
|
||||
* @return true if load was successful, false if there was an error
|
||||
*/
|
||||
bool LoadLayer(FILE * file) {
|
||||
m_nodes.clear();
|
||||
|
||||
if (fread(&m_num_nodes, sizeof(m_num_nodes), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (fread(&m_num_inputs_per_node, sizeof(m_num_inputs_per_node), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (fread(&(m_activation_function_type), sizeof(ACTIVATION_FUNCTIONS), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::pair<activation_func_t<T>,
|
||||
activation_func_t<T> > *pair;
|
||||
bool ret_val = utils::ActivationFunctionsManager<T>::Singleton().
|
||||
GetActivationFunctionPair(m_activation_function_type,
|
||||
&pair);
|
||||
if (!ret_val) {
|
||||
return false;
|
||||
}
|
||||
m_activation_function = (*pair).first;
|
||||
m_deriv_activation_function = (*pair).second;
|
||||
|
||||
m_nodes.resize(m_num_nodes);
|
||||
for (size_t i = 0; i < m_nodes.size(); i++) {
|
||||
if (!m_nodes[i].LoadNode(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
std::vector<Node<T>> m_nodes;
|
||||
|
||||
std::vector<T> cachedOutputs;
|
||||
|
||||
size_t m_num_inputs_per_node{ 0 }; /**< Number of inputs per node in this layer */
|
||||
size_t m_num_nodes{ 0 }; /**< Number of nodes in this layer */
|
||||
|
||||
protected:
|
||||
ACTIVATION_FUNCTIONS m_activation_function_type; /**< Type of activation function used */
|
||||
activation_func_t<T> m_activation_function; /**< Pointer to activation function */
|
||||
activation_func_t<T> m_deriv_activation_function; /**< Pointer to derivative of activation function */
|
||||
|
||||
bool m_cacheOutputs{false}; /**< Flag controlling output caching */
|
||||
std::vector<T> grads; /**< Stored gradients for optimization */
|
||||
};
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif //NISPS_LAYER_HPP
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
/**
|
||||
* @file loss.hpp
|
||||
* @brief Loss functions and management for machine learning operations
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
*/
|
||||
|
||||
#ifndef NISPS_LOSS_HPP
|
||||
#define NISPS_LOSS_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
// #include <string>
|
||||
|
||||
|
||||
#define MLP_LOSS_FN
|
||||
|
||||
namespace nisps {
|
||||
|
||||
namespace loss {
|
||||
|
||||
/**
|
||||
* @enum LOSS_FUNCTIONS
|
||||
* @brief Enumeration of supported loss functions.
|
||||
*/
|
||||
enum LOSS_FUNCTIONS {
|
||||
LOSS_MSE, /**< Mean Squared Error loss function */
|
||||
LOSS_CATEGORICAL_CROSSENTROPY /**< Categorical Cross-Entropy loss function */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Computes the Mean Squared Error loss between expected and actual values
|
||||
* @tparam T The type of the values
|
||||
* @param expected Vector of expected values
|
||||
* @param actual Vector of actual values
|
||||
* @param loss_deriv Vector to store the loss derivatives
|
||||
* @param sampleSizeReciprocal Reciprocal of the sample size for normalization
|
||||
* @return The computed MSE loss value
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_LOSS_FN
|
||||
inline T MSE(const std::vector<T> &expected, const std::vector<T> &actual,
|
||||
std::vector<T> &loss_deriv, T sampleSizeReciprocal) {
|
||||
|
||||
T accum_loss = 0.;
|
||||
T n_elem = actual.size();
|
||||
T one_over_n_elem = (T)1. / n_elem;
|
||||
|
||||
for (unsigned int j = 0; j < actual.size(); j++) {
|
||||
//TODO CK separate out diff for efficiency, replace pow with diff*diff
|
||||
const T diff = expected[j] - actual[j];
|
||||
accum_loss += (diff * diff) //std::pow((expected[j] - actual[j]), 2)
|
||||
* one_over_n_elem;
|
||||
loss_deriv[j] =
|
||||
(T)-2 * one_over_n_elem
|
||||
* diff * sampleSizeReciprocal;
|
||||
}
|
||||
accum_loss *= sampleSizeReciprocal;
|
||||
|
||||
return accum_loss;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the Categorical Cross-Entropy loss between expected and actual values
|
||||
* @tparam T The type of the values
|
||||
* @param expected Vector of one-hot encoded expected values
|
||||
* @param actual Vector of raw logits (pre-softmax)
|
||||
* @param loss_deriv Vector to store the loss derivatives
|
||||
* @param sampleSizeReciprocal Reciprocal of the sample size for normalization
|
||||
* @return The computed categorical cross-entropy loss value
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_LOSS_FN
|
||||
inline T CategoricalCrossEntropy(const std::vector<T> &expected, const std::vector<T> &actual,
|
||||
std::vector<T> &loss_deriv, T sampleSizeReciprocal) {
|
||||
|
||||
// T n_elem = actual.size();
|
||||
|
||||
// Find maximum logit for numerical stability (log-sum-exp trick)
|
||||
T max_logit = actual[0];
|
||||
for (unsigned int i = 1; i < actual.size(); i++) {
|
||||
if (actual[i] > max_logit) {
|
||||
max_logit = actual[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Compute log-sum-exp with numerical stability
|
||||
T sum_exp = 0.;
|
||||
for (unsigned int i = 0; i < actual.size(); i++) {
|
||||
sum_exp += expf(actual[i] - max_logit);
|
||||
}
|
||||
T log_sum_exp = max_logit + logf(sum_exp);
|
||||
|
||||
// Find target class index and compute loss
|
||||
T loss = 0.;
|
||||
// int target_class = -1;
|
||||
for (unsigned int i = 0; i < expected.size(); i++) {
|
||||
if (expected[i] > (T)0.5) { // One-hot encoded, so target class has value 1
|
||||
// target_class = i;
|
||||
loss = -actual[i] + log_sum_exp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute softmax probabilities and gradients
|
||||
for (unsigned int i = 0; i < actual.size(); i++) {
|
||||
T softmax_prob = expf(actual[i] - max_logit) / sum_exp;
|
||||
loss_deriv[i] = (softmax_prob - expected[i]) * sampleSizeReciprocal;
|
||||
}
|
||||
|
||||
return loss * sampleSizeReciprocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef loss_func_t
|
||||
* @brief Type definition for loss function pointers
|
||||
* @tparam T The type of the values
|
||||
*/
|
||||
template<typename T>
|
||||
using loss_func_t = T(*)(const std::vector<T> &, const std::vector<T> &, std::vector<T> &, T);
|
||||
|
||||
/**
|
||||
* @class LossFunctionsManager
|
||||
* @brief Manages loss functions and their access
|
||||
* @tparam T The type of the values used in loss calculations
|
||||
*/
|
||||
template<typename T>
|
||||
class LossFunctionsManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Retrieves a loss function by its identifier
|
||||
* @param loss_name The identifier of the loss function
|
||||
* @param loss_fun Pointer to store the retrieved loss function
|
||||
* @return True if the loss function is found, false otherwise
|
||||
*/
|
||||
bool GetLossFunction(const LOSS_FUNCTIONS loss_name,
|
||||
loss_func_t<T> *loss_fun) {
|
||||
|
||||
auto iter = loss_functions_map.find(loss_name);
|
||||
if (iter != loss_functions_map.end()) {
|
||||
*loss_fun = iter->second;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieves the singleton instance of LossFunctionsManager
|
||||
* @return The singleton instance
|
||||
*/
|
||||
static LossFunctionsManager & Singleton() {
|
||||
static LossFunctionsManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Adds a new loss function to the manager
|
||||
* @param function_name The identifier for the loss function
|
||||
* @param function The loss function to add
|
||||
*/
|
||||
void AddNew(LOSS_FUNCTIONS function_name,
|
||||
loss_func_t<T> function) {
|
||||
loss_functions_map.insert(
|
||||
std::make_pair(function_name, function)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Private constructor for singleton pattern
|
||||
*/
|
||||
LossFunctionsManager() {
|
||||
AddNew(LOSS_FUNCTIONS::LOSS_MSE, &MSE<T>);
|
||||
AddNew(LOSS_FUNCTIONS::LOSS_CATEGORICAL_CROSSENTROPY, &CategoricalCrossEntropy<T>);
|
||||
};
|
||||
|
||||
std::unordered_map<
|
||||
LOSS_FUNCTIONS,
|
||||
loss_func_t<T>
|
||||
> loss_functions_map; /**< Map storing loss functions */
|
||||
};
|
||||
|
||||
} // namespace loss
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif // NISPS_LOSS_HPP
|
||||
|
|
@ -1,478 +0,0 @@
|
|||
/**
|
||||
* @file mlp.hpp
|
||||
* @brief Multi-layer perceptron neural network implementation
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
*/
|
||||
|
||||
#ifndef NISPS_MLP_HPP
|
||||
#define NISPS_MLP_HPP
|
||||
|
||||
// Debug macros (no-op by default, override before including if needed)
|
||||
#ifndef NISPS_DEBUG_PRINT
|
||||
#define NISPS_DEBUG_PRINT(...)
|
||||
#define NISPS_DEBUG_PRINTLN(...)
|
||||
#define NISPS_DEBUG_PRINTF(...)
|
||||
#endif
|
||||
|
||||
#include "layer.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "loss.hpp"
|
||||
#include "sample.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
/**
|
||||
* @class MLP
|
||||
* @brief Multi-layer perceptron neural network with flexible architecture
|
||||
*
|
||||
* This class implements a fully-connected multi-layer perceptron with configurable layers,
|
||||
* nodes per layer, and activation functions. It supports both training and inference modes,
|
||||
* and includes special features for reinforcement learning applications.
|
||||
*
|
||||
* @tparam T The numeric type used for weights and calculations (typically float)
|
||||
*/
|
||||
template<typename T>
|
||||
class MLP {
|
||||
public:
|
||||
/**
|
||||
* @brief Data type for training data pairs (features, labels)
|
||||
*/
|
||||
using training_pair_t = std::pair<
|
||||
std::vector< std::vector<T> >,
|
||||
std::vector< std::vector<T> >
|
||||
>;
|
||||
|
||||
/**
|
||||
* @brief Data type for storing network weights
|
||||
*/
|
||||
using mlp_weights = std::vector< std::vector <std::vector<T> > >;
|
||||
|
||||
/**
|
||||
* @brief Constructs an MLP with specified architecture
|
||||
*
|
||||
* @param layers_nodes Vector specifying number of nodes in each layer (including input and output)
|
||||
* @param layers_activfuncs Vector of activation functions for each layer (except input)
|
||||
* @param loss_function Loss function for training (default: MSE)
|
||||
* @param use_constant_weight_init Whether to use constant weight initialization (default: false)
|
||||
* @param constant_weight_init Value for constant weight initialization if enabled (default: 0.5)
|
||||
*/
|
||||
MLP(const std::vector<size_t> & layers_nodes,
|
||||
const std::vector<ACTIVATION_FUNCTIONS> & layers_activfuncs,
|
||||
loss::LOSS_FUNCTIONS loss_function = loss::LOSS_FUNCTIONS::LOSS_MSE,
|
||||
bool use_constant_weight_init = false,
|
||||
T constant_weight_init = 0.5);
|
||||
|
||||
MLP(const std::string & filename);
|
||||
~MLP();
|
||||
|
||||
/**
|
||||
* @brief Save the MLP network to a file
|
||||
* @param filename Path to the file where the network will be saved
|
||||
* @return true if save was successful, false if there was an error
|
||||
*/
|
||||
bool SaveMLPNetwork(const std::string & filename) const;
|
||||
|
||||
/**
|
||||
* @brief Load the MLP network from a file
|
||||
* @param filename Path to the file containing the network
|
||||
* @return true if load was successful, false if file doesn't exist or there was an error
|
||||
*/
|
||||
bool LoadMLPNetwork(const std::string & filename);
|
||||
|
||||
// Binary serialization methods - not currently implemented in nisps-core
|
||||
// size_t Serialise(size_t w_head, std::vector<uint8_t> &buffer);
|
||||
// size_t FromSerialised(size_t w_head, const std::vector<uint8_t> &buffer);
|
||||
|
||||
/**
|
||||
* @brief Get predicted outputs for given input
|
||||
*
|
||||
* @param input Input feature vector
|
||||
* @param output Pointer to store output predictions
|
||||
* @param all_layers_activations Optional pointer to store activations of all layers
|
||||
* @param for_inference If true and using categorical cross-entropy, applies softmax to output
|
||||
*/
|
||||
void GetOutput(const std::vector<T> &input,
|
||||
std::vector<T> * output,
|
||||
std::vector<std::vector<T>> * all_layers_activations = nullptr,
|
||||
bool for_inference = true);
|
||||
|
||||
/**
|
||||
* @brief Determines the output class from network outputs
|
||||
*
|
||||
* @param output Network output vector
|
||||
* @param class_id Pointer to store the predicted class ID
|
||||
*/
|
||||
void GetOutputClass(const std::vector<T> &output, size_t * class_id) const;
|
||||
|
||||
/**
|
||||
* @brief Train the network using batch gradient descent
|
||||
*
|
||||
* @param training_sample_set_with_bias Training data pairs
|
||||
* @param learning_rate Learning rate for gradient descent
|
||||
* @param max_iterations Maximum training iterations
|
||||
* @param min_error_cost Minimum error threshold for early stopping
|
||||
* @param output_log Whether to output training progress
|
||||
* @return Final training error
|
||||
*/
|
||||
T Train(const training_pair_t& training_sample_set_with_bias,
|
||||
float learning_rate,
|
||||
int max_iterations = 5000,
|
||||
float min_error_cost = 0.001,
|
||||
bool output_log = true,
|
||||
const std::vector<T>* sample_weights = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Training with batch support
|
||||
* @param use_batch_update If true, accumulate gradients for batch update
|
||||
*/
|
||||
T TrainBatch(const training_pair_t& training_sample_set,
|
||||
float learning_rate,
|
||||
int max_iterations = 5000,
|
||||
size_t batch_size = 8,
|
||||
float min_error_cost = 0.001,
|
||||
bool output_log = true);
|
||||
|
||||
// /**
|
||||
// * @brief Train the network using mini-batch gradient descent
|
||||
// *
|
||||
// * @param training_sample_set_with_bias Training data pairs
|
||||
// * @param learning_rate Learning rate for gradient descent
|
||||
// * @param max_iterations Maximum training iterations
|
||||
// * @param miniBatchSize Size of mini-batches
|
||||
// * @param min_error_cost Minimum error threshold for early stopping
|
||||
// * @param output_log Whether to output training progress
|
||||
// * @return Final training error
|
||||
// */
|
||||
// T MiniBatchTrain(const training_pair_t& training_sample_set_with_bias,
|
||||
// float learning_rate,
|
||||
// int max_iterations = 5000,
|
||||
// size_t miniBatchSize=8,
|
||||
// float min_error_cost = 0.001,
|
||||
// bool output_log = true);
|
||||
|
||||
/**
|
||||
* @deprecated Use Train() with training_pair_t instead
|
||||
* @brief Legacy training method using TrainingSample objects
|
||||
*/
|
||||
[[deprecated("Use TrainBatch")]]
|
||||
void Train(const std::vector<TrainingSample<T>> &training_sample_set_with_bias,
|
||||
float learning_rate,
|
||||
int max_iterations = 5000,
|
||||
float min_error_cost = 0.001,
|
||||
bool output_log = true);
|
||||
|
||||
/**
|
||||
* @brief Reset optimizer state for all layers (useful for recovery from numerical issues)
|
||||
*/
|
||||
void ResetOptimizerState() {
|
||||
for (auto& layer : m_layers) {
|
||||
layer.ResetOptimizerState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check and fix NaN/Inf in all network weights
|
||||
* @return true if any corruption was detected and fixed
|
||||
*/
|
||||
bool CheckAndFixWeights() {
|
||||
bool had_corruption = false;
|
||||
for (auto& layer : m_layers) {
|
||||
had_corruption |= layer.CheckAndFixWeights();
|
||||
}
|
||||
return had_corruption;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get number of layers in the network
|
||||
*/
|
||||
size_t GetNumLayers();
|
||||
|
||||
/**
|
||||
* @brief Get weights for a specific layer
|
||||
* @param layer_i Layer index
|
||||
*/
|
||||
std::vector<std::vector<T>> GetLayerWeights( size_t layer_i );
|
||||
|
||||
/**
|
||||
* @brief Get all network weights
|
||||
* @return 3D vector of weights (layer, node, weight)
|
||||
*/
|
||||
mlp_weights GetWeights();
|
||||
|
||||
/**
|
||||
* @brief Set weights for a specific layer
|
||||
* @param layer_i Layer index
|
||||
* @param weights 2D vector of weights for the layer
|
||||
*/
|
||||
void SetLayerWeights( size_t layer_i, std::vector<std::vector<T>> & weights );
|
||||
|
||||
/**
|
||||
* @brief Set all network weights
|
||||
* @param weights 3D vector of weights (layer, node, weight)
|
||||
*/
|
||||
void SetWeights(mlp_weights &weights);
|
||||
|
||||
/**
|
||||
* @brief Randomize network weights
|
||||
*/
|
||||
[[deprecated]]
|
||||
void DrawWeights(float scale=1.f);
|
||||
|
||||
/**
|
||||
* @brief Randomize weights with spread-controlled scaling
|
||||
* @param spread 0 = uniform [-1,1] (polarised outputs), 1 = Xavier-scaled (centered outputs)
|
||||
*
|
||||
* Interpolates weight scale between uniform and Xavier initialization per layer.
|
||||
* At spread=0: weights are uniform [-1,1] (original behavior).
|
||||
* At spread=1: weights are scaled by 1/sqrt(fan_in) per layer (Xavier).
|
||||
* Biases are set to 0.
|
||||
*/
|
||||
void DrawWeightsSpread(T spread);
|
||||
|
||||
void RandomiseWeightsAndBiasesLin(T weightMin, T weightMax, T biasMin, T biasMax);
|
||||
|
||||
void InitXavier();
|
||||
|
||||
/**
|
||||
* @brief Add Gaussian noise to network weights
|
||||
* @param speed Standard deviation of the noise
|
||||
*/
|
||||
void MoveWeights(T speed);
|
||||
|
||||
/**
|
||||
* @brief Add Gaussian noise to weights with spread-controlled scaling and decay
|
||||
* @param speed Base noise standard deviation
|
||||
* @param spread 0 = flat noise (original), 1 = Xavier-scaled noise with weight decay
|
||||
*
|
||||
* At spread=0: noise is uniform across all layers, no weight decay (original behavior).
|
||||
* At spread=1: noise is scaled by 1/sqrt(fan_in) per layer, weights decay 10% per call.
|
||||
* Weight decay prevents unbounded magnitude drift from repeated perturbation.
|
||||
*/
|
||||
void MoveWeightsSpread(T speed, T spread);
|
||||
|
||||
/**
|
||||
* @brief Enable/disable caching of layer outputs
|
||||
*
|
||||
* Required for backpropagation and some RL algorithms
|
||||
*
|
||||
* @param on True to enable caching, false to disable
|
||||
*/
|
||||
void SetCachedLayerOutputs(bool on) {
|
||||
for(auto &layer : m_layers) {
|
||||
layer.SetCachedOutputs(on);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform soft update of network weights (for RL)
|
||||
*
|
||||
* Updates this network's weights using exponential moving average with another network's weights.
|
||||
* Commonly used in RL for target networks.
|
||||
*
|
||||
* @param anotherMLP Source network for weight update
|
||||
* @param alpha Learning rate (0-1) for the update
|
||||
*/
|
||||
inline void SmoothUpdateWeights(std::shared_ptr<MLP<T>> anotherMLP, const float alpha) {
|
||||
//assuming the other MLP has the same structure
|
||||
//calc this once here
|
||||
const float alphaInv = 1.f-alpha;
|
||||
|
||||
for(size_t i=0; i < m_layers.size(); i++) {
|
||||
m_layers[i].SmoothUpdateWeights(anotherMLP->m_layers[i], alpha, alphaInv);
|
||||
}
|
||||
}
|
||||
|
||||
inline void SmoothUpdateWeights(MLP<T> *anotherMLP, const float alpha) {
|
||||
//assuming the other MLP has the same structure
|
||||
//calc this once here
|
||||
const float alphaInv = 1.f-alpha;
|
||||
|
||||
for(size_t i=0; i < m_layers.size(); i++) {
|
||||
m_layers[i].SmoothUpdateWeights(anotherMLP->m_layers[i], alpha, alphaInv);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Calculate gradients through the network (autograd)
|
||||
*
|
||||
* Similar to TensorFlow's tf.gradients(), computes gradients of the network
|
||||
* with respect to the inputs. Useful for policy gradients in RL.
|
||||
*
|
||||
* @param feat Input feature vector
|
||||
* @param deriv_error_output Initial gradient at the output layer
|
||||
*/
|
||||
void CalcGradients(std::vector<T> &feat, std::vector<T> & deriv_error_output);
|
||||
|
||||
/**
|
||||
* @brief Clear accumulated gradients
|
||||
*/
|
||||
void ClearGradients() {
|
||||
for(auto &v: m_layers) {
|
||||
v.SetGrads({});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Backpropagation of loss through the network
|
||||
*
|
||||
* @param feat Input feature vector
|
||||
* @param loss Loss values
|
||||
* @param learning_rate Learning rate for weight updates
|
||||
*/
|
||||
void ApplyLoss(std::vector<T> feat,
|
||||
std::vector<T> loss,
|
||||
float learning_rate);
|
||||
|
||||
|
||||
// void ApplyPolicyGradient(const std::vector<T>& state,
|
||||
// const std::vector<T>& action_gradient,
|
||||
// float learning_rate);
|
||||
void AccumulatePolicyGradient(const std::vector<T>& state,
|
||||
const std::vector<T>& action_gradient);
|
||||
|
||||
void PurturbWeights(const size_t nWeights, const float scale=0.1f) {
|
||||
utils::gen_rand<float> randf(scale);
|
||||
for(size_t i=0; i < nWeights; i++) {
|
||||
size_t layer_i = rand() % (m_layers.size()-1);
|
||||
size_t node_i = rand() % (m_layers[layer_i].GetOutputSize()-1);
|
||||
size_t weight_i = rand() % (m_layers[layer_i].GetInputSize()-1);
|
||||
|
||||
T perturbation = randf();
|
||||
m_layers[layer_i].GetNodesChangeable()[node_i].GetWeights()[weight_i] += perturbation;
|
||||
}
|
||||
}
|
||||
|
||||
void SetProgressCallback(std::function<void(size_t,float)> callback) {
|
||||
m_progress_callback = std::move(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate global gradient norm across all layers
|
||||
*
|
||||
* @return Global gradient norm
|
||||
*/
|
||||
T GetGlobalWeightNorm() {
|
||||
T sum_sq = 0.0f;
|
||||
for(auto &layer : m_layers) {
|
||||
T layerWeightNorm = layer.getWeightNorm();
|
||||
sum_sq += layerWeightNorm * layerWeightNorm;
|
||||
}
|
||||
return std::sqrt(sum_sq);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Vector of network layers
|
||||
*
|
||||
* Public access is provided for advanced usage scenarios like reinforcement learning.
|
||||
* Generally, prefer using the provided interface methods instead of direct access.
|
||||
* Each Layer contains nodes and their weights, biases, and activation functions.
|
||||
* @warning Modifying layers directly may break network functionality unless you know what you're doing
|
||||
*/
|
||||
std::vector<Layer<T>> m_layers;
|
||||
int get_num_inputs() const {
|
||||
return m_num_inputs;
|
||||
}
|
||||
int get_num_outputs() const {
|
||||
return m_num_outputs;
|
||||
}
|
||||
int get_num_hidden_layers() const {
|
||||
return m_num_hidden_layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize gradient accumulators for all layers
|
||||
*/
|
||||
void InitializeAllGradientAccumulators() {
|
||||
for (auto& layer : m_layers) {
|
||||
layer.InitializeGradientAccumulators();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply all accumulated gradients
|
||||
*/
|
||||
void ApplyAllAccumulatedGradients(float learning_rate, float batch_size_inv) {
|
||||
for (auto& layer : m_layers) {
|
||||
layer.ApplyAccumulatedGradients(learning_rate, batch_size_inv);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear all gradient accumulators
|
||||
*/
|
||||
void ClearAllGradientAccumulators() {
|
||||
for (auto& layer : m_layers) {
|
||||
layer.ClearGradientAccumulators();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
/**
|
||||
* @brief Process single sample with optional gradient accumulation
|
||||
*/
|
||||
T ProcessSample(const std::vector<T>& features,
|
||||
const std::vector<T>& labels,
|
||||
bool accumulate_only = false);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Backpropagate with optional gradient accumulation
|
||||
*/
|
||||
void BackpropagateWithAccumulation(const std::vector<std::vector<T>>& all_layers_activations,
|
||||
const std::vector<T>& deriv_error,
|
||||
bool accumulate = true);
|
||||
|
||||
|
||||
void UpdateWeights(const std::vector<std::vector<T>> & all_layers_activations,
|
||||
const std::vector<T> &error,
|
||||
float learning_rate);
|
||||
|
||||
[[deprecated("Use TrainBatch")]]
|
||||
T _TrainOnExample(std::vector<T> feat, std::vector<T> label,
|
||||
float learning_rate, T sampleSizeReciprocal);
|
||||
void CreateMLP(const std::vector<size_t> & layers_nodes,
|
||||
const std::vector<ACTIVATION_FUNCTIONS> & layers_activfuncs,
|
||||
loss::LOSS_FUNCTIONS loss_function = loss::LOSS_FUNCTIONS::LOSS_MSE,
|
||||
bool use_constant_weight_init = false,
|
||||
T constant_weight_init = 0.5);
|
||||
void ReportProgress(const bool output_log,
|
||||
const unsigned int every_n_iter,
|
||||
const unsigned int i,
|
||||
const T current_iteration_cost_function);
|
||||
void ReportFinish(const unsigned int i,
|
||||
const float current_iteration_cost_function);
|
||||
size_t m_num_inputs{ 0 };
|
||||
int m_num_outputs{ 0 };
|
||||
int m_num_hidden_layers{ 0 };
|
||||
std::vector<size_t> m_layers_nodes;
|
||||
MLP_LOSS_FN loss::loss_func_t<T> loss_fn_;
|
||||
loss::LOSS_FUNCTIONS m_loss_function_type; /**< Store loss function type for runtime checks */
|
||||
std::function<void(size_t,float)> m_progress_callback{};
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 g;
|
||||
|
||||
};
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
// Include implementation
|
||||
#include "mlp_impl.hpp"
|
||||
|
||||
#endif //NISPS_MLP_HPP
|
||||
|
|
@ -1,904 +0,0 @@
|
|||
/**
|
||||
* @file mlp_impl.hpp
|
||||
* @brief Multi-layer perceptron implementation
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
* Original author: David Nogueira
|
||||
*/
|
||||
|
||||
#ifndef NISPS_MLP_IMPL_HPP
|
||||
#define NISPS_MLP_IMPL_HPP
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
// #define SAFE_MODE
|
||||
|
||||
|
||||
//desired call syntax : MLP({64*64,20,4}, {"sigmoid", "linear"},
|
||||
namespace nisps {
|
||||
|
||||
template<typename T>
|
||||
MLP<T>::MLP(const std::vector<size_t> & layers_nodes,
|
||||
const std::vector<ACTIVATION_FUNCTIONS> & layers_activfuncs,
|
||||
loss::LOSS_FUNCTIONS loss_function,
|
||||
bool use_constant_weight_init,
|
||||
T constant_weight_init) : g(rd()) {
|
||||
#ifdef SAFE_MODE
|
||||
assert(layers_nodes.size() >= 2);
|
||||
assert(layers_activfuncs.size() + 1 == layers_nodes.size());
|
||||
#endif
|
||||
|
||||
CreateMLP(layers_nodes,
|
||||
layers_activfuncs,
|
||||
loss_function,
|
||||
use_constant_weight_init,
|
||||
constant_weight_init);
|
||||
};
|
||||
|
||||
|
||||
template<typename T>
|
||||
MLP<T>::MLP(const std::string & filename) {
|
||||
if (!LoadMLPNetwork(filename)) {
|
||||
// If loading fails, we need to have a valid but empty network
|
||||
// Initialize with minimal valid configuration
|
||||
m_num_inputs = 0;
|
||||
m_num_outputs = 0;
|
||||
m_num_hidden_layers = 0;
|
||||
m_layers_nodes.clear();
|
||||
m_layers.clear();
|
||||
// Consider throwing an exception or setting an error flag here
|
||||
// For now, we'll have an invalid network that should be checked
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
MLP<T>::~MLP() {
|
||||
m_num_inputs = 0;
|
||||
m_num_outputs = 0;
|
||||
m_num_hidden_layers = 0;
|
||||
m_layers_nodes.clear();
|
||||
m_layers.clear();
|
||||
};
|
||||
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::CreateMLP(const std::vector<size_t> & layers_nodes,
|
||||
const std::vector<ACTIVATION_FUNCTIONS> & layers_activfuncs,
|
||||
loss::LOSS_FUNCTIONS loss_function,
|
||||
bool use_constant_weight_init,
|
||||
T constant_weight_init) {
|
||||
m_layers_nodes = layers_nodes;
|
||||
m_num_inputs = m_layers_nodes[0];
|
||||
m_num_outputs = m_layers_nodes[m_layers_nodes.size() - 1];
|
||||
m_num_hidden_layers = m_layers_nodes.size() - 2;
|
||||
|
||||
// Store loss function type for inference decisions
|
||||
m_loss_function_type = loss_function;
|
||||
|
||||
// Loss function selection
|
||||
loss::LossFunctionsManager<T> loss_mgr =
|
||||
loss::LossFunctionsManager<T>::Singleton();
|
||||
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],
|
||||
m_layers_nodes[i + 1],
|
||||
layers_activfuncs[i],
|
||||
use_constant_weight_init,
|
||||
constant_weight_init));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::ReportProgress(const bool output_log,
|
||||
const unsigned int every_n_iter,
|
||||
const unsigned int i,
|
||||
const T sampleLoss)
|
||||
{
|
||||
if (output_log && ((i % every_n_iter) == 0)) {
|
||||
NISPS_DEBUG_PRINTF("Iteration %u cost function f(error): %f\n",
|
||||
i, static_cast<double>(sampleLoss));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::ReportFinish(const unsigned int i, const float current_iteration_cost_function)
|
||||
{
|
||||
NISPS_DEBUG_PRINTF("Iteration %u cost function f(error): %f\n",
|
||||
i, static_cast<double>(current_iteration_cost_function));
|
||||
|
||||
NISPS_DEBUG_PRINTLN("******************************");
|
||||
NISPS_DEBUG_PRINTLN("******* TRAINING ENDED *******");
|
||||
NISPS_DEBUG_PRINTF("******* %d iters *******\n", i);
|
||||
NISPS_DEBUG_PRINTLN("******************************");
|
||||
};
|
||||
|
||||
|
||||
template<typename T>
|
||||
bool MLP<T>::SaveMLPNetwork(const std::string & filename) const {
|
||||
FILE * file = fopen(filename.c_str(), "wb");
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write network structure
|
||||
if (fwrite(&m_num_inputs, sizeof(m_num_inputs), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
if (fwrite(&m_num_outputs, sizeof(m_num_outputs), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
if (fwrite(&m_num_hidden_layers, sizeof(m_num_hidden_layers), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write layer nodes
|
||||
if (!m_layers_nodes.empty()) {
|
||||
if (fwrite(&m_layers_nodes[0], sizeof(m_layers_nodes[0]), m_layers_nodes.size(), file) != m_layers_nodes.size()) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Write layer weights
|
||||
for (size_t i = 0; i < m_layers.size(); i++) {
|
||||
if (!m_layers[i].SaveLayer(file)) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool MLP<T>::LoadMLPNetwork(const std::string & filename) {
|
||||
// Check if file exists
|
||||
FILE * file = fopen(filename.c_str(), "rb");
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear existing network
|
||||
m_layers_nodes.clear();
|
||||
m_layers.clear();
|
||||
|
||||
// Read network structure
|
||||
if (fread(&m_num_inputs, sizeof(m_num_inputs), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
if (fread(&m_num_outputs, sizeof(m_num_outputs), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
if (fread(&m_num_hidden_layers, sizeof(m_num_hidden_layers), 1, file) != 1) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read layer nodes
|
||||
m_layers_nodes.resize(m_num_hidden_layers + 2);
|
||||
if (!m_layers_nodes.empty()) {
|
||||
if (fread(&m_layers_nodes[0], sizeof(m_layers_nodes[0]), m_layers_nodes.size(), file) != m_layers_nodes.size()) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read layer weights
|
||||
m_layers.resize(m_layers_nodes.size() - 1);
|
||||
for (size_t i = 0; i < m_layers.size(); i++) {
|
||||
if (!m_layers[i].LoadLayer(file)) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Serialization methods commented out - not needed for nisps-core basic functionality
|
||||
// Uncomment and implement if binary serialization is required
|
||||
/*
|
||||
template <typename T>
|
||||
size_t MLP<T>::Serialise(size_t w_head, std::vector<uint8_t> &buffer)
|
||||
{
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
auto layer_weights = GetLayerWeights(n);
|
||||
w_head = Serialise::FromVector2D(w_head, layer_weights, buffer);
|
||||
}
|
||||
return w_head;
|
||||
}
|
||||
|
||||
|
||||
template <typename T>
|
||||
size_t MLP<T>::FromSerialised(size_t r_head, const std::vector<uint8_t> &buffer)
|
||||
{
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
std::vector< std::vector<T> > layer_weights;
|
||||
r_head = Serialise::ToVector2D(r_head, buffer, layer_weights);
|
||||
SetLayerWeights(n, layer_weights);
|
||||
}
|
||||
return r_head;
|
||||
};
|
||||
*/
|
||||
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::GetOutput(const std::vector<T> &input,
|
||||
std::vector<T> * output,
|
||||
std::vector<std::vector<T>> * all_layers_activations,
|
||||
bool for_inference) {
|
||||
// Add safety check
|
||||
if (input.size() != m_num_inputs) {
|
||||
NISPS_DEBUG_PRINTF("ERROR: input.size()=%zu != m_num_inputs=%zu\n",
|
||||
input.size(), m_num_inputs);
|
||||
return;
|
||||
}
|
||||
|
||||
int temp_size;
|
||||
if (m_num_hidden_layers == 0)
|
||||
temp_size = m_num_outputs;
|
||||
else
|
||||
temp_size = m_layers_nodes[1];
|
||||
|
||||
// Pre-allocate with capacity to avoid reallocations
|
||||
std::vector<T> temp_in;
|
||||
temp_in.reserve(m_num_inputs);
|
||||
temp_in = input;
|
||||
|
||||
std::vector<T> temp_out;
|
||||
temp_out.reserve(temp_size);
|
||||
|
||||
for (size_t i = 0; i < m_layers.size(); ++i) {
|
||||
if (i > 0) {
|
||||
//Store this layer activation
|
||||
if (all_layers_activations != nullptr)
|
||||
all_layers_activations->emplace_back(std::move(temp_in));
|
||||
|
||||
temp_in.clear();
|
||||
temp_in = temp_out;
|
||||
temp_out.clear();
|
||||
temp_out.resize(m_layers[i].GetOutputSize());
|
||||
}
|
||||
m_layers[i].GetOutputAfterActivationFunction(temp_in, &temp_out);
|
||||
}
|
||||
|
||||
// Apply softmax for inference with categorical cross-entropy
|
||||
if (for_inference &&
|
||||
m_loss_function_type == loss::LOSS_FUNCTIONS::LOSS_CATEGORICAL_CROSSENTROPY &&
|
||||
temp_out.size() > 1) {
|
||||
utils::Softmax(&temp_out);
|
||||
}
|
||||
|
||||
*output = temp_out;
|
||||
|
||||
//Add last layer activation
|
||||
if (all_layers_activations != nullptr)
|
||||
all_layers_activations->emplace_back(std::move(temp_in));
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::GetOutputClass(const std::vector<T> &output, size_t * class_id) const {
|
||||
utils::GetIdMaxElement(output, class_id);
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::UpdateWeights(const std::vector<std::vector<T>> & all_layers_activations,
|
||||
const std::vector<T> &deriv_error,
|
||||
float learning_rate) {
|
||||
|
||||
std::vector<T> temp_deriv_error = deriv_error;
|
||||
std::vector<T> deltas{};
|
||||
//m_layers.size() equals (m_num_hidden_layers + 1)
|
||||
for (int i = m_num_hidden_layers; i >= 0; --i) {
|
||||
m_layers[i].UpdateWeights(all_layers_activations[i], temp_deriv_error, learning_rate, &deltas);
|
||||
if (i > 0) {
|
||||
temp_deriv_error.clear();
|
||||
temp_deriv_error = std::move(deltas);
|
||||
deltas.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
T MLP<T>::TrainBatch(const training_pair_t& training_sample_set,
|
||||
float learning_rate,
|
||||
int max_iterations,
|
||||
size_t batch_size,
|
||||
float min_error_cost,
|
||||
bool output_log) {
|
||||
|
||||
auto training_features = training_sample_set.first;
|
||||
auto training_labels = training_sample_set.second;
|
||||
|
||||
size_t n_samples = training_features.size();
|
||||
size_t n_batches = (n_samples + batch_size - 1) / batch_size;
|
||||
|
||||
T epoch_loss = 0;
|
||||
for (int iter = 0; iter < max_iterations; iter++) {
|
||||
|
||||
epoch_loss = 0;
|
||||
|
||||
// Shuffle indices
|
||||
std::vector<size_t> indices(n_samples);
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
|
||||
std::shuffle(indices.begin(), indices.end(), g);
|
||||
|
||||
size_t sample_idx = 0;
|
||||
|
||||
for (size_t batch = 0; batch < n_batches; batch++) {
|
||||
size_t current_batch_size = std::min(batch_size, n_samples - sample_idx);
|
||||
T batch_size_reciprocal = (T)1.0 / static_cast<T>(current_batch_size);
|
||||
|
||||
// Initialize gradient accumulators
|
||||
InitializeAllGradientAccumulators();
|
||||
|
||||
T batch_loss = 0;
|
||||
|
||||
// Pre-allocate vectors outside loop to avoid repeated allocations
|
||||
std::vector<T> predicted_output;
|
||||
std::vector<std::vector<T>> all_layers_activations;
|
||||
std::vector<T> deriv_error_output;
|
||||
|
||||
// Process batch - accumulate gradients
|
||||
for (size_t i = 0; i < current_batch_size; i++) {
|
||||
size_t idx = indices[sample_idx++];
|
||||
#ifdef SAFE_MODE
|
||||
// Bounds check
|
||||
if (idx >= training_features.size()) {
|
||||
NISPS_DEBUG_PRINTF("ERROR: idx %zu >= training_features.size() %zu\n",
|
||||
idx, training_features.size());
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
// Clear and reuse vectors
|
||||
predicted_output.clear();
|
||||
all_layers_activations.clear();
|
||||
|
||||
// Forward pass
|
||||
// NISPS_DEBUG_PRINTF("Processing sample %zu (idx=%zu), input_size=%zu\n", i, idx, training_features[idx].size());
|
||||
GetOutput(training_features[idx],
|
||||
&predicted_output,
|
||||
&all_layers_activations,
|
||||
false);
|
||||
|
||||
// Compute loss and derivatives
|
||||
deriv_error_output.clear();
|
||||
deriv_error_output.resize(predicted_output.size());
|
||||
T loss = loss_fn_(training_labels[idx],
|
||||
predicted_output,
|
||||
deriv_error_output,
|
||||
1.0f);
|
||||
|
||||
#ifdef MLP_ALLOW_DEBUG
|
||||
if (std::isinf(loss) || std::isnan(loss)) {
|
||||
NISPS_DEBUG_PRINTF("[MLP DEBUG] *** INF/NAN loss at sample %zu! loss=%f\n",
|
||||
i, static_cast<double>(loss));
|
||||
NISPS_DEBUG_PRINTF("[MLP DEBUG] pred[0]=%f, label[0]=%f\n",
|
||||
static_cast<double>(predicted_output[0]),
|
||||
static_cast<double>(training_labels[idx][0]));
|
||||
}
|
||||
#endif
|
||||
|
||||
batch_loss += loss;
|
||||
|
||||
// Accumulate gradients through backpropagation
|
||||
BackpropagateWithAccumulation(all_layers_activations,
|
||||
deriv_error_output,
|
||||
true);
|
||||
}
|
||||
|
||||
// clipping gradients
|
||||
T grad_sumsq = 0.0f;
|
||||
for (auto& layer : m_layers) {
|
||||
grad_sumsq += layer.GetGradSumSquared(batch_size_reciprocal);
|
||||
}
|
||||
T grad_norm = std::sqrt(grad_sumsq );
|
||||
|
||||
#ifdef MLP_ALLOW_DEBUG
|
||||
NISPS_DEBUG_PRINTF("[MLP DEBUG] Batch %zu/%zu: batch_loss=%f, grad_norm=%f\n",
|
||||
batch, n_batches, static_cast<double>(batch_loss / current_batch_size),
|
||||
static_cast<double>(grad_norm));
|
||||
if (std::isinf(grad_norm) || std::isnan(grad_norm)) {
|
||||
NISPS_DEBUG_PRINTLN("[MLP DEBUG] *** INF/NAN grad_norm! ***");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (grad_norm > 5.0f) {
|
||||
T clip_coef = 5.0f / grad_norm;
|
||||
for (auto& layer : m_layers) {
|
||||
layer.ScaleAccumulatedGradients(clip_coef);
|
||||
}
|
||||
// NISPS_DEBUG_PRINTF("Clipped gradients with coef: %f\n", static_cast<double>(clip_coef));
|
||||
}
|
||||
|
||||
// Apply accumulated gradients
|
||||
ApplyAllAccumulatedGradients(learning_rate, batch_size_reciprocal);
|
||||
|
||||
epoch_loss += batch_loss / current_batch_size;
|
||||
}
|
||||
|
||||
epoch_loss /= n_batches;
|
||||
|
||||
// Periodic weight corruption check (every 10 iterations)
|
||||
// if (iter % 10 == 0) {
|
||||
// if (CheckAndFixWeights()) {
|
||||
// #ifdef MLP_ALLOW_DEBUG
|
||||
// NISPS_DEBUG_PRINTF("[MLP DEBUG] *** Weight corruption detected and fixed at iteration %d! ***\n", iter);
|
||||
// #endif
|
||||
// // Optionally reset optimizer state after corruption
|
||||
// // ResetOptimizerState();
|
||||
// }
|
||||
// }
|
||||
|
||||
#ifdef MLP_ALLOW_DEBUG
|
||||
if (std::isinf(epoch_loss) || std::isnan(epoch_loss)) {
|
||||
NISPS_DEBUG_PRINTF("[MLP DEBUG] *** INF/NAN epoch_loss after iteration %d! ***\n", iter);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (output_log && (iter % 100 == 0)) {
|
||||
ReportProgress(output_log, 100, iter, epoch_loss);
|
||||
}
|
||||
|
||||
if (m_progress_callback) {
|
||||
m_progress_callback(iter, epoch_loss);
|
||||
}
|
||||
|
||||
if (epoch_loss < min_error_cost) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MLP_ALLOW_DEBUG
|
||||
NISPS_DEBUG_PRINTF("[MLP DEBUG] TrainBatch returning epoch_loss=%f (inf=%d, nan=%d)\n",
|
||||
static_cast<double>(epoch_loss),
|
||||
std::isinf(epoch_loss), std::isnan(epoch_loss));
|
||||
#endif
|
||||
|
||||
return epoch_loss;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::BackpropagateWithAccumulation(const std::vector<std::vector<T>>& all_layers_activations,
|
||||
const std::vector<T>& deriv_error,
|
||||
bool accumulate) {
|
||||
std::vector<T> temp_deriv_error = deriv_error;
|
||||
std::vector<T> deltas;
|
||||
|
||||
for (int i = m_num_hidden_layers; i >= 0; --i) {
|
||||
m_layers[i].UpdateWeights(all_layers_activations[i],
|
||||
temp_deriv_error,
|
||||
0, // Learning rate not used when accumulating
|
||||
&deltas,
|
||||
accumulate); // Use accumulation flag
|
||||
|
||||
if (i > 0) {
|
||||
temp_deriv_error = std::move(deltas);
|
||||
deltas.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T MLP<T>::Train(const training_pair_t& training_sample_set_with_bias,
|
||||
float learning_rate,
|
||||
int max_iterations,
|
||||
float min_error_cost,
|
||||
bool,
|
||||
const std::vector<T>* sample_weights) {
|
||||
|
||||
int i = 0;
|
||||
T current_iteration_cost_function = 0.f;
|
||||
|
||||
const size_t n_samples = training_sample_set_with_bias.first.size();
|
||||
T sampleSizeReciprocal = 1.f / n_samples;
|
||||
|
||||
for (i = 0; i < max_iterations; i++) {
|
||||
current_iteration_cost_function = 0.f;
|
||||
|
||||
auto training_features = training_sample_set_with_bias.first;
|
||||
auto training_labels = training_sample_set_with_bias.second;
|
||||
|
||||
for (size_t s = 0; s < n_samples; s++) {
|
||||
T weight = sample_weights ? (*sample_weights)[s] : sampleSizeReciprocal;
|
||||
|
||||
current_iteration_cost_function +=
|
||||
_TrainOnExample(training_features[s], training_labels[s], learning_rate, weight);
|
||||
}
|
||||
|
||||
// When using custom weights (already normalized to sum to 1), loss is already scaled.
|
||||
// With uniform weights, multiply by sampleSizeReciprocal for backward compat.
|
||||
if (!sample_weights) {
|
||||
current_iteration_cost_function *= sampleSizeReciprocal;
|
||||
}
|
||||
|
||||
ReportProgress(true, 100, i, current_iteration_cost_function);
|
||||
|
||||
if (m_progress_callback && !(i & 0x1F)) { // Call progress callback every 32 iterations
|
||||
m_progress_callback(i, current_iteration_cost_function);
|
||||
}
|
||||
|
||||
// Early stopping
|
||||
// TODO AM early stopping should be optional and metric-dependent
|
||||
if (current_iteration_cost_function < min_error_cost) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ReportFinish(i, current_iteration_cost_function);
|
||||
|
||||
if (m_progress_callback) {
|
||||
// Final callback to report completion
|
||||
m_progress_callback(i, current_iteration_cost_function);
|
||||
}
|
||||
|
||||
return current_iteration_cost_function;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::CalcGradients(std::vector<T> & feat, std::vector<T> & deriv_error_output)
|
||||
{
|
||||
std::vector<T> predicted_output;
|
||||
std::vector< std::vector<T> > all_layers_activations;
|
||||
|
||||
GetOutput(feat,
|
||||
&predicted_output,
|
||||
&all_layers_activations,
|
||||
false); // Training mode - no softmax
|
||||
|
||||
|
||||
// std::vector<T> deriv_error_output(predicted_output.size(), 1.0);
|
||||
|
||||
// UpdateWeights(all_layers_activations,
|
||||
// deriv_error_output,
|
||||
// learning_rate);
|
||||
|
||||
std::vector<T> temp_deriv_error = deriv_error_output;
|
||||
std::vector<T> deltas{};
|
||||
//m_layers.size() equals (m_num_hidden_layers + 1)
|
||||
for (int i = m_num_hidden_layers; i >= 0; --i) {
|
||||
m_layers[i].CalcGradients(all_layers_activations[i], temp_deriv_error, &deltas);
|
||||
if (i > 0) {
|
||||
temp_deriv_error.clear();
|
||||
temp_deriv_error = std::move(deltas);
|
||||
deltas.clear();
|
||||
}else {
|
||||
m_layers[0].SetGrads(deltas);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T MLP<T>::_TrainOnExample(std::vector<T> feat,
|
||||
std::vector<T> label,
|
||||
float learning_rate,
|
||||
T sampleSizeReciprocal)
|
||||
{
|
||||
std::vector<T> predicted_output;
|
||||
std::vector< std::vector<T> > all_layers_activations;
|
||||
|
||||
GetOutput(feat,
|
||||
&predicted_output,
|
||||
&all_layers_activations,
|
||||
false); // Training mode - no softmax
|
||||
|
||||
const std::vector<T>& correct_output{ label };
|
||||
|
||||
assert(correct_output.size() == predicted_output.size());
|
||||
std::vector<T> deriv_error_output(predicted_output.size());
|
||||
|
||||
// Loss function
|
||||
T current_iteration_cost_function =
|
||||
this->loss_fn_(correct_output, predicted_output,
|
||||
deriv_error_output, sampleSizeReciprocal);
|
||||
|
||||
UpdateWeights(all_layers_activations,
|
||||
deriv_error_output,
|
||||
learning_rate);
|
||||
|
||||
return current_iteration_cost_function;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::ApplyLoss(std::vector<T> feat,
|
||||
std::vector<T> loss,
|
||||
float learning_rate)
|
||||
{
|
||||
std::vector<T> predicted_output;
|
||||
std::vector< std::vector<T> > all_layers_activations;
|
||||
|
||||
GetOutput(feat,
|
||||
&predicted_output,
|
||||
&all_layers_activations,
|
||||
false); // Training mode - no softmax
|
||||
|
||||
assert(loss.size() == predicted_output.size());
|
||||
|
||||
UpdateWeights(all_layers_activations,
|
||||
loss,
|
||||
learning_rate);
|
||||
}
|
||||
|
||||
// template<typename T>
|
||||
// void MLP<T>::ApplyPolicyGradient(const std::vector<T>& state,
|
||||
// const std::vector<T>& action_gradient,
|
||||
// float learning_rate) {
|
||||
// std::vector<T> predicted_output;
|
||||
// std::vector<std::vector<T>> all_layers_activations;
|
||||
|
||||
// // Forward pass
|
||||
// GetOutput(state, &predicted_output, &all_layers_activations, false);
|
||||
|
||||
// // Negate gradients for maximization
|
||||
// std::vector<T> neg_gradient(action_gradient.size());
|
||||
// for(size_t i = 0; i < action_gradient.size(); i++) {
|
||||
// neg_gradient[i] = -action_gradient[i];
|
||||
// }
|
||||
|
||||
// // Backprop
|
||||
// UpdateWeights(all_layers_activations, neg_gradient, learning_rate);
|
||||
// }
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::AccumulatePolicyGradient(const std::vector<T>& state,
|
||||
const std::vector<T>& action_gradient) {
|
||||
std::vector<T> predicted_output;
|
||||
std::vector<std::vector<T>> all_layers_activations;
|
||||
|
||||
// Forward pass
|
||||
GetOutput(state, &predicted_output, &all_layers_activations, false);
|
||||
|
||||
// Negate gradients for maximization
|
||||
std::vector<T> neg_gradient(action_gradient.size());
|
||||
for(size_t i = 0; i < action_gradient.size(); i++) {
|
||||
neg_gradient[i] = -action_gradient[i];
|
||||
}
|
||||
|
||||
// Accumulate gradients through backpropagation
|
||||
BackpropagateWithAccumulation(all_layers_activations,
|
||||
neg_gradient,
|
||||
true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::Train(const std::vector<TrainingSample<T>>
|
||||
&training_sample_set_with_bias,
|
||||
float learning_rate,
|
||||
int max_iterations,
|
||||
float min_error_cost,
|
||||
bool output_log)
|
||||
{
|
||||
std::vector< std::vector<T> > features, labels;
|
||||
|
||||
for (const auto &sample : training_sample_set_with_bias) {
|
||||
features.push_back(sample.input_vector());
|
||||
labels.push_back(sample.output_vector());
|
||||
}
|
||||
|
||||
training_pair_t t_pair(features, labels);
|
||||
Train(t_pair, learning_rate, max_iterations,
|
||||
min_error_cost, output_log);
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
size_t MLP<T>::GetNumLayers()
|
||||
{
|
||||
return m_layers.size();
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
std::vector<std::vector<T>> MLP<T>::GetLayerWeights( size_t layer_i )
|
||||
{
|
||||
std::vector<std::vector<T>> ret_val;
|
||||
// check parameters
|
||||
assert(layer_i < m_layers.size() /* Incorrect layer number in GetLayerWeights call */);
|
||||
{
|
||||
Layer<T> current_layer = m_layers[layer_i];
|
||||
for( Node<T> & node : current_layer.GetNodesChangeable() )
|
||||
{
|
||||
ret_val.push_back( node.GetWeights() );
|
||||
}
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
typename MLP<T>::mlp_weights MLP<T>::GetWeights()
|
||||
{
|
||||
MLP<T>::mlp_weights out;
|
||||
|
||||
out.resize(m_layers.size());
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
out[n].resize(m_layers[n].m_nodes.size());
|
||||
for (unsigned int k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
out[n][k].resize(m_layers[n].m_nodes[k].m_weights.size());
|
||||
for (unsigned int j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
out[n][k][j] = m_layers[n].m_nodes[k].m_weights[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void MLP<T>::SetLayerWeights( size_t layer_i, std::vector<std::vector<T>> & weights )
|
||||
{
|
||||
// check parameters
|
||||
assert(layer_i < m_layers.size() /* Incorrect layer number in SetLayerWeights call */);
|
||||
{
|
||||
m_layers[layer_i].SetWeights( weights );
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::SetWeights(MLP<T>::mlp_weights &weights)
|
||||
{
|
||||
#ifdef SAFE_MODE
|
||||
NISPS_DEBUG_PRINTF("SetWeights: vector dim check. Expected=%zu, actual=%zu\n",
|
||||
m_layers.size(), weights.size());
|
||||
assert(weights.size() == m_layers.size());
|
||||
#endif
|
||||
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
SetLayerWeights(n, weights[n]);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::DrawWeights(float scale)
|
||||
{
|
||||
// T before = m_layers[0].m_nodes[0].m_weights[0];
|
||||
utils::gen_rand<T> gen;
|
||||
// utils::gen_randn<T> gen(0.f, scale); //mean, stddev
|
||||
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
for (unsigned int k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (unsigned int j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
float mod = gen() * scale;
|
||||
m_layers[n].m_nodes[k].m_weights[j] = mod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assert(m_layers[0].m_nodes[0].m_weights[0] != before);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::DrawWeightsSpread(T spread) {
|
||||
utils::gen_rand<T> gen;
|
||||
|
||||
for (size_t n = 0; n < m_layers.size(); n++) {
|
||||
const size_t fanIn = m_layers_nodes[n];
|
||||
const T xavierScale = static_cast<T>(1.0) / std::sqrt(static_cast<T>(fanIn));
|
||||
const T scale = static_cast<T>(1.0) * (static_cast<T>(1.0) - spread) + xavierScale * spread;
|
||||
|
||||
for (size_t k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
m_layers[n].m_nodes[k].m_weights[j] = gen() * scale;
|
||||
}
|
||||
m_layers[n].m_nodes[k].m_bias = static_cast<T>(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::MoveWeights(T speed)
|
||||
{
|
||||
T before = m_layers[0].m_nodes[0].m_weights[0];
|
||||
utils::gen_randn<T> gen(speed);
|
||||
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
// size_t num_inputs = m_layers_nodes[n];
|
||||
for (unsigned int k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (unsigned int j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
T w = m_layers[n].m_nodes[k].m_weights[j];
|
||||
m_layers[n].m_nodes[k].m_weights[j] = gen(m_layers[n].m_nodes[k].m_weights[j]);
|
||||
T w2 = m_layers[n].m_nodes[k].m_weights[j];
|
||||
if (speed != 0) {
|
||||
assert(w != w2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(m_layers[0].m_nodes[0].m_weights[0] != before);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::MoveWeightsSpread(T speed, T spread) {
|
||||
const T decay = static_cast<T>(1.0) - static_cast<T>(0.1) * spread;
|
||||
// spread=0 → decay=1.0 (no decay), spread=1 → decay=0.9
|
||||
|
||||
for (size_t n = 0; n < m_layers.size(); n++) {
|
||||
const size_t fanIn = m_layers_nodes[n];
|
||||
const T xavierScale = static_cast<T>(1.0) / std::sqrt(static_cast<T>(fanIn));
|
||||
const T layerScale = static_cast<T>(1.0) * (static_cast<T>(1.0) - spread) + xavierScale * spread;
|
||||
|
||||
for (size_t k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
// Decay toward zero
|
||||
m_layers[n].m_nodes[k].m_weights[j] *= decay;
|
||||
|
||||
// Sum of 3 uniform randoms × kN_times(3) × speed × layerScale
|
||||
T accum = static_cast<T>(0);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
accum += static_cast<T>(rand()) / static_cast<T>(RAND_MAX) * static_cast<T>(2) - static_cast<T>(1);
|
||||
}
|
||||
m_layers[n].m_nodes[k].m_weights[j] += static_cast<T>(3) * accum * speed * layerScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::InitXavier() {
|
||||
for(auto & layer : m_layers) {
|
||||
layer.InitXavier();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::RandomiseWeightsAndBiasesLin(T weightMin, T weightMax, T biasMin, T biasMax) {
|
||||
std::uniform_real_distribution<> disWeight(weightMin, weightMax);
|
||||
std::uniform_real_distribution<> disBias(biasMin, biasMin);
|
||||
|
||||
// utils::gen_randn<T> gen(0.f, scale); //mean, stddev
|
||||
for (unsigned int n = 0; n < m_layers.size(); n++) {
|
||||
for (unsigned int k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (unsigned int j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
m_layers[n].m_nodes[k].m_weights[j] = disWeight(g);
|
||||
}
|
||||
m_layers[n].m_nodes[k].m_bias = disBias(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Explicit instantiations
|
||||
#if !defined(__XS3A__)
|
||||
template class MLP<double>;
|
||||
#endif
|
||||
template class MLP<float>;
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif // NISPS_MLP_IMPL_HPP
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#ifndef NISPS_HPP
|
||||
#define NISPS_HPP
|
||||
|
||||
#include "iml.hpp"
|
||||
|
||||
#endif // NISPS_HPP
|
||||
|
|
@ -1,477 +0,0 @@
|
|||
/**
|
||||
* @file node.hpp
|
||||
* @brief Neural network node implementation with weight management and activation functions
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
*/
|
||||
|
||||
#ifndef NISPS_NODE_HPP
|
||||
#define NISPS_NODE_HPP
|
||||
|
||||
#include "utils.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <cassert> // for assert()
|
||||
#include <numeric>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <span>
|
||||
#include <cstdio> // for FILE
|
||||
|
||||
#define CONSTANT_WEIGHT_INITIALIZATION 0
|
||||
|
||||
namespace nisps {
|
||||
|
||||
/**
|
||||
* @brief Definition of activation function pointer
|
||||
* @tparam T The numeric type used for calculations
|
||||
*/
|
||||
template<typename T>
|
||||
using activation_func_t = T(*)(T);
|
||||
|
||||
/**
|
||||
* @class Node
|
||||
* @brief Represents a single neural network node with weights and activation capabilities
|
||||
* @tparam T The numeric type used for calculations (typically float or double)
|
||||
*/
|
||||
template <typename T>
|
||||
class Node {
|
||||
public:
|
||||
/**
|
||||
* @brief Default constructor
|
||||
*/
|
||||
Node() {
|
||||
m_num_inputs = 0;
|
||||
m_bias = 0;
|
||||
m_weights.clear();
|
||||
squared_gradient_avg.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Constructor with initialization parameters
|
||||
* @param num_inputs Number of input connections to the node
|
||||
* @param use_constant_weight_init Flag to use constant weight initialization
|
||||
* @param constant_weight_init Value for constant weight initialization
|
||||
*/
|
||||
Node(int num_inputs,
|
||||
bool use_constant_weight_init = true,
|
||||
T constant_weight_init = 0.5) {
|
||||
m_num_inputs = num_inputs;
|
||||
m_bias = 0.0;
|
||||
m_weights.clear();
|
||||
//initialize weight vector
|
||||
WeightInitialization(m_num_inputs,
|
||||
use_constant_weight_init,
|
||||
constant_weight_init);
|
||||
};
|
||||
|
||||
~Node() {
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Initializes the node's weights
|
||||
* @param num_inputs Number of input connections
|
||||
* @param use_constant_weight_init Flag to use constant weight initialization
|
||||
* @param constant_weight_init Value for constant weight initialization
|
||||
*/
|
||||
void WeightInitialization(int num_inputs,
|
||||
bool use_constant_weight_init = true,
|
||||
T constant_weight_init = 0.5) {
|
||||
m_num_inputs = num_inputs;
|
||||
//initialize weight vector
|
||||
if (use_constant_weight_init) {
|
||||
m_weights.resize(m_num_inputs, constant_weight_init);
|
||||
} else {
|
||||
m_weights.resize(m_num_inputs);
|
||||
std::generate_n(m_weights.begin(),
|
||||
m_num_inputs,
|
||||
utils::gen_rand<T>());
|
||||
}
|
||||
squared_gradient_avg.resize(m_num_inputs);
|
||||
std::fill(squared_gradient_avg.begin(), squared_gradient_avg.end(), 0.f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Randomizes weights with Gaussian noise
|
||||
* @param variance The variance of the Gaussian distribution
|
||||
*/
|
||||
void WeightRandomisation(const float variance) {
|
||||
std::transform(m_weights.begin(),
|
||||
m_weights.end(),
|
||||
m_weights.begin(),
|
||||
utils::gen_randn<T>(variance));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize gradient accumulator
|
||||
*/
|
||||
void InitializeGradientAccumulator() {
|
||||
m_gradient_accumulator.clear();
|
||||
m_gradient_accumulator.resize(m_weights.size(), 0);
|
||||
m_bias_gradient_accumulator = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Accumulate gradients without updating weights
|
||||
* @param x Input vector
|
||||
* @param error Error signal from backpropagation
|
||||
* @param learning_rate Not used here, kept for compatibility
|
||||
*/
|
||||
inline void AccumulateGradients(std::span<const T> x,
|
||||
T error) {
|
||||
assert(x.size() == m_weights.size());
|
||||
for (size_t i = 0; i < m_weights.size(); i++) {
|
||||
m_gradient_accumulator[i] += x[i] * error;
|
||||
}
|
||||
m_bias_gradient_accumulator += error;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @brief Apply accumulated gradients and clear accumulator
|
||||
// * @param learning_rate Learning rate for weight update
|
||||
// * @param batch_size Size of the batch for averaging
|
||||
// */
|
||||
// inline void ApplyAccumulatedGradients(float learning_rate, T batch_size_inv) {
|
||||
// T scale = learning_rate * batch_size_inv;
|
||||
// for (size_t i = 0; i < m_weights.size(); i++) {
|
||||
// m_weights[i] -= m_gradient_accumulator[i] * scale;
|
||||
// m_gradient_accumulator[i] = 0; // Reset accumulator
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
static constexpr float rmsPropDecay = 0.9f;
|
||||
static constexpr float rmsPropDecayInv = 0.1f;
|
||||
static constexpr float rmsPropEpsilon = 1e-6f;
|
||||
|
||||
inline void ApplyAccumulatedGradients(float learning_rate, T batch_size_inv) {
|
||||
// Constants with proper type casting
|
||||
const T maxSquaredGradAvg = static_cast<T>(1e6); // Prevent unbounded accumulation
|
||||
const T maxAdjustedLR = static_cast<T>(1.0); // Cap learning rate adjustments
|
||||
const T gradientClipValue = static_cast<T>(10.0); // Gradient clipping threshold
|
||||
|
||||
for (size_t i = 0; i < m_weights.size(); i++) {
|
||||
T gradient = m_gradient_accumulator[i] * batch_size_inv;
|
||||
|
||||
// Clamp gradient to prevent extreme values before squaring
|
||||
gradient = std::max(std::min(gradient, gradientClipValue), -gradientClipValue);
|
||||
|
||||
squared_gradient_avg[i] = (rmsPropDecay * squared_gradient_avg[i]) +
|
||||
(rmsPropDecayInv * gradient * gradient);
|
||||
|
||||
// Clamp squared gradient average to prevent unbounded growth
|
||||
squared_gradient_avg[i] = std::min(squared_gradient_avg[i], maxSquaredGradAvg);
|
||||
|
||||
T adjusted_learning_rate = static_cast<T>(learning_rate) /
|
||||
(std::sqrt(squared_gradient_avg[i]) + static_cast<T>(rmsPropEpsilon));
|
||||
|
||||
// Clamp adjusted learning rate to prevent extreme updates
|
||||
adjusted_learning_rate = std::min(adjusted_learning_rate, maxAdjustedLR);
|
||||
|
||||
m_weights[i] -= adjusted_learning_rate * gradient;
|
||||
|
||||
m_gradient_accumulator[i] = 0.f; // Reset accumulator
|
||||
}
|
||||
T bias_gradient = m_bias_gradient_accumulator * batch_size_inv;
|
||||
|
||||
// Clamp bias gradient
|
||||
bias_gradient = std::max(std::min(bias_gradient, gradientClipValue), -gradientClipValue);
|
||||
|
||||
bias_squared_gradient_avg = (rmsPropDecay * bias_squared_gradient_avg) +
|
||||
(rmsPropDecayInv * bias_gradient * bias_gradient);
|
||||
|
||||
// Clamp bias squared gradient average
|
||||
bias_squared_gradient_avg = std::min(bias_squared_gradient_avg, maxSquaredGradAvg);
|
||||
|
||||
T bias_adjusted_lr = static_cast<T>(learning_rate) / (std::sqrt(bias_squared_gradient_avg) + static_cast<T>(rmsPropEpsilon));
|
||||
|
||||
// Clamp bias adjusted learning rate
|
||||
bias_adjusted_lr = std::min(bias_adjusted_lr, maxAdjustedLR);
|
||||
|
||||
m_bias -= bias_adjusted_lr * bias_gradient;
|
||||
m_bias_gradient_accumulator = 0;
|
||||
// printf("Bias: %f\n", m_bias);
|
||||
}
|
||||
|
||||
|
||||
inline float GetGradSumSquared(T batch_size_inv) {
|
||||
T sumsq = 0;
|
||||
for (size_t i = 0; i < m_gradient_accumulator.size(); i++) {
|
||||
T scaledGrad = m_gradient_accumulator[i] * batch_size_inv;
|
||||
sumsq += scaledGrad*scaledGrad;
|
||||
}
|
||||
return sumsq;
|
||||
}
|
||||
|
||||
void ScaleAccumulatedGradients(T clip_coef) {
|
||||
for (size_t i = 0; i < m_gradient_accumulator.size(); i++) {
|
||||
m_gradient_accumulator[i] *= clip_coef;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset RMSProp optimizer state (useful for recovery from numerical issues)
|
||||
*/
|
||||
inline void ResetOptimizerState() {
|
||||
std::fill(squared_gradient_avg.begin(), squared_gradient_avg.end(), static_cast<T>(0.0));
|
||||
bias_squared_gradient_avg = static_cast<T>(0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check for and fix NaN/Inf in weights (returns true if corruption detected)
|
||||
*/
|
||||
inline bool CheckAndFixWeights() {
|
||||
bool had_corruption = false;
|
||||
for (size_t i = 0; i < m_weights.size(); i++) {
|
||||
if (std::isinf(m_weights[i]) || std::isnan(m_weights[i])) {
|
||||
m_weights[i] = static_cast<T>(0.0); // Reset corrupted weight
|
||||
squared_gradient_avg[i] = static_cast<T>(0.0); // Reset its optimizer state
|
||||
had_corruption = true;
|
||||
}
|
||||
}
|
||||
if (std::isinf(m_bias) || std::isnan(m_bias)) {
|
||||
m_bias = static_cast<T>(0.0);
|
||||
bias_squared_gradient_avg = static_cast<T>(0.0);
|
||||
had_corruption = true;
|
||||
}
|
||||
return had_corruption;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear gradient accumulator
|
||||
*/
|
||||
inline void ClearGradientAccumulator() {
|
||||
std::fill(m_gradient_accumulator.begin(), m_gradient_accumulator.end(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the number of inputs to this node
|
||||
* @return Number of inputs
|
||||
*/
|
||||
int GetInputSize() const {
|
||||
return m_num_inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the number of inputs to this node
|
||||
* @param num_inputs New number of inputs
|
||||
*/
|
||||
void SetInputSize(int num_inputs) {
|
||||
m_num_inputs = num_inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the node's bias value
|
||||
* @return Current bias value
|
||||
*/
|
||||
T GetBias() const {
|
||||
return m_bias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the node's bias value
|
||||
* @param bias New bias value
|
||||
*/
|
||||
void SetBias(T bias) {
|
||||
m_bias = bias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets reference to the weight vector
|
||||
* @return Reference to weights vector
|
||||
*/
|
||||
std::vector<T> & GetWeights() {
|
||||
return m_weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets const reference to the weight vector
|
||||
* @return Const reference to weights vector
|
||||
*/
|
||||
const std::vector<T> & GetWeights() const {
|
||||
return m_weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets new weights for the node
|
||||
* @param weights Vector of new weights
|
||||
*/
|
||||
void SetWeights( std::span<T> weights ){
|
||||
// check size of the weights vector
|
||||
assert(weights.size() == m_num_inputs);
|
||||
// m_weights = weights;
|
||||
m_weights.assign(weights.begin(), weights.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Updates weights using exponential moving average
|
||||
* @param incomingWeights New weights to blend with current weights
|
||||
* @param alpha Learning rate for new weights
|
||||
* @param alphaInv Learning rate for current weights (typically 1-alpha)
|
||||
*/
|
||||
inline void SmoothUpdateWeights(std::span<T> incomingWeights, const float alpha, const float alphaInv) {
|
||||
assert(incomingWeights.size() == m_weights.size());
|
||||
for(size_t i = 0; i < m_weights.size(); i++) {
|
||||
m_weights[i] = (alphaInv * m_weights[i]) + (alpha * incomingWeights[i]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the size of the weights vector
|
||||
* @return Number of weights
|
||||
*/
|
||||
inline size_t GetWeightsVectorSize() const {
|
||||
return m_weights.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes inner product of input with weights
|
||||
* @param input Vector of input values
|
||||
* @return Inner product result
|
||||
*/
|
||||
inline T GetInputInnerProdWithWeights(std::span<const T> input) {
|
||||
T res = 0;
|
||||
|
||||
for(size_t j=0; j < input.size(); j++) {
|
||||
res += input[j] * m_weights[j];
|
||||
}
|
||||
|
||||
res += m_bias;
|
||||
inner_prod = res;
|
||||
|
||||
return inner_prod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes node output using specified activation function
|
||||
* @param input Input vector
|
||||
* @param activation_function Activation function to use
|
||||
* @param output Pointer to store the output value
|
||||
*/
|
||||
inline void GetOutputAfterActivationFunction(std::span<const T> input,
|
||||
MLP_ACTIVATION_FN activation_func_t<T> activation_function,
|
||||
T * output) {
|
||||
// T inner_prod = 0.0;
|
||||
GetInputInnerProdWithWeights(input);
|
||||
*output = activation_function(inner_prod);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes binary output based on activation threshold
|
||||
* @param input Input vector
|
||||
* @param activation_function Activation function to use
|
||||
* @param bool_output Pointer to store the binary output
|
||||
* @param threshold Threshold value for binary decision
|
||||
*/
|
||||
void GetBooleanOutput(std::vector<const T> input,
|
||||
MLP_ACTIVATION_FN activation_func_t<T> activation_function,
|
||||
bool * bool_output,
|
||||
T threshold = 0.5) {
|
||||
T value;
|
||||
GetOutputAfterActivationFunction(input, activation_function, &value);
|
||||
*bool_output = (value > threshold) ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Updates weights based on error and learning rate
|
||||
* @param x Input vector
|
||||
* @param error Error value
|
||||
* @param learning_rate Learning rate for weight update
|
||||
*/
|
||||
inline void UpdateWeights(std::span<const T> x,
|
||||
T error,
|
||||
T learning_rate) {
|
||||
assert(x.size() == m_weights.size());
|
||||
for (size_t i = 0; i < m_weights.size(); i++)
|
||||
m_weights[i] += x[i] * learning_rate * error;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Updates a single weight
|
||||
* @param weight_id Index of weight to update
|
||||
* @param increment Amount to increment the weight
|
||||
* @param learning_rate Learning rate for weight update
|
||||
*/
|
||||
inline void UpdateWeight(int weight_id,
|
||||
float increment,
|
||||
float learning_rate) {
|
||||
m_weights[weight_id] += static_cast<T>(learning_rate*increment);
|
||||
}
|
||||
|
||||
size_t m_num_inputs{ 0 }; /**< Number of inputs to this node */
|
||||
T m_bias{ 0.0 }; /**< Bias value for this node */
|
||||
std::vector<T> m_weights; /**< Vector of input weights */
|
||||
|
||||
/**
|
||||
* @brief Saves node state to file
|
||||
* @param file File pointer for saving
|
||||
* @return true if save was successful, false if there was an error
|
||||
*/
|
||||
bool SaveNode(FILE * file) const {
|
||||
if (fwrite(&m_num_inputs, sizeof(m_num_inputs), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (fwrite(&m_bias, sizeof(m_bias), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (!m_weights.empty()) {
|
||||
if (fwrite(&m_weights[0], sizeof(m_weights[0]), m_weights.size(), file) != m_weights.size()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Loads node state from file
|
||||
* @param file File pointer for loading
|
||||
* @return true if load was successful, false if there was an error
|
||||
*/
|
||||
bool LoadNode(FILE * file) {
|
||||
m_weights.clear();
|
||||
|
||||
if (fread(&m_num_inputs, sizeof(m_num_inputs), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (fread(&m_bias, sizeof(m_bias), 1, file) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_weights.resize(m_num_inputs);
|
||||
if (!m_weights.empty()) {
|
||||
if (fread(&m_weights[0], sizeof(m_weights[0]), m_weights.size(), file) != m_weights.size()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
squared_gradient_avg.resize(m_num_inputs);
|
||||
std::fill(squared_gradient_avg.begin(), squared_gradient_avg.end(), 0.f);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Accumulated gradients for batch training
|
||||
*/
|
||||
std::vector<T> m_gradient_accumulator;
|
||||
std::vector<T> squared_gradient_avg;
|
||||
T m_bias_gradient_accumulator{0};
|
||||
T bias_squared_gradient_avg=0;
|
||||
|
||||
inline T GetInnerProd() const {
|
||||
return inner_prod;
|
||||
}
|
||||
private:
|
||||
Node<T>& operator=(Node<T> const &) = delete; /**< Deleted assignment operator */
|
||||
T inner_prod; /**< Cached inner product value */
|
||||
};
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif //NISPS_NODE_HPP
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
/**
|
||||
* @file sample.hpp
|
||||
* @brief Sample and TrainingSample class definitions for NISPS Core
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
*/
|
||||
|
||||
#ifndef NISPS_SAMPLE_HPP
|
||||
#define NISPS_SAMPLE_HPP
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <vector>
|
||||
|
||||
#if defined(MLP_DEBUG_BUILD)
|
||||
#include <iostream>
|
||||
#endif
|
||||
|
||||
namespace nisps {
|
||||
|
||||
/**
|
||||
* @brief Base class representing a sample with input features
|
||||
*
|
||||
* @tparam T The data type of the input features (typically float)
|
||||
*/
|
||||
template<typename T>
|
||||
class Sample {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a new Sample object
|
||||
*
|
||||
* @param input_vector Vector containing the input features
|
||||
*/
|
||||
Sample(const std::vector<T> & input_vector) {
|
||||
m_input_vector = input_vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the input vector
|
||||
* @return const reference to the input vector
|
||||
*/
|
||||
const std::vector<T> & input_vector() const {
|
||||
return m_input_vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the size of the input vector
|
||||
* @return Size of input vector
|
||||
*/
|
||||
size_t GetInputVectorSize() const {
|
||||
return m_input_vector.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a bias value to the beginning of input vector
|
||||
* @param bias_value The bias value to add
|
||||
*/
|
||||
void AddBiasValue(T bias_value) {
|
||||
m_input_vector.insert(m_input_vector.begin(), bias_value);
|
||||
}
|
||||
|
||||
#if defined(MLP_DEBUG_BUILD)
|
||||
friend std::ostream & operator<<(std::ostream &stream, Sample const & obj) {
|
||||
obj.PrintMyself(stream);
|
||||
return stream;
|
||||
};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#if defined(MLP_DEBUG_BUILD)
|
||||
virtual void PrintMyself(std::ostream& stream) const {
|
||||
stream << "Input vector: [";
|
||||
for (size_t i = 0; i < m_input_vector.size(); i++) {
|
||||
if (i != 0)
|
||||
stream << ", ";
|
||||
stream << m_input_vector[i];
|
||||
}
|
||||
stream << "]";
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<T> m_input_vector;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Class representing a training sample with both input features and expected outputs
|
||||
*
|
||||
* Extends the base Sample class to include output/target values for training
|
||||
*
|
||||
* @tparam T The data type of the input/output values (typically float)
|
||||
*/
|
||||
template<typename T>
|
||||
class TrainingSample : public Sample<T> {
|
||||
using Sample<T>::m_input_vector;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a new Training Sample object
|
||||
*
|
||||
* @param input_vector Vector containing the input features
|
||||
* @param output_vector Vector containing the expected outputs/targets
|
||||
*/
|
||||
TrainingSample(const std::vector<T> & input_vector,
|
||||
const std::vector<T> & output_vector) :
|
||||
Sample<T>(input_vector) {
|
||||
m_output_vector = output_vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the output vector
|
||||
* @return const reference to the output vector
|
||||
*/
|
||||
const std::vector<T> & output_vector() const {
|
||||
return m_output_vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the size of the output vector
|
||||
* @return Size of output vector
|
||||
*/
|
||||
size_t GetOutputVectorSize() const {
|
||||
return m_output_vector.size();
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
#if defined(MLP_DEBUG_BUILD)
|
||||
virtual void PrintMyself(std::ostream& stream) const {
|
||||
stream << "Input vector: [";
|
||||
for (size_t i = 0; i < m_input_vector.size(); i++) {
|
||||
if (i != 0)
|
||||
stream << ", ";
|
||||
stream << m_input_vector[i];
|
||||
}
|
||||
stream << "]";
|
||||
|
||||
stream << "; ";
|
||||
|
||||
stream << "Output vector: [";
|
||||
for (size_t i = 0; i < m_output_vector.size(); i++) {
|
||||
if (i != 0)
|
||||
stream << ", ";
|
||||
stream << m_output_vector[i];
|
||||
}
|
||||
stream << "]";
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<T> m_output_vector;
|
||||
};
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif // NISPS_SAMPLE_HPP
|
||||
|
|
@ -1,444 +0,0 @@
|
|||
/**
|
||||
* @file Utils.h
|
||||
* @brief Utility functions and structures for machine learning operations
|
||||
* @copyright Copyright (c) 2024. Licensed under Mozilla Public License Version 2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* This code is derived from David Alberto Nogueira's MLP project:
|
||||
* https://github.com/davidalbertonogueira/MLP
|
||||
*/
|
||||
|
||||
#ifndef NISPS_UTILS_HPP
|
||||
#define NISPS_UTILS_HPP
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
/**
|
||||
* @enum ACTIVATION_FUNCTIONS
|
||||
* @brief Enumeration of supported activation functions.
|
||||
*/
|
||||
enum ACTIVATION_FUNCTIONS {
|
||||
SIGMOID, /**< Sigmoid activation function */
|
||||
TANH, /**< Hyperbolic tangent activation function */
|
||||
LINEAR, /**< Linear activation function */
|
||||
RELU, /**< Rectified Linear Unit (ReLU) activation function */
|
||||
// LEAKY_RELU /**< Leaky ReLU activation function */
|
||||
HARDSIGMOID,
|
||||
HARDSWISH,
|
||||
HARDTANH
|
||||
};
|
||||
|
||||
#define MLP_ACTIVATION_FN
|
||||
|
||||
/**
|
||||
* @namespace utils
|
||||
* @brief Contains utility functions and structures for machine learning.
|
||||
*/
|
||||
namespace utils {
|
||||
|
||||
/**
|
||||
* @brief Computes the sigmoid of a value.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The sigmoid of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T sigmoid(T x) {
|
||||
return 1 / (1 + std::exp(-x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the sigmoid function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the sigmoid function at the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T deriv_sigmoid(T x) {
|
||||
return sigmoid(x)*((T)1 - sigmoid(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the hyperbolic tangent of a value.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The hyperbolic tangent of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T hyperbolic_tan(T x) {
|
||||
return (std::tanh)(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the hyperbolic tangent function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the hyperbolic tangent function at the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T deriv_hyperbolic_tan(T x) {
|
||||
return (T)1 - (std::pow)(hyperbolic_tan(x), (T)2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the linear function of a value.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The linear function of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T linear(T x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the linear function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the linear function.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T deriv_linear(T) {
|
||||
return static_cast<T>(1);
|
||||
}
|
||||
|
||||
static const float kReLUSlope = 0.01f;
|
||||
|
||||
/**
|
||||
* @brief Computes the ReLU function of a value.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The ReLU function of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T relu(T x) {
|
||||
return (x > (T)0) ? (T)x : kReLUSlope * x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the ReLU function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the ReLU function.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T deriv_relu(T x) {
|
||||
return (x > (T)0) ? (T)1 : kReLUSlope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the Hard Sigmoid activation function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The Hard Sigmoid of x: clip((x + 3) / 6, 0, 1)
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T hardsigmoid(T x) {
|
||||
constexpr T oneOverSix = (T)1/(T)6;
|
||||
if (x <= (T)-3) return (T)0;
|
||||
if (x >= (T)3) return (T)1;
|
||||
return (x + (T)3) * oneOverSix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the Hard Sigmoid function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the Hard Sigmoid function.
|
||||
*/
|
||||
template<typename T>
|
||||
// MLP_ACTIVATION_FN
|
||||
inline T deriv_hardsigmoid(T x) {
|
||||
constexpr T oneOverSix = (T)1/(T)6;
|
||||
return (x > (T)-3 && x < (T)3) ? oneOverSix : (T)0;
|
||||
}
|
||||
/**
|
||||
* @brief Computes the Hard Tanh activation function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The Hard Tanh of x: clip(x, -1, 1)
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T hardtanh(T x) {
|
||||
if (x <= (T)-1) return (T)-1;
|
||||
if (x >= (T)1) return (T)1;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the Hard Tanh function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the Hard Tanh function.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T deriv_hardtanh(T x) {
|
||||
return (x > (T)-1 && x < (T)1) ? (T)1 : (T)0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the Hard Swish activation function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The Hard Swish of x: x * hardsigmoid(x)
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T hardswish(T x) {
|
||||
if (x <= (T)-3) return (T)0;
|
||||
if (x >= (T)3) return x;
|
||||
return x * (x + (T)3) / (T)6;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the derivative of the Hard Swish function.
|
||||
* @tparam T The type of the input value.
|
||||
* @param x The input value.
|
||||
* @return The derivative of the Hard Swish function.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T deriv_hardswish(T x) {
|
||||
if (x <= (T)-3) return (T)0;
|
||||
if (x >= (T)3) return (T)1;
|
||||
return ((T)2 * x + (T)3) / (T)6;
|
||||
}
|
||||
/**
|
||||
* @brief Computes the sign of a value.
|
||||
* @tparam T The type of the input value.
|
||||
* @param val The input value.
|
||||
* @return The sign of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline T sgn(T val) {
|
||||
return static_cast<T>( (T(0) < val) - (val < T(0)) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef activation_func_t
|
||||
* @brief Type definition for activation function pointers.
|
||||
* @tparam T The type of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
using activation_func_t = T(*)(T);
|
||||
|
||||
/**
|
||||
* @struct ActivationFunctionsManager
|
||||
* @brief Manages activation functions and their derivatives.
|
||||
* @tparam T The type of the input value.
|
||||
*/
|
||||
template<typename T>
|
||||
struct ActivationFunctionsManager {
|
||||
/**
|
||||
* @brief Retrieves the activation function pair for a given activation name.
|
||||
* @param activation_name The name of the activation function.
|
||||
* @param pair Pointer to the activation function pair.
|
||||
* @return True if the activation function pair is found, false otherwise.
|
||||
*/
|
||||
bool GetActivationFunctionPair(const ACTIVATION_FUNCTIONS & activation_name,
|
||||
std::pair<activation_func_t<T>,
|
||||
activation_func_t<T>> **pair) {
|
||||
auto iter = activation_functions_map.find(activation_name);
|
||||
if (iter != activation_functions_map.end())
|
||||
*pair = &(iter->second);
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieves the singleton instance of ActivationFunctionsManager.
|
||||
* @return The singleton instance.
|
||||
*/
|
||||
static ActivationFunctionsManager & Singleton() {
|
||||
static ActivationFunctionsManager instance;
|
||||
return instance;
|
||||
}
|
||||
private:
|
||||
/**
|
||||
* @brief Adds a new activation function pair to the manager.
|
||||
* @param function_name The name of the activation function.
|
||||
* @param function The activation function.
|
||||
* @param deriv_function The derivative of the activation function.
|
||||
*/
|
||||
void AddNewPair(ACTIVATION_FUNCTIONS function_name,
|
||||
activation_func_t<T> function,
|
||||
activation_func_t<T> deriv_function) {
|
||||
activation_functions_map.insert(std::make_pair(function_name,
|
||||
std::make_pair(function,
|
||||
deriv_function)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructor for ActivationFunctionsManager.
|
||||
*/
|
||||
ActivationFunctionsManager() {
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::SIGMOID, &sigmoid<T>, &deriv_sigmoid<T>);
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::TANH, &hyperbolic_tan<T>, &deriv_hyperbolic_tan<T>);
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::LINEAR, &linear<T>, &deriv_linear<T>);
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::RELU, &relu<T>, &deriv_relu<T>);
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::HARDSIGMOID, &hardsigmoid<T>, &deriv_hardsigmoid<T>);
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::HARDSWISH, &hardswish<T>, &deriv_hardswish<T>);
|
||||
AddNewPair(ACTIVATION_FUNCTIONS::HARDTANH, &hardtanh<T>, &deriv_hardtanh<T>);
|
||||
}
|
||||
|
||||
std::unordered_map<
|
||||
ACTIVATION_FUNCTIONS,
|
||||
std::pair< activation_func_t<T>, activation_func_t<T> >
|
||||
> activation_functions_map;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct gen_rand
|
||||
* @brief Generates random numbers in a uniform distribution.
|
||||
* @tparam T The type of the generated random numbers.
|
||||
*/
|
||||
template<typename T>
|
||||
struct gen_rand {
|
||||
T factor; /**< Scaling factor for random number generation. */
|
||||
T offset; /**< Offset for random number generation. */
|
||||
|
||||
/**
|
||||
* @brief Constructor for gen_rand.
|
||||
* @param r The range of the random numbers.
|
||||
*/
|
||||
gen_rand(T r = 2.0) : factor(r / static_cast<T>(RAND_MAX)), offset(r * 0.5) {}
|
||||
|
||||
/**
|
||||
* @brief Generates a random number.
|
||||
* @return A random number in the range [-offset, offset].
|
||||
*/
|
||||
T operator()() {
|
||||
return static_cast<T>(rand()) * factor - offset;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct gen_randn
|
||||
* @brief Generates random numbers in a normal distribution.
|
||||
* @tparam T The type of the generated random numbers.
|
||||
*/
|
||||
template<typename T>
|
||||
struct gen_randn {
|
||||
T mean_; /**< Mean of the normal distribution. */
|
||||
T stddev_; /**< Standard deviation of the normal distribution. */
|
||||
gen_rand<T> gen_; /**< Uniform random number generator. */
|
||||
|
||||
/**
|
||||
* @brief Constructor for gen_randn.
|
||||
* @param stddev The standard deviation of the normal distribution.
|
||||
* @param mean The mean of the normal distribution.
|
||||
*/
|
||||
gen_randn(T stddev, T mean = 0) : mean_(mean), stddev_(stddev) {}
|
||||
|
||||
/**
|
||||
* @brief Sets the mean of the normal distribution.
|
||||
* @param mean The mean to set.
|
||||
*/
|
||||
inline void SetMean(T mean) { mean_ = mean; }
|
||||
|
||||
/**
|
||||
* @brief Generates a random number with the current mean.
|
||||
* @return A random number in the normal distribution.
|
||||
*/
|
||||
inline T operator()() {
|
||||
return operator()(mean_);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generates a random number with a specified mean.
|
||||
* @param mean The mean to use for generation.
|
||||
* @return A random number in the normal distribution.
|
||||
*/
|
||||
inline T operator()(T mean) {
|
||||
T accum = 0;
|
||||
static const unsigned int kN_times = 3;
|
||||
for (unsigned int n = 0; n < kN_times; n++) {
|
||||
accum += gen_();
|
||||
}
|
||||
return kN_times*(accum) * stddev_ + mean;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Applies the softmax function to a vector.
|
||||
* @tparam T The type of the elements in the vector.
|
||||
* @param output Pointer to the vector to apply softmax to.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline void Softmax(std::vector<T> *output) {
|
||||
size_t num_elements = output->size();
|
||||
std::vector<T> exp_output(num_elements);
|
||||
T exp_total = 0;
|
||||
for (size_t i = 0; i < num_elements; i++) {
|
||||
float output_i = (*output)[i];
|
||||
if (output_i > 15.f) {
|
||||
output_i = 15.f;
|
||||
} else if (output_i < -15.f) {
|
||||
output_i = -15.f;
|
||||
}
|
||||
exp_output[i] = std::exp((*output)[i]);
|
||||
exp_total += exp_output[i];
|
||||
}
|
||||
for (size_t i = 0; i < num_elements; i++) {
|
||||
(*output)[i] = exp_output[i] / exp_total;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds the index of the maximum element in a vector.
|
||||
* @tparam T The type of the elements in the vector.
|
||||
* @param output The vector to search.
|
||||
* @param class_id Pointer to store the index of the maximum element.
|
||||
*/
|
||||
template<typename T>
|
||||
MLP_ACTIVATION_FN
|
||||
inline void GetIdMaxElement(const std::vector<T> &output, size_t * class_id) {
|
||||
*class_id = std::distance(output.begin(),
|
||||
std::max_element(output.begin(),
|
||||
output.end()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if two values are approximately equal.
|
||||
* @tparam T The type of the values.
|
||||
* @param a The first value.
|
||||
* @param b The second value.
|
||||
* @return True if the values are approximately equal, false otherwise.
|
||||
*/
|
||||
template<typename T>
|
||||
inline bool is_close(T a, T b) {
|
||||
static const T kRelTolerance = 0.0001;
|
||||
a = std::abs(a);
|
||||
b = std::abs(b);
|
||||
T abs_tolerance = b*kRelTolerance;
|
||||
return (a < b + abs_tolerance) && (a > b - abs_tolerance);
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif // NISPS_UTILS_HPP
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
add_executable(nisps_test main.cpp)
|
||||
target_link_libraries(nisps_test PRIVATE nisps)
|
||||
add_test(NAME nisps_test COMMAND nisps_test)
|
||||
|
|
@ -1,480 +0,0 @@
|
|||
#include <nisps/nisps.hpp>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
|
||||
void log_callback(const char* msg) {
|
||||
std::cout << " [nisps] " << msg << "\n";
|
||||
}
|
||||
|
||||
bool test_construction_and_inference() {
|
||||
std::cout << "--- Test: Construction and inference ---\n";
|
||||
|
||||
nisps::IML<float> iml(2, 1, {4, 4}, 1000, 1.0f, 0.0001f);
|
||||
iml.set_logger(log_callback);
|
||||
|
||||
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);
|
||||
|
||||
// Add a single example programmatically
|
||||
float in[] = {0.0f, 0.0f};
|
||||
float out[] = {0.0f};
|
||||
iml.add_example(in, 2, out, 1);
|
||||
|
||||
// Switch to inference (triggers training)
|
||||
iml.set_mode(nisps::IML<float>::Mode::Inference);
|
||||
|
||||
// Should not crash, training on 1 example
|
||||
iml.set_input(0, 0.0f);
|
||||
iml.set_input(1, 0.0f);
|
||||
iml.process();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool test_draw_weights_spread_zero() {
|
||||
std::cout << "--- Test: DrawWeightsSpread(0) — uniform [-1, 1] ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
mlp.DrawWeightsSpread(0.0f);
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
// Check bias is 0
|
||||
if (std::abs(mlp.m_layers[l].m_nodes[k].m_bias) > 1e-6f) {
|
||||
std::cerr << "FAIL: Bias not zero at layer " << l << " node " << k
|
||||
<< " (got " << mlp.m_layers[l].m_nodes[k].m_bias << ")\n";
|
||||
return false;
|
||||
}
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::isnan(w) || std::isinf(w)) {
|
||||
std::cerr << "FAIL: NaN/Inf weight at layer " << l << " node " << k << " weight " << j << "\n";
|
||||
return false;
|
||||
}
|
||||
if (w < -1.0f || w > 1.0f) {
|
||||
std::cerr << "FAIL: Weight " << w << " outside [-1, 1] at layer " << l
|
||||
<< " node " << k << " weight " << j << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_draw_weights_spread_one() {
|
||||
std::cout << "--- Test: DrawWeightsSpread(1) — Xavier-scaled weights ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
mlp.DrawWeightsSpread(1.0f);
|
||||
|
||||
// Layer 0: fan_in=3, xavier=1/sqrt(3)≈0.577
|
||||
// Layer 1: fan_in=8, xavier=1/sqrt(8)≈0.354
|
||||
float expected_xavier[] = {
|
||||
1.0f / std::sqrt(3.0f), // layer 0
|
||||
1.0f / std::sqrt(8.0f) // layer 1
|
||||
};
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
float max_abs = 0.0f;
|
||||
float xavier = expected_xavier[l];
|
||||
float limit = xavier * 1.1f;
|
||||
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
// Check bias is 0
|
||||
if (std::abs(mlp.m_layers[l].m_nodes[k].m_bias) > 1e-6f) {
|
||||
std::cerr << "FAIL: Bias not zero at layer " << l << " node " << k << "\n";
|
||||
return false;
|
||||
}
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
float aw = std::abs(w);
|
||||
if (aw > max_abs) max_abs = aw;
|
||||
if (aw > limit) {
|
||||
std::cerr << "FAIL: Weight " << w << " exceeds xavier limit " << limit
|
||||
<< " at layer " << l << " node " << k << " weight " << j << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " Layer " << l << ": xavier=" << xavier
|
||||
<< ", limit=" << limit << ", max|w|=" << max_abs << "\n";
|
||||
}
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_move_weights_spread_decay() {
|
||||
std::cout << "--- Test: MoveWeightsSpread decay (speed=0, spread=1) ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
|
||||
// Set all weights to 1.0 via SetWeights
|
||||
auto weights = mlp.GetWeights();
|
||||
for (auto& layer_w : weights) {
|
||||
for (auto& node_w : layer_w) {
|
||||
for (auto& w : node_w) {
|
||||
w = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
mlp.SetWeights(weights);
|
||||
|
||||
// Call with speed=0 (no noise), spread=1 (full decay: multiply by 0.9)
|
||||
mlp.MoveWeightsSpread(0.0f, 1.0f);
|
||||
|
||||
// Verify weights are approximately 0.9
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::abs(w - 0.9f) > 0.01f) {
|
||||
std::cerr << "FAIL: After first decay, weight=" << w
|
||||
<< " (expected ~0.9) at layer " << l << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " After 1st call: weights ~0.9 (OK)\n";
|
||||
|
||||
// Call again: 0.9 * 0.9 = 0.81
|
||||
mlp.MoveWeightsSpread(0.0f, 1.0f);
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::abs(w - 0.81f) > 0.01f) {
|
||||
std::cerr << "FAIL: After second decay, weight=" << w
|
||||
<< " (expected ~0.81) at layer " << l << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " After 2nd call: weights ~0.81 (OK)\n";
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_move_weights_spread_no_decay() {
|
||||
std::cout << "--- Test: MoveWeightsSpread no decay (speed=0, spread=0) ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
|
||||
// Set all weights to 1.0
|
||||
auto weights = mlp.GetWeights();
|
||||
for (auto& layer_w : weights) {
|
||||
for (auto& node_w : layer_w) {
|
||||
for (auto& w : node_w) {
|
||||
w = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
mlp.SetWeights(weights);
|
||||
|
||||
// speed=0, spread=0 → no noise, no decay
|
||||
mlp.MoveWeightsSpread(0.0f, 0.0f);
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::abs(w - 1.0f) > 1e-6f) {
|
||||
std::cerr << "FAIL: Weight changed to " << w
|
||||
<< " (expected 1.0) at layer " << l << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " All weights still 1.0 (OK)\n";
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_iml_spread_api() {
|
||||
std::cout << "--- Test: IML spread API (randomise_weights / move_weights) ---\n";
|
||||
|
||||
nisps::IML<float> iml(2, 4, {8});
|
||||
iml.set_logger(log_callback);
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// randomise_weights with spread should not crash
|
||||
iml.randomise_weights(0.5f);
|
||||
|
||||
// Set inputs and process
|
||||
iml.set_input(0, 0.3f);
|
||||
iml.set_input(1, 0.7f);
|
||||
iml.process();
|
||||
|
||||
const float* out_before = iml.get_outputs();
|
||||
float saved[4];
|
||||
for (int i = 0; i < 4; i++) saved[i] = out_before[i];
|
||||
|
||||
// move_weights with spread should not crash and should change outputs
|
||||
iml.move_weights(0.1f, 0.5f);
|
||||
iml.process();
|
||||
|
||||
const float* out_after = iml.get_outputs();
|
||||
|
||||
bool any_changed = false;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (std::isnan(out_after[i]) || std::isinf(out_after[i])) {
|
||||
std::cerr << "FAIL: Output " << i << " is NaN/Inf\n";
|
||||
return false;
|
||||
}
|
||||
if (out_after[i] < 0.0f || out_after[i] > 1.0f) {
|
||||
std::cerr << "FAIL: Output " << i << " = " << out_after[i] << " outside [0, 1]\n";
|
||||
return false;
|
||||
}
|
||||
if (std::abs(out_after[i] - saved[i]) > 1e-6f) {
|
||||
any_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!any_changed) {
|
||||
std::cerr << "FAIL: move_weights did not change any outputs\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << " Outputs valid and changed after move_weights\n";
|
||||
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());
|
||||
run(test_draw_weights_spread_zero());
|
||||
run(test_draw_weights_spread_one());
|
||||
run(test_move_weights_spread_decay());
|
||||
run(test_move_weights_spread_no_decay());
|
||||
run(test_iml_spread_api());
|
||||
|
||||
std::cout << "=== Results: " << passed << " passed, " << failed << " failed ===\n\n";
|
||||
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
Loading…
Reference in a new issue