lib/model_interface.mjs
import tf from '@tensorflow/tfjs';
/**
* 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
*/
export 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 = {}) {
/** @type {Object} */
this.settings = options;
/** @type {Object} */
this.model = properties.model;
/** @type {Object} */
this.tf = properties.tf || tf;
/** @type {Function} */
this.reshape = TensorScriptModelInterface.reshape;
/** @type {Function} */
this.getInputShape = TensorScriptModelInterface.getInputShape;
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) {
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),
product(size(array)),
'!='
);
}
try {
newArray = _reshape(flatArray, shape);
} catch (e) {
if (e instanceof DimensionError) {
throw new DimensionError(
product(shape),
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 ,});
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])
*/
static getInputShape(matrix=[]) {
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;
}
/**
* Asynchronously trains tensorflow model, must be implemented by tensorscript class
* @abstract
* @param {Array<Array<number>>} x_matrix - independent variables
* @param {Array<Array<number>>} y_matrix - dependent variables
* @return {Object} returns trained tensorflow model
*/
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
* @param {Object} options - tensorflow load model options
* @return {Object} tensorflow model
*/
async loadModel(options) {
this.model = await this.tf.loadModel(options);
return this.model;
}
/**
* 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
* @return {Array<number>|Array<Array<number>>} predicted model values
*/
async predict(input_matrix, options = {}) {
if (!input_matrix || Array.isArray(input_matrix)===false) throw new Error('invalid input matrix');
const x_matrix = (Array.isArray(input_matrix[ 0 ]))
? input_matrix
: [
input_matrix,
];
const config = Object.assign({
json: true,
probability: true,
}, options);
return this.calculate(x_matrix)
.data()
.then(predictions => {
if (config.json === false) {
return predictions;
} else {
const shape = [x_matrix.length, this.yShape[ 1 ], ];
const predictionValues = (options.probability === false) ? Array.from(predictions).map(Math.round) : Array.from(predictions);
return this.reshape(predictionValues, shape);
}
})
.catch(e => {
throw e;
});
}
}
/**
* 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 */
export function size (x) {
let s = [];
while (Array.isArray(x)) {
s.push(x.length);
x = x[0];
}
return s;
}
/**
* 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 */
export 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
tmpArray = tmpArray2;
}
return tmpArray;
}
/**
* 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 */
export 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;
}
}
/**
* 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 */
export 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;
}