diff options
Diffstat (limited to 'src/mind')
-rw-r--r-- | src/mind/README.md | 37 | ||||
-rw-r--r-- | src/mind/topology.js | 183 | ||||
-rw-r--r-- | src/mind/topology.test.js | 235 |
3 files changed, 0 insertions, 455 deletions
diff --git a/src/mind/README.md b/src/mind/README.md deleted file mode 100644 index 1ece125..0000000 --- a/src/mind/README.md +++ /dev/null @@ -1,37 +0,0 @@ -mind -==== - -This module is used to create arbitrary stateful neural networks. - -The only export is the following function: - -``` -network(input_count, internal_count, output_count, weight_max = 4 : number) -``` - -This function returns an object that represents a neural network with `input_count` input neurons, -`internal_count` internal (and stateful) neurons, and `output_count` output neurons. `max_weight` determines -the maximum absolute value allowed for connection weights. - -A network object has two methods: - -``` -connect(source, sink, weight : number) -``` - -This method returns a new network object that is an exact copy of the original, except with a new -connection between the neuron indexed by `source` and `sink`, with weight `weight`. Neuron indices -are zero-indexed, and span all neurons, first the inputs, then the internal neurons, and finally the outputs. -An error will be thrown if `source` is in the range of output neurons or if `sink` is in the range of input -neurons. - - -``` -compute(inputs, state : array[number]) -``` - -This method returns a tuple `[output, newState]`, where `output` is an array of `output_count` values -corresponding to the output neuron's computed values, and `newState` is the new state of the internal neurons. - -`input` must be an array of numbers with length equal to `input_count`, and `state` must be an array of numbers -with length equal to `internal_count` or an error will be raised. diff --git a/src/mind/topology.js b/src/mind/topology.js deleted file mode 100644 index 946dd86..0000000 --- a/src/mind/topology.js +++ /dev/null @@ -1,183 +0,0 @@ -'use strict'; - -import { create } from '../util.js'; - - -const DEFAULT_WEIGHT_MAX = 4; - - -// prototype for network objects -const network_proto = { - connect: function(source, sink, weight) { - return network_connect(this, source, sink, weight); - }, - compute: function(inputs, state) { - return network_compute(this, inputs, state); - }, -}; - - -// create a new network -export function network(input_count, internal_count, output_count, weight_max = 4) { - const count = input_count + internal_count + output_count; - const n = create({ - input_count, - output_count, - adjacency: new Array(count).fill([]), - weight: [], - }, network_proto); - return n; -} - - -// check index is an input -function is_input(n, index) { - return index < n.input_count; -} -// check if index is an output -function is_output(n, index) { - return index >= (n.adjacency.length - n.output_count); -} -// check if index is a hidden neuron -function is_hidden(n, index) { - return (!is_input(n, index)) && (!is_output(n, index)); -} - - -// returns a new network with an edge between the given nodes -// with the given weight -export function network_connect(n, source, sink, weight) { - if (is_input(n, sink)) { - // inputs cannot be sinks - throw new Error(`attempt to use input as sink (${source} -> ${sink})`); - } - if (is_output(n, source)) { - // outputs cannot be sources - throw new Error(`attempt to use output as source (${source} -> ${sink})`); - } - - return create({ - ...n, - adjacency: n.adjacency.map((row, i) => { - if (i === source && i === sink) { - // self-loop - return [...row, 2]; - } else if (i === source) { - return [...row, 1]; - } else if (i === sink) { - return [...row, -1]; - } else { - return [...row, 0]; - } - }), - weight: [...n.weight, weight], - }, network_proto); -} - - -// gets the indices of the edges incident on the given adjacency list -function incident_edges(n, adj) { - const incident = adj - .map((edge, index) => (edge < 0) || (edge === 2) ? index : null) - .filter(index => index !== null); - - return incident; -} - - -// get the indices of the ends of an edge -// in the case of self-loops, both values are the same -function edge_ends(n, edge) { - const ends = n.adjacency - .map((adj, index) => adj[edge] !== 0 ? index : null) - .filter(index => index != null); - - ends.sort((a, b) => n.adjacency[a][edge] < n.adjacency[b][edge] ? -1 : 1); - - if (ends.length === 1) { - return { source: ends[0], sink: ends[0] }; - } else if (ends.length === 2) { - return { source: ends[1], sink: ends[0] }; - } else { - throw new Error("something bad happened with the ends"); - } -} - - -// recursively get the value of a node from the input nodes, -// optionally caching the computed values -function get_value(n, index, input, prev, cache) { - // check if value is cached - if (cache !== undefined && cache[index]) { - return cache[index]; - } - // check if value is input - if (is_input(n, index)) { - return input[index]; - } - - const adj = n.adjacency[index]; // get adjacency list - const incident = incident_edges(n, adj); // get incident edges - const weight = incident.map(x => n.weight[x]); // edge weights - const sources = incident // get ancestor nodes - .map(x => edge_ends(n, x).source); - - // get the value of each ancestor - const values = sources - .map(x => x === index // if the ancestor is this node - ? prev[x - n.input_count] // then the value is the previous value - : get_value(n, x, input, prev, cache)); // else recurse - - const sum = values // compute the weighted sum of the values - .reduce((acc, x, i) => acc + (weight[i] * x), 0); - - if (sum !== sum) { // NaN test - console.log(n); - console.log(sources); - console.log(input); - throw new Error(`failed to get output for index ${index}`); - } - - // compute result - const value = Math.tanh(sum); - - // !!! impure caching !!! - // cache result - if (cache !== undefined) { - cache[index] = value; - } - - return value; -} - - -// compute a network's output and new hidden state -// given the input and previous hidden state -export function network_compute(n, input, state) { - // validate input - if (input.length !== n.input_count) { - throw new Error("incorrect number of input elements"); - } - // validate state - const hidden_count = n.adjacency.length - n.input_count - n.output_count; - if (state.length !== hidden_count) { - throw new Error("incorrect number of state elements"); - } - - // !!! impure caching !!! - const value_cache = {}; - - const result = Object.freeze(n.adjacency - .map((x, i) => is_output(n, i) ? i : null) // output index or null - .filter(i => i !== null) // remove nulls - .map(x => get_value(n, x, input, state, value_cache)) // map to computed value - ); - - const newstate = Object.freeze(n.adjacency - .map((x, i) => is_hidden(n, i) ? i : null) // hidden index or null - .filter(i => i !== null) // remove nulls - .map(x => get_value(n, x, input, state, value_cache)) // map to computed value (using cache) - ); - - return Object.freeze([result, newstate]); -} diff --git a/src/mind/topology.test.js b/src/mind/topology.test.js deleted file mode 100644 index 52c196f..0000000 --- a/src/mind/topology.test.js +++ /dev/null @@ -1,235 +0,0 @@ -'use strict'; - -import { network } from './topology'; - - -test('basic network functionality', () => { - const n = network(0, 5, 0); - expect(n).toEqual({ - input_count: 0, - output_count: 0, - adjacency: [ [], [], [], [], [] ], - weight: [], - }); - - expect(() => n.adjacency = []).toThrow(); - expect(() => n.weight = []).toThrow(); - - const nn = n.connect(0, 1, -2); - expect(nn).toEqual({ - input_count: 0, - output_count: 0, - adjacency: [ - [ 1 ], - [ -1 ], - [ 0 ], - [ 0 ], - [ 0 ] - ], - weight: [ -2 ], - }); - - expect(() => nn.adjacency = []).toThrow(); - expect(() => nn.weight = []).toThrow(); - - const nnn = nn.connect(2, 4, 3); - expect(nnn).toEqual({ - input_count: 0, - output_count: 0, - adjacency: [ - [ 1, 0 ], - [ -1, 0 ], - [ 0, 1 ], - [ 0, 0 ], - [ 0, -1 ] - ], - weight: [ -2, 3 ], - }); - - expect(() => nnn.adjacency = []).toThrow(); - expect(() => nnn.weight = []).toThrow(); -}); - - -test( -'networks are restricted from sinking to inputs or sourcing from outputs', -() => { - const n = network(2, 2, 2); - - expect(n.connect(1,2,0)).toEqual({ - input_count: 2, - output_count: 2, - adjacency: [ - [ 0 ], - [ 1 ], - [ -1 ], - [ 0 ], - [ 0 ], - [ 0 ], - ], - weight: [ 0 ], - }); - expect(() => n.connect(2, 1, 0)).toThrow(); - - expect(n.connect(3, 4, 2)).toEqual({ - input_count: 2, - output_count: 2, - adjacency: [ - [ 0 ], - [ 0 ], - [ 0 ], - [ 1 ], - [ -1 ], - [ 0 ], - ], - weight: [ 2 ], - }); - expect(() => n.connect(4, 3, 2)).toThrow(); -}); - - -test('self-connections work correctly', () => { - const n = network(0, 1, 0).connect(0, 0, 2.0); - expect(n).toEqual({ - input_count: 0, - output_count: 0, - adjacency: [ - [ 2 ], - ], - weight: [ 2 ], - }); -}); - - -test('network computations', () => { - const n = network(1, 0, 1).connect(0, 1, 2.0); - const input = [ -0.5 ]; - const state = []; - const result = n.compute(input, state); - expect(result).toEqual([ - [ Math.tanh(-0.5 * 2.0) ], - [], - ]); - - expect(input).toEqual([ -0.5 ]); - expect(state).toEqual([]); - - expect(() => result[0] = 'hi').toThrow(); - expect(() => result[0].push('hi')).toThrow(); - expect(() => result[1] = 'hi').toThrow(); - expect(() => result[1].push('hi')).toThrow(); -}); - - -test('multiple input network', () => { - const n = network(4, 0, 1) - .connect(0, 4, -1.0) - .connect(1, 4, -2.0) - .connect(2, 4, 1.0) - .connect(3, 4, 2.0) - - expect(n.compute([1, 2, 3, 5], [])).toEqual([ - [ Math.tanh( - (-1.0 * 1) + - (-2.0 * 2) + - (1.0 * 3) + - (2.0 * 5))], - [], - ]); -}); - - -test('multiple outputs', () => { - const n = network(4, 0, 2) - .connect(0, 4, -1) - .connect(1, 4, 1) - .connect(2, 5, -1) - .connect(3, 5, 1); - - expect(n.compute([1,2,3,5], [])).toEqual([ - [ Math.tanh(2-1), Math.tanh(5-3) ], - [], - ]); -}); - - -test('hidden neurons', () => { - const n = network(4, 2, 1) - .connect(0, 4, -1) - .connect(1, 4, 1) - .connect(2, 5, -1) - .connect(3, 5, 1) - .connect(4, 6, -1) - .connect(5, 6, 1); - - expect(n.compute([1,2,3,5], [ 0, 0 ])).toEqual([ - [ Math.tanh( Math.tanh(5-3) - Math.tanh(2-1) ) ], - [ Math.tanh(2-1), Math.tanh(5-3) ], - ]); -}); - - -test('arbitrary hidden neurons', () => { - const n = network(1, 2, 1) - .connect(0, 1, 1) - .connect(1, 2, -1) - .connect(2, 3, 2) - - const [output, state] = n.compute([1], [0, 0]); - - expect(output).toEqual([ - Math.tanh( - 2*Math.tanh( - -1*Math.tanh(1) - ) - ) - ]); - - expect(state).toEqual([ - Math.tanh(1), - Math.tanh( -Math.tanh(1) ), - ]); -}); - - -test('memory', () => { - const n = network(0, 1, 1).connect(0, 0, -0.5).connect(0, 1, 2); - - expect(n.compute([], [1])).toEqual([ - [ Math.tanh( 2 * Math.tanh( -0.5 * 1 ) ) ], - [ Math.tanh( -0.5 * 1) ], - ]); -}); - - -test('memory and input', () => { - const n = network(1, 1, 1) - .connect(0, 1, 1) - .connect(1, 1, 1) - .connect(1, 2, 1); - - expect(n.compute([2], [-1])).toEqual([ - [ Math.tanh( Math.tanh( 2-1 ) ) ], - [ Math.tanh( 2-1 ) ], - ]); -}); - - -test('input and state must be the correct size', () => { - const n = network(2, 1, 1) - .connect(0, 2, 1) - .connect(1, 2, 1) - .connect(2, 3, 1); - - // wrong input size - expect(() => n.compute([], [4])).toThrow(); - expect(() => n.compute([1], [4])).toThrow(); - expect(() => n.compute([1, 1, 1], [4])).toThrow(); - - // wrong state size - expect(() => n.compute([1, 1], [])).toThrow(); - expect(() => n.compute([1, 1], [4, 4])).toThrow(); - - // prove correct sizes work - n.compute([1, 1], [4]); -}); |