s3db.js 12.3.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/README.md +117 -0
- package/dist/s3db.cjs.js +3075 -66
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +3074 -69
- package/dist/s3db.es.js.map +1 -1
- package/package.json +6 -2
- package/src/clients/index.js +14 -0
- package/src/clients/memory-client.class.js +900 -0
- package/src/clients/memory-client.md +917 -0
- package/src/clients/memory-storage.class.js +504 -0
- package/src/{client.class.js → clients/s3-client.class.js} +11 -10
- package/src/database.class.js +54 -19
- package/src/index.js +2 -1
- 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/plugins/replicators/bigquery-replicator.class.js +100 -20
- package/src/plugins/replicators/schema-sync.helper.js +34 -2
- package/src/plugins/tfstate/s3-driver.js +3 -3
- package/src/resource.class.js +106 -34
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification Model
|
|
3
|
+
*
|
|
4
|
+
* Binary and multi-class classification using TensorFlow.js
|
|
5
|
+
* Predicts categorical labels/classes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseModel } from './base-model.class.js';
|
|
9
|
+
import { ModelConfigError, DataValidationError } from '../ml.errors.js';
|
|
10
|
+
|
|
11
|
+
export class ClassificationModel extends BaseModel {
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
super(config);
|
|
14
|
+
|
|
15
|
+
// Classification-specific config
|
|
16
|
+
this.config.modelConfig = {
|
|
17
|
+
...this.config.modelConfig,
|
|
18
|
+
units: config.modelConfig?.units || 64, // Hidden layer units
|
|
19
|
+
activation: config.modelConfig?.activation || 'relu',
|
|
20
|
+
dropout: config.modelConfig?.dropout || 0.2 // Dropout rate for regularization
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Class mapping (label -> index)
|
|
24
|
+
this.classes = [];
|
|
25
|
+
this.classToIndex = {};
|
|
26
|
+
this.indexToClass = {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build classification model architecture
|
|
31
|
+
*/
|
|
32
|
+
buildModel() {
|
|
33
|
+
const numFeatures = this.config.features.length;
|
|
34
|
+
const numClasses = this.classes.length;
|
|
35
|
+
|
|
36
|
+
if (numClasses < 2) {
|
|
37
|
+
throw new ModelConfigError(
|
|
38
|
+
'Classification requires at least 2 classes',
|
|
39
|
+
{ model: this.config.name, numClasses }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create sequential model
|
|
44
|
+
this.model = this.tf.sequential();
|
|
45
|
+
|
|
46
|
+
// Input + first hidden layer
|
|
47
|
+
this.model.add(this.tf.layers.dense({
|
|
48
|
+
inputShape: [numFeatures],
|
|
49
|
+
units: this.config.modelConfig.units,
|
|
50
|
+
activation: this.config.modelConfig.activation,
|
|
51
|
+
useBias: true
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Dropout for regularization
|
|
55
|
+
if (this.config.modelConfig.dropout > 0) {
|
|
56
|
+
this.model.add(this.tf.layers.dropout({
|
|
57
|
+
rate: this.config.modelConfig.dropout
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Second hidden layer
|
|
62
|
+
this.model.add(this.tf.layers.dense({
|
|
63
|
+
units: Math.floor(this.config.modelConfig.units / 2),
|
|
64
|
+
activation: this.config.modelConfig.activation
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Output layer
|
|
68
|
+
const isBinary = numClasses === 2;
|
|
69
|
+
this.model.add(this.tf.layers.dense({
|
|
70
|
+
units: isBinary ? 1 : numClasses,
|
|
71
|
+
activation: isBinary ? 'sigmoid' : 'softmax'
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// Compile model
|
|
75
|
+
this.model.compile({
|
|
76
|
+
optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
|
|
77
|
+
loss: isBinary ? 'binaryCrossentropy' : 'categoricalCrossentropy',
|
|
78
|
+
metrics: ['accuracy']
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (this.config.verbose) {
|
|
82
|
+
console.log(`[MLPlugin] ${this.config.name} - Built classification model (${numClasses} classes, ${isBinary ? 'binary' : 'multi-class'})`);
|
|
83
|
+
this.model.summary();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Prepare training data (override to handle class labels)
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
_prepareData(data) {
|
|
92
|
+
const features = [];
|
|
93
|
+
const targets = [];
|
|
94
|
+
|
|
95
|
+
// Extract unique classes
|
|
96
|
+
const uniqueClasses = [...new Set(data.map(r => r[this.config.target]))];
|
|
97
|
+
this.classes = uniqueClasses.sort();
|
|
98
|
+
|
|
99
|
+
// Build class mappings
|
|
100
|
+
this.classes.forEach((cls, idx) => {
|
|
101
|
+
this.classToIndex[cls] = idx;
|
|
102
|
+
this.indexToClass[idx] = cls;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (this.config.verbose) {
|
|
106
|
+
console.log(`[MLPlugin] ${this.config.name} - Detected ${this.classes.length} classes:`, this.classes);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const record of data) {
|
|
110
|
+
// Validate record has required fields
|
|
111
|
+
const missingFeatures = this.config.features.filter(f => !(f in record));
|
|
112
|
+
if (missingFeatures.length > 0) {
|
|
113
|
+
throw new DataValidationError(
|
|
114
|
+
`Missing features in training data: ${missingFeatures.join(', ')}`,
|
|
115
|
+
{ model: this.config.name, missingFeatures, record }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!(this.config.target in record)) {
|
|
120
|
+
throw new DataValidationError(
|
|
121
|
+
`Missing target "${this.config.target}" in training data`,
|
|
122
|
+
{ model: this.config.name, target: this.config.target, record }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Extract features
|
|
127
|
+
const featureValues = this._extractFeatures(record);
|
|
128
|
+
features.push(featureValues);
|
|
129
|
+
|
|
130
|
+
// Extract target (class label)
|
|
131
|
+
const targetClass = record[this.config.target];
|
|
132
|
+
if (!(targetClass in this.classToIndex)) {
|
|
133
|
+
throw new DataValidationError(
|
|
134
|
+
`Unknown class "${targetClass}" in training data`,
|
|
135
|
+
{ model: this.config.name, targetClass, knownClasses: this.classes }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
targets.push(this.classToIndex[targetClass]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Calculate normalization parameters for features
|
|
143
|
+
this._calculateNormalizer(features, targets);
|
|
144
|
+
|
|
145
|
+
// Normalize features only (not targets)
|
|
146
|
+
const normalizedFeatures = features.map(f => this._normalizeFeatures(f));
|
|
147
|
+
|
|
148
|
+
// Convert to tensors
|
|
149
|
+
return {
|
|
150
|
+
xs: this.tf.tensor2d(normalizedFeatures),
|
|
151
|
+
ys: this._prepareTargetTensor(targets)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Prepare target tensor for classification (one-hot encoding or binary)
|
|
157
|
+
* @protected
|
|
158
|
+
*/
|
|
159
|
+
_prepareTargetTensor(targets) {
|
|
160
|
+
const isBinary = this.classes.length === 2;
|
|
161
|
+
|
|
162
|
+
if (isBinary) {
|
|
163
|
+
// Binary classification: [0, 1] labels
|
|
164
|
+
return this.tf.tensor2d(targets.map(t => [t]));
|
|
165
|
+
} else {
|
|
166
|
+
// Multi-class: one-hot encoding
|
|
167
|
+
return this.tf.oneHot(targets, this.classes.length);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate normalization parameters (skip target normalization for classification)
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
_calculateNormalizer(features, targets) {
|
|
176
|
+
const numFeatures = features[0].length;
|
|
177
|
+
|
|
178
|
+
// Initialize normalizer for features only
|
|
179
|
+
for (let i = 0; i < numFeatures; i++) {
|
|
180
|
+
const featureName = this.config.features[i];
|
|
181
|
+
const values = features.map(f => f[i]);
|
|
182
|
+
this.normalizer.features[featureName] = {
|
|
183
|
+
min: Math.min(...values),
|
|
184
|
+
max: Math.max(...values)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// No normalization for target (class indices)
|
|
189
|
+
this.normalizer.target = { min: 0, max: 1 };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Make a prediction (override to return class label)
|
|
194
|
+
*/
|
|
195
|
+
async predict(input) {
|
|
196
|
+
if (!this.isTrained) {
|
|
197
|
+
throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
|
|
198
|
+
model: this.config.name
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Validate input
|
|
204
|
+
this._validateInput(input);
|
|
205
|
+
|
|
206
|
+
// Extract and normalize features
|
|
207
|
+
const features = this._extractFeatures(input);
|
|
208
|
+
const normalizedFeatures = this._normalizeFeatures(features);
|
|
209
|
+
|
|
210
|
+
// Convert to tensor
|
|
211
|
+
const inputTensor = this.tf.tensor2d([normalizedFeatures]);
|
|
212
|
+
|
|
213
|
+
// Predict
|
|
214
|
+
const predictionTensor = this.model.predict(inputTensor);
|
|
215
|
+
const predictionArray = await predictionTensor.data();
|
|
216
|
+
|
|
217
|
+
// Cleanup
|
|
218
|
+
inputTensor.dispose();
|
|
219
|
+
predictionTensor.dispose();
|
|
220
|
+
|
|
221
|
+
const isBinary = this.classes.length === 2;
|
|
222
|
+
|
|
223
|
+
let predictedClassIndex;
|
|
224
|
+
let confidence;
|
|
225
|
+
|
|
226
|
+
if (isBinary) {
|
|
227
|
+
// Binary classification: threshold at 0.5
|
|
228
|
+
confidence = predictionArray[0];
|
|
229
|
+
predictedClassIndex = confidence >= 0.5 ? 1 : 0;
|
|
230
|
+
} else {
|
|
231
|
+
// Multi-class: argmax
|
|
232
|
+
predictedClassIndex = predictionArray.indexOf(Math.max(...predictionArray));
|
|
233
|
+
confidence = predictionArray[predictedClassIndex];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const predictedClass = this.indexToClass[predictedClassIndex];
|
|
237
|
+
|
|
238
|
+
this.stats.predictions++;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
prediction: predictedClass,
|
|
242
|
+
confidence,
|
|
243
|
+
probabilities: isBinary ? {
|
|
244
|
+
[this.classes[0]]: 1 - predictionArray[0],
|
|
245
|
+
[this.classes[1]]: predictionArray[0]
|
|
246
|
+
} : Object.fromEntries(
|
|
247
|
+
this.classes.map((cls, idx) => [cls, predictionArray[idx]])
|
|
248
|
+
)
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.stats.errors++;
|
|
252
|
+
if (error instanceof ModelNotTrainedError || error instanceof DataValidationError) {
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
throw new PredictionError(`Prediction failed: ${error.message}`, {
|
|
256
|
+
model: this.config.name,
|
|
257
|
+
input,
|
|
258
|
+
originalError: error.message
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Calculate confusion matrix
|
|
265
|
+
* @param {Array} data - Test data
|
|
266
|
+
* @returns {Object} Confusion matrix and metrics
|
|
267
|
+
*/
|
|
268
|
+
async calculateConfusionMatrix(data) {
|
|
269
|
+
if (!this.isTrained) {
|
|
270
|
+
throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
|
|
271
|
+
model: this.config.name
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const matrix = {};
|
|
276
|
+
const numClasses = this.classes.length;
|
|
277
|
+
|
|
278
|
+
// Initialize matrix
|
|
279
|
+
for (const actualClass of this.classes) {
|
|
280
|
+
matrix[actualClass] = {};
|
|
281
|
+
for (const predictedClass of this.classes) {
|
|
282
|
+
matrix[actualClass][predictedClass] = 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Populate matrix
|
|
287
|
+
for (const record of data) {
|
|
288
|
+
const { prediction } = await this.predict(record);
|
|
289
|
+
const actual = record[this.config.target];
|
|
290
|
+
matrix[actual][prediction]++;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Calculate metrics
|
|
294
|
+
let totalCorrect = 0;
|
|
295
|
+
let total = 0;
|
|
296
|
+
|
|
297
|
+
for (const cls of this.classes) {
|
|
298
|
+
totalCorrect += matrix[cls][cls];
|
|
299
|
+
total += Object.values(matrix[cls]).reduce((sum, val) => sum + val, 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const accuracy = total > 0 ? totalCorrect / total : 0;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
matrix,
|
|
306
|
+
accuracy,
|
|
307
|
+
total,
|
|
308
|
+
correct: totalCorrect
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Export model with classification-specific data
|
|
314
|
+
*/
|
|
315
|
+
async export() {
|
|
316
|
+
const baseExport = await super.export();
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
...baseExport,
|
|
320
|
+
type: 'classification',
|
|
321
|
+
classes: this.classes,
|
|
322
|
+
classToIndex: this.classToIndex,
|
|
323
|
+
indexToClass: this.indexToClass
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Import model (override to restore class mappings)
|
|
329
|
+
*/
|
|
330
|
+
async import(data) {
|
|
331
|
+
await super.import(data);
|
|
332
|
+
this.classes = data.classes || [];
|
|
333
|
+
this.classToIndex = data.classToIndex || {};
|
|
334
|
+
this.indexToClass = data.indexToClass || {};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export default ClassificationModel;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neural Network Model
|
|
3
|
+
*
|
|
4
|
+
* Generic customizable neural network using TensorFlow.js
|
|
5
|
+
* Flexible architecture for complex non-linear problems
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseModel } from './base-model.class.js';
|
|
9
|
+
import { ModelConfigError } from '../ml.errors.js';
|
|
10
|
+
|
|
11
|
+
export class NeuralNetworkModel extends BaseModel {
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
super(config);
|
|
14
|
+
|
|
15
|
+
// Neural network-specific config
|
|
16
|
+
this.config.modelConfig = {
|
|
17
|
+
...this.config.modelConfig,
|
|
18
|
+
layers: config.modelConfig?.layers || [
|
|
19
|
+
{ units: 64, activation: 'relu', dropout: 0.2 },
|
|
20
|
+
{ units: 32, activation: 'relu', dropout: 0.1 }
|
|
21
|
+
], // Array of hidden layer configurations
|
|
22
|
+
outputActivation: config.modelConfig?.outputActivation || 'linear', // Output layer activation
|
|
23
|
+
outputUnits: config.modelConfig?.outputUnits || 1, // Number of output units
|
|
24
|
+
loss: config.modelConfig?.loss || 'meanSquaredError', // Loss function
|
|
25
|
+
metrics: config.modelConfig?.metrics || ['mse', 'mae'] // Metrics to track
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Validate layers configuration
|
|
29
|
+
this._validateLayersConfig();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate layers configuration
|
|
34
|
+
* @private
|
|
35
|
+
*/
|
|
36
|
+
_validateLayersConfig() {
|
|
37
|
+
if (!Array.isArray(this.config.modelConfig.layers) || this.config.modelConfig.layers.length === 0) {
|
|
38
|
+
throw new ModelConfigError(
|
|
39
|
+
'Neural network must have at least one hidden layer',
|
|
40
|
+
{ model: this.config.name, layers: this.config.modelConfig.layers }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [index, layer] of this.config.modelConfig.layers.entries()) {
|
|
45
|
+
if (!layer.units || typeof layer.units !== 'number' || layer.units < 1) {
|
|
46
|
+
throw new ModelConfigError(
|
|
47
|
+
`Layer ${index} must have a valid "units" property (positive number)`,
|
|
48
|
+
{ model: this.config.name, layer, index }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (layer.activation && !this._isValidActivation(layer.activation)) {
|
|
53
|
+
throw new ModelConfigError(
|
|
54
|
+
`Layer ${index} has invalid activation function "${layer.activation}"`,
|
|
55
|
+
{ model: this.config.name, layer, index, validActivations: ['relu', 'sigmoid', 'tanh', 'softmax', 'elu', 'selu'] }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if activation function is valid
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_isValidActivation(activation) {
|
|
66
|
+
const validActivations = ['relu', 'sigmoid', 'tanh', 'softmax', 'elu', 'selu', 'linear'];
|
|
67
|
+
return validActivations.includes(activation);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build custom neural network architecture
|
|
72
|
+
*/
|
|
73
|
+
buildModel() {
|
|
74
|
+
const numFeatures = this.config.features.length;
|
|
75
|
+
|
|
76
|
+
// Create sequential model
|
|
77
|
+
this.model = this.tf.sequential();
|
|
78
|
+
|
|
79
|
+
// Add hidden layers
|
|
80
|
+
for (const [index, layerConfig] of this.config.modelConfig.layers.entries()) {
|
|
81
|
+
const isFirstLayer = index === 0;
|
|
82
|
+
|
|
83
|
+
// Dense layer
|
|
84
|
+
const layerOptions = {
|
|
85
|
+
units: layerConfig.units,
|
|
86
|
+
activation: layerConfig.activation || 'relu',
|
|
87
|
+
useBias: true
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (isFirstLayer) {
|
|
91
|
+
layerOptions.inputShape = [numFeatures];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.model.add(this.tf.layers.dense(layerOptions));
|
|
95
|
+
|
|
96
|
+
// Dropout (if specified)
|
|
97
|
+
if (layerConfig.dropout && layerConfig.dropout > 0) {
|
|
98
|
+
this.model.add(this.tf.layers.dropout({
|
|
99
|
+
rate: layerConfig.dropout
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Batch normalization (if specified)
|
|
104
|
+
if (layerConfig.batchNormalization) {
|
|
105
|
+
this.model.add(this.tf.layers.batchNormalization());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Output layer
|
|
110
|
+
this.model.add(this.tf.layers.dense({
|
|
111
|
+
units: this.config.modelConfig.outputUnits,
|
|
112
|
+
activation: this.config.modelConfig.outputActivation
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
// Compile model
|
|
116
|
+
this.model.compile({
|
|
117
|
+
optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
|
|
118
|
+
loss: this.config.modelConfig.loss,
|
|
119
|
+
metrics: this.config.modelConfig.metrics
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (this.config.verbose) {
|
|
123
|
+
console.log(`[MLPlugin] ${this.config.name} - Built custom neural network:`);
|
|
124
|
+
console.log(` - Hidden layers: ${this.config.modelConfig.layers.length}`);
|
|
125
|
+
console.log(` - Total parameters:`, this._countParameters());
|
|
126
|
+
this.model.summary();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Count total trainable parameters
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
_countParameters() {
|
|
135
|
+
if (!this.model) return 0;
|
|
136
|
+
|
|
137
|
+
let totalParams = 0;
|
|
138
|
+
for (const layer of this.model.layers) {
|
|
139
|
+
if (layer.countParams) {
|
|
140
|
+
totalParams += layer.countParams();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return totalParams;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Add layer to model (before building)
|
|
148
|
+
* @param {Object} layerConfig - Layer configuration
|
|
149
|
+
*/
|
|
150
|
+
addLayer(layerConfig) {
|
|
151
|
+
if (this.model) {
|
|
152
|
+
throw new ModelConfigError(
|
|
153
|
+
'Cannot add layer after model is built. Use addLayer() before training.',
|
|
154
|
+
{ model: this.config.name }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.config.modelConfig.layers.push(layerConfig);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Set output configuration
|
|
163
|
+
* @param {Object} outputConfig - Output layer configuration
|
|
164
|
+
*/
|
|
165
|
+
setOutput(outputConfig) {
|
|
166
|
+
if (this.model) {
|
|
167
|
+
throw new ModelConfigError(
|
|
168
|
+
'Cannot change output after model is built. Use setOutput() before training.',
|
|
169
|
+
{ model: this.config.name }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (outputConfig.activation) {
|
|
174
|
+
this.config.modelConfig.outputActivation = outputConfig.activation;
|
|
175
|
+
}
|
|
176
|
+
if (outputConfig.units) {
|
|
177
|
+
this.config.modelConfig.outputUnits = outputConfig.units;
|
|
178
|
+
}
|
|
179
|
+
if (outputConfig.loss) {
|
|
180
|
+
this.config.modelConfig.loss = outputConfig.loss;
|
|
181
|
+
}
|
|
182
|
+
if (outputConfig.metrics) {
|
|
183
|
+
this.config.modelConfig.metrics = outputConfig.metrics;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get model architecture summary
|
|
189
|
+
*/
|
|
190
|
+
getArchitecture() {
|
|
191
|
+
return {
|
|
192
|
+
inputFeatures: this.config.features,
|
|
193
|
+
hiddenLayers: this.config.modelConfig.layers.map((layer, index) => ({
|
|
194
|
+
index,
|
|
195
|
+
units: layer.units,
|
|
196
|
+
activation: layer.activation || 'relu',
|
|
197
|
+
dropout: layer.dropout || 0,
|
|
198
|
+
batchNormalization: layer.batchNormalization || false
|
|
199
|
+
})),
|
|
200
|
+
outputLayer: {
|
|
201
|
+
units: this.config.modelConfig.outputUnits,
|
|
202
|
+
activation: this.config.modelConfig.outputActivation
|
|
203
|
+
},
|
|
204
|
+
totalParameters: this._countParameters(),
|
|
205
|
+
loss: this.config.modelConfig.loss,
|
|
206
|
+
metrics: this.config.modelConfig.metrics
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Train with early stopping callback
|
|
212
|
+
* @param {Array} data - Training data
|
|
213
|
+
* @param {Object} earlyStoppingConfig - Early stopping configuration
|
|
214
|
+
* @returns {Object} Training results
|
|
215
|
+
*/
|
|
216
|
+
async trainWithEarlyStopping(data, earlyStoppingConfig = {}) {
|
|
217
|
+
const {
|
|
218
|
+
patience = 10,
|
|
219
|
+
minDelta = 0.001,
|
|
220
|
+
monitor = 'val_loss',
|
|
221
|
+
restoreBestWeights = true
|
|
222
|
+
} = earlyStoppingConfig;
|
|
223
|
+
|
|
224
|
+
// Prepare data
|
|
225
|
+
const { xs, ys } = this._prepareData(data);
|
|
226
|
+
|
|
227
|
+
// Build model if not already built
|
|
228
|
+
if (!this.model) {
|
|
229
|
+
this.buildModel();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Early stopping callback
|
|
233
|
+
let bestValue = Infinity;
|
|
234
|
+
let patienceCounter = 0;
|
|
235
|
+
let bestWeights = null;
|
|
236
|
+
|
|
237
|
+
const callbacks = {
|
|
238
|
+
onEpochEnd: async (epoch, logs) => {
|
|
239
|
+
const monitorValue = logs[monitor] || logs.loss;
|
|
240
|
+
|
|
241
|
+
if (this.config.verbose && epoch % 10 === 0) {
|
|
242
|
+
console.log(`[MLPlugin] ${this.config.name} - Epoch ${epoch}: ${monitor}=${monitorValue.toFixed(4)}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for improvement
|
|
246
|
+
if (monitorValue < bestValue - minDelta) {
|
|
247
|
+
bestValue = monitorValue;
|
|
248
|
+
patienceCounter = 0;
|
|
249
|
+
|
|
250
|
+
if (restoreBestWeights) {
|
|
251
|
+
bestWeights = await this.model.getWeights();
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
patienceCounter++;
|
|
255
|
+
|
|
256
|
+
if (patienceCounter >= patience) {
|
|
257
|
+
if (this.config.verbose) {
|
|
258
|
+
console.log(`[MLPlugin] ${this.config.name} - Early stopping at epoch ${epoch}`);
|
|
259
|
+
}
|
|
260
|
+
this.model.stopTraining = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Train
|
|
267
|
+
const history = await this.model.fit(xs, ys, {
|
|
268
|
+
epochs: this.config.modelConfig.epochs,
|
|
269
|
+
batchSize: this.config.modelConfig.batchSize,
|
|
270
|
+
validationSplit: this.config.modelConfig.validationSplit,
|
|
271
|
+
verbose: this.config.verbose ? 1 : 0,
|
|
272
|
+
callbacks
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Restore best weights
|
|
276
|
+
if (restoreBestWeights && bestWeights) {
|
|
277
|
+
this.model.setWeights(bestWeights);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update stats
|
|
281
|
+
this.isTrained = true;
|
|
282
|
+
this.stats.trainedAt = new Date().toISOString();
|
|
283
|
+
this.stats.samples = data.length;
|
|
284
|
+
this.stats.loss = history.history.loss[history.history.loss.length - 1];
|
|
285
|
+
|
|
286
|
+
// Cleanup
|
|
287
|
+
xs.dispose();
|
|
288
|
+
ys.dispose();
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
loss: this.stats.loss,
|
|
292
|
+
epochs: history.epoch.length,
|
|
293
|
+
samples: this.stats.samples,
|
|
294
|
+
stoppedEarly: history.epoch.length < this.config.modelConfig.epochs
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Export model with neural network-specific data
|
|
300
|
+
*/
|
|
301
|
+
async export() {
|
|
302
|
+
const baseExport = await super.export();
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
...baseExport,
|
|
306
|
+
type: 'neural-network',
|
|
307
|
+
architecture: this.getArchitecture()
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default NeuralNetworkModel;
|