Red Neuronal: EA autooptimizable

Jose Miguel Soriano | 15 septiembre, 2016

Introducción

Una vez decidida la estrategia e implementada en un EA, al tráder se le presentan dos problemas que invalidan la estrategia si no son resueltos:

Evidentemente, hay parámetros que pueden ser fijados con carácter previo (par de trabajo, marco temporal...) y otros que serán variables y que son, realmente, el problema: periodo de cálculo de los indicadores, niveles de compra/venta, niveles TP/SL...

¿Podríamos diseñar un EA que periódicamente (según le dejáramos instruido) autooptimizara los criterios de apertura o cierre de posición?

¿Qué pasaría si implementamos en el EA una red neuronal (NN) del tipo perceptrón multicapa (MCP) que sea el módulo que analice el historial y evalúe la estrategia? Podríamos decirle al código: "optimiza cada semana (o cada día o cada hora) la red neuronal y continúa tu trabajo". ¡De esta forma, tendríamos un EA autooptimizable!

No es la primera vez que se trata "MQL y Red neuronal" en este foro (artículo 497, artículo 1565, artículo 1165 artículo 830, artículo 252) pero el tratamiento que se da implica, o bien usar la información que facilita una NN exterior al EA (y que hay que "suministrarle" incluso manualmente), o bien la NN es optimizada por el tráder (en un ejercicio de "entrenamiento no supervisado") con el optimizador de MT5/MT4 y, en definitiva, hemos reemplazado los parámetros de entrada del EA habituales por los de entrada a la red ( artículo 497).

Es este artículo no describiremos un robot comercial. Vamos a diseñar e implementar modularmente una maqueta de EA que, utilizando un perceptrón multicapa (MCP implementado en mql5 (librería ALGLIB), resuelva el algoritmo expresado en los párrafos anteriores. Posteriormente plantearemos dos problemas matemáticos, cuyo resultado es fácilmente comprobable desde otro algoritmo, a la Red Neuronal; ello nos permitirá analizar la respuesta y los problemas que plantea un MCP según su estructura interna y adquirir criterio para luego implementar el EA comercial con sólo cambiar el módulo de entrada de datos. La maqueta deberá resolver la autooptimización.

Consideramos conocida por el lector la teoría general sobre redes neuronales: estructura y formas de organización, número de capas, número de neuronas en cada una de las capas, enlaces y pesos, etc. En todo caso, en los artículos referidos podrá informarse al respecto.


1. Algoritmo base

  1. Creación de la red neuronal.
  2. Preparación de los datos de entrada (y de los respectivos datos de salida) mediante la carga en una matriz de datos.
  3. Normalización de los datos dentro de un rango determinado (normalmente [0, 1] o [-1, 1]).
  4. Entrenamiento y optimización de la red.
  5. Cálculo y uso de la predicción de la red según la estrategia del EA.
  6. Autooptimización: vuelta al punto 2 y reiteración del proceso al incluirlo en la función OnTimer().

Según el algoritmo descrito, el robot ejecutará la optimización de la red periódicamente, según un intervalo temporal definido por el usuario. El usuario no tendrá que preocuparse de esta faceta. El paso 5 no se incluirá en el proceso reiterativo; el EA siempre tendrá a su disposición los valores predictivos del EA aun cuando este se encuentre en proceso de optimización: veremos cómo.

 

2. La librería ALGLIB

Esta librería fue publicada y comentada en un artículo de su autor Sergey Bochkanov y el sitio web del proyecto ALGLIB, http://www.alglib.net/, donde se describe como un cruce entre plataforma de análisis numérico y una librería de procesamiento de datos. Es compatible con varios lenguajes de programación (C++, C#, Pascal, VBA) y varios sistemas operativos (Windows, Linux, Solaris). Características incluidas en ALGLIB:

  • Álgebra lineal (direct algorithms, EVD/SVD)
  • Solucionadores (lineal y no lineal)
  • Interpolación
  • Optimización
  • Transformadas rápidas de Fourier
  • Integración numérica
  • Mínimos cuadrados lineales y no lineales adecuados
  • Ecuaciones diferenciales ordinarias
  • Funciones especiales
  • Estadísticas (estadística descriptiva, pruebas de hipótesis)
  • Análisis de los datos (clasificación / regresión, incluyendo redes neuronales)
  • Múltiples versiones de precisión de álgebra lineal, optimización de la interpolación y otros algoritmos (utilizando MPFR para los cálculos de punto flotante)

Las funciones estáticas de la clase CAlglib deben utilizarse para trabajar con la librería. Todas las funciones de la librería se han trasladado a la clase del sistema CAlglib como funciones estáticas.

Contiene los scripts de prueba testclasses.mq5 y testinterfaces.mq5 junto con un simple script de demostración, usealglib.mq5. Los archivos incluidos del mismo nombre (testclasses.mqh y testinterfaces.mqh) se utilizan para poner en marcha los casos de prueba. Deben ser colocados en \MQL5\Scripts\Alglib\Testcases\.

De los numerosos ficheros y centenares de funciones que incluye, a los efectos de este artículo nos van a interesar principal, pero no exclusivamente, los ficheros:

Paquetes 
Descripción 
alglib.mqh
El paquete principal de la biblioteca incluye funciones personalizadas. Estas funciones deben ser llamadas para trabajar con la librería.
dataanalysis.mqhClases de análisis de datos:
  1. CMLPBase - perceptrón multicapa (redes neuronales).
  2. CMLPTrain - formación del perceptrón multicapa.
  3. CMLPE - conjuntos de redes neuronales.

Al descargar la librería, la ruta natural de instalación llevará los ficheros a "MQL5\Include\Math\Alglib\". Para su uso en el código de nuestro programa nos bastará con la instrucción...

#include <Math\Alglib\alglib.mqh>

Y de todas las funciones que contienen esos dos ficheros, aplicaremos a la solución que proponemos las funciones de la clase CAlglib...

//--- create neural networks
static void    MLPCreate0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateR0(const int nin,const int nout,double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR1(const int nin,int nhid,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR2(const int nin,const int nhid1,const int nhid2,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateC0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network)

Las funciones "MLPCreate" crearán una red neuronal con salida lineal. Este tipo de red será el que crearemos en los ejemplos de este artículo.

Las funciones "MLPCreateR" crearán una red neuronal con salida dentro del intervalo [a, b].

Las funciones "MLPCreateC" crearán una red neuronal con salida clasificada por "clases" (0 ó 1; -1, 0 ó 1, por ejemplo). 

//--- Properties and error of the neural network
static void    MLPProperties(CMultilayerPerceptronShell &network,int &nin,int &nout,int &wcount);
static int     MLPGetLayersCount(CMultilayerPerceptronShell &network);
static int     MLPGetLayerSize(CMultilayerPerceptronShell &network,const int k);
static void    MLPGetInputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetOutputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int &fkind,double &threshold);
static double  MLPGetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1);
static void    MLPSetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int fkind,double threshold);
static void    MLPSetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1,const double w);
static void    MLPActivationFunction(const double net,const int k,double &f,double &df,double &d2f);
static void    MLPProcess(CMultilayerPerceptronShell &network,double &x[],double &y[]);
static double  MLPError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int ssize);
static double  MLPRMSError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints);

//--- training neural networks
static void    MLPTrainLM(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,int &info,CMLPReportShell &rep);
static void    MLPTrainLBFGS(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,const double wstep,int maxits,int &info,CMLPReportShell &rep);

Estas funciones permiten crear y optimizar Redes Neuronales de entre dos y cuatro capas (capa de entrada, de salida y cero, una o dos capas ocultas). Los parámetros principales de entrada se definen casi por su nombre:

  • nin: número de neuronas de la capa de entrada.
  • nout: ídem capa de salida.
  • nhid1: ídem capa oculta 1.
  • nhid2: ídem capa oculta 2.
  • network: objeto de la clase CMultilayerPerceptronShell que contendrá la definición de los enlaces y pesos entre las neuronas y las funciones de activación.
  • xy: objeto de la clase CMatrixDouble que contendrá los datos de entrada/salida para acometer el entrenamiento y optimización de la red neuronal.

El entrenamiento / optimización será efectuado mediante el algoritmo de Levenberg-Marquardt (MLPTrainLM()) o el de L-BFGS con regularización (MLPTrainLBFGS()). Este último lo usaremos si la red tiene más de 500 enlaces /pesos; la información de la función advierte "para redes con varios cientos de pesos"). Son algoritmos mucho más eficaces que el conocido como "de retropropagación", normalmente usado en NN. La librería ofrece más funciones de optimización; el lector puede indagar si lo que necesita no es resuelto por las dos citadas.

 

3. Implementación en MQL

Definimos las variables que contienen el número de neuronas de cada capa como parámetros de entrada externos. También las que contienen la definición de los intervalos de normalización opcionales.

input int nNeuronEntra= 35;      //Núm neuronas capa entrada
input int nNeuronSal= 1;         //Núm neuronas capa salida
input int nNeuronCapa1= 45;      //Núm neuronas capa oculta 1 (<1 no existe)
input int nNeuronCapa2= 10;      //Núm neuronas capa oculta 2 (<1 no existe)
input string intervEntrada= "0;1";        //Normalización entrada: mín y máx deseados (vacío= NO normaliza)
input string intervSalida= "";            //Normalización salida: mín y máx deseados (vacío= NO normaliza)

Otras variables externas serán...

input int velaIniDesc= 15;
input int historialEntrena= 1500;

Que nos permitirán especificar el número de barra (velaIniDesc) desde la que iniciar la descarga de los datos históricos para entrenar la red y la cantidad total de datos de la barra a descargar (historialEntrena).

Como variables públicas globales definimos el objeto de red y el objeto array doble "arDatosAprende". 
CMultilayerPerceptronShell *objRed;
CMatrixDouble arDatosAprende(0, 0);

"arDatosAprende" contendrá las filas de datos entrada/salida para el entrenamiento de la red. Es una matriz doble dinámica en las dos dimensiones (sabido es que mql5 sólo permite la definición estándar de arrays dinámicos en la dimensión 1 y estáticos en el resto). 

Los puntos del 1 al 4 del algoritmo base son implementados en la función "gestionRed()"...

//---------------------------------- CREA y OPTIMIZA LA RED NEURONAL --------------------------------------------------
bool gestionRed(CMultilayerPerceptronShell &objRed, string simb, bool normEntrada= true , bool normSalida= true,
                bool imprDatos= true, bool barajar= true)
{
   double tasaAprende= 0.001;             //Tasa de aprendizaje red
   int ciclosEntren= 2;                   //ciclos de entrenamiento
   ResetLastError();
   bool creada= creaRedNeuronal(objRed);				//crea NN
   if(creada) 
   {
      preparaDatosEntra(objRed, simb, arDatosAprende);			//carga datos entrada/salida en arDatosAprende
      if(imprDatos) imprimeDatosEntra(simb, arDatosAprende);		//imprimir los datos para evaluar su veracidad
      if(normEntrada || normSalida) normalizaDatosRed(objRed, arDatosAprende, normEntrada, normSalida);	//normalización opcional datos entrada/salida
      if(barajar) barajaDatosEntra(arDatosAprende, nNeuronEntra+nNeuronSal);	//barajamos filas de matriz de datos
      errorMedioEntren= entrenaEvalRed(objRed, arDatosAprende, ciclosEntren, tasaAprende);	//ejecutamos entrenamiento / optimización
      salvaRedFich(arObjRed[codS], "copiaSegurRed_"+simb);	//salvamos a fichero de disco la red
   }
   else infoError(GetLastError(), __FUNCTION__);
   
   return(_LastError==0);
}

función en la que creamos la NN (creaRedNeuronal(objRed)); cargamos los datos en "arDatosAprende" con la función preparaDatosEntra(); podremos imprimir los datos para evaluar su veracidad con la función imprimeDatosEntra(); si los datos de entrada y salida deben ser normalizados, lo serán con la función normalizaDatosRed(); igualmente, si queremos barajar las filas de la matriz de datos antes de optimizar, ejecutamos barajaDatosEntra(); ejecutamos el entrenamiento con entrenaEvalRed(), que nos devuelve el error de optimización obtenido; para terminar, guardamos en el disco la red para su recuperación opcional sin tener que crear y reoptimizar otra vez.

En el inicio de la función gestionRed() existen dos variables (tasaAprende y ciclosEntrena) que definen el coeficiente de aprendizaje y los ciclos de entrenamiento de la NN. AlgLib advierte que se suelen usar en los valores que refleja la función, pero es que en las numerosas pruebas que se han hecho con los dos algoritmos de optimización que se proponen el ajuste es tal que los resultados apenas cambian modificando los valores de esas variables. En principio esas dos variables se codificaron como parámetros de entrada pero dada su escasa repercusión se han dispuesto en el interior de la función. El lector decidirá si las convierte en parámetro de entrada.

Sólo se debe aplicar la función normalizaDatosRed(), para normalizar las entradas de entrenamiento a la NN dentro de un rango, si los datos reales de que vamos a disponer en el futuro, para pedir a la NN una predicción, van a estar dentro de ese rango. Si no es así es erróneo normalizar. También se debe tener la precaución de normalizar los datos reales, si los de entrenamiento fueron normalizados, antes de pedir una predicción.

3.1 Creación de la Red Neuronal (NN) 

//--------------------------------- CREA RED NEURONAL --------------------------------------
bool creaRedNeuronal(CMultilayerPerceptronShell &objRed)
{
   bool creada= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   if(nNeuronCapa1<1 && nNeuronCapa2<1) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);   	//SALIDA LINEAL   
   else if(nNeuronCapa2<1) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);   	//SALIDA LINEAL
   else CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);   		//SALIDA LINEAL                    
   creada= existeRed(objRed);
   if(!creada) Print("Error creación RED NEURONAL ==> ", __FUNCTION__, " ", _LastError);
   else
   {
      CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
      Print("Creada Red nº capas ", propiedadRed(objRed, N_CAPAS));
      Print("Nº neuronas entrada ", nEntradas);
      Print("Nº neuronas capaOculta 1 ", nNeuronCapa1);
      Print("Nº neuronas capaOculta 2 ", nNeuronCapa2);
      Print("Nº neuronas salida ", nSalidas);
      Print("Nº pesos ", nPesos);
   }
   return(creada);
}

La función que vemos arriba crea la NN según el número de capas y de neuronas que deseamos (nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal); comprueba que la red ha sido creada correctamente con la función...

//--------------------------------- EXISTE RED --------------------------------------------
bool existeRed(CMultilayerPerceptronShell &objRed)
{
   bool resp= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
   resp= nEntradas>0 && nSalidas>0;
   return(resp);
}

Y si es correcta la creación de la red, informará al usuario de las propiedades con las que ha sido creada a partir de la función MLPProperties() de la clase CAlglib que contiene la librería AlgLib.

Como ya se dijo en apartado "B", ALGLIB tiene otras funciones que permiten crear NN con salida por clases o dentro de un rango.

Una vez creada la NN, para recuperar alguna de sus propiedades en el resto del EA, podemos definir la función "propiedadRed()"...

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};

//---------------------------------- PROPIEDADES de la RED  -------------------------------------------
int propiedadRed(CMultilayerPerceptronShell &objRed, mis_PROPIEDADES_RED prop= N_CAPAS, int numCapa= 0)
{           //si se pide N_NEURONAS hay que especificar el numCapa
   int resp= 0, numEntras= 0, numSals= 0, numPesos= 0;
   if(prop>N_NEURONAS) CAlglib::MLPProperties(objRed, numEntras, numSals, numPesos);    
   switch(prop)
   {
      case N_CAPAS:
         resp= CAlglib::MLPGetLayersCount(objRed);
         break;
      case N_NEURONAS:
         resp= CAlglib::MLPGetLayerSize(objRed, numCapa);
         break;
      case N_ENTRADAS:
         resp= numEntras;
         break;
      case N_SALIDAS:
         resp= numSals;
         break;
      case N_PESOS:
         resp= numPesos;
   }
   return(resp);
}   

3.2  Preparación de datos de entrada/salida 

La función que se propone puede variar en función de qué datos de entrada se usen y como se delimite su número

//---------------------------------- PREPARA DATOS ENTRADA / SALIDA --------------------------------------------------
void preparaDatosEntra(CMultilayerPerceptronShell &objRed, string simb, CMatrixDouble &arDatos, bool normEntrada= true , bool normSalida= true)
{
   int fin= 0, fila= 0, colum= 0,
       nEntras= propiedadRed(objRed, N_ENTRADAS),
       nSals= propiedadRed(objRed, N_SALIDAS);
   double valor= 0, arResp[];   
   arDatos.Resize(historialEntrena, nEntras+nSals);
   fin= velaIniDesc+historialEntrena;
   for(fila= velaIniDesc; fila<fin; fila++)
   {                   
      for(colum= 0; colum<NUM_INDIC;  colum++)
      {
         valor= valorIndic(codS, fila, colum);
         arDatos[fila-1].Set(colum, valor);
      }
      calcEstrat(fila-nVelasPredic, arResp);
      for(colum= 0; colum<nSals; colum++) arDatos[fila-1].Set(colum+nEntras, arResp[colum]);
   }
   return;
}

En el procedimiento descrito, recorremos el historial desde "velaIniDesc" hasta "velaIniDesc+historialEntrena", y en cada barra obtenemos el valor de cada uno de los indicadores usados en la estrategia (NUM_INDIC) para cargarlo en la columna respectiva del array doble arDatos, de la clase CMatrixDouble. Incorporamos también para cada barra el resultado de la estrategia ("calcEstrat()"), que se corresponde con los valores de los indicadores referidos. La variable "nVelasPredic" nos permite relacionar esos valores de indicadores con el resultado de la estrategia n velas posterior; normalmente, "nVelasPredic", será definida como parámetro externo.

Es decir, en cada fila de la matriz "arDatos" de la clase CMatrixDouble tendremos tantas columnas como datos de entrada o valores de indicador usamos en la estrategia y tantas columnas como datos de salida define nuestra estrategia. "arDatos" tendrá tantas filas como valor tenga "historialEntrena".

3.3 Impresión de la matriz de datos entrada/salida

Si queremos imprimir el contenido de la matriz doble para comprobar la exactitud de los datos entrada/salida podemos usar la función "imprimeDatosEntra()"...

//---------------------------------- IMPRIME DATOS ENTRADA / SALIDA --------------------------------------------------
void imprimeDatosEntra(string simb, CMatrixDouble &arDatos)
{
   string encabeza= "indic1;indic2;indic3...;resultEstrat",     //nombres de los indicadores separados por ";"
          fichImprime= "dataEntrenaRed_"+simb+".csv";
   bool entrar= false, copiado= false;
   int fila= 0, colum= 0, resultEstrat= -1, nBuff= 0,
       nFilas= arDatos.Size(),
       nColum= nNeuronEntra+nNeuronSal,
       puntFich= FileOpen(fichImprime, FILE_WRITE|FILE_CSV|FILE_COMMON);
   FileWrite(puntFich, encabeza);
   for(fila= 0; fila<nFilas; fila++)
   {
      linea= IntegerToString(fila)+";"+TimeToString(iTime(simb, PERIOD_CURRENT, velaIniDesc+fila), TIME_MINUTES)+";";                
      for(colum= 0; colum<nColum;  colum++) 
         linea= linea+DoubleToString(arDatos[fila][colum], 8)+(colum<(nColum-1)? ";": "");
      FileWrite(puntFich, linea);
   }
   FileFlush(puntFich);
   FileClose(puntFich);
   Alert("Download file= ", fichImprime);
   Alert("Path= ", TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files");
   return;
}

Que recorre el array fila a fila creando en cada paso una cadena "fila" con todos los valores de las columnas de la fila separados por ";" y la imprime en el fichero .csv creado al efecto con la función FileOpen(). Es una función secundaria para el tema de este artículo y no la comentamos; el fichero .csv creado puede ser leído con Excell para labores de comprobación.

3.4  Normalización de los datos dentro de un intervalo

Normalmente, antes de realizar optimización de la red será conveniente que los datos de entrada queden englobados entre los extremos de un intervalo, es decir, normalizados. Proponemos para ello la siguiente función que normalizará, opcionalmente, los datos de entrada y/o de salida contenidos en el array "arDatos" de la clase CMatrixDouble

//------------------------------------ NORMALIZA ENTRADA/SALIDA RED -------------------------------------
void normalizaDatosRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatos, bool normEntrada= true, bool normSalida= true)
{
   int fila= 0, colum= 0, maxFila= arDatos.Size(),
       nEntradas= propiedadRed(objRed, N_ENTRADAS),
       nSalidas= propiedadRed(objRed, N_SALIDAS);
   double maxAbs= 0, minAbs= 0, maxRel= 0, minRel= 0, arMaxMinRelEntra[], arMaxMinRelSals[];
   ushort valCaract= StringGetCharacter(";", 0);
   if(normEntrada) StringSplit(intervEntrada, valCaract, arMaxMinRelEntra);
   if(normSalida) StringSplit(intervSalida, valCaract, arMaxMinRelSals);
   for(colum= 0; normEntrada && colum<nEntradas; colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelEntra[0]);
      maxRel= StringToDouble(arMaxMinRelEntra[1]); 
      for(fila= 0; fila<maxFila; fila++)                //identificamos maxAbs y minAbs de cada columna de datos
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      for(fila= 0; fila<maxFila; fila++)                //establecemos el nuevo valor normalizado
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   for(colum= nEntradas; normSalida && colum<(nEntradas+nSalidas); colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelSals[0]);
      maxRel= StringToDouble(arMaxMinRelSals[1]);
      for(fila= 0; fila<maxFila; fila++)
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      minAbsSalida= minAbs;
      maxAbsSalida= maxAbs;
      for(fila= 0; fila<maxFila; fila++)
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   return;
}

Se reitera que si se decide normalizar las entradas de entrenamiento a la NN dentro de un rango, se debe considerar si los datos reales de que vamos a disponer en el futuro para pedir a la NN una predicción van a estar dentro de ese rango. Si no es así es erróneo normalizar.

Recordemos que "intervEntrada" e "intervSalida" son variables de tipo cadena definidas como parámetro de entrada (ver inicio del capítulo "Implementación en MQL5") y de la forma "0;1" o "-1;1", por ejemplo; es decir, contienen los máximos y mínimos relativos. La función "StringSplit()" pasa la cadena al array que contendrá los máximos y mínimos relativos. Para cada columna tendremos que...

  1. Identificar el máximo y mínimo absolutos (variables "maxAbs" y "minAbs")
  2. Recorrer toda la columna normalizando los valores entre "maxRel" y "minRel": ver abajo la función "normValor()"
  3. Establecer en "arDatos" el nuevo valor normalizado mediante el método .set de la clase CMatrixDouble.

//------------------------------------ FUNCION DE NORMALIZACIÓN ---------------------------------
double normValor(double valor, double maxAbs, double minAbs, double maxRel= 1, double minRel= -1)
{
   double valorNorm= 0;
   if(maxAbs>minAbs) valorNorm= (valor-minAbs)*(maxRel-minRel))/(maxAbs-minAbs) + minRel;
   return(valorNorm);
} 
3.5 Barajado de los datos de entrada/salida

Con objeto de evitar tendencias relacionadas con la sucesión de los valores dentro de la matriz de datos, puede ser conveniente cambiar aleatoriamente (barajar) el orden de las filas dentro de la matriz. Para ello podemos aplicar la siguiente función "barajaDatosEntra" que, recorriendo el array CMatrixDouble por filas, define para cada fila una nueva fila destino respetando la posición de los datos de cada columna y realizando la reubicación de los datos mediante una "fila burbuja" (variable "filaTmp"). 

//------------------------------------ BARAJA DATOS ENTRADA/SALIDA POR FILAS COMPLETAS -----------------------------------
void barajaDatosEntra(CMatrixDouble &arDatos, int nColum)
{
   int fila= 0, colum= 0, filaDestino= 0, nFilas= arDatos.Size();
   double filaTmp[];
   ArrayResize(filaTmp, nColum);
   MathSrand(GetTickCount());          //reinicia semilla serie aleatoria
   while(fila<nFilas)
   {
      filaDestino= randomEntero(0, nFilas-1);	//obtiene aleatoriamente nueva fila destino
      if(filaDestino!=fila)
      {
         for(colum= 0; colum<nColum; colum++) filaTmp[colum]= arDatos[filaDestino][colum];
         for(colum= 0; colum<nColum; colum++) arDatos[filaDestino].Set(colum, arDatos[fila][colum]);
         for(colum= 0; colum<nColum; colum++) arDatos[fila].Set(colum, filaTmp[colum]);
         fila++;
      }
   }
   return;
}

Una vez reiniciada la semilla de la serie aleatoria con "MathSrand(GetTcikCount())", la función responsable del "destino final" aleatorio de cada fila es "randomEntero()"...

//---------------------------------- RAMDOM ENTERO -----------------------------------------------
int randomEntero(int minRel= 0, int maxRel= 1000)
{
   int num= (int)MathRound(randomDouble((double)minRel, (double)maxRel));
   return(num);
}

3.6. Entrenamiento / optimización de la Red Neuronal
La librería AlgLib permite aplicar algoritmos de ajuste de la red que reducen mucho el tiempo de entrenamiento y optimización en relación al sistema tradicional aplicado al perceptrón multicapa: algoritmo de "propagación hacia atrás" o "back propagation". Usaremos, pues y como dijimos al inicio,

  • el algoritmo de Levenberg-Marquardt con regularización y cálculo Hessian exacto (MLPTrainLM()) o
  • el algoritmo L-BFGS con regularización (MLPTrainLBFGS()).

El segundo lo aplicaremos para atender la optimización de la red en la que el número de pesos sea superior a 500.

//------------------------------------- ENTRENAMIENTO de la RED ----------------------------------------
double entrenaEvalRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatosEntrena, int ciclosEntrena= 2, double tasaAprende= 0.001)
{
   bool salir= false;
   double errorMedio= 0; string mens= "Entrenamiento Red";
   int k= 0, i= 0, codResp= 0,
       historialEntrena= arDatosEntrena.Size();
   CMLPReportShell infoEntren;
   ResetLastError();
   datetime tmpIni= TimeLocal();
   Alert("Iniciando OPTIMIZACIÓN de RED NEURONAL...");
   Alert("Espere unos minutos... según cantidad de historial implicado.");
   Alert("...///...");
   if(propiedadRed(objRed, N_PESOS)<500)
      CAlglib::MLPTrainLM(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, codResp, infoEntren);
   else
      CAlglib::MLPTrainLBFGS(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, 0.01, 0, codResp, infoEntren);
   if(codResp==2 || codResp==6) errorMedio= CAlglib::MLPRMSError(objRed, arDatosEntrena, historialEntrena);
   else Print("Cod entrena Resp: ", codResp);
   datetime tmpFin= TimeLocal();
   Alert("NGrad ", infoEntren.GetNGrad(), " NHess ", infoEntren.GetNHess(), " NCholesky ", infoEntren.GetNCholesky());
   Alert("codResp ", codResp," Error medio Entren "+DoubleToString(errorMedio, 8), " ciclosEntrena ", ciclosEntrena);
   Alert("tmpEntren ", DoubleToString(((double)(tmpFin-tmpIni))/60.0, 2), " min", "---> tmpIni ", TimeToString(tmpIni, _SEG), " tmpFin ", TimeToString(tmpFin, _SEG));
   infoError(GetLastError(), __FUNCTION__);
   return(errorMedio);
}

Como vemos, la función recibe como parámetros de entrada el "objeto de red" y la matriz de datos entrada/salida que, a estas alturas ha sido normalizada y barajada. También definimos los ciclos de entrenamiento o épocas ("ciclosEntrena"; o número de veces que el algoritmo realizará el ajuste buscando el menor "error de entrenamiento" posible); la documentación aconseja 2 y, en las pruebas efectuadas, no se encuentran mejoras al elevar esta cifra. Igual decimos del parámetro "tasa de aprendizaje" ("tasaAprende").

Definimos al inicio de la función el objeto "infoEntren" (de la clase CMLPReportShell) que recogerá información sobre el resultado del entrenamiento y que obtendremos después con los métodos GetNGrad() y GetNCholesky(). El error medio de entrenamiento (error medio cuadrático de todos los datos de salida originales en relación a los datos de salida conseguidos tras el ajuste con el algoritmo) se obtiene de la función "MLPRMSError()". Igualmente, informamos al usuario del tiempo invertido en el proceso de optimización recogiendo la hora inicial y final en las variables tmpIni y tmpFin.

Estas funciones de optimización devuelven un código de calidad de ejecución ("codResp") que puede tomar los valores

  • -2, si la muestra de entrenamiento tiene un número mayor de datos de salida que el de neuronas de la capa de salida.
  • -1, si algún parámetro de entrada a la función es incorrecto.
  • 2, ejecución correcta y tamaño de error menor que el criterio de parada ("MLPTrainLM()").
  • 6, ídem anterior para la función "MLPTrainLBFGS()".

De manera que la ejecución correcta devolverá un 2 o un 6 según el número de pesos de la red optimizada. 

El ajuste que realizan estos algoritmos es tal que la reiteración de ciclos de entrenamiento (variable "ciclosEntrena"; en "back propagation" puede afectar mucho a la precisión conseguida) apenas afecta al error conseguido. Una red de 4 capas con 35, 45, 10 y 2 neuronas y una matriz de entrada de 2000 filas puede ser optimizada con la función descrita arriba en 4-6 minutos (I5, core 4, RAM 8 gb) con un error del orden de 2 a 4 cienmilésimas (4x10^-5).

3.7 Salvar/recuperar la red a/desde fichero de texto

Llegados a este punto del artículo, hemos creado la NN, hemos preparado los datos de entrada/salida y hemos entrenado la NN. La precaución nos pide salvar a disco la red por si ocurre alguna contingencia durante la ejecución del EA. Para ello tendremos que usar las funciones que facilita AlgLib para obtener las características y valores internos de la red (número de capas y neuronas por capa, valor de los pesos, etc.) y escribir esos datos en un fichero de texto volcado a disco...

//-------------------------------- SALVAR RED A DISCO -------------------------------------------------
bool salvaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool redSalvada= false;
   int k= 0, i= 0, j= 0, numCapas= 0, arNeurCapa[], neurCapa1= 1, funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   FileDelete(nombArch, FILE_COMMON);
   ResetLastError();
   puntFichRed= FileOpen(nombArch, FILE_WRITE|FILE_BIN|FILE_COMMON);
   redSalvada= puntFichRed!=INVALID_HANDLE;
   if(redSalvada)
   {
      numCapas= CAlglib::MLPGetLayersCount(objRed);   
      redSalvada= redSalvada && FileWriteDouble(puntFichRed, numCapas)>0;
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         arNeurCapa[k]= CAlglib::MLPGetLayerSize(objRed, k);
         redSalvada= redSalvada && FileWriteDouble(puntFichRed, arNeurCapa[k])>0;
      }
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         for(i= 0; redSalvada && i<arNeurCapa[k]; i++)
         {
            if(k==0)
            {
               CAlglib::MLPGetInputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            else if(k==numCapas-1)
            {
               CAlglib::MLPGetOutputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            CAlglib::MLPGetNeuronInfo(objRed, k, i, funcTipo, umbral);
            FileWriteDouble(puntFichRed, funcTipo);
            FileWriteDouble(puntFichRed, umbral);
            for(j= 0; redSalvada && k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
            {
               peso= CAlglib::MLPGetWeight(objRed, k, i, k+1, j);
               redSalvada= redSalvada && FileWriteDouble(puntFichRed, peso)>0;
            }
         }      
      }
      FileClose(puntFichRed);
   }
   if(!redSalvada) infoError(_LastError, __FUNCTION__);
   return(redSalvada);
} 

Como se ve en la sexta línea de código, al fichero le damos la extensión ".red", lo que nos facilitará búsquedas y comprobaciones posteriores. Es una función que ha costado horas de depuración pero... ¡funciona!.

Igualmente, si tras el evento que nos detuvo la ejecución del EA hay que continuar el trabajo, recuperaremos la red del fichero de disco con una función inversa a la anterior, que creará el objeto de red y lo alimentará con los datos leídos desde el fichero de texto en el que guardamos la NN.

//-------------------------------- RECUPERA RED DE DISCO -------------------------------------------------
bool recuperaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool exito= false;
   int k= 0, i= 0, j= 0, nEntradas= 0, nSalidas= 0, nPesos= 0,
       numCapas= 0, arNeurCapa[], funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   puntFichRed= FileOpen(nombArch, FILE_READ|FILE_BIN|FILE_COMMON);
   exito= puntFichRed!=INVALID_HANDLE;
   if(exito)
   {
      numCapas= (int)FileReadDouble(puntFichRed);
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; k<numCapas; k++) arNeurCapa[k]= (int)FileReadDouble(puntFichRed); 
      if(numCapas==2) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);
      else if(numCapas==3) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);
      else if(numCapas==4) CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);
      exito= existeRed(arObjRed[0]);
      if(!exito) Print("Error creación RED NEURONAL ==> ", __FUNCTION__, " ", _LastError);
      else
      {
         CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
         Print("Recuperada Red nº capas ", propiedadRed(objRed, N_CAPAS));
         Print("Nº neuronas entrada ", nEntradas);
         Print("Nº neuronas capaOc 1 ", nNeuronCapa1);
         Print("Nº neuronas capaOc 2 ", nNeuronCapa2);
         Print("Nº neuronas salida ", nSalidas);
         Print("Nº pesos ", nPesos);
         for(k= 0; k<numCapas; k++)
         {
            for(i= 0; i<arNeurCapa[k]; i++)
            {
               if(k==0)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetInputScaling(objRed, i, media, sigma);
               }
               else if(k==numCapas-1)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetOutputScaling(objRed, i, media, sigma);
               }
               funcTipo= (int)FileReadDouble(puntFichRed);
               umbral= FileReadDouble(puntFichRed);
               CAlglib::MLPSetNeuronInfo(objRed, k, i, funcTipo, umbral);
               for(j= 0; k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
               {
                  peso= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetWeight(objRed, k, i, k+1, j, peso);
               }
            }      
         }
      }
   }
   FileClose(puntFichRed);
   return(exito);
} 

Para obtener la predicción de la red al cargarle los datos del momento actual llamamos a la función "respuestaRed()"...

//--------------------------------------- SOLICITA RESPUESTA A RED ---------------------------------
double respuestaRed(CMultilayerPerceptronShell &ObjRed, double &arEntradas[], double &arSalidas[], bool desnorm= false)
{
   double resp= 0, nNeuron= 0;
   CAlglib::MLPProcess(ObjRed, arEntradas, arSalidas);   
   if(desnorm)             //Si hay que invertir normalización de datos de salida
   {
      nNeuron= ArraySize(arSalidas);
      for(int k= 0; k<nNeuron; k++)
         arSalidas[k]= desNormValor(arSalidas[k], maxAbsSalida, minAbsSalida, arMaxMinRelSals[1], arMaxMinRelSals[0]);
   }
   resp= arSalidas[0];
   return(resp);
}

En esta función se prevé la posibilidad de que hubiera que invertir la normalización efectuada a los datos de salida en la matriz de entrenamiento.

 

4. Autooptimización

Una vez conseguido que el EA optimize la red neuronal (y por tanto, indirectamente, los valores de entrada aplicados al EA) como parte de su rutina de ejecución y no como parte de las labores de optimización del usuario con el probador de estrategias...

          1. ¿Cómo reiteramos el algoritmo base descrito en el apartado "A"?.

Durante el proceso de optimización de la NN, que supone un gran consumo computacional,...

          2. ¿Cómo evitamos detener el control sobre el mercado que debe hacer continuamente el EA?

Definimos un tipo de enumeración, "mis_PLAZO_OPTIM", que describe los intervalos temporales que el usuario podrá elegir para reiterar el algoritmo base descrito: decidimos hacerlo diariamente, en días alternos o en fin de semana. Definimos otra enumeración para que el usuario decida si el EA va a actuar como "optimizador" de la red o como "ejecutor" de la estrategia.

enum mis_PLAZO_OPTIM {_DIARIO, _DIA_ALTERNO, _FIN_SEMANA};
enum mis_TIPO_EAred {_OPTIMIZA, _EJECUTA};

En este punto, recordemos que MT5 permite la ejecución simultánea de un EA por cada gráfico abierto... Por tanto, en un primer gráfico cargaremos el EA en modo "_EJECUTA" y en un segundo gráfico lo cargaremos en el modo "_OPTIMIZA". Mientras en el primer gráfico sólo se atenderá a la gestión de la estrategia, en el segundo gráfico el EA sólo atenderá a la optimización de la Red Neuronal. De este modo resolvemos el problema 2 anterior. En el primer gráfico el EA "usa" la Red Neuronal "leyéndola" desde un fichero de texto que el EA genera en modo "optimizador" cada vez que optimiza la NN.

En las pruebas de optimización, que antes hemos dicho que implicaban entre 4 y 6 minutos de computación, este método explicado ahora eleva el proceso de optimización entre 8 y 15 minutos, según actuemos en la hora asiática o europea de los mercados, pero el control sobre la estrategia no se detiene.

Para atender a lo dicho en el párrafo anterior definimos los parámetros de entrada expresados en recuadro siguiente... 

input mis_TIPO_EAred tipoEAred            = _OPTIMIZA;        //Tipo de tarea a ejecutar
input mis_PLAZO_OPTIM plazoOptim          = _DIARIO;          //Intervalo para optimizar red
input int horaOptim                       = 3;                //Hora local optimizar red

El parámetro "horaOptim" guarda la hora local en que el usuario decide acometer la optimización. Debería ser una hora de baja o nula actividad de los mercados; en Europa, por ejemplo, de madrugada (03:00 h como valor por defecto) o en fin de semana. Si, en todo caso, deseamos ejecutar la optimización siempre que se inicie una nueva ejecución del EA, sin esperar a la hora y días previstos, definimos también...

input bool optimInicio                    = true;         //Optimizar red neuronal al inicio

Y para controlar si la red se considera optimizada (modo "optimizador") y la hora en que se efectuó última lectura del fichero de red (modo "ejecutor") definimos las variables públicas...

double fechaUltLectura;
bool reOptimizada= false;

Para resolver el "problema 1", el núcleo gestor del método expuesto se codifica en OnTimer(), función que se ejecutará según la cadencia "tmp" establecida con EventSetTimer(tmp) en OnInit(), - al menos cada hora -. De manera que cada tmp segundos el EA "optimizador" comprobará si debe reoptimizar la red, y el EA "ejecutor" comprobará si debe leer el nuevo fichero de red porque lo hubiera actualizado el EA "optimizador".

/---------------------------------- ON TIMER --------------------------------------
void OnTimer()
{
   bool existe= false;
   string fichRed= "";
   if(tipoEAred==_OPTIMIZA)            //el EA actúa en modo "optimizador"
   {
      bool optimizar= false;
      int codS= 0,
          hora= infoFechaHora(TimeLocal(), _HORA);    //obtenemos la hora entera actual
      if(!redOptimizada) optimizar= horaOptim==hora && permReoptimDia();
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //define nombre del fichero de red neuronal
      existe= buscaFich(fichRed, "*.red");            //busca en disco el fichero donde se guardó la red neuronal
      if(!existe || optimizar)
         redOptimizada= gestionRed(objRed, simb, intervEntrada!="", intervSalida!="", imprDatosEntrena, barajaDatos);
      if(hora>(horaOptim+6)) redOptimizada= false;    //pasadas 6 horas desde la hora prevista, considera obsoleta la red optimizada actual
      guardaVarGlobal(redOptimizada);                 //guarda en disco el valor de "reoptimizada"
   }
   else if(tipoEAred==_EJECUTA)        //el EA actúa en modo "ejecutor"
   {
      datetime fechaUltOpt= 0;
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //define nombre del fichero de red neuronal
      existe= buscaFich(fichRed, "*.red");            //busca en disco el fichero donde se guardó la red neuronal
      if(existe)
      {
         fechaUltOpt= fechaModifFich(0, fichRed);     //identifica la fecha de la última optimización (modificación del fichero de red)
         if(fechaUltOpt>fechaUltLectura)              //si fecha de optimización es posterior a la última lectura
         {
            recuperaRedFich(objRed, fichRed);         //lee y genera la nueva red neuronal
            fechaUltLectura= (double)TimeCurrent();
            guardaVarGlobal(fechaUltLectura);         //guarda en disco la nueva fecha de lectura
            Print("Red recuperada tras optimización... "+simb);      //informa
         }
      }
      else Alert("tipoEAred==_EJECUTA --> No existe fichero RED NEURONAL: "+fichRed+".red");
   }
   return;
}

Las funciones auxiliares de lo expuesto, que no se comentan, son...

//--------------------------------- PERMISO REOPTIMIZAR ---------------------------------
bool permReoptimDia()
{
   int diaSemana= infoFechaHora(TimeLocal(), _DSEM);
   bool permiso= (plazoOptim==_DIARIO && diaSemana!=6 && diaSemana!=0) ||     //optimiza [martes a sábado, todos los días]
                 (plazoOptim==_DIA_ALTERNO && diaSemana%2==1) ||              //optimiza [martes, jueves y sábado]
                 (plazoOptim==_FIN_SEMANA && diaSemana==5);                   //optimiza [sábado]
   return(permiso);
}

//-------------------------------------- BUSCA ARCHIVO --------------------------------------------
bool buscaFich(string fichBusca, string filtro= "*.*", int carpeta= FILE_COMMON)
{
   bool existe= false;
   string fichActual= "";
   long puntBusca= FileFindFirst(filtro, fichActual, carpeta);
   if(puntBusca!=INVALID_HANDLE)
   {
      ResetLastError();
      while(!existe)
      {
         FileFindNext(puntBusca, fichActual);
         existe= fichActual==fichBusca;
      }
      FileFindClose(puntBusca);
   }
   else Print("Fichero no encontrado!");
   infoError(_LastError, __FUNCTION__);
   return(existe);

El algoritmo descrito está siendo actualmente utilizado en el EA que tenemos en pruebas. Ello nos permite el control total de la estrategia mientras, cada noche, a partir de la 3:00 h AM local se reoptimiza la red neuronal con los datos de la vela H1 de los 3 meses anteriores: 35 neuronas en la capa de entrada, 45 en la 1ª capa oculta, 8 en la 2ª capa oculta y 2 en la capa de salida; 35-45 minutos de tiempo de optimización.

5. Problema 1: conversor binario-decimal

Para comprobar el sistema comentado, vamos a resolver un problema del cual podemos conocer previamente la solución exacta (existe un algoritmo que la facilita) y la vamos a comparar con la que facilita la red neuronal. Nos planteamos un conversor binario a decimal. Para la prueba proponemos el siguiente script...

#property script_show_confirm
#property script_show_inputs

#define FUNC_CAPA_OCULTA   1  
#define FUNC_SALIDA        -5
            //1= func tangente hiperbólica; 2= e^(-x^2); 3= x>=0 raizC(1+x^2) x<0 e^x; 4= func sigmoide;
            //5= binomial x>0.5? 1: 0; -5= func lineal
#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  parametros entrada  ---------------------
sinput int nNeuronEntra= 10;                 //Núm neuronas capa entrada
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 0;                  //Núm neuronas capa oculta 1 (<1 no existe)
sinput int nNeuronCapa2= 0;                  //Núm neuronas capa oculta 2 (<1 no existe)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Núm neuronas capa salida

sinput int    historialEntrena= 800;         //Historial entrenamiento
sinput int    historialEvalua= 200;          //Historial evaluación
sinput int    ciclosEntrena= 2;              //Ciclos de entrenamiento
sinput double tasaAprende= 0.001;            //Tasa de aprendizaje
sinput string intervEntrada= "";             //Normalización entrada: mín y máx deseados (vacío= NO normaliza)
sinput string intervSalida= "";              //Normalización salida: mín y máx deseados (vacío= NO normaliza)
sinput bool   imprEntrena= true;             //Imprimir datos entrena/evalúa
      
// ------------------------------ VARIABLES GLOBALES -----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
CMatrixDouble arDatosEval(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptBinDec";

//+------------------------------------------------------------------+
void OnStart()              //Conversor binario a decimal
{
   string mensIni= "Script conversor BINARIO-DECIMAL",
          mens= "", cadNumBin= "", cadNumRed= "";
   int contAciertos= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], salida= 0, umbral= 0, peso= 0;
   double errorMedioEntren= 0;
   bool normEntrada= intervEntrada!="", normSalida= intervSalida!="", correcto= false,
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numBinEntra;numDecSalidaRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         ArrayCopy(arNumEntra, arNumBin);
         salida= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         salida= MathRound(salida);
         correcto= k==(int)salida;
         escrTexto(cadNumBin+";"+IntegerToString((int)salida)+";"+correcto, puntFichTexto);
         cadNumRed= "";
      }
   }      
   deIniFichImprime(puntFichTexto);
   return;
}

Creamos la NN y la entrenamos con los primeros 800 número naturales en binario (10 dígitos, 10 neuronas de entrada, una de salida). Luego convertimos los siguientes 200 números naturales a binario (del 801 al 1000 en binario) y comparamos el resultado real con el predicho por la NN. Por ejemplo, si a la red le facilitamos la cadena 1100110100 (820 en binario; 10 dígitos, 10 neuronas de entrada), la red debería obtener 820 en la salida, o un número cercano. El bucle For descrito arriba es el que se encarga de obtener la predicción de la red para esos 200 números y comparar la respuesta esperada con la predicha.

Ejecutado el script con los parámetros definidos (NN sin capas ocultas, 10 neuronas de entrada y 1 de salida), la respuesta es "perfecta". El fichero "ScriptBinDec-infRN.csv" generado en la carpeta "Terminal\Common\Files" nos da la siguiente información...

Predicción de NN a partir del número 801 

En la imagen vemos que el script ha impreso la matriz de entrenamiento hasta el número 800 en binario (entrada) y decimal (salida). Se ha entrenado la NN y hemos impreso la respuesta de la NN a partir del 801. La 3ª columna "correcto" es el resultado de la comparación entre lo esperado y lo obtenido. Como hemos dicho, el resultado es perfecto.

Sin embargo, si definimos la estructura de NN como "10 neuronas de entrada, 20 neuronas 1ª capa oculta, 8 neuronas 2ª capa oculta, 1 neurona salida", se obtiene el siguiente resultado...

Predicción NN con segunda configuración 

que es un resultado desastroso... y es que con esto tocamos el gran problema del gestor de una NN: ¿qué configuración interna, -número de capas y número de neuronas y funciones de activación-, es la más conveniente?. A este problema, que sepamos, sólo se responde con la experiencia, mil pruebas por parte del usuario y la lectura de artículos como "Evaluación y selección de variables en modelos de aprendizaje de máquinas". Nosotros, además, hemos usado los datos de la matriz de entrenamiento en el programa de análisis estadístico Rapid Miner para intentar encontrar la estructura más eficaz antes de implementarla en mql5.

 

6. Problema 2: detector de números primos

Veamos ahora el mismo planteamiento pero, esta vez, la NN detectará si un número es primo o no. La matriz de entrenamiento contendrá 10 columnas con los 10 dígitos de cada número natural en binario hasta el 800 y una columna que informará sobre si ese número es primo ("1") o no ("0")... es decir, serán 800 filas x 11 columnas. Luego le pediremos a la NN que analice los siguientes 200 números naturales (del 801 al 1000) en binario y responda cuál es primo y cuál no. Al final, previendo este problema más "difícil", imprimimos una estadística de los aciertos conseguidos...

#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  parámetros entrada  ---------------------
sinput int nNeuronEntra= 10;                 //Núm neuronas capa entrada
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 20;                 //Núm neuronas capa oculta 1 (<1 no existe)
sinput int nNeuronCapa2= 0;                  //Núm neuronas capa oculta 2 (<1 no existe)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Núm neuronas capa salida

sinput int    historialEntrena= 800;         //Historial entrenamiento
sinput int    historialEvalua= 200;          //Historial predicción
sinput int    ciclosEntrena= 2;              //Ciclos de entrenamiento
sinput double tasaAprende= 0.001;            //Tasa de aprendizaje
sinput string intervEntrada= "";             //Normalización entrada: mín y máx deseados (vacío= NO normaliza)
sinput string intervSalida= "";              //Normalización salida: mín y máx deseados (vacío= NO normaliza)
sinput bool   imprEntrena= true;             //Imprimir datos entrena/evalúa
      
// ------------------------------ VARIABLES GLOBALES -----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptNumPrimo";

//+----------------------- Detector números primos -------------------------------------------+
void OnStart()
{
   string mensIni= "Script comprobación NÚMEROS PRIMOS", cadNumBin= "", linea= "";
   int contAciertos= 0, totalPrimos= 0, aciertoPrimo= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], numPrimoRed= 0;
   double errorMedioEntren= 0;
   bool correcto= false,
        esNumPrimo= false, 
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numDec;numBin;numPrimo;numPrimoRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         esNumPrimo= esPrimo(k);
         ArrayCopy(arNumEntra, arNumBin);
         numPrimoRed= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         numPrimoRed= MathRound(numPrimoRed);
         correcto= esNumPrimo==(int)numPrimoRed;
         if(esNumPrimo)
         {
            totalPrimos++;
            if(correcto) aciertoPrimo++;  
         }
         if(correcto) contAciertos++;
         linea= IntegerToString(k)+";"+cadNumBin+";"+esNumPrimo+";"+(numPrimoRed==0? "false": "true")+";"+correcto;
         escrTexto(linea, puntFichTexto);
      }
   }     
   escrTexto("porc Aciertos / total;"+DoubleToString((double)contAciertos/(double)historialEvalua*100, 2)+" %", puntFichTexto); 
   escrTexto("Aciertos primos;"+IntegerToString(aciertoPrimo)+";"+"total primos;"+IntegerToString(totalPrimos), puntFichTexto); 
   escrTexto("porc Aciertos / total primos;"+DoubleToString((double)aciertoPrimo/(double)totalPrimos*100, 2)+" %", puntFichTexto); 
   deIniFichImprime(puntFichTexto);
   return;
}

Ejecutado el script con los parámetros definidos (NN sin capas ocultas, 10 neuronas de entrada, 20 en la 1ª capa oculta y 1 en la capa de salida), la respuesta no es tan "perfecta" como en el problema anterior. El fichero "ScriptNumPrimo-infRN.csv" generado en la carpeta "Trminal\Common\Files" nos da la siguiente información...

 

Donde vemos que el primer número primo después del 800 (el 809) no ha sido detectado por la red (correcto = false). El resumen estadístico...

 

Nos informa de que la NN ha acertado el 78% de las 200 veces al clasificar un número como primo o no en el intervalo (801 al 200) de evaluación. Pero de los 29 números primos que existen en ese intervalo, ha detectado 13 (el 44,83 %).

Si la prueba la hacemos con la siguiente estructura de red: "10 neuronas en la capa entrada, 35 neuronas en la 1ª capa oculta, 10 neuronas en la 2ª capa oculta, 1 neurona en la capa salida", el script nos facilita la siguiente información conforme se ejecuta...

 

Como vemos abajo, en 0.53 minutos y con un error medio de entrenamiento de 0.04208383, los resultados son peores...

 

Es decir, volvemos a la cuestión anterior sobre el cómo definir la estructura interna de red más conveniente.

 

Conclusión

Buscando un sistema autooptimizable, hemos implementado el código de optimización de una Red Neuronal de la librería ALGLIB en un programa MQL5 y hemos propuesto una solución para el problema que supone el gran consumo computacional que implica el ajuste de la Red Neuronal y que impide al EA gestionar la estrategia comercial mientras está introducido en los bucles reiterativos que derivan en el ajuste de la Red.

Después hemos utilizado parte del código propuesto para resolver desde un programa MQL5 dos problemas: la conversión de binario a decimal y la detección de números primos y observar la diferencia de resultados según la estructura interna dada a la NN.

¿Que si lo expuesto sirve para implementar una estrategia comercial rentable?... Estamos en ello. Nos hemos limitado, por ahora, a aportar nuestro granito de arena.