fanny
Version:
FANN Fast Artificial Neural Network Node.JS Bindings
1,073 lines (984 loc) • 32.7 kB
JavaScript
// Copyright 2016 Zipscene, LLC
// Licensed under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
var createSchema = require('common-schema').createSchema;
var FieldError = require('common-schema').FieldError;
var XError = require('xerror');
var utils = require('./utils');
var createTrainingData = require('./training-data').createTrainingData;
var pasync = require('pasync');
var ACTIVATION_FUNCTIONS = [
'LINEAR', 'THRESHOLD', 'THRESHOLD_SYMMETRIC', 'SIGMOID', 'SIGMOID_STEPWISE',
'SIGMOID_SYMMETRIC', 'SIGMOID_SYMMETRIC_STEPWISE', 'GAUSSIAN', 'GAUSSIAN_SYMMETRIC',
'ELLIOT', 'ELLIOT_SYMMETRIC', 'LINEAR_PIECE', 'LINEAR_PIECE_SYMMETRIC', 'SIN_SYMMETRIC',
'COS_SYMMETRIC', 'SIN', 'COS'
];
var annConfigSchema = createSchema({
type: 'object',
properties: {
type: {
type: String,
default: 'standard',
enum: [ 'standard', 'sparse', 'shortcut' ]
},
layers: {
type: [ {
type: Number,
required: true,
min: 1
} ],
required: true,
validate: function(val) {
if (val.length < 2) throw new FieldError('invalid', 'Must have at least 2 layers');
}
},
connectionRate: {
type: Number,
min: 0,
max: 1
},
datatype: {
type: String,
default: 'float',
enum: [ 'float', 'double', 'fixed' ]
},
activationFunctions: {
type: 'map',
values: {
type: String,
required: true,
enum: ACTIVATION_FUNCTIONS
},
validate: validateNeuronConfigKey
},
activationSteepnesses: {
type: 'map',
values: Number,
validate: validateNeuronConfigKey
}
}
});
// Note that, for enums, common prefixes (such as "FANN_" or "TRAIN_" are removed
// and converted in the getValue and setValue functions, as with the trainingAlgorithm option)
var annOptionsSchema = createSchema({
trainingAlgorithm: {
type: String,
enum: [ 'INCREMENTAL', 'BATCH', 'RPROP', 'QUICKPROP', 'SARPROP' ]
},
learningRate: {
type: Number
},
trainErrorFunction: {
type: String,
enum: [ 'LINEAR', 'TANH' ]
},
quickpropDecay: {
type: Number
},
quickpropMu: {
type: Number,
min: 1
},
rpropIncreaseFactor: {
type: Number,
min: 1
},
rpropDecreaseFactor: {
type: Number,
max: 1 },
rpropDeltaZero: {
type: Number,
min: 0
},
rpropDeltaMin: {
type: Number,
min: 0
},
rpropDeltaMax: {
type: Number,
min: 0
},
sarpropWeightDecayShift: {
type: Number
},
sarpropStepErrorThresholdFactor: {
type: Number
},
sarpropStepErrorShift: {
type: Number
},
sarpropTemperature: {
type: Number
},
learningMomentum: {
type: Number
},
trainStopFunction: {
type: String,
enum: [ 'BIT', 'MSE' ]
},
bitFailLimit: {
type: Number
},
cascadeOutputChangeFraction: {
type: Number,
min: 0,
max: 1
},
cascadeOutputStagnationEpochs: {
type: Number
},
cascadeCandidateChangeFraction: {
type: Number,
min: 0,
max: 1
},
cascadeCandidateStagnationEpochs: {
type: Number
},
cascadeWeightMultiplier: {
type: Number
},
cascadeCandidateLimit: {
type: Number
},
cascadeMaxOutEpochs: {
type: Number
},
cascadeMaxCandEpochs: {
type: Number
},
cascadeActivationFunctions: {
type: 'array',
elements: {
type: String,
required: true,
enum: ACTIVATION_FUNCTIONS
}
},
cascadeActivationSteepnesses: {
type: 'array',
elements: {
type: 'number',
required: true
}
},
cascadeNumCandidateGroups: {
type: Number
},
userDataString: {
type: String
}
});
function validateNeuronConfigKey(val) {
var keyRegex = /^[0-9]+$|^[0-9]+-[0-9]+$|^hidden$|^output$/;
for (var key in val) {
if (!keyRegex.test(key)) {
var msg = 'Keys must be "hidden", "output", a layer number, ' +
'or <Layer>-<Neuron> for a single neuron.';
throw new FieldError('invalid', msg);
}
}
}
function wrapThrows(fn) {
return function() {
try {
return fn.apply(this, Array.prototype.slice.call(arguments, 0));
} catch (ex) {
if (!XError.isXError(ex)) {
throw new XError(ex);
} else {
throw ex;
}
}
};
}
function nextQueueOp(ann) {
if (ann._opQueue.length) {
ann._currentlyRunning = true;
var op = ann._opQueue.shift();
var promise;
try {
promise = op.fn.apply(ann, op.args);
} catch (ex) {
op.waiter.reject(ex);
nextQueueOp(ann);
return;
}
if (!promise || typeof promise.then !== 'function') {
op.waiter.resolve(promise);
nextQueueOp(ann);
return;
}
promise.then(function(res) {
op.waiter.resolve(res);
nextQueueOp(ann);
}, function(err) {
op.waiter.reject(err);
nextQueueOp(ann);
}).catch(pasync.abort);
} else {
ann._currentlyRunning = false;
}
}
function asyncOpQueue(fn) {
fn = wrapThrows(fn);
return function() {
var self = this;
var waiter = pasync.waiter();
self._opQueue.push({
fn: fn,
args: Array.prototype.slice.call(arguments, 0),
waiter: waiter
});
if (!self._currentlyRunning) nextQueueOp(self);
return waiter.promise;
};
};
function blockOnAsync(fn) {
return function() {
if (this._currentlyRunning) {
throw new XError(XError.INTERNAL_ERROR, 'Cannot execute this operation while training or running ann');
}
return fn.apply(this, Array.prototype.slice.call(arguments, 0));
};
};
function ANN(fanny, datatype) {
this.userData = {};
this._fanny = fanny;
this._datatype = datatype;
this._recalculateInfo();
this._opQueue = [];
this._currentlyRunning = false;
var userDataString = this.getOption('userDataString');
if (userDataString && userDataString[0] === '{') {
this.userData = JSON.parse(userDataString);
}
}
var annOptionFunctions = {
trainingAlgorithm: {
setValue: function(value) {
var fannTrainingAlgorithmName = (value === 'SARPROP') ? 'FANN_TRAIN_SARPROP' : ('TRAIN_' + value);
this._fanny.setTrainingAlgorithm(fannTrainingAlgorithmName);
},
getValue: function() {
var fannTrainingAlgorithmName = this._fanny.getTrainingAlgorithm();
if (fannTrainingAlgorithmName === 'FANN_TRAIN_SARPROP') return 'SARPROP';
if (fannTrainingAlgorithmName.slice(0, 6) === 'TRAIN_') return fannTrainingAlgorithmName.slice(6);
return fannTrainingAlgorithmName;
}
},
learningRate: {
setValue: function(value) {
this._fanny.setLearningRate(value);
},
getValue: function() {
return this._fanny.getLearningRate();
}
},
trainErrorFunction: {
setValue: function(value) {
this._fanny.setTrainErrorFunction('ERRORFUNC_' + value);
},
getValue: function() {
var trainErrorFunction = this._fanny.getTrainErrorFunction();
if (trainErrorFunction.slice(0, 10) === 'ERRORFUNC_') return trainErrorFunction.slice(10);
return trainErrorFunction;
}
},
quickpropDecay: {
setValue: function(value) {
this._fanny.setQuickpropDecay(value);
},
getValue: function() {
return this._fanny.getQuickpropDecay();
}
},
quickpropMu: {
setValue: function(value) {
this._fanny.setQuickpropMu(value);
},
getValue: function() {
return this._fanny.getQuickpropMu();
}
},
rpropIncreaseFactor: {
setValue: function(value) {
this._fanny.setRpropIncreaseFactor(value);
},
getValue: function() {
return this._fanny.getRpropIncreaseFactor();
}
},
rpropDecreaseFactor: {
setValue: function(value) {
this._fanny.setRpropDecreaseFactor(value);
},
getValue: function() {
return this._fanny.getRpropDecreaseFactor();
}
},
rpropDeltaZero: {
setValue: function(value) {
this._fanny.setRpropDeltaZero(value);
},
getValue: function() {
return this._fanny.getRpropDeltaZero();
}
},
rpropDeltaMax: {
setValue: function(value) {
this._fanny.setRpropDeltaMax(value);
},
getValue: function() {
return this._fanny.getRpropDeltaMax();
}
},
rpropDeltaMin: {
setValue: function(value) {
this._fanny.setRpropDeltaMax(value);
},
getValue: function() {
return this._fanny.getRpropDeltaMax();
}
},
sarpropWeightDecayShift: {
setValue: function(value) {
this._fanny.setSarpropWeightDecayShift(value);
},
getValue: function() {
return this._fanny.getSarpropWeightDecayShift();
}
},
sarpropStepErrorThresholdFactor: {
setValue: function(value) {
this._fanny.setSarpropStepErrorThresholdFactor(value);
},
getValue: function() {
return this._fanny.getSarpropStepErrorThresholdFactor();
}
},
sarpropStepErrorShift: {
setValue: function(value) {
this._fanny.setSarpropStepErrorShift(value);
},
getValue: function() {
return this._fanny.getSarpropStepErrorShift();
}
},
sarpropTemperature: {
setValue: function(value) {
this._fanny.setSarpropTemperature(value);
},
getValue: function() {
return this._fanny.getSarpropTemperature();
}
},
learningMomentum: {
setValue: function(value) {
this._fanny.setLearningMomentum(value);
},
getValue: function() {
return this._fanny.getLearningMomentum();
}
},
trainStopFunction: {
setValue: function(value) {
this._fanny.setTrainStopFunction('STOPFUNC_' + value);
},
getValue: function() {
var trainStopFunction = this._fanny.getTrainStopFunction();
if (trainStopFunction.slice(0, 9) === 'STOPFUNC_') return trainStopFunction.slice(9);
return trainStopFunction;
}
},
bitFailLimit: {
setValue: function(value) {
this._fanny.setBitFailLimit(value);
},
getValue: function() {
return this._fanny.getBitFailLimit();
}
},
cascadeOutputChangeFraction: {
setValue: function(value) {
this._fanny.setCascadeOutputChangeFraction(value);
},
getValue: function() {
return this._fanny.getCascadeOutputChangeFraction();
}
},
cascadeOutputStagnationEpochs: {
setValue: function(value) {
this._fanny.setCascadeOutputStagnationEpochs(value);
},
getValue: function() {
return this._fanny.getCascadeOutputStagnationEpochs();
}
},
cascadeCandidateChangeFraction: {
setValue: function(value) {
this._fanny.setCascadeCandidateChangeFraction(value);
},
getValue: function() {
return this._fanny.getCascadeCandidateChangeFraction();
}
},
cascadeCandidateStagnationEpochs: {
setValue: function(value) {
this._fanny.setCascadeCandidateStagnationEpochs(value);
},
getValue: function() {
return this._fanny.getCascadeCandidateStagnationEpochs();
}
},
cascadeWeightMultiplier: {
setValue: function(value) {
this._fanny.setCascadeWeightMultiplier(value);
},
getValue: function() {
return this._fanny.getCascadeWeightMultiplier();
}
},
cascadeCandidateLimit: {
setValue: function(value) {
this._fanny.setCascadeCandidateLimit(value);
},
getValue: function() {
return this._fanny.getCascadeCandidateLimit();
}
},
cascadeMaxOutEpochs: {
setValue: function(value) {
this._fanny.setCascadeMaxOutEpochs(value);
},
getValue: function() {
return this._fanny.getCascadeMaxOutEpochs();
}
},
cascadeMaxCandEpochs: {
setValue: function(value) {
this._fanny.setCascadeMaxCandEpochs(value);
},
getValue: function() {
return this._fanny.getCascadeMaxCandEpochs();
}
},
cascadeActivationFunctions: {
setValue: function(value) {
this._fanny.setCascadeActivationFunctions(value, value.length);
},
getValue: function() {
return this._fanny.getCascadeActivationFunctions();
}
},
cascadeActivationSteepnesses: {
setValue: function(value) {
this._fanny.setCascadeActivationSteepnesses(value, value.length);
},
getValue: function() {
return this._fanny.getCascadeActivationSteepnesses();
}
},
cascadeNumCandidateGroups: {
setValue: function(value) {
this._fanny.setCascadeNumCandidateGroups(value);
},
getValue: function() {
return this._fanny.getCascadeNumCandidateGroups();
}
},
userDataString: {
setValue: function(value) {
this._fanny.setUserDataString(value);
if (value && value[0] === '{') {
this.userData = JSON.parse(value);
} else {
this.userData = {};
}
},
getValue: function() {
return this._fanny.getUserDataString();
}
}
};
ANN.prototype.setOptions = blockOnAsync(function(options) {
annOptionsSchema.normalize(options);
for (var key in options) {
if (!annOptionFunctions[key] || !annOptionFunctions[key].setValue) throw new XError(XError.INTERNAL_ERROR, 'No setter for option ' + key);
annOptionFunctions[key].setValue.call(this, options[key]);
}
this._recalculateInfo();
});
ANN.prototype.getOption = function(name) {
if (!annOptionFunctions[name] || !annOptionFunctions[name].getValue) throw new XError(XError.INTERNAL_ERROR, 'No getter for option ' + name);
return annOptionFunctions[name].getValue.call(this);
};
ANN.prototype.setOption = function(name, value) {
var obj = {};
obj[name] = value;
return this.setOptions(obj);
};
ANN.prototype.getOptions = function() {
var ret = {};
for (var key in annOptionFunctions) {
ret[key] = annOptionFunctions.getValue.call(this);
}
return ret;
};
ANN.prototype.clone = blockOnAsync(function() {
var addon = utils.getAddon(this._datatype);
var fanny = new addon.FANNY(this._fanny);
return new ANN(fanny, this._datatype);
});
ANN.prototype._recalculateInfo = function() {
this.info = {};
var fns = {
numInput: 'getNumInput',
numOutput: 'getNumOutput',
totalNeurons: 'getTotalNeurons',
totalConnections: 'getTotalConnections',
decimalPoint: 'getDecimalPoint',
multiplier: 'getMultiplier',
networkType: 'getNetworkType',
connectionRate: 'getConnectionRate',
numLayers: 'getNumLayers'
};
for (var key in fns) {
if (this._fanny[fns[key]]) {
this.info[key] = this._fanny[fns[key]]();
}
}
};
ANN.prototype.save = asyncOpQueue(function(filename, toFixed) {
var self = this;
return new Promise(function(resolve, reject) {
var cb = function(err) {
if (err) return reject(err);
resolve();
};
var curUserDataString = self.getOption('userDataString');
if (!curUserDataString || curUserDataString[0] === '{') {
self._fanny.setUserDataString(JSON.stringify(self.userData));
}
if (toFixed) {
self._fanny.saveToFixed(filename, cb);
} else {
self._fanny.save(filename, cb);
}
});
});
// data can either be a TrainingData class or a filename
// options can include: maxEpochs, progressInterval (in epochs), desiredError, cascade (boolean true for cascade training),
// maxNeurons (for cascade training), stopFunction (either "MSE" or "BIT"). Without supplying an options object, this
// only trains a single epoch.
// Options for cascading: maxNeurons (default 10000), progressInterval, desiredError
// Options for non-cascading: maxEpochs (default 2000000000), progressInerval, desiredError
// progress is an optional callback that is periodically called for multi-epoch training. It receives a single
// parameter: an object containing the keys "epochs", "neurons", "mse", and "bitfail". If
// this progress function returns false or -1, training is cancelled on the next iteration.
// Instead of a function, you can instead pass the special value "default", to enable the default libfann
// behavior of printing out progress information.
ANN.prototype.train = asyncOpQueue(function(data, options, progress) {
if (Array.isArray(data) && Array.isArray(options)) return this.trainOne(data, options);
var self = this;
var filename;
var addonTrainingData;
if (Array.isArray(data)) data = createTrainingData(data);
if (data && typeof data === 'object' && typeof data.setTrainData === 'function') {
addonTrainingData = data;
} else if (data && typeof data === 'object' && typeof data.setData === 'function') {
addonTrainingData = data._fannyTrainingData;
} else if (typeof data === 'string') {
filename = data;
} else {
throw new XError(XError.INVALID_ARGUMENT, 'Invalid training data type');
}
if (!options) {
if (!addonTrainingData) throw new XError(XError.INVALID_ARGUMENT, 'Cannot train single epoch from file');
return new Promise(function(resolve, reject) {
self._fanny.trainEpoch(addonTrainingData, function(err, res) {
if (err) return reject(new XError(err));
resolve(res);
});
});
}
if (!options.maxEpochs && !options.maxNeurons && typeof options.desiredError !== 'number') options.desiredError = 0.01;
if (!options.maxEpochs) options.maxEpochs = 2000000000;
if (!options.maxNeurons) options.maxNeurons = 10000;
if (typeof options.desiredError !== 'number') options.desiredError = 0;
if (!options.progressInterval) options.progressInterval = 1;
if (options.stopFunction) {
self._fanny.setTrainStopFunction('STOPFUNC_' + options.stopFunction);
}
if (progress === 'default') {
self._fanny.setCallback();
} else if (typeof progress === 'function') {
self._fanny.setCallback(function(info) {
var result = progress(info);
if (result === false || result === -1) return -1;
});
} else {
self._fanny.setCallback(function() {});
}
return new Promise(function(resolve, reject) {
var cb = function(err, res) {
if (err && !res) return reject(new XError(err));
self._recalculateInfo();
if (err && err.message === 'canceled') {
self.userData.canceled = true;
self.info.canceled = true;
}
resolve(res);
};
var args = [
addonTrainingData || filename,
options.cascade ? options.maxNeurons : options.maxEpochs,
options.progressInterval,
options.desiredError,
cb
];
if (!options.cascade) {
if (filename) {
self._fanny.trainOnFile.apply(self._fanny, args);
} else {
self._fanny.trainOnData.apply(self._fanny, args);
}
} else {
if (filename) {
self._fanny.cascadetrainOnFile.apply(self._fanny, args);
} else {
self._fanny.cascadetrainOnData.apply(self._fanny, args);
}
}
});
});
ANN.prototype.run = blockOnAsync(function(inputs) {
return this._fanny.run(inputs);
});
ANN.prototype.runAsync = asyncOpQueue(function(inputs) {
var self = this;
return new Promise(function(resolve, reject) {
self._fanny.runAsync(inputs, function(err, res) {
if (err) return reject(new XError(err));
resolve(res);
});
});
});
ANN.prototype.randomizeWeights = blockOnAsync(wrapThrows(function(min, max) {
if (typeof min !== 'number' || Number.isNaN(min)) throw new XError(XError.INVALID_ARGUMENT, 'min must be a number');
if (typeof max !== 'number' || Number.isNaN(max)) throw new XError(XError.INVALID_ARGUMENT, 'max must be a number');
return this._fanny.randomizeWeights(min, max);
}));
ANN.prototype.initWeights = blockOnAsync(wrapThrows(function(data) {
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instanceof TrianingData');
}
return this._fanny.initWeights(data._fannyTrainingData);
}));
ANN.prototype.printConnections = function() {
return this._fanny.printConnections();
};
ANN.prototype.printParameters = function() {
return this._fanny.printParameters();
};
ANN.prototype.getConnectionArray = function() {
return this._fanny.getConnectionArray();
};
ANN.prototype.getBitFail = function() {
return this._fanny.getBitFail();
};
ANN.prototype.getMSE = function() {
return this._fanny.getMSE();
};
ANN.prototype.resetMSE = blockOnAsync(function() {
return this._fanny.resetMSE();
});
ANN.prototype.getActivationFunction = wrapThrows(function(layer, neruon) {
if (typeof layer !== 'number' || Number.isNaN(layer)) throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number');
if (typeof neruon !== 'number' || Number.isNaN(neruon)) throw new XError(XError.INVALID_ARGUMENT, 'neruon must be a number');
var activationFunction = this._fanny.getActivationFunction(layer, neruon);
return activationFunction;
});
ANN.prototype.setActivationFunction = blockOnAsync(wrapThrows(function(activationFunction, layer, neruon) {
if (typeof activationFunction !== 'string') throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be a string');
if (!ACTIVATION_FUNCTIONS.find(function(name) { return name === activationFunction })) {
throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be included in ' + ACTIVATION_FUNCTIONS.join(', '));
}
if (typeof layer !== 'number' || Number.isNaN(layer)) throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number');
if (typeof neruon !== 'number' || Number.isNaN(neruon)) throw new XError(XError.INVALID_ARGUMENT, 'neruon must be a number');
return this._fanny.setActivationFunction(activationFunction, layer, neruon);
}));
ANN.prototype.setActivationFunctionLayer = blockOnAsync(wrapThrows(function(activationFunction, layer) {
// check activationFunction from enum
if (typeof activationFunction !== 'string') throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be a string');
if (!ACTIVATION_FUNCTIONS.find(function(name) { return name === activationFunction })) {
throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be included in ' + ACTIVATION_FUNCTIONS.join(', '));
}
if (typeof layer !== 'number' || Number.isNaN(layer)) throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number');
return this._fanny.setActivationFunctionLayer(activationFunction, layer);
}));
ANN.prototype.setActivationFunctionHidden = blockOnAsync(wrapThrows(function(activationFunction) {
if (typeof activationFunction !== 'string') throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be a string');
if (!ACTIVATION_FUNCTIONS.find(function(name) { return name === activationFunction })) {
throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be included in ' + ACTIVATION_FUNCTIONS.join(', '));
}
return this._fanny.setActivationFunctionHidden(activationFunction);
}));
ANN.prototype.setActivationFunctionOutput = blockOnAsync(wrapThrows(function(activationFunction) {
if (typeof activationFunction !== 'string') throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be a string');
if (!ACTIVATION_FUNCTIONS.find(function(name) { return name === activationFunction })) {
throw new XError(XError.INVALID_ARGUMENT, 'activationFunction must be included in ' + ACTIVATION_FUNCTIONS.join(', '));
}
return this._fanny.setActivationFunctionOutput(activationFunction);
}));
ANN.prototype.getLayerArray = function() {
return this._fanny.getLayerArray();
};
ANN.prototype.getBiasArray = function() {
return this._fanny.getBiasArray();
};
ANN.prototype.scaleTrainingData = blockOnAsync(wrapThrows(function(data) {
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instance of training data');
}
return this._fanny.scaleTrain(data._fannyTrainingData);
}));
ANN.prototype.descaleTrainingData = blockOnAsync(wrapThrows(function(data) {
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instance of training data');
}
return this._fanny.descaleTrain(data._fannyTrainingData);
}));
ANN.prototype.setInputScalingParams = blockOnAsync(wrapThrows(function(data, min, max) {
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instance of trainingData');
}
if (typeof min !== 'number' || Number.isNaN(min)) {
throw new XError(XError.INVALID_ARGUMENT, 'min must be a number');
}
if (typeof max !== 'number'|| Number.isNaN(max)) {
throw new XError(XError.INVALID_ARGUMENT, 'max must be a number');
}
return this._fanny.setInputScalingParams(data._fannyTrainingData, min, max);
}));
ANN.prototype.setOutputScalingParams = blockOnAsync(wrapThrows(function(data, min, max) {
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instance of trainingData');
}
if (typeof min !== 'number' || Number.isNaN(min)) {
throw new XError(XError.INVALID_ARGUMENT, 'min must be a number');
}
if (typeof max !== 'number' || Number.isNaN(max)) {
throw new XError(XError.INVALID_ARGUMENT, 'max must be a number');
}
return this._fanny.setOutputScalingParams(data._fannyTrainingData, min, max);
}));
ANN.prototype.setScalingParams = blockOnAsync(wrapThrows(function(data, inputMin, inputMax, outputMin, outputMax) {
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instance of trainingData');
}
if (typeof inputMin !== 'number' || Number.isNaN(inputMin)) {
throw new XError(XError.INVALID_ARGUMENT, 'inputMin must be a number');
}
if (typeof inputMax !== 'number' || Number.isNaN(inputMax)) {
throw new XError(XError.INVALID_ARGUMENT, 'inputMax must be a number');
}
if (typeof outputMin !== 'number' || Number.isNaN(outputMin)) {
throw new XError(XError.INVALID_ARGUMENT, 'outputMin must be a number');
}
if (typeof outputMax !== 'number' || Number.isNaN(outputMax)) {
throw new XError(XError.INVALID_ARGUMENT, 'outputMax must be a number');
}
return this._fanny.setScalingParams(data._fannyTrainingData, inputMin, inputMax, outputMin, outputMax);
}));
ANN.prototype.clearScalingParams = blockOnAsync(wrapThrows(function() {
return this._fanny.clearScalingParams();
}));
ANN.prototype.scaleInput = blockOnAsync(wrapThrows(function(input) {
if (!Array.isArray(input) || !input.length) {
throw new XError(XError.INVALID_ARGUMENT, 'input must an array');
}
return this._fanny.scaleInput(input);
}));
ANN.prototype.scaleOutput = blockOnAsync(wrapThrows(function(output) {
if (!Array.isArray(output) || !output.length) {
throw new XError(XError.INVALID_ARGUMENT, 'output must an array');
}
return this._fanny.scaleOutput(output);
}));
ANN.prototype.descaleInput = blockOnAsync(wrapThrows(function(input) {
if (!Array.isArray(input) || !input.length) {
throw new XError(XError.INVALID_ARGUMENT, 'input must an array');
}
return this._fanny.descaleInput(input);
}));
ANN.prototype.descaleOutput = blockOnAsync(wrapThrows(function(output) {
if (!Array.isArray(output) || !output.length) {
throw new XError(XError.INVALID_ARGUMENT, 'output must an array');
}
return this._fanny.descaleOutput(output);
}));
ANN.prototype.getActivationSteepness = wrapThrows(function(layer, neuron) {
if (typeof layer !== 'number' || Number.isNaN(layer)) {
throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number');
}
if (typeof neuron !== 'number' || Number.isNaN(neuron)) {
throw new XError(XError.INVALID_ARGUMENT, 'neuron must be a number');
}
return this._fanny.getActivationSteepness(layer, neuron);
});
ANN.prototype.setActivationSteepness = blockOnAsync(wrapThrows(function(steepness, layer, neuron) {
if (typeof steepness !== 'number' || Number.isNaN(steepness)) {
throw new XError(XError.INVALID_ARGUMENT, 'steepness must be a number');
}
if (typeof layer !== 'number' || Number.isNaN(layer) || layer < 0) {
throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number and greater or equal to 0');
}
if (typeof neuron !== 'number' || Number.isNaN(neuron) || neuron < 0) {
throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number and greater or equal to 0');
}
return this._fanny.setActivationSteepness(steepness, layer, neuron);
}));
ANN.prototype.setActivationSteepnessLayer = blockOnAsync(wrapThrows(function(steepness, layer) {
if (typeof steepness !== 'number' || Number.isNaN(steepness)) {
throw new XError(XError.INVALID_ARGUMENT, 'steepness must be a number');
}
if (typeof layer !== 'number' || Number.isNaN(layer) || layer < 0) {
throw new XError(XError.INVALID_ARGUMENT, 'layer must be a number and greater or equal to 0');
}
return this._fanny.setActivationSteepnessLayer(steepness, layer);
}));
ANN.prototype.setActivationSteepnessHidden = blockOnAsync(wrapThrows(function(steepness) {
if (typeof steepness !== 'number' || Number.isNaN(steepness)) {
throw new XError(XError.INVALID_ARGUMENT, 'steepness must be a number');
}
return this._fanny.setActivationSteepnessHidden(steepness);
}));
ANN.prototype.setActivationSteepnessOutput = blockOnAsync(wrapThrows(function(steepness) {
if (typeof steepness !== 'number' || Number.isNaN(steepness)) {
throw new XError(XError.INVALID_ARGUMENT, 'steepness must be a number');
}
return this._fanny.setActivationSteepnessOutput(steepness);
}));
ANN.prototype.setWeightArray = blockOnAsync(wrapThrows(function(connections) {
if (!Array.isArray(connections) || !connections.length) {
throw new XError(XError.INVALID_ARGUMENT, 'connections must be an array');
}
for (var i = 0; i < connections.length; ++i) {
if (!('toNeuron' in connections[i]) || !('fromNeuron' in connections[i]) || !('weight' in connections[i])) {
throw new XError(XError.INVALID_ARGUMENT, 'all connections must have toNeuron, fromNeuron and weight');
}
}
return this._fanny.setWeightArray(connections, connections.length);
}));
ANN.prototype.setWeight = blockOnAsync(wrapThrows(function(fromNeuron, toNeuron, weight) {
if (typeof fromNeuron !== 'number' || Number.isNaN(fromNeuron) || fromNeuron < 0) {
throw new XError(XError.INVALID_ARGUMENT, 'fromNeuron must be a number and greater or equal to 0');
}
if (typeof toNeuron !== 'number' || Number.isNaN(toNeuron) || toNeuron < 0) {
throw new XError(XError.INVALID_ARGUMENT, 'toNeuron must be a number and greater or equal to 0');
}
if (typeof weight !== 'number' || Number.isNaN(weight)) {
throw new XError(XError.INVALID_ARGUMENT, 'weight must be a number');
}
return this._fanny.setWeight(fromNeuron, toNeuron, weight);
}));
ANN.prototype.trainOne = blockOnAsync(wrapThrows(function(input, output) {
if (!Array.isArray(input) || !Array.isArray(output)) {
throw new XError(XError.INVALID_ARGUMENT, 'Both input and output should be arrays');
}
return this._fanny.train(input, output);
}));
ANN.prototype.testOne = blockOnAsync(wrapThrows(function(input, output) {
if (!Array.isArray(input) || !Array.isArray(output)) {
throw new XError(XError.INVALID_ARGUMENT, 'Both input and output should be arrays');
}
if (input.length !== this.info.numInput) {
throw new XError(XError.INVALID_ARGUMENT, 'Input must be the right length');
}
if (output.length !== this.info.numOutput) {
throw new XError(XError.INVALID_ARGUMENT, 'Output must be the right length');
}
return this._fanny.test(input, output);
}));
ANN.prototype.testData = asyncOpQueue(function(data) {
var self = this;
if (!data || !data._fannyTrainingData) {
throw new XError(XError.INVALID_ARGUMENT, 'data must be an instanceof TrianingData');
}
return new Promise(function(resolve, reject) {
self._fanny.testData(data._fannyTrainingData, function(err, res) {
if (err) return reject(new XError(err));
resolve(res);
});
});
});
for (var key in ANN.prototype) {
ANN.prototype[key] = wrapThrows(ANN.prototype[key]);
}
function createANN(config, options) {
if (!config) throw new XError(XError.INVALID_ARGUMENT, 'config is required');
if (Array.isArray(config)) config = { layers: config };
annConfigSchema.normalize(config);
var addon = utils.getAddon(config.datatype);
var fanny = new addon.FANNY(config);
if (config.activationFunctions) {
for (var key in config.activationFunctions) {
var value = config.activationFunctions[key];
if (key === 'hidden') {
fanny.setActivationFunctionHidden(value);
} else if (key === 'output') {
fanny.setActivationFunctionOutput(value);
} else if (/^[0-9]+$/.test(key)) {
fanny.setActivationFunctionLayer(value, parseInt(key));
} else {
var matches = /^([0-9]+)-([0-9]+)$/.exec(key);
if (matches) {
fanny.setActivationFunction(value, parseInt(matches[1]), parseInt(matches[2]));
}
}
}
}
if (config.activationSteepnesses) {
for (var key in config.activationSteepnesses) {
var value = config.activationSteepnesses[key];
if (key === 'hidden') {
fanny.setActivationSteepnessHidden(value);
} else if (key === 'output') {
fanny.setActivationSteepnessOutput(value);
} else if (/^[0-9]+$/.test(key)) {
fanny.setActivationSteepnessLayer(value, parseInt(key));
} else {
var matches = /^([0-9]+)-([0-9]+)$/.exec(key);
if (matches) {
fanny.setActivationSteepness(value, parseInt(matches[1]), parseInt(matches[2]));
}
}
}
}
var ann = new ANN(fanny, config.datatype);
// FANN seeds the libc PRNG every time a neural net is created. We want to disable this after the
// first time it's seeded.
addon.FANNY.disableSeedRand();
if (options) ann.setOptions(options);
return ann;
}
function loadANN(filename, datatype) {
if (!filename) throw new XError(XError.INVALID_ARGUMENT, 'filename is required');
if (!datatype) datatype = 'float';
var addon = utils.getAddon(datatype);
return new Promise(function(resolve, reject) {
addon.FANNY.loadFile(filename, function(err, fanny) {
if (err) return reject(new XError(err));
var ann = new ANN(fanny, datatype);
resolve(ann);
});
});
}
module.exports = {
createANN: createANN,
loadANN: loadANN,
annConfigSchema: annConfigSchema,
annOptionsSchema: annOptionsSchema
};