oslib/tst/BPNN_27396/mql5/Include/BPNN_MQL_IMPL.mqh
super.admin 07f69c4478 convert
2025-05-30 16:15:18 +02:00

727 lines
23 KiB
MQL5

//+----------------------------------------------------------------------+
//| BPNN_MQL_IMPL.mqh |
//| Copyright (c) 2009-2019, gpwr, Marketeer |
//| https://www.mql5.com/en/users/marketeer |
//| https://www.mql5.com/en/users/gpwr |
//| Based on original idea and source codes of gpwr |
//| rev.18.12.2019 |
//+----------------------------------------------------------------------+
// this let it know to the included BPNN_MQL.mqh that we don't need the import
// because the source is embedded directly (inline)
#ifndef BPNN_LIBRARY
#define BPNN_LIBRARY
#define BPNN_LIBRARY_DESC "\nBPNN MQL library is embedded"
#endif
#include <BPNN_MQL.mqh>
int ValidationPercent = 0;
int TrainValidationMseRatio = 0;
int EpochPercent = 1;
#define min(A,B) MathMin((A),(B))
#define max(A,B) MathMax((A),(B))
// ======================= Multidimensional arrays/matrices =========================
template<typename T>
class ObjectArray
{
public:
ObjectArray(){}
ObjectArray(int n)
{
resize(n);
}
void resize(int n)
{
ArrayResize(data, n);
}
public:
T data[];
};
template<typename T>
class ObjectArray2D
{
public:
ObjectArray2D(){}
ObjectArray2D(int n)
{
resize(n);
}
void resize(int n)
{
ArrayResize(data, n);
for(int i = 0; i < n; i++)
{
data[i] = new ObjectArray<T>();
}
}
ObjectArray<T> *operator[](int i) const
{
return GetPointer(data[i]);
}
~ObjectArray2D()
{
for(int i = 0; i < ArraySize(data); i++)
{
delete data[i];
}
}
private:
ObjectArray<T> *data[];
};
template<typename T>
class ObjectArray3D
{
public:
ObjectArray3D(){}
ObjectArray3D(int n)
{
resize(n);
}
void resize(int n)
{
ArrayResize(data, n);
for(int i = 0; i < n; i++)
{
data[i] = new ObjectArray2D<T>();
}
}
ObjectArray2D<T> *operator[](int i) const
{
return GetPointer(data[i]);
}
~ObjectArray3D()
{
for(int i = 0; i < ArraySize(data); i++)
{
delete data[i];
}
}
private:
ObjectArray2D<T> *data[];
};
// ================================== NN classes & functions ==================================
class NN
{
protected:
// output of each neuron
ObjectArray2D<double> out;
// delta value for each neuron; delta[i][j]*out[i-1][k] = -dE/dw[i][j][k]
ObjectArray2D<double> delta;
// weights for each neuron
ObjectArray3D<double> w;
// update values
ObjectArray3D<double> d;
// gradients in curent epoch
ObjectArray3D<double> g;
// gradient signs in previous epoch
ObjectArray3D<int> gSign;
// no of layers in net including input, hidden and output layers
int numl;
// number of neurons in each layer
int lsize[];
// type of neuron activation function
const int AFT;
// switch to turn activation function in the output layer on/off
const int OAF;
// neuron activation function
double af(double in);
// derivative of activation function
double afDeriv(double t);
// sign function
int sign(double val);
// training parameters
double d0;
double dmin;
double dmax;
double plus;
double minus;
public:
~NN();
// initialize and allocate memory
NN(const int nl, const int &sz[], const int aft, const int oaf, const int uew, const double &extWt[]);
// backpropogate error for one batch of input training sets
NNStatus xprop(ObjectArray2D<double> &in, ObjectArray2D<double> &tgt, const int ntr, int nep, const double maxMSE);
// feedforward activations for one set of inputs
void ffwd(ObjectArray<double> &in);
// return i'th output of the net
double Out(int i) const;
// return weight
double Wt(int i, int j, int k) const;
};
// Initialize and allocate memory on heap ---------------------------------------------------+
NN::NN(const int nl, const int &sz[], const int aft, const int oaf,
const int uew, const double &extWt[]) : AFT(aft), OAF(oaf)
{
// set training parameters
d0 = 0.02; // orig 0.01, opt 0.02
dmin = 0.0;
dmax = 50.0; // orig 50.0
plus = 1.2; // orig 1.2
minus = 0.8; // orig 0.5, opt 0.8-0.85
// set number of layers and their sizes
numl = nl;
ArrayResize(lsize, numl);
for(int i = 0; i <numl; i++) lsize[i] = sz[i];
// allocate memory for output of each neuron
out.resize(numl);
for(int i = 0; i < numl; i++) out[i].resize(lsize[i]);
// allocate memory for deltas
delta.resize(numl);
for(int i = 1; i < numl; i++) delta[i].resize(lsize[i]);
// allocate memory for weights
// w[curr lr #][neuron # in curr lr][input # of curr neuron = neuron # in prev lr]
w.resize(numl);
for(int i = 1; i < numl; i++) w[i].resize(lsize[i]);
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
w[i][j].resize(lsize[i - 1] + 1); // w[][][lsize[]] is bias
// allocate memory for update values
d.resize(numl);
for(int i = 1; i < numl; i++) d[i].resize(lsize[i]);
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
d[i][j].resize(lsize[i - 1] + 1);
// allocate memory for new gradients
g.resize(numl);
for(int i = 1; i < numl; i++) g[i].resize(lsize[i]);
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
g[i][j].resize(lsize[i - 1] + 1);
// allocate memory for old gradient signs
gSign.resize(numl);
for(int i = 1; i < numl; i++) gSign[i].resize(lsize[i]);
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0;j < lsize[i]; j++) // for each neuron in current layer
gSign[i][j].resize(lsize[i - 1] + 1);
// seed and assign random weights (uew=0), or set them equal to external weights (uew=1)
srand((unsigned)(GetTickCount()));
int iw = 0;
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
for(int k = 0; k <= lsize[i - 1]; k++) // for each input of curr neuron incl bias
if(uew == 0) w[i][j].data[k] = (0.6 * (rand()/(double)32767 - 0.5));
else w[i][j].data[k] = extWt[iw++];
// initialize update values to d0 for the first epoch
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
for(int k = 0; k <= lsize[i-1]; k++) // for each input of curr neuron incl bias
d[i][j].data[k] = d0;
// initialize signs of previous gradients to 0 for the first epoch
for(int i = 1; i < numl; i++) // for each layer except input
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
for(int k = 0; k <= lsize[i-1]; k++) // for each input of curr neuron incl bias
gSign[i][j].data[k] = 0;
// Note that the following variables are not used:
//
// delta[0][][]
// w[0][][]
// d[0][][]
// g[0][][]
// gSign[0][][]
//
// to maintain consistancy in layer numbering: for a net having n layers, input layer is
// numbered as 0th layer, first hidden layer as 1st layer, and the output layer as
// (n-1)th layer. First (0th) layer just stores the inputs, hence there is no delta or
// weight values corresponding to it. Its outputs out[0][] are the net inputs.
}
// Free up memory ---------------------------------------------------------------------------+
NN::~NN()
{
}
// Neuron activation function ---------------------------------------------------------------+
double NN::af(double in)
{
if(AFT==1)
{
// tanh
double tmp = 2.0 * in;
if(tmp >= 0)
{
tmp = exp(-tmp);
return ((1.0 - tmp) / (1.0 + tmp));
}
else
{
tmp = exp(tmp);
return ((tmp - 1.0) / (tmp + 1.0));
}
}
else if(AFT == 2)
{
// x/(1+|x|)
return (in / (1.0 + fabs(in)));
}
else
{
// sigmoid
if(in >= 0) return (1.0 / (1.0 + exp(-in)));
else
{
double tmp = exp(in);
return (tmp / (tmp + 1.0));
}
}
}
// Derivative of activation function --------------------------------------------------------+
double NN::afDeriv(double t)
{
if(AFT == 1)
{
// tanh
return 2 * (1 - t * t);
}
else if(AFT == 2)
{
// rational x/(1+|x|)
return (pow(1.0 - fabs(t), 2));
}
else
{
// sigmoid
return (t * (1.0 - t));
}
}
// Sign function ----------------------------------------------------------------------------+
int NN::sign(double val)
{
if (val < 0.0) return -1;
else if(val == 0.0) return 0;
else return 1;
}
// Return i'th output of the net ------------------------------------------------------------+
double NN::Out(int j) const
{
return out[numl - 1].data[j];
}
// Return weight ----------------------------------------------------------------------------+
double NN::Wt(int i, int j, int k) const
{
return w[i][j].data[k];
}
// Feedforward one set of input -------------------------------------------------------------+
void NN::ffwd(ObjectArray<double> &in)
{
// assign input data to the outputs of the 0th layer (i=0)
for(int j = 0; j < lsize[0]; j++) out[0].data[j] = in.data[j];
// compute output of each neuron as a sum of its scaled inputs passed through activation func
for(int i = 1; i < numl; i++) // for each layer except input
{
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
{
double sum = 0.0;
for(int k = 0; k < lsize[i - 1]; k++) // for each input of curr neuron excl bias
sum += out[i - 1].data[k] * w[i][j].data[k]; // apply weights to inputs and add to sum
sum += w[i][j].data[lsize[i - 1]]; // add bias
if(i==numl-1 && OAF==0) out[i].data[j] = sum; // apply activation function to all neurons
else
{
out[i].data[j] = af(sum); // except in the output layer if OAF=0
}
}
}
}
// Compute new weights and MSE --------------------------------------------------------------+
NNStatus NN::xprop(ObjectArray2D<double> &in, ObjectArray2D<double> &tgt, const int ntr, int nep, const double maxMSE)
{
double MSE = 0, prevMSE=1.0e9;
double MSEs, MSEM = 0;
double MSET = 0, minMSET;
double MSEsT, MSEMT = 0;
int ep;
int minMSETep = 0;
ulong dwStart = GetTickCount()/1000; // seconds
int ntr_learn;
string status;
ntr_learn = ntr * (100 - ValidationPercent) / 100; // for example, 5% - left for validation set
minMSET = 1000000;
for(ep = 0; ep < nep; ep++) // for each epoch
{
// compute MSE and gradients using backpropagation of error
MSE = 0.0;
MSEM = 0.0;
for(int s = 0; s < ntr_learn; s++) // for each training set
{
// update output values for each neuron
ffwd(in[s]);
MSEs = 0.0;
// find deltas for each neuron in the output layer i=numl-1
for(int j = 0; j < lsize[numl - 1]; j++) // for each neuron in output layer
{
delta[numl - 1].data[j] = (tgt[s].data[j] - out[numl - 1].data[j]);
MSEs += pow(delta[numl - 1].data[j], 2);
if(OAF == 1)
delta[numl - 1].data[j] *= afDeriv(out[numl - 1].data[j]);
}
MSEs = sqrt(MSEs);
MSE += MSEs;
if(MSEs > MSEM) MSEM = MSEs;
// propagate deltas from output layer to hidden layers
for(int i = numl - 2; i > 0; i--) // for each layer except input & output
{
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
{
double sum = 0.0;
for(int k = 0; k < lsize[i+1]; k++) // for each neuron in later layer
sum += delta[i + 1].data[k] * w[i + 1][k].data[j];
delta[i].data[j] = afDeriv(out[i].data[j]) * sum;
}
}
// compute gradients: dE/dw[i][j][k]=-delta[i][j]*out[i-1][k]
for(int i = 1; i < numl; i++) // for each layer except input
{
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
{
for(int k = 0; k <= lsize[i - 1]; k++) // for each input of curr neuron incl bias
{
// accumulate gradients for all training sets in each epoch
if(s == 0) g[i][j].data[k] = 0.0; // set gradients to 0 at start of each epoch
if(k == lsize[i - 1]) // acc grad's of bias inputs
g[i][j].data[k] -= delta[i].data[j];
else // acc gradients of non-bias inputs
g[i][j].data[k] -= delta[i].data[j] * out[i - 1].data[k];
}
}
}
}
MSE /= ntr_learn;
if(IsStopped())
{
NNStatus result(NN_STATE_ABORTED,
StringFormat("Process aborted at epoch [%u] with max.err. %E, ave.err %E, max.v.err. %E, ave.v. MSE %E",
ep, MSEM, MSE, MSEMT, MSET));
return result;
}
if(ValidationPercent > 0)
{
MSET = 0.0;
MSEMT = 0.0;
// count MSE on test part of training set
for(int s = ntr_learn; s < ntr; s++)
{
// update output values for each neuron
ffwd(in[s]);
MSEsT = 0.0;
// find deltas for each neuron in the output layer i=numl-1
for(int j = 0; j < lsize[numl - 1]; j++) // for each neuron in output layer
{
delta[numl-1].data[j] = tgt[s].data[j] - out[numl-1].data[j];
MSEsT += pow(delta[numl-1].data[j], 2);
if(OAF == 1)
delta[numl - 1].data[j] *= afDeriv(out[numl - 1].data[j]);
}
MSEsT = sqrt(MSEsT);
MSET += MSEsT;
if(MSEsT > MSEMT) MSEMT = MSEsT;
}
MSET /= (ntr - ntr_learn);
}
else // use fictious values to keep training
{
MSET = MSE;
MSEMT = MSEM;
}
if(MSET < minMSET)
{
minMSET = MSET;
minMSETep = ep;
}
else
{
if(ep - minMSETep > nep * EpochPercent / 100 && (MSET > TrainValidationMseRatio * minMSET && TrainValidationMseRatio > 0))
{
NNStatus result(NN_STATE_TRAINED_BY_VALIDATION,
StringFormat("Validation stop after epoch [%u]; max.err. %E, ave.err %E, max.v.err. %E, ave.v. MSE %E",
ep + 1, MSEM, MSE, MSEMT, MSET));
return result;
}
}
if(MSET < maxMSE)
{
NNStatus result(NN_STATE_TRAINED_BY_ACCURACY,
StringFormat("Network trained in [%u] epochs with max.err. %E, ave. MSE %E",
ep + 1, MSEMT, MSET));
return result;
}
// compute new weights in batch mode
for(int i = 1; i < numl; i++) // for each layer except input
{
for(int j = 0; j < lsize[i]; j++) // for each neuron in current layer
{
for(int k = 0; k <= lsize[i-1]; k++) // for each input of current neuron incl bias
{
// batch Rprop
double prod = g[i][j].data[k] * gSign[i][j].data[k];
if(prod > 0.0) // previous weight step reduced error
{
d[i][j].data[k] = min(d[i][j].data[k] * plus, dmax); // increase step
gSign[i][j].data[k] = sign(g[i][j].data[k]);
w[i][j].data[k] -= d[i][j].data[k] * gSign[i][j].data[k];
}
else if(prod < 0.0) // previous weight step increased error
{
if(MSE > prevMSE)
w[i][j].data[k] += d[i][j].data[k] * gSign[i][j].data[k]; // backtrack
d[i][j].data[k] = max(d[i][j].data[k] * minus, dmin); // reduce step
gSign[i][j].data[k] = 0;
}
else // typically only happens in the first epoch
{
gSign[i][j].data[k] = sign(g[i][j].data[k]);
w[i][j].data[k] -= d[i][j].data[k] * gSign[i][j].data[k];
}
}
}
}
prevMSE = MSE;
status = StringFormat("Epoch %u, max. %E, ave. %E, max.t. %E, ave.t. %E",
ep + 1, MSEM, MSE, MSEMT, MSET);
Comment(status);
}
if(ep == nep)
{
NNStatus result(NN_STATE_TRAINED_BY_EPOCH_LIMIT,
StringFormat("Reached the limit of [%u] epochs with max.err. %E, ave.err %E, max.v.err. %E, ave.v. MSE %E",
nep, MSEM, MSE, MSEMT, MSET));
return result;
}
NNStatus result(NN_STATE_UNDEFINED, status);
return result;
}
// =========================================== Train ==========================================
NNStatus Train(
const double &inpTrain[], // Input training data (2D data as 1D array, oldest first)
const double &outTarget[], // Output target data for training (2D data as 1D array, oldest first)
double &outTrain[], // Output 1D array to hold net outputs from training
const int ntr, // # of training sets
const int UEW, // Use External Weights for initialization (1=use extInitWt, 0=use rnd)
const double &extInitWt[], // Input 1D array to hold 3D array of external initial weights
double &trainedWt[], // Output 1D array to hold 3D array of trained weights
const int numLayers, // # of net layers including input, hidden and output
const int &lSz[], // # of neurons in layers. lSz[0] is # of net inputs (nin)
const int AFT, // Type of neuron activation function (0:sigm, 1:tanh, 2:x/(1+x))
const int OAF, // 1 enables activation function for output layer neurons; 0 disables
const int nep, // Max # of training epochs
const double maxMSE // Max MSE; training stops once maxMSE is reached
) export
{
uint first = GetTickCount();
// Prepare input data -----------------------------------------------------------------------+
int nin = lSz[0];
int nout = lSz[numLayers - 1];
// Create a 2D array to hold input training data
ObjectArray2D<double> trainData(ntr);
for(int i = 0; i < ntr; i++) trainData[i].resize(nin);
for(int i = 0; i < ntr; i++)
for(int j = 0; j < nin; j++) trainData[i].data[j] = inpTrain[i * nin + j];
// Create a 2D array to hold output target data used for training
ObjectArray2D<double> targetData(ntr);
for(int i = 0; i < ntr; i++) targetData[i].resize(nout);
for(int i = 0; i < ntr; i++)
for(int j = 0; j < nout; j++) targetData[i].data[j] = outTarget[i * nout + j];
// The input data is arranged as follows:
//
// trainData[i][j] = inpTrain[i*nin+j]
// j= 0...nin-1
// |
// i=0 <inputs>
// ... <inputs>
// i=ntr-1 <inputs>
//
// targetData[i][j] = outTarget[i*nout+j]
// j= 0...nout-1
// |
// i=0 <targets>
// ... <targets>
// i=ntr-1 <targets>
// Create & train NN ------------------------------------------------------------------------+
NN *bp = new NN(numLayers, lSz, AFT, OAF, UEW, extInitWt);
NNStatus result = bp.xprop(trainData, targetData, ntr, nep, maxMSE);
// Save output data -------------------------------------------------------------------------+
for(int i = 0; i < ntr; i++)
{
bp.ffwd(trainData[i]);
for(int j = 0; j < nout; j++) outTrain[i * nout + j] = bp.Out(j);
}
int iw = 0;
for(int i = 1; i < numLayers; i++) // for each layer except input
for(int j = 0; j < lSz[i]; j++) // for each neuron in current layer
for(int k = 0; k <= lSz[i - 1]; k++) // for each input of current neuron including bias
trainedWt[iw++] = bp.Wt(i, j, k);
// The output data is arranged as follows:
//
// outTrain[i*nout+j]
// j= 0...nout-1
// |
// i=0 <outputs>
// ... <outputs>
// i=ntr-1 <outputs>
// Free up memory ---------------------------------------------------------------------------+
delete bp;
Print("Training finished in " + (string)((GetTickCount() - first) / 1000) + " seconds");
return result;
}
// =========================================== Test ===========================================
void Test(
const double &inpTest[], // Input test data (2D data as 1D array, oldest first)
double &outTest[], // Net outputs from testing
const int ntt, // # of test sets
const double &extInitWt[],// Input 1D array to hold 3D array of external initial weights
const int numLayers, // # of net layers including input, hidden and output
const int &lSz[], // # of neurons in layers. lSz[0] is # of net inputs (nin)
const int AFT, // Type of neuron activation function (0:sigm, 1:tanh, 2:x/(1+x))
const int OAF // 1 enables activation function for output layer neurons; 0 disables
) export
{
// Prepare input data -----------------------------------------------------------------------+
int nin = lSz[0];
int nout = lSz[numLayers - 1];
ObjectArray2D<double> testData(ntt);
for(int i = 0; i < ntt; i++) testData[i].resize(nin);
for(int i = 0; i < ntt; i++)
{
for(int j = 0; j < nin; j++)
{
testData[i].data[j] = inpTest[i * nin + j];
}
}
// The input data is arranged as follows:
//
// testData[i][j] = inpTest[i*nin+j]
// j= 0...nin-1
// |
// i=0 <inputs>
// ... <inputs>
// i=ntt-1 <inputs>
//
// <inputs> start with the oldest value first
// Create & test NN -------------------------------------------------------------------------+
NN *bp = new NN(numLayers, lSz, AFT, OAF, 1, extInitWt);
for(int i = 0; i < ntt; i++)
{
bp.ffwd(testData[i]);
for(int j = 0; j < nout; j++)
{
outTest[i * nout + j] = bp.Out(j);
}
}
// The output data is arranged as follows:
//
// outTest[i*nout+j]
// j= 0...nout-1
// |
// i=0 <outputs>
// ... <outputs>
// i=ntt-1 <outputs>
// Free up memory ---------------------------------------------------------------------------+
delete bp;
}
void ValidationSet(int Percent, int MseRatio, int BadEpochCountPercent) export
{
ValidationPercent = Percent;
TrainValidationMseRatio = MseRatio;
EpochPercent = BadEpochCountPercent;
}