// Start is called before the first frame update void Start() { List <int> dims = new List <int>(); dims.Add(3); dims.Add(2); NeuralNetwork nn = new NeuralNetwork(dims); Jatrix entered = new Jatrix(3, 2); entered[0, 0] = 1; entered[1, 0] = 0; entered[2, 0] = 0; entered[0, 1] = 0; entered[1, 1] = 1; entered[2, 1] = -1; nn.BeatPerceptronsWithHammer(1); Jatrix result = nn.GetOutputs(entered); Debug.Log(result.width + ", " + result.height); result.DebugMatrix(); nn.DebugPerceptron(0); }
//Overloaded multiplication operator public static Jatrix operator *(Jatrix a, Jatrix b) { if (a.width != b.height) { Debug.LogError("ERROR: MATRIX DIMENSION MISMATCH!"); return(null); } Jatrix toRet = new Jatrix(b.width, a.height); for (int i = 0; i < toRet.width; ++i) { for (int j = 0; j < toRet.height; ++j) { float sum = 0; for (int ii = 0; ii < a.width; ++ii) { sum += a[ii, j] * b[i, ii]; } toRet[i, j] = sum; } } return(toRet); }
//New neural network constructor //Takes in a list of ints, dictating size of each perceptron public NeuralNetwork(List <int> dimensions) { //Catch if less than 2 dimensions have been provided, as no weight matrices would be generated if (dimensions.Count <= 1) { isOk = false; Debug.LogError("ERROR: INSUFFICIENT DIMENSIONS FOR NEURAL NETWORK!"); return; } //Sets the input and output sizes to be the beginning and end of the dimension list respectively inputSize = dimensions[0]; outputSize = dimensions[dimensions.Count - 1]; //Generates a list of perceptrons and weight matrics accordingly perceptrons = new Jatrix[dimensions.Count - 1]; weightMatrices = new Jatrix[dimensions.Count - 1]; //Create a series of weight matrices, so that they can be multiplied together, to form perceptrons //By making the previous' width equal the next height, we can ensure that they are able to be multiplied for (int i = 0; i < dimensions.Count - 1; ++i) { perceptrons[i] = new Jatrix(dimensions[i + 1], 1); weightMatrices[i] = new Jatrix(dimensions[i + 1], dimensions[i]); } }
//Copy initialization. //Abuses the copy constructors of other classes to create a duplicate of the original matrix //No randomization here, for the sake of consistency. public void InitCar(CarController toCopy) { brain = new NeuralNetwork(toCopy.brain); inputData = new Jatrix(toCopy.inputData); outputData = new Jatrix(toCopy.outputData); previousPosition = transform.position; rb.velocity = transform.forward * carSpeedMedium; }
//Quickly makes an identity matrix public static Jatrix Identity(int rows) { Jatrix toReturn = new Jatrix(rows, rows); for (int i = 0; i < rows; ++i) { toReturn[i, i] = 1; } return(toReturn); }
//Overloaded multiplication operator, using a float public static Jatrix operator *(Jatrix a, float b) { Jatrix toRet = new Jatrix(a.width, a.height); for (int i = 0; i < toRet.width; ++i) { for (int j = 0; j < toRet.height; ++j) { toRet[i, j] = a[i, j] * b; } } return(toRet); }
//Non-Copy initialization of cars. //Forms input/output matrices as well as a neural network to compute them. //Also randomizes them slightly. public void InitCar(List <int> neuralNetworkShape, float randomizationStrength) { inputData = new Jatrix(neuralNetworkShape[0], 1); outputData = new Jatrix(neuralNetworkShape[neuralNetworkShape.Count - 1], 1); brain = new NeuralNetwork(neuralNetworkShape); //Alternate name for the weight randomization function. Same purpose. brain.BeatPerceptronsWithHammer(randomizationStrength); previousPosition = transform.position; rb.velocity = transform.forward * carSpeedMedium; }
//Copy constructor, creates an exact copy of a neural network public NeuralNetwork(NeuralNetwork copy) { inputSize = copy.inputSize; outputSize = copy.outputSize; weightMatrices = new Jatrix[copy.weightMatrices.Length]; perceptrons = new Jatrix[copy.perceptrons.Length]; for (int i = 0; i < weightMatrices.Length; ++i) { weightMatrices[i] = new Jatrix(copy.weightMatrices[i]); perceptrons[i] = new Jatrix(copy.perceptrons[i]); } }
//Also randomize the weight matrices public void RandomizeWeights(float range) { for (int i = 0; i < weightMatrices.Length; ++i) { Jatrix mat = weightMatrices[i]; for (int j = 0; j < mat.width; ++j) { for (int k = 0; k < mat.height; ++k) { //Offset every element of the weight matrices by a given range mat[j, k] += Random.Range(-range, range); } } } }
//Copy constructor, allows us to efficiently duplicate matrics public Jatrix(Jatrix toCopy) { width = toCopy.width; height = toCopy.height; elements = new float[width][]; for (int i = 0; i < width; ++i) { elements[i] = new float[height]; for (int j = 0; j < height; ++j) { elements[i][j] = toCopy[i, j]; } } }
//Get outputs from inputs //Does the multiplication of all the weight matrices, and generates perceptrons public Jatrix GetOutputs(Jatrix inputs) { //Check if the network is ok before doing this if (!isOk) { Debug.LogError("ERROR: NEURAL NETWORK FAILED CREATION! OPERATION INVALID ON BAD NN!"); return(null); } //Check if the input size matches what's expected if (inputs.width != inputSize) { Debug.LogError("ERROR: INPUT SIZE DIMENSION MISMATCH!"); return(null); } //Matrix used to iterate through the weight matrices Jatrix combined = new Jatrix(inputs); for (int i = 0; i < weightMatrices.Length; ++i) { //Multiply by a weight matrix combined *= weightMatrices[i]; for (int j = 0; j < combined.width; ++j) { //Shifted sigmoid, generates a value on [-1, 1] as opposed to [0, 1] combined[j, 0] = 2f / (1f + Mathf.Exp(-combined[j, 0])) - 1; //Below is the step function originally used //if (combined[j, 0] > 0.5f) // combined[j, 0] = 1; //else if (combined[j, 0] < 0.5f) // combined[j, 0] = -1; //else // combined[j, 0] = 0; } //Save as a perceptron perceptrons[i] = new Jatrix(combined); } return(combined); }
//Overloaded matrix addition public static Jatrix operator+(Jatrix a, Jatrix b) { if (a.width != b.width || a.height != b.height) { Debug.LogError("ERROR: MATRIX DIMENSION MISMATCH!"); return(null); } Jatrix toRet = new Jatrix(a.width, a.height); for (int i = 0; i < toRet.width; ++i) { for (int j = 0; j < toRet.height; ++j) { toRet[i, j] = a[i, j] + b[i, j]; } } return(toRet); }
//Pseudo fixed update, used to offer more control of what executes and when //Originally was split up into several functions, to test them individually //Parts of it can return false to indicate something is wrong OR the car has crashed. public void PseudoFixedUpdateStep(CarManager cm) { if (!CalculateReward()) { return; } //Gravity, to offer us more control should we want it rb.AddForce(Vector3.down * 30f, ForceMode.Force); if (!CreateInputs()) { return; } //Prints the values of the perceptrons if (TagForDebug) { string bigData = ""; for (int i = 0; i < brain.perceptrons.Length; ++i) { bigData += "\n"; for (int j = 0; j < brain.perceptrons[i].width; ++j) { bigData += ("\t " + brain.perceptrons[i][j, 0]); } } Debug.Log(bigData); } //Calculate output data from input data outputData = brain.GetOutputs(inputData); if (!PerformOutputs()) { return; } }