@astermind/astermind-premium
Version:
Astermind Premium - Premium ML Toolkit
205 lines • 7.09 kB
JavaScript
// forgetting-online-elm.ts — Forgetting Online ELM with time-decay for concept drift
// Handles concept drift by decaying old samples over time
import { requireLicense } from '../core/license.js';
// Import OnlineELM directly - now that we're using ES modules, this works!
import { OnlineELM } from '@astermind/astermind-elm';
/**
* Forgetting Online ELM with time-decay for concept drift
* Features:
* - Exponential decay of old samples
* - Time-based or sample-based forgetting
* - Sliding window for memory efficiency
* - Handles concept drift automatically
*/
export class ForgettingOnlineELM {
constructor(options) {
this.elm = null;
this.samples = [];
this.trained = false;
this.currentTime = 0;
requireLicense(); // Premium feature - requires valid license
this.categories = options.categories;
this.options = {
categories: options.categories,
hiddenUnits: options.hiddenUnits ?? 256,
decayRate: options.decayRate ?? 0.99,
windowSize: options.windowSize ?? 1000,
timeBasedDecay: options.timeBasedDecay ?? false,
activation: options.activation ?? 'relu',
};
// inputDim will be set during first fit
// Note: OnlineELM will be initialized during fit() when we have inputDim
this.elm = null;
}
/**
* Initial training with batch data
*/
fit(X, y) {
const oneHotY = this._toOneHot(y);
// Store samples with timestamps
for (let i = 0; i < X.length; i++) {
this.samples.push({
x: [...X[i]],
y: [...oneHotY[i]],
timestamp: this.currentTime++,
weight: 1.0,
});
}
// Train on all samples (will initialize OnlineELM if needed)
this._retrain();
this.trained = true;
}
/**
* Incremental update with forgetting mechanism
*/
update(x, y) {
if (!this.trained) {
throw new Error('Model must be initially trained with fit() before incremental updates');
}
const oneHotY = Array.isArray(y)
? y
: (() => {
const oh = new Array(this.categories.length).fill(0);
oh[y] = 1;
return oh;
})();
// Apply decay to existing samples
this._applyDecay();
// Add new sample
this.samples.push({
x: [...x],
y: [...oneHotY],
timestamp: this.currentTime++,
weight: 1.0,
});
// Remove old samples if window exceeded
if (this.samples.length > this.options.windowSize) {
const removeCount = this.samples.length - this.options.windowSize;
this.samples.splice(0, removeCount);
}
// Retrain with weighted samples
this._retrain();
}
/**
* Predict with forgetting model
*/
predict(x, topK = 3) {
if (!this.trained) {
throw new Error('Model must be trained before prediction');
}
const XArray = Array.isArray(x[0]) ? x : [x];
const results = [];
for (const xi of XArray) {
const predVec = this.elm ? this.elm.predictLogitsFromVector(xi) : null;
if (!predVec)
continue;
// Convert to probabilities
const probs = this._softmax(Array.from(predVec));
// Get top-K
const indexed = [];
for (let idx = 0; idx < probs.length; idx++) {
indexed.push({
label: this.categories[idx],
prob: probs[idx],
index: idx,
});
}
indexed.sort((a, b) => b.prob - a.prob);
for (let i = 0; i < Math.min(topK, indexed.length); i++) {
results.push({
label: indexed[i].label,
prob: indexed[i].prob,
});
}
}
return results;
}
/**
* Apply decay to all samples
*/
_applyDecay() {
if (this.options.timeBasedDecay) {
// Time-based: decay based on time difference
const currentTime = this.currentTime;
for (const sample of this.samples) {
const timeDiff = currentTime - sample.timestamp;
sample.weight *= Math.pow(this.options.decayRate, timeDiff);
}
}
else {
// Sample-based: uniform decay
for (const sample of this.samples) {
sample.weight *= this.options.decayRate;
}
}
}
/**
* Retrain model with weighted samples
*/
_retrain() {
if (this.samples.length === 0)
return;
// Get inputDim from first sample
const inputDim = this.samples[0].x.length;
// Reinitialize ELM if inputDim changed or not set
if (!this.elm || (this.elm && this.elm.inputDim !== inputDim)) {
this.elm = new OnlineELM({
inputDim: inputDim,
outputDim: this.categories.length,
hiddenUnits: this.options.hiddenUnits,
activation: this.options.activation,
});
}
// Train with weighted samples
// In practice, you'd use weighted training
// For now, we'll use samples with weights above threshold
const threshold = 0.01;
const activeSamples = this.samples.filter(s => s.weight > threshold);
// Batch samples for efficiency
const X = [];
const Y = [];
for (const sample of activeSamples) {
// Repeat samples based on weight (simplified approach)
const repetitions = Math.max(1, Math.floor(sample.weight * 10));
for (let i = 0; i < repetitions; i++) {
X.push(sample.x);
Y.push(sample.y);
}
}
if (X.length > 0) {
this.elm.fit(X, Y);
}
}
_toOneHot(y) {
if (Array.isArray(y[0])) {
return y;
}
const labels = y;
return labels.map((label) => {
const oneHot = new Array(this.categories.length).fill(0);
oneHot[label] = 1;
return oneHot;
});
}
_softmax(logits) {
const max = Math.max(...logits);
const exp = logits.map(x => Math.exp(x - max));
const sum = exp.reduce((a, b) => a + b, 0);
return exp.map(x => x / sum);
}
/**
* Get sample statistics
*/
getSampleStats() {
const active = this.samples.filter(s => s.weight > 0.01).length;
const avgWeight = this.samples.length > 0
? this.samples.reduce((sum, s) => sum + s.weight, 0) / this.samples.length
: 0;
return {
total: this.samples.length,
active,
avgWeight,
};
}
}
//# sourceMappingURL=forgetting-online-elm.js.map