s3db.js 12.4.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Regression Model
3
+ *
4
+ * Linear and polynomial regression using TensorFlow.js
5
+ * Predicts continuous numerical values
6
+ */
7
+
8
+ import { BaseModel } from './base-model.class.js';
9
+ import { ModelConfigError } from '../ml.errors.js';
10
+
11
+ export class RegressionModel extends BaseModel {
12
+ constructor(config = {}) {
13
+ super(config);
14
+
15
+ // Regression-specific config
16
+ this.config.modelConfig = {
17
+ ...this.config.modelConfig,
18
+ polynomial: config.modelConfig?.polynomial || 1, // Degree (1 = linear, 2+ = polynomial)
19
+ units: config.modelConfig?.units || 64, // Hidden layer units for polynomial regression
20
+ activation: config.modelConfig?.activation || 'relu'
21
+ };
22
+
23
+ // Validate polynomial degree
24
+ if (this.config.modelConfig.polynomial < 1 || this.config.modelConfig.polynomial > 5) {
25
+ throw new ModelConfigError(
26
+ 'Polynomial degree must be between 1 and 5',
27
+ { model: this.config.name, polynomial: this.config.modelConfig.polynomial }
28
+ );
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Build regression model architecture
34
+ */
35
+ buildModel() {
36
+ const numFeatures = this.config.features.length;
37
+ const polynomial = this.config.modelConfig.polynomial;
38
+
39
+ // Create sequential model
40
+ this.model = this.tf.sequential();
41
+
42
+ if (polynomial === 1) {
43
+ // Linear regression: single dense layer
44
+ this.model.add(this.tf.layers.dense({
45
+ inputShape: [numFeatures],
46
+ units: 1,
47
+ useBias: true
48
+ }));
49
+ } else {
50
+ // Polynomial regression: hidden layer + output
51
+ this.model.add(this.tf.layers.dense({
52
+ inputShape: [numFeatures],
53
+ units: this.config.modelConfig.units,
54
+ activation: this.config.modelConfig.activation,
55
+ useBias: true
56
+ }));
57
+
58
+ // Additional hidden layer for higher degrees
59
+ if (polynomial >= 3) {
60
+ this.model.add(this.tf.layers.dense({
61
+ units: Math.floor(this.config.modelConfig.units / 2),
62
+ activation: this.config.modelConfig.activation
63
+ }));
64
+ }
65
+
66
+ // Output layer
67
+ this.model.add(this.tf.layers.dense({
68
+ units: 1
69
+ }));
70
+ }
71
+
72
+ // Compile model
73
+ this.model.compile({
74
+ optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
75
+ loss: 'meanSquaredError',
76
+ metrics: ['mse', 'mae']
77
+ });
78
+
79
+ if (this.config.verbose) {
80
+ console.log(`[MLPlugin] ${this.config.name} - Built regression model (polynomial degree: ${polynomial})`);
81
+ this.model.summary();
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Override confidence calculation for regression
87
+ * Uses prediction variance/uncertainty as confidence
88
+ * @protected
89
+ */
90
+ _calculateConfidence(value) {
91
+ // For regression, confidence is based on how close the normalized prediction
92
+ // is to the training data range (0-1 after normalization)
93
+
94
+ // If prediction is within expected range [0, 1], high confidence
95
+ if (value >= 0 && value <= 1) {
96
+ return 0.9 + Math.random() * 0.1; // 0.9-1.0 confidence
97
+ }
98
+
99
+ // If outside range, confidence decreases with distance
100
+ const distance = Math.abs(value < 0 ? value : value - 1);
101
+ return Math.max(0.5, 1.0 - distance);
102
+ }
103
+
104
+ /**
105
+ * Get R² score (coefficient of determination)
106
+ * Measures how well the model explains the variance in the data
107
+ * @param {Array} data - Test data
108
+ * @returns {number} R² score (0-1, higher is better)
109
+ */
110
+ async calculateR2Score(data) {
111
+ if (!this.isTrained) {
112
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
113
+ model: this.config.name
114
+ });
115
+ }
116
+
117
+ const predictions = [];
118
+ const actuals = [];
119
+
120
+ for (const record of data) {
121
+ const { prediction } = await this.predict(record);
122
+ predictions.push(prediction);
123
+ actuals.push(record[this.config.target]);
124
+ }
125
+
126
+ // Calculate mean of actuals
127
+ const meanActual = actuals.reduce((sum, val) => sum + val, 0) / actuals.length;
128
+
129
+ // Calculate total sum of squares (TSS)
130
+ const tss = actuals.reduce((sum, actual) => {
131
+ return sum + Math.pow(actual - meanActual, 2);
132
+ }, 0);
133
+
134
+ // Calculate residual sum of squares (RSS)
135
+ const rss = predictions.reduce((sum, pred, i) => {
136
+ return sum + Math.pow(actuals[i] - pred, 2);
137
+ }, 0);
138
+
139
+ // R² = 1 - (RSS / TSS)
140
+ const r2 = 1 - (rss / tss);
141
+
142
+ return r2;
143
+ }
144
+
145
+ /**
146
+ * Export model with regression-specific data
147
+ */
148
+ async export() {
149
+ const baseExport = await super.export();
150
+
151
+ return {
152
+ ...baseExport,
153
+ type: 'regression',
154
+ polynomial: this.config.modelConfig.polynomial
155
+ };
156
+ }
157
+ }
158
+
159
+ export default RegressionModel;
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Time Series Model
3
+ *
4
+ * LSTM-based time series prediction using TensorFlow.js
5
+ * Predicts future values based on historical sequence data
6
+ */
7
+
8
+ import { BaseModel } from './base-model.class.js';
9
+ import { ModelConfigError, DataValidationError, InsufficientDataError } from '../ml.errors.js';
10
+
11
+ export class TimeSeriesModel extends BaseModel {
12
+ constructor(config = {}) {
13
+ super(config);
14
+
15
+ // Time series-specific config
16
+ this.config.modelConfig = {
17
+ ...this.config.modelConfig,
18
+ lookback: config.modelConfig?.lookback || 10, // Number of past timesteps to use
19
+ lstmUnits: config.modelConfig?.lstmUnits || 50, // LSTM layer units
20
+ denseUnits: config.modelConfig?.denseUnits || 25, // Dense layer units
21
+ dropout: config.modelConfig?.dropout || 0.2,
22
+ recurrentDropout: config.modelConfig?.recurrentDropout || 0.2
23
+ };
24
+
25
+ // Validate lookback
26
+ if (this.config.modelConfig.lookback < 2) {
27
+ throw new ModelConfigError(
28
+ 'Lookback window must be at least 2',
29
+ { model: this.config.name, lookback: this.config.modelConfig.lookback }
30
+ );
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Build LSTM model architecture for time series
36
+ */
37
+ buildModel() {
38
+ const numFeatures = this.config.features.length + 1; // features + target as feature
39
+ const lookback = this.config.modelConfig.lookback;
40
+
41
+ // Create sequential model
42
+ this.model = this.tf.sequential();
43
+
44
+ // LSTM layer
45
+ this.model.add(this.tf.layers.lstm({
46
+ inputShape: [lookback, numFeatures],
47
+ units: this.config.modelConfig.lstmUnits,
48
+ returnSequences: false,
49
+ dropout: this.config.modelConfig.dropout,
50
+ recurrentDropout: this.config.modelConfig.recurrentDropout
51
+ }));
52
+
53
+ // Dense hidden layer
54
+ this.model.add(this.tf.layers.dense({
55
+ units: this.config.modelConfig.denseUnits,
56
+ activation: 'relu'
57
+ }));
58
+
59
+ // Dropout
60
+ if (this.config.modelConfig.dropout > 0) {
61
+ this.model.add(this.tf.layers.dropout({
62
+ rate: this.config.modelConfig.dropout
63
+ }));
64
+ }
65
+
66
+ // Output layer (predicts next value)
67
+ this.model.add(this.tf.layers.dense({
68
+ units: 1
69
+ }));
70
+
71
+ // Compile model
72
+ this.model.compile({
73
+ optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
74
+ loss: 'meanSquaredError',
75
+ metrics: ['mse', 'mae']
76
+ });
77
+
78
+ if (this.config.verbose) {
79
+ console.log(`[MLPlugin] ${this.config.name} - Built LSTM time series model (lookback: ${lookback})`);
80
+ this.model.summary();
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Prepare time series data with sliding window
86
+ * @private
87
+ */
88
+ _prepareData(data) {
89
+ const lookback = this.config.modelConfig.lookback;
90
+
91
+ if (data.length < lookback + 1) {
92
+ throw new InsufficientDataError(
93
+ `Insufficient time series data: ${data.length} samples (minimum: ${lookback + 1})`,
94
+ { model: this.config.name, samples: data.length, minimum: lookback + 1 }
95
+ );
96
+ }
97
+
98
+ const sequences = [];
99
+ const targets = [];
100
+ const allValues = [];
101
+
102
+ // Extract all values for normalization
103
+ for (const record of data) {
104
+ const features = this._extractFeatures(record);
105
+ const target = record[this.config.target];
106
+ allValues.push([...features, target]);
107
+ }
108
+
109
+ // Calculate normalization parameters
110
+ this._calculateTimeSeriesNormalizer(allValues);
111
+
112
+ // Create sliding windows
113
+ for (let i = 0; i <= data.length - lookback - 1; i++) {
114
+ const sequence = [];
115
+
116
+ // Build sequence of lookback timesteps
117
+ for (let j = 0; j < lookback; j++) {
118
+ const record = data[i + j];
119
+ const features = this._extractFeatures(record);
120
+ const target = record[this.config.target];
121
+
122
+ // Combine features and target as input (all are features for LSTM)
123
+ const combined = [...features, target];
124
+ const normalized = this._normalizeSequenceStep(combined);
125
+ sequence.push(normalized);
126
+ }
127
+
128
+ // Target is the next value
129
+ const nextRecord = data[i + lookback];
130
+ const nextTarget = nextRecord[this.config.target];
131
+
132
+ sequences.push(sequence);
133
+ targets.push(this._normalizeTarget(nextTarget));
134
+ }
135
+
136
+ // Convert to tensors
137
+ return {
138
+ xs: this.tf.tensor3d(sequences), // [samples, lookback, features]
139
+ ys: this.tf.tensor2d(targets.map(t => [t])) // [samples, 1]
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Calculate normalization for time series
145
+ * @private
146
+ */
147
+ _calculateTimeSeriesNormalizer(allValues) {
148
+ const numFeatures = allValues[0].length;
149
+
150
+ for (let i = 0; i < numFeatures; i++) {
151
+ const values = allValues.map(v => v[i]);
152
+ const min = Math.min(...values);
153
+ const max = Math.max(...values);
154
+
155
+ if (i < this.config.features.length) {
156
+ // Feature normalization
157
+ const featureName = this.config.features[i];
158
+ this.normalizer.features[featureName] = { min, max };
159
+ } else {
160
+ // Target normalization
161
+ this.normalizer.target = { min, max };
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Normalize a sequence step (features + target)
168
+ * @private
169
+ */
170
+ _normalizeSequenceStep(values) {
171
+ return values.map((value, i) => {
172
+ let min, max;
173
+
174
+ if (i < this.config.features.length) {
175
+ const featureName = this.config.features[i];
176
+ ({ min, max } = this.normalizer.features[featureName]);
177
+ } else {
178
+ ({ min, max } = this.normalizer.target);
179
+ }
180
+
181
+ if (max === min) return 0.5;
182
+ return (value - min) / (max - min);
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Predict next value in time series
188
+ * @param {Array} sequence - Array of recent records (length = lookback)
189
+ * @returns {Object} Prediction result
190
+ */
191
+ async predict(sequence) {
192
+ if (!this.isTrained) {
193
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
194
+ model: this.config.name
195
+ });
196
+ }
197
+
198
+ try {
199
+ // Validate sequence length
200
+ if (!Array.isArray(sequence)) {
201
+ throw new DataValidationError(
202
+ 'Time series prediction requires an array of recent records',
203
+ { model: this.config.name, input: typeof sequence }
204
+ );
205
+ }
206
+
207
+ if (sequence.length !== this.config.modelConfig.lookback) {
208
+ throw new DataValidationError(
209
+ `Time series sequence must have exactly ${this.config.modelConfig.lookback} timesteps, got ${sequence.length}`,
210
+ { model: this.config.name, expected: this.config.modelConfig.lookback, got: sequence.length }
211
+ );
212
+ }
213
+
214
+ // Prepare sequence
215
+ const normalizedSequence = [];
216
+ for (const record of sequence) {
217
+ this._validateInput(record);
218
+ const features = this._extractFeatures(record);
219
+ const target = record[this.config.target];
220
+ const combined = [...features, target];
221
+ normalizedSequence.push(this._normalizeSequenceStep(combined));
222
+ }
223
+
224
+ // Convert to tensor [1, lookback, features]
225
+ const inputTensor = this.tf.tensor3d([normalizedSequence]);
226
+
227
+ // Predict
228
+ const predictionTensor = this.model.predict(inputTensor);
229
+ const predictionArray = await predictionTensor.data();
230
+
231
+ // Cleanup
232
+ inputTensor.dispose();
233
+ predictionTensor.dispose();
234
+
235
+ // Denormalize prediction
236
+ const prediction = this._denormalizePrediction(predictionArray[0]);
237
+
238
+ this.stats.predictions++;
239
+
240
+ return {
241
+ prediction,
242
+ confidence: this._calculateConfidence(predictionArray[0])
243
+ };
244
+ } catch (error) {
245
+ this.stats.errors++;
246
+ if (error instanceof ModelNotTrainedError || error instanceof DataValidationError) {
247
+ throw error;
248
+ }
249
+ throw new PredictionError(`Time series prediction failed: ${error.message}`, {
250
+ model: this.config.name,
251
+ originalError: error.message
252
+ });
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Predict multiple future timesteps
258
+ * @param {Array} initialSequence - Initial sequence of records
259
+ * @param {number} steps - Number of steps to predict ahead
260
+ * @returns {Array} Array of predictions
261
+ */
262
+ async predictMultiStep(initialSequence, steps = 1) {
263
+ if (!this.isTrained) {
264
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
265
+ model: this.config.name
266
+ });
267
+ }
268
+
269
+ const predictions = [];
270
+ let currentSequence = [...initialSequence];
271
+
272
+ for (let i = 0; i < steps; i++) {
273
+ const { prediction } = await this.predict(currentSequence);
274
+ predictions.push(prediction);
275
+
276
+ // Shift sequence: remove oldest, add predicted value
277
+ currentSequence.shift();
278
+
279
+ // Create synthetic record with predicted target
280
+ // (features are copied from last record - this is a simplification)
281
+ const lastRecord = currentSequence[currentSequence.length - 1];
282
+ const syntheticRecord = {
283
+ ...lastRecord,
284
+ [this.config.target]: prediction
285
+ };
286
+
287
+ currentSequence.push(syntheticRecord);
288
+ }
289
+
290
+ return predictions;
291
+ }
292
+
293
+ /**
294
+ * Calculate Mean Absolute Percentage Error (MAPE)
295
+ * @param {Array} data - Test data (must be sequential)
296
+ * @returns {number} MAPE (0-100, lower is better)
297
+ */
298
+ async calculateMAPE(data) {
299
+ if (!this.isTrained) {
300
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
301
+ model: this.config.name
302
+ });
303
+ }
304
+
305
+ const lookback = this.config.modelConfig.lookback;
306
+
307
+ if (data.length < lookback + 1) {
308
+ throw new InsufficientDataError(
309
+ `Insufficient test data for MAPE calculation`,
310
+ { model: this.config.name, samples: data.length, minimum: lookback + 1 }
311
+ );
312
+ }
313
+
314
+ let totalPercentageError = 0;
315
+ let count = 0;
316
+
317
+ for (let i = lookback; i < data.length; i++) {
318
+ const sequence = data.slice(i - lookback, i);
319
+ const { prediction } = await this.predict(sequence);
320
+ const actual = data[i][this.config.target];
321
+
322
+ if (actual !== 0) {
323
+ const percentageError = Math.abs((actual - prediction) / actual) * 100;
324
+ totalPercentageError += percentageError;
325
+ count++;
326
+ }
327
+ }
328
+
329
+ return count > 0 ? totalPercentageError / count : 0;
330
+ }
331
+
332
+ /**
333
+ * Export model with time series-specific data
334
+ */
335
+ async export() {
336
+ const baseExport = await super.export();
337
+
338
+ return {
339
+ ...baseExport,
340
+ type: 'timeseries',
341
+ lookback: this.config.modelConfig.lookback
342
+ };
343
+ }
344
+ }
345
+
346
+ export default TimeSeriesModel;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Machine Learning Plugin Errors
3
+ *
4
+ * Custom error classes for the ML Plugin with detailed context
5
+ */
6
+
7
+ /**
8
+ * Base ML Error
9
+ */
10
+ export class MLError extends Error {
11
+ constructor(message, context = {}) {
12
+ super(message);
13
+ this.name = 'MLError';
14
+ this.context = context;
15
+
16
+ // Capture stack trace
17
+ if (Error.captureStackTrace) {
18
+ Error.captureStackTrace(this, this.constructor);
19
+ }
20
+ }
21
+
22
+ toJSON() {
23
+ return {
24
+ name: this.name,
25
+ message: this.message,
26
+ context: this.context,
27
+ stack: this.stack
28
+ };
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Model Configuration Error
34
+ * Thrown when model configuration is invalid
35
+ */
36
+ export class ModelConfigError extends MLError {
37
+ constructor(message, context = {}) {
38
+ super(message, context);
39
+ this.name = 'ModelConfigError';
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Training Error
45
+ * Thrown when model training fails
46
+ */
47
+ export class TrainingError extends MLError {
48
+ constructor(message, context = {}) {
49
+ super(message, context);
50
+ this.name = 'TrainingError';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Prediction Error
56
+ * Thrown when prediction fails
57
+ */
58
+ export class PredictionError extends MLError {
59
+ constructor(message, context = {}) {
60
+ super(message, context);
61
+ this.name = 'PredictionError';
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Model Not Found Error
67
+ * Thrown when trying to use a model that doesn't exist
68
+ */
69
+ export class ModelNotFoundError extends MLError {
70
+ constructor(message, context = {}) {
71
+ super(message, context);
72
+ this.name = 'ModelNotFoundError';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Model Not Trained Error
78
+ * Thrown when trying to predict with an untrained model
79
+ */
80
+ export class ModelNotTrainedError extends MLError {
81
+ constructor(message, context = {}) {
82
+ super(message, context);
83
+ this.name = 'ModelNotTrainedError';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Data Validation Error
89
+ * Thrown when input data is invalid
90
+ */
91
+ export class DataValidationError extends MLError {
92
+ constructor(message, context = {}) {
93
+ super(message, context);
94
+ this.name = 'DataValidationError';
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Insufficient Data Error
100
+ * Thrown when there's not enough data to train
101
+ */
102
+ export class InsufficientDataError extends MLError {
103
+ constructor(message, context = {}) {
104
+ super(message, context);
105
+ this.name = 'InsufficientDataError';
106
+ }
107
+ }
108
+
109
+ /**
110
+ * TensorFlow Dependency Error
111
+ * Thrown when TensorFlow.js is not installed
112
+ */
113
+ export class TensorFlowDependencyError extends MLError {
114
+ constructor(message = 'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node', context = {}) {
115
+ super(message, context);
116
+ this.name = 'TensorFlowDependencyError';
117
+ }
118
+ }
119
+
120
+ export default {
121
+ MLError,
122
+ ModelConfigError,
123
+ TrainingError,
124
+ PredictionError,
125
+ ModelNotFoundError,
126
+ ModelNotTrainedError,
127
+ DataValidationError,
128
+ InsufficientDataError,
129
+ TensorFlowDependencyError
130
+ };