fanny
Version:
FANN Fast Artificial Neural Network Node.JS Bindings
588 lines (550 loc) • 22 kB
JavaScript
// Copyright 2016 Zipscene, LLC
// Licensed under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
var expect = require('chai').expect;
var fanny = require('../lib');
var annOptionsSchema = fanny.annOptionsSchema;
var createANN = fanny.createANN;
var loadANN = fanny.loadANN;
var createTrainingData = fanny.createTrainingData;
var loadTrainingData = fanny.loadTrainingData;
var XError = require('xerror');
// Inputs: A, B Outputs: AND, OR, NAND, NOR, XOR
var booleanTrainingData = [
[ [ 1, 0 ], [ 0, 1, 1, 0, 1 ] ],
[ [ 0, 1 ], [ 0, 1, 1, 0, 1 ] ],
[ [ 0, 0 ], [ 0, 0, 1, 1, 0 ] ],
[ [ 1, 1 ], [ 1, 1, 0, 0, 0 ] ]
];
function booleanThreshold(array) {
return array.map(function(elem) {
return (elem >= 0.5) ? 1 : 0;
});
}
describe('Tests', function() {
it('basic test', function() {
var ann = createANN({ layers: [ 2, 20, 20, 5 ] });
return ann.train(booleanTrainingData, { desiredError: 0, stopFunction: 'BIT' })
.then(function() {
expect(booleanThreshold(ann.run([ 1, 1 ]))).to.deep.equal([ 1, 1, 0, 0, 0 ]);
expect(booleanThreshold(ann.run([ 1, 0 ]))).to.deep.equal([ 0, 1, 1, 0, 1 ]);
});
});
it('cascade test', function() {
var ann = createANN({
layers: [ 2, 5 ],
type: 'shortcut'
}, {
bitFailLimit: 0.1
});
var trainOptions = {
desiredError: 0,
stopFunction: 'BIT',
cascade: true,
maxNeurons: 50
};
return ann.train(booleanTrainingData, trainOptions)
.then(function() {
expect(booleanThreshold(ann.run([ 1, 1 ]))).to.deep.equal([ 1, 1, 0, 0, 0 ]);
expect(booleanThreshold(ann.run([ 1, 0 ]))).to.deep.equal([ 0, 1, 1, 0, 1 ]);
});
});
it('user data', function() {
var ann = createANN({ layers: [ 2, 5, 2 ] });
ann.userData.foo = 'bar';
return ann.save('/tmp/fanny_test_save')
.then(function() { return fanny.loadANN('/tmp/fanny_test_save'); })
.then(function(ann) {
expect(ann.userData).to.deep.equal({ foo: 'bar' });
});
});
describe('Options Tests', function() {
var optionsToTest = annOptionsSchema.getData().properties;
var stringTest = function(optionToTest, validValues) {
var ann = createANN({ layers: [ 2, 20, 5 ] });
var initalValue = ann.getOption(optionToTest);
if (initalValue) expect(initalValue).to.be.a('string');
// Test all valid values
for (var testValue of validValues) {
ann.setOption(optionToTest, testValue);
var updatedValue = ann.getOption(optionToTest);
expect(updatedValue).to.equal(testValue);
}
};
var numberTest = function(optionToTest, min, max) {
// List of FANN methods known to fail to update value in a way that is observable
var numberExceptions = {
sarpropWeightDecayShift: true,
sarpropStepErrorThresholdFactor: true,
sarpropStepErrorShift: true,
sarpropTemperature: true
};
var ann = createANN({ layers: [ 2, 20, 5 ] });
var initalValue = ann.getOption(optionToTest);
var testIncrement = 1;
var hasMax = (typeof max === 'number');
var hasMin = (typeof min === 'number');
// Determine value to set setter with based on min, max and the intial value.
if (hasMax && hasMin && ((max - min) <= 1) ) testIncrement = 0.1;
if (max <= testIncrement) testIncrement = 0.1;
if (initalValue === max) testIncrement = 0 - testIncrement;
var testValue = initalValue + testIncrement;
var setTestFunc = function() { ann.setOption(optionToTest, testValue); }
expect(initalValue).to.be.a('number');
expect(setTestFunc).to.not.throw();
// If a optionToTest is a known exception don't test the value.
if (numberExceptions[optionToTest]) return;
var updatedValue = ann.getOption(optionToTest);
expect(updatedValue).to.be.closeTo(testValue, 1e-7);
};
var arrayTest = function(optionToTest, validArray) {
var ann = createANN({ layers: [ 2, 20, 5 ] });
var initalValue = ann.getOption(optionToTest);
var setTestFunc = function() { ann.setOption(optionToTest, validArray); }
expect(initalValue).to.be.an('array');
expect(setTestFunc).to.not.throw();
var updatedArray = ann.getOption(optionToTest);
if (typeof validArray[0] === 'number') expect(updatedArray).to.eql(validArray);
if (typeof validArray[0] === 'string') {
for (var i = 0; i < validArray.length; i++) {
expect(updatedArray[i]).to.include(validArray[i]);
}
}
};
var optionTestRunner = function(optionToTest) {
it(optionToTest + ' has working getters and setters', function() {
expect(optionsToTest).to.have.property(optionToTest);
var testSettings = optionsToTest[optionToTest];
expect(testSettings).to.have.property('type');
var testType = testSettings.type;
switch (testType) {
case 'string':
stringTest(optionToTest, testSettings.enum || [ 'test' ]);
break;
case 'number':
numberTest(optionToTest, testSettings.min, testSettings.max);
break;
case 'array':
var testArray =(testSettings.elements.type === 'string') ? testSettings.elements.enum : [ 0, 1, 2 ];
arrayTest(optionToTest, testArray);
break;
default:
expect(true).to.be.false;
}
});
}
for (var optionToTest in optionsToTest) {
optionTestRunner(optionToTest);
}
});
describe('Weights', function() {
it('Can call randomizeWeights', function() {
var ann = createANN({ layers: [ 2, 1, 2 ] });
var initalConnections = ann.getConnectionArray();
ann.randomizeWeights(-5, 5);
var updatedConnections = ann.getConnectionArray();
expect(initalConnections).to.be.an('array');
expect(updatedConnections).to.be.an('array');
expect(updatedConnections).to.have.lengthOf(initalConnections.length);
for (var i = 0; i < updatedConnections.length; i++) {
expect(updatedConnections[i]).to.have.property('fromNeuron').that.equals(initalConnections[i].fromNeuron);
expect(updatedConnections[i]).to.have.property('toNeuron').that.equals(initalConnections[i].toNeuron);
expect(updatedConnections[i]).to.have.property('weight').that.is.not.equal(initalConnections[i].weight);
}
});
it('Can call initWeights', function() {
var ann = createANN({ layers: [ 2, 1, 5 ] });
var initalConnections = ann.getConnectionArray();
var td = createTrainingData(booleanTrainingData);
ann.initWeights(td);
var updatedConnections = ann.getConnectionArray();
expect(initalConnections).to.be.an('array');
expect(updatedConnections).to.be.an('array');
expect(updatedConnections).to.have.lengthOf(initalConnections.length);
for (var i = 0; i < updatedConnections.length; i++) {
expect(updatedConnections[i]).to.have.property('fromNeuron').that.equals(initalConnections[i].fromNeuron);
expect(updatedConnections[i]).to.have.property('toNeuron').that.equals(initalConnections[i].toNeuron);
expect(updatedConnections[i]).to.have.property('weight').that.is.not.equal(initalConnections[i].weight);
}
});
it('can set a single weight', function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 1, 5 ] });
ann.initWeights(data);
var fromNeuron = 0;
var toNeuron = 3;
var newWeight = 0.22;
ann.setWeight(fromNeuron, toNeuron, newWeight);
var array = ann.getConnectionArray();
for (var connection of array) {
if (connection.fromNeuron === fromNeuron && connection.toNeuron === toNeuron) {
expect(connection.weight).to.be.at.most(newWeight);
break;
}
}
});
it('can set weights by array', function() {
var newWeight = 0.22;
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 1, 5 ] });
ann.initWeights(data);
var connections = ann.getConnectionArray();
for (var connect of connections) {
connect.weight = newWeight;
}
ann.setWeightArray(connections, connections.length);
var updatedConnections = ann.getConnectionArray();
expect(updatedConnections).to.deep.equal(updatedConnections);
});
});
describe('Print Commands', function() {
it('Can call printConnections', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var testFunc = function() { ann.printConnections(); }
expect(testFunc).to.not.throw();
});
it('Can call printParameters', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var testFunc = function() { ann.printParameters(); }
expect(testFunc).to.not.throw();
});
});
describe('Get Bit Fail', function() {
it('Can call getBitFail', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var bit = ann.getBitFail();
expect(bit).to.be.a('number');
});
});
describe('Get MSE', function() {
it('Can call getMSE', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var mse = ann.getMSE();
expect(mse).to.be.a('number');
});
});
describe('Reset MSE', function() {
it('Can call resetMSE', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var testFunc = function() { ann.resetMSE(); }
expect(testFunc).to.not.throw();
});
});
describe('Get Activation Function', function() {
it('Can call getActivationFunction', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var activationFunction = ann.getActivationFunction(1, 1);
expect(activationFunction).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
});
it('Returns null for calls outside the network', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var activationFunction = ann.getActivationFunction(3, 3);
expect(activationFunction).to.be.null;
});
});
describe('Set Activation Function', function() {
it('Can call setActivationFunction', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var initialActivationFunction = ann.getActivationFunction(1, 1);
expect(initialActivationFunction).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
ann.setActivationFunction('LINEAR', 1, 1);
var updatedActivationFunction = ann.getActivationFunction(1, 1);
expect(updatedActivationFunction).to.be.a('string').and.to.equal('LINEAR');
});
it('Can set to all ActivationFunction enum values', function() {
var activationFunctions = [
'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 ann = createANN({ layers: [ 2, 2, 2 ] });
// Test all enum values
for (var activationFunction of activationFunctions) {
ann.setActivationFunction(activationFunction, 1, 1);
var setActivationFunction = ann.getActivationFunction(1, 1);
expect(setActivationFunction).to.equal(activationFunction);
}
});
it('Calls outside the network have no effect', function() {
var ann = createANN({ layers: [ 1, 1, 1 ] });
var initalActivationFunction1 = ann.getActivationFunction(1, 1);
var initalActivationFunction2 = ann.getActivationFunction(2, 1);
ann.setActivationFunction('LINEAR_PIECE', 3, 1);
expect(ann.getActivationFunction(1, 1)).to.be.a('string').and.to.equal(initalActivationFunction1);
expect(ann.getActivationFunction(2, 1)).to.be.a('string').and.to.equal(initalActivationFunction2);
});
it('Can call setActivationFunctionLayer', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
// Setup state of functions
ann.setActivationFunction('SIGMOID_STEPWISE', 1, 1);
ann.setActivationFunction('SIGMOID_STEPWISE', 1, 2);
var initialActivationFunction1 = ann.getActivationFunction(1, 1);
var initialActivationFunction2 = ann.getActivationFunction(1, 2);
expect(initialActivationFunction1).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
expect(initialActivationFunction2).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
ann.setActivationFunctionLayer('LINEAR', 1);
var updatedActivationFunction1 = ann.getActivationFunction(1, 1);
var updatedActivationFunction2 = ann.getActivationFunction(1, 2);
expect(updatedActivationFunction1).to.be.a('string').and.to.equal('LINEAR');
expect(updatedActivationFunction2).to.be.a('string').and.to.equal('LINEAR');
});
it('Can call setActivationFunctionHidden', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
// Setup state of functions
ann.setActivationFunction('SIGMOID_STEPWISE', 1, 1);
ann.setActivationFunction('SIGMOID_STEPWISE', 1, 2);
var initialActivationFunction1 = ann.getActivationFunction(1, 1);
var initialActivationFunction2 = ann.getActivationFunction(1, 2);
expect(initialActivationFunction1).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
expect(initialActivationFunction2).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
ann.setActivationFunctionHidden('LINEAR');
var updatedActivationFunction1 = ann.getActivationFunction(1, 1);
var updatedActivationFunction2 = ann.getActivationFunction(1, 2);
expect(updatedActivationFunction1).to.be.a('string').and.to.equal('LINEAR');
expect(updatedActivationFunction2).to.be.a('string').and.to.equal('LINEAR');
});
it('Can call setActivationFunctionOutput', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
// Setup state of functions
ann.setActivationFunction('SIGMOID_STEPWISE', 2, 1);
ann.setActivationFunction('SIGMOID_STEPWISE', 2, 2);
var initialActivationFunction1 = ann.getActivationFunction(2, 1);
var initialActivationFunction2 = ann.getActivationFunction(2, 2);
expect(initialActivationFunction1).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
expect(initialActivationFunction2).to.be.a('string').and.to.equal('SIGMOID_STEPWISE');
ann.setActivationFunctionOutput('LINEAR');
var updatedActivationFunction1 = ann.getActivationFunction(2, 1);
var updatedActivationFunction2 = ann.getActivationFunction(2, 2);
expect(updatedActivationFunction1).to.be.a('string').and.to.equal('LINEAR');
expect(updatedActivationFunction2).to.be.a('string').and.to.equal('LINEAR');
});
});
describe('#getBiasArray', function() {
it('should return an array', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
expect(ann.getBiasArray()).to.be.instanceof(Array);
});
});
describe('#getLayerArray', function() {
it('should return an array', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
expect(ann.getLayerArray()).to.be.instanceof(Array);
});
});
describe('#scaleTrainingData', function() {
it('can throw an error', function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
var func = function() {
return ann.scaleTrainingData([]);
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can call scale train data', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 2, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.scaleTrainingData(data);
}).to.not.throw();
});
});
describe('#descaleTrainingData', function() {
it('can throw an error', function() {
var func = function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
return ann.descaleTrainingData([]);
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can call descale train data', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.descaleTrainingData(data);
}).to.not.throw();
});
});
describe('#setInputScalingParams', function() {
it('can throw an error if data is not trainingData', function() {
var func = function() {
var ann = createANN({ layers: [ 2, 2, 2 ] });
return ann.setInputScalingParams([], 0, 1);
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can throw an error if min is not a number', function() {
var func = function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 2, 2 ] });
return ann.setInputScalingParams(data, 'blah', 1);
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can throw an error if max is not a number', function() {
var func = function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 2, 2 ] });
return ann.setInputScalingParams(data, 0, 'blah');
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can call setInputScalingParams', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
return ann.setInputScalingParams(data, 0, 1);
}).to.not.throw();
});
});
describe('#setOutputScalingParams', function() {
it('can throw an error if data is not trainingData', function() {
var func = function() {
var ann = createANN({ layers: [ 2, 3, 2 ] });
return ann.setOutputScalingParams([], 0, 1);
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can throw an error if min is not a number', function() {
var func = function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 2 ] });
return ann.setOutputScalingParams(data, 'blah', 1);
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can throw an error if max is not a number', function() {
var func = function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 2 ] });
return ann.setOutputScalingParams(data, 0, 'blah');
};
expect(func).to.throw(XError.INVALID_ARGUMENT);
});
it('can call setOutputScalingParams', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
return ann.setOutputScalingParams(data, 0, 1);
}).to.not.throw();
});
});
describe('#setScalingParams', function() {
it('can call setScalingParams', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
return ann.setScalingParams(data, 0, 1, 0, 1);
}).to.not.throw();
});
});
describe('#clearScalingParams', function() {
it('can clear scaling params', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 1, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.clearScalingParams();
}).to.not.throw();
});
});
describe('#scaleInput', function() {
it('can scale input', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.scaleInput([ 0, 1 ]);
}).to.not.throw();
});
});
describe('#scaleOutput', function() {
it('can scale output', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.scaleOutput([ 0, 1, 1, 1, 0 ]);
}).to.not.throw();
});
});
describe('#descaleInput', function() {
it('can descale input', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.descaleInput([ 0, 1 ]);
}).to.not.throw();
});
});
describe('#descaleOutput', function() {
it('can descale output', function() {
expect(function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 1, 5 ] });
ann.setScalingParams(data, 0, 1, 0, 1);
return ann.descaleOutput([ 0, 1, 1, 1, 0 ]);
}).to.not.throw();
});
});
describe('Activation Steepness', function() {
it('can get activation steepness', function() {
var ann = createANN({ layers: [ 2, 3, 5 ] });
expect(ann.getActivationSteepness(1, 1))
.to.be.a('number')
.to.not.equal(-1);
});
it('can set activation steepness', function() {
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setActivationSteepness(0.2, 1, 1);
expect(ann.getActivationSteepness(1, 1))
.to.be.a('number')
.to.not.equal(-1);
});
it('can set activation steepness on layers', function() {
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setActivationSteepnessLayer(0.2, 1);
expect(ann.getActivationSteepness(1, 2))
.to.equal(ann.getActivationSteepness(1, 3))
.to.be.a('number')
.to.not.equal(-1);
});
it('can set activation steepness on hidden layers', function() {
var ann = createANN({ layers: [ 2, 2, 5 ] });
ann.setActivationSteepnessHidden(0.2);
expect(ann.getActivationSteepness(1,1))
.to.equal(ann.getActivationSteepness(1,2))
.to.be.a('number')
.to.not.equal(-1);
});
it('can set activation steepness on output', function() {
var ann = createANN({ layers: [ 2, 3, 5 ] });
ann.setActivationSteepnessOutput(0.3);
expect(ann.getActivationSteepness(2,4))
.to.equal(ann.getActivationSteepness(2,5))
.to.be.a('number')
.to.not.equal(-1);
});
});
describe('Testing Data', function() {
it('can test set of data', function() {
var data = createTrainingData(booleanTrainingData);
var ann = createANN({ layers: [ 2, 3, 5 ] });
return ann.testData(data)
.then(function(res) {
expect(res).to.be.a('number');
expect(ann.getMSE()).to.be.below(0.5);
expect(ann.getOption('bitFailLimit')).to.be.below(0.5);
});
});
it('can test one input for output error', function() {
var ann = createANN({ layers: [ 2, 3, 5 ] });
var result = ann.testOne([ 1, 0 ], [ 1, 1, 1, 1, 1 ]);
expect(result).to.be.an.instanceof(Array).to.have.a.lengthOf(5);
expect(ann.getMSE()).to.be.below(0.5);
expect(ann.getOption('bitFailLimit')).to.be.below(0.5);
});
});
});