UNPKG

@modelx/model

Version:

Deep Learning Classification, LSTM Time Series, Regression and Multi-Layered Perceptrons with Tensorflow

421 lines (420 loc) 16 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.asyncForEach = exports.flatten = exports.DimensionError = exports._reshape = exports.size = exports.TensorScriptModelInterface = exports.LambdaLayer = void 0; // import * as tensorflow from '@tensorflow/tfjs'; // import '@tensorflow/tfjs-node'; // import * as tensorflow from '@tensorflow/tfjs-node'; const tf = __importStar(require("@tensorflow/tfjs-node")); ; /****************************************************************************** * tensorflow.js lambda layer * written by twitter.com/benjaminwegener * license: MIT * @see https://benjamin-wegener.blogspot.com/2020/02/tensorflowjs-lambda-layer.html */ class LambdaLayer extends tf.layers.Layer { constructor(config) { super(config); if (config.name === undefined) { config.name = ((+new Date) * Math.random()).toString(36); //random name from timestamp in case name hasn't been set } this.name = config.name; this.lambdaFunction = config.lambdaFunction; if (config.lambdaOutputShape) this.lambdaOutputShape = config.lambdaOutputShape; } call(input, kwargs) { // console.log({ input }, 'input[0].shape', input[0].shape) // input[0].data().then(inputData=>console.log) // console.log('input[0].data()', input[0].data()) // return input; return tf.tidy(() => { // return tf.mean(tf.tensor(input),1,true) let result = new Array(); // eval(this.lambdaFunction); result = (new Function('input', 'tf', this.lambdaFunction))(input, tf); // result = tf.mean(input,1); return result; }); } computeOutputShape(inputShape) { // console.log('computeOutputShape',{inputShape}) if (this.lambdaOutputShape === undefined) { //if no outputshape provided, try to set as inputshape return inputShape[0]; } else { return this.lambdaOutputShape; } } getConfig() { const config = { ...super.getConfig(), lambdaFunction: this.lambdaFunction, lambdaOutputShape: this.lambdaOutputShape, }; return config; } static get className() { return 'LambdaLayer'; } } exports.LambdaLayer = LambdaLayer; /** * Base class for tensorscript models * @interface TensorScriptModelInterface * @property {Object} settings - tensorflow model hyperparameters * @property {Object} model - tensorflow model * @property {Object} tf - tensorflow / tensorflow-node / tensorflow-node-gpu * @property {Function} reshape - static reshape array function * @property {Function} getInputShape - static TensorScriptModelInterface */ class TensorScriptModelInterface { /** * @param {Object} options - tensorflow model hyperparameters * @param {Object} customTF - custom, overridale tensorflow / tensorflow-node / tensorflow-node-gpu * @param {{model:Object,tf:Object,}} properties - extra instance properties */ constructor(options = {}, properties = {}) { // tf.setBackend('cpu'); this.type = 'ModelInterface'; /** @type {Object} */ this.settings = Object.assign({}, options); /** @type {Object} */ this.model = properties.model; /** @type {Object} */ this.tf = properties.tf || tf; /** @type {Boolean} */ this.trained = false; this.compiled = false; /** @type {Function} */ this.reshape = TensorScriptModelInterface.reshape; /** @type {Function} */ this.getInputShape = TensorScriptModelInterface.getInputShape; if (this.tf && this.tf.serialization && this.tf.serialization.registerClass) this.tf.serialization.registerClass(LambdaLayer); return this; } /** * Reshapes an array * @function * @example * const array = [ 0, 1, 1, 0, ]; * const shape = [2,2]; * TensorScriptModelInterface.reshape(array,shape) // => * [ * [ 0, 1, ], * [ 1, 0, ], * ]; * @param {Array<number>} array - input array * @param {Array<number>} shape - shape array * @return {Array<Array<number>>} returns a matrix with the defined shape */ /* istanbul ignore next */ static reshape(array, shape) { const flatArray = flatten(array); function product(arr) { return arr.reduce((prev, curr) => prev * curr); } if (!Array.isArray(array) || !Array.isArray(shape)) { throw new TypeError('Array expected'); } if (shape.length === 0) { //@ts-ignore throw new DimensionError(0, product(size(array)), '!='); } let newArray; let totalSize = 1; const rows = shape[0]; for (let sizeIndex = 0; sizeIndex < shape.length; sizeIndex++) { totalSize *= shape[sizeIndex]; } if (flatArray.length !== totalSize) { throw new DimensionError(product(shape), //@ts-ignore product(size(array)), '!='); } try { newArray = _reshape(flatArray, shape); } catch (e) { if (e instanceof DimensionError) { throw new DimensionError(product(shape), //@ts-ignore product(size(array)), '!='); } throw e; } if (newArray.length !== rows) throw new SyntaxError(`specified shape (${shape}) is compatible with input array or length (${array.length})`); // console.log({ newArray ,}); //@ts-ignore return newArray; } /** * Returns the shape of an input matrix * @function * @example * const input = [ * [ 0, 1, ], * [ 1, 0, ], * ]; * TensorScriptModelInterface.getInputShape(input) // => [2,2] * @see {https://stackoverflow.com/questions/10237615/get-size-of-dimensions-in-array} * @param {Array<Array<number>>} matrix - input matrix * @return {Array<number>} returns the shape of a matrix (e.g. [2,2]) */ //@ts-ignore static getInputShape(matrix = []) { // static getInputShape(matrix:NestedArray<V>=[]):Shape { if (Array.isArray(matrix) === false || !matrix[0] || !matrix[0].length || Array.isArray(matrix[0]) === false) throw new TypeError('input must be a matrix'); const dim = []; const x_dimensions = matrix[0].length; let vectors = matrix; matrix.forEach((vector) => { if (vector.length !== x_dimensions) throw new SyntaxError('input must have the same length in each row'); }); for (;;) { dim.push(vectors.length); if (Array.isArray(vectors[0])) { vectors = vectors[0]; } else { break; } } return dim; } exportConfiguration() { return { type: this.type, settings: this.settings, trained: this.trained, compiled: this.compiled, xShape: this.xShape, yShape: this.yShape, layers: this.layers, }; } importConfiguration(configuration) { this.type = configuration.type || this.type; this.settings = { ...this.settings, ...configuration.settings, }; this.trained = configuration.trained || this.trained; this.compiled = configuration.compiled || this.compiled; this.xShape = configuration.xShape || this.xShape; this.yShape = configuration.yShape || this.yShape; this.layers = configuration.layers || this.layers; } train(x_matrix, y_matrix) { throw new ReferenceError('train method is not implemented'); } /** * Predicts new dependent variables * @abstract * @param {Array<Array<number>>|Array<number>} matrix - new test independent variables * @return {{data: Promise}} returns tensorflow prediction */ calculate(matrix) { throw new ReferenceError('calculate method is not implemented'); } /** * Loads a saved tensoflow / keras model, this is an alias for * @param {Object} options - tensorflow load model options * @return {Object} tensorflow model * @see {@link https://www.tensorflow.org/js/guide/save_load#loading_a_tfmodel} */ async loadModel(options) { this.model = await this.tf.loadLayersModel(options); this.xShape = this.model.inputs[0].shape; this.yShape = this.model.outputs[0].shape; this.trained = true; return this.model; } /** * saves a tensorflow model, this is an alias for * @param {Object} options - tensorflow save model options * @return {Object} tensorflow model * @see {@link https://www.tensorflow.org/js/guide/save_load#save_a_tfmodel} */ async saveModel(options) { const savedStatus = await this.model.save(options); return savedStatus; } /** * Returns prediction values from tensorflow model * @param {Array<Array<number>>|Array<number>} input_matrix - new test independent variables * @param {Boolean} [options.json=true] - return object instead of typed array * @param {Boolean} [options.probability=true] - return real values instead of integers * @param {Boolean} [options.skip_matrix_check=false] - validate input is a matrix * @return {Array<number>|Array<Array<number>>} predicted model values */ // async predict(options?:Matrix|Vector|InputTextArray|PredictionOptions):Promise<Matrix> async predict(input_matrix, options) { if (!input_matrix || Array.isArray(input_matrix) === false) throw new Error('invalid input matrix'); const config = { json: true, probability: true, ...options }; const x_matrix = (Array.isArray(input_matrix) || config.skip_matrix_check) ? input_matrix : [ input_matrix, ]; return this.calculate(x_matrix) .data() .then((predictions) => { // console.log({ predictions }); if (config.json === false) { return predictions; } else { if (!this.yShape) throw new Error('Model is missing yShape'); const shape = [x_matrix.length, this.yShape[1],]; const predictionValues = (config.probability === false) ? Array.from(predictions).map(Math.round) : Array.from(predictions); return this.reshape(predictionValues, shape); } }) .catch((e) => { throw e; }); } } exports.TensorScriptModelInterface = TensorScriptModelInterface; /** * Calculate the size of a multi dimensional array. * This function checks the size of the first entry, it does not validate * whether all dimensions match. (use function `validate` for that) (from math.js) * @param {Array} x * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js} * @ignore * @return {Number[]} size */ /* istanbul ignore next */ function size(x) { let s = []; while (Array.isArray(x)) { s.push(x.length); x = x[0]; } return s; } exports.size = size; /** * Iteratively re-shape a multi dimensional array to fit the specified dimensions (from math.js) * @param {Array} array Array to be reshaped * @param {Array.<number>} sizes List of sizes for each dimension * @returns {Array} Array whose data has been formatted to fit the * specified dimensions * @ignore * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js} */ /* istanbul ignore next */ function _reshape(array, sizes) { // testing if there are enough elements for the requested shape var tmpArray = array; var tmpArray2; // for each dimensions starting by the last one and ignoring the first one for (var sizeIndex = sizes.length - 1; sizeIndex > 0; sizeIndex--) { var size = sizes[sizeIndex]; tmpArray2 = []; // aggregate the elements of the current tmpArray in elements of the requested size var length = tmpArray.length / size; for (var i = 0; i < length; i++) { tmpArray2.push(tmpArray.slice(i * size, (i + 1) * size)); } // set it as the new tmpArray for the next loop turn or for return //@ts-ignore tmpArray = tmpArray2; } //@ts-ignore return tmpArray; } exports._reshape = _reshape; /** * Create a range error with the message: * 'Dimension mismatch (<actual size> != <expected size>)' (from math.js) * @param {number | number[]} actual The actual size * @param {number | number[]} expected The expected size * @param {string} [relation='!='] Optional relation between actual * and expected size: '!=', '<', etc. * @extends RangeError * @ignore * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js} */ /* istanbul ignore next */ class DimensionError extends RangeError { constructor(actual, expected, relation) { /* istanbul ignore next */ const message = 'Dimension mismatch (' + (Array.isArray(actual) ? ('[' + actual.join(', ') + ']') : actual) + ' ' + ('!=') + ' ' + (Array.isArray(expected) ? ('[' + expected.join(', ') + ']') : expected) + ')'; super(message); this.actual = actual; this.expected = expected; this.relation = relation; // this.stack = (new Error()).stack this.message = message; this.name = 'DimensionError'; this.isDimensionError = true; } } exports.DimensionError = DimensionError; /** * Flatten a multi dimensional array, put all elements in a one dimensional * array * @param {Array} array A multi dimensional array * @ignore * @see {https://github.com/josdejong/mathjs/blob/develop/src/utils/array.js} * @return {Array} The flattened array (1 dimensional) */ /* istanbul ignore next */ function flatten(array) { /* istanbul ignore next */ if (!Array.isArray(array)) { // if not an array, return as is /* istanbul ignore next */ return array; } let flat = []; /* istanbul ignore next */ array.forEach(function callback(value) { if (Array.isArray(value)) { value.forEach(callback); // traverse through sub-arrays recursively } else { flat.push(value); } }); return flat; } exports.flatten = flatten; async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array); } } exports.asyncForEach = asyncForEach;