memlnaut-nisps/playground/js/nisps/dataset.js

122 lines
3.6 KiB
JavaScript
Raw Normal View History

// NISPS Dataset - faithful port of nisps-core/include/nisps/dataset.hpp
// Manages feature-label pairs for training
export class Dataset {
constructor(maxExamples = 100) {
this.features = [];
this.labels = [];
this.maxExamples = maxExamples;
}
add(feature, label) {
if (this.features.length > 0) {
if (feature.length !== this.features[0].length || label.length !== this.labels[0].length) {
return false;
}
}
if (this.features.length >= this.maxExamples) {
// FIFO: remove oldest
this.features.shift();
this.labels.shift();
}
this.features.push([...feature]);
this.labels.push([...label]);
return true;
}
clear() {
this.features = [];
this.labels = [];
}
getFeatures(withBias = true) {
return this.features.map(f => withBias ? [...f, 1.0] : [...f]);
}
getLabels() {
return this.labels;
}
/**
* Compute per-sample training weights. Returns Float32Array normalized to sum to 1.
* @param {'global'|'local'|'combined'} mode
* @param {object} params
* @param {number} params.recencyBias - 0 = uniform, 1 = strong recency (global/combined)
* @param {number[]} [params.queryInput] - current input position (local/combined)
* @param {number} [params.radius] - spatial radius in input space (local/combined), default 0.15
* @returns {Float32Array} weights summing to 1
*/
computeWeights(mode = 'global', params = {}) {
const n = this.features.length;
if (n === 0) return new Float32Array(0);
if (n === 1) return new Float32Array([1.0]);
const weights = new Float32Array(n).fill(1.0);
// Global recency: exponential decay — newest = 1, each older *= decay
if (mode === 'global' || mode === 'combined') {
const bias = params.recencyBias ?? 0.6;
if (bias > 0) {
// decay per step: at bias=1, decay=0.7 (newest ~10x oldest for 10 examples)
// at bias=0.5, decay=0.85 (gentler)
const decay = 1 - 0.3 * bias;
for (let i = n - 2; i >= 0; i--) {
weights[i] = weights[i + 1] * decay;
}
}
}
// Local recency: newer examples near the query suppress older nearby ones
if ((mode === 'local' || mode === 'combined') && params.queryInput) {
const query = params.queryInput;
const radius = params.radius ?? 0.15;
const radiusSq = radius * radius;
for (let i = 0; i < n; i++) {
const feat = this.features[i];
// Distance from this example to the query point
let distSq = 0;
for (let d = 0; d < feat.length; d++) {
const diff = feat[d] - (query[d] ?? 0);
distSq += diff * diff;
}
if (distSq < radiusSq) {
// Count newer examples also within the radius
const proximity = 1 - Math.sqrt(distSq) / radius; // 1 = on top, 0 = at edge
let newerNearby = 0;
for (let j = i + 1; j < n; j++) {
let djSq = 0;
for (let d = 0; d < feat.length; d++) {
const diff = feat[d] - this.features[j][d];
djSq += diff * diff;
}
if (djSq < radiusSq) newerNearby++;
}
// Suppress: more newer neighbors + closer to query = more suppression
if (newerNearby > 0) {
weights[i] *= Math.pow(1 - proximity, newerNearby);
}
}
}
}
// Normalize to sum to 1
let sum = 0;
for (let i = 0; i < n; i++) sum += weights[i];
if (sum > 0) {
for (let i = 0; i < n; i++) weights[i] /= sum;
}
return weights;
}
get size() {
return this.features.length;
}
isEmpty() {
return this.features.length === 0;
}
}