/*
Copyright (c) 2005-2007 Joseph Gleason
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Current versions of this and other code can be downloaded at:
http://gleason.cc/
*/
package cc.glsn.v15.neuralnet;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.HashMap;
import java.util.TreeSet;
/**
* This class acts as a container for an artificial feed-forward neural network.
*
* Supportes any number of inputs, any number of hidden layers of any size and any
* number of outputs. The inputs and outputs are identified with a string identifier
* so the user doesn't have to worry about indexing. Also, inputs and outputs may be added
* at any time, so not all inputs and outputs need to be known initialy.
*
* Each input connects to each neuron on the first hidden layer is using the default
* input mapping of 'Full'. Each hidden layer is fully connected to the next hidden
* layer. Each neuron on the last hidden layer connects to the output neuron.
*
* It is serializable, as are all the contained classes. Thus its state
* can be saved to files, databases, etc.
*
* @author Joseph Gleason
* @see #Brain(NetFunction, LinkedList, String)
* @see #Brain(NetFunction, LinkedList)
*/
public class Brain implements java.io.Serializable
{
private static final long serialVersionUID = 3460737085322424862L;
private long StateSerial;
private Random R;
private HashMap AllNeurons;
private HashMap InputLayer;
private TreeSet OutputLayer;
private HashMap > HiddenLayers;
private Input BiasInput;
private LinkedList LayerSizes;
private LinkedList NeuronOrder;
private NetFunction FunctG;
private String InputMappingStyle;
/**
*
* Uses the default Input mapping style of 'Full'.
*
* @see #Brain(NetFunction, LinkedList, String)
*
* @param functg The function used inside neurons
* @param layerSizes a list of hidden layer sizes
*/
public Brain(NetFunction functg, LinkedList layerSizes)
{
this(functg,layerSizes,"Full");
}
/**
*
* @param functg The function used inside neurons
* @param layerSizes a list of hidden layer sizes
* @param inputMappingStyle Controls how the input layer is connected to the first hidden layer.
*
*
Current options are:
*
* 'Full' for each input node connected to each layer 1 neuron
*
* 'EvenSplit' each input node is connected to one neuron and each neuron has
* roughly equan number of inputs. This is for handling a very large number of inputs.
*/
public Brain(NetFunction functg, LinkedList layerSizes, String inputMappingStyle)
{
StateSerial=1;
R=new Random();
InputMappingStyle=inputMappingStyle;
InputLayer=new HashMap();
OutputLayer=new TreeSet();
FunctG=functg;
BiasInput=new Input();
BiasInput.setValue(0.5);
setHiddenLayers(layerSizes);
}
/**
* Resets the hidden layer sizes
* @param layerSizes - new sizes. The first element of the linked list will
* be the layer closest to the input. The last element will be the layer closest to
* the output layer.
*
*
* Example: a list of [32,24,16] will make a network with 32 neurons on the first hidden layer,
* 24 on the next and 16 on the next.
*
*/
public void setHiddenLayers(LinkedList layerSizes)
{
if (LayerSizes!=null)
{
if (layerSizes.equals(LayerSizes)) return;
}
NeuronOrder=null;
LayerSizes=new LinkedList();
LayerSizes.addAll(layerSizes);
AllNeurons=new HashMap(16,0.5f);
HiddenLayers=new HashMap > (16,0.5f);
int Layer=0;
for(Integer I : LayerSizes)
{
for(int i=0; i());
HiddenLayers.get(Layer).add(id);
}
Layer++;
}
//Link internal layers
for(int l=1; l last=HiddenLayers.get(l-1);
LinkedList inputs=new LinkedList();
inputs.add(BiasInput);
for(NetElementID id_b : last)
{
inputs.add(AllNeurons.get(id_b));
}
NetworkSource input_array[]=getArray(inputs);
for(NetElementID id_a : HiddenLayers.get(l))
{
AllNeurons.get(id_a).setSources(input_array);
}
}
resetAllInputs();
resetAllOutputs();
}
/**
* Converts a collection into an array. The neurons
* use arrays for sources internally so that they use less memory.
* @param in
* @return
*/
private NetworkSource[] getArray(Collection in)
{
NetworkSource arr[]=new NetworkSource[in.size()];
int idx=0;
for(NetworkSource s : in )
{
arr[idx]=s;
idx++;
}
return arr;
}
/**
* Sets the mapping between input and first layer as per
* input mapping style.
*/
private void resetAllInputs()
{
if (InputMappingStyle.equals("Full"))
{
LinkedList inputs=new LinkedList();
inputs.add(BiasInput);
for(Input in : InputLayer.values())
{
inputs.add(in);
}
NetworkSource input_array[]=getArray(inputs);
for(NetElementID id_a : HiddenLayers.get(0))
{
AllNeurons.get(id_a).setSources(input_array);
}
}
else if (InputMappingStyle.equals("EvenSplit"))
{
//I am lazy, so just doing round robin
//Get each neuron in first layer
TreeMap > InputMap=
new TreeMap >();
for(NetElementID id_a : HiddenLayers.get(0))
{
LinkedList ll=new LinkedList();
ll.add(BiasInput);
InputMap.put(id_a, ll);
}
//get all the inputs to distrubte
TreeMap SourcesToDistribute=new TreeMap();
SourcesToDistribute.putAll(InputLayer);
//Hand them out one at a time to each neuron
while(SourcesToDistribute.size()>0)
{
for(LinkedList ll : InputMap.values())
{
if (SourcesToDistribute.size()>0)
{
NetworkSource src=SourcesToDistribute.firstEntry().getValue();
SourcesToDistribute.remove(SourcesToDistribute.firstKey());
ll.add(src);
}
if (SourcesToDistribute.size()>0)
{
NetworkSource src=SourcesToDistribute.firstEntry().getValue();
SourcesToDistribute.remove(SourcesToDistribute.firstKey());
ll.add(src);
}
}
}
//Save the neuron input lists
for(Map.Entry > me : InputMap.entrySet())
{
NetworkSource[] srcs=getArray(me.getValue());
AllNeurons.get(me.getKey()).setSources(srcs);
}
}
else
{
throw new Error("Unknown input mapping style: " + InputMappingStyle);
}
}
/**
* Sets each element of the last layer as an input for each of the outputs
*/
private void resetAllOutputs()
{
int LastLayer=LayerSizes.size()-1;
LinkedList inputs=new LinkedList();
inputs.add(BiasInput);
for(NetElementID id_a : HiddenLayers.get(LastLayer))
{
inputs.add(AllNeurons.get(id_a));
}
NetworkSource input_array[]=getArray(inputs);
for(NetElementID out_id : OutputLayer)
{
Neuron n=new Neuron(FunctG,R);
n.setSources(input_array);
AllNeurons.put(out_id,n);
}
}
/**
* Add a new input of the given name. Maps input to first layer.
* The remapping function is expensive, so if you are doing
* more than a handful, use addInputGroup() instead.
*
* @see #addInputGroup(Collection)
*
* @param name
*/
public void addInput(String name)
{
NetElementID id=new NetElementID(name);
if (!InputLayer.containsKey(id))
{
InputLayer.put(id, new Input());
resetAllInputs();
StateSerial++;
}
}
/**
* Add a group if inputs at once. Advantage over single addInput() above
* is that for multiple entrys, this method only remaps the input to the first
* hidden layer once. The remapping function is expensive, so if you are doing
* more than a handful, use this.
*
* @see #addInput(String)
*
* @param names
*/
public void addInputGroup(Collection names)
{
for(String name : names )
{
NetElementID id=new NetElementID(name);
InputLayer.put(id,new Input());
}
StateSerial++;
resetAllInputs();
}
/**
* Add an output with the given name.
* @param name
*/
public void addOutput(String name)
{
NeuronOrder=null;
NetElementID id=new NetElementID(name);
if (!OutputLayer.contains(id))
{
Neuron n=new Neuron(FunctG,R);
AllNeurons.put(id,n);
OutputLayer.add(id);
LinkedList inputs=new LinkedList();
inputs.add(BiasInput);
int LastLayer=LayerSizes.size()-1;
for(NetElementID id_a : HiddenLayers.get(LastLayer))
{
inputs.add(AllNeurons.get(id_a));
}
NetworkSource input_array[]=getArray(inputs);
n.setSources(input_array);
}
}
/**
* Remove the output with the given name, if it exists
* @param name
*/
public void removeOutput(String name)
{
NeuronOrder=null;
NetElementID id=new NetElementID(name);
if (OutputLayer.contains(id))
{
AllNeurons.remove(id);
OutputLayer.remove(id);
}
}
/**
* Sets the value of the given input to 'val'
* @param name
* @param val
*/
public void setInput(String name, double val)
{
NetElementID id=new NetElementID(name);
InputLayer.get(id).setValue(val);
StateSerial++;
}
/**
* So that inputs can be set directly to avoid the Map lookup
* that happens when setInput is used. In order for this to give
* any speed improvement, the caller has to save the output from this call
* somewhere.
*
* Note: caller is responcible for calling incSerial() on the Brain
* after inputs are updated. If that is not done, the neurons
* will not re-read their inputs and wont do anything.
*
* @see #incSerial()
* @see #setInput(String, double)
*
* @param name name of input to get
* @return The input node for the input
*/
public Input getInputNode(String name)
{
NetElementID id=new NetElementID(name);
return InputLayer.get(id);
}
/**
* Manually increment the serial number. Useful when updating the values
* of input nodes directly (after getting the input nodes with getInputNode()
*
* @see #getInputNode(String)
*/
public void incSerial()
{
StateSerial++;
}
/**
* Set all inputs to the given value. Useful in cases where not every input
* is set on every pass.
*
* @param val - value to set inputs to
*/
public void setAllInput(double val)
{
for(Input I : InputLayer.values())
{
I.setValue(val);
}
StateSerial++;
}
/**
* Get the output from the single output neuron.
* It will internally cache until the inputs change, so it may be called repeatedly
* without incurring recomputation expense.
*
* If called after inputs change, the network will of course have to be traversed
* to get the answer
*
* Note: this can also be used to get the output value of hidden neurons if
* you know their name. Hidden neurons are named "hidden_L_N" where L is the layer number
* (zero indexed) and N is their position inside the layer (zero indexed).
*
* If you ask for a non-existant neuron, you will get a null pointer exception
* @return output value for neuron
*/
public double getOutput(String name)
{
Neuron N=AllNeurons.get(new NetElementID(name));
return N.getValue(StateSerial);
}
/**
* Get the output values for all output neurons in a map
* @return a map of output names to values
*/
public Map getAllOutputs()
{
Map M=new TreeMap();
for(NetElementID id : OutputLayer)
{
M.put(id.getID(),getOutput(id.getID()));
}
return M;
}
/**
* Do back propogation learning with the given alpha and target.
* Time complexity n where n is number of neuron links
*
* @param name - Output name to learn
* @param alpha - The learning factor. Should be positive and less than one. 0.01 is a good starting point.
* @param target - What the output value should be for the set inputs
*/
public void backPropogate(String name, double alpha, double target)
{
{
Neuron n=AllNeurons.get(new NetElementID(name));
n.addToBackProp(target - n.getValue(StateSerial));
}
backPropogateAll(alpha);
}
/**
* Do back propogation on a group of output targets at once.
* Time complexity n where n is number of neuron links
*
* @param alpha - learning factor. Should be positive and less than one. 0.01 is a good starting point.
* @param targets a map of string(output names) to doubles (target output values) to learn on
*/
public void backPropogate( double alpha, Map targets)
{
for(Map.Entry me : targets.entrySet())
{
NetElementID id=new NetElementID(me.getKey());
double tar=me.getValue();
Neuron n=AllNeurons.get(id);
n.addToBackProp(tar - n.getValue(StateSerial));
}
backPropogateAll(alpha);
}
private void backPropogateAll(double alpha)
{
if (NeuronOrder==null)
{
NeuronOrder=new LinkedList();
NeuronOrder.addAll(OutputLayer);
for(int layer=LayerSizes.size()-1; layer>=0; layer--)
{
NeuronOrder.addAll(HiddenLayers.get(layer));
}
}
for(NetElementID id : NeuronOrder)
{
AllNeurons.get(id).doAccumulatedBackProp(alpha);
}
StateSerial++;
}
public String toString()
{
return "Brain{"+ AllNeurons +"," + InputLayer +"}";
}
}