ml5-save
Version:
1,002 lines (876 loc) • 27.7 kB
JavaScript
import * as tf from '@tensorflow/tfjs';
import NeuralNetwork from './NeuralNetwork';
import NeuralNetworkData from './NeuralNetworkData';
import NeuralNetworkVis from './NeuralNetworkVis';
import callCallback from '../utils/callcallback';
import nnUtils from './NeuralNetworkUtils';
import { imgToPixelArray, isInstanceOfSupportedElement } from '../utils/imageUtilities';
const DEFAULTS = {
inputs: [],
outputs: [],
dataUrl: null,
modelUrl: null,
layers: [],
task: null,
debug: false,
learningRate: 0.2,
hiddenUnits: 16,
};
class DiyNeuralNetwork {
constructor(options, cb) {
this.callback = cb;
this.options =
{
...DEFAULTS,
...options,
} || DEFAULTS;
this.neuralNetwork = new NeuralNetwork();
this.neuralNetworkData = new NeuralNetworkData();
this.neuralNetworkVis = new NeuralNetworkVis();
this.data = {
training: [],
};
this.ready = false;
// Methods
this.init = this.init.bind(this);
// adding data
this.addData = this.addData.bind(this);
this.loadDataFromUrl = this.loadDataFromUrl.bind(this);
this.loadDataInternal = this.loadDataInternal.bind(this);
// metadata prep
this.createMetaData = this.createMetaData.bind(this);
// data prep and handling
this.prepareForTraining = this.prepareForTraining.bind(this);
this.normalizeData = this.normalizeData.bind(this);
this.normalizeInput = this.normalizeInput.bind(this);
this.searchAndFormat = this.searchAndFormat.bind(this);
this.formatInputItem = this.formatInputItem.bind(this);
this.convertTrainingDataToTensors = this.convertTrainingDataToTensors.bind(this);
this.formatInputsForPrediction = this.formatInputsForPrediction.bind(this);
this.formatInputsForPredictionAll = this.formatInputsForPredictionAll.bind(this);
this.isOneHotEncodedOrNormalized = this.isOneHotEncodedOrNormalized.bind(this);
// model prep
this.train = this.train.bind(this);
this.trainInternal = this.trainInternal.bind(this);
this.addLayer = this.addLayer.bind(this);
this.createNetworkLayers = this.createNetworkLayers.bind(this);
this.addDefaultLayers = this.addDefaultLayers.bind(this);
this.compile = this.compile.bind(this);
// prediction / classification
this.predict = this.predict.bind(this);
this.predictMultiple = this.predictMultiple.bind(this);
this.classify = this.classify.bind(this);
this.classifyMultiple = this.classifyMultiple.bind(this);
this.predictInternal = this.predictInternal.bind(this);
this.classifyInternal = this.classifyInternal.bind(this);
// save / load data
this.saveData = this.saveData.bind(this);
this.loadData = this.loadData.bind(this);
// save / load model
this.save = this.save.bind(this);
this.load = this.load.bind(this);
// Initialize
this.init(this.callback);
}
/**
* ////////////////////////////////////////////////////////////
* Initialization
* ////////////////////////////////////////////////////////////
*/
/**
* init
* @param {*} callback
*/
init(callback) {
if (this.options.dataUrl !== null) {
this.ready = this.loadDataFromUrl(this.options, callback);
} else if (this.options.modelUrl !== null) {
// will take a URL to model.json, an object, or files array
this.ready = this.load(this.options.modelUrl, callback);
} else {
this.ready = true;
}
}
/**
* ////////////////////////////////////////////////////////////
* Adding Data
* ////////////////////////////////////////////////////////////
*/
/**
* addData
* @param {Array | Object} xInputs
* @param {Array | Object} yInputs
* @param {*} options
*/
addData(xInputs, yInputs, options = null) {
const { inputs, outputs } = this.options;
// get the input and output labels
// or infer them from the data
let inputLabels;
let outputLabels;
if (options !== null) {
// eslint-disable-next-line prefer-destructuring
inputLabels = options.inputLabels;
// eslint-disable-next-line prefer-destructuring
outputLabels = options.outputLabels;
} else if (inputs.length > 0 && outputs.length > 0) {
// if the inputs and outputs labels have been defined
// in the constructor
if (inputs.every(item => typeof item === 'string')) {
inputLabels = inputs;
}
if (outputs.every(item => typeof item === 'string')) {
outputLabels = outputs;
}
} else if (typeof xInputs === 'object' && typeof yInputs === 'object') {
inputLabels = Object.keys(xInputs);
outputLabels = Object.keys(yInputs);
} else {
inputLabels = nnUtils.createLabelsFromArrayValues(xInputs, 'input');
outputLabels = nnUtils.createLabelsFromArrayValues(yInputs, 'output');
}
// Make sure that the inputLabels and outputLabels are arrays
if (!(inputLabels instanceof Array)) {
throw new Error('inputLabels must be an array');
}
if (!(outputLabels instanceof Array)) {
throw new Error('outputLabels must be an array');
}
const formattedInputs = this.searchAndFormat(xInputs);
const xs = nnUtils.formatDataAsObject(formattedInputs, inputLabels);
const ys = nnUtils.formatDataAsObject(yInputs, outputLabels);
this.neuralNetworkData.addData(xs, ys);
}
/**
* loadData
* @param {*} options
* @param {*} callback
*/
loadDataFromUrl(options, callback) {
return callCallback(this.loadDataInternal(options), callback);
}
/**
* loadDataInternal
* @param {*} options
*/
async loadDataInternal(options) {
const { dataUrl, inputs, outputs } = options;
const data = await this.neuralNetworkData.loadDataFromUrl(dataUrl, inputs, outputs);
// once the data are loaded, create the metadata
// and prep the data for training
// if the inputs are defined as an array of [img_width, img_height, channels]
this.createMetadata(data);
this.prepareForTraining(data);
}
/**
* ////////////////////////////////////////////////////////////
* Metadata prep
* ////////////////////////////////////////////////////////////
*/
createMetaData(dataRaw) {
const { inputs } = this.options;
let inputShape;
if (Array.isArray(inputs) && inputs.length > 0) {
inputShape =
inputs.every(item => typeof item === 'number') && inputs.length > 0 ? inputs : null;
}
this.neuralNetworkData.createMetadata(dataRaw, inputShape);
}
/**
* ////////////////////////////////////////////////////////////
* Data prep and handling
* ////////////////////////////////////////////////////////////
*/
/**
* Prepare data for training by applying oneHot to raw
* @param {*} dataRaw
*/
prepareForTraining(_dataRaw = null) {
const dataRaw = _dataRaw === null ? this.neuralNetworkData.data.raw : _dataRaw;
const unnormalizedTrainingData = this.neuralNetworkData.applyOneHotEncodingsToDataRaw(dataRaw);
this.data.training = unnormalizedTrainingData;
this.neuralNetworkData.isWarmedUp = true;
return unnormalizedTrainingData;
}
/**
* normalizeData
* @param {*} _dataRaw
* @param {*} _meta
*/
normalizeData(_dataRaw = null) {
const dataRaw = _dataRaw === null ? this.neuralNetworkData.data.raw : _dataRaw;
if (!this.neuralNetworkData.isMetadataReady) {
// if the inputs are defined as an array of [img_width, img_height, channels]
this.createMetaData(dataRaw);
}
if (!this.neuralNetworkData.isWarmedUp) {
this.prepareForTraining(dataRaw);
}
const trainingData = this.neuralNetworkData.normalizeDataRaw(dataRaw);
// set this equal to the training data
this.data.training = trainingData;
// set isNormalized to true
this.neuralNetworkData.meta.isNormalized = true;
return trainingData;
}
/**
* normalize the input value
* @param {*} value
* @param {*} _key
* @param {*} _meta
*/
// eslint-disable-next-line class-methods-use-this
normalizeInput(value, _key, _meta) {
const key = _key;
const { min, max } = _meta[key];
return nnUtils.normalizeValue(value, min, max);
}
/**
* search though the xInputs and format for adding to data.raws
* @param {*} input
*/
searchAndFormat(input) {
let formattedInputs;
if (Array.isArray(input)) {
formattedInputs = input.map(item => this.formatInputItem(item));
} else if (typeof input === 'object') {
const newXInputs = Object.assign({}, input);
Object.keys(input).forEach(k => {
const val = input[k];
newXInputs[k] = this.formatInputItem(val);
});
formattedInputs = newXInputs;
}
return formattedInputs;
}
/**
* Returns either the original input or a pixelArray[]
* @param {*} input
*/
// eslint-disable-next-line class-methods-use-this
formatInputItem(input) {
let imgToPredict;
let formattedInputs;
if (isInstanceOfSupportedElement(input)) {
imgToPredict = input;
} else if (typeof input === 'object' && isInstanceOfSupportedElement(input.elt)) {
imgToPredict = input.elt; // Handle p5.js image and video.
} else if (typeof input === 'object' && isInstanceOfSupportedElement(input.canvas)) {
imgToPredict = input.canvas; // Handle p5.js image and video.
}
if (imgToPredict) {
formattedInputs = imgToPixelArray(imgToPredict);
} else {
formattedInputs = input;
}
return formattedInputs;
}
/**
* convertTrainingDataToTensors
* @param {*} _trainingData
* @param {*} _meta
*/
convertTrainingDataToTensors(_trainingData = null, _meta = null) {
const trainingData = _trainingData === null ? this.data.training : _trainingData;
const meta = _meta === null ? this.neuralNetworkData.meta : _meta;
return this.neuralNetworkData.convertRawToTensors(trainingData, meta);
}
/**
* format the inputs for prediction
* this means applying onehot or normalization
* so that the user can use original data units rather
* than having to normalize
* @param {*} _input
* @param {*} meta
* @param {*} inputHeaders
*/
formatInputsForPrediction(_input, meta, inputHeaders) {
let inputData = [];
// TODO: check to see if it is a nested array
// to run predict or classify on a batch of data
if (_input instanceof Array) {
inputData = inputHeaders.map((prop, idx) => {
return this.isOneHotEncodedOrNormalized(_input[idx], prop, meta.inputs);
});
} else if (_input instanceof Object) {
// TODO: make sure that the input order is preserved!
inputData = inputHeaders.map(prop => {
return this.isOneHotEncodedOrNormalized(_input[prop], prop, meta.inputs);
});
}
// inputData = tf.tensor([inputData.flat()])
inputData = inputData.flat();
return inputData;
}
/**
* formatInputsForPredictionAll
* @param {*} _input
* @param {*} meta
* @param {*} inputHeaders
*/
formatInputsForPredictionAll(_input, meta, inputHeaders) {
let output;
if (_input instanceof Array) {
if (_input.every(item => Array.isArray(item))) {
output = _input.map(item => {
return this.formatInputsForPrediction(item, meta, inputHeaders);
});
return tf.tensor(output, [_input.length, inputHeaders.length]);
}
output = this.formatInputsForPrediction(_input, meta, inputHeaders);
return tf.tensor([output]);
}
output = this.formatInputsForPrediction(_input, meta, inputHeaders);
return tf.tensor([output]);
}
/**
* check if the input needs to be onehot encoded or
* normalized
* @param {*} _input
* @param {*} _meta
*/
// eslint-disable-next-line class-methods-use-this
isOneHotEncodedOrNormalized(_input, _key, _meta) {
const input = _input;
const key = _key;
let output;
if (typeof _input !== 'number') {
output = _meta[key].legend[input];
} else {
output = _input;
if (this.neuralNetworkData.meta.isNormalized) {
output = this.normalizeInput(_input, key, _meta);
}
}
return output;
}
/**
* ////////////////////////////////////////////////////////////
* Model prep
* ////////////////////////////////////////////////////////////
*/
/**
* train
* @param {*} optionsOrCallback
* @param {*} optionsOrWhileTraining
* @param {*} callback
*/
train(optionsOrCallback, optionsOrWhileTraining, callback) {
let options;
let whileTrainingCb;
let finishedTrainingCb;
if (
typeof optionsOrCallback === 'object' &&
typeof optionsOrWhileTraining === 'function' &&
typeof callback === 'function'
) {
options = optionsOrCallback;
whileTrainingCb = optionsOrWhileTraining;
finishedTrainingCb = callback;
} else if (
typeof optionsOrCallback === 'object' &&
typeof optionsOrWhileTraining === 'function'
) {
options = optionsOrCallback;
whileTrainingCb = null;
finishedTrainingCb = optionsOrWhileTraining;
} else if (
typeof optionsOrCallback === 'function' &&
typeof optionsOrWhileTraining === 'function'
) {
options = {};
whileTrainingCb = optionsOrCallback;
finishedTrainingCb = optionsOrWhileTraining;
} else {
options = {};
whileTrainingCb = null;
finishedTrainingCb = optionsOrCallback;
}
this.trainInternal(options, whileTrainingCb, finishedTrainingCb);
}
/**
* train
* @param {*} _options
* @param {*} _cb
*/
trainInternal(_options, whileTrainingCb, finishedTrainingCb) {
const options = {
epochs: 10,
batchSize: 32,
validationSplit: 0.1,
whileTraining: null,
..._options,
};
// if debug mode is true, then use tf vis
if (this.options.debug === true) {
options.whileTraining = [
this.neuralNetworkVis.trainingVis(),
{
onEpochEnd: null,
},
];
} else {
// if not use the default training
// options.whileTraining = whileTrainingCb === null ? [{
// onEpochEnd: (epoch, loss) => {
// console.log(epoch, loss.loss)
// }
// }] :
// [{
// onEpochEnd: whileTrainingCb
// }];
options.whileTraining = [
{
onEpochEnd: whileTrainingCb,
},
];
}
// if metadata needs to be generated about the data
if (!this.neuralNetworkData.isMetadataReady) {
// if the inputs are defined as an array of [img_width, img_height, channels]
this.createMetaData(this.neuralNetworkData.data.raw);
}
// if the data still need to be summarized, onehotencoded, etc
if (!this.neuralNetworkData.isWarmedUp) {
this.prepareForTraining(this.neuralNetworkData.data.raw);
}
// if inputs and outputs are not specified
// in the options, then create the tensors
// from the this.neuralNetworkData.data.raws
if (!options.inputs && !options.outputs) {
const { inputs, outputs } = this.convertTrainingDataToTensors();
options.inputs = inputs;
options.outputs = outputs;
}
// check to see if layers are passed into the constructor
// then use those to create your architecture
if (!this.neuralNetwork.isLayered) {
this.options.layers = this.createNetworkLayers(
this.options.layers,
this.neuralNetworkData.meta,
);
}
// if the model does not have any layers defined yet
// then use the default structure
if (!this.neuralNetwork.isLayered) {
this.options.layers = this.addDefaultLayers(this.options.task, this.neuralNetworkData.meta);
}
if (!this.neuralNetwork.isCompiled) {
// compile the model with defaults
this.compile();
}
// train once the model is compiled
this.neuralNetwork.train(options, finishedTrainingCb);
}
/**
* addLayer
* @param {*} options
*/
addLayer(options) {
this.neuralNetwork.addLayer(options);
}
/**
* add custom layers in options
*/
createNetworkLayers(layerJsonArray, meta) {
const layers = [...layerJsonArray];
const { inputUnits, outputUnits } = Object.assign({}, meta);
const layersLength = layers.length;
if (!(layers.length >= 2)) {
return false;
}
// set the inputShape
layers[0].inputShape = layers[0].inputShape ? layers[0].inputShape : inputUnits;
// set the output units
const lastIndex = layersLength - 1;
const lastLayer = layers[lastIndex];
lastLayer.units = lastLayer.units ? lastLayer.units : outputUnits;
layers.forEach(layer => {
this.addLayer(tf.layers[layer.type](layer));
});
return layers;
}
// /**
// * createDenseLayer
// * @param {*} _options
// */
// // eslint-disable-next-line class-methods-use-this
// createDenseLayer(_options) {
// const options = Object.assign({}, {
// units: this.options.hiddenUnits,
// activation: 'relu',
// ..._options
// });
// return tf.layers.dense(options);
// }
// /**
// * createConv2dLayer
// * @param {*} _options
// */
// // eslint-disable-next-line class-methods-use-this
// createConv2dLayer(_options) {
// const options = Object.assign({}, {
// kernelSize: 5,
// filters: 8,
// strides: 1,
// activation: 'relu',
// kernelInitializer: 'varianceScaling',
// ..._options
// })
// return tf.layers.conv2d(options);
// }
/**
* addDefaultLayers
* @param {*} _task
*/
addDefaultLayers(task, meta) {
let layers;
switch (task.toLowerCase()) {
// if the task is classification
case 'classification':
layers = [
{
type: 'dense',
units: this.options.hiddenUnits,
activation: 'relu',
},
{
type: 'dense',
activation: 'softmax',
},
];
return this.createNetworkLayers(layers, meta);
// if the task is regression
case 'regression':
layers = [
{
type: 'dense',
units: this.options.hiddenUnits,
activation: 'relu',
},
{
type: 'dense',
activation: 'sigmoid',
},
];
return this.createNetworkLayers(layers, meta);
// if the task is imageClassification
case 'imageclassification':
layers = [
{
type: 'conv2d',
filters: 2,
kernelSize: 2,
strides: 2,
activation: 'relu',
kernelInitializer: 'varianceScaling',
},
{
type: 'maxPooling2d',
poolSize: [1, 1],
strides: [1, 1],
},
{
type: 'conv2d',
filters: 1,
kernelSize: 1,
strides: 1,
activation: 'relu',
kernelInitializer: 'varianceScaling',
},
{
type: 'maxPooling2d',
poolSize: [1, 1],
strides: [1, 1],
},
{
type: 'flatten',
},
{
type: 'dense',
kernelInitializer: 'varianceScaling',
activation: 'softmax',
},
];
return this.createNetworkLayers(layers, meta);
default:
console.log('no imputUnits or outputUnits defined');
layers = [
{
type: 'dense',
units: this.options.hiddenUnits,
activation: 'relu',
},
{
type: 'dense',
activation: 'sigmoid',
},
];
return this.createNetworkLayers(layers, meta);
}
}
/**
* compile the model
* @param {*} _options
*/
compile(_modelOptions = null, _learningRate = null) {
const LEARNING_RATE = _learningRate === null ? this.options.learningRate : _learningRate;
let options = {};
if (_modelOptions !== null) {
options = {
..._modelOptions,
};
} else if (
this.options.task === 'classification' ||
this.options.task === 'imageClassification'
) {
options = {
loss: 'categoricalCrossentropy',
optimizer: tf.train.sgd,
metrics: ['accuracy'],
};
} else if (this.options.task === 'regression') {
options = {
loss: 'meanSquaredError',
optimizer: tf.train.adam,
metrics: ['accuracy'],
};
}
options.optimizer = options.optimizer
? this.neuralNetwork.setOptimizerFunction(LEARNING_RATE, options.optimizer)
: this.neuralNetwork.setOptimizerFunction(LEARNING_RATE, tf.train.sgd);
this.neuralNetwork.compile(options);
// if debug mode is true, then show the model summary
if (this.options.debug) {
this.neuralNetworkVis.modelSummary(
{
name: 'Model Summary',
},
this.neuralNetwork.model,
);
}
}
/**
* ////////////////////////////////////////////////////////////
* Prediction / classification
* ////////////////////////////////////////////////////////////
*/
/**
* predict
* @param {*} _input
* @param {*} _cb
*/
predict(_input, _cb) {
return callCallback(this.predictInternal(_input), _cb);
}
/**
* predictMultiple
* @param {*} _input
* @param {*} _cb
*/
predictMultiple(_input, _cb) {
return callCallback(this.predictInternal(_input), _cb);
}
/**
* classify
* @param {*} _input
* @param {*} _cb
*/
classify(_input, _cb) {
return callCallback(this.classifyInternal(_input), _cb);
}
/**
* classifyMultiple
* @param {*} _input
* @param {*} _cb
*/
classifyMultiple(_input, _cb) {
return callCallback(this.classifyInternal(_input), _cb);
}
/**
* predict
* @param {*} _input
* @param {*} _cb
*/
async predictInternal(_input) {
const { meta } = this.neuralNetworkData;
const headers = Object.keys(meta.inputs);
const inputData = this.formatInputsForPredictionAll(_input, meta, headers);
const unformattedResults = await this.neuralNetwork.predict(inputData);
inputData.dispose();
if (meta !== null) {
const labels = Object.keys(meta.outputs);
const formattedResults = unformattedResults.map(unformattedResult => {
return labels.map((item, idx) => {
// check to see if the data were normalized
// if not, then send back the values, otherwise
// unnormalize then return
let val;
let unNormalized;
if (meta.isNormalized) {
const { min, max } = meta.outputs[item];
val = nnUtils.unnormalizeValue(unformattedResult[idx], min, max);
unNormalized = unformattedResult[idx];
} else {
val = unformattedResult[idx];
}
const d = {
[labels[idx]]: val,
label: item,
value: val,
};
// if unNormalized is not undefined, then
// add that to the output
if (unNormalized) {
d.unNormalizedValue = unNormalized;
}
return d;
});
});
// return single array if the length is less than 2,
// otherwise return array of arrays
if (formattedResults.length < 2) {
return formattedResults[0];
}
return formattedResults;
}
// if no meta exists, then return unformatted results;
return unformattedResults;
}
/**
* classify
* @param {*} _input
* @param {*} _cb
*/
async classifyInternal(_input) {
const { meta } = this.neuralNetworkData;
const headers = Object.keys(meta.inputs);
let inputData;
if (this.options.task === 'imageClassification') {
// get the inputData for classification
// if it is a image type format it and
// flatten it
inputData = this.searchAndFormat(_input);
if (Array.isArray(inputData)) {
inputData = inputData.flat();
} else {
inputData = inputData[headers[0]];
}
if (meta.isNormalized) {
// TODO: check to make sure this property is not static!!!!
const { min, max } = meta.inputs[headers[0]];
inputData = this.neuralNetworkData.normalizeArray(Array.from(inputData), { min, max });
} else {
inputData = Array.from(inputData);
}
inputData = tf.tensor([inputData], [1, ...meta.inputUnits]);
} else {
inputData = this.formatInputsForPredictionAll(_input, meta, headers);
}
const unformattedResults = await this.neuralNetwork.classify(inputData);
inputData.dispose();
if (meta !== null) {
const label = Object.keys(meta.outputs)[0];
const vals = Object.entries(meta.outputs[label].legend);
const formattedResults = unformattedResults.map(unformattedResult => {
return vals
.map((item, idx) => {
return {
[item[0]]: unformattedResult[idx],
label: item[0],
confidence: unformattedResult[idx],
};
})
.sort((a, b) => b.confidence - a.confidence);
});
// return single array if the length is less than 2,
// otherwise return array of arrays
if (formattedResults.length < 2) {
return formattedResults[0];
}
return formattedResults;
}
return unformattedResults;
}
/**
* ////////////////////////////////////////////////////////////
* Save / Load Data
* ////////////////////////////////////////////////////////////
*/
/**
* save data
* @param {*} name
*/
saveData(name) {
this.neuralNetworkData.saveData(name);
}
/**
* load data
* @param {*} filesOrPath
* @param {*} callback
*/
async loadData(filesOrPath = null, callback) {
this.neuralNetworkData.loadData(filesOrPath, callback);
}
/**
* ////////////////////////////////////////////////////////////
* Save / Load Model
* ////////////////////////////////////////////////////////////
*/
/**
* saves the model, weights, and metadata
* @param {*} nameOrCb
* @param {*} cb
*/
save(nameOrCb, cb) {
let modelName;
let callback;
if (typeof nameOrCb === 'function') {
modelName = 'model';
callback = nameOrCb;
} else if (typeof nameOrCb === 'string') {
modelName = nameOrCb;
if (typeof cb === 'function') {
callback = cb;
}
} else {
modelName = 'model';
}
// save the model
this.neuralNetwork.save(modelName, () => {
this.neuralNetworkData.saveMeta(modelName, callback);
});
}
/**
* load a model and metadata
* @param {*} filesOrPath
* @param {*} callback
*/
async load(filesOrPath = null, cb) {
let callback;
if (cb) {
callback = cb;
}
this.neuralNetwork.load(filesOrPath, () => {
this.neuralNetworkData.loadMeta(filesOrPath, callback);
return this.neuralNetwork.model;
});
}
}
const neuralNetwork = (inputsOrOptions, outputsOrCallback, callback) => {
let options;
let cb;
if (inputsOrOptions instanceof Object) {
options = inputsOrOptions;
cb = outputsOrCallback;
} else {
options = {
inputs: inputsOrOptions,
outputs: outputsOrCallback,
};
cb = callback;
}
const instance = new DiyNeuralNetwork(options, cb);
return instance;
};
export default neuralNetwork;