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.
- package/dist/s3db.cjs.js +1923 -57
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1923 -58
- package/dist/s3db.es.js.map +1 -1
- package/package.json +5 -1
- package/src/clients/memory-client.class.js +41 -24
- package/src/database.class.js +52 -17
- package/src/plugins/api/index.js +12 -9
- package/src/plugins/api/routes/resource-routes.js +78 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/ml/base-model.class.js +459 -0
- package/src/plugins/ml/classification-model.class.js +338 -0
- package/src/plugins/ml/neural-network-model.class.js +312 -0
- package/src/plugins/ml/regression-model.class.js +159 -0
- package/src/plugins/ml/timeseries-model.class.js +346 -0
- package/src/plugins/ml.errors.js +130 -0
- package/src/plugins/ml.plugin.js +655 -0
- package/src/resource.class.js +106 -34
|
@@ -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
|
+
};
|