Scripting the neural network

With the new project set up, we can now start writing our scripts to build a neural network. Go back to Unity and perform the following steps:

  1. Open the ARCoreML/Scripts folder and then from the menu, select Assets | Create | C# Script. Name the script as Neuron and double-click to open it in your editor of choice.
The code for this example was originally sourced from https://github.com/Blueteak/Unity-Neural-Network.git, which shows an excellent example of a simple and concise neural network with training explicitly developed for Unity. We will modify the original code for our needs, but feel free to check out and contribute to the original source if you are interested. This code is great for learning, but certainly, it's not something you may want to use in production. We will look at options for production-ready neural networks in the section on TensorFlow.
  1. Delete all the code, leave the using statements, and then add the following:
using System.Linq; //add after other using's

public class Neuron
{
private static readonly System.Random Random = new System.Random();
public List<Synapse> InputSynapses;
public List<Synapse> OutputSynapses;
public double Bias;
public double BiasDelta;
public double Gradient;
public double Value;
}
  1. Note how this class does not inherit MonoBehaviour and thus will not be a game object, which means we will load this class in another script. Then, we create a placeholder for Random; we do this because we are using System.Random rather than Unity.RandomUnity.Random only supports generating a random float, but we need the precision of a double. The rest are just properties that we will discuss as we get to the relevant code sections.
  2. Enter the following after the last property declaration but before the class's ending brace:
public static double GetRandom()
{
return 2 * Random.NextDouble() - 1;
}
  1. We create this static helper method in order to generate double random numbers from -1.0 to 1.0. This allows for greater precision and assures that our values are always getting generated around 0. Keeping values close to 0 avoids rounding errors and just generally makes things easier to calculate.
  2. Next, enter the following code after the static method:
public Neuron()
{
InputSynapses = new List<Synapse>();
OutputSynapses = new List<Synapse>();
Bias = GetRandom();
}

public Neuron(IEnumerable<Neuron> inputNeurons) : this()
{
foreach (var inputNeuron in inputNeurons)
{
var synapse = new Synapse(inputNeuron, this);
inputNeuron.OutputSynapses.Add(synapse);
InputSynapses.Add(synapse);
}
}
  1. Here, we set up a base and single parameter constructors. The base constructor creates a List<Synapse> for the input and output connections to the neuron. A Synapse represents a connection. The other constructor calls the base (this) and takes an IEnumerable<Neuron> of neurons that it then connects back to. This way, networks can be built bottom up; we will see how this works when we get to the NeuralNet class.
  2. Next, we will add the rest of the methods for the Neuron class:
public virtual double CalculateValue()
{
return Value = Sigmoid.Output(InputSynapses.Sum(a => a.Weight *
a.InputNeuron.Value) + Bias);
}

public double CalculateError(double target)
{
return target - Value;
}

public double CalculateGradient(double? target = null)
{
if (target == null)
return Gradient = OutputSynapses.Sum(a =>
a.OutputNeuron.Gradient * a.Weight) * Sigmoid.Derivative(Value);
return Gradient = CalculateError(target.Value) * Sigmoid.Derivative(Value);
}

public void UpdateWeights(double learnRate, double momentum)
{
var prevDelta = BiasDelta;
BiasDelta = learnRate * Gradient;
Bias += BiasDelta + momentum * prevDelta;
foreach (var synapse in InputSynapses)
{
prevDelta = synapse.WeightDelta;
synapse.WeightDelta = learnRate * Gradient * synapse.InputNeuron.Value;
synapse.Weight += synapse.WeightDelta + momentum * prevDelta;
}
}
  1. We added four methods here: CalculateValue, CalculateError, CalculateGradient, and UpdateWeightsCalculateValue is used to determine the neuron's output based on the activation function we defined in Sigmoid. We will get to Sigmoid shortly. The other methods are used to train the neuron. Training a neuron is something we will cover in the next section.
  2. Stay in the same file, and add the following three new helper classes outside the Neuron class:
} // end of Neuron class definition

public class Synapse
{
public Neuron InputNeuron;
public Neuron OutputNeuron;
public double Weight;
public double WeightDelta;
public Synapse(Neuron inputNeuron, Neuron outputNeuron)
{
InputNeuron = inputNeuron;
OutputNeuron = outputNeuron;
Weight = Neuron.GetRandom();
}
}

public static class Sigmoid
{
public static double Output(double x)
{
return x < -45.0 ? 0.0 : x > 45.0 ? 1.0 : 1.0 / (1.0 +
Mathf.Exp((float)-x));
}
public static double Derivative(double x)
{
return x * (1 - x);
}
}
public class DataSet
{
public double[] Values;
public double[] Targets;
public DataSet(double[] values, double[] targets)
{
Values = values;
Targets = targets;
}
}
  1. The first class Synapse, as we already know, defines a connection between neurons. Next comes Sigmoid, which, conveniently enough, is just a wrapper class for the sigmoid activation function we use. Note that the values are getting capped at -45.0 and +45.0. This limits the size of our network, but we can manually change that later. Then comes DataSet, which is just a holder for our training data.

That completes the Neuron class. Create another script in Unity, and this time, call it NeuralNet; open it up in your editor of choice and perform the following steps:

  1. Delete the starter code again, but leave the using's statements, and enter the following:
public class NeuralNet
{
public double LearnRate;
public double Momentum;
public List<Neuron> InputLayer;
public List<Neuron> HiddenLayer;
public List<Neuron> OutputLayer;

} //be sure to add ending brace
  1. Again, this is another set of public properties that define the LearnRate network and Momentum. Then, three List<Neuron> to hold the collection of neurons in the input, hidden (middle), and output layers. In this example, we use a single hidden layer, but more sophisticated networks often support several more layers. You guessed it, LearnRate and Momentum will be covered in the section on training.
We generally prefer not to use properties with getters and setters in Unity. Why? Primarily because the Unity editor just plays better with public fields. Secondarily, game programming is all about performance, and it only makes sense to avoid the overhead of getters and setters where possible. Using a list is also a no-no, but it makes the code easier to understand in this case.
  1. Next, let's add a constructor for our NeuralNet:
public NeuralNet(int inputSize, int hiddenSize, int outputSize, 
double? learnRate = null, double? momentum = null)
{
LearnRate = learnRate ?? .4;
Momentum = momentum ?? .9;
InputLayer = new List<Neuron>();
HiddenLayer = new List<Neuron>();
OutputLayer = new List<Neuron>();
for (var i = 0; i < inputSize; i++){
InputLayer.Add(new Neuron());
}

for (var i = 0; i < hiddenSize; i++){
HiddenLayer.Add(new Neuron(InputLayer));
}

for (var i = 0; i < outputSize; i++){
OutputLayer.Add(new Neuron(HiddenLayer));
}
}
  1. This constructor expects several inputs, including the number of neurons in the input, hidden, and output layers, in addition to a value for the learnRate and momentum. Inside the constructor, the properties are initialized based on the input values. Note how the first layer uses the default Neuron constructor, and the successive layers use the single parameter constructor with the previous layer as input. Remember from building the Neuron class that this is where all the synapse connections between the neuron layers are added.
  2. Next, we will add a couple of methods for training:
public void Train(List<DataSet> dataSets, int numEpochs)
{
for (var i = 0; i < numEpochs; i++)
{
foreach (var dataSet in dataSets)
{
ForwardPropagate(dataSet.Values);
BackPropagate(dataSet.Targets);
}
}
}

public void Train(List<DataSet> dataSets, double minimumError)
{
var error = 1.0;
var numEpochs = 0;
while (error > minimumError && numEpochs < int.MaxValue)
{
var errors = new List<double>();
foreach (var dataSet in dataSets)
{
ForwardPropagate(dataSet.Values);
BackPropagate(dataSet.Targets);
errors.Add(CalculateError(dataSet.Targets));
}
error = errors.Average();
numEpochs++;
}
}
  1. Then, we will add methods to propagate the network forward and backward:
private void ForwardPropagate(params double[] inputs)
{
var i = 0;
InputLayer.ForEach(a => a.Value = inputs[i++]);
HiddenLayer.ForEach(a => a.CalculateValue());
OutputLayer.ForEach(a => a.CalculateValue());
}

private void BackPropagate(params double[] targets)
{
var i = 0;
OutputLayer.ForEach(a => a.CalculateGradient(targets[i++]));
HiddenLayer.ForEach(a => a.CalculateGradient());
HiddenLayer.ForEach(a => a.UpdateWeights(LearnRate, Momentum));
OutputLayer.ForEach(a => a.UpdateWeights(LearnRate, Momentum));
}
  1. Finally, add the following methods to compute the whole network and to calculate errors:
public double[] Compute(params double[] inputs)
{
ForwardPropagate(inputs);
return OutputLayer.Select(a => a.Value).ToArray();
}

private double CalculateError(params double[] targets)
{
var i = 0;
return OutputLayer.Sum(a => Mathf.Abs((float)a.CalculateError(targets[i++])));
}

That completes the neural network code. We left a number of areas for discussion in the next section on training the neural network.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.190.239.166