UNPKG

encog

Version:

Encog is a NodeJs ES6 framework based on the Encog Machine Learning Framework by Jeff Heaton, plus some the of basic data manipulation helpers.

588 lines (514 loc) 19.2 kB
const FreeformNetworkError = require(PATHS.ERROR_HANDLING + 'freeformNetwork'); const ActivationTANH = require(PATHS.ACTIVATION_FUNCTIONS + 'tanh'); const BasicFreeformNeuron = require(PATHS.FREEFORM + 'basic/neuron'); const BasicActivationSummation = require(PATHS.FREEFORM + 'basic/activationSummation'); const BasicFreeformConnection = require(PATHS.FREEFORM + 'basic/connection'); const BasicFreeformLayer = require(PATHS.FREEFORM + 'basic/layer'); const FreeformContextNeuron = require(PATHS.FREEFORM + 'contextNeuron'); const ErrorUtil = require(PATHS.UTILS + 'error'); const RangeRandomizer = require(PATHS.RANDOMIZERS + 'range'); const ArrayUtils = require(PATHS.PREPROCESSING + 'array'); /** * Implements a freefrom neural network. A freeform neural network can represent * much more advanced structures than the flat networks that the Encog * BasicNetwork implements. However, while freeform networks are more advanced * than the BasicNetwork, they are also much slower. * * Freeform networks allow just about any neuron to be connected to another * neuron. You can have neuron layers if you want, but they are not required. * */ class FreeformNetwork { __loadBasicNetwork(network) { // handle each layer let previousLayer = null; let currentLayer; for (let currentLayerIndex = 0; currentLayerIndex < network.getLayerCount(); currentLayerIndex++) { // create the layer currentLayer = new BasicFreeformLayer(); // Is this the input layer? if (this.inputLayer == null) { this.inputLayer = currentLayer; } // Add the neurons for this layer for (let i = 0; i < network.getLayerNeuronCount(currentLayerIndex); i++) { // obtain the summation object. let summation = null; if (previousLayer != null) { summation = new BasicActivationSummation(network.getActivation(currentLayerIndex)); } // add the new neuron currentLayer.add(new BasicFreeformNeuron(summation)); } // Fully connect this layer to previous if (previousLayer != null) { this.connectLayersFromBasic(network, currentLayerIndex - 1, previousLayer, currentLayerIndex, currentLayer, currentLayerIndex, false); } // Add the bias neuron // The bias is added after connections so it has no inputs if (network.isLayerBiased(currentLayerIndex)) { const biasNeuron = new FreeformContextNeuron(null); biasNeuron.setBias(true); biasNeuron.setActivation(network.getLayerBiasActivation(currentLayerIndex)); currentLayer.add(biasNeuron); } // update previous layer previousLayer = currentLayer; currentLayer = null; } // finally, set the output layer. this.outputLayer = previousLayer; } /** * Create a freeform network from a basic network. * * @param network {BasicNetwork} The basic network to use. */ constructor(network) { if (arguments.length === 0) { return; } if (network.getLayerCount() < 2) { throw new FreeformNetworkError( "The BasicNetwork must have at least two layers to be converted."); } this.__loadBasicNetwork(network); } /** * Classify the input into a group. * @param {Array} input The input data to classify. * @return {number} The group that the data was classified into. */ classify(input) { const output = this.compute(input); return _.max(output); } /** * Clear any data from any context layers. */ clearContext() { this.performNeuronTask((neuron)=> { if (neuron.constructor.name === 'FreeformContextNeuron') { neuron.setActivation(0); } }); } /** * Compute the output for this network. * * @param input {Array} * The input. * @return {Array} * The output. */ compute(input) { // Allocate result const result = ArrayUtils.newFloatArray(this.outputLayer.size()); // Copy the input for (let i = 0; i < input.length; i++) { this.inputLayer.setActivation(i, input[i]); } // Request calculation of outputs for (let i = 0; i < this.outputLayer.size(); i++) { const outputNeuron = this.outputLayer.getNeurons()[i]; outputNeuron.performCalculation(); result[i] = outputNeuron.getActivation(); } this.updateContext(); return result; } /** * Connect two layers. * * @param source {FreeformLayer} * The source layer. * @param target {FreeformLayer} * The target layer. * @param theActivationFunction {ActivationFunction} * The activation function to use. * @param biasActivation {Number} * The bias activation to use. * @param isRecurrent {Boolean} * True, if this is a recurrent connection. */ connectLayers(source, target, theActivationFunction = new ActivationTANH(), biasActivation = 1.0, isRecurrent = false) { // create bias, if requested if (biasActivation > PATHS.CONSTANTS.DEFAULT_DOUBLE_EQUAL) { // does the source already have a bias? if (source.hasBias()) { throw new FreeformNetworkError("The source layer already has a bias neuron, you cannot create a second."); } const biasNeuron = new BasicFreeformNeuron(null); biasNeuron.setActivation(biasActivation); biasNeuron.setBias(true); source.add(biasNeuron); } // create connections for (let targetNeuron of target.getNeurons()) { // create the summation for the target let summation = targetNeuron.getInputSummation(); // do not create a second input summation if (summation == null) { summation = new BasicActivationSummation(theActivationFunction); targetNeuron.setInputSummation(summation); } // connect the source neurons to the target neuron for (let sourceNeuron of source.getNeurons()) { const connection = new BasicFreeformConnection(sourceNeuron, targetNeuron); sourceNeuron.addOutput(connection); targetNeuron.addInput(connection); } } } /** * Connect layers from a BasicNetwork. Used internally only. * * @param network {BasicNetwork} * The BasicNetwork. * @param fromLayerIdx {Number} * The from layer index. * @param source {FreeformLayer} * The from layer. * @param sourceIdx {Number} * The source index. * @param target {FreeformLayer} * The target. * @param targetIdx {Number} * The target index. * @param isRecurrent {Boolean} * True, if this is recurrent. */ connectLayersFromBasic(network, fromLayerIdx, source, sourceIdx, target, targetIdx, isRecurrent) { for (let targetNeuronIdx = 0; targetNeuronIdx < target.size(); targetNeuronIdx++) { for (let sourceNeuronIdx = 0; sourceNeuronIdx < source.size(); sourceNeuronIdx++) { const sourceNeuron = source.getNeurons()[sourceNeuronIdx]; const targetNeuron = target.getNeurons()[targetNeuronIdx]; // neurons with no input (i.e. bias neurons) if (targetNeuron.getInputSummation() == null) { continue; } const connection = new BasicFreeformConnection(sourceNeuron, targetNeuron); sourceNeuron.addOutput(connection); targetNeuron.addInput(connection); connection.setWeight(network.getWeight(fromLayerIdx, sourceNeuronIdx, targetNeuronIdx)); } } } /** * Create a context connection, such as those used by Jordan/Elmann. * * @param source {FreeformLayer} * The source layer. * @param target {FreeformLayer} * The target layer. * @return {FreeformLayer} The newly created context layer. */ createContext(source, target) { const biasActivation = 0.0; let activatonFunction; if (source.getNeurons()[0].getOutputs().length < 1) { throw new FreeformNetworkError( "A layer cannot have a context layer connected if there are no other outbound connections from the source layer." + " Please connect the source layer somewhere else first."); } activatonFunction = source.getNeurons()[0].getInputSummation().getActivationFunction(); // first create the context layer const result = new BasicFreeformLayer(); for (let i = 0; i < source.size(); i++) { const neuron = source.getNeurons()[i]; if (neuron.isBias()) { const biasNeuron = new BasicFreeformNeuron(null); biasNeuron.setBias(true); biasNeuron.setActivation(neuron.getActivation()); result.add(biasNeuron); } else { result.add(new FreeformContextNeuron(neuron)); } } // now connect the context layer to the target layer this.connectLayers(result, target, activatonFunction, biasActivation, false); return result; } /** * Create the input layer. * * @param neuronCount {Number} The input neuron count. * @return {FreeformLayer} The newly created layer. */ createInputLayer(neuronCount) { if (neuronCount < 1) { throw new FreeformNetworkError("Input layer must have at least one neuron."); } this.inputLayer = this.createLayer(neuronCount); return this.inputLayer; } /** * Create a hidden layer. * * @param neuronCount {Number} The neuron count. * @return {FreeformLayer} The newly created layer. */ createLayer(neuronCount) { if (neuronCount < 1) { throw new FreeformNetworkError("Layer must have at least one neuron."); } const result = new BasicFreeformLayer(); // Add the neurons for this layer for (let i = 0; i < neuronCount; i++) { result.add(new BasicFreeformNeuron()); } return result; } /** * Create the output layer. * * @param neuronCount {Number} The neuron count. * @return {FreeformLayer} The newly created output layer. */ createOutputLayer(neuronCount) { if (neuronCount < 1) { throw new FreeformNetworkError("Output layer must have at least one neuron."); } this.outputLayer = this.createLayer(neuronCount); return this.outputLayer; } /** * {@inheritDoc} */ decodeFromArray(encoded) { let index = 0; const visited = []; const queue = []; let neuron; // first copy outputs to queue for (let neuron of this.outputLayer.getNeurons()) { queue.push(neuron); } while (queue.length > 0) { // pop a neuron off the queue neuron = queue[0]; queue.shift(); visited.push(neuron); // find anymore neurons and add them to the queue. if (neuron.getInputSummation() != null) { for (let connection of neuron.getInputSummation().list()) { connection.setWeight(encoded[index++]); const nextNeuron = connection.getSource(); if (visited.indexOf(nextNeuron) === -1) { queue.push(nextNeuron); } } } } } /** * {@inheritDoc} */ encodedArrayLength() { let result = 0; const visited = []; const queue = []; // first copy outputs to queue for (let neuron of this.outputLayer.getNeurons()) { queue.push(neuron); } while (queue.length > 0) { // pop a neuron off the queue const neuron = queue[0]; queue.shift(); visited.push(neuron); // find anymore neurons and add them to the queue. if (neuron.getInputSummation() != null) { for (let connection of neuron.getInputSummation().list()) { result++; const nextNeuron = connection.getSource(); if (visited.indexOf(nextNeuron) === -1) { queue.push(nextNeuron); } } } } return result; } /** * {@inheritDoc} */ encodeToArray(encoded) { let index = 0; const visited = []; const queue = []; // first copy outputs to queue for (let neuron of this.outputLayer.getNeurons()) { queue.push(neuron); } while (queue.length > 0) { // pop a neuron off the queue const neuron = queue[0]; queue.shift(); visited.push(neuron); // find anymore neurons and add them to the queue. if (neuron.getInputSummation() != null) { for (let connection of neuron.getInputSummation().list()) { encoded[index++] = connection.getWeight(); const nextNeuron = connection.getSource(); if (visited.indexOf(nextNeuron) === -1) { queue.push(nextNeuron); } } } } } /** * {@inheritDoc} */ getInputCount() { return this.inputLayer.sizeNonBias(); } /** * {@inheritDoc} */ getOutputCount() { return this.outputLayer.sizeNonBias(); } /** * @return {FreeformLayer} The output layer. */ getOutputLayer() { return this.outputLayer; } /** * Perform the specified connection task. This task will be performed over * all connections. * * @param task {Function} * The connection task. */ performConnectionTask(task) { const visited = []; for (let neuron of this.outputLayer.getNeurons()) { this.__performConnectionTask(visited, neuron, task); } } /** * Perform the specified connection task. * * @param visited {Array} * The list of visited neurons. * @param parentNeuron {FreeformNeuron} * The parent neuron. * @param task {Function} * The task. */ __performConnectionTask(visited, parentNeuron, task) { visited.push(parentNeuron); // does this neuron have any inputs? if (parentNeuron.getInputSummation() != null) { // visit the inputs for (let connection of parentNeuron.getInputSummation().list()) { task(connection); const neuron = connection.getSource(); // have we already visited this neuron? if (visited.indexOf(neuron) === -1) { this.__performConnectionTask(visited, neuron, task); } } } } /** * Perform the specified neuron task. This task will be executed over all * neurons. * * @param task {Function} The neuron task to perform. */ performNeuronTask(task) { const visited = []; for (let neuron of this.outputLayer.getNeurons()) { this.__performNeuronTask(visited, neuron, task); } } /** * Perform the specified neuron task. * @param visited {Array} The visited list. * @param parentNeuron {FreeformNeuron} The neuron to start with. * @param task {Function} The task to perform. */ __performNeuronTask(visited, parentNeuron, task) { visited.push(parentNeuron); task(parentNeuron); // does this neuron have any inputs? if (parentNeuron.getInputSummation() != null) { // visit the inputs for (let connection of parentNeuron.getInputSummation().list()) { const neuron = connection.getSource(); // have we already visited this neuron? if (visited.indexOf(neuron) === -1) { this.__performNeuronTask(visited, neuron, task); } } } } /** * {@inheritDoc} */ randomize() { const randomizer = new RangeRandomizer(-1, 1); /** * {@inheritDoc} */ this.performConnectionTask((connection) => { connection.setWeight(randomizer.nextDouble()); }); } /** * Allocate temp training space. * @param neuronSize {Number} The number of elements to allocate on each neuron. * @param connectionSize {Number} The number of elements to allocate on each connection. */ tempTrainingAllocate(neuronSize, connectionSize) { this.performNeuronTask((neuron) => { neuron.allocateTempTraining(neuronSize); if (neuron.getInputSummation() != null) { for (let connection of neuron.getInputSummation().list()) { connection.allocateTempTraining(connectionSize); } } }); } /** * Clear the temp training data. */ tempTrainingClear() { this.performNeuronTask((neuron) => { neuron.clearTempTraining(); if (neuron.getInputSummation() != null) { for (let connection of neuron.getInputSummation().list()) { connection.clearTempTraining(); } } }); } /** * Update context. */ updateContext() { this.performNeuronTask((neuron) => { neuron.updateContext(); }); } /** * Calculate the error for this neural network. We always calculate the error * using the "regression" calculator. Neural networks don't directly support * classification, rather they use one-of-encoding or similar. So just using * the regression calculator gives a good approximation. * * @param input {Array} * @param output {Array} * @return {Number} The error percentage. */ calculateError(input, output) { return ErrorUtil.calculateRegressionError(this, input, output); } } module.exports = FreeformNetwork;