<commit_before>package sergeysav.neuralnetwork.chess;

import java.io.Serializable;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import sergeysav.neuralnetwork.NeuralNetwork;
import sergeysav.neuralnetwork.Neuron;

/**
 * An implementation of a backpropogation neural network trainer for the chess games
 * 
 * @author sergeys
 *
 */
public class ChessTrainer implements Serializable {

	private static final long serialVersionUID = 3561024763642610745L;
	
	private transient double learningRate;
	private transient Supplier<Stream<double[]>> trainingData;
	//private transient int trainingSize;
	private transient Supplier<Stream<double[]>> testingData;
	private transient double epsilon;
	//private transient int testingSize;
	
	private double lastTestingError = Double.MAX_VALUE;

	private transient NeuralNetwork network;
	private double[][][] learningMomentum;
	private int epochs = 0;

	public ChessTrainer(double k, Supplier<Stream<double[]>> trainingData, Supplier<Stream<double[]>> testingData, NeuralNetwork network, double epsilon) {
		init(k, trainingData, testingData, network, epsilon);
		learningMomentum = new double[network.getNeuralData().length][][];
		for (int i = 0; i<learningMomentum.length; i++) {
			learningMomentum[i] = new double[network.getNeuralData()[i].length][];
			for (int j = 0; j<learningMomentum[i].length; j++) {
				learningMomentum[i][j] = new double[network.getNeuralData()[i][j].getParentNeurons() + 1];
			}
		}
	}
	
	private double[][][] generateMomentumArr() {
		double[][][] m = new double[network.getNeuralData().length][][];
		for (int i = 0; i<m.length; i++) {
			m[i] = new double[network.getNeuralData()[i].length][];
			for (int j = 0; j<m[i].length; j++) {
				m[i][j] = new double[network.getNeuralData()[i][j].getParentNeurons() + 1];
			}
		}
		return m;
	}
	
	public void init(double k, Supplier<Stream<double[]>> trainingData, Supplier<Stream<double[]>> testingData, NeuralNetwork network, double epsilon) {
		this.learningRate = k;
		this.trainingData = trainingData;
		this.testingData = testingData;
		this.network = network;
		this.epsilon = epsilon;
	}
	
	public boolean isNextEpochNeeded() {
		double err = calculateAverageError(trainingData);
		ChessAIMain.print("Training Error: " + err);
		return err > epsilon && testingCheck();
	}
	
	private boolean testingCheck() {
		double e = calculateAverageError(testingData);
		ChessAIMain.print("Testing Error: " + e);
		boolean b = lastTestingError > e;
		lastTestingError = e;
		return b;
	}
	
	/**
	 * Is next epoch needed MUST ALWAYS be called before this
	 */
	public void performEpoch() {
		epochs++;
		
		//Calculate the error
		//lastTestingError = calculateAverageError(testingData);
		
		//Perform backpropogation on every set of inputs
		//This is what I want to work: it should use multiple threads for the backpropogation
		//trainingData.get().map(this::performBackpropogation).reduce(new double[][][], accumulator)
		trainingData.get().sequential().forEach(this::performBackpropogation);
		
		//Apply the learning momentum
		for (int i = 0; i<learningMomentum.length; i++) {
			for (int j = 0; j<learningMomentum[i].length; j++) {
				Neuron n = network.getNeuralData()[i][j];
				n.setBias(n.getBias() + learningMomentum[i][j][0]);
				learningMomentum[i][j][0] *= 0.9;
				for (int k = 1; k<learningMomentum[i][j].length; k++) {
					n.getWeights()[k-1] += learningMomentum[i][j][k];
					learningMomentum[i][j][k] *= 0.9;
				}
			}
		}
	}

	public TrainingResult getResult() {
		return new TrainingResult(calculateAverageError(trainingData), calculateAverageError(testingData), epochs, calculateAverageError(trainingData) <= epsilon);
	}

	private void performBackpropogation(double[] data) {
		Neuron[][] neuralData = network.getNeuralData();
		double[][] neuralOutputs = new double[network.getNeuralData().length][];
		double[][] neuralDeltas = new double[network.getNeuralData().length][];

		double[][] trial = splitArray(data, network.getInputNeurons());

		double[] input =  trial[0];
		double[] target = trial[1];

		{
			double[] lastLayer = trial[0];
			for (int i = 0; i<neuralOutputs.length; i++) {
				neuralOutputs[i] = new double[network.getNeuralData()[i].length];
				for (int j = 0; j<neuralOutputs[i].length; j++) {
					neuralOutputs[i][j] = neuralData[i][j].getOutput(lastLayer);
				}
				lastLayer = neuralOutputs[i];
			}
		}

		//double[][][] result = new double[network.getNeuralData().length][][];

		for (int i = neuralOutputs.length-1; i>=0; i--) {
			//result[i] = new double[network.getNeuralData()[i].length][];
			neuralDeltas[i] = new double[network.getNeuralData()[i].length];
			for (int j = 0; j<neuralOutputs[i].length; j++) {
				//result[i][j] = new double[network.getNeuralData()[i][j].getParentNeurons() + 1];
				Neuron neuron = neuralData[i][j];
				double output = neuralOutputs[i][j];
				double deltaWeight;
				if (i == neuralOutputs.length-1) { //Last Hidden -> Output Layer Connection Delta Weight
					deltaWeight = neuron.getDerivativeFunction().apply(output) * (target[j] - output); //Derivative of error squared (Chain rule). 2 ignored b/c of later multiplied constant
				} else { //Layer -> Hidden Layer Connection Delta Weight
					final int iVal = i;
					final int jVal = j;
					double sum = IntStream.range(0, neuralOutputs[i+1].length).mapToDouble((idx)->neuralData[iVal+1][idx].getWeights()[jVal] * neuralDeltas[iVal+1][idx]).sum();
					deltaWeight = neuron.getDerivativeFunction().apply(output) * sum;
				}
				neuralDeltas[i][j] = deltaWeight;
				//New weight = oldWeight + k * output(source) * deltaWeight(thisNode)
				learningMomentum[i][j][0] += learningRate * deltaWeight;
				for (int k = 0; k < neuron.getWeights().length; k++) {
					learningMomentum[i][j][k+1] += learningRate * (i > 0 ? neuralOutputs[i-1][k] : input[k]) * deltaWeight;
					//neuron.getWeights()[k] += learningRate * (i > 0 ? neuralOutputs[i-1][k] : input[k]) * deltaWeight;
				}
				//neuron.setBias(neuron.getBias() + learningRate * deltaWeight);
			}
		}
		//System.out.println(Arrays.deepToString(neuralOutputs));
		//return result;
	} 
	//Output neurons: constant * FromOutput * error
	//Hidden neurons: constant * FromOutput * error

	private double[][] splitArray(double[] arr, int lengthFirst) {
		double[][] output = new double[][] {new double[lengthFirst], new double[arr.length-lengthFirst]};

		System.arraycopy(arr, 0, output[0], 0, lengthFirst);
		System.arraycopy(arr, lengthFirst, output[1], 0, arr.length - lengthFirst);<commit_msg>adds multithreading to the backpropogation algorithm<commit_after>package sergeysav.neuralnetwork.chess;

import java.io.Serializable;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import sergeysav.neuralnetwork.NeuralNetwork;
import sergeysav.neuralnetwork.Neuron;

/**
 * An implementation of a backpropogation neural network trainer for the chess games
 * 
 * @author sergeys
 *
 */
public class ChessTrainer implements Serializable {

	private static final long serialVersionUID = 3561024763642610745L;
	
	private transient double learningRate;
	private transient Supplier<Stream<double[]>> trainingData;
	//private transient int trainingSize;
	private transient Supplier<Stream<double[]>> testingData;
	private transient double epsilon;
	//private transient int testingSize;
	
	private double lastTestingError = Double.MAX_VALUE;

	private transient NeuralNetwork network;
	private double[][][] learningMomentum;
	private int epochs = 0;

	public ChessTrainer(double k, Supplier<Stream<double[]>> trainingData, Supplier<Stream<double[]>> testingData, NeuralNetwork network, double epsilon) {
		init(k, trainingData, testingData, network, epsilon);
		learningMomentum = generateMomentumArr();
	}
	
	private double[][][] generateMomentumArr() {
		double[][][] m = new double[network.getNeuralData().length][][];
		for (int i = 0; i<m.length; i++) {
			m[i] = new double[network.getNeuralData()[i].length][];
			for (int j = 0; j<m[i].length; j++) {
				m[i][j] = new double[network.getNeuralData()[i][j].getParentNeurons() + 1];
			}
		}
		return m;
	}
	
	public void init(double k, Supplier<Stream<double[]>> trainingData, Supplier<Stream<double[]>> testingData, NeuralNetwork network, double epsilon) {
		this.learningRate = k;
		this.trainingData = trainingData;
		this.testingData = testingData;
		this.network = network;
		this.epsilon = epsilon;
	}
	
	public boolean isNextEpochNeeded() {
		double err = calculateAverageError(trainingData);
		ChessAIMain.print("Training Error: " + err);
		return err > epsilon && testingCheck();
	}
	
	private boolean testingCheck() {
		double e = calculateAverageError(testingData);
		ChessAIMain.print("Testing Error: " + e);
		boolean b = lastTestingError > e;
		lastTestingError = e;
		return b;
	}
	
	/**
	 * Is next epoch needed MUST ALWAYS be called before this
	 */
	public void performEpoch() {
		epochs++;
		
		//Calculate the error
		//lastTestingError = calculateAverageError(testingData);
		
		//Perform backpropogation on every set of inputs
		//This is what I want to work: it should use multiple threads for the backpropogation
		learningMomentum = sumArray3(trainingData.get().map(this::performBackpropogation).reduce(generateMomentumArr(), this::sumArray3), learningMomentum);
		//trainingData.get().sequential().forEach(this::performBackpropogation);
		
		//Apply the learning momentum
		for (int i = 0; i<learningMomentum.length; i++) {
			for (int j = 0; j<learningMomentum[i].length; j++) {
				Neuron n = network.getNeuralData()[i][j];
				n.setBias(n.getBias() + learningMomentum[i][j][0]);
				learningMomentum[i][j][0] *= 0.9;
				for (int k = 1; k<learningMomentum[i][j].length; k++) {
					n.getWeights()[k-1] += learningMomentum[i][j][k];
					learningMomentum[i][j][k] *= 0.9;
				}
			}
		}
	}
	
	private double[][][] sumArray3(double[][][] a1, double[][][] a2) {
		double[][][] result = new double[a1.length][][];
		for (int i = 0; i<result.length; i++) {
			result[i] = new double[a1[i].length][];
			for (int j = 0; j<result[i].length; j++) {
				result[i][j] = new double[a1[i][j].length];
				for (int k = 1; k<result[i][j].length; k++) {
					result[i][j][k] = a1[i][j][k] + a2[i][j][k];
				}
			}
		}
		return result;
	}

	public TrainingResult getResult() {
		return new TrainingResult(calculateAverageError(trainingData), calculateAverageError(testingData), epochs, calculateAverageError(trainingData) <= epsilon);
	}

	private double[][][] performBackpropogation(double[] data) {
		Neuron[][] neuralData = network.getNeuralData();
		double[][] neuralOutputs = new double[network.getNeuralData().length][];
		double[][] neuralDeltas = new double[network.getNeuralData().length][];

		double[][] trial = splitArray(data, network.getInputNeurons());

		double[] input =  trial[0];
		double[] target = trial[1];
		
		double[][][] result = generateMomentumArr();

		{
			double[] lastLayer = trial[0];
			for (int i = 0; i<neuralOutputs.length; i++) {
				neuralOutputs[i] = new double[network.getNeuralData()[i].length];
				for (int j = 0; j<neuralOutputs[i].length; j++) {
					neuralOutputs[i][j] = neuralData[i][j].getOutput(lastLayer);
				}
				lastLayer = neuralOutputs[i];
			}
		}

		//double[][][] result = new double[network.getNeuralData().length][][];

		for (int i = neuralOutputs.length-1; i>=0; i--) {
			//result[i] = new double[network.getNeuralData()[i].length][];
			neuralDeltas[i] = new double[network.getNeuralData()[i].length];
			for (int j = 0; j<neuralOutputs[i].length; j++) {
				//result[i][j] = new double[network.getNeuralData()[i][j].getParentNeurons() + 1];
				Neuron neuron = neuralData[i][j];
				double output = neuralOutputs[i][j];
				double deltaWeight;
				if (i == neuralOutputs.length-1) { //Last Hidden -> Output Layer Connection Delta Weight
					deltaWeight = neuron.getDerivativeFunction().apply(output) * (target[j] - output); //Derivative of error squared (Chain rule). 2 ignored b/c of later multiplied constant
				} else { //Layer -> Hidden Layer Connection Delta Weight
					final int iVal = i;
					final int jVal = j;
					double sum = IntStream.range(0, neuralOutputs[i+1].length).mapToDouble((idx)->neuralData[iVal+1][idx].getWeights()[jVal] * neuralDeltas[iVal+1][idx]).sum();
					deltaWeight = neuron.getDerivativeFunction().apply(output) * sum;
				}
				neuralDeltas[i][j] = deltaWeight;
				//New weight = oldWeight + k * output(source) * deltaWeight(thisNode)
				result[i][j][0] += learningRate * deltaWeight;
				for (int k = 0; k < neuron.getWeights().length; k++) {
					result[i][j][k+1] += learningRate * (i > 0 ? neuralOutputs[i-1][k] : input[k]) * deltaWeight;
					//neuron.getWeights()[k] += learningRate * (i > 0 ? neuralOutputs[i-1][k] : input[k]) * deltaWeight;
				}
				//neuron.setBias(neuron.getBias() + learningRate * deltaWeight);
			}
		}
		//System.out.println(Arrays.deepToString(neuralOutputs));
		return result;
	} 
	//Output neurons: constant * FromOutput * error
	//Hidden neurons: constant * FromOutput * error

	private double[][] splitArray(double[] arr, int lengthFirst) {
		double[][] output = new double[][] {new double[lengthFirst], new double[arr.length-lengthFirst]};

		System.arraycopy(arr, 0, output[0], 0, lengthFirst);
		System.arraycopy(arr, lengthFirst, output[1], 0, arr.length - lengthFirst);
{'fraction_non_alphanumeric': 0.15562839901661327, 'fraction_numerical': 0.010131863219846531, 'mean_word_length': 4.570124481327801, 'pattern_counts': {'":': 0, '<': 52, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '671183', 'n_tokens_mistral': 4997, 'n_tokens_neox': 4382, 'n_words': 1003}