From b703b0181967c7eeacc79eb826c7a2d117f0b7b8 Mon Sep 17 00:00:00 2001 From: Will Newmarch Date: Sat, 13 Jul 2019 15:06:55 +0100 Subject: [PATCH] Library can now output network as JSON. Networks can save themselves in JSON form to supplied filename. --- package-lock.json | 50 ++++++------------- package.json | 3 ++ src/Layer.js | 6 +++ src/Network.js | 16 +++--- src/Synapse.js | 10 ++++ src/neuron/Hidden.js | 7 --- src/neuron/Input.js | 5 -- src/neuron/Neuron.js | 13 +++++ src/neuron/Output.js | 6 --- tests/model-to-json.test.js | 27 ++++++++++ tests/saves-models-to-file.test.js | 28 +++++++++++ .../xor-problem.test.js | 2 +- 12 files changed, 114 insertions(+), 59 deletions(-) create mode 100644 tests/model-to-json.test.js create mode 100644 tests/saves-models-to-file.test.js rename xor-problem.test.js => tests/xor-problem.test.js (95%) diff --git a/package-lock.json b/package-lock.json index 8614dce..6015678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "intuitive-neural-network", - "version": "0.1.0", + "version": "0.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1579,6 +1579,11 @@ "map-cache": "^0.2.2" } }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3045,9 +3050,9 @@ "dev": true }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -3804,9 +3809,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -4342,38 +4347,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unset-value": { diff --git a/package.json b/package.json index f0c0cd6..c657096 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "homepage": "https://github.com/will-newmarch/intuitive-neural-network#readme", "devDependencies": { "jest": "^24.5.0" + }, + "dependencies": { + "fs": "0.0.1-security" } } diff --git a/src/Layer.js b/src/Layer.js index eb9f59f..6309dc9 100644 --- a/src/Layer.js +++ b/src/Layer.js @@ -44,6 +44,12 @@ class Layer { } return totalError; } + + toObject() { + return { + neurons: this.neurons.map(n => n.toObject()) + }; + } }; module.exports = Layer; \ No newline at end of file diff --git a/src/Network.js b/src/Network.js index 6cdaabf..2c20ba5 100644 --- a/src/Network.js +++ b/src/Network.js @@ -1,3 +1,5 @@ +const fs = require('fs'); + const Layer = require('./Layer.js'); const Input = require('./neuron/Input.js'); const Hidden = require('./neuron/Hidden.js'); @@ -175,14 +177,16 @@ class Network { return this; } - /** - * Log the network in a human readable fashion - */ - log(synapses = false) { // This could definitely be improved... - this.layers.map(l => { - l.neurons.map(n => console.log(n.label,n.outputs.map(s => s.weight))); + toJSON() { + return JSON.stringify({ + layers: this.layers.map(l => l.toObject()) }); } + + save(filename,callback) { + const model = this.toJSON(); + fs.writeFile(filename, model, callback); + } }; module.exports = Network; \ No newline at end of file diff --git a/src/Synapse.js b/src/Synapse.js index 4d3e4bb..445b473 100644 --- a/src/Synapse.js +++ b/src/Synapse.js @@ -47,6 +47,16 @@ class Synapse { applyError(learningRate) { this.weight -= learningRate * this.error; } + + toObject() { + return { + type: this.constructor.name, + label: this.label, + inputLabel: this.input.label, + outputLabel: this.output.label, + weight: this.weight + } + } }; module.exports = Synapse; \ No newline at end of file diff --git a/src/neuron/Hidden.js b/src/neuron/Hidden.js index c05514f..b49d011 100644 --- a/src/neuron/Hidden.js +++ b/src/neuron/Hidden.js @@ -9,13 +9,6 @@ class Hidden extends Neuron { */ constructor(label) { super(label); - - this.inputs = []; // Reference to the input synapses - this.inputSignals = []; // Array to collect signals from inputs (when firing) - this.outputs = []; // Reference to the output synapses - this.outputSignals = []; // Array to collect signals FROM outputs (when backpropagating) - this.activation = 0; // Activation of the neuron - this.error = 0; // Error to be persisted (not actually used in Input neuron, more kept for interest) } /** diff --git a/src/neuron/Input.js b/src/neuron/Input.js index bc59f66..d5f9b45 100644 --- a/src/neuron/Input.js +++ b/src/neuron/Input.js @@ -8,11 +8,6 @@ class Input extends Neuron { */ constructor(label) { super(label); - - this.outputs = []; // Reference to the output synapses - this.outputSignals = []; // Array to collect signals FROM outputs (when backpropagating) - this.activation = 0; // Activation of the neuron - this.error = 0; // Error to be persisted (not actually used in Input neuron, more kept for interest) } /** diff --git a/src/neuron/Neuron.js b/src/neuron/Neuron.js index 002f3e5..9b5ac3a 100644 --- a/src/neuron/Neuron.js +++ b/src/neuron/Neuron.js @@ -8,6 +8,10 @@ class Neuron { */ constructor(label) { this.label = label; // Human readable label + this.inputs = []; // Reference to the input synapses + this.inputSignals = []; // Array to collect signals from inputs (when firing) + this.outputs = []; // Reference to the output synapses + this.outputSignals = []; // Array to collect signals FROM outputs (when backpropagating) this.activation = 0; // Activation of the neuron this.error = 0; // Error to be persisted (not actually used in Input neuron, more kept for interest) this.setActivationType('sigmoid'); // Neuron activation function (defaulting to sigmoid) @@ -46,6 +50,15 @@ class Neuron { backPropagate(backSignal) { throw 'backPropagate method must be overidden!'; } + + toObject() { + return { + type: this.constructor.name, + label: this.label, + activationType: this.activationType, + outputs: this.outputs.map(o => o.toObject()) + }; + } }; module.exports = Neuron; \ No newline at end of file diff --git a/src/neuron/Output.js b/src/neuron/Output.js index ba5192d..b65fa13 100644 --- a/src/neuron/Output.js +++ b/src/neuron/Output.js @@ -9,12 +9,6 @@ class Output extends Neuron { */ constructor(label) { super(label); - - this.inputs = []; // Reference to the input synapses - this.inputSignals = []; // Array to collect signals from inputs (when firing) - this.activation = 0; // Activation of the neuron - this.error = 0; // Error to be persisted and applied by network - } /** diff --git a/tests/model-to-json.test.js b/tests/model-to-json.test.js new file mode 100644 index 0000000..021260f --- /dev/null +++ b/tests/model-to-json.test.js @@ -0,0 +1,27 @@ +'use strict'; + +const Network = require('../src/Network.js'); + +test('library can convert a model to JSON', () => { + + // Build the network... + var network = new Network({ + layers: [2,2,1], + bias: false + }); + + // Hard-code weights to create exact JSON + network.layers.map(l => { + l.neurons.map(n => { + n.outputs.map(o => { + o.weight = 0.5; + }); + }); + }); + + const expectedJSON = '{"layers":[{"neurons":[{"type":"Input","label":"input-0","activationType":"sigmoid","outputs":[{"type":"Synapse","label":"input-0--hidden-0","inputLabel":"input-0","outputLabel":"hidden-0","weight":0.5},{"type":"Synapse","label":"input-0--hidden-1","inputLabel":"input-0","outputLabel":"hidden-1","weight":0.5}]},{"type":"Input","label":"input-1","activationType":"sigmoid","outputs":[{"type":"Synapse","label":"input-1--hidden-0","inputLabel":"input-1","outputLabel":"hidden-0","weight":0.5},{"type":"Synapse","label":"input-1--hidden-1","inputLabel":"input-1","outputLabel":"hidden-1","weight":0.5}]}]},{"neurons":[{"type":"Hidden","label":"hidden-0","activationType":"sigmoid","outputs":[{"type":"Synapse","label":"hidden-0--output-0","inputLabel":"hidden-0","outputLabel":"output-0","weight":0.5}]},{"type":"Hidden","label":"hidden-1","activationType":"sigmoid","outputs":[{"type":"Synapse","label":"hidden-1--output-0","inputLabel":"hidden-1","outputLabel":"output-0","weight":0.5}]}]},{"neurons":[{"type":"Output","label":"output-0","activationType":"identity","outputs":[]}]}]}'; + + expect(network.toJSON()).toBe(expectedJSON); + +}); + diff --git a/tests/saves-models-to-file.test.js b/tests/saves-models-to-file.test.js new file mode 100644 index 0000000..db001f9 --- /dev/null +++ b/tests/saves-models-to-file.test.js @@ -0,0 +1,28 @@ +'use strict'; + +const fs = require('fs'); + +const Network = require('../src/Network.js'); + +test('library can convert a model to JSON', (done) => { + + // Build the network... + var network = new Network({ + layers: [2,2,1], + bias: false + }); + + const filename = './temp.json'; + + network.save(filename,() => { + + expect(fs.existsSync(filename)).toBe(true); + + fs.unlinkSync(filename); + + done(); + + }); + +}); + diff --git a/xor-problem.test.js b/tests/xor-problem.test.js similarity index 95% rename from xor-problem.test.js rename to tests/xor-problem.test.js index 1a3f45e..0aa3480 100644 --- a/xor-problem.test.js +++ b/tests/xor-problem.test.js @@ -1,6 +1,6 @@ 'use strict'; -const Network = require('./src/Network.js'); +const Network = require('../src/Network.js'); test('library solves XOR problem', () => {