//+------------------------------------------------------------------+ //| NeuralNet.mqh | //| Copyright 2025, Google Gemini | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Google Gemini" #property link "https://www.google.com" #property strict #include //+------------------------------------------------------------------+ //| Neuron Connection Structure | //+------------------------------------------------------------------+ struct SConnection { double weight; double deltaWeight; }; //+------------------------------------------------------------------+ //| Neuron Class | //+------------------------------------------------------------------+ class CNeuron : public CObject { public: double m_outputVal; double m_gradient; SConnection m_outputWeights[]; int m_myIndex; CNeuron(int numOutputs, int myIndex); ~CNeuron() {} void SetOutputVal(double val) { m_outputVal = val; } double GetOutputVal() const { return m_outputVal; } void FeedForward(CArrayObj *prevLayer); void CalcOutputGradients(double targetVal); void CalcHiddenGradients(CArrayObj *nextLayer); void UpdateInputWeights(CArrayObj *prevLayer); // Serialization void SaveWeights(int fileHandle); void LoadWeights(int fileHandle, int numOutputs); void Jitter(double magnitude); private: static double TransferFunction(double x); static double TransferFunctionDerivative(double x); public: // Hyperparameters (per-neuron, managed by CNeuralNet) double m_eta; double m_alpha; double m_lambda; }; //--- Constructor CNeuron::CNeuron(int numOutputs, int myIndex) { ArrayResize(m_outputWeights, numOutputs); m_myIndex = myIndex; m_eta = 0.15; m_alpha = 0.5; m_lambda = 0.001; for(int c = 0; c < numOutputs; ++c) { m_outputWeights[c].weight = (MathRand() / 32767.0) - 0.5; // Random -0.5 .. 0.5 m_outputWeights[c].deltaWeight = 0; } } //--- Activation Function: Tanh double CNeuron::TransferFunction(double x) { // Tanh - output range [-1.0..1.0] return tanh(x); } //--- Derivative for Backprop double CNeuron::TransferFunctionDerivative(double x) { // tanh derivative approximation: 1.0 - x*x return 1.0 - x * x; } //--- Feed Forward void CNeuron::FeedForward(CArrayObj *prevLayer) { double sum = 0.0; // Sum the previous layer's outputs (inputs to this neuron) // Include logic for bias neuron if needed (usually last neuron in layer) for(int n = 0; n < prevLayer.Total(); ++n) { CNeuron *neuron = (CNeuron*)prevLayer.At(n); sum += neuron.GetOutputVal() * neuron.m_outputWeights[m_myIndex].weight; } m_outputVal = TransferFunction(sum); } //--- Calculate Gradients for Output Layer void CNeuron::CalcOutputGradients(double targetVal) { double delta = targetVal - m_outputVal; m_gradient = delta * TransferFunctionDerivative(m_outputVal); } //--- Calculate Gradients for Hidden Layers void CNeuron::CalcHiddenGradients(CArrayObj *nextLayer) { double dow = 0.0; // Sum of derivative of weights // Loop through neurons in next layer (excluding bias) for(int n = 0; n < nextLayer.Total() - 1; ++n) { CNeuron *neuron = (CNeuron*)nextLayer.At(n); dow += m_outputWeights[n].weight * neuron.m_gradient; } m_gradient = dow * TransferFunctionDerivative(m_outputVal); } //--- Update Weights void CNeuron::UpdateInputWeights(CArrayObj *prevLayer) { // The inputs to THIS neuron are the outputs of the PREVIOUS layer for(int n = 0; n < prevLayer.Total(); ++n) { CNeuron *neuron = (CNeuron*)prevLayer.At(n); double oldDeltaWeight = neuron.m_outputWeights[m_myIndex].deltaWeight; double newDeltaWeight = // Individual input, magnified by the gradient and train rate m_eta * neuron.GetOutputVal() * m_gradient // Also add momentum = a fraction of the previous delta weight + m_alpha * oldDeltaWeight; // Apply L2 Regularization (Weight Decay) // The weight itself is decayed by a factor of (1 - lambda) before the newDeltaWeight is added. neuron.m_outputWeights[m_myIndex].deltaWeight = newDeltaWeight; neuron.m_outputWeights[m_myIndex].weight = (neuron.m_outputWeights[m_myIndex].weight * (1.0 - m_lambda)) + newDeltaWeight; } } //--- Save Weights void CNeuron::SaveWeights(int fileHandle) { // Save all output weights from this neuron for(int c = 0; c < ArraySize(m_outputWeights); ++c) { FileWriteDouble(fileHandle, m_outputWeights[c].weight); } } //--- Load Weights void CNeuron::LoadWeights(int fileHandle, int numOutputs) { ArrayResize(m_outputWeights, numOutputs); for(int c = 0; c < numOutputs; ++c) { m_outputWeights[c].weight = FileReadDouble(fileHandle); m_outputWeights[c].deltaWeight = 0; } } //--- Add noise to weights void CNeuron::Jitter(double magnitude) { for(int c = 0; c < ArraySize(m_outputWeights); ++c) { m_outputWeights[c].weight += (MathRand() / 32767.0 - 0.5) * magnitude; } } //+------------------------------------------------------------------+ //| Neural Network Class | //+------------------------------------------------------------------+ class CNeuralNet { private: // m_layers[layerNum][neuronNum] // We use ArrayObj to hold Neurons for each layer CArrayObj *m_layers[]; double m_error; double m_recentAverageError; double m_recentAverageSmoothingFactor; public: CNeuralNet(const int &topology[]); ~CNeuralNet(); void FeedForward(const double &inputVals[]); void BackProp(const double &targetVals[]); void GetResults(double &resultVals[]) const; double GetRecentAverageError(void) const { return m_recentAverageError; } bool Save(string fileName, int flags=0); bool Save(int handle); bool Load(string fileName, const int &topology[], int flags=0); bool Load(int handle, const int &topology[]); // Hyperparameters double eta; // Learning Rate double alpha; // Momentum double lambda; // L2 Regularization void SetLearningRate(double rate); void SetAlpha(double a); void SetLambda(double l); void Jitter(double magnitude); }; //--- Constructor CNeuralNet::CNeuralNet(const int &topology[]) { int numLayers = ArraySize(topology); ArrayResize(m_layers, numLayers); m_recentAverageSmoothingFactor = 100.0; // simple moving average over approx 100 samples eta = 0.15; alpha = 0.5; lambda = 0.001; for(int layerNum = 0; layerNum < numLayers; ++layerNum) { // Create new layer m_layers[layerNum] = new CArrayObj(); // Number of outputs for neurons in this layer // is the number of neurons in the next layer // (Exception: Output layer has 0 outputs) int numOutputs = (layerNum == numLayers - 1) ? 0 : topology[layerNum + 1]; // We add a bias neuron to every layer -> topology[layerNum] + 1 // For Output layer, bias neuron is technically not needed but harmless, // however usually we iterate <= Total() - 1 to ignore bias output. // Let's add <= topology[layerNum] for neurons, plus one for BIAS for(int neuronNum = 0; neuronNum <= topology[layerNum]; ++neuronNum) { CNeuron *neuron = new CNeuron(numOutputs, neuronNum); m_layers[layerNum].Add(neuron); // Force bias node's output to be 1.0 (it's the last one) if(neuronNum == topology[layerNum]) neuron.SetOutputVal(1.0); } } // Propagate initial hyperparameters to all neurons SetLearningRate(eta); SetAlpha(alpha); SetLambda(lambda); } //--- Destructor CNeuralNet::~CNeuralNet() { for(int i = 0; i < ArraySize(m_layers); ++i) { if(CheckPointer(m_layers[i]) != POINTER_INVALID) { delete m_layers[i]; } } } //--- Feed Forward void CNeuralNet::FeedForward(const double &inputVals[]) { // Check inputs match input layer size (minus bias) CArrayObj *inputLayer = m_layers[0]; if(ArraySize(inputVals) != inputLayer.Total() - 1) { Print("Error: NeuralNet Input Input size mismatch."); return; } // Assign (latch) the input values into the input neurons for(int i = 0; i < ArraySize(inputVals); ++i) { CNeuron *n = (CNeuron*)inputLayer.At(i); n.SetOutputVal(inputVals[i]); } // Forward propagate for(int layerNum = 1; layerNum < ArraySize(m_layers); ++layerNum) { CArrayObj *prevLayer = m_layers[layerNum - 1]; CArrayObj *currLayer = m_layers[layerNum]; // For each neuron in current layer (excluding bias) for(int n = 0; n < currLayer.Total() - 1; ++n) { CNeuron *neuron = (CNeuron*)currLayer.At(n); neuron.FeedForward(prevLayer); } } } //--- Back Propagation void CNeuralNet::BackProp(const double &targetVals[]) { // Calculate overall net error (RMS of output neuron errors) CArrayObj *outputLayer = m_layers[ArraySize(m_layers) - 1]; m_error = 0.0; for(int n = 0; n < outputLayer.Total() - 1; ++n) { CNeuron *neuron = (CNeuron*)outputLayer.At(n); double delta = targetVals[n] - neuron.GetOutputVal(); m_error += delta * delta; } m_error /= (outputLayer.Total() - 1); // Get average error squared m_error = sqrt(m_error); // RMS // Implement a recent average measurement m_recentAverageError = (m_recentAverageError * m_recentAverageSmoothingFactor + m_error) / (m_recentAverageSmoothingFactor + 1.0); // Calculate Output Layer Gradients for(int n = 0; n < outputLayer.Total() - 1; ++n) { CNeuron *neuron = (CNeuron*)outputLayer.At(n); neuron.CalcOutputGradients(targetVals[n]); } // Calculate Gradients on Hidden Layers for(int layerNum = ArraySize(m_layers) - 2; layerNum > 0; --layerNum) { CArrayObj *hiddenLayer = m_layers[layerNum]; CArrayObj *nextLayer = m_layers[layerNum + 1]; for(int n = 0; n < hiddenLayer.Total(); ++n) { CNeuron *neuron = (CNeuron*)hiddenLayer.At(n); neuron.CalcHiddenGradients(nextLayer); } } // For all layers from outputs to first hidden layer, // update connection weights for(int layerNum = ArraySize(m_layers) - 1; layerNum > 0; --layerNum) { CArrayObj *layer = m_layers[layerNum]; CArrayObj *prevLayer = m_layers[layerNum - 1]; for(int n = 0; n < layer.Total() - 1; ++n) { CNeuron *neuron = (CNeuron*)layer.At(n); neuron.UpdateInputWeights(prevLayer); } } } //--- Get Results void CNeuralNet::GetResults(double &resultVals[]) const { CArrayObj *outputLayer = m_layers[ArraySize(m_layers) - 1]; ArrayResize(resultVals, outputLayer.Total() - 1); for(int n = 0; n < outputLayer.Total() - 1; ++n) { CNeuron *neuron = (CNeuron*)outputLayer.At(n); resultVals[n] = neuron.GetOutputVal(); } } //--- Save to file (Filename) bool CNeuralNet::Save(string fileName, int flags=0) { int handle = FileOpen(fileName, FILE_WRITE|FILE_BIN|flags); if(handle == INVALID_HANDLE) { Print("Failed to save NeuralNet to ", fileName); return false; } bool res = Save(handle); FileClose(handle); return res; } //--- Save to file (Handle) bool CNeuralNet::Save(int handle) { if(handle == INVALID_HANDLE) return false; for(int layerNum = 0; layerNum < ArraySize(m_layers) - 1; ++layerNum) { CArrayObj *layer = m_layers[layerNum]; for(int n = 0; n < layer.Total(); ++n) { CNeuron *neuron = (CNeuron*)layer.At(n); neuron.SaveWeights(handle); } } return true; } //--- Load from file (Filename) bool CNeuralNet::Load(string fileName, const int &topology[], int flags=0) { if(ArraySize(topology) != ArraySize(m_layers)) { PrintFormat("Error: Topology size mismatch. Expected %d layers, but Topology has %d layers.", ArraySize(m_layers), ArraySize(topology)); return false; } int handle = FileOpen(fileName, FILE_READ|FILE_BIN|flags); if(handle == INVALID_HANDLE) { PrintFormat("Error: Failed to open file '%s'. Error Code: %d", fileName, GetLastError()); return false; } bool res = Load(handle, topology); FileClose(handle); return res; } //--- Load from file (Handle) bool CNeuralNet::Load(int handle, const int &topology[]) { if(handle == INVALID_HANDLE) return false; for(int layerNum = 0; layerNum < ArraySize(m_layers) - 1; ++layerNum) { CArrayObj *layer = m_layers[layerNum]; // Note: Use topology[layerNum+1] for numOutputs for(int n = 0; n < layer.Total(); ++n) { CNeuron *neuron = (CNeuron*)layer.At(n); neuron.LoadWeights(handle, topology[layerNum+1]); } } return true; } //--- Set Learning Rate for all neurons void CNeuralNet::SetLearningRate(double rate) { eta = rate; for(int i=0; i